2.0.0-RC1
We are quite excited to publish the first release candidate of ZIO-CONFIG 2.0!
On a high level, zio-config is now way more powerful for the following reasons:
-
Real life ConfigSource. Now zio-config has more machineries that allow users to define any
ConfigSource
, such as a Dynamodb from AWS, a database table, or a secret store in cloud. This is backed by meaningful semantics around initialisation, laziness and memoization at various levels of usage ofConfigSource
. -
More integrations, mainly cats, scalaz, enumeratum and AWS. Integrations with AWS can even serve as examples for users to create their own complex
ConfigSource
. -
More composable. For example,
ConfigSource.fromHoconFile
returns just aConfigSource
making it even nicer to compose your config-sources without the verbosity offlatMap
. -
More ergonomic. No more
|@|
,apply
, andunapply
. It's justzip
and.to[CaseClass]
-
More features. Example: Zoom into an existing
ConfigSource
and make a newConfigSource
. -
Unified API. More details further down below.
While most of the changes are already specified during the milestone release of ZIO-CONFIG-2.0.0-M1, its important to point them out here as well, with a few corrections and clarifications.
What's Changed
|@|
disappears and use zip
, which returns flattened tuple
final case class MyConfg(age: Int, name: String)
val config: ConfigDescroptor[MyConfig] =
int("AGE").zip(string("NAME")).to[MyConfig]
// and not, (int("AGE") |@| string("NAME"))(MyConfig.apply, MyConfig.unapply)
val config: ConfigDescriptor[(Int, String, Double)] =
int("AGE") zip string("NAME") zip double("AMOUNT")
// implies, no more `.tupled` functionality that worked with `|@|` to retrieve tuples
Apply
and Unapply
is no more required and supported. Use .to[CaseClass]
final case class MyConfg(age: Int, name: String)
val config: ConfigDescroptor[MyConfig] =
int("AGE").zip(string("NAME")).to[MyConfig]
// and not, (int("AGE") |@| string("NAME"))(MyConfig.apply, MyConfig.unapply)
ConfigSource
return types are simplified, and now more composable with ConfigDescriptor
ConfigSource.from
will not return ZLayer
or ZIO
anymore. This means we have more easier composability with ConfigSource
regardless of the type of ConfigSource
.
import zio.config._, typesafe._
val source = ConfigSource.fromMap(Map("a" -> "b")) orElse ConfigSource.fromHoconFile(file)
read(config from source)
read
returns ZIO
and not Either
Now on read(config from source)
returns a ZIO[Any, ReadError[String], Config]
instead of Either[ReadError[String], Config]
This implies, there isn't a need of doing ZIO.fromEither
if you happen to use read
in a ZIO context.
val app: ZIO[Any, ReadError[String], (Int, String)] =
read(int("age").zip(string("name")) from ConfigSource.fromMap(Map.empty))
Better semantics around resource handling for ConfigSource
ConfigSource internals have been changed to support the fact that forming a ConfigSource involves an ZManaged
effect.
More details further down below.
ConfigSource is now just a function (a Reader
), and initialisation of Reader
can be memoized
zio-config can memoize the initialisation of config-source. Example: Reading 5 config values to summon a case class instance can spin up a new "db connection" 5 times, unless we memoize this initialisation using configSource.memoize
. If memoized
, the resource acquisition happens only once to read all the 5 config keys, and then release the resource only at the end of the read.
More details
This is mainly because, at the core, ConfigSource is a Reader
, which is a function f: PropertyTreePath[K] => IO[ReadErrror[K], PropertyTree[K, V]
. Reader
itself is wrapped in an ZManaged
effect. Infact, there is more to it, that its a double layered ZManaged
to allow memoization of the effect (ZManaged).
An excerpt from the original code, to expose the fact that ConfigSource
has really changed.
type TreeReader = PropertyTreePath[K] => ZIO[Any, ReadError[K], PropertyTree[K, V]]
type MemoizableManaged[A] = ZManaged[Any, Nothing, ZManaged[Any, ReadError[K], A]]
type MemoizableManagedReader = MemoizableManaged[TreeReader]
// ConfigSource is conceptually MemoizableManagedReader
As a user, probably this is more of heavy lifting details, which you would like to forget. Most of the inbuilt sources as of now are memoized for it's initialisation per read
. This is mainly because the underlying pre-built sources are not quite complex yet, and it doesn't make sense to acquire and release resources per key, since most of the sources can form an entire property tree using a single resource acquisition and a single release at the end. For example: We don't have a database table as ConfigSource
yet which requires acquiring a java.sql.Connection
to even form the function PropertyTreePath[K] => ZIO[Any, ReadError[K], PropertyTree[K, V]]
.
For those who are curious, take a look at the implementation of a ConfigSource
in the below example.
In here, app
results in printing out Acquire connection
and Release connection
twice, since there are two
keys that are being retrieved.
import zio.config._, ConfigSource._, ConfigDescriptor._
// pseudo code
def getConnection(): ZManaged[Any, Throwable, Connection] =
ZManaged.make(ZIO.debug("Acquire connection") *> acquireConnection )(connection => ZIO.debug("Release connection") *> ZIO.effect(connection.close))
def readDb(connection: Connection, key: PropertyTreePath[String]): ZIO[Any, Throwable, PropertyTree[String, String]] = ???
// value is represented by `PropertyTree[String, String]`
val reader: ZManaged[Any, ReadError[String], TreeReader] =
getConnection
.flatMap(connection => ZManaged.succeed((key: PropertyTreePath[String]) => readDb(connection, key)))
val source: ConfigSource =
ConfigSource.fromManaged("my db source", reader)
val app: ZIO[Any, ReadError[String], (Int, String)] =
read((int("age") zip string("name")) from source)
// [info] Acquire connection
// [info] Release connection
// [info] Acquire connection
// [info] Release connection
// [info] (1,afsal)
Now if source
is memoized
it will acquire and release. connection only once.
val app: ZIO[Any, ReadError[String], (Int, String)] =
read((int("age") zip string("name")) from source.memoize)
// [info] Acquire connection
// [info] Release connection
// [info] (1,afsal)
ConfigSource
initialisation per read
and multiple reads
There is a second level of memoization (logically) where config-source can be initialised once and for all, and is reused across the app in multiple reads. This memoization is done by calling configSource.strictlyOnce
returning a ZIO effect indicating the fact the communication with the actual data source happens prior to the reads
. This is not a recommended approach, however, there could be instances where you want to guarantee that the real IO happens only once in the entire lifecycle of the application when it comes to Config Management.
import zio.config._
import zio.config.typesafe._
import zio.ZIO
import ConfigDescriptor._
Example, in the below case, it will hit the actual file twice.
val source =
ConfigSource.fromHoconFilePath("/Users/afsalthaj/sample.txt")
val app: ZIO[Any, ReadError[String], (Int, String)] =
for {
age <- read(int("age") from source)
name <- read(string("name") from source)
} yield (age, name)
However in the below case, it will read the actual file (IO) only once
val source =
ConfigSource.fromHoconFilePath("/Users/afsalthaj/sample.txt")
val app: ZIO[Any, ReadError[String], (Int, String)] =
for {
src <- source.strictlyOnce
age <- read(int("age") from src)
name <- read(string("name") from src)
} yield (age, name)
Release of various integrations
The first experimental release of zio-config-aws
that can recursively read SSM parameter-store path, along with integrations with cats
, scalaz
, and enumeratum
. Example: Now on, you can read cat's NonEmptyList
from any source.
Unify API for ConfigSource
With right set of imports, i.e, import zio.config.typesafe._
or import zio.config.yaml._
, you can just use ConfigSource.from...
all over the place instead of, say, TypesafeConfigSource.from..
. This gives us a sense of consistency.
import zio.config._
import zio.config.typesafe._
ConfigSource.fromHoconFilePath(..)
ConfigSource.fromMap(..)
ConfigSource.fromSystemEnv()
Zoom into ConfigSource
using PropertyTreePath
- an experimental feature.
We can now peek into a subtree of a source and make it another ConfigSource, and thereby reduce redundant case class configurations. This is especially useful when dealing with auto-derivations where your case class is not corresponding to the original source.
import zio.config._, typesafe._
{
"a" : {
"b" : {
"c" : [
{
"x" : 1
"y" : 2
}
{
"x" : 3
"y" : 4
}
]
}
}
}
final case class ShortConfig(x: Int, y: Int)
read(descriptor[ShortConfig] from ConfigSource.fromHoconFile(file).at(path"a.b.c[0]"))
// returning ShortConfig(3, 4)
We have seen users struggling to zoom into ConfigSource
since the original source may not allow this when it comes to details. For example, see this issue: lightbend/config#30. By using zio-config, this feature will be available for any ConfigSource
regardless of whether the underlying libraries support it or not.
There is still more experimentations done at this level. Hence consider this as an experimental feature.