ZIO Config

ZIO Config

  • Documentation
  • GitHub

›ZIO-CONFIG

ZIO-CONFIG

  • Quick Start
  • Manual creation of ConfigDescriptor
  • Automatic Derivation of ConfigDescriptor
  • Read from various Sources
  • Using ConfigDescriptor for Read, Write, Document and Report
  • Automatic Validations
  • Design Principles

Automatic Derivation of ConfigDescriptor

By bringing in zio-config-magnolia we avoid all the boilerplate required to define the config. With a single import, ConfigDescriptor is automatically derived.

Also, we will get to see the Hocon config for coproducts is devoid of any extra tagging to satisfy the library. This is much easier/intuitive unlike many existing implementations.

Take a look at the magnolia examples in zio-config. One of them is provided here for quick access.

Note: zio-config-shapeless is an alternative to zio-config-magnolia to support scala 2.11 projects. It will be deprecated once we find users have moved on from scala 2.11.

Example

Config



   sealed trait X
 
   object X {
     case object A extends X
     case object B extends X
     case object C extends X
     case class  DetailsWrapped(detail: Detail) extends X
     
     case class Detail(firstName: String, lastName: String, region: Region)
     case class Region(suburb: String, city: String)
   }

   /**
    * We use automatic derivation here.
    * As an example, In order to specify, {{{ x = a }}} in the source where `a`
    * represents X.A object, we need a case class that wraps
    * the sealed trait, and we use the field name of this case class as the key
    */
  case class MyConfig(x: X)

AutoDerivation

  // Setting up imports

  import zio.config._, zio.config.typesafe._
  import zio.config.magnolia.DeriveConfigDescriptor._

  import X._
  // Only for example purpose
  implicit class ImpureEither[A, B](either: Either[A, B]) {
    def loadOrThrow: B = either match {
      case Left(_) => throw new Exception()
      case Right(v) => v
     }
  }

  // Defining different possibility of HOCON source

  val aHoconSource =
    TypesafeConfigSource
      .fromHoconString("x = A")
      .loadOrThrow
  val bHoconSource =
    TypesafeConfigSource
      .fromHoconString("x = B")
      .loadOrThrow
  val cHoconSource =
    TypesafeConfigSource
      .fromHoconString("x = C")
      .loadOrThrow
  val dHoconSource =
    TypesafeConfigSource
      .fromHoconString(
        s"""
           | x {
           |   DetailsWrapped {
           |    detail  {
           |      firstName : ff
           |      lastName  : ll
           |      region {
           |        city   : syd
           |        suburb : strath
           |     }
           |   }
           |  }
           |}
           |""".stripMargin
      )
      .loadOrThrow

  // Let's try automatic derivation

  read(descriptor[MyConfig] from aHoconSource)
  // res0: Right(MyConfig(A))

  read(descriptor[MyConfig] from bHoconSource)
  // res0: Right(MyConfig(B))

  read(descriptor[MyConfig] from cHoconSource)
  // res0: Right(MyConfig(C))

  read(descriptor[MyConfig] from dHoconSource)
  // res0: Right(MyConfig(DetailsWrapped(Detail("ff", "ll", Region("strath", "syd")))))

To know more about various semantics of descriptor, please refer to the api docs.

NOTE

The fieldNames and classnames remain the same as that of case-classes and sealed-traits.

If you want custom names for your fields, use name annotation.

  import zio.config.derivation.name

  @name("detailsWrapped")
  case class  DetailsWrapped(detail: Detail) extends X

import zio.config._

descriptor[MyConfig].mapKey(toKebabCase)

With the above changefirstName and lastName in the above HOCON example can be first-name and last-name respectively.

There are various ways in which you can customise the derivation of sealed traits. This is a bit involving, and more documentations will be provided soon.

Documentation while automatic derivation

With describe annotation you can still document your config while automatically generating the config


import zio.config.magnolia.describe

@describe("This config is about aws")
case class Aws(region: String, dburl: DbUrl)
case class DbUrl(value: String)

This will be equivalent to the manual configuration of:

   (string("region") |@| string("dburl").transform(DbUrl, _.value))(Aws.apply, Aws.unapply) ?? "This config is about aws"

You could provide describe annotation at field level

Example:

