@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,278 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: errors
|
|
3
|
+
title: "Configuration Errors"
|
|
4
|
+
sidebar_label: "Errors"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
`ConfigError` is the failure type for every configuration operation. It is a sealed hierarchy divided into four category traits, so code can match on the kind of problem — a missing key, an unparseable value, a source-level failure, a derivation failure — without enumerating every constructor. `ConfigLoadException` wraps a set of errors when a load is allowed to throw. The hierarchy:
|
|
8
|
+
|
|
9
|
+
```scala
|
|
10
|
+
sealed trait ConfigError extends NoStackTrace {
|
|
11
|
+
def message: String
|
|
12
|
+
override def getMessage: String = message
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
sealed trait ConfigParseError extends ConfigError
|
|
16
|
+
sealed trait ConfigValidationError extends ConfigError
|
|
17
|
+
sealed trait ConfigSourceError extends ConfigError
|
|
18
|
+
sealed trait ConfigDerivationError extends ConfigError
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Motivation
|
|
22
|
+
|
|
23
|
+
Configuration failures need three properties that a plain exception does not provide.
|
|
24
|
+
|
|
25
|
+
They need to be **plural**. A config file with four mistakes should report four mistakes, so the person fixing it makes one pass rather than four. That rules out throwing at the first problem, which is why every decoding operation returns `Either[::[ConfigError], A]` — a non-empty list, so a `Left` is guaranteed to say something.
|
|
26
|
+
|
|
27
|
+
They need to be **cheap**. `ConfigError` extends `NoStackTrace`, so constructing one does not capture a stack trace. Config errors are diagnosed by reading the message, not by reading a trace through the deriver's internals, and a decode that fails on twenty fields should not pay for twenty stack captures.
|
|
28
|
+
|
|
29
|
+
They need to be **classifiable**. Whether a value was missing, malformed, or rejected by the source changes what the operator should do about it. The four category traits exist so that distinction survives into your error handling.
|
|
30
|
+
|
|
31
|
+
## Category Traits
|
|
32
|
+
|
|
33
|
+
Every concrete error extends exactly one category, except `ConfigError.Composite`, which extends `ConfigError` directly because it aggregates errors that may span categories:
|
|
34
|
+
|
|
35
|
+
| Category | Meaning | Constructors |
|
|
36
|
+
| ------------------------ | ---------------------------------------------- | ------------------------------------------------------- |
|
|
37
|
+
| `ConfigParseError` | Value present but not convertible | `InvalidValue`, `ParseError` |
|
|
38
|
+
| `ConfigValidationError` | Value parsed but semantically rejected | *(none built in)* |
|
|
39
|
+
| `ConfigSourceError` | Problem with the source itself | `MissingKey`, `DuplicateKey`, `Unauthorized` |
|
|
40
|
+
| `ConfigDerivationError` | Schema-driven decoding could not proceed | `UnknownDiscriminator`, `MissingDiscriminatorKey` |
|
|
41
|
+
| *(no category)* | Aggregate of several errors | `Composite` |
|
|
42
|
+
|
|
43
|
+
:::note[`ConfigValidationError` has no constructors]
|
|
44
|
+
The category is declared and documented, but no error in the module currently extends it, and because the trait is sealed it cannot be extended from outside. Matching on it compiles and never matches. Validation failures raised by wrapper types surface as `ConfigError.InvalidValue` instead.
|
|
45
|
+
:::
|
|
46
|
+
|
|
47
|
+
## Error Constructors
|
|
48
|
+
|
|
49
|
+
Each constructor carries the fields needed to act on the failure without re-reading the config, and renders them into a single-line `message`.
|
|
50
|
+
|
|
51
|
+
### ConfigError.MissingKey
|
|
52
|
+
|
|
53
|
+
A required key was not found. Produced by primitive decoding when the key is absent and the field has neither a default nor an `Option` type:
|
|
54
|
+
|
|
55
|
+
```scala
|
|
56
|
+
import zio.blocks.config._
|
|
57
|
+
import zio.blocks.schema.Schema
|
|
58
|
+
|
|
59
|
+
case class Db(host: String, port: Int)
|
|
60
|
+
|
|
61
|
+
object Db {
|
|
62
|
+
implicit val schema: Schema[Db] = Schema.derived[Db]
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
The error names the dotted path and the source that was searched, which for a composed source is the pipe-joined id of every source in the chain:
|
|
67
|
+
|
|
68
|
+
```scala
|
|
69
|
+
Config.load[Db](ConfigSource.fromMap(Map("port" -> "5432"), "defaults"))
|
|
70
|
+
// res0: Either[::[ConfigError], Db] = Left(
|
|
71
|
+
// List(MissingKey(path = "host", source = "defaults"))
|
|
72
|
+
// )
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
`MissingKey` is the error that optional fields and defaults absorb. A field of type `Option[A]` becomes `None`, and a field with a declared default takes that default, but only when *every* error from that field is a `MissingKey` — a mix of missing and invalid still fails.
|
|
76
|
+
|
|
77
|
+
### ConfigError.InvalidValue
|
|
78
|
+
|
|
79
|
+
A key was present but its value could not be converted to the expected type. Carries the path, the offending string, a human-readable expected type, the source id, and an optional underlying exception:
|
|
80
|
+
|
|
81
|
+
```scala
|
|
82
|
+
Config.load[Db](ConfigSource.fromMap(Map("host" -> "localhost", "port" -> "eight"), "defaults"))
|
|
83
|
+
// res1: Either[::[ConfigError], Db] = Left(
|
|
84
|
+
// List(
|
|
85
|
+
// InvalidValue(
|
|
86
|
+
// path = "port",
|
|
87
|
+
// value = "eight",
|
|
88
|
+
// expectedType = "Int",
|
|
89
|
+
// source = "defaults",
|
|
90
|
+
// cause = Some(java.lang.NumberFormatException: For input string: "eight")
|
|
91
|
+
// )
|
|
92
|
+
// )
|
|
93
|
+
// )
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
The `expectedType` string is derived from the schema's primitive type name. When a `cause` is present, its message is appended to the rendered message, which is where the underlying `NumberFormatException` text comes from.
|
|
97
|
+
|
|
98
|
+
`InvalidValue` is also what a failing wrapper type produces: if a newtype's constructor throws while wrapping a successfully-decoded inner value, the thrown exception becomes the `cause`.
|
|
99
|
+
|
|
100
|
+
### ConfigError.ParseError
|
|
101
|
+
|
|
102
|
+
The raw value could not be decoded at the format level, as distinct from a type mismatch on a well-formed value. Carries the path, source, expected format, and optional cause. The HOCON adapter produces it when the document itself is malformed, and the JVM file loader produces it for a missing file or a rejected include path.
|
|
103
|
+
|
|
104
|
+
The difference from `InvalidValue` is which layer failed. `InvalidValue` means "this string is not an `Int`"; `ParseError` means "this document is not HOCON", so there is no string to quote.
|
|
105
|
+
|
|
106
|
+
### ConfigError.DuplicateKey
|
|
107
|
+
|
|
108
|
+
The same key appears in multiple conflicting sources. Carries the path and the ids of every source that defined it. Nothing in the module produces it — `ConfigSource#orElse` resolves conflicts by precedence rather than reporting them — so it exists for custom sources that want to treat ambiguity as an error rather than resolving it silently.
|
|
109
|
+
|
|
110
|
+
### ConfigError.Unauthorized
|
|
111
|
+
|
|
112
|
+
Access to a key was denied by the source. Carries the path and source id. Like `DuplicateKey`, no built-in source produces it; it is the error a custom source backed by a secrets manager should return when a lookup is rejected rather than merely absent, so that "you may not read this" is distinguishable from "this does not exist".
|
|
113
|
+
|
|
114
|
+
### ConfigError.MissingDiscriminatorKey
|
|
115
|
+
|
|
116
|
+
A sealed trait was decoded but the key naming which case to use was absent. Carries the record's path and the discriminator key that was expected:
|
|
117
|
+
|
|
118
|
+
```scala
|
|
119
|
+
import zio.blocks.config._
|
|
120
|
+
import zio.blocks.schema.Schema
|
|
121
|
+
|
|
122
|
+
sealed trait Backend
|
|
123
|
+
|
|
124
|
+
object Backend {
|
|
125
|
+
case class Postgres(host: String, port: Int) extends Backend
|
|
126
|
+
case class Sqlite(path: String) extends Backend
|
|
127
|
+
|
|
128
|
+
implicit val schema: Schema[Backend] = Schema.derived[Backend]
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
The message states both, so the fix is unambiguous:
|
|
133
|
+
|
|
134
|
+
```scala
|
|
135
|
+
Config.load[Backend](ConfigSource.fromMap(Map("host" -> "localhost"), "conf"))
|
|
136
|
+
// res3: Either[::[ConfigError], Backend] = Left(
|
|
137
|
+
// List(MissingDiscriminatorKey(path = "", key = "type"))
|
|
138
|
+
// )
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### ConfigError.UnknownDiscriminator
|
|
142
|
+
|
|
143
|
+
The discriminator key was present but named a case that does not exist. Carries the full key path, the value found, and every accepted value in sorted order:
|
|
144
|
+
|
|
145
|
+
```scala
|
|
146
|
+
Config.load[Backend](ConfigSource.fromMap(Map("type" -> "MySql"), "conf"))
|
|
147
|
+
// res4: Either[::[ConfigError], Backend] = Left(
|
|
148
|
+
// List(
|
|
149
|
+
// UnknownDiscriminator(
|
|
150
|
+
// path = "type",
|
|
151
|
+
// found = "MySql",
|
|
152
|
+
// expected = List("Postgres", "Sqlite")
|
|
153
|
+
// )
|
|
154
|
+
// )
|
|
155
|
+
// )
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Listing the expected values in the message is deliberate: a typo in a discriminator is one of the few config mistakes where the error can name the correct answer.
|
|
159
|
+
|
|
160
|
+
### ConfigError.Composite
|
|
161
|
+
|
|
162
|
+
Two or more errors accumulated while decoding one record. Carries them as a `::[ConfigError]`, and renders as the child messages joined with newlines:
|
|
163
|
+
|
|
164
|
+
```scala
|
|
165
|
+
import zio.blocks.config._
|
|
166
|
+
import zio.blocks.schema.Schema
|
|
167
|
+
|
|
168
|
+
case class Db(host: String, port: Int)
|
|
169
|
+
|
|
170
|
+
object Db {
|
|
171
|
+
implicit val schema: Schema[Db] = Schema.derived[Db]
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
val failed = Config.load[Db](ConfigSource.fromMap(Map.empty[String, String], "empty"))
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
A record with a single failing field returns that error directly, so `Composite` appears only when there is genuinely more than one:
|
|
178
|
+
|
|
179
|
+
```scala
|
|
180
|
+
failed.left.map(_.head.getClass.getSimpleName)
|
|
181
|
+
// res6: Either[String, Db] = Left("Composite")
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
Composites nest. Wrapping happens once per record, so a nested config whose inner and outer records both have multiple failures produces a `Composite` containing a `Composite`. Flatten before reporting if you want one list:
|
|
185
|
+
|
|
186
|
+
```scala
|
|
187
|
+
import zio.blocks.config._
|
|
188
|
+
|
|
189
|
+
def flatten(error: ConfigError): List[ConfigError] = error match {
|
|
190
|
+
case ConfigError.Composite(errors) => errors.toList.flatMap(flatten)
|
|
191
|
+
case other => List(other)
|
|
192
|
+
}
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
## Handling Errors
|
|
196
|
+
|
|
197
|
+
How you consume a `Left` depends on whether the response varies by kind of failure. Matching on categories keeps the handler stable as new constructors are added; matching on constructors gets you the fields.
|
|
198
|
+
|
|
199
|
+
### Matching by Category
|
|
200
|
+
|
|
201
|
+
Categories answer "whose fault is this?", which is usually what determines the response — a missing key is an operator problem, a derivation error is a schema problem:
|
|
202
|
+
|
|
203
|
+
```scala
|
|
204
|
+
import zio.blocks.config._
|
|
205
|
+
|
|
206
|
+
def describe(error: ConfigError): String = error match {
|
|
207
|
+
case _: ConfigSourceError => "configuration is incomplete or unreadable"
|
|
208
|
+
case _: ConfigParseError => "a configured value has the wrong format"
|
|
209
|
+
case _: ConfigDerivationError => "the config shape does not match the schema"
|
|
210
|
+
case _ => "multiple problems"
|
|
211
|
+
}
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
### Matching by Constructor
|
|
215
|
+
|
|
216
|
+
Matching the concrete type gives access to the fields, which is what you need to build a targeted message or a machine-readable report:
|
|
217
|
+
|
|
218
|
+
```scala
|
|
219
|
+
import zio.blocks.config._
|
|
220
|
+
|
|
221
|
+
def remedy(error: ConfigError): String = error match {
|
|
222
|
+
case ConfigError.MissingKey(path, source) =>
|
|
223
|
+
s"set $path in $source"
|
|
224
|
+
case ConfigError.InvalidValue(path, value, expected, _, _) =>
|
|
225
|
+
s"$path is '$value' but must be a $expected"
|
|
226
|
+
case ConfigError.UnknownDiscriminator(path, found, expected) =>
|
|
227
|
+
s"$path is '$found'; use one of ${expected.mkString(", ")}"
|
|
228
|
+
case other =>
|
|
229
|
+
other.message
|
|
230
|
+
}
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
Because `ConfigError` extends `NoStackTrace`, and therefore `Throwable`, an individual error can also be thrown or wrapped directly when integrating with code that expects exceptions.
|
|
234
|
+
|
|
235
|
+
## ConfigLoadException
|
|
236
|
+
|
|
237
|
+
`Config.loadOrThrow` throws `ConfigLoadException` rather than returning a `Left`. The exception carries both a formatted report for humans and the original error list for programs:
|
|
238
|
+
|
|
239
|
+
```scala
|
|
240
|
+
final class ConfigLoadException(val report: String, val errors: ::[ConfigError])
|
|
241
|
+
extends RuntimeException(report)
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
`report` is a multi-line summary: a count line followed by one indented bullet per top-level error. Catching the exception and reading `errors` recovers the structured form, so throwing does not lose information:
|
|
245
|
+
|
|
246
|
+
```scala
|
|
247
|
+
import zio.blocks.config._
|
|
248
|
+
import zio.blocks.schema.Schema
|
|
249
|
+
|
|
250
|
+
case class Db(host: String, port: Int)
|
|
251
|
+
|
|
252
|
+
object Db {
|
|
253
|
+
implicit val schema: Schema[Db] = Schema.derived[Db]
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
try Config.loadOrThrow[Db](ConfigSource.fromMap(Map.empty[String, String], "empty"))
|
|
257
|
+
catch {
|
|
258
|
+
case e: ConfigLoadException =>
|
|
259
|
+
println(e.report)
|
|
260
|
+
e.errors.toList.foreach(err => println(err.message))
|
|
261
|
+
}
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
Use `Config.loadOrThrow` at the top of `main`, where an unrecoverable config problem should stop startup with a readable message. Prefer `Config.load` anywhere the failure is recoverable — a reload, a per-request decode, or a validation pass that reports rather than aborts.
|
|
265
|
+
|
|
266
|
+
:::warning[`Config.wire` throws]
|
|
267
|
+
`Config.wire` decodes with `Config.loadOrThrow`, so a bad config surfaces as a `ConfigLoadException` while the dependency graph is being allocated rather than as a typed failure. Validate with `Config.load` first if you need to report config problems before touching the graph.
|
|
268
|
+
:::
|
|
269
|
+
|
|
270
|
+
## Flag Exceptions
|
|
271
|
+
|
|
272
|
+
Flag resolution has a separate hierarchy, `FlagException`, because flags fail at class-load time and cannot return an `Either` from an object initializer. `ConfigError` still appears inside those exceptions as the underlying cause — a flag whose value will not parse carries the same `ConfigError.InvalidValue` a config field would. See [Flags](./flags.md).
|
|
273
|
+
|
|
274
|
+
## Integration Points
|
|
275
|
+
|
|
276
|
+
`ConfigError` is produced by `ConfigDecoder`, by every `ConfigSource` constructor that can fail, and by `Rollout` when an expression will not parse. It is consumed by `Config`, which either returns it or formats it into a `ConfigLoadException`, and by `Flag.ReloadResult.Failed`, which carries it out of a failed dynamic-flag reload.
|
|
277
|
+
|
|
278
|
+
See [Config Decoder](./config-decoder.md) for which rule produces which error, and [Rollout](./rollout.md) for expression-level failures.
|
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: flags
|
|
3
|
+
title: "Flags"
|
|
4
|
+
sidebar_label: "Flags"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
A flag is a named scalar whose value comes from outside the program. `StaticFlag[A]` resolves once at class-load time and never changes; `DynamicFlag[A]` holds a rollout expression that can be updated while the process runs. Both read through `FlagSource`, parse through `Flag.Reader`, and register themselves in a global registry that `Flag.dump` can print. Supporting types: `Flag.Source`, `FlagException`. The three declarations you write against:
|
|
8
|
+
|
|
9
|
+
```scala
|
|
10
|
+
trait FlagSource {
|
|
11
|
+
def sourceId: String
|
|
12
|
+
def get(name: String): Maybe[SourceValue[String]]
|
|
13
|
+
final def orElse(fallback: FlagSource): FlagSource
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
abstract class StaticFlag[A](default: A)(implicit reader: Flag.Reader[A], displayable: Displayable[A])
|
|
17
|
+
|
|
18
|
+
abstract class DynamicFlag[A](default: A, defaultExpression: String)(implicit reader: Flag.Reader[A])
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Motivation
|
|
22
|
+
|
|
23
|
+
Configuration and flags answer different questions. Configuration describes a deployment: which database, which port, which region. Flags describe a decision: is this feature on, for whom, and at what percentage. The two have different shapes — configuration is a typed record loaded once, a flag is a single value read at the point of use — and different failure modes.
|
|
24
|
+
|
|
25
|
+
Flags in this module are Scala objects. That choice buys three things. The name is derived from the object's fully-qualified name, so it cannot drift from the code that reads it. The value is a `val`, so reading it is a field access with no lookup cost. And references are compile-checked: a deleted flag is a compile error at every use site, not a silently-missing key.
|
|
26
|
+
|
|
27
|
+
The cost is that resolution happens during class initialization, which makes ordering matter. A `StaticFlag` touched before its `FlagSource` is registered resolves from the environment or its default and stays that way.
|
|
28
|
+
|
|
29
|
+
## FlagSource
|
|
30
|
+
|
|
31
|
+
`FlagSource` is the scalar half of `ConfigSource`: a name-to-string lookup with provenance, without prefix enumeration. Because `ConfigSource extends FlagSource`, any config source is also a flag source.
|
|
32
|
+
|
|
33
|
+
### Creating a Source
|
|
34
|
+
|
|
35
|
+
`FlagSource.fromMap` builds an in-memory source, which is what tests and local overrides use:
|
|
36
|
+
|
|
37
|
+
```scala
|
|
38
|
+
import zio.blocks.config._
|
|
39
|
+
|
|
40
|
+
val source = FlagSource.fromMap(Map("myapp.poolSize" -> "32"), "local")
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Lookups return the value with a `Provenance.Resolved` naming the source and the name it answered under:
|
|
44
|
+
|
|
45
|
+
```scala
|
|
46
|
+
source.get("myapp.poolSize")
|
|
47
|
+
// res0: Maybe[SourceValue[String]] = SourceValue(
|
|
48
|
+
// value = "32",
|
|
49
|
+
// provenance = Resolved(
|
|
50
|
+
// sourceId = "local",
|
|
51
|
+
// key = "myapp.poolSize",
|
|
52
|
+
// rawValue = "32"
|
|
53
|
+
// )
|
|
54
|
+
// )
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
`FlagSource#orElse` layers sources with the receiver taking priority, exactly as on `ConfigSource`.
|
|
58
|
+
|
|
59
|
+
### The Registry
|
|
60
|
+
|
|
61
|
+
Flags do not take a source as a constructor argument. They consult `FlagSource.Registry`, a thread-safe global that flags read during initialization:
|
|
62
|
+
|
|
63
|
+
| Method | Effect |
|
|
64
|
+
| ------------------------------- | ---------------------------------------------------------------------- |
|
|
65
|
+
| `Registry.register(source)` | Adds a source, keyed by `sourceId`; re-registering an id replaces it in place. |
|
|
66
|
+
| `Registry.unregister(sourceId)` | Removes a source by id. |
|
|
67
|
+
| `Registry.get(sourceId)` | Looks up a registered source. |
|
|
68
|
+
| `Registry.all` | Every source in registration order. |
|
|
69
|
+
| `Registry.resolve(name)` | First source that answers the name wins. |
|
|
70
|
+
| `Registry.clear()` | Removes everything; intended for tests. |
|
|
71
|
+
|
|
72
|
+
Registration order is resolution order, so the first source registered has the highest priority:
|
|
73
|
+
|
|
74
|
+
```scala
|
|
75
|
+
import zio.blocks.config._
|
|
76
|
+
|
|
77
|
+
FlagSource.Registry.register(FlagSource.fromMap(Map("myapp.poolSize" -> "64"), "remote"))
|
|
78
|
+
FlagSource.Registry.register(FlagSource.fromMap(Map("myapp.poolSize" -> "32"), "fallback"))
|
|
79
|
+
|
|
80
|
+
FlagSource.Registry.resolve("myapp.poolSize")
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Re-registering an existing `sourceId` swaps the source without changing its position, so refreshing a source's contents does not reshuffle priorities.
|
|
84
|
+
|
|
85
|
+
:::warning[Register before first touch]
|
|
86
|
+
A `StaticFlag` resolves during its object's initialization, which happens the first time anything references it. Sources registered after that point are never consulted for that flag. Register every source at the very top of `main`, before touching any flag.
|
|
87
|
+
:::
|
|
88
|
+
|
|
89
|
+
## StaticFlag
|
|
90
|
+
|
|
91
|
+
`StaticFlag[A]` is for values that are fixed for the lifetime of the process: pool sizes, buffer limits, endpoints that cannot change without a restart.
|
|
92
|
+
|
|
93
|
+
### Defining a Flag
|
|
94
|
+
|
|
95
|
+
Extend `StaticFlag[A]` from a Scala `object`, passing the default:
|
|
96
|
+
|
|
97
|
+
```scala
|
|
98
|
+
import zio.blocks.config._
|
|
99
|
+
|
|
100
|
+
object poolSize extends StaticFlag[Int](10)
|
|
101
|
+
|
|
102
|
+
val size: Int = poolSize()
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
The flag's `name` is the object's fully-qualified name with `$` separators rewritten to dots, so an object `Limits.poolSize` in package `com.example` is named `com.example.Limits.poolSize`. Nothing declares that name — it follows from where the object lives, which means moving the object renames the flag.
|
|
106
|
+
|
|
107
|
+
### Resolution Order
|
|
108
|
+
|
|
109
|
+
Resolution runs once, in a fixed order, and stops at the first source that produces a value:
|
|
110
|
+
|
|
111
|
+
```
|
|
112
|
+
1. FlagSource.Registry.resolve(name) ← registration order across all sources
|
|
113
|
+
2. System property with the flag's name ← -Dcom.example.Limits.poolSize=32
|
|
114
|
+
3. Environment variable ← COM_EXAMPLE_LIMITS_POOLSIZE=32
|
|
115
|
+
4. The constructor default
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
The environment variable name is the flag name with dots replaced by underscores, uppercased. That translation is fixed and not configurable.
|
|
119
|
+
|
|
120
|
+
### Members
|
|
121
|
+
|
|
122
|
+
Each resolved flag exposes what it resolved to and where from:
|
|
123
|
+
|
|
124
|
+
| Member | Type | Meaning |
|
|
125
|
+
| -------------- | ------------- | --------------------------------------------------------- |
|
|
126
|
+
| `name` | `String` | Derived fully-qualified flag name. |
|
|
127
|
+
| `value` | `A` | The resolved value. |
|
|
128
|
+
| `apply()` | `A` | Same as `value`; reads as a call at use sites. |
|
|
129
|
+
| `source` | `Flag.Source` | Which of the four resolution steps answered. |
|
|
130
|
+
| `provenance` | `Provenance` | Source id, key, and raw string. |
|
|
131
|
+
| `displayValue` | `String` | Rendered through `Displayable[A]`, used by `Flag.dump`. |
|
|
132
|
+
|
|
133
|
+
### Failure at Initialization
|
|
134
|
+
|
|
135
|
+
An object initializer cannot return an `Either`, so a flag that cannot resolve throws. A value that fails to parse throws `ExceptionInInitializerError` wrapping `FlagException.FlagValueParseException`, and a flag whose defining class is not a Scala object throws `FlagException.FlagNameException`.
|
|
136
|
+
|
|
137
|
+
Failing at class load is the point: a malformed flag stops the process at startup rather than producing a plausible-looking wrong value that surfaces later.
|
|
138
|
+
|
|
139
|
+
:::note[Secrets in flags]
|
|
140
|
+
`Flag.Reader[Secret]` accepts any string, and the module provides `Displayable[Secret]` in the `zio.blocks.config` package object, so a `StaticFlag[Secret]` renders as `<secret>` in dumps without extra configuration. See [ConfigSource](./config-source.md) for `Secret` itself.
|
|
141
|
+
:::
|
|
142
|
+
|
|
143
|
+
## DynamicFlag
|
|
144
|
+
|
|
145
|
+
`DynamicFlag[A]` is for decisions that change without a restart: enabling a feature for a subset of users, shifting traffic to a new code path, turning off an expensive query under load.
|
|
146
|
+
|
|
147
|
+
### Defining a Flag
|
|
148
|
+
|
|
149
|
+
Extend `DynamicFlag[A]` with both a default value and a default rollout expression:
|
|
150
|
+
|
|
151
|
+
```scala
|
|
152
|
+
import zio.blocks.config._
|
|
153
|
+
|
|
154
|
+
object newCheckout extends DynamicFlag[Boolean](false, "true@*/50%; false")
|
|
155
|
+
|
|
156
|
+
val enabled: Boolean = newCheckout("user-1234")
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
The initial expression resolves through the same chain as a static flag's value — registry, then system property, then environment variable, then the constructor default — but what it resolves is the *expression*, not the value. A malformed expression throws `ExceptionInInitializerError` wrapping `FlagException.FlagExpressionParseException`.
|
|
160
|
+
|
|
161
|
+
### Evaluation
|
|
162
|
+
|
|
163
|
+
`DynamicFlag#apply` takes a bucketing key and any number of path attributes, evaluates the expression, and tracks a counter for the key. `DynamicFlag#evaluate` does the same without touching counters:
|
|
164
|
+
|
|
165
|
+
```scala
|
|
166
|
+
import zio.blocks.config._
|
|
167
|
+
|
|
168
|
+
object newCheckout extends DynamicFlag[Boolean](false, "true@*/prod/*/50%; false")
|
|
169
|
+
|
|
170
|
+
newCheckout("user-1234", "prod", "eu") // counted
|
|
171
|
+
newCheckout.evaluate("user-1234", "prod", "eu") // not counted
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
The key determines the percentage bucket, and it is also the *first* segment of the path a selector matches against: the attributes are appended to it with slashes, so the call above matches against `user-1234/prod/eu`. A selector must have exactly as many segments as that path, which is why the expression starts with a wildcard. Evaluation details are in [Rollout](./rollout.md).
|
|
175
|
+
|
|
176
|
+
When a matched value cannot be parsed into `A`, evaluation falls back to the flag's default and increments `DynamicFlag#parseErrorCount`. A non-zero count means the expression contains values the reader rejects — a misconfiguration that would otherwise be invisible, since every call still returns something usable.
|
|
177
|
+
|
|
178
|
+
### Updating and Reloading
|
|
179
|
+
|
|
180
|
+
`DynamicFlag#update` replaces the expression from code, returning `Left` if the new expression will not parse and leaving the old one in place:
|
|
181
|
+
|
|
182
|
+
```scala
|
|
183
|
+
import zio.blocks.config._
|
|
184
|
+
|
|
185
|
+
object newCheckout extends DynamicFlag[Boolean](false, "false")
|
|
186
|
+
|
|
187
|
+
newCheckout.update("true@prod/25%; false")
|
|
188
|
+
newCheckout.expression
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
An empty or whitespace-only expression is accepted as a no-op and returns `Right(())` without changing anything.
|
|
192
|
+
|
|
193
|
+
`DynamicFlag#reload` re-reads the expression from `FlagSource.Registry` and reports what happened as a `Flag.ReloadResult`. See [Rollout](./rollout.md) for the result variants and the update history.
|
|
194
|
+
|
|
195
|
+
## Flag.Reader
|
|
196
|
+
|
|
197
|
+
`Flag.Reader[A]` parses a raw string into `A` and supplies a type name for error messages. Instances are resolved implicitly by both flag types:
|
|
198
|
+
|
|
199
|
+
```scala
|
|
200
|
+
trait Reader[A] {
|
|
201
|
+
def parse(flagName: String, raw: String): Either[ConfigError, A]
|
|
202
|
+
def typeName: String
|
|
203
|
+
}
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### Built-in Instances
|
|
207
|
+
|
|
208
|
+
| Type | Accepted form | Example |
|
|
209
|
+
| ------------------- | ---------------------------------------------------------- | -------- |
|
|
210
|
+
| `Int`, `Long` | Decimal integer literal | `32` |
|
|
211
|
+
| `Double` | Decimal or scientific literal | `0.5` |
|
|
212
|
+
| `Boolean` | `true`/`false`, `1`/`0`, `yes`/`no`, `on`/`off` (any case) | `on` |
|
|
213
|
+
| `String` | Any string, taken as-is | `eu-west` |
|
|
214
|
+
| `Secret` | Any string, wrapped so it cannot be printed | `hunter2` |
|
|
215
|
+
| `FiniteDuration` | Digits followed by a unit suffix | `30s` |
|
|
216
|
+
| `Seq[A]` | Comma-separated values of a scalar `A`, each trimmed | `a, b, c` |
|
|
217
|
+
|
|
218
|
+
Boolean parsing accepts the same set of spellings as config decoding:
|
|
219
|
+
|
|
220
|
+
```scala
|
|
221
|
+
Flag.Reader.booleanReader.parse("example", "ON")
|
|
222
|
+
// res6: Either[ConfigError, Boolean] = Right(true)
|
|
223
|
+
Flag.Reader.booleanReader.parse("example", "maybe")
|
|
224
|
+
// res7: Either[ConfigError, Boolean] = Left(
|
|
225
|
+
// InvalidValue(
|
|
226
|
+
// path = "example",
|
|
227
|
+
// value = "maybe",
|
|
228
|
+
// expectedType = "Boolean (true/false/1/0/yes/no/on/off)",
|
|
229
|
+
// source = "flag",
|
|
230
|
+
// cause = None
|
|
231
|
+
// )
|
|
232
|
+
// )
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
Duration suffixes are `ms`/`millis`/`milliseconds`, `s`/`sec`/`secs`/`seconds`, `m`/`min`/`mins`/`minutes`, `h`/`hour`/`hours`, and `d`/`day`/`days`:
|
|
236
|
+
|
|
237
|
+
```scala
|
|
238
|
+
Flag.Reader.durationReader.parse("example", "500ms")
|
|
239
|
+
// res8: Either[ConfigError, FiniteDuration] = Right(500 milliseconds)
|
|
240
|
+
Flag.Reader.durationReader.parse("example", "2hours")
|
|
241
|
+
// res9: Either[ConfigError, FiniteDuration] = Right(2 hours)
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
The grammar is digits immediately followed by letters, so a space between the number and the unit is rejected:
|
|
245
|
+
|
|
246
|
+
```scala
|
|
247
|
+
Flag.Reader.durationReader.parse("example", "2 hours")
|
|
248
|
+
// res10: Either[ConfigError, FiniteDuration] = Left(
|
|
249
|
+
// InvalidValue(
|
|
250
|
+
// path = "example",
|
|
251
|
+
// value = "2 hours",
|
|
252
|
+
// expectedType = "Duration (e.g., 10s, 5m, 1h, 100ms, 2d)",
|
|
253
|
+
// source = "flag",
|
|
254
|
+
// cause = None
|
|
255
|
+
// )
|
|
256
|
+
// )
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
:::warning[Flag durations are not config durations]
|
|
260
|
+
`Flag.Reader[FiniteDuration]` parses `30s`. Config decoding parses `java.time.Duration` with `Duration.parse`, which requires ISO-8601 (`PT30S`). Neither accepts the other's format — see [Config Decoder](./config-decoder.md).
|
|
261
|
+
:::
|
|
262
|
+
|
|
263
|
+
### Scalar and Sequence Readers
|
|
264
|
+
|
|
265
|
+
`Flag.Reader.Scalar[A]` marks a reader whose values contain no commas. `Flag.Reader.seqReader` requires that marker, because it splits on commas — a non-scalar element type would make the split ambiguous:
|
|
266
|
+
|
|
267
|
+
```scala
|
|
268
|
+
Flag.Reader.seqReader[Int].parse("example", "1, 2, 3")
|
|
269
|
+
// res11: Either[ConfigError, Seq[Int]] = Right(ArraySeq(1, 2, 3))
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
An empty string parses to an empty sequence rather than failing. If any element fails, its error stands for the whole list.
|
|
273
|
+
|
|
274
|
+
### Custom Readers
|
|
275
|
+
|
|
276
|
+
`Flag.Reader.apply` builds a reader from a parse function and a type name, and `Flag.Reader.scalar` does the same for comma-free types so that `Seq` of them works:
|
|
277
|
+
|
|
278
|
+
```scala
|
|
279
|
+
import zio.blocks.config._
|
|
280
|
+
|
|
281
|
+
final case class Region(code: String)
|
|
282
|
+
|
|
283
|
+
implicit val regionReader: Flag.Reader.Scalar[Region] =
|
|
284
|
+
Flag.Reader.scalar(
|
|
285
|
+
(flagName, raw) =>
|
|
286
|
+
if (raw.matches("[a-z]{2}-[a-z]+-\\d")) Right(Region(raw))
|
|
287
|
+
else Left(ConfigError.InvalidValue(flagName, raw, "Region (e.g. eu-west-1)", "flag")),
|
|
288
|
+
"Region"
|
|
289
|
+
)
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
Return `ConfigError.InvalidValue` from a failing parse so the resulting message matches every other config failure.
|
|
293
|
+
|
|
294
|
+
## Flag.Source
|
|
295
|
+
|
|
296
|
+
`Flag.Source` records which resolution step answers a static flag:
|
|
297
|
+
|
|
298
|
+
| Variant | Meaning |
|
|
299
|
+
| ---------------------------------- | ---------------------------------------------- |
|
|
300
|
+
| `Flag.Source.FlagSourceValue(id)` | A registered `FlagSource` answered; `id` names it. |
|
|
301
|
+
| `Flag.Source.SystemProperty` | A JVM system property answered. |
|
|
302
|
+
| `Flag.Source.EnvironmentVariable` | An environment variable answered. |
|
|
303
|
+
| `Flag.Source.Default` | Nothing answered; the constructor default applies. |
|
|
304
|
+
|
|
305
|
+
Checking for `Flag.Source.Default` at startup is how you detect a flag you meant to configure and did not.
|
|
306
|
+
|
|
307
|
+
## Diagnostics
|
|
308
|
+
|
|
309
|
+
Two helpers on `Flag` exist for the two questions that come up when a flag has the wrong value: what did everything resolve to, and did I misspell something?
|
|
310
|
+
|
|
311
|
+
### Flag.dump
|
|
312
|
+
|
|
313
|
+
`Flag.dump()` renders every registered flag — static and dynamic — as a table of name, type, value, and source. Static flags show their `displayValue` and provenance source id; dynamic flags show their current expression:
|
|
314
|
+
|
|
315
|
+
```scala
|
|
316
|
+
import zio.blocks.config._
|
|
317
|
+
|
|
318
|
+
println(Flag.dump())
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
The registry is populated by construction, so a flag appears only after something has touched it. Print the dump after startup has forced every flag, not before.
|
|
322
|
+
|
|
323
|
+
### Flag.nearMissWarnings
|
|
324
|
+
|
|
325
|
+
`Flag.nearMissWarnings` looks for names that resemble a flag but do not match it, which catches the common failures: a case mismatch in a system property, a case mismatch in an environment variable, or a registered source holding a nearly-right key:
|
|
326
|
+
|
|
327
|
+
```scala
|
|
328
|
+
import zio.blocks.config._
|
|
329
|
+
|
|
330
|
+
Flag.nearMissWarnings("com.example.Limits.poolSize").foreach(println)
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
Candidates checked include the name lowercased and uppercased, dots swapped for underscores, and underscores swapped for dots. A warning means a source contains something similar — it does not mean the flag is wrong, only that a typo is plausible.
|
|
334
|
+
|
|
335
|
+
## FlagException
|
|
336
|
+
|
|
337
|
+
Flag failures are exceptions rather than values because they occur inside object initializers. `FlagException` extends `NoStackTrace`, so constructing one is cheap:
|
|
338
|
+
|
|
339
|
+
| Exception | Thrown when |
|
|
340
|
+
| ---------------------------------------- | --------------------------------------------------------------------------- |
|
|
341
|
+
| `FlagValueParseException` | A static flag's raw value fails its `Flag.Reader`. |
|
|
342
|
+
| `FlagExpressionParseException` | A dynamic flag's initial rollout expression will not parse. |
|
|
343
|
+
| `FlagNameException` | The defining class is not a Scala object, or is a lambda or anonymous class. |
|
|
344
|
+
| `FlagDuplicateNameException` | A second flag registers under an existing name. |
|
|
345
|
+
|
|
346
|
+
`FlagValueParseException` and `FlagExpressionParseException` are wrapped in `ExceptionInInitializerError` by the JVM when thrown from an initializer, so catch that and inspect its cause:
|
|
347
|
+
|
|
348
|
+
```scala
|
|
349
|
+
import zio.blocks.config._
|
|
350
|
+
|
|
351
|
+
try {
|
|
352
|
+
// Touching a flag object forces its initialization.
|
|
353
|
+
Class.forName("com.example.Limits$poolSize$")
|
|
354
|
+
} catch {
|
|
355
|
+
case e: ExceptionInInitializerError =>
|
|
356
|
+
e.getCause match {
|
|
357
|
+
case f: FlagException => println(f.getMessage)
|
|
358
|
+
case other => throw other
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
`FlagDuplicateNameException` means two objects derived the same name, which happens when two flags share a fully-qualified name after `$`-to-dot rewriting. Renaming either object resolves it.
|
|
364
|
+
|
|
365
|
+
## Integration Points
|
|
366
|
+
|
|
367
|
+
Flags depend on `FlagSource` — and therefore accept any `ConfigSource` — on `Flag.Reader` for parsing, on `Displayable` for rendering, and on `ConfigError` for parse failures. `DynamicFlag` additionally depends on `Rollout` for expression evaluation.
|
|
368
|
+
|
|
369
|
+
See [Rollout](./rollout.md) for the expression language and reload lifecycle, [ConfigSource](./config-source.md) for sources and `Secret`, and [Errors](./errors.md) for the shared error model.
|