In some projects there can be several configurations for different “environments”, say for “Production” and “Development”. For example:
@readers
case class Config(
threadsNb: Int,
defaultTimeout: FiniteDuration,
paymentsUri: Uri,
ordersUri: Uri)
val productionConfig: Config =
Config(
threadsNb = 8,
defaultTimeout = 3.seconds,
paymentsUri = Uri("http://acme.org/payments"),
ordersUri = Uri("http://acme.org/orders"))
val developmentConfig: Config =
Config(
threadsNb = 8,
defaultTimeout = 3.seconds,
paymentsUri = Uri("http://acme-test.org/payments"),
ordersUri = Uri("http://acme-test.org/orders"))
Since Config
is just a case class which can have lots of fields it can be very tempting to derive the development config from the production config:
val productionConfig: Config =
Config(
threadsNb = 8,
defaultTimeout = 3.seconds,
paymentsUri = Uri("http://acme.org/payments"),
ordersUri = Uri("http://acme.org/orders"))
val developmentConfig: Config =
productionConfig.copy(
paymentsUri = Uri("http://acme-test.org/payments"),
ordersUri = Uri("http://acme-test.org/orders")
)
The good thing here is that we are removing some duplication in our declarations because developmentConfig
sort of inherits from productionConfig
. This is slightly dangerous though because if you forget to override one of the production fields, like paymentsUri
you might accidentally run some code against production while executing your tests!
The opposite situation, where you define productionConfig
in terms of developmentConfig
is not enviable either because you might run your production application against tests services and some data might be simply lost.
It is possible to make the configuration typesafe with one simple type parameter:
trait Prod
object Prod extends Prod
trait Dev
object Dev extends Dev
type ->[A, B] = (A, B)
@readers
case class Config[C](
threadsNb: Int,
defaultTimeout: FiniteDuration,
paymentsUri: Uri -> C,
ordersUri: Uri -> C)
val prodPaymentsUri: Uri -> Prod =
Uri("http://acme.org/payments") -> Prod
val prodOrdersUri: Uri -> Prod =
Uri("http://acme-test.org/orders") -> Prod
val devPaymentsUri: Uri -> Dev =
Uri("http://acme-test.org/payments") -> Dev
val devOrdersUri: Uri -> Dev =
Uri("http://acme-test.org/orders") -> Dev
val productionConfig: Config[Prod] =
Config(
threadsNb = 8,
defaultTimeout = 3.seconds,
paymentsUri = prodPaymentsUri,
ordersUri = prodOrdersUri)
val developmentConfig: Config[Dev] =
productionConfig.copy(
paymentsUri = devPaymentsUri,
ordersUri = devOrdersUri
)
// Now it is a lot harder to mix-up configurations
// The following 2 tentatives of mixing-up production and development
// configurations do not compile
/*
val accidentalProdOverride: Config[Prod] =
productionConfig.copy(paymentsUri = devPaymentsUri)
val accidentalDevOverride: Config[Dev] =
developmentConfig.copy(paymentsUri = prodPaymentsUri)
*/
// But this compiles ok
val sandboxPaymentsUri: Uri -> Dev =
Uri("http://acme-sandbox.org/payments") -> Dev
val okDevOverride: Config[Dev] =
developmentConfig.copy(paymentsUri = sandboxPaymentsUri)
What about the @readers
annotation? What does it generate for fields annotated with Prod
or Dev
?
The readers
annotation is smart enough to recognize those fields and create a Reader
instance stripping out the environment annotation, for example:
implicit def paymentsUriReader[A1]: Reader[Config[A1], Uri] =
Reader(_.paymentsUri._1)