Skip to main content

How to Feed Configuration into a Wire Dependency Graph

In this guide, we build a small bookstore backend from three services, Db, InventoryRepo and Catalog, and let each one receive its own typed settings through the dependency graph. By the end, you will have one ConfigSource that layers environment variables over a YAML file over defaults, a graph that hands each service the settings section it asks for, a shutdown that closes the database last, a startup that stops with a full report when a setting is wrong, and tests that need no files or environment variables.

We read one settings section first, then build the whole graph, look at the shutdown order, break a setting on purpose, and finish with tests:

The Problem​

A service graph needs every service built after its dependencies, with the right settings, and closed in the opposite order. Writing that by hand mixes three jobs in main: reading raw strings, constructing objects in the right order, and closing them in reverse.

Without a configuration library or a dependency-injection library, the bookstore's main usually looks like this:

final case class DbSettings(url: String, user: String, password: String, poolSize: Int)
final case class CatalogSettings(featuredTitle: String, pageSize: Int)

class Db(settings: DbSettings) extends AutoCloseable {
def close(): Unit = ()
}
class InventoryRepo(db: Db)
class Catalog(repo: InventoryRepo, settings: CatalogSettings) extends AutoCloseable {
def close(): Unit = ()
}

object Main {
private def setting(name: String, default: => String): String =
sys.env.getOrElse(name, default)

def main(args: Array[String]): Unit = {
val db = new Db(
DbSettings(
setting("DB_URL", "jdbc:postgresql://localhost:5432/bookstore"),
setting("DB_USER", "bookstore"),
setting("DB_PASSWORD", sys.error("DB_PASSWORD is not set")),
setting("DB_POOL_SIZE", "10").toInt
)
)
try {
val repo = new InventoryRepo(db)
val catalog = new Catalog(
repo,
CatalogSettings(
setting("CATALOG_FEATURED_TITLE", "Zionomicon"),
setting("CATALOG_PAGE_SIZE", "3").toInt
)
)
try println("serving") // the application runs here
finally catalog.close()
} finally db.close()
}
}

Each new service adds another hand-placed try/finally, and moving a constructor call means rechecking the closing order. A CATALOG_PAGE_SIZE=ten throws NumberFormatException: For input string: "ten", which names neither the key nor the source, and it throws only after the database is already open, so you find out about the next bad setting on the next restart. Testing Catalog means exporting environment variables.

In this guide, the config module turns each settings section into a typed case class with every error reported at once, and the scope module builds the graph from the constructors and closes it in reverse order.

Prerequisites​

Add the config module and its YAML format to your build.sbt. The config module depends on the scope and context modules, so Scope, Resource, Wire and Context come with it:

// Scala 2.13 and Scala 3.3 to 3.8
libraryDependencies ++= Seq(
"dev.zio" %% "zio-blocks-config" % "0.0.56",
"dev.zio" %% "zio-blocks-config-yaml" % "0.0.56"
)

// Scala 3.9 and newer (also needs scalacOptions += "-experimental")
libraryDependencies ++= Seq(
"dev.zio" %% "zio-blocks-next-config" % "0.0.56",
"dev.zio" %% "zio-blocks-next-config-yaml" % "0.0.56"
)

This guide assumes you have loaded a case class from a layered ConfigSource, as in Build a Typed Configuration Loader, and that you have built a Resource and used it inside Scope.global.scoped, as in Compile-Time Resource Safety with Scope. The snippets below share these imports:

import zio.blocks.config._
import zio.blocks.config.yaml._
import zio.blocks.context.Context
import zio.blocks.schema.Schema
import zio.blocks.scope._

The Bookstore's Services and Their Settings​

The bookstore has a database pool, a repository on top of it, and a catalog that renders the front page. Db and Catalog each take a settings case class as a constructor parameter, and each settings class derives a Schema, which is all the config module needs to decode it:

final case class DbConfig(url: String, user: String, password: Secret, poolSize: Int)
object DbConfig {
implicit val schema: Schema[DbConfig] = Schema.derived
}

final case class CatalogConfig(featuredTitle: String, pageSize: Int)
object CatalogConfig {
implicit val schema: Schema[CatalogConfig] = Schema.derived
}

class Db(config: DbConfig) extends AutoCloseable {
println(s"Db: opening ${config.poolSize} connections to ${config.url} as ${config.user}, password ${config.password}")

def titles(): List[String] =
List("Zionomicon", "Programming in Scala", "Functional Programming in Scala")

def close(): Unit = println("Db: closing the connection pool")
}

