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

Quick Start

The library aims to have a powerful & purely functional, yet a thin interface to access configuration information inside an application. There are many examples in here straight away as well.

Adding dependency

If you are using sbt:


libraryDependencies += "dev.zio" %% "zio-config" % <version>

Optional Dependency with magnolia module (Auto derivation)

libraryDependencies += "dev.zio" %% "zio-config-magnolia" % <version>

Optional Dependency with refined module (Integration with refined library)

libraryDependencies += "dev.zio" %% "zio-config-refined" % <version>

Optional Dependency with typesafe module (HOCON/Json source)

libraryDependencies += "dev.zio" %% "zio-config-typesafe" % <version>

Optional Dependency with yaml module (Yaml source)

libraryDependencies += "dev.zio" %% "zio-config-yaml" % <version>

Optional Dependency for a random generation of a config

libraryDependencies += "dev.zio" %% "zio-config-gen" % <version>

Describe the config by hand

We must fetch the configuration from the environment to a case class (product) in scala. Let it be MyConfig

import zio.IO

import zio.config._, ConfigDescriptor._, ConfigSource._


case class MyConfig(ldap: String, port: Int, dburl: String)

To perform any action using zio-config, we need a configuration description. Let's define a simple one.


val myConfig: ConfigDescriptor[MyConfig] =
  (string("LDAP") |@| int("PORT")|@| string("DB_URL"))(MyConfig.apply, MyConfig.unapply)

 // ConfigDescriptor[ MyConfig]

To get a tuple,


val myConfigTupled: ConfigDescriptor[(String, Int, String)] =
  (string("LDAP") |@| int("PORT")|@| string("DB_URL")).tupled


Fully automated Config Description

If you don't like describing your configuration manually, and rely on the names of the parameter in the case class (or sealed trait), there is a separate module called zio-config-magnolia.

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.


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

val myConfigAutomatic = descriptor[MyConfig]

myConfig and myConfigAutomatic are same description, and is of the same type.

Refer to API docs for more explanations on descriptor More examples on automatic derivation is in examples module of zio-config

Read config from various sources

There are more information on various sources in here.

Below given is a simple example.

val map =
  Map(
    "LDAP" -> "xyz",
    "PORT" -> "8888",
    "DB_URL" -> "postgres"
  )

val source = ConfigSource.fromMap(map)

read(myConfig from source)
// Either[ReadError[String], MyConfig]

// Alternatively, you can rely on `Config.from..` pattern to get ZLayers.
val result =
  ZConfig.fromMap(map, myConfig)

// Layer[ReadError[String], Config[A]]  

You can run this to completion as in any zio application.

How to use config descriptor

Readers from configdescriptor

As mentioned before, you can use config descriptor to read from various sources.


val anotherResult =
  read(myConfig from source)
// Either[ReadError[String], MyConfig]

Note that, this is almost similar to Config.fromMap(map, myConfig) in the previous section.

More details in here.

Documentations using configdescriptor

generateDocs(myConfig)
//Creates documentation (automatic)


val betterConfig =
  (string("LDAP") ?? "Related to auth" |@|  int("PORT") ?? "Database port" |@|
    string("DB_URL") ?? "url of database"
   )(MyConfig.apply, MyConfig.unapply)

generateDocs(betterConfig).toTable.toGithubFlavouredMarkdown
// Custom documentation along with auto generated docs

More details in here.

Writers from configdescriptor


write(myConfig, MyConfig("xyz", 8888, "postgres")).map(_.flattenString())
//  Map("LDAP" -> "xyz", "PORT" -> "8888", "DB_URL" -> "postgres")

More details in here.

Report generation from configdescriptor

generateReport(myConfig, MyConfig("xyz", 8888, "postgres"))
// Generates documentation showing value of each parameter

Generate a random config


import zio.config.derivation.name
import zio.config.magnolia._, zio.config.gen._

object RandomConfigGenerationSimpleExample extends App {
  sealed trait Region

  @name("ap-southeast-2")
  case object ApSouthEast2 extends Region

  @name("us-east")
  case object UsEast extends Region

  case class DetailsConfig(username: String, region: Region)

  println(generateConfigJson(descriptor[DetailsConfig]).unsafeRunChunk)

  // yields for example

