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. Removing it should make things work.
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 config-source per key in your config. Example: Reading 5 config values to summon a case class instance can spin up a new "connection" 5 times, unless we memoize this initialisation.
If there is a resource acquisition and release involved in reading each config parameter, then acquire and release will happen 5 times as well. We can memoize ConfigSource by calling .memoize
, and in this case the resource acquisition and release happens once, with an in-memory tree representing your config for further reads.
More details
This is mainly because, at the core, ConfigSource is a Reader
, which is a function f: PropertyTreePath[K] => IO[ReadErrror[String], PropertyTree[String, String]
. However there is more to it, the Reader
itself is wrapped in an ZManaged
effect. Infact, there is more to it, where its a double layered ZManaged
to allow memoization of the effect (ZManaged) to even form this function.
An excerpt from the original code
type Managed[A] = ZManaged[Any, ReadError[K], A]
type TreeReader = PropertyTreePath[K] => ZIO[Any, ReadError[K], PropertyTree[K, V]]
type MemoizableManaged[A] = ZManaged[Any, Nothing, ZManaged[Any, ReadError[K], A]]
type ManagedReader = Managed[TreeReader]
type MemoizableManagedReader = MemoizableManaged[TreeReader]
As a user, probably this is more of heavy lifting details, which you would like to forget. Most of the 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. For example: We don't have database table as ConfigSource
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
using a dummy PropertyTree
in the below example.
In here, app
results in printing out Acquiring resource
and Releasing resource
twice, since there are two
keys that's being retrieved.
import zio.config._, ConfigSource._, ConfigDescriptor._
val dummySource =
PropertyTree.Record(Map("age" -> PropertyTree.Leaf("1"), "name" -> PropertyTree.Leaf("afsal")))
val effectFulReader: ZManaged[Any, ReadError[String], TreeReader] =
ZManaged
.make(ZIO.debug("Acquiring resource"))(_ => ZIO.debug("Releasing resource"))
.flatMap(_ => ZManaged.succeed((path: PropertyTreePath[String]) => ZIO.succeed(dummySource.at(path))))
val source: ConfigSource =
ConfigSource.Reader(Set(ConfigSourceName("my source")), ZManaged.succeed(effectFulReader))
val app: ZIO[Any, ReadError[String], (Int, String)] =
read((int("age") zip string("name")) from source)
// [info] Acquiring resource
// [info] Releasing resource
// [info] Acquiring resource
// [info] Releasing resource
// [info] (1,afsal)
Now if source
is memoized
it will acquire and release only once.
val app: ZIO[Any, ReadError[String], (Int, String)] =
read((int("age") zip string("name")) from source.memoize)
// [info] Acquiring resource
// [info] Releasing resource
// [info] (1,afsal)
ConfigSource
initialisation per read
and multiple reads
There is a second level of memoization where config-source can be initialised once and for all, and is reused in multiple reads. This memoization is done by calling configSource.strictlyOnce
returning a ZIO effect indicating that the communication with the actual data source happens prior to the reads
.
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 involve talking to the actual file 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)
Integration with Enumeratum - an experimental module
Unify API for ConfigSource
With right set of imports, example import zio.config.typesafe._
or import zio.config.yaml._
, you can now use ConfigSource.from...
instead of being a bit inconsistent by specifying TypesafeConfigSource.from..
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 ConfigSource, and thereby reduce redundant case class configurations, 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. Example: lightbend/config#30. Hopefully this feature will be useful for the direct users of typesafe/config too.
There is still more experimentations done at this level.
New way of Layer
Dependency Updates
- Update refined to 0.9.28 (#676) @scala-steward
- Update sbt to 1.5.7 (#688) @scala-steward
- Update auxlib, javalib, nativelib, nscplugin, ... to 0.4.2 (#681) @scala-steward
- Update Node.js to v16.13.1 (#678) @renovate
- Update actions/checkout action to v2.4.0 (#661) @renovate
- Update Node.js to v16.13.0 (#654) @renovate
- Update Node.js to v16 (#653) @renovate
- Update sbt-bloop to 1.4.10 (#645) @scala-steward
- Update sbt-mdoc to 2.2.24 (#643) @scala-steward
- Update actions/checkout action to v2.3.5 (#640) @renovate
- Update Node.js to v14.18.1 (#639) @renovate
- Update sbt-scalajs, scalajs-compiler, ... to 1.7.1 (#638) @scala-steward
- Update Node.js to v14.18.0 (#632) @renovate