class InventoryRepo(db: Db) {
println("InventoryRepo: ready")

def inStock(): List[String] = db.titles()
}

class Catalog(repo: InventoryRepo, config: CatalogConfig) extends AutoCloseable {
println(s"Catalog: featuring ${config.featuredTitle}")

def frontPage(): String =
s"Featured: ${config.featuredTitle} | In stock: ${repo.inStock().take(config.pageSize).mkString(", ")}"

def close(): Unit = println("Catalog: flushing the page cache")
}

The services print when they start and stop, so the output below shows the order. The password is a Secret, so printing it shows <secret> instead of the value.

One settings type per service

The graph finds a constructor's arguments by type. Give each service its own settings case class instead of loose String or Int parameters: two String parameters for a URL and a user name can't be told apart by type. How to Fix Common Scope and Wire Compile Errors shows what the macro reports for such a constructor.

Reading One Settings Section​

Config.wire[A](prefix) turns one settings section into a wire, the scope module's recipe for building a value. Its type, Wire.Shared[ConfigSource, DbConfig], says that it needs a ConfigSource and produces a DbConfig; Shared means the graph builds it once and hands the same value to every service that asks for it. The prefix "db" makes the wire read db.url, db.user and so on. A wire is only a recipe, so we run it by hand with Wire.Shared#make, passing a scope and a Context that holds the source. The source holds the db section:

val sectionSource = ConfigSource.fromMap(
Map(
"db.url" -> "jdbc:postgresql://localhost:5432/bookstore",
"db.user" -> "bookstore",
"db.password" -> "change-me",
"db.poolSize" -> "10"
),
"sections"
)

Running the wire prints the decoded section, with the password hidden:

Scope.global.scoped { scope =>
val dbConfig = Config.wire[DbConfig]("db").make(scope, Context[ConfigSource](sectionSource))
println(dbConfig)
}
// DbConfig(jdbc:postgresql://localhost:5432/bookstore,bookstore,<secret>,10)

Config.wire[A] without a prefix reads the fields from the top level of the source. Inside a graph, you never call Wire.Shared#make yourself: Resource.from does it, as the next section shows.

Building the Whole Graph​

A real service reads its settings from several places, so we layer them as Build a Typed Configuration Loader does: environment variables first, then a YAML file, then defaults. In production, the environment layer is EnvSource, keyed to the BOOKSTORE_ prefix and converted to upper snake case, so catalog.featuredTitle is read from BOOKSTORE_CATALOG_FEATURED_TITLE:

val productionEnv: ConfigSource = EnvSource.keyFormat(KeyFormat.UpperSnakeCase).prefix("bookstore")

This page must give the same output on every machine, so it stands in a map for the environment, holding one exported variable, BOOKSTORE_DB_PASSWORD. The YAML file and the defaults supply the rest:

val env = ConfigSource.fromMap(Map("db.password" -> "change-me"), "env")

val bookstoreYaml =
"""db:
| url: jdbc:postgresql://localhost:5432/bookstore
| user: bookstore
|catalog:
| featuredTitle: Zionomicon
|""".stripMargin

val yaml = ConfigSource.fromYaml(bookstoreYaml, "bookstore.yaml") match {
case Right(source) => source
case Left(error) => throw error
}

val defaults = ConfigSource.fromMap(Map("db.poolSize" -> "10", "catalog.pageSize" -> "3"), "defaults")

val settings = env.orElse(yaml).orElse(defaults)

Resource.from[Catalog] builds the whole graph from Catalog's constructor. The wires passed to it supply what the constructors can't: Wire(source) hands the graph the ConfigSource value, and the two Config.wire calls turn it into DbConfig and CatalogConfig. The macro derives a wire for Db and InventoryRepo from their constructors:

def catalogResource(source: ConfigSource): Resource[Catalog] =
Resource.from[Catalog](
Wire(source),
Config.wire[DbConfig]("db"),
Config.wire[CatalogConfig]("catalog")
)

Allocating the resource in a scope builds every service, and $ calls the catalog inside the scope:

val frontPage = Scope.global.scoped { scope =>
import scope._
val catalog = allocate(catalogResource(settings))
$(catalog)(_.frontPage())
}
// Db: opening 10 connections to jdbc:postgresql://localhost:5432/bookstore as bookstore, password <secret>
// InventoryRepo: ready
// Catalog: featuring Zionomicon
// Catalog: flushing the page cache
// Db: closing the connection pool
// frontPage: String = "Featured: Zionomicon | In stock: Zionomicon, Programming in Scala, Functional Programming in Scala"