  // Chunk(
  //   {
  //    "region" : "ap-southeast-2",
  //     "username" : "eU2KlfATwYZ5s0Y"
  //   }
  // )
}


Accumulating all errors

For any misconfiguration, the ReadError collects all of them with proper semantics: AndErrors and OrErrors. Instead of directly printing misconfigurations, the ReadError.prettyPrint shows the path, detail of collected misconfigurations.

  1. All misconfigurations of AndErrors are put in parallel lines.
╥
╠══╗ 
║  ║ FormatError
║ MissingValue
  1. OrErrors are in the same line which indicates a sequential misconfiguration
╥
╠MissingValue
║
╠FormatError

Here is a complete example:

   ReadError:
   ╥
   ╠══╦══╗
   ║  ║  ║
   ║  ║  ╠─MissingValue
   ║  ║  ║ path: var2
   ║  ║  ║ Details: value of type string
   ║  ║  ║ 
   ║  ║  ╠─MissingValue path: envvar3
   ║  ║  ║ path: var3
   ║  ║  ║ Details: value of type string
   ║  ║  ║ 
   ║  ║  ▼
   ║  ║
   ║  ╠─FormatError
   ║  ║ cause: Provided value is wrong, expecting the type int
   ║  ║ path: var1
   ║  ▼
   ▼

It says, fix FormatError related to path "var1" in the source. For the next error, either provide var2 or var3 to fix MissingValue error.

Note: Use prettyPrint method to avoid having to avoid seeing highly nested ReadErrors, that can be difficult to read.

Config is your ZIO environment

Take a look at the below example that shows an entire mini app. This will tell you how to consider configuration as just a part of Environment of your ZIO functions across your application.


import zio.{ ZIO, ZLayer, Has }
import zio.console._

case class ApplicationConfig(bridgeIp: String, userName: String)

val configuration =
  (string("bridgeIp") |@| string("username"))(ApplicationConfig.apply, ApplicationConfig.unapply)

val finalExecution: ZIO[Has[ApplicationConfig] with Console, Nothing, Unit] =
  for {
    appConfig <- getConfig[ApplicationConfig]
    _         <- putStrLn(appConfig.bridgeIp)
    _         <- putStrLn(appConfig.userName)
  } yield ()

val configLayer = ZConfig.fromPropertiesFile("file-location", configuration)

// Main App
val pgm = finalExecution.provideLayer(configLayer ++ Console.live)

Separation of concerns with narrow

In bigger apps you can have a lot of components and, consequently - a lot of configuration fields. It's not ideal to pass around the whole configuration object as a dependency for all of those components: this way you break the separation of concerns principle. Component should be aware only about dependencies it cares about and uses somehow.

So to avoid that and do it in an ergonomic way, there's a narrow syntax extension for ZLayer, available under import zio.config.syntax._ import:

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

trait Endpoint
trait Repository

case class AppConfig(api: ApiConfig, db: DbConfig)
case class DbConfig (url: String,    driver: String)
case class ApiConfig(host: String,   port: Int)

val configDescription = DeriveConfigDescriptor.descriptor[AppConfig]

// components have only required dependencies
val endpoint: ZLayer[Has[ApiConfig], Nothing, Has[Endpoint]]    = ZLayer.fromService(_ => new Endpoint {})
val repository: ZLayer[Has[DbConfig], Nothing, Has[Repository]] = ZLayer.fromService(_ => new Repository {}) 

val cfg = TypesafeConfig.fromDefaultLoader(configDescription)

cfg.narrow(_.api) >>> endpoint // narrowing down to a proper config subtree
cfg.narrow(_.db) >>> repository
Last updated on 1/5/2021 by Afsal Thaj
Manual creation of ConfigDescriptor →
  • Adding dependency
  • Describe the config by hand
  • Fully automated Config Description
  • Read config from various sources
  • How to use config descriptor
    • Readers from configdescriptor
    • Documentations using configdescriptor
    • Writers from configdescriptor
    • Report generation from configdescriptor
    • Accumulating all errors
  • Config is your ZIO environment
    • Separation of concerns with narrow
ZIO Config
GitHub
Star
Chat with us on Discord
discord
Additional resources
ZIO HomepageScaladoc
Copyright © 2021 ZIO Maintainers