Ciris v1.0.0
Ciris v1.0.0 was announced less than a week ago, on October 28th, 2019. The release features a complete rewrite of the library, now reimagined and based upon Cats and Cats Effect. In this post, we'll highlight the most important changes in v1.0.0, and compare it with the v0.x versions.
Refer to the documentation for more detailed information on v1.0.0.
flatMap
, parMapN
, ...?
Why can't I just Ciris v0.x had custom functions loadConfig
(roughly equivalent to parMapN
) and withValues
(roughly equivalent to parTupled.flatMap
) for loading configurations. These were a consequence of having Scala Native support, a platform where Cats is still unavailable. Ciris v1.0.0 is not available on Scala Native, and depends on Cats while providing relevant type class instances.
As a result, code which used to look something like this in v0.x:
withValues(envF[IO, AppEnvironment]("APP_ENV")) { environment =>
loadConfig(
apiConfig(environment),
databaseConfig
) { (api, database) =>
Config(
appName = "my-api",
environment = environment,
api = api,
database = database
)
}
}
can now instead be written as follows in v1.0.0.
env("APP_ENV").as[AppEnvironment].flatMap {
(
apiConfig(environment),
databaseConfig
).parMapN { (api, database) =>
Config(
appName = "my-api",
environment = environment,
api = api,
database = database
)
}
}
env
/envF
, prop
/propF
, ...?
Why Ciris v0.x distinguished between pure and effectful configuration values. In Ciris v1.0.0, we require an effect type compatible with Cats Effect to load configurations. Loading is described using Cats Effect type classes, meaning you don't have to keep repeating the effect type for every envF
, propF
, and so on.
Additionally, there is now only one version per source: env
, prop
, and file
. These no longer accept type arguments, and decoding of configuration values is done through as
. Code which used to look something along the following lines in Ciris v0.x:
loadConfig(
envF[IO, AppEnvironment]("APP_ENV"),
envF[IO, Int]("MAX_RETRIES")
)(Config).orRaiseThrowable
can now instead be written as follows in v1.0.0.
(
env("APP_ENV").as[AppEnvironment],
env("MAX_RETRIES").as[Int]
).parMapN(Config).load[IO]
This allows configuration sources to focus on loading values, and not how to decode to different types.
Unifying and simplifying concepts
In Ciris v0.x, there were three similar concepts: ConfigEntry
, ConfigValue
, and ConfigResult
. In Ciris v1.0.0, there is now only a single concept, ConfigValue
, for describing configurations. This concept can be used to describe both horizontal composition (fallback values) and vertical composition (sequentially or parallelly).
Similarly to orElse
, orNone
, and orValue
in v0.x, or
can be used in v1.0.0 to use fallback values. Default values are now a first-class construct, meaning they can be reasoned about. In v0.x, we could not distinguish between default values and loaded values.
env("MAX_RETRIES").or(prop("max.retries")).as[Int].option
In the above example, .option
is equivalent to .map(_.some).default(None)
, and .default(None)
can also be written as .or(default(None))
. Later defaults override earlier defaults, and this extends to defaults defined in arguments to or
, and to composed configurations when using parallel composition (e.g. parMapN
).
The ConfigError
and ConfigErrors
concepts in v0.x have also been combined into a single ConfigError
concept, with support for both horizontal composition (through or
) and vertical composition (parallelly, through and
). Errors are mostly handled internally, and as users we most often only use ConfigValue
.
Decoders are only for conversions
In Ciris v0.x, we used to say env[Option[Int]]("MAX_RETRIES")
for optional values and Secret[Int]
for secret values. This worked because ConfigDecoder
s could inspect errors of the value they were decoding, and create a new value based on that information.
In Ciris v1.0.0, ConfigDecoder
s can only access the value (and a description of the key, if available). So while it's possible to describe Option
or Secret
decoders, they won't get the right semantics — i.e. None
as default value for Option
, and sensitive details redacted from error messages for Secret
.
Instead, we use functions like option
and secret
on ConfigValue
s to achieve the same result.
env("MAX_RETRIES").as[Int].option
env("API_KEY").or(prop("api.key")).secret
Configuration sources as functions
Ciris 0.x had the ConfigSource
concept for describing how to load ConfigValue
s. In v1.0.0, this concept has been removed, and we can now create ConfigValue
s. Functions ConfigValue.loaded
, ConfigValue.missing
, ConfigValue.suspend
, and similar functions enable us to describe configuration loading without ever talking about specific effect types.
Following is the definition of file
, showing how to load file contents.
def file(path: Path, blocker: Blocker, charset: Charset): ConfigValue[String] =
ConfigValue.blockOn(blocker) {
ConfigValue.suspend {
val key = ConfigKey.file(path, charset)
try {
val bytes = Files.readAllBytes(path)
val value = new String(bytes, charset)
ConfigValue.loaded(key, value)
} catch {
case _: NoSuchFileException =>
ConfigValue.missing(key)
}
}
}
Keys are actually descriptions
In Ciris 0.x, ConfigKeyType
provided the name for keys, e.g. environment variable
. Key and ConfigKeyType
would then be passed throughout the configuration loading, in order to support sensible error messages. This has been simplified in v1.0.0 to ConfigKey
, which fully describes the key, e.g. environment variable API_KEY
.