println(frontPage)
// Featured: Zionomicon | In stock: Zionomicon, Programming in Scala, Functional Programming in Scala

The password came from the environment stand-in, the URL, user and featured title from YAML, and the pool and page sizes from the defaults. Neither Db nor Catalog knows where its settings came from.

Make the graph a def, not a val

A Resource built by Resource.from is shared: the first allocation builds the services, and closing the last scope that uses them closes them for good. A val holding it works once, and a second Scope.global.scoped that allocates the same val throws IllegalStateException: Cannot allocate from a destroyed shared resource. A def, as above, builds a fresh resource for each scope.

Closing Services in Reverse Order​

The scope closes what it opened when the scoped block ends, in the reverse order of opening. The graph opens Db first because everything else depends on it, so Db closes last, after Catalog has flushed its cache. The output above shows the order; here it is again with the work in the middle:

Scope.global.scoped { scope =>
import scope._
val catalog = allocate(catalogResource(settings))
println("Serving: " + $(catalog)(_.frontPage()))
}
// Db: opening 10 connections to jdbc:postgresql://localhost:5432/bookstore as bookstore, password <secret>
// InventoryRepo: ready
// Catalog: featuring Zionomicon
// Serving: Featured: Zionomicon | In stock: Zionomicon, Programming in Scala, Functional Programming in Scala
// Catalog: flushing the page cache
// Db: closing the connection pool

You write no close() calls. The macro registers close() for every class that extends AutoCloseable and is built from its constructor, as Db and Catalog are here. InventoryRepo holds no resources of its own, so it has nothing to close.

Let the macro build anything you need to close

Build Db from its constructor, as above, not with Wire.Shared.fromFunction. That function runs your code and registers nothing, so a Db built there is never closed. Config.wire is built that way, which is fine because a settings case class holds no resources. Also keep Db and Catalog inside scoped: return plain data such as the front-page String, never the service, which is closed once the block ends.

Stopping at Startup on a Bad Setting​

Config.wire decodes its section while the graph is built, and when anything is missing or malformed it throws ConfigLoadException with every error in that section. Here the catalog section has a page size of two and no featured title, layered over sectionSource, the complete db section from Reading One Settings Section:

val badCatalog = ConfigSource.fromMap(Map("catalog.pageSize" -> "two"), "bad-catalog").orElse(sectionSource)

The scope opens Db and InventoryRepo, stops at CatalogConfig, and closes what it had opened before the exception leaves scoped:

try {
Scope.global.scoped { scope =>
import scope._
val catalog = allocate(catalogResource(badCatalog))
$(catalog)(_.frontPage())
}
} catch {
case e: ConfigLoadException => println(e.getMessage)
}
// Db: opening 10 connections to jdbc:postgresql://localhost:5432/bookstore as bookstore, password <secret>
// InventoryRepo: ready
// Db: closing the connection pool
// Configuration loading failed with 1 error(s):
// - Missing required key 'featuredTitle' in source 'bad-catalog|sections'
// Invalid value 'two' for key 'pageSize' (expected Int) in source 'bad-catalog|sections': For input string: "two"
//

The application never serves a request with half its settings. When the bad setting is in the db section, the graph stops before it opens Db, so nothing needs closing.

Two details of the report trip readers up. The keys are relative to the section's prefix: pageSize means catalog.pageSize, and the report doesn't name the section. The count in the first line also reads 1 error(s) even when more lines follow, because the errors of one case class are collected into a single composite error; read the lines, not the count.

Testing Without Files or Environment Variables​

The graph depends only on the ConfigSource passed to catalogResource, so a test passes a ConfigSource.fromMap instead of the production layers. A test usually changes one or two settings, so it puts a small map in front of the defaults and leaves the rest alone, as frontPageWith does:

def frontPageWith(overrides: Map[String, String]): String = {
val testSettings = ConfigSource.fromMap(overrides, "test").orElse(settings)
Scope.global.scoped { scope =>
import scope._
val catalog = allocate(catalogResource(testSettings))
$(catalog)(_.frontPage())
}
}

Each call builds and closes its own graph, so tests don't share services:

val page = frontPageWith(Map("catalog.featuredTitle" -> "Functional Programming in Scala", "catalog.pageSize" -> "1"))
// Db: opening 10 connections to jdbc:postgresql://localhost:5432/bookstore as bookstore, password <secret>
// InventoryRepo: ready
// Catalog: featuring Functional Programming in Scala
// Catalog: flushing the page cache
// Db: closing the connection pool
// page: String = "Featured: Functional Programming in Scala | In stock: Zionomicon"

