Type Gymnastics with Builders - Part 4 - Flexibility

Views
Article hero image

Up to this point, our focus in this series has been mostly internal. The last post has improved our errors enough that we can start making improvements to our API.

Guest post by Morgen Peschke

The basic builder exposes requires fields be set in an all-or-nothing way, which makes using our builder a bit awkward for healthChecks - not only does it require supplying them all at once, the user maybe shouldn’t have to care that AppConfig stores them in a NonEmptyList.

Relaxing our constraints a bit

We can fix this with fairly small adjustments to AppBuilder and AppBuilderHealthChecksOps.

First, we’ll modify AppBuilder#withHealthChecks to append, instead of replace:

  private[builder] def withHealthChecks(value: NonEmptyChain[HealthCheck[F]]): AppBuilder[F, H, P, MC, CT, U, B, Some[NonEmptyChain[HealthCheck[F]]]] =
    copy(healthChecksOpt = Some(healthChecksOpt.fold(value)(_.concat(value))))

Next we’ll update AppBuilderHealthChecksOps so that in can be called regardless of if HC is a Some or a None and add a method which accepts a single HealthCheck:

implicit final class AppBuilderHealthChecksOps[
  F[_],
  H <: Option[String],
  P <: Option[Int],
  MC <: Option[Int],
  CT <: Option[Int],
  U <: Option[Users[F]],
  B <: Option[Books[F]],
  HC <: Option[NonEmptyChain[HealthCheck[F]]]
](private val builder: AppBuilder[F, H, P, MC, CT, U, B, HC]) extends AnyVal {
  def healthCheck(hc: HealthCheck[F]): AppBuilder[F, H, P, MC, CT, U, B, Some[NonEmptyChain[HealthCheck[F]]]] =
    builder.withHealthChecks(NonEmptyChain.one(hc))

  def healthChecks(hc: NonEmptyList[HealthCheck[F]]): AppBuilder[F, H, P, MC, CT, U, B, Some[NonEmptyChain[HealthCheck[F]]]] =
    builder.withHealthChecks(NonEmptyChain.fromNonEmptyList(hc))
}

It’s important to note that, while this accepts any HC, it only emits a Some[NonEmptyChain[HealthCheck[F]]], so the builder still enforces that it must be called at least once.

Sometimes Splitting The Party Is Good?

This allows us the flexibility to set multiple HealthChecks at once, while also allowing us to set single health checks, when that makes more sense.

Here we’ve decided to keep the health checks for our algebras close to where we specify them:

import builder.AppBuilder
import cats.data.NonEmptyList
import cats.effect.IO
import models._

object Example {
  private val auth = Auth.impl[IO]
  private val users = Users.impl(auth)
  private val books = Books.impl(auth, users)
  AppBuilder
    .init[IO]
    .host("Test")
    .port(8080)
    .maxConnections(5)
    .connectionTimeoutSeconds(50)
    .healthChecks(NonEmptyList.of(
      HealthCheck.impl,
      HealthCheck.impl
    ))
    .users(users)
    .healthCheck(HealthCheck.users(auth, users))
    .books(books)
    .healthCheck(HealthCheck.books(auth, books))
    .build
}

While a builder will generally have a shape very similar to the class it builds, this does not have to be the case - and this is going to come in handy later in this series.

Maybe we’ve gone a bit too far towards flexibility, next time we’ll look at how we can add some additional order to our API.

scala builder types series