case class Aws(@describe("AWS region") region: String, dburl: DbUrl)

This will be equivalent to the manual configuration of:

   (string("region") ?? "AWS region" |@| string("dburl").transform(DbUrl, _.value))(Aws.apply, Aws.unapply) ?? "This config is about aws"

Custom ConfigDescription

Every field in a case class should have an instance of Descriptor in order for the automatic derivation to work. This doesn't mean the entire design of zio-config is typeclass based. For the same reason, the typeclass Descriptor exists only in zio-config-magnolia (or zio-config-shapeless) package.

As an example, given below is a case class where automatic derivation won't work, and result in a compile time error: Assume that, AwsRegion is a type that comes from AWS SDK.

  import java.time.ZonedDateTime

  case class Execution(time: AwsRegion, id: Int)

In this case, descriptor[Execution] will give us the following descriptive compile error.

magnolia: could not find Descriptor.Typeclass for type <outside.library.package>.AwsRegion
  in parameter 'time' of product type <your.packagename>.Execution

This is because zio-config-magnolia failed to derive an instance of Descriptor for AwsRegion.

In order to provide implicit instances, following choices are there

 import zio.config.magnolia.DeriveConfigDescriptor.{Descriptor, descriptor}

 implicit val awsRegionDescriptor: Descriptor[Aws.Region] =
   Descriptor[String].transform(string => AwsRegion.from(string), _.toString)

Now descriptor[Execution] compiles.

Is that the only way for custom derivation ?

What if our custom type is complex enough that, parsing from a string would actually fail? The answer is, zio-config provides with all the functions that you need to handle errors.

 import zio.config.magnolia.DeriveConfigDescriptor.{Descriptor, descriptor}

  implicit val descriptorO: Descriptor[ZonedDateTime] =
    Descriptor[String].transformOrFailLeft(x => Try (ZonedDateTime.parse(x)).toEither.swap.map(_.getMessage).swap)(_.toString)

What is transformOrFailLeft ? Parsing a String to ZonedDateTime can fail, but converting it back to a string won't fail. Logically, these are respectively the first 2 functions that we passed to transformEitherLeft.

PS: We recommend not to use throwable.getMessage. Provide more descriptive error message.

You can also rely on transformOrfail if both to and fro can fail.

Please give descriptions wherever possible for a better experience

Giving descriptions is going to be helpful. While all the built-in types have documentations, it is better we give some description to custom types as well. For example:

 import zio.config.magnolia.DeriveConfigDescriptor.{Descriptor, descriptor}


  implicit val awsRegionDescriptor: Descriptor[Aws.Region] =
    Descriptor[String].transform(string => AwsRegion.from(string), _.toString) ?? "value of type AWS.Region"

This way, when there is an error due to MissingValue, we get an error message (don't forget to use prettyPrint) that describes about the config parameter. For example, see the Details corresponding to the first MissingValue in a sample error message below.

 ReadError:
 ╥
 ╠─MissingValue
 ║ path: aws.region
 ║ Details: value of type AWS.Region
 ║
 ╠─FormatError
 ║ cause: Provided value is 3dollars, expecting the type double
 ║ path: cost
 ▼

Where to place these implicits ?

If the types are owned by us, then the best place to keep implicit instance is the companion object of that type.


final case clas MyAwsRegion(value: AwsRegion)

object MyAwsRegion {
  implicit val awsRegionDescriptor: Descriptor[MyAwsRegion] =
    Descriptor[String]
      .transform(
        string => MyAwsRegion(AwsRegion.from(string)), 
        _.value.toString
      ) ?? "value of type AWS.Region"
}

However, sometimes, the types are owned by an external library.

In these situations, better off place the implicit closer to where we call the automatic derivation. Please find the example in magnolia package in examples module.

Change Keys (CamelCase, kebab-case etc)

Please find the examples in ChangeKeys.scala in magnolia module to find how to manipulate keys in an automatic derivation such as being able to specify keys as camelCase, kebabCase or snakeCase in the source config.

Last updated on 1/5/2021 by Afsal Thaj
← Manual creation of ConfigDescriptorRead from various Sources →
ZIO Config
GitHub
Star
Chat with us on Discord
discord
Additional resources
ZIO HomepageScaladoc
Copyright © 2021 ZIO Maintainers