github zio/zio-config v2.0.0-RC1

latest releases: v4.0.2, v4.0.1, v4.0.0...
2 years ago

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:

  1. 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 of ConfigSource.

  2. More integrations, mainly cats, scalaz, enumeratum and AWS. Integrations with AWS can even serve as examples for users to create their own complex ConfigSource.

  3. More composable. For example, ConfigSource.fromHoconFile returns just a ConfigSource making it even nicer to compose your config-sources without the verbosity of flatMap.

  4. More ergonomic. No more |@| , apply, and unapply. It's just zip and .to[CaseClass]

  5. More features. Example: Zoom into an existing ConfigSourceand make a new ConfigSource.

  6. 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.

https://github.com/zio/zio-config/blob/master/examples/shared/src/main/scala/zio/config/examples/typesafe/SubConfigExample.scala

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.

New way of Layer

https://github.com/zio/zio-config/blob/master/examples/shared/src/main/scala/zio/config/examples/LayerExample.scala

https://github.com/zio/zio-config/blob/master/core/shared/src/main/scala/zio/config/ConfigModule.scala#L75

Don't miss a new zio-config release

NewReleases is sending notifications on new releases.