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.
- All misconfigurations of
AndErrors
are put in parallel lines.
╥
╠══╗
║ ║ FormatError
║ MissingValue
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)
narrow
Separation of concerns with 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