println(page == "Featured: Functional Programming in Scala | In stock: Zionomicon")
// true

In a test suite, settings would be a map with every key, so the test reads no file and no environment variable at all.

Putting It Together​

The complete example layers the real environment, a YAML document and defaults, builds the bookstore graph, prints the front page and the shutdown order, and shows the report for a bad setting:

config-examples/src/main/scala/feedconfigintoawiregraph/CompleteExample.scala
package feedconfigintoawiregraph

import zio.blocks.config._
import zio.blocks.config.yaml._
import zio.blocks.context.Context
import zio.blocks.schema.Schema
import zio.blocks.scope._

/**
* How to Feed Configuration into a Wire Dependency Graph — Putting It Together
*
* Layers the real environment over YAML over defaults, builds the graph, and
* shows the report for a bad catalog setting.
*
* Run with: cd config-examples && sbt "runMain
* feedconfigintoawiregraph.CompleteExample"
*/
object CompleteExample {

final case class DbConfig(url: String, user: String, password: Secret, poolSize: Int)
object DbConfig {
implicit val schema: Schema[DbConfig] = Schema.derived
}

final case class CatalogConfig(featuredTitle: String, pageSize: Int)
object CatalogConfig {
implicit val schema: Schema[CatalogConfig] = Schema.derived
}

class Db(config: DbConfig) extends AutoCloseable {
println(
s"Db: opening ${config.poolSize} connections to ${config.url} as ${config.user}, password ${config.password}"
)

def titles(): List[String] =
List("Zionomicon", "Programming in Scala", "Functional Programming in Scala")

def close(): Unit = println("Db: closing the connection pool")
}

class InventoryRepo(db: Db) {
println("InventoryRepo: ready")

def inStock(): List[String] = db.titles()
}

class Catalog(repo: InventoryRepo, config: CatalogConfig) extends AutoCloseable {
println(s"Catalog: featuring ${config.featuredTitle}")

def frontPage(): String =
s"Featured: ${config.featuredTitle} | In stock: ${repo.inStock().take(config.pageSize).mkString(", ")}"

def close(): Unit = println("Catalog: flushing the page cache")
}

val productionEnv: ConfigSource = EnvSource.keyFormat(KeyFormat.UpperSnakeCase).prefix("bookstore")

val bookstoreYaml =
"""db:
| url: jdbc:postgresql://localhost:5432/bookstore
| user: bookstore
|catalog:
| featuredTitle: Zionomicon
|""".stripMargin

val yaml = ConfigSource.fromYaml(bookstoreYaml, "bookstore.yaml") match {
case Right(source) => source
case Left(error) => throw error
}

// The password default is for local runs only; production exports BOOKSTORE_DB_PASSWORD.
val defaults = ConfigSource.fromMap(
Map("db.password" -> "change-me", "db.poolSize" -> "10", "catalog.pageSize" -> "3"),
"defaults"
)

val settings = productionEnv.orElse(yaml).orElse(defaults)

def catalogResource(source: ConfigSource): Resource[Catalog] =
Resource.from[Catalog](
Wire(source),
Config.wire[DbConfig]("db"),
Config.wire[CatalogConfig]("catalog")
)

def main(args: Array[String]): Unit = {
// 1. Build the graph, serve the front page, and close in reverse order.
val frontPage = Scope.global.scoped { scope =>
import scope._
val catalog = allocate(catalogResource(settings))
$(catalog)(_.frontPage())
}
println(frontPage)

// 2. A bad catalog setting stops startup with a full report.
val dbSection = ConfigSource.fromMap(
Map(
"db.url" -> "jdbc:postgresql://localhost:5432/bookstore",
"db.user" -> "bookstore",
"db.password" -> "change-me",
"db.poolSize" -> "10"
),
"db-section"
)
val badCatalog = ConfigSource.fromMap(Map("catalog.pageSize" -> "two"), "bad-catalog").orElse(dbSection)
try {
Scope.global.scoped { scope =>
import scope._
val catalog = allocate(catalogResource(badCatalog))
$(catalog)(_.frontPage())
}
} catch {
case e: ConfigLoadException => println(e.getMessage)
}
}
}

Running the Examples​

All examples in this guide have corresponding runnable Scala files in the config-examples project. To get them, clone the repository and change into the examples project:

git clone https://github.com/zio/zio-blocks.git
cd zio-blocks/config-examples

Each file below matches one section of this guide.

Reading One Settings Section​

