@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.
Files changed (215) hide show
  1. package/adr/2026-07-18-data-migration.md +123 -0
  2. package/guides/async-getting-started.md +687 -0
  3. package/guides/compile-time-resource-safety-with-scope.md +21 -16
  4. package/guides/getting-started-with-mux.md +1395 -0
  5. package/guides/query-dsl-extending.md +161 -102
  6. package/guides/query-dsl-fluent-builder.md +217 -157
  7. package/guides/query-dsl-reified-optics.md +12 -10
  8. package/guides/query-dsl-sql.md +640 -165
  9. package/guides/sql-checked-interpolation.md +173 -0
  10. package/guides/sql-transactions.md +286 -0
  11. package/guides/telemetry-guide.md +1130 -0
  12. package/guides/zio-schema-migration.md +29 -22
  13. package/index.md +248 -389
  14. package/package.json +1 -1
  15. package/plans/config-follow-up-prs.md +188 -0
  16. package/plans/config-pr-assessment-roadmap.md +310 -0
  17. package/reference/MuxDataFlow.jsx +250 -0
  18. package/reference/async.md +1499 -0
  19. package/reference/chunk.md +3533 -308
  20. package/reference/codegen/case-class.md +436 -0
  21. package/reference/codegen/emitter-config.md +383 -0
  22. package/reference/codegen/examples.md +664 -0
  23. package/reference/codegen/field.md +316 -0
  24. package/reference/codegen/index.md +317 -0
  25. package/reference/codegen/scala-emitter.md +392 -0
  26. package/reference/codegen/scala-file.md +276 -0
  27. package/reference/codegen/sealed-trait.md +408 -0
  28. package/reference/codegen/type-definition.md +340 -0
  29. package/reference/codegen/type-ref.md +201 -0
  30. package/reference/combinators.md +347 -117
  31. package/reference/config/config-decoder.md +460 -0
  32. package/reference/config/config-source.md +489 -0
  33. package/reference/config/errors.md +278 -0
  34. package/reference/config/flags.md +369 -0
  35. package/reference/config/formats.md +314 -0
  36. package/reference/config/index.md +304 -0
  37. package/reference/config/rollout.md +336 -0
  38. package/reference/context.md +9 -52
  39. package/reference/data-migration.md +269 -0
  40. package/reference/datastar/attributes.md +302 -0
  41. package/reference/datastar/events.md +234 -0
  42. package/reference/datastar/index.md +256 -0
  43. package/reference/datastar/signals.md +230 -0
  44. package/reference/datastar/sse.md +295 -0
  45. package/reference/datastar.md +346 -0
  46. package/reference/docs.md +1461 -345
  47. package/reference/endpoint/auth-type.md +146 -0
  48. package/reference/endpoint/bulk-creation.md +96 -0
  49. package/reference/endpoint/endpoint.md +297 -0
  50. package/reference/endpoint/http-codec.md +249 -0
  51. package/reference/endpoint/index.md +745 -0
  52. package/reference/endpoint/path-codec.md +225 -0
  53. package/reference/endpoint/route-pattern.md +194 -0
  54. package/reference/endpoint/route-tree.md +111 -0
  55. package/reference/endpoint/segment-codec.md +199 -0
  56. package/reference/html.md +1424 -0
  57. package/reference/htmx/attribute-values.md +359 -0
  58. package/reference/htmx/hx-encoding.md +111 -0
  59. package/reference/htmx/hx-params.md +204 -0
  60. package/reference/htmx/hx-swap.md +276 -0
  61. package/reference/htmx/hx-sync.md +251 -0
  62. package/reference/htmx/hx-target.md +314 -0
  63. package/reference/htmx/hx-trigger.md +457 -0
  64. package/reference/htmx/hx-url-update.md +239 -0
  65. package/reference/htmx/index.md +807 -0
  66. package/reference/htmx/response-headers.md +240 -0
  67. package/reference/http-model/headers.md +735 -0
  68. package/reference/http-model/index.md +49 -0
  69. package/reference/http-model/model.md +1517 -0
  70. package/reference/http-model/schema-codecs.md +522 -0
  71. package/reference/http-model/schema.md +750 -0
  72. package/reference/http-model/server-sent-event.md +341 -0
  73. package/reference/jwt.md +195 -0
  74. package/reference/maybe.md +943 -0
  75. package/reference/media-type.md +2 -2
  76. package/reference/mux.md +254 -0
  77. package/reference/mux.mdx +828 -0
  78. package/reference/openapi.md +1351 -0
  79. package/reference/projection.md +654 -0
  80. package/reference/resource-management/defer-handle.md +1 -1
  81. package/reference/resource-management/resource.md +31 -98
  82. package/reference/resource-management/scope.md +28 -220
  83. package/reference/resource-management/wire.md +5 -55
  84. package/reference/ringbuffer/MpmcDiagram.jsx +717 -0
  85. package/reference/ringbuffer/MpscDiagram.jsx +618 -0
  86. package/reference/ringbuffer/SpmcDiagram.jsx +680 -0
  87. package/reference/ringbuffer/SpscDiagram.jsx +677 -0
  88. package/reference/ringbuffer/advanced.mdx +109 -0
  89. package/reference/ringbuffer/index.mdx +145 -0
  90. package/reference/ringbuffer/mpmc.mdx +185 -0
  91. package/reference/ringbuffer/mpsc.mdx +164 -0
  92. package/reference/ringbuffer/spmc.mdx +108 -0
  93. package/reference/ringbuffer/spsc.mdx +416 -0
  94. package/reference/{allows.md → schema/allows.md} +4 -100
  95. package/reference/{binding-resolver.md → schema/binding-resolver.md} +1 -1
  96. package/reference/{binding.md → schema/binding.md} +3 -4
  97. package/reference/schema/built-in-codecs/avro.md +451 -0
  98. package/reference/schema/built-in-codecs/bson.md +510 -0
  99. package/reference/schema/built-in-codecs/csv.md +564 -0
  100. package/reference/schema/built-in-codecs/index.md +77 -0
  101. package/reference/schema/built-in-codecs/json/index.md +295 -0
  102. package/reference/schema/built-in-codecs/json/json-config.md +217 -0
  103. package/reference/{json-patch.md → schema/built-in-codecs/json/json-patch.md} +5 -5
  104. package/reference/{json-schema.md → schema/built-in-codecs/json/json-schema.md} +14 -47
  105. package/reference/schema/built-in-codecs/json/json-selection.md +322 -0
  106. package/reference/{json.md → schema/built-in-codecs/json/json.md} +32 -64
  107. package/reference/schema/built-in-codecs/messagepack.md +508 -0
  108. package/reference/schema/built-in-codecs/thrift.md +433 -0
  109. package/reference/schema/built-in-codecs/toon.md +1078 -0
  110. package/reference/{xml.md → schema/built-in-codecs/xml.md} +13 -9
  111. package/reference/schema/built-in-codecs/yaml.md +552 -0
  112. package/reference/{codec.md → schema/codec.md} +11 -11
  113. package/reference/{dynamic-optic.md → schema/dynamic-optic.md} +196 -5
  114. package/reference/{dynamic-schema.md → schema/dynamic-schema.md} +8 -8
  115. package/reference/schema/format.md +92 -0
  116. package/reference/schema/index.md +52 -0
  117. package/reference/schema/migration.md +297 -0
  118. package/reference/{modifier.md → schema/modifier.md} +58 -7
  119. package/reference/{optics.md → schema/optics.md} +2 -2
  120. package/reference/{patch.md → schema/patch.md} +1 -1
  121. package/{path-interpolator.md → reference/schema/path-interpolator.md} +167 -72
  122. package/reference/schema/reflect-transformer.md +140 -0
  123. package/reference/{schema-evolution → schema/schema-evolution}/as.md +8 -8
  124. package/reference/{schema-evolution → schema/schema-evolution}/index.md +2 -2
  125. package/reference/{schema-evolution → schema/schema-evolution}/into.md +8 -8
  126. package/reference/{schema-expr.md → schema/schema-expr.md} +110 -175
  127. package/reference/schema/schema-search.md +263 -0
  128. package/reference/{schema.md → schema/schema.md} +22 -2
  129. package/reference/{structural-types.md → schema/structural-types.md} +1 -1
  130. package/reference/{type-class-derivation.md → schema/type-class-derivation.md} +63 -1
  131. package/reference/smithy.md +1032 -0
  132. package/reference/sql/db-codec-deriver.md +71 -0
  133. package/reference/sql/db-codec.md +687 -0
  134. package/reference/sql/db-con.md +271 -0
  135. package/reference/sql/db-connection.md +153 -0
  136. package/reference/sql/db-param-writer.md +77 -0
  137. package/reference/sql/db-param.md +66 -0
  138. package/reference/sql/db-result-reader.md +148 -0
  139. package/reference/sql/db-tx.md +114 -0
  140. package/reference/sql/db-value.md +41 -0
  141. package/reference/sql/ddl.md +85 -0
  142. package/reference/sql/frag.md +288 -0
  143. package/reference/sql/index.md +341 -0
  144. package/reference/sql/repo.md +600 -0
  145. package/reference/sql/sql-dialect.md +73 -0
  146. package/reference/sql/sql-logger.md +62 -0
  147. package/reference/sql/sql-name-mapper.md +70 -0
  148. package/reference/sql/table-metadata.md +134 -0
  149. package/reference/sql/table.md +448 -0
  150. package/reference/sql/transactor-zio.md +399 -0
  151. package/reference/sql/transactor.md +363 -0
  152. package/reference/sql-zio.md +112 -0
  153. package/reference/streams/core/index.md +32 -0
  154. package/reference/streams/core/pipeline.md +854 -0
  155. package/reference/streams/core/sink.md +1404 -0
  156. package/reference/streams/core/stream.md +3236 -0
  157. package/reference/streams/execution-and-compatibility/async-execution.md +822 -0
  158. package/reference/streams/execution-and-compatibility/index.md +35 -0
  159. package/reference/streams/execution-and-compatibility/platform-differences.md +297 -0
  160. package/reference/streams/execution-and-compatibility/scala-2-compatibility.md +88 -0
  161. package/reference/streams/execution-and-compatibility/zero-boxing.md +393 -0
  162. package/reference/streams/index.md +726 -0
  163. package/reference/streams/primitives/index.md +30 -0
  164. package/reference/streams/primitives/reader.md +1992 -0
  165. package/reference/streams/primitives/writer.md +1201 -0
  166. package/reference/telemetry/common/any-value.md +90 -0
  167. package/reference/telemetry/common/attribute-key.md +87 -0
  168. package/reference/telemetry/common/attributes.md +118 -0
  169. package/reference/telemetry/common/index.md +39 -0
  170. package/reference/telemetry/common/instrumentation-scope.md +24 -0
  171. package/reference/telemetry/common/resource.md +34 -0
  172. package/reference/telemetry/index.md +311 -0
  173. package/reference/telemetry/logging/index.md +197 -0
  174. package/reference/telemetry/logging/log-enrichment.md +72 -0
  175. package/reference/telemetry/logging/log-formatter.md +100 -0
  176. package/reference/telemetry/logging/log-record-processor.md +56 -0
  177. package/reference/telemetry/logging/log-record.md +44 -0
  178. package/reference/telemetry/logging/log-writer.md +64 -0
  179. package/reference/telemetry/logging/logger-provider.md +142 -0
  180. package/reference/telemetry/logging/logger.md +83 -0
  181. package/reference/telemetry/logging/severity.md +62 -0
  182. package/reference/telemetry/metrics/index.md +150 -0
  183. package/reference/telemetry/metrics/instruments.md +183 -0
  184. package/reference/telemetry/metrics/labeled-instruments.md +74 -0
  185. package/reference/telemetry/metrics/meter-provider.md +76 -0
  186. package/reference/telemetry/metrics/meter.md +98 -0
  187. package/reference/telemetry/metrics/metric-data.md +57 -0
  188. package/reference/telemetry/otel/custom-exporter.md +216 -0
  189. package/reference/telemetry/otel/index.md +212 -0
  190. package/reference/telemetry/tracing/index.md +155 -0
  191. package/reference/telemetry/tracing/sampler.md +89 -0
  192. package/reference/telemetry/tracing/span-builder.md +57 -0
  193. package/reference/telemetry/tracing/span-context.md +39 -0
  194. package/reference/telemetry/tracing/span-data.md +32 -0
  195. package/reference/telemetry/tracing/span-kind.md +55 -0
  196. package/reference/telemetry/tracing/span-processor.md +53 -0
  197. package/reference/telemetry/tracing/span-status.md +47 -0
  198. package/reference/telemetry/tracing/span.md +117 -0
  199. package/reference/telemetry/tracing/tracer-provider.md +91 -0
  200. package/reference/telemetry/tracing/tracer.md +52 -0
  201. package/reference/typeid.md +5 -83
  202. package/sidebars.js +376 -43
  203. package/undocumented-report.md +528 -270
  204. package/reference/formats.md +0 -694
  205. package/reference/http-model.md +0 -1716
  206. package/reference/streams.md +0 -989
  207. package/ringbuffer.md +0 -249
  208. /package/reference/{json-differ.md → schema/built-in-codecs/json/json-differ.md} +0 -0
  209. /package/reference/{dynamic-value.md → schema/dynamic-value.md} +0 -0
  210. /package/reference/{lazy.md → schema/lazy.md} +0 -0
  211. /package/reference/{reflect.md → schema/reflect.md} +0 -0
  212. /package/reference/{registers.md → schema/registers.md} +0 -0
  213. /package/reference/{schema-error.md → schema/schema-error.md} +0 -0
  214. /package/reference/{syntax.md → schema/syntax.md} +0 -0
  215. /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.