In our previous post, we constrained the order fields could be set, and prior to that we’d hinted that our builder doesn’t have to exactly conform to the shape of the class we’re building.
Guest post by Morgen Peschke
In this post, we’ll take those two ideas and use them to get rid of some ugly boilerplate that, up to this point, had to exist above every usage of AppBuilder
:
private val auth = Auth.impl[IO]
private val users = Users.impl(auth)
private val books = Books.impl(auth, users)
We Need Someplace To Put This
First step is to add a field for Auth
to AppBuilder
, so we can access it later (copy
, unrelated methods, and adding the new type parameter to the various implicit classes elided for brevity):
final class AppBuilder[
F[_],
H <: Option[String],
P <: Option[Int],
MC <: Option[Int],
CT <: Option[Int],
A <: Option[Auth[F]],
U <: Option[Users[F]],
B <: Option[Books[F]],
HC <: Option[NonEmptyChain[HealthCheck[F]]]
] private[builder](
private[builder] val hostOpt: H,
private[builder] val portOpt: P,
private[builder] val maxConnectionsOpt: MC,
private[builder] val connectionTimeoutOpt: CT,
private[builder] val authOpt: A,
private[builder] val usersOpt: U,
private[builder] val booksOpt: B,
private[builder] val healthChecksOpt: HC
) {
private[builder] def withAuth(value: Auth[F]): AppBuilder[F, H, P, MC, CT, Some[Auth[F]], U, B, HC] =
copy(authOpt = Some(value))
}
We’ll also need an implicit class to set authOpt
:
implicit final class AppBuilderAuthOps[
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, None.type, U, B, HC]) extends AnyVal {
def auth(a: Auth[F]): AppBuilder[F, H, P, MC, CT, Some[Auth[F]], U, B, HC] = builder.withAuth(a)
}
So far, nothing we haven’t done before.
Lastly, we need to adjust WhenReady
to account for the new type parameter. We won’t need a new constraint because we can build AppConfig
with or without an Auth
, so we’ll put that as a parameter on WhenReady#build
instead of on the class:
sealed trait WhenReady[
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]]]
] {
def build[A <: Option[Auth[F]]](builder: AppBuilder[F, H, P, MC, CT, A, U, B, HC]): AppConfig[F]
}
object WhenReady {
implicit def allFieldsPresent[F[_]]: WhenReady[
F,
Some[String],
Some[Int],
Some[Int],
Some[Int],
Some[Users[F]],
Some[Books[F]],
Some[NonEmptyChain[HealthCheck[F]]]
] = new WhenReady[
F,
Some[String],
Some[Int],
Some[Int],
Some[Int],
Some[Users[F]],
Some[Books[F]],
Some[NonEmptyChain[HealthCheck[F]]]
] {
override def build[A <: Option[Auth[F]]](builder: AppBuilder[F, Some[String], Some[Int], Some[Int], Some[Int], A, Some[Users[F]], Some[Books[F]], Some[NonEmptyChain[HealthCheck[F]]]]): AppConfig[F] =
AppConfig[F](
host = builder.hostOpt.value,
port = builder.portOpt.value,
maxConnections = builder.maxConnectionsOpt.value,
connectionTimeout = builder.connectionTimeoutOpt.value,
users = builder.usersOpt.value,
books = builder.booksOpt.value,
healthChecks = builder.healthChecksOpt.value.toNonEmptyList,
)
}
}
Shiny New Methods
Now that we have an Auth
inside AppBuilder
, lets add some helpers to take advantage of them. We’ll need new implicit classes, because the constraints will be different from the existing users
and books
methods:
implicit final class AppBuilderUsersFromAuthOps[
F[_],
H <: Option[String],
P <: Option[Int],
MC <: Option[Int],
CT <: Option[Int],
A <: Option[Auth[F]],
B <: Option[Books[F]],
HC <: Option[NonEmptyChain[HealthCheck[F]]]
](private val builder: AppBuilder[F, H, P, MC, CT, Some[Auth[F]], None.type, B, HC]) extends AnyVal {
def usersFromAuth(f: Auth[F] => Users[F]): AppBuilder[F, H, P, MC, CT, Some[Auth[F]], Some[Users[F]], B, Some[NonEmptyChain[HealthCheck[F]]]] = {
val u = f(builder.authOpt.value)
builder
.withUsers(u)
.withHealthChecks(NonEmptyChain.one {
HealthCheck.users(builder.authOpt.value, u)
})
}
}
implicit final class AppBuilderBooksFromAuthOps[
F[_],
H <: Option[String],
P <: Option[Int],
MC <: Option[Int],
CT <: Option[Int],
HC <: Option[NonEmptyChain[HealthCheck[F]]]
](private val builder: AppBuilder[F, H, P, MC, CT, Some[Auth[F]], Some[Users[F]], None.type, HC]) extends AnyVal {
def booksFromAuth(f: (Auth[F], Users[F]) => Books[F]): AppBuilder[F, H, P, MC, CT, Some[Auth[F]], Some[Users[F]], Some[Books[F]], Some[NonEmptyChain[HealthCheck[F]]]] = {
val b = f(builder.authOpt.value, builder.usersOpt.value)
builder
.withBooks(b)
.withHealthChecks(NonEmptyChain.one {
HealthCheck.books(builder.authOpt.value, b)
})
}
}
These handle both building the algebra from an Auth
, and making sure that the HealthCheck
for each algebra gets added.
We also use the principles we previously used to constrain order to make sure we can only call booksFromAuth
when we have both an Auth
and a Users
.
The two style of methods can co-exist, so we can do a partial or a full conversion:
import cats.data.NonEmptyList
import cats.effect.IO
import builder.AppBuilder
import models._
object Partial {
private val auth = Auth.impl[IO]
private val users = Users.impl(auth)
AppBuilder
.init[IO]
.host("Test")
.port(8080)
.maxConnections(5)
.connectionTimeoutSeconds(50)
.auth(auth)
.users(users)
.booksFromAuth(Books.impl)
.healthChecks(NonEmptyList.of(
HealthCheck.users(auth, users),
HealthCheck.impl
))
.build
}
object Full {
AppBuilder
.init[IO]
.host("Test")
.port(8080)
.maxConnections(5)
.connectionTimeoutSeconds(50)
.auth(Auth.impl)
.usersFromAuth(Users.impl)
.booksFromAuth(Books.impl)
.healthCheck(HealthCheck.impl)
.build
}
That concludes the “how” portion of this series, come back next time for a discussion of when it makes sense to use something this complicated.