config-examples/src/main/scala/feedconfigintoawiregraph/Step1ReadOneSection.scala
config-examples/src/main/scala/feedconfigintoawiregraph/Step1ReadOneSection.scala
package feedconfigintoawiregraph

import zio.blocks.config._
import zio.blocks.config.yaml._
import zio.blocks.context.Context
import zio.blocks.schema.Schema
import zio.blocks.scope._

/**
* How to Feed Configuration into a Wire Dependency Graph — Reading One Settings
* Section
*
* Runs one Config.wire by hand against a map source.
*
* Run with: cd config-examples && sbt "runMain
* feedconfigintoawiregraph.Step1ReadOneSection"
*/
object Step1ReadOneSection {

final case class DbConfig(url: String, user: String, password: Secret, poolSize: Int)
object DbConfig {
implicit val schema: Schema[DbConfig] = Schema.derived
}

val sectionSource = ConfigSource.fromMap(
Map(
"db.url" -> "jdbc:postgresql://localhost:5432/bookstore",
"db.user" -> "bookstore",
"db.password" -> "change-me",
"db.poolSize" -> "10"
),
"sections"
)

def main(args: Array[String]): Unit =
Scope.global.scoped { scope =>
val dbConfig = Config.wire[DbConfig]("db").make(scope, Context[ConfigSource](sectionSource))
println(dbConfig)
}
}

Run it from the config-examples directory:

sbt "runMain feedconfigintoawiregraph.Step1ReadOneSection"

Building the Whole Graph​

config-examples/src/main/scala/feedconfigintoawiregraph/Step2BuildTheGraph.scala
config-examples/src/main/scala/feedconfigintoawiregraph/Step2BuildTheGraph.scala
package feedconfigintoawiregraph

import zio.blocks.config._
import zio.blocks.config.yaml._
import zio.blocks.context.Context
import zio.blocks.schema.Schema
import zio.blocks.scope._

/**
* How to Feed Configuration into a Wire Dependency Graph — Building the Whole
* Graph
*
* Layers env over YAML over defaults and builds the bookstore graph.
*
* Run with: cd config-examples && sbt "runMain
* feedconfigintoawiregraph.Step2BuildTheGraph"
*/
object Step2BuildTheGraph {

final case class DbConfig(url: String, user: String, password: Secret, poolSize: Int)
object DbConfig {
implicit val schema: Schema[DbConfig] = Schema.derived
}

final case class CatalogConfig(featuredTitle: String, pageSize: Int)
object CatalogConfig {
implicit val schema: Schema[CatalogConfig] = Schema.derived
}

class Db(config: DbConfig) extends AutoCloseable {
println(
s"Db: opening ${config.poolSize} connections to ${config.url} as ${config.user}, password ${config.password}"
)

def titles(): List[String] =
List("Zionomicon", "Programming in Scala", "Functional Programming in Scala")

def close(): Unit = println("Db: closing the connection pool")
}

class InventoryRepo(db: Db) {
println("InventoryRepo: ready")

def inStock(): List[String] = db.titles()
}

class Catalog(repo: InventoryRepo, config: CatalogConfig) extends AutoCloseable {
println(s"Catalog: featuring ${config.featuredTitle}")

def frontPage(): String =
s"Featured: ${config.featuredTitle} | In stock: ${repo.inStock().take(config.pageSize).mkString(", ")}"

def close(): Unit = println("Catalog: flushing the page cache")
}

val env = ConfigSource.fromMap(Map("db.password" -> "change-me"), "env")

val bookstoreYaml =
"""db:
| url: jdbc:postgresql://localhost:5432/bookstore
| user: bookstore
|catalog:
| featuredTitle: Zionomicon
|""".stripMargin

val yaml = ConfigSource.fromYaml(bookstoreYaml, "bookstore.yaml") match {
case Right(source) => source
case Left(error) => throw error
}

val defaults = ConfigSource.fromMap(Map("db.poolSize" -> "10", "catalog.pageSize" -> "3"), "defaults")

val settings = env.orElse(yaml).orElse(defaults)

def catalogResource(source: ConfigSource): Resource[Catalog] =
Resource.from[Catalog](
Wire(source),
Config.wire[DbConfig]("db"),
Config.wire[CatalogConfig]("catalog")
)

def main(args: Array[String]): Unit = {
val frontPage = Scope.global.scoped { scope =>
import scope._
val catalog = allocate(catalogResource(settings))
$(catalog)(_.frontPage())
}

println(frontPage)
}
}

Run it from the config-examples directory:

sbt "runMain feedconfigintoawiregraph.Step2BuildTheGraph"

Closing Services in Reverse Order​

