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 HealthCheck
s 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.