@zio.dev/zio-blocks 0.0.33 → 0.0.55
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/adr/2026-07-18-data-migration.md +123 -0
- package/guides/async-getting-started.md +687 -0
- package/guides/compile-time-resource-safety-with-scope.md +21 -16
- package/guides/getting-started-with-mux.md +1395 -0
- package/guides/query-dsl-extending.md +161 -102
- package/guides/query-dsl-fluent-builder.md +217 -157
- package/guides/query-dsl-reified-optics.md +12 -10
- package/guides/query-dsl-sql.md +640 -165
- package/guides/sql-checked-interpolation.md +173 -0
- package/guides/sql-transactions.md +286 -0
- package/guides/telemetry-guide.md +1130 -0
- package/guides/zio-schema-migration.md +29 -22
- package/index.md +248 -389
- package/package.json +1 -1
- package/plans/config-follow-up-prs.md +188 -0
- package/plans/config-pr-assessment-roadmap.md +310 -0
- package/reference/MuxDataFlow.jsx +250 -0
- package/reference/async.md +1499 -0
- package/reference/chunk.md +3533 -308
- package/reference/codegen/case-class.md +436 -0
- package/reference/codegen/emitter-config.md +383 -0
- package/reference/codegen/examples.md +664 -0
- package/reference/codegen/field.md +316 -0
- package/reference/codegen/index.md +317 -0
- package/reference/codegen/scala-emitter.md +392 -0
- package/reference/codegen/scala-file.md +276 -0
- package/reference/codegen/sealed-trait.md +408 -0
- package/reference/codegen/type-definition.md +340 -0
- package/reference/codegen/type-ref.md +201 -0
- package/reference/combinators.md +347 -117
- package/reference/config/config-decoder.md +460 -0
- package/reference/config/config-source.md +489 -0
- package/reference/config/errors.md +278 -0
- package/reference/config/flags.md +369 -0
- package/reference/config/formats.md +314 -0
- package/reference/config/index.md +304 -0
- package/reference/config/rollout.md +336 -0
- package/reference/context.md +9 -52
- package/reference/data-migration.md +269 -0
- package/reference/datastar/attributes.md +302 -0
- package/reference/datastar/events.md +234 -0
- package/reference/datastar/index.md +256 -0
- package/reference/datastar/signals.md +230 -0
- package/reference/datastar/sse.md +295 -0
- package/reference/datastar.md +346 -0
- package/reference/docs.md +1461 -345
- package/reference/endpoint/auth-type.md +146 -0
- package/reference/endpoint/bulk-creation.md +96 -0
- package/reference/endpoint/endpoint.md +297 -0
- package/reference/endpoint/http-codec.md +249 -0
- package/reference/endpoint/index.md +745 -0
- package/reference/endpoint/path-codec.md +225 -0
- package/reference/endpoint/route-pattern.md +194 -0
- package/reference/endpoint/route-tree.md +111 -0
- package/reference/endpoint/segment-codec.md +199 -0
- package/reference/html.md +1424 -0
- package/reference/htmx/attribute-values.md +359 -0
- package/reference/htmx/hx-encoding.md +111 -0
- package/reference/htmx/hx-params.md +204 -0
- package/reference/htmx/hx-swap.md +276 -0
- package/reference/htmx/hx-sync.md +251 -0
- package/reference/htmx/hx-target.md +314 -0
- package/reference/htmx/hx-trigger.md +457 -0
- package/reference/htmx/hx-url-update.md +239 -0
- package/reference/htmx/index.md +807 -0
- package/reference/htmx/response-headers.md +240 -0
- package/reference/http-model/headers.md +735 -0
- package/reference/http-model/index.md +49 -0
- package/reference/http-model/model.md +1517 -0
- package/reference/http-model/schema-codecs.md +522 -0
- package/reference/http-model/schema.md +750 -0
- package/reference/http-model/server-sent-event.md +341 -0
- package/reference/jwt.md +195 -0
- package/reference/maybe.md +943 -0
- package/reference/media-type.md +2 -2
- package/reference/mux.md +254 -0
- package/reference/mux.mdx +828 -0
- package/reference/openapi.md +1351 -0
- package/reference/projection.md +654 -0
- package/reference/resource-management/defer-handle.md +1 -1
- package/reference/resource-management/resource.md +31 -98
- package/reference/resource-management/scope.md +28 -220
- package/reference/resource-management/wire.md +5 -55
- package/reference/ringbuffer/MpmcDiagram.jsx +717 -0
- package/reference/ringbuffer/MpscDiagram.jsx +618 -0
- package/reference/ringbuffer/SpmcDiagram.jsx +680 -0
- package/reference/ringbuffer/SpscDiagram.jsx +677 -0
- package/reference/ringbuffer/advanced.mdx +109 -0
- package/reference/ringbuffer/index.mdx +145 -0
- package/reference/ringbuffer/mpmc.mdx +185 -0
- package/reference/ringbuffer/mpsc.mdx +164 -0
- package/reference/ringbuffer/spmc.mdx +108 -0
- package/reference/ringbuffer/spsc.mdx +416 -0
- package/reference/{allows.md → schema/allows.md} +4 -100
- package/reference/{binding-resolver.md → schema/binding-resolver.md} +1 -1
- package/reference/{binding.md → schema/binding.md} +3 -4
- package/reference/schema/built-in-codecs/avro.md +451 -0
- package/reference/schema/built-in-codecs/bson.md +510 -0
- package/reference/schema/built-in-codecs/csv.md +564 -0
- package/reference/schema/built-in-codecs/index.md +77 -0
- package/reference/schema/built-in-codecs/json/index.md +295 -0
- package/reference/schema/built-in-codecs/json/json-config.md +217 -0
- package/reference/{json-patch.md → schema/built-in-codecs/json/json-patch.md} +5 -5
- package/reference/{json-schema.md → schema/built-in-codecs/json/json-schema.md} +14 -47
- package/reference/schema/built-in-codecs/json/json-selection.md +322 -0
- package/reference/{json.md → schema/built-in-codecs/json/json.md} +32 -64
- package/reference/schema/built-in-codecs/messagepack.md +508 -0
- package/reference/schema/built-in-codecs/thrift.md +433 -0
- package/reference/schema/built-in-codecs/toon.md +1078 -0
- package/reference/{xml.md → schema/built-in-codecs/xml.md} +13 -9
- package/reference/schema/built-in-codecs/yaml.md +552 -0
- package/reference/{codec.md → schema/codec.md} +11 -11
- package/reference/{dynamic-optic.md → schema/dynamic-optic.md} +196 -5
- package/reference/{dynamic-schema.md → schema/dynamic-schema.md} +8 -8
- package/reference/schema/format.md +92 -0
- package/reference/schema/index.md +52 -0
- package/reference/schema/migration.md +297 -0
- package/reference/{modifier.md → schema/modifier.md} +58 -7
- package/reference/{optics.md → schema/optics.md} +2 -2
- package/reference/{patch.md → schema/patch.md} +1 -1
- package/{path-interpolator.md → reference/schema/path-interpolator.md} +167 -72
- package/reference/schema/reflect-transformer.md +140 -0
- package/reference/{schema-evolution → schema/schema-evolution}/as.md +8 -8
- package/reference/{schema-evolution → schema/schema-evolution}/index.md +2 -2
- package/reference/{schema-evolution → schema/schema-evolution}/into.md +8 -8
- package/reference/{schema-expr.md → schema/schema-expr.md} +110 -175
- package/reference/schema/schema-search.md +263 -0
- package/reference/{schema.md → schema/schema.md} +22 -2
- package/reference/{structural-types.md → schema/structural-types.md} +1 -1
- package/reference/{type-class-derivation.md → schema/type-class-derivation.md} +63 -1
- package/reference/smithy.md +1032 -0
- package/reference/sql/db-codec-deriver.md +71 -0
- package/reference/sql/db-codec.md +687 -0
- package/reference/sql/db-con.md +271 -0
- package/reference/sql/db-connection.md +153 -0
- package/reference/sql/db-param-writer.md +77 -0
- package/reference/sql/db-param.md +66 -0
- package/reference/sql/db-result-reader.md +148 -0
- package/reference/sql/db-tx.md +114 -0
- package/reference/sql/db-value.md +41 -0
- package/reference/sql/ddl.md +85 -0
- package/reference/sql/frag.md +288 -0
- package/reference/sql/index.md +341 -0
- package/reference/sql/repo.md +600 -0
- package/reference/sql/sql-dialect.md +73 -0
- package/reference/sql/sql-logger.md +62 -0
- package/reference/sql/sql-name-mapper.md +70 -0
- package/reference/sql/table-metadata.md +134 -0
- package/reference/sql/table.md +448 -0
- package/reference/sql/transactor-zio.md +399 -0
- package/reference/sql/transactor.md +363 -0
- package/reference/sql-zio.md +112 -0
- package/reference/streams/core/index.md +32 -0
- package/reference/streams/core/pipeline.md +854 -0
- package/reference/streams/core/sink.md +1404 -0
- package/reference/streams/core/stream.md +3236 -0
- package/reference/streams/execution-and-compatibility/async-execution.md +822 -0
- package/reference/streams/execution-and-compatibility/index.md +35 -0
- package/reference/streams/execution-and-compatibility/platform-differences.md +297 -0
- package/reference/streams/execution-and-compatibility/scala-2-compatibility.md +88 -0
- package/reference/streams/execution-and-compatibility/zero-boxing.md +393 -0
- package/reference/streams/index.md +726 -0
- package/reference/streams/primitives/index.md +30 -0
- package/reference/streams/primitives/reader.md +1992 -0
- package/reference/streams/primitives/writer.md +1201 -0
- package/reference/telemetry/common/any-value.md +90 -0
- package/reference/telemetry/common/attribute-key.md +87 -0
- package/reference/telemetry/common/attributes.md +118 -0
- package/reference/telemetry/common/index.md +39 -0
- package/reference/telemetry/common/instrumentation-scope.md +24 -0
- package/reference/telemetry/common/resource.md +34 -0
- package/reference/telemetry/index.md +311 -0
- package/reference/telemetry/logging/index.md +197 -0
- package/reference/telemetry/logging/log-enrichment.md +72 -0
- package/reference/telemetry/logging/log-formatter.md +100 -0
- package/reference/telemetry/logging/log-record-processor.md +56 -0
- package/reference/telemetry/logging/log-record.md +44 -0
- package/reference/telemetry/logging/log-writer.md +64 -0
- package/reference/telemetry/logging/logger-provider.md +142 -0
- package/reference/telemetry/logging/logger.md +83 -0
- package/reference/telemetry/logging/severity.md +62 -0
- package/reference/telemetry/metrics/index.md +150 -0
- package/reference/telemetry/metrics/instruments.md +183 -0
- package/reference/telemetry/metrics/labeled-instruments.md +74 -0
- package/reference/telemetry/metrics/meter-provider.md +76 -0
- package/reference/telemetry/metrics/meter.md +98 -0
- package/reference/telemetry/metrics/metric-data.md +57 -0
- package/reference/telemetry/otel/custom-exporter.md +216 -0
- package/reference/telemetry/otel/index.md +212 -0
- package/reference/telemetry/tracing/index.md +155 -0
- package/reference/telemetry/tracing/sampler.md +89 -0
- package/reference/telemetry/tracing/span-builder.md +57 -0
- package/reference/telemetry/tracing/span-context.md +39 -0
- package/reference/telemetry/tracing/span-data.md +32 -0
- package/reference/telemetry/tracing/span-kind.md +55 -0
- package/reference/telemetry/tracing/span-processor.md +53 -0
- package/reference/telemetry/tracing/span-status.md +47 -0
- package/reference/telemetry/tracing/span.md +117 -0
- package/reference/telemetry/tracing/tracer-provider.md +91 -0
- package/reference/telemetry/tracing/tracer.md +52 -0
- package/reference/typeid.md +5 -83
- package/sidebars.js +376 -43
- package/undocumented-report.md +528 -270
- package/reference/formats.md +0 -694
- package/reference/http-model.md +0 -1716
- package/reference/streams.md +0 -989
- package/ringbuffer.md +0 -249
- /package/reference/{json-differ.md → schema/built-in-codecs/json/json-differ.md} +0 -0
- /package/reference/{dynamic-value.md → schema/dynamic-value.md} +0 -0
- /package/reference/{lazy.md → schema/lazy.md} +0 -0
- /package/reference/{reflect.md → schema/reflect.md} +0 -0
- /package/reference/{registers.md → schema/registers.md} +0 -0
- /package/reference/{schema-error.md → schema/schema-error.md} +0 -0
- /package/reference/{syntax.md → schema/syntax.md} +0 -0
- /package/reference/{validation.md → schema/validation.md} +0 -0
|
@@ -0,0 +1,460 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: config-decoder
|
|
3
|
+
title: "ConfigDecoder"
|
|
4
|
+
sidebar_label: "ConfigDecoder"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
`ConfigDecoder[A]` reads a value of type `A` out of a `ConfigSource`, accumulating every failure instead of stopping at the first one. Instances are derived from `Schema[A]` by `ConfigDecoderDeriver`, which defines how records, variants, sequences, maps, and primitives map onto dot-separated keys. The trait and its derivation entry point:
|
|
8
|
+
|
|
9
|
+
```scala
|
|
10
|
+
trait ConfigDecoder[A] {
|
|
11
|
+
def decode(source: ConfigSource, prefix: String): Either[::[ConfigError], A]
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
object ConfigDecoder {
|
|
15
|
+
def apply[A](implicit decoder: ConfigDecoder[A]): ConfigDecoder[A]
|
|
16
|
+
def derive[A](implicit schema: Schema[A]): ConfigDecoder[A]
|
|
17
|
+
}
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Motivation
|
|
21
|
+
|
|
22
|
+
Writing a config decoder by hand means writing the same code once per field: look up a key, parse the string, handle the missing case, collect the error. For a twenty-field config that is twenty near-identical blocks that drift out of sync with the case class the moment someone adds a field.
|
|
23
|
+
|
|
24
|
+
`Schema[A]` already knows the field names, the types, and the defaults. `ConfigDecoderDeriver` turns that knowledge into the lookup-and-parse loop, so adding a field to a case class adds a key to the config with no other change. The derivation is a runtime walk over the schema, which means it is also introspectable: the same `Deriver` machinery lets you override a single type's decoding without rewriting the rest.
|
|
25
|
+
|
|
26
|
+
The `prefix` parameter exists so decoders compose. A record decoder calls its field decoders with `s"$prefix.$fieldName"`, and the recursion bottoms out at primitives that look up exactly one key.
|
|
27
|
+
|
|
28
|
+
## Deriving a Decoder
|
|
29
|
+
|
|
30
|
+
`ConfigDecoder.derive` builds a decoder from an implicit `Schema[A]` using the default deriver:
|
|
31
|
+
|
|
32
|
+
```scala
|
|
33
|
+
import zio.blocks.config._
|
|
34
|
+
import zio.blocks.schema.Schema
|
|
35
|
+
|
|
36
|
+
case class Db(host: String, port: Int)
|
|
37
|
+
|
|
38
|
+
object Db {
|
|
39
|
+
implicit val schema: Schema[Db] = Schema.derived[Db]
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
val decoder = ConfigDecoder.derive[Db]
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Decoding with an empty prefix treats field names as top-level keys:
|
|
46
|
+
|
|
47
|
+
```scala
|
|
48
|
+
decoder.decode(ConfigSource.fromMap(Map("host" -> "localhost", "port" -> "5432")), "")
|
|
49
|
+
// res0: Either[::[ConfigError], Db] = Right(
|
|
50
|
+
// Db(host = "localhost", port = 5432)
|
|
51
|
+
// )
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Passing a non-empty prefix roots the same decoder under a subtree, which is how one decoder serves both a standalone config file and a section of a larger one:
|
|
55
|
+
|
|
56
|
+
```scala
|
|
57
|
+
decoder.decode(ConfigSource.fromMap(Map("db.host" -> "localhost", "db.port" -> "5432")), "db")
|
|
58
|
+
// res1: Either[::[ConfigError], Db] = Right(
|
|
59
|
+
// Db(host = "localhost", port = 5432)
|
|
60
|
+
// )
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
`Config.load[A]` is a thin wrapper over these two lines. The difference matters for repeated loads: `Config.load` derives a fresh decoder on every call, while a decoder you hold onto is derived once.
|
|
64
|
+
|
|
65
|
+
:::tip[Derive once for repeated loads]
|
|
66
|
+
Derivation walks the entire schema and allocates a decoder per field. For a config reloaded on a timer, or decoded per request, call `ConfigDecoder.derive[A]` at startup and reuse the instance.
|
|
67
|
+
:::
|
|
68
|
+
|
|
69
|
+
## Mapping Rules
|
|
70
|
+
|
|
71
|
+
The deriver has one rule per schema shape. Together they determine every key your config file needs, so this section is the reference for "what key does field X read?".
|
|
72
|
+
|
|
73
|
+
### Records
|
|
74
|
+
|
|
75
|
+
A record's fields become keys under the record's prefix, joined with dots. Nesting composes: a field whose type is itself a record contributes another path segment:
|
|
76
|
+
|
|
77
|
+
```scala
|
|
78
|
+
import zio.blocks.config._
|
|
79
|
+
import zio.blocks.schema.Schema
|
|
80
|
+
|
|
81
|
+
case class Db(host: String, port: Int)
|
|
82
|
+
|
|
83
|
+
object Db {
|
|
84
|
+
implicit val schema: Schema[Db] = Schema.derived[Db]
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
case class Http(host: String, port: Int)
|
|
88
|
+
|
|
89
|
+
object Http {
|
|
90
|
+
implicit val schema: Schema[Http] = Schema.derived[Http]
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
case class App(db: Db, http: Http)
|
|
94
|
+
|
|
95
|
+
object App {
|
|
96
|
+
implicit val schema: Schema[App] = Schema.derived[App]
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Four keys, two per nested record, all reachable from one top-level decode:
|
|
101
|
+
|
|
102
|
+
```scala
|
|
103
|
+
Config.load[App](
|
|
104
|
+
ConfigSource.fromMap(
|
|
105
|
+
Map(
|
|
106
|
+
"db.host" -> "dbhost",
|
|
107
|
+
"db.port" -> "5432",
|
|
108
|
+
"http.host" -> "0.0.0.0",
|
|
109
|
+
"http.port" -> "8080"
|
|
110
|
+
)
|
|
111
|
+
)
|
|
112
|
+
)
|
|
113
|
+
// res3: Either[::[ConfigError], App] = Right(
|
|
114
|
+
// App(
|
|
115
|
+
// db = Db(host = "dbhost", port = 5432),
|
|
116
|
+
// http = Http(host = "0.0.0.0", port = 8080)
|
|
117
|
+
// )
|
|
118
|
+
// )
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
The deriver uses field names verbatim, in the casing the Scala source declares. To read a differently-cased namespace, wrap the source with `ConfigSource#keyFormat` rather than renaming fields — see [ConfigSource](./config-source.md).
|
|
122
|
+
|
|
123
|
+
### Primitives
|
|
124
|
+
|
|
125
|
+
A primitive field looks up exactly one key and parses the string. Parsing rules by type:
|
|
126
|
+
|
|
127
|
+
| Schema type | Accepted form | Example |
|
|
128
|
+
| -------------------------------------------------------- | ---------------------------------------------- | -------------- |
|
|
129
|
+
| `Boolean` | `true`/`false`, `1`/`0`, `yes`/`no`, `on`/`off` (any case) | `on` |
|
|
130
|
+
| `Byte`, `Short`, `Int`, `Long` | Decimal integer literal | `5432` |
|
|
131
|
+
| `Float`, `Double` | Decimal or scientific literal | `0.75` |
|
|
132
|
+
| `Char` | Exactly one character | `x` |
|
|
133
|
+
| `String` | Any string, taken as-is | `localhost` |
|
|
134
|
+
| `BigInt`, `BigDecimal` | Arbitrary-precision numeric literal | `1e40` |
|
|
135
|
+
| `UUID` | Canonical 36-character form | `f81d4fae-…` |
|
|
136
|
+
| `java.time.Duration` | ISO-8601 duration | `PT30S` |
|
|
137
|
+
| `Instant`, `LocalDate`, `LocalDateTime`, `LocalTime` | ISO-8601 | `2026-08-21` |
|
|
138
|
+
| `OffsetDateTime`, `OffsetTime`, `ZonedDateTime` | ISO-8601 with offset or zone | `2026-08-21T00:00:00Z` |
|
|
139
|
+
| `ZoneId`, `ZoneOffset` | Zone name or offset | `Europe/Berlin` |
|
|
140
|
+
| `Period`, `Year`, `YearMonth`, `MonthDay` | ISO-8601 | `P1M` |
|
|
141
|
+
| `Month`, `DayOfWeek` | Enum name, uppercased before lookup | `monday` |
|
|
142
|
+
| `Currency` | ISO 4217 code | `EUR` |
|
|
143
|
+
| `Unit` | Any value; the key must exist | `()` |
|
|
144
|
+
|
|
145
|
+
A value that fails to parse produces `ConfigError.InvalidValue` naming the path, the offending string, the expected type, and the source:
|
|
146
|
+
|
|
147
|
+
```scala
|
|
148
|
+
import zio.blocks.config._
|
|
149
|
+
import zio.blocks.schema.Schema
|
|
150
|
+
|
|
151
|
+
case class Db(host: String, port: Int)
|
|
152
|
+
|
|
153
|
+
object Db {
|
|
154
|
+
implicit val schema: Schema[Db] = Schema.derived[Db]
|
|
155
|
+
}
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
The underlying parse exception is retained as the error's `cause`:
|
|
159
|
+
|
|
160
|
+
```scala
|
|
161
|
+
Config.load[Db](ConfigSource.fromMap(Map("host" -> "localhost", "port" -> "not-a-number"), "bad"))
|
|
162
|
+
// res5: Either[::[ConfigError], Db] = Left(
|
|
163
|
+
// List(
|
|
164
|
+
// InvalidValue(
|
|
165
|
+
// path = "port",
|
|
166
|
+
// value = "not-a-number",
|
|
167
|
+
// expectedType = "Int",
|
|
168
|
+
// source = "bad",
|
|
169
|
+
// cause = Some(
|
|
170
|
+
// java.lang.NumberFormatException: For input string: "not-a-number"
|
|
171
|
+
// )
|
|
172
|
+
// )
|
|
173
|
+
// )
|
|
174
|
+
// )
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
:::warning[Two different duration formats]
|
|
178
|
+
Config decoding parses `java.time.Duration` with `Duration.parse`, so it requires ISO-8601 (`PT30S`). Flag readers parse `scala.concurrent.duration.FiniteDuration` with a suffix grammar instead (`30s`). The same string is not valid in both places — see [Flags](./flags.md).
|
|
179
|
+
:::
|
|
180
|
+
|
|
181
|
+
### Optional Fields
|
|
182
|
+
|
|
183
|
+
A field of type `Option[A]` decodes the inner `A` at the same key the field would otherwise use — there is no extra path segment for the `Option` itself. A missing key yields `None`:
|
|
184
|
+
|
|
185
|
+
```scala
|
|
186
|
+
import zio.blocks.config._
|
|
187
|
+
import zio.blocks.schema.Schema
|
|
188
|
+
|
|
189
|
+
case class Service(name: String, tag: Option[String])
|
|
190
|
+
|
|
191
|
+
object Service {
|
|
192
|
+
implicit val schema: Schema[Service] = Schema.derived[Service]
|
|
193
|
+
}
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
Supplying the key produces `Some`, and omitting it produces `None`:
|
|
197
|
+
|
|
198
|
+
```scala
|
|
199
|
+
Config.load[Service](ConfigSource.fromMap(Map("name" -> "api", "tag" -> "v2")))
|
|
200
|
+
// res7: Either[::[ConfigError], Service] = Right(
|
|
201
|
+
// Service(name = "api", tag = Some("v2"))
|
|
202
|
+
// )
|
|
203
|
+
Config.load[Service](ConfigSource.fromMap(Map("name" -> "api")))
|
|
204
|
+
// res8: Either[::[ConfigError], Service] = Right(
|
|
205
|
+
// Service(name = "api", tag = None)
|
|
206
|
+
// )
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
The distinction that matters is *missing* versus *invalid*. An optional field absorbs missing keys only. A key that is present but unparseable still fails the whole decode, because silently returning `None` for a typo would hide the mistake.
|
|
210
|
+
|
|
211
|
+
### Default Values
|
|
212
|
+
|
|
213
|
+
A field with a default in the case class falls back to that default when its key is missing, exactly as an `Option` falls back to `None`:
|
|
214
|
+
|
|
215
|
+
```scala
|
|
216
|
+
import zio.blocks.config._
|
|
217
|
+
import zio.blocks.schema.Schema
|
|
218
|
+
|
|
219
|
+
case class Db(host: String, port: Int = 5432)
|
|
220
|
+
|
|
221
|
+
object Db {
|
|
222
|
+
implicit val schema: Schema[Db] = Schema.derived[Db]
|
|
223
|
+
}
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
Omitting `port` uses the declared default rather than reporting a missing key:
|
|
227
|
+
|
|
228
|
+
```scala
|
|
229
|
+
Config.load[Db](ConfigSource.fromMap(Map("host" -> "localhost")))
|
|
230
|
+
// res10: Either[::[ConfigError], Db] = Right(
|
|
231
|
+
// Db(host = "localhost", port = 5432)
|
|
232
|
+
// )
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
As with optional fields, the fallback applies only to missing keys. A present-but-invalid value is an error, not a reason to use the default.
|
|
236
|
+
|
|
237
|
+
### Sequences
|
|
238
|
+
|
|
239
|
+
A sequence is read in one of two shapes. The indexed shape uses one key per element, numbered from zero, and the deriver probes upward until it finds a gap:
|
|
240
|
+
|
|
241
|
+
```scala
|
|
242
|
+
import zio.blocks.config._
|
|
243
|
+
import zio.blocks.schema.Schema
|
|
244
|
+
|
|
245
|
+
case class Hosts(items: List[String])
|
|
246
|
+
|
|
247
|
+
object Hosts {
|
|
248
|
+
implicit val schema: Schema[Hosts] = Schema.derived[Hosts]
|
|
249
|
+
}
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
Indexed keys are what the YAML and JSON adapters produce when they flatten an array:
|
|
253
|
+
|
|
254
|
+
```scala
|
|
255
|
+
Config.load[Hosts](
|
|
256
|
+
ConfigSource.fromMap(Map("items.0" -> "alpha", "items.1" -> "beta", "items.2" -> "gamma"))
|
|
257
|
+
)
|
|
258
|
+
// res12: Either[::[ConfigError], Hosts] = Right(
|
|
259
|
+
// Hosts(List("alpha", "beta", "gamma"))
|
|
260
|
+
// )
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
When no indexed key exists, the deriver falls back to reading the prefix itself as a single comma-separated string, which is what an environment variable can realistically hold:
|
|
264
|
+
|
|
265
|
+
```scala
|
|
266
|
+
Config.load[Hosts](ConfigSource.fromMap(Map("items" -> "alpha, beta, gamma")))
|
|
267
|
+
// res13: Either[::[ConfigError], Hosts] = Right(
|
|
268
|
+
// Hosts(List("alpha", "beta", "gamma"))
|
|
269
|
+
// )
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
Elements are trimmed after splitting, so `"alpha, beta"` and `"alpha,beta"` are equivalent. An element containing a comma cannot be expressed in the flat form; use indexed keys for those.
|
|
273
|
+
|
|
274
|
+
Probing stops at the first index that has neither a value nor any nested keys beneath it, so `items.0` and `items.2` without `items.1` yields a one-element list rather than an error.
|
|
275
|
+
|
|
276
|
+
### Maps
|
|
277
|
+
|
|
278
|
+
A map's keys are discovered by enumeration: the deriver calls `ConfigSource#all` on the prefix and takes the first path segment after it as a map key. Values decode at `prefix.key`:
|
|
279
|
+
|
|
280
|
+
```scala
|
|
281
|
+
import zio.blocks.config._
|
|
282
|
+
import zio.blocks.schema.Schema
|
|
283
|
+
|
|
284
|
+
case class Limits(counts: Map[String, Int])
|
|
285
|
+
|
|
286
|
+
object Limits {
|
|
287
|
+
implicit val schema: Schema[Limits] = Schema.derived[Limits]
|
|
288
|
+
}
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
Each distinct segment under `counts` becomes one entry:
|
|
292
|
+
|
|
293
|
+
```scala
|
|
294
|
+
Config.load[Limits](ConfigSource.fromMap(Map("counts.read" -> "100", "counts.write" -> "20")))
|
|
295
|
+
// res15: Either[::[ConfigError], Limits] = Right(
|
|
296
|
+
// Limits(Map("read" -> 100, "write" -> 20))
|
|
297
|
+
// )
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
Because discovery goes through `ConfigSource#all`, a map cannot be decoded from a source that does not enumerate. Map keys themselves are decoded through the key type's decoder, so a `Map[Int, String]` requires segments that parse as integers.
|
|
301
|
+
|
|
302
|
+
### Sealed Traits
|
|
303
|
+
|
|
304
|
+
A sealed trait is decoded by reading a discriminator key that names which case to use. The default key is `type`, read at `prefix.type`:
|
|
305
|
+
|
|
306
|
+
```scala
|
|
307
|
+
import zio.blocks.config._
|
|
308
|
+
import zio.blocks.schema.Schema
|
|
309
|
+
|
|
310
|
+
sealed trait Backend
|
|
311
|
+
|
|
312
|
+
object Backend {
|
|
313
|
+
case class Postgres(host: String, port: Int) extends Backend
|
|
314
|
+
case class Sqlite(path: String) extends Backend
|
|
315
|
+
|
|
316
|
+
implicit val schema: Schema[Backend] = Schema.derived[Backend]
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
case class Store(backend: Backend)
|
|
320
|
+
|
|
321
|
+
object Store {
|
|
322
|
+
implicit val schema: Schema[Store] = Schema.derived[Store]
|
|
323
|
+
}
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
The discriminator selects the case, and the remaining keys are read at the same prefix as the case's own fields:
|
|
327
|
+
|
|
328
|
+
```scala
|
|
329
|
+
Config.load[Store](
|
|
330
|
+
ConfigSource.fromMap(
|
|
331
|
+
Map("backend.type" -> "Postgres", "backend.host" -> "localhost", "backend.port" -> "5432")
|
|
332
|
+
)
|
|
333
|
+
)
|
|
334
|
+
// res17: Either[::[ConfigError], Store] = Right(
|
|
335
|
+
// Store(Postgres(host = "localhost", port = 5432))
|
|
336
|
+
// )
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
A missing discriminator produces `ConfigError.MissingDiscriminatorKey`, which names the path and the key it expected:
|
|
340
|
+
|
|
341
|
+
```scala
|
|
342
|
+
Config.load[Store](ConfigSource.fromMap(Map("backend.host" -> "localhost")))
|
|
343
|
+
// res18: Either[::[ConfigError], Store] = Left(
|
|
344
|
+
// List(MissingDiscriminatorKey(path = "backend", key = "type"))
|
|
345
|
+
// )
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
An unrecognized discriminator produces `ConfigError.UnknownDiscriminator`, which lists every accepted value in sorted order — the error tells the reader what to write instead:
|
|
349
|
+
|
|
350
|
+
```scala
|
|
351
|
+
Config.load[Store](ConfigSource.fromMap(Map("backend.type" -> "MySql")))
|
|
352
|
+
// res19: Either[::[ConfigError], Store] = Left(
|
|
353
|
+
// List(
|
|
354
|
+
// UnknownDiscriminator(
|
|
355
|
+
// path = "backend.type",
|
|
356
|
+
// found = "MySql",
|
|
357
|
+
// expected = List("Postgres", "Sqlite")
|
|
358
|
+
// )
|
|
359
|
+
// )
|
|
360
|
+
// )
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
Case names are matched exactly as the schema reports them, which for a plain sealed trait is the simple class name.
|
|
364
|
+
|
|
365
|
+
### Wrappers
|
|
366
|
+
|
|
367
|
+
A wrapper type — one whose schema is a `Binding.Wrapper`, such as a newtype over `String` — decodes its underlying representation and then applies the wrapping function. If wrapping throws, the failure is reported as `ConfigError.InvalidValue` at the wrapper's path, with the thrown exception as the cause. This is how validating newtypes surface their validation failures as ordinary config errors.
|
|
368
|
+
|
|
369
|
+
### Dynamic Values
|
|
370
|
+
|
|
371
|
+
A `DynamicValue` field is not parsed at all. A present key becomes a `DynamicValue.Primitive` holding the raw string, and an absent key becomes `DynamicValue.Null` rather than an error. Use it for config sections whose shape is not known at compile time.
|
|
372
|
+
|
|
373
|
+
## Customizing Derivation
|
|
374
|
+
|
|
375
|
+
`ConfigDecoderDeriver` is an ordinary `Deriver[ConfigDecoder]`, so the schema layer's override mechanisms apply. Two customizations are specific to config decoding.
|
|
376
|
+
|
|
377
|
+
### Choosing a Discriminator Key
|
|
378
|
+
|
|
379
|
+
`ConfigDecoderDeriver#discriminator` returns a new deriver that reads a different discriminator key. Pass the deriver to `Config.load` to use it:
|
|
380
|
+
|
|
381
|
+
```scala
|
|
382
|
+
import zio.blocks.config._
|
|
383
|
+
import zio.blocks.schema.Schema
|
|
384
|
+
|
|
385
|
+
sealed trait Backend
|
|
386
|
+
|
|
387
|
+
object Backend {
|
|
388
|
+
case class Postgres(host: String, port: Int) extends Backend
|
|
389
|
+
case class Sqlite(path: String) extends Backend
|
|
390
|
+
|
|
391
|
+
implicit val schema: Schema[Backend] = Schema.derived[Backend]
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
case class Store(backend: Backend)
|
|
395
|
+
|
|
396
|
+
object Store {
|
|
397
|
+
implicit val schema: Schema[Store] = Schema.derived[Store]
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
val kindDeriver = ConfigDecoderDeriver.discriminator("kind")
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
The same config now selects its case from `backend.kind`:
|
|
404
|
+
|
|
405
|
+
```scala
|
|
406
|
+
Config.load[Store](
|
|
407
|
+
ConfigSource.fromMap(Map("backend.kind" -> "Sqlite", "backend.path" -> "/tmp/db.sqlite")),
|
|
408
|
+
kindDeriver
|
|
409
|
+
)
|
|
410
|
+
// res21: Either[::[ConfigError], Store] = Right(
|
|
411
|
+
// Store(Sqlite("/tmp/db.sqlite"))
|
|
412
|
+
// )
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
`ConfigDecoderDeriver` — the object — is the default instance, equivalent to `new ConfigDecoderDeriver("type")`.
|
|
416
|
+
|
|
417
|
+
### Overriding a Single Type
|
|
418
|
+
|
|
419
|
+
To change how one type decodes while leaving everything else alone, build the decoder through the schema's `deriving` API and supply an instance override. This is the standard `Deriver` workflow described in the schema module's derivation documentation; the only config-specific part is that the type class being overridden is `ConfigDecoder`.
|
|
420
|
+
|
|
421
|
+
## Error Accumulation
|
|
422
|
+
|
|
423
|
+
Decoding does not stop at the first failure. A record decoder attempts every field, collects the errors, and only then decides the outcome. The exact shape of the `Left` depends on how many errors it finds: a single error is returned on its own, and two or more are wrapped in one `ConfigError.Composite`.
|
|
424
|
+
|
|
425
|
+
That wrapping happens per record, so a nested config produces nested composites — one per record that had more than one failing field:
|
|
426
|
+
|
|
427
|
+
```scala
|
|
428
|
+
import zio.blocks.config._
|
|
429
|
+
import zio.blocks.schema.Schema
|
|
430
|
+
|
|
431
|
+
case class Db(host: String, port: Int)
|
|
432
|
+
|
|
433
|
+
object Db {
|
|
434
|
+
implicit val schema: Schema[Db] = Schema.derived[Db]
|
|
435
|
+
}
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
An empty source fails both fields, so both errors arrive together:
|
|
439
|
+
|
|
440
|
+
```scala
|
|
441
|
+
Config.load[Db](ConfigSource.fromMap(Map.empty[String, String], "empty"))
|
|
442
|
+
// res23: Either[::[ConfigError], Db] = Left(
|
|
443
|
+
// List(
|
|
444
|
+
// Composite(
|
|
445
|
+
// List(
|
|
446
|
+
// MissingKey(path = "host", source = "empty"),
|
|
447
|
+
// MissingKey(path = "port", source = "empty")
|
|
448
|
+
// )
|
|
449
|
+
// )
|
|
450
|
+
// )
|
|
451
|
+
// )
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
`ConfigError.Composite#message` joins the child messages with newlines, and `Config.loadOrThrow` formats them into a numbered report. See [Errors](./errors.md) for the full error model.
|
|
455
|
+
|
|
456
|
+
## Integration Points
|
|
457
|
+
|
|
458
|
+
`ConfigDecoder` sits between the schema module and the source layer. It depends on `Schema[A]` and the `Deriver` machinery from `zio-blocks-schema`, reads through `ConfigSource`, and reports failures as `ConfigError`. `Config` is the user-facing wrapper, and `Config.wire` places a derived decoder inside a `zio-blocks-scope` dependency graph.
|
|
459
|
+
|
|
460
|
+
See [ConfigSource](./config-source.md) for building the sources a decoder reads from, and [Errors](./errors.md) for handling what it returns.
|