config-examples/src/main/scala/feedconfigintoawiregraph/Step3ShutdownOrder.scala
config-examples/src/main/scala/feedconfigintoawiregraph/Step3ShutdownOrder.scala
package feedconfigintoawiregraph

import zio.blocks.config._
import zio.blocks.config.yaml._
import zio.blocks.context.Context
import zio.blocks.schema.Schema
import zio.blocks.scope._

/**
* How to Feed Configuration into a Wire Dependency Graph — Closing Services in
* Reverse Order
*
* Shows Catalog closing before Db when the scope ends.
*
* Run with: cd config-examples && sbt "runMain
* feedconfigintoawiregraph.Step3ShutdownOrder"
*/
object Step3ShutdownOrder {

final case class DbConfig(url: String, user: String, password: Secret, poolSize: Int)
object DbConfig {
implicit val schema: Schema[DbConfig] = Schema.derived
}

final case class CatalogConfig(featuredTitle: String, pageSize: Int)
object CatalogConfig {
implicit val schema: Schema[CatalogConfig] = Schema.derived
}

class Db(config: DbConfig) extends AutoCloseable {
println(
s"Db: opening ${config.poolSize} connections to ${config.url} as ${config.user}, password ${config.password}"
)

def titles(): List[String] =
List("Zionomicon", "Programming in Scala", "Functional Programming in Scala")

def close(): Unit = println("Db: closing the connection pool")
}

class InventoryRepo(db: Db) {
println("InventoryRepo: ready")

def inStock(): List[String] = db.titles()
}

class Catalog(repo: InventoryRepo, config: CatalogConfig) extends AutoCloseable {
println(s"Catalog: featuring ${config.featuredTitle}")

def frontPage(): String =
s"Featured: ${config.featuredTitle} | In stock: ${repo.inStock().take(config.pageSize).mkString(", ")}"

def close(): Unit = println("Catalog: flushing the page cache")
}

val env = ConfigSource.fromMap(Map("db.password" -> "change-me"), "env")

val bookstoreYaml =
"""db:
| url: jdbc:postgresql://localhost:5432/bookstore
| user: bookstore
|catalog:
| featuredTitle: Zionomicon
|""".stripMargin

val yaml = ConfigSource.fromYaml(bookstoreYaml, "bookstore.yaml") match {
case Right(source) => source
case Left(error) => throw error
}

val defaults = ConfigSource.fromMap(Map("db.poolSize" -> "10", "catalog.pageSize" -> "3"), "defaults")

val settings = env.orElse(yaml).orElse(defaults)

def catalogResource(source: ConfigSource): Resource[Catalog] =
Resource.from[Catalog](
Wire(source),
Config.wire[DbConfig]("db"),
Config.wire[CatalogConfig]("catalog")
)

def main(args: Array[String]): Unit =
Scope.global.scoped { scope =>
import scope._
val catalog = allocate(catalogResource(settings))
println("Serving: " + $(catalog)(_.frontPage()))
}
}

Run it from the config-examples directory:

sbt "runMain feedconfigintoawiregraph.Step3ShutdownOrder"

Stopping at Startup on a Bad Setting​

config-examples/src/main/scala/feedconfigintoawiregraph/Step4BadSetting.scala
config-examples/src/main/scala/feedconfigintoawiregraph/Step4BadSetting.scala
package feedconfigintoawiregraph

import zio.blocks.config._
import zio.blocks.config.yaml._
import zio.blocks.context.Context
import zio.blocks.schema.Schema
import zio.blocks.scope._

