@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,489 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: config-source
|
|
3
|
+
title: "ConfigSource"
|
|
4
|
+
sidebar_label: "ConfigSource"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
`ConfigSource` is a flat, string-keyed namespace of configuration values with dot-separated paths. It answers single-key lookups, enumerates keys under a prefix, and pairs every value it returns with a `Provenance` recording where that value came from. Supporting types: `SourceValue`, `Provenance`, `ProvenanceMap`, `KeyMapper`, `KeyFormat`, `Secret`. The reading and composition surface:
|
|
8
|
+
|
|
9
|
+
```scala
|
|
10
|
+
trait ConfigSource extends FlagSource {
|
|
11
|
+
def sourceId: String
|
|
12
|
+
def get(key: String): Maybe[SourceValue[String]]
|
|
13
|
+
def all(prefix: String): Map[String, SourceValue[String]]
|
|
14
|
+
|
|
15
|
+
final def orElse(fallback: ConfigSource): ConfigSource
|
|
16
|
+
final def prefix(prefix: String): ConfigSource
|
|
17
|
+
final def keyMapper(mapper: KeyMapper, targetFormat: KeyFormat): ConfigSource
|
|
18
|
+
final def keyFormat(format: KeyFormat): ConfigSource
|
|
19
|
+
}
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Motivation
|
|
23
|
+
|
|
24
|
+
Configuration arrives as strings, from places that disagree about structure. Environment variables are flat and uppercase. YAML is nested. HOCON has substitutions. A decoder that had to understand all three would be three decoders.
|
|
25
|
+
|
|
26
|
+
`ConfigSource` is the one shape they all reduce to: a map from dotted path to string. Nested documents flatten into it, environment variables translate into it, and in-memory maps are already it. Because the shape is uniform, everything built above it — decoding, composition, provenance, key renaming — is written once.
|
|
27
|
+
|
|
28
|
+
The trait extends `FlagSource`, which is the same idea minus prefix enumeration. That inheritance is what lets a single source object serve both typed configuration and feature flags.
|
|
29
|
+
|
|
30
|
+
## Construction
|
|
31
|
+
|
|
32
|
+
Sources come from three places in the core module — a map, the environment, and system properties — plus one constructor per file format.
|
|
33
|
+
|
|
34
|
+
### From a Map
|
|
35
|
+
|
|
36
|
+
`ConfigSource.fromMap` wraps a `Map[String, String]`, optionally with an identifier that shows up in provenance and error messages:
|
|
37
|
+
|
|
38
|
+
```scala
|
|
39
|
+
import zio.blocks.config._
|
|
40
|
+
|
|
41
|
+
val source = ConfigSource.fromMap(
|
|
42
|
+
Map("db.host" -> "localhost", "db.port" -> "5432"),
|
|
43
|
+
"defaults"
|
|
44
|
+
)
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Looking up a key returns the value wrapped in a `SourceValue`, which carries the provenance alongside the string:
|
|
48
|
+
|
|
49
|
+
```scala
|
|
50
|
+
source.get("db.host")
|
|
51
|
+
// res0: Maybe[SourceValue[String]] = SourceValue(
|
|
52
|
+
// value = "localhost",
|
|
53
|
+
// provenance = Resolved(
|
|
54
|
+
// sourceId = "defaults",
|
|
55
|
+
// key = "db.host",
|
|
56
|
+
// rawValue = "localhost"
|
|
57
|
+
// )
|
|
58
|
+
// )
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
An absent key yields `Maybe.absent` rather than throwing or returning `null`:
|
|
62
|
+
|
|
63
|
+
```scala
|
|
64
|
+
source.get("db.user")
|
|
65
|
+
// res1: Maybe[SourceValue[String]] = zio.blocks.maybe.Absent$@16d4dcbe
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
`ConfigSource.MapSource` is the underlying case class, so you can pattern match on it or construct it directly when you want the concrete type rather than the trait.
|
|
69
|
+
|
|
70
|
+
### From the Environment
|
|
71
|
+
|
|
72
|
+
`EnvSource` reads environment variables. It performs its own key translation: a dotted lookup becomes an upper-snake environment variable name, so `get("db.host")` reads `DB_HOST`:
|
|
73
|
+
|
|
74
|
+
```scala
|
|
75
|
+
import zio.blocks.config._
|
|
76
|
+
|
|
77
|
+
EnvSource.get("db.host") // reads DB_HOST
|
|
78
|
+
EnvSource.all("db") // every DB_* variable, keys mapped back to dotted form
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
An unset variable is absent. An empty string is present — `FOO=""` resolves to `Some("")`, not a missing key, which matters when you use an empty value to mean "explicitly disabled".
|
|
82
|
+
|
|
83
|
+
`SysPropSource` reads JVM system properties using the dotted path directly, with no translation.
|
|
84
|
+
|
|
85
|
+
:::info[Scala.js behavior]
|
|
86
|
+
On Scala.js, `EnvSource` reads `process.env` when it is available and returns absent for everything when it is not. `SysPropSource` always returns empty results, since JS has no system properties.
|
|
87
|
+
:::
|
|
88
|
+
|
|
89
|
+
### From a File Format
|
|
90
|
+
|
|
91
|
+
The YAML, JSON, and HOCON adapters each add a constructor to the `ConfigSource` companion via an implicit class, so importing the adapter package makes `ConfigSource.fromYaml`, `ConfigSource.fromJson`, or `ConfigSource.fromHocon` available. Because parsing can fail, those constructors return `Either[ConfigError, ConfigSource]`. See [File Formats](./formats.md).
|
|
92
|
+
|
|
93
|
+
## Lookup Operations
|
|
94
|
+
|
|
95
|
+
Two methods make up the reading surface: one resolves a single key, the other enumerates a subtree.
|
|
96
|
+
|
|
97
|
+
### ConfigSource#get
|
|
98
|
+
|
|
99
|
+
`ConfigSource#get` takes a full dotted path and returns `Maybe[SourceValue[String]]`. It never partially matches: `get("db")` on a source containing only `db.host` is absent, because `db` itself has no value.
|
|
100
|
+
|
|
101
|
+
### ConfigSource#all
|
|
102
|
+
|
|
103
|
+
`ConfigSource#all` returns every entry whose key equals the prefix or begins with the prefix followed by a dot. Passing an empty prefix returns everything:
|
|
104
|
+
|
|
105
|
+
```scala
|
|
106
|
+
source.all("db")
|
|
107
|
+
// res3: Map[String, SourceValue[String]] = Map(
|
|
108
|
+
// "db.host" -> SourceValue(
|
|
109
|
+
// value = "localhost",
|
|
110
|
+
// provenance = Resolved(
|
|
111
|
+
// sourceId = "defaults",
|
|
112
|
+
// key = "db.host",
|
|
113
|
+
// rawValue = "localhost"
|
|
114
|
+
// )
|
|
115
|
+
// ),
|
|
116
|
+
// "db.port" -> SourceValue(
|
|
117
|
+
// value = "5432",
|
|
118
|
+
// provenance = Resolved(
|
|
119
|
+
// sourceId = "defaults",
|
|
120
|
+
// key = "db.port",
|
|
121
|
+
// rawValue = "5432"
|
|
122
|
+
// )
|
|
123
|
+
// )
|
|
124
|
+
// )
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
The decoder uses `ConfigSource#all` to discover the shape of collections — how many elements a sequence has, which keys a map contains — so a source that cannot enumerate cannot decode those types.
|
|
128
|
+
|
|
129
|
+
## Composition
|
|
130
|
+
|
|
131
|
+
Real deployments layer configuration: defaults from a file, overrides from the environment, and a subtree per component. Two operations cover both needs, and both are `final` so every source gets them.
|
|
132
|
+
|
|
133
|
+
### ConfigSource#orElse
|
|
134
|
+
|
|
135
|
+
`ConfigSource#orElse` consults the receiver first and falls back to the argument. Provenance records whichever source actually answered, so layering does not obscure the origin:
|
|
136
|
+
|
|
137
|
+
```scala
|
|
138
|
+
import zio.blocks.config._
|
|
139
|
+
|
|
140
|
+
val defaults = ConfigSource.fromMap(Map("host" -> "localhost", "port" -> "5432"), "defaults")
|
|
141
|
+
val envOverrides = ConfigSource.fromMap(Map("host" -> "db.prod.internal"), "env")
|
|
142
|
+
|
|
143
|
+
val layered = defaults.orElse(envOverrides)
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
The receiver wins on conflict, which means the *first* source listed has priority — write the highest-priority source on the left:
|
|
147
|
+
|
|
148
|
+
```scala
|
|
149
|
+
layered.get("host")
|
|
150
|
+
// res5: Maybe[SourceValue[String]] = SourceValue(
|
|
151
|
+
// value = "localhost",
|
|
152
|
+
// provenance = Resolved(
|
|
153
|
+
// sourceId = "defaults",
|
|
154
|
+
// key = "host",
|
|
155
|
+
// rawValue = "localhost"
|
|
156
|
+
// )
|
|
157
|
+
// )
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Keys only the fallback provides still resolve, and their provenance names the fallback:
|
|
161
|
+
|
|
162
|
+
```scala
|
|
163
|
+
layered.get("port")
|
|
164
|
+
// res6: Maybe[SourceValue[String]] = SourceValue(
|
|
165
|
+
// value = "5432",
|
|
166
|
+
// provenance = Resolved(sourceId = "defaults", key = "port", rawValue = "5432")
|
|
167
|
+
// )
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
The composed source's `sourceId` is the two ids joined with a pipe, which is what you see in error messages when a key is missing from both:
|
|
171
|
+
|
|
172
|
+
```scala
|
|
173
|
+
layered.sourceId
|
|
174
|
+
// res7: String = "defaults|env"
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
`ConfigSource#all` on a composed source merges both key sets, with the receiver's entries overwriting the fallback's on collision. That merge is why enumeration-driven decoding — sequences and maps — behaves the same on a layered source as on a single one.
|
|
178
|
+
|
|
179
|
+
### ConfigSource#prefix
|
|
180
|
+
|
|
181
|
+
`ConfigSource#prefix` re-roots a source so that lookups are relative to a subtree. A source prefixed with `db` turns `get("host")` into `get("db.host")` against the underlying source:
|
|
182
|
+
|
|
183
|
+
```scala
|
|
184
|
+
import zio.blocks.config._
|
|
185
|
+
|
|
186
|
+
val full = ConfigSource.fromMap(
|
|
187
|
+
Map("db.host" -> "localhost", "db.port" -> "5432", "http.port" -> "8080"),
|
|
188
|
+
"app"
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
val db = full.prefix("db")
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
Relative lookups resolve against the composed key, so the caller never spells the prefix again:
|
|
195
|
+
|
|
196
|
+
```scala
|
|
197
|
+
db.get("host")
|
|
198
|
+
// res9: Maybe[SourceValue[String]] = SourceValue(
|
|
199
|
+
// value = "localhost",
|
|
200
|
+
// provenance = Resolved(
|
|
201
|
+
// sourceId = "app",
|
|
202
|
+
// key = "db.host",
|
|
203
|
+
// rawValue = "localhost"
|
|
204
|
+
// )
|
|
205
|
+
// )
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
Enumeration strips the prefix back off the returned keys, which keeps the view consistent — what `ConfigSource#get` resolves is what `ConfigSource#all` reports:
|
|
209
|
+
|
|
210
|
+
```scala
|
|
211
|
+
db.all("")
|
|
212
|
+
// res10: Map[String, SourceValue[String]] = Map(
|
|
213
|
+
// "host" -> SourceValue(
|
|
214
|
+
// value = "localhost",
|
|
215
|
+
// provenance = Resolved(
|
|
216
|
+
// sourceId = "app",
|
|
217
|
+
// key = "db.host",
|
|
218
|
+
// rawValue = "localhost"
|
|
219
|
+
// )
|
|
220
|
+
// ),
|
|
221
|
+
// "port" -> SourceValue(
|
|
222
|
+
// value = "5432",
|
|
223
|
+
// provenance = Resolved(sourceId = "app", key = "db.port", rawValue = "5432")
|
|
224
|
+
// )
|
|
225
|
+
// )
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
Prefixing preserves `sourceId`, since re-rooting does not change where values come from. This is the mechanism behind `Config.wire[A](prefix)`: one injected source, several independently-rooted config sections.
|
|
229
|
+
|
|
230
|
+
## Key Mapping
|
|
231
|
+
|
|
232
|
+
Field names in Scala are camelCase. Environment variables are `UPPER_SNAKE_CASE`. Some YAML files use `kebab-case`. Rather than renaming fields or duplicating keys, a source can translate between the canonical form the decoder asks for and whatever form the source actually uses.
|
|
233
|
+
|
|
234
|
+
### KeyFormat
|
|
235
|
+
|
|
236
|
+
`KeyFormat` enumerates the four supported spellings of a key:
|
|
237
|
+
|
|
238
|
+
| Variant | Example |
|
|
239
|
+
| ------------------------- | -------------- |
|
|
240
|
+
| `KeyFormat.CamelCase` | `databaseUrl` |
|
|
241
|
+
| `KeyFormat.SnakeCase` | `database_url` |
|
|
242
|
+
| `KeyFormat.KebabCase` | `database-url` |
|
|
243
|
+
| `KeyFormat.UpperSnakeCase`| `DATABASE_URL` |
|
|
244
|
+
|
|
245
|
+
### KeyMapper
|
|
246
|
+
|
|
247
|
+
`KeyMapper` converts in both directions: `KeyMapper#toCanonical` normalizes a source-facing key into lower camelCase, and `KeyMapper#fromCanonical` renders a canonical key into a requested `KeyFormat`:
|
|
248
|
+
|
|
249
|
+
```scala
|
|
250
|
+
import zio.blocks.config._
|
|
251
|
+
|
|
252
|
+
val mapper = KeyMapper.default
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
The default mapper treats snake_case and kebab-case as equivalent inputs, collapsing both to camelCase:
|
|
256
|
+
|
|
257
|
+
```scala
|
|
258
|
+
mapper.toCanonical("database_url")
|
|
259
|
+
// res12: String = "databaseUrl"
|
|
260
|
+
mapper.toCanonical("database-url")
|
|
261
|
+
// res13: String = "databaseUrl"
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
Rendering goes the other way, one output per format:
|
|
265
|
+
|
|
266
|
+
```scala
|
|
267
|
+
mapper.fromCanonical("databaseUrl", KeyFormat.UpperSnakeCase)
|
|
268
|
+
// res14: String = "DATABASE_URL"
|
|
269
|
+
mapper.fromCanonical("databaseUrl", KeyFormat.KebabCase)
|
|
270
|
+
// res15: String = "database-url"
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
A key containing neither separator passes through `KeyMapper#toCanonical` unchanged, so already-canonical keys cost nothing.
|
|
274
|
+
|
|
275
|
+
### Applying a Mapper to a Source
|
|
276
|
+
|
|
277
|
+
`ConfigSource#keyFormat` wraps a source so that every lookup is rendered into the given format before it reaches the underlying source. Ask for `databaseUrl`, and the wrapped source looks up `DATABASE_URL`:
|
|
278
|
+
|
|
279
|
+
```scala
|
|
280
|
+
import zio.blocks.config._
|
|
281
|
+
|
|
282
|
+
val upperSnake = ConfigSource.fromMap(Map("DATABASE_URL" -> "postgres://localhost"), "env-style")
|
|
283
|
+
val canonical = upperSnake.keyFormat(KeyFormat.UpperSnakeCase)
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
The decoder — which only ever asks for camelCase field names — now resolves against an upper-snake namespace:
|
|
287
|
+
|
|
288
|
+
```scala
|
|
289
|
+
canonical.get("databaseUrl")
|
|
290
|
+
// res17: Maybe[SourceValue[String]] = SourceValue(
|
|
291
|
+
// value = "postgres://localhost",
|
|
292
|
+
// provenance = Resolved(
|
|
293
|
+
// sourceId = "env-style",
|
|
294
|
+
// key = "DATABASE_URL",
|
|
295
|
+
// rawValue = "postgres://localhost"
|
|
296
|
+
// )
|
|
297
|
+
// )
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
`ConfigSource#keyMapper` is the general form, taking an explicit `KeyMapper` as well as the target format, for sources whose naming convention the default mapper does not cover. Both operations compose with `ConfigSource#orElse` and `ConfigSource#prefix`.
|
|
301
|
+
|
|
302
|
+
:::note[EnvSource already maps keys]
|
|
303
|
+
`EnvSource` performs dot-to-underscore uppercasing internally, so it does not need `ConfigSource#keyFormat`. Use `ConfigSource#keyFormat` for sources that hold environment-style keys without being the environment — a `Map` scraped from an env file, for instance.
|
|
304
|
+
:::
|
|
305
|
+
|
|
306
|
+
## Provenance
|
|
307
|
+
|
|
308
|
+
Every value a source returns is wrapped with a record of its origin. That record survives composition, prefixing, and key mapping, which is what makes "where did this value come from?" answerable after the fact rather than only at the point of lookup.
|
|
309
|
+
|
|
310
|
+
### SourceValue and Provenance
|
|
311
|
+
|
|
312
|
+
`SourceValue` is the pair, and `Provenance` is the origin:
|
|
313
|
+
|
|
314
|
+
```scala
|
|
315
|
+
final case class SourceValue[A](value: A, provenance: Provenance)
|
|
316
|
+
|
|
317
|
+
sealed trait Provenance {
|
|
318
|
+
def sourceId: String
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
object Provenance {
|
|
322
|
+
final case class Resolved(sourceId: String, key: String, rawValue: Maybe[String]) extends Provenance
|
|
323
|
+
case object Default extends Provenance
|
|
324
|
+
}
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
`Provenance.Resolved` names the source that answered, the source-facing key it answered under, and the raw string. The key matters when mapping is involved: a lookup of `databaseUrl` against an upper-snake source records `DATABASE_URL`, telling you the actual variable to change.
|
|
328
|
+
|
|
329
|
+
`Provenance.Default` marks a value that came from a schema default rather than any source. Its `sourceId` is the constant `"schema-default"`.
|
|
330
|
+
|
|
331
|
+
### ProvenanceMap
|
|
332
|
+
|
|
333
|
+
`ProvenanceMap[A]` pairs a decoded value with the source it was decoded from, which allows per-key queries after the load has already succeeded:
|
|
334
|
+
|
|
335
|
+
```scala
|
|
336
|
+
import zio.blocks.config._
|
|
337
|
+
import zio.blocks.schema.Schema
|
|
338
|
+
|
|
339
|
+
case class Db(host: String, port: Int)
|
|
340
|
+
|
|
341
|
+
object Db {
|
|
342
|
+
implicit val schema: Schema[Db] = Schema.derived[Db]
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
val source = ConfigSource.fromMap(Map("host" -> "localhost", "port" -> "5432"), "startup")
|
|
346
|
+
val loaded = Config.loadWithProvenance[Db](source).toOption.get
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
The decoded value is available directly, so a `ProvenanceMap` can be passed around in place of the raw config:
|
|
350
|
+
|
|
351
|
+
```scala
|
|
352
|
+
loaded.value
|
|
353
|
+
// res19: Db = Db(host = "localhost", port = 5432)
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
`ProvenanceMap#provenanceOf` looks up a single dotted path and returns absent for keys the source does not have:
|
|
357
|
+
|
|
358
|
+
```scala
|
|
359
|
+
loaded.provenanceOf("port")
|
|
360
|
+
// res20: Maybe[Provenance] = Resolved(
|
|
361
|
+
// sourceId = "startup",
|
|
362
|
+
// key = "port",
|
|
363
|
+
// rawValue = "5432"
|
|
364
|
+
// )
|
|
365
|
+
loaded.provenanceOf("nonexistent")
|
|
366
|
+
// res21: Maybe[Provenance] = zio.blocks.maybe.Absent$@16d4dcbe
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
### Dumping Configuration
|
|
370
|
+
|
|
371
|
+
`ProvenanceMap#dump` renders every key visible under a prefix as a box-drawn table of key, value, and source. It is meant for a single startup log line that makes a misconfigured deployment obvious:
|
|
372
|
+
|
|
373
|
+
```scala
|
|
374
|
+
println(loaded.dump())
|
|
375
|
+
// ┌──────┬───────────┬─────────┐
|
|
376
|
+
// │ Key │ Value │ Source │
|
|
377
|
+
// ├──────┼───────────┼─────────┤
|
|
378
|
+
// │ host │ localhost │ startup │
|
|
379
|
+
// │ port │ 5432 │ startup │
|
|
380
|
+
// └──────┴───────────┴─────────┘
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
Values are redacted when the key name looks sensitive. The check lowercases the path, normalizes hyphens to underscores, and looks for any of `secret`, `password`, `passwd`, `token`, `apikey`, `api_key`, `accesskey`, `access_key`, `privatekey`, `private_key`, `credential`, or `credentials` as a substring:
|
|
384
|
+
|
|
385
|
+
```scala
|
|
386
|
+
import zio.blocks.config._
|
|
387
|
+
|
|
388
|
+
val withSecret = ConfigSource.fromMap(
|
|
389
|
+
Map("db.host" -> "localhost", "db.password" -> "hunter2"),
|
|
390
|
+
"startup"
|
|
391
|
+
)
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
The key is still listed — you can see that it was set — but the value is replaced:
|
|
395
|
+
|
|
396
|
+
```scala
|
|
397
|
+
println(ProvenanceMap((), withSecret).dump())
|
|
398
|
+
// ┌─────────────┬───────────┬─────────┐
|
|
399
|
+
// │ Key │ Value │ Source │
|
|
400
|
+
// ├─────────────┼───────────┼─────────┤
|
|
401
|
+
// │ db.host │ localhost │ startup │
|
|
402
|
+
// │ db.password │ <secret> │ startup │
|
|
403
|
+
// └─────────────┴───────────┴─────────┘
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
:::warning[Redaction is name-based only]
|
|
407
|
+
`ProvenanceMap#dump` redacts by key name, not by type. A secret stored under a key like `db.credentials_blob` is redacted; the same secret under `db.blob` is printed in full. For values that must never be printed regardless of key, use `Secret`.
|
|
408
|
+
:::
|
|
409
|
+
|
|
410
|
+
## Secrets
|
|
411
|
+
|
|
412
|
+
`Secret` is a wrapper whose `toString` is always `<secret>`, so a value inside one cannot leak through string interpolation, logging, or a case class `toString`:
|
|
413
|
+
|
|
414
|
+
```scala
|
|
415
|
+
import zio.blocks.config._
|
|
416
|
+
|
|
417
|
+
val token = Secret("s3cr3t-token")
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
Rendering the wrapper reveals nothing, even inside a larger string:
|
|
421
|
+
|
|
422
|
+
```scala
|
|
423
|
+
token.toString
|
|
424
|
+
// res26: String = "<secret>"
|
|
425
|
+
s"token = $token"
|
|
426
|
+
// res27: String = "token = <secret>"
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
`Secret#equals` and `Secret#hashCode` compare the underlying value, so secrets remain usable as map keys and in equality checks:
|
|
430
|
+
|
|
431
|
+
```scala
|
|
432
|
+
token == Secret("s3cr3t-token")
|
|
433
|
+
// res28: Boolean = true
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
Reading the value back requires the explicit `Secret.unwrap`, which makes every access point visible in a code search:
|
|
437
|
+
|
|
438
|
+
```scala
|
|
439
|
+
Secret.unwrap(token)
|
|
440
|
+
// res29: String = "s3cr3t-token"
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
### Displayable
|
|
444
|
+
|
|
445
|
+
`Displayable[A]` is the type class behind rendered flag values, with instances for `String`, `Int`, `Long`, `Double`, and `Boolean`, plus a low-priority fallback that calls `toString`. The module provides `Displayable[Secret]` as an implicit in the `zio.blocks.config` package object, so a `StaticFlag[Secret]` renders as `<secret>` in `Flag.dump` output without any per-flag configuration.
|
|
446
|
+
|
|
447
|
+
To control how a custom type appears in flag dumps, provide your own instance with `Displayable.instance`:
|
|
448
|
+
|
|
449
|
+
```scala
|
|
450
|
+
import zio.blocks.config._
|
|
451
|
+
|
|
452
|
+
final case class Port(value: Int)
|
|
453
|
+
|
|
454
|
+
implicit val portDisplayable: Displayable[Port] = Displayable.instance(p => s"port ${p.value}")
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
## Writing a Custom Source
|
|
458
|
+
|
|
459
|
+
Implementing `ConfigSource` requires three members: an id, a single-key lookup, and prefix enumeration. Constructing `Provenance.Resolved` yourself is what makes the new source participate in provenance tracking:
|
|
460
|
+
|
|
461
|
+
```scala
|
|
462
|
+
import zio.blocks.config._
|
|
463
|
+
import zio.blocks.maybe.Maybe
|
|
464
|
+
|
|
465
|
+
final class UppercaseSource(entries: Map[String, String]) extends ConfigSource {
|
|
466
|
+
val sourceId: String = "uppercase"
|
|
467
|
+
|
|
468
|
+
def get(key: String): Maybe[SourceValue[String]] =
|
|
469
|
+
Maybe.fromOption(
|
|
470
|
+
entries.get(key).map(v => SourceValue(v.toUpperCase, Provenance.Resolved(sourceId, key, Maybe.present(v))))
|
|
471
|
+
)
|
|
472
|
+
|
|
473
|
+
def all(prefix: String): Map[String, SourceValue[String]] = {
|
|
474
|
+
val dotted = if (prefix.isEmpty) "" else s"$prefix."
|
|
475
|
+
entries.collect {
|
|
476
|
+
case (k, v) if prefix.isEmpty || k == prefix || k.startsWith(dotted) =>
|
|
477
|
+
k -> SourceValue(v.toUpperCase, Provenance.Resolved(sourceId, k, Maybe.present(v)))
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
Keep `rawValue` as the original string even when `value` is transformed. Provenance is meant to explain what the source held, not what the source returned.
|
|
484
|
+
|
|
485
|
+
## Integration Points
|
|
486
|
+
|
|
487
|
+
`ConfigSource` is the input to every other part of the module: `ConfigDecoder#decode` takes one, `Config.wire` injects one, and `FlagSource.Registry` accepts one because `ConfigSource` extends `FlagSource`. It depends only on `Maybe` from `zio-blocks-maybe`.
|
|
488
|
+
|
|
489
|
+
See [Config Decoder](./config-decoder.md) for how a source becomes a typed value, [Errors](./errors.md) for what a failed lookup produces, and [File Formats](./formats.md) for the YAML, JSON, and HOCON constructors.
|