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)