/**
* How to Feed Configuration into a Wire Dependency Graph — Stopping at Startup
* on a Bad Setting
*
* A malformed catalog section stops the graph with a full report.
*
* Run with: cd config-examples && sbt "runMain
* feedconfigintoawiregraph.Step4BadSetting"
*/
object Step4BadSetting {

final case class DbConfig(url: String, user: String, password: Secret, poolSize: Int)
object DbConfig {
implicit val schema: Schema[DbConfig] = Schema.derived
}

final case class CatalogConfig(featuredTitle: String, pageSize: Int)
object CatalogConfig {
implicit val schema: Schema[CatalogConfig] = Schema.derived
}

class Db(config: DbConfig) extends AutoCloseable {
println(
s"Db: opening ${config.poolSize} connections to ${config.url} as ${config.user}, password ${config.password}"
)

def titles(): List[String] =
List("Zionomicon", "Programming in Scala", "Functional Programming in Scala")

def close(): Unit = println("Db: closing the connection pool")
}

class InventoryRepo(db: Db) {
println("InventoryRepo: ready")

def inStock(): List[String] = db.titles()
}

class Catalog(repo: InventoryRepo, config: CatalogConfig) extends AutoCloseable {
println(s"Catalog: featuring ${config.featuredTitle}")

def frontPage(): String =
s"Featured: ${config.featuredTitle} | In stock: ${repo.inStock().take(config.pageSize).mkString(", ")}"

def close(): Unit = println("Catalog: flushing the page cache")
}

val sectionSource = ConfigSource.fromMap(
Map(
"db.url" -> "jdbc:postgresql://localhost:5432/bookstore",
"db.user" -> "bookstore",
"db.password" -> "change-me",
"db.poolSize" -> "10"
),
"sections"
)

def catalogResource(source: ConfigSource): Resource[Catalog] =
Resource.from[Catalog](
Wire(source),
Config.wire[DbConfig]("db"),
Config.wire[CatalogConfig]("catalog")
)

val badCatalog = ConfigSource.fromMap(Map("catalog.pageSize" -> "two"), "bad-catalog").orElse(sectionSource)

def main(args: Array[String]): Unit =
try {
Scope.global.scoped { scope =>
import scope._
val catalog = allocate(catalogResource(badCatalog))
$(catalog)(_.frontPage())
}
} catch {
case e: ConfigLoadException => println(e.getMessage)
}
}

Run it from the config-examples directory:

sbt "runMain feedconfigintoawiregraph.Step4BadSetting"

Testing Without Files or Environment Variables​

config-examples/src/main/scala/feedconfigintoawiregraph/Step5Testing.scala
config-examples/src/main/scala/feedconfigintoawiregraph/Step5Testing.scala
package feedconfigintoawiregraph

import zio.blocks.config._
import zio.blocks.config.yaml._
import zio.blocks.context.Context
import zio.blocks.schema.Schema
import zio.blocks.scope._

/**
* How to Feed Configuration into a Wire Dependency Graph — Testing Without
* Files or Environment Variables
*
* Overrides a few settings with a map and checks the front page.
*
* Run with: cd config-examples && sbt "runMain
* feedconfigintoawiregraph.Step5Testing"
*/
object Step5Testing {

final case class DbConfig(url: String, user: String, password: Secret, poolSize: Int)
object DbConfig {
implicit val schema: Schema[DbConfig] = Schema.derived
}

final case class CatalogConfig(featuredTitle: String, pageSize: Int)
object CatalogConfig {
implicit val schema: Schema[CatalogConfig] = Schema.derived
}

class Db(config: DbConfig) extends AutoCloseable {
println(
s"Db: opening ${config.poolSize} connections to ${config.url} as ${config.user}, password ${config.password}"
)

def titles(): List[String] =
List("Zionomicon", "Programming in Scala", "Functional Programming in Scala")

def close(): Unit = println("Db: closing the connection pool")
}

class InventoryRepo(db: Db) {
println("InventoryRepo: ready")

def inStock(): List[String] = db.titles()
}

class Catalog(repo: InventoryRepo, config: CatalogConfig) extends AutoCloseable {
println(s"Catalog: featuring ${config.featuredTitle}")

def frontPage(): String =
s"Featured: ${config.featuredTitle} | In stock: ${repo.inStock().take(config.pageSize).mkString(", ")}"

def close(): Unit = println("Catalog: flushing the page cache")
}

val env = ConfigSource.fromMap(Map("db.password" -> "change-me"), "env")

val bookstoreYaml =
"""db:
| url: jdbc:postgresql://localhost:5432/bookstore
| user: bookstore
|catalog:
| featuredTitle: Zionomicon
|""".stripMargin

val yaml = ConfigSource.fromYaml(bookstoreYaml, "bookstore.yaml") match {
case Right(source) => source
case Left(error) => throw error
}

val defaults = ConfigSource.fromMap(Map("db.poolSize" -> "10", "catalog.pageSize" -> "3"), "defaults")

val settings = env.orElse(yaml).orElse(defaults)

def catalogResource(source: ConfigSource): Resource[Catalog] =
Resource.from[Catalog](
Wire(source),
Config.wire[DbConfig]("db"),
Config.wire[CatalogConfig]("catalog")
)

def frontPageWith(overrides: Map[String, String]): String = {
val testSettings = ConfigSource.fromMap(overrides, "test").orElse(settings)
Scope.global.scoped { scope =>
import scope._
val catalog = allocate(catalogResource(testSettings))
$(catalog)(_.frontPage())
}
}

def main(args: Array[String]): Unit = {
val page =
frontPageWith(Map("catalog.featuredTitle" -> "Functional Programming in Scala", "catalog.pageSize" -> "1"))

println(page == "Featured: Functional Programming in Scala | In stock: Zionomicon")
}
}

Run it from the config-examples directory:

sbt "runMain feedconfigintoawiregraph.Step5Testing"

The Complete Example​

config-examples/src/main/scala/feedconfigintoawiregraph/CompleteExample.scala
config-examples/src/main/scala/feedconfigintoawiregraph/CompleteExample.scala
package feedconfigintoawiregraph

import zio.blocks.config._
import zio.blocks.config.yaml._
import zio.blocks.context.Context
import zio.blocks.schema.Schema
import zio.blocks.scope._

/**
* How to Feed Configuration into a Wire Dependency Graph — Putting It Together
*
* Layers the real environment over YAML over defaults, builds the graph, and
* shows the report for a bad catalog setting.
*
* Run with: cd config-examples && sbt "runMain
* feedconfigintoawiregraph.CompleteExample"
*/
object CompleteExample {

final case class DbConfig(url: String, user: String, password: Secret, poolSize: Int)
object DbConfig {
implicit val schema: Schema[DbConfig] = Schema.derived
}

final case class CatalogConfig(featuredTitle: String, pageSize: Int)
object CatalogConfig {
implicit val schema: Schema[CatalogConfig] = Schema.derived
}

class Db(config: DbConfig) extends AutoCloseable {
println(
s"Db: opening ${config.poolSize} connections to ${config.url} as ${config.user}, password ${config.password}"
)

def titles(): List[String] =
List("Zionomicon", "Programming in Scala", "Functional Programming in Scala")

def close(): Unit = println("Db: closing the connection pool")
}

class InventoryRepo(db: Db) {
println("InventoryRepo: ready")

def inStock(): List[String] = db.titles()
}

class Catalog(repo: InventoryRepo, config: CatalogConfig) extends AutoCloseable {
println(s"Catalog: featuring ${config.featuredTitle}")

def frontPage(): String =
s"Featured: ${config.featuredTitle} | In stock: ${repo.inStock().take(config.pageSize).mkString(", ")}"

def close(): Unit = println("Catalog: flushing the page cache")
}

val productionEnv: ConfigSource = EnvSource.keyFormat(KeyFormat.UpperSnakeCase).prefix("bookstore")

val bookstoreYaml =
"""db:
| url: jdbc:postgresql://localhost:5432/bookstore
| user: bookstore
|catalog:
| featuredTitle: Zionomicon
|""".stripMargin

val yaml = ConfigSource.fromYaml(bookstoreYaml, "bookstore.yaml") match {
case Right(source) => source
case Left(error) => throw error
}

// The password default is for local runs only; production exports BOOKSTORE_DB_PASSWORD.
val defaults = ConfigSource.fromMap(
Map("db.password" -> "change-me", "db.poolSize" -> "10", "catalog.pageSize" -> "3"),
"defaults"
)

val settings = productionEnv.orElse(yaml).orElse(defaults)

def catalogResource(source: ConfigSource): Resource[Catalog] =
Resource.from[Catalog](
Wire(source),
Config.wire[DbConfig]("db"),
Config.wire[CatalogConfig]("catalog")
)

def main(args: Array[String]): Unit = {
// 1. Build the graph, serve the front page, and close in reverse order.
val frontPage = Scope.global.scoped { scope =>
import scope._
val catalog = allocate(catalogResource(settings))
$(catalog)(_.frontPage())
}
println(frontPage)

// 2. A bad catalog setting stops startup with a full report.
val dbSection = ConfigSource.fromMap(
Map(
"db.url" -> "jdbc:postgresql://localhost:5432/bookstore",
"db.user" -> "bookstore",
"db.password" -> "change-me",
"db.poolSize" -> "10"
),
"db-section"
)
val badCatalog = ConfigSource.fromMap(Map("catalog.pageSize" -> "two"), "bad-catalog").orElse(dbSection)
try {
Scope.global.scoped { scope =>
import scope._
val catalog = allocate(catalogResource(badCatalog))
$(catalog)(_.frontPage())
}
} catch {
case e: ConfigLoadException => println(e.getMessage)
}
}
}

Run it from the config-examples directory, optionally overriding a setting through the environment:

BOOKSTORE_CATALOG_FEATURED_TITLE="Programming in Scala" sbt "runMain feedconfigintoawiregraph.CompleteExample"

To check that every example compiles without running it, use:

sbt compile

Going Further​