@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,263 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: schema-search
|
|
3
|
+
title: "Schema Search and Update"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
`SchemaSearch` and `TypeSearch` are the two `DynamicOptic` node kinds that turn a path into a search: instead of naming one field, they match every value of a shape or type anywhere in a structure. This page covers the machinery behind that search once a path resolves against a value or a schema: `SchemaMatch`, the structural predicate the search matches against; `SearchTraversal`, the `Traversal[S, A]` engine that executes a search over a typed value and lets you fold, modify, or check the matches; and `Updater`, the mechanism for rewriting a `Reflect`/`Term` tree at a resolved path rather than the values it describes.
|
|
7
|
+
|
|
8
|
+
For the search DSLs themselves — the typed `.searchFor[T]` macro, the `#Pattern` string syntax, and the supported pattern grammar — see [DynamicOptic](./dynamic-optic.md#search-optics). This page assumes you can already build a search path and focuses on what runs once you have one.
|
|
9
|
+
|
|
10
|
+
## Design & Structure
|
|
11
|
+
|
|
12
|
+
A search path splits into two phases that operate on different representations of your data:
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
Typed value S Schema tree Reflect[F, S]
|
|
16
|
+
│ │
|
|
17
|
+
▼ ▼
|
|
18
|
+
SearchTraversal[S, A] Reflect#updated(path)(Updater)
|
|
19
|
+
(built from .searchFor[T] (rewrites the Reflect/Term
|
|
20
|
+
or SearchTraversal.apply) node found at a path)
|
|
21
|
+
│
|
|
22
|
+
▼
|
|
23
|
+
DynamicValue tree, walked depth-first
|
|
24
|
+
│
|
|
25
|
+
▼
|
|
26
|
+
SchemaMatch.matches(pattern, value) ← only for #Pattern searches;
|
|
27
|
+
(the structural predicate) .searchFor[T] matches by TypeId instead
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
`SearchTraversal` finds and transforms **values** — it decodes candidate subtrees of a concrete `S` and collects the ones that decode as `A`. `Updater` rewrites **schema metadata** — it walks the `Reflect`/`Term` tree that describes a type and replaces the node at a path, independent of any particular value. The two are easy to conflate because both start from a `DynamicOptic` path, but a `TypeSearch`/`SchemaSearch` node is only meaningful to the value-searching side: `Reflect#updated` does not special-case those nodes, so a path that reaches `Reflect#updated`/`Schema#updated` must resolve through `Field`/`Case`/`AtIndex`/`Elements`/`Wrapped`/`MapKeys`/`MapValues` nodes only.
|
|
31
|
+
|
|
32
|
+
## SchemaMatch — Structural Pattern Matching
|
|
33
|
+
|
|
34
|
+
`SchemaMatch.matches` is the predicate a `#Pattern` search node matches against once it reaches a `DynamicValue`: given a `SchemaRepr` pattern and a `DynamicValue`, it returns whether the value has that shape:
|
|
35
|
+
|
|
36
|
+
```scala
|
|
37
|
+
object SchemaMatch {
|
|
38
|
+
def matches(pattern: SchemaRepr, value: DynamicValue): Boolean
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
`SchemaRepr` is the small pattern language `#Pattern` strings parse into — `Wildcard`, `Primitive(name)`, `Record(fields)`, `Variant(cases)`, `Sequence(element)`, `Map(key, value)`, `Optional(inner)`, and `Nominal(name)`. Constructing one directly and matching it against a `DynamicValue` shows the same rules the `#record { ... }` syntax compiles down to:
|
|
43
|
+
|
|
44
|
+
```scala
|
|
45
|
+
import zio.blocks.schema._
|
|
46
|
+
|
|
47
|
+
val personPattern = SchemaRepr.Record(
|
|
48
|
+
IndexedSeq("name" -> SchemaRepr.Primitive("string"), "age" -> SchemaRepr.Primitive("int"))
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
val alice = DynamicValue.Record("name" -> DynamicValue.string("Alice"), "age" -> DynamicValue.int(30))
|
|
52
|
+
val bob = DynamicValue.Record("name" -> DynamicValue.string("Bob"), "role" -> DynamicValue.string("admin"))
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
`Record` matching is a subset match — every field named in the pattern must exist with a matching type, but extra fields on the value are fine, which is why `bob` fails only because `age` is missing, not because of the extra `role` field:
|
|
56
|
+
|
|
57
|
+
```scala
|
|
58
|
+
SchemaMatch.matches(personPattern, alice)
|
|
59
|
+
// res0: Boolean = true
|
|
60
|
+
SchemaMatch.matches(personPattern, bob)
|
|
61
|
+
// res1: Boolean = false
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
`Wildcard` matches anything, and `Sequence`/`Map` patterns match when every element or entry matches — an empty sequence or map always matches, since there is nothing to fail on:
|
|
65
|
+
|
|
66
|
+
```scala
|
|
67
|
+
import zio.blocks.schema._
|
|
68
|
+
|
|
69
|
+
val ages = DynamicValue.Sequence(DynamicValue.int(1), DynamicValue.int(2))
|
|
70
|
+
SchemaMatch.matches(SchemaRepr.Sequence(SchemaRepr.Primitive("int")), ages)
|
|
71
|
+
SchemaMatch.matches(SchemaRepr.Wildcard, ages)
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
`Nominal(name)` always returns `false` against a `DynamicValue`, because a decoded value carries no record of the Scala type it came from — the same limitation documented for `#Person`-style patterns in [DynamicOptic's known limitation](./dynamic-optic.md#known-limitation-nominal-matching-in-untyped-contexts). Matching by nominal type requires the typed `.searchFor[T]` API instead, which matches by `TypeId` rather than by structure.
|
|
75
|
+
|
|
76
|
+
## SearchTraversal — Executing a Search Over Values
|
|
77
|
+
|
|
78
|
+
`SearchTraversal` is the `Traversal[S, A]` that `.searchFor[T]` and `#Pattern` searches compile down to. Construct one directly from a pair of schemas when you want the traversal without going through the macro or the path interpolator:
|
|
79
|
+
|
|
80
|
+
```scala
|
|
81
|
+
import zio.blocks.schema._
|
|
82
|
+
|
|
83
|
+
case class Address(city: String)
|
|
84
|
+
object Address {
|
|
85
|
+
implicit val schema: Schema[Address] = Schema.derived
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
case class Person(name: String, age: Int, address: Address)
|
|
89
|
+
object Person {
|
|
90
|
+
implicit val schema: Schema[Person] = Schema.derived
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
case class Team(name: String, lead: Person, members: List[Person])
|
|
94
|
+
object Team {
|
|
95
|
+
implicit val schema: Schema[Team] = Schema.derived
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
case class Company(name: String, ceo: Person, teams: List[Team])
|
|
99
|
+
object Company extends CompanionOptics[Company] {
|
|
100
|
+
implicit val schema: Schema[Company] = Schema.derived
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
val findPeople: Traversal[Company, Person] = SearchTraversal[Company, Person]
|
|
104
|
+
|
|
105
|
+
val acme = Company(
|
|
106
|
+
"Acme",
|
|
107
|
+
ceo = Person("Alice", 45, Address("NYC")),
|
|
108
|
+
teams = List(
|
|
109
|
+
Team("Platform", Person("Bob", 32, Address("SF")), List(Person("Carol", 28, Address("SF")))),
|
|
110
|
+
Team("Sales", Person("Dave", 39, Address("LA")), Nil)
|
|
111
|
+
)
|
|
112
|
+
)
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
`SearchTraversal[Company, Person]` resolves the two implicit `Schema` instances the same way `Company.optic(_.searchFor[Person])` does—both produce the identical traversal, so pick whichever reads better at the call site. `Traversal#fold` walks every match depth-first, left-to-right, which is why the CEO comes before any team, and each team's lead comes before its members:
|
|
116
|
+
|
|
117
|
+
```scala
|
|
118
|
+
findPeople.fold(acme)(List.empty[String], (names, p) => names :+ p.name)
|
|
119
|
+
// res3: List[String] = List("Alice", "Bob", "Carol", "Dave")
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
`Traversal#modify` rewrites every match in place and rebuilds the structure around it, `modifyOption` returns `None` instead of a no-op result when nothing matched, and `modifyOrFail` surfaces a decoding failure as `Left` instead of silently keeping the original value:
|
|
123
|
+
|
|
124
|
+
```scala
|
|
125
|
+
findPeople.modify(acme, p => p.copy(age = p.age + 1)).ceo.age
|
|
126
|
+
// res4: Int = 46
|
|
127
|
+
SearchTraversal[Company, Address].modifyOption(acme, a => a.copy(city = a.city.toUpperCase)).map(_.ceo.address.city)
|
|
128
|
+
// res5: Option[String] = Some("NYC")
|
|
129
|
+
SearchTraversal[Team, Address].modifyOption(acme.teams.head, a => a).isDefined
|
|
130
|
+
// res6: Boolean = true
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
`Traversal#check` reports whether a traversal has at least one match, which is how a search-backed traversal signals "nothing here" without throwing:
|
|
134
|
+
|
|
135
|
+
```scala
|
|
136
|
+
SearchTraversal[Company, Person].check(acme).isEmpty
|
|
137
|
+
// res7: Boolean = true
|
|
138
|
+
SearchTraversal[Team, Boolean].check(acme.teams.head).isEmpty
|
|
139
|
+
// res8: Boolean = false
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### Composing a Search With Other Optics
|
|
143
|
+
|
|
144
|
+
A search traversal composes with `Lens`, `Prism`, `Optional`, and other `Traversal`s on either side, so you can narrow the search to part of a structure or refine each match further. Searching from a specific field finds only what is reachable from there, and appending a lens after a search projects each match down to one of its fields:
|
|
145
|
+
|
|
146
|
+
```scala
|
|
147
|
+
import zio.blocks.schema._
|
|
148
|
+
|
|
149
|
+
object CompanyOptics extends CompanionOptics[Company] {
|
|
150
|
+
implicit val schema: Schema[Company] = Company.schema
|
|
151
|
+
|
|
152
|
+
// Search restricted to one team: only Bob and Carol, never Alice or Dave
|
|
153
|
+
val platformMembers: Traversal[Company, Person] = optic(_.teams.at(0).searchFor[Person])
|
|
154
|
+
|
|
155
|
+
// Search first, then focus each match's city
|
|
156
|
+
val allCities: Traversal[Company, String] = optic(_.searchFor[Address].city)
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Both directions rebuild the same way a plain path-based traversal does: `Traversal#modify`, `Traversal#fold`, and `Traversal#check` all work on the composed traversal exactly as they do on a bare `SearchTraversal`, because composition produces another `Traversal[S, A]` — the fact that a search sits inside it is an implementation detail, not a different API.
|
|
161
|
+
|
|
162
|
+
### Recursive Types Are Safe to Search
|
|
163
|
+
|
|
164
|
+
Because `SearchTraversal` walks the decoded `DynamicValue` of a concrete value rather than the schema definition, searching a recursive type (a tree, a linked structure) terminates naturally — the value itself is always finite, even though its `Reflect` describes an unbounded type. There is nothing extra to opt into; a self-referential case class searches the same way a flat one does.
|
|
165
|
+
|
|
166
|
+
## Updater — Rewriting Schema Metadata
|
|
167
|
+
|
|
168
|
+
`Reflect.Updater` and `Term.Updater` are the callbacks behind `Schema#updated` and `Reflect#updated` — the mechanism for rewriting a schema's metadata (documentation, defaults, validations, or a field's shape entirely) at a resolved path, as opposed to rewriting the values that schema describes:
|
|
169
|
+
|
|
170
|
+
```scala
|
|
171
|
+
object Reflect {
|
|
172
|
+
trait Updater[F[_, _]] {
|
|
173
|
+
def update[A](reflect: Reflect[F, A]): Reflect[F, A]
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
object Term {
|
|
178
|
+
trait Updater[F[_, _]] {
|
|
179
|
+
def update[S, A](input: Term[F, S, A]): Option[Term[F, S, A]]
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
The two differ in one important way: `Reflect.Updater#update` is total — it always returns a `Reflect`, because a schema node can be re-shaped but not removed. `Term.Updater#update` is partial — returning `None` deletes the field or case the updater targets, which is how `Record#modifyField` and `Variant#modifyCase` support dropping a member rather than only renaming or retyping it.
|
|
185
|
+
|
|
186
|
+
[Schema](./schema.md#updating-nested-schemas) already covers the common case, updating one field through an optic with a plain function: `Schema[Person].updated(Person.address)(_.doc("Mailing address"))`. That convenience overload builds a `Reflect.Updater` for you. Reaching for `Reflect.Updater` directly is what you need for the case that overload cannot express — a `DynamicOptic` path built at runtime, or a rewrite that needs the full node rather than just its focus:
|
|
187
|
+
|
|
188
|
+
```scala
|
|
189
|
+
import zio.blocks.schema._
|
|
190
|
+
import zio.blocks.schema.binding.Binding
|
|
191
|
+
|
|
192
|
+
case class Config(host: String, port: Int)
|
|
193
|
+
object Config {
|
|
194
|
+
implicit val schema: Schema[Config] = Schema.derived
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Attach documentation to a field found by a runtime-built DynamicOptic path
|
|
198
|
+
val documented: Option[Schema[Config]] =
|
|
199
|
+
Schema[Config].updated(DynamicOptic.root.field("port"))(new Reflect.Updater[Binding] {
|
|
200
|
+
def update[A](reflect: Reflect[Binding, A]): Reflect[Binding, A] =
|
|
201
|
+
reflect.doc("The TCP port the server listens on")
|
|
202
|
+
})
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
`Term.Updater` operates one level up, on the named field or case itself rather than its value, which is what makes rename and delete possible. Renaming reuses the term's existing `value`; deleting a field returns `None` and the field disappears from the record entirely:
|
|
206
|
+
|
|
207
|
+
```scala
|
|
208
|
+
import zio.blocks.schema._
|
|
209
|
+
import zio.blocks.schema.binding.Binding
|
|
210
|
+
|
|
211
|
+
case class LegacyUser(id: Long, username: String, internalNotes: String)
|
|
212
|
+
object LegacyUser {
|
|
213
|
+
implicit val schema: Schema[LegacyUser] = Schema.derived
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
val userRecord = Schema[LegacyUser].reflect.asRecord.get
|
|
217
|
+
|
|
218
|
+
// Rename username -> name
|
|
219
|
+
val renamed = userRecord.modifyField("username")(new Term.Updater[Binding] {
|
|
220
|
+
def update[S, A](input: Term[Binding, S, A]): Option[Term[Binding, S, A]] =
|
|
221
|
+
Some(input.copy(name = "name"))
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
// Drop internalNotes entirely by returning None
|
|
225
|
+
val withoutNotes = userRecord.modifyField("internalNotes")(new Term.Updater[Binding] {
|
|
226
|
+
def update[S, A](input: Term[Binding, S, A]): Option[Term[Binding, S, A]] = None
|
|
227
|
+
})
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
Both updaters ran against the same `userRecord` independently, so `renamed` still has three fields with one renamed, while `withoutNotes` has two:
|
|
231
|
+
|
|
232
|
+
```scala
|
|
233
|
+
renamed.map(_.fields.map(_.name))
|
|
234
|
+
// res11: Option[IndexedSeq[String]] = Some(
|
|
235
|
+
// Vector("id", "name", "internalNotes")
|
|
236
|
+
// )
|
|
237
|
+
withoutNotes.map(_.fields.map(_.name))
|
|
238
|
+
// res12: Option[IndexedSeq[String]] = None
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
:::warning[Search nodes are not valid `updated` paths]
|
|
242
|
+
`Reflect#updated`/`Schema#updated` walk a `DynamicOptic` through `Field`, `Case`, `AtIndex`/`AtIndices`/`Elements`, `Wrapped`, and `MapKeys`/`MapValues` nodes only. A path containing a `TypeSearch` or `SchemaSearch` node is not rejected outright, but it is not handled either — it falls into the same branch as `MapKeys`/`MapValues` and produces an unspecified result. Build search-based rewrites with `SearchTraversal#modify` on a value instead of `Schema#updated` on a schema. `DynamicMigration`, by contrast, rejects search nodes outright with an explicit error, since a migration requires one statically-known path.
|
|
243
|
+
:::
|
|
244
|
+
|
|
245
|
+
## Where Search Nodes Are (and Aren't) Handled
|
|
246
|
+
|
|
247
|
+
The same `TypeSearch`/`SchemaSearch` node means different things depending on which API resolves the path it's part of:
|
|
248
|
+
|
|
249
|
+
| API | Search nodes |
|
|
250
|
+
| -------------------------- | ---------------------------------------------------------- |
|
|
251
|
+
| `SearchTraversal` / `.searchFor[T]` / `#Pattern` on a value | The intended use — this is what builds and executes the search |
|
|
252
|
+
| `Reflect#get` / `Schema#get` | Supported — resolves the first match, then continues the remaining path from there |
|
|
253
|
+
| `Json` / `DynamicValue` patch paths (`JsonPatch`) | Supported — rewrites every match, not just the first |
|
|
254
|
+
| `Reflect#updated` / `Schema#updated` | Not handled — falls through to the map-key/value branch; use `SearchTraversal#modify` instead |
|
|
255
|
+
| `DynamicMigration` | Rejected outright with `"Type/Schema search nodes are not supported in migration paths"` |
|
|
256
|
+
|
|
257
|
+
## See Also
|
|
258
|
+
|
|
259
|
+
- [DynamicOptic](./dynamic-optic.md#search-optics) — the `.searchFor[T]` and `#Pattern` search DSLs, and the full pattern grammar table.
|
|
260
|
+
- [Path Interpolator](./path-interpolator.md) — the `p"..."` string syntax that `#Pattern` search nodes parse from.
|
|
261
|
+
- [Schema](./schema.md#updating-nested-schemas) — `Schema#updated` and `Schema#@@` for the common, optic-based case of rewriting one field's metadata.
|
|
262
|
+
- [Reflect](./reflect.md) — the node types (`Record`, `Variant`, `Sequence`, `Map`, `Wrapper`, `Deferred`) that `Updater` rewrites and `SearchTraversal` decodes against.
|
|
263
|
+
- [Optics](./optics.md) — the base `Traversal[S, A]` type that `SearchTraversal` implements and composes with.
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
id: schema
|
|
3
|
+
slug: schema
|
|
3
4
|
title: "Schema"
|
|
4
5
|
---
|
|
5
6
|
|
|
@@ -186,6 +187,17 @@ import zio.blocks.schema.Schema
|
|
|
186
187
|
Schema[Option[A]] // Generic option for reference types
|
|
187
188
|
```
|
|
188
189
|
|
|
190
|
+
### Either Values
|
|
191
|
+
|
|
192
|
+
An `Either[A, B]` schema is available whenever both branch types have schemas. Primitive values and primitive-backed wrappers use specialized primitive register layouts, while other values use the layouts described by their schemas:
|
|
193
|
+
|
|
194
|
+
```scala
|
|
195
|
+
import zio.blocks.schema.Schema
|
|
196
|
+
|
|
197
|
+
Schema[Either[String, Int]]
|
|
198
|
+
Schema[Either[Int, Long]]
|
|
199
|
+
```
|
|
200
|
+
|
|
189
201
|
### Collection Types
|
|
190
202
|
|
|
191
203
|
ZIO Blocks also provides polymorphic schemas for standard Scala collections. You can summon schemas for collections of any element type `A` (and key/value types `K`/`V` for maps):
|
|
@@ -510,6 +522,8 @@ val updated: Option[Schema[Person]] = Schema[Person]
|
|
|
510
522
|
.updated(Person.address)(_.doc("Mailing address"))
|
|
511
523
|
```
|
|
512
524
|
|
|
525
|
+
For the lower-level `Reflect.Updater`/`Term.Updater` callbacks this method builds on — including how `Term.Updater` can rename or delete a field by returning `None` — see [Schema Search and Update](./schema-search.md#updater--rewriting-schema-metadata).
|
|
526
|
+
|
|
513
527
|
## Schema Aspects
|
|
514
528
|
|
|
515
529
|
Schema aspects are a powerful mechanism in ZIO Blocks for transforming schemas. You can think of the schema aspect as a function that takes a reflect and produces a new reflect:
|
|
@@ -517,17 +531,21 @@ Schema aspects are a powerful mechanism in ZIO Blocks for transforming schemas.
|
|
|
517
531
|
```scala
|
|
518
532
|
trait SchemaAspect[-Upper, +Lower, F[_, _]] {
|
|
519
533
|
def apply[A >: Lower <: Upper](reflect: Reflect[F, A]): Reflect[F, A]
|
|
520
|
-
def recursive(implicit ev1: Any <:< Upper, ev2: Lower <:< Nothing): SchemaAspect[Upper, Lower, F]
|
|
521
534
|
}
|
|
522
535
|
```
|
|
523
536
|
|
|
524
|
-
The `Schema` data type has a `@@` method
|
|
537
|
+
The `Schema` data type has a `@@` method that applies schema aspects, and it's a thin wrapper over two `Reflect#aspect` overloads: applying an aspect to a whole schema just calls `aspect(reflect)` directly, and applying one at a path uses [`Reflect#updated`](./reflect-transformer.md) to rewrite the subtree the optic points to:
|
|
525
538
|
|
|
526
539
|
```scala
|
|
527
540
|
case class Schema[A](reflect: Reflect.Bound[A]) {
|
|
528
541
|
def @@[Min >: A, Max <: A](aspect: SchemaAspect[Min, Max, Binding]): Schema[A] = ???
|
|
529
542
|
def @@[B](part: Optic[A, B], aspect: SchemaAspect[B, B, Binding]) = ???
|
|
530
543
|
}
|
|
544
|
+
|
|
545
|
+
sealed trait Reflect[F[_, _], A] {
|
|
546
|
+
def aspect[Min >: A, Max <: A](aspect: SchemaAspect[Min, Max, F]): Reflect[F, A]
|
|
547
|
+
def aspect[B, Min >: B, Max <: B](optic: Optic[A, B], aspect: SchemaAspect[Min, Max, F]): Reflect[F, A]
|
|
548
|
+
}
|
|
531
549
|
```
|
|
532
550
|
|
|
533
551
|
These methods enable us to use `@@` syntax for applying aspects to either the entire schema or a specific path within the schema using optics:
|
|
@@ -551,6 +569,8 @@ Currently, ZIO Blocks provides the following built-in schema aspects:
|
|
|
551
569
|
- `SchemaAspect.doc`: Attach documentation to schema or field
|
|
552
570
|
- `SchemaAspect.examples`: Attach example values to schema or field
|
|
553
571
|
|
|
572
|
+
The path-targeted overload doesn't fail on an optic that doesn't resolve against the schema — it falls back to the original schema unchanged, the same behavior [`Reflect#updated`](./reflect-transformer.md) has when a path finds nothing. This matters most when an optic is built by hand rather than through the `optic(_.field)` macro: a lens pointing at a field name that isn't actually on the record silently leaves the schema untouched rather than throwing.
|
|
573
|
+
|
|
554
574
|
## Modifiers
|
|
555
575
|
|
|
556
576
|
Modifiers in ZIO Blocks provide a mechanism to attach metadata and configuration to schema elements without polluting the domain types themselves. They serve as the successor to ZIO Schema 1's annotation system, with the critical advantage of being **pure data** so, unlike Scala annotations, modifiers are runtime values that can be serialized.
|
|
@@ -294,7 +294,7 @@ Structural types integrate seamlessly with ZIO Blocks' broader ecosystem:
|
|
|
294
294
|
|
|
295
295
|
### With Schema Evolution Macros
|
|
296
296
|
|
|
297
|
-
Structural schemas work with [Schema Evolution](
|
|
297
|
+
Structural schemas work with [Schema Evolution](schema-evolution/into.md) macros for cross-type conversion. When two types share the same structural shape, the conversion machinery can work across type boundaries:
|
|
298
298
|
|
|
299
299
|
```scala
|
|
300
300
|
import zio.blocks.schema.Schema
|
|
@@ -167,6 +167,68 @@ import zio.blocks.schema.json._
|
|
|
167
167
|
val jsonCodec = Person.schema.derive(JsonFormat)
|
|
168
168
|
```
|
|
169
169
|
|
|
170
|
+
## Customizing Derivation with Instance and Modifier Overrides
|
|
171
|
+
|
|
172
|
+
By default, `Deriver` automatically derives codecs for all types. But sometimes you need to customize how specific types are encoded or decoded—for example, encoding `LocalDate` as `"dd/MM/yyyy"` instead of ISO format.
|
|
173
|
+
|
|
174
|
+
ZIO Blocks provides three levels of customization, in progressive order:
|
|
175
|
+
|
|
176
|
+
1. **Type-level override** (`Deriver.withInstance`) — Override the codec for ALL occurrences of a type. Configure once, use everywhere:
|
|
177
|
+
|
|
178
|
+
```scala
|
|
179
|
+
import java.time.LocalDate
|
|
180
|
+
import java.time.format.DateTimeFormatter
|
|
181
|
+
|
|
182
|
+
// Create a custom JsonCodec for LocalDate
|
|
183
|
+
val customDateCodec: JsonCodec[LocalDate] = new JsonCodec[LocalDate] {
|
|
184
|
+
private val fmt = DateTimeFormatter.ofPattern("dd/MM/yyyy")
|
|
185
|
+
def decodeValue(in: JsonReader): LocalDate = LocalDate.parse(in.readString(), fmt)
|
|
186
|
+
def encodeValue(x: LocalDate, out: JsonWriter): Unit = out.writeVal(fmt.format(x))
|
|
187
|
+
// ... AST overrides ...
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Configure the deriver once
|
|
191
|
+
val myDeriver = JsonCodecDeriver.withInstance[LocalDate](customDateCodec)
|
|
192
|
+
|
|
193
|
+
// Use everywhere — all LocalDate fields use the custom codec
|
|
194
|
+
val codec1 = Schema[Event].deriving(myDeriver).derive
|
|
195
|
+
val codec2 = Schema[Meeting].deriving(myDeriver).derive
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
2. **Field-level override** (`Deriver.withInstance` with typeId + termName) — Override a specific field only:
|
|
199
|
+
|
|
200
|
+
```scala
|
|
201
|
+
// Only Event.date uses custom format; other LocalDate fields are unchanged
|
|
202
|
+
val deriver = JsonCodecDeriver.withInstance[Event, LocalDate](
|
|
203
|
+
TypeId.of[Event], "date", customDateCodec
|
|
204
|
+
)
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
3. **Modifier override** (`Deriver.withModifier`) — Rename fields, add aliases:
|
|
208
|
+
|
|
209
|
+
```scala
|
|
210
|
+
val deriver = JsonCodecDeriver.withModifier(
|
|
211
|
+
TypeId.of[Person], "firstName", Modifier.rename("first_name")
|
|
212
|
+
)
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
### Chaining Overrides
|
|
216
|
+
|
|
217
|
+
Overrides compose, allowing you to build complex derivers incrementally:
|
|
218
|
+
|
|
219
|
+
```scala
|
|
220
|
+
val myDeriver = JsonCodecDeriver
|
|
221
|
+
.withInstance[LocalDate](customDateCodec)
|
|
222
|
+
.withModifier(TypeId.of[Event], "name", Modifier.rename("title"))
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
### Important Notes
|
|
226
|
+
|
|
227
|
+
- `withInstance` and `withModifier` return a NEW deriver (they are immutable).
|
|
228
|
+
- When combined with `DerivationBuilder`, deriver-level instance overrides take precedence over builder-level instance overrides. Modifier override precedence is order-sensitive and should not be assumed to follow the same rule.
|
|
229
|
+
- Unknown `termName` values are silently ignored.
|
|
230
|
+
- The `B` type parameter in field-level `withInstance` is not statically checked against the actual field type.
|
|
231
|
+
|
|
170
232
|
## Example 1: Deriving a `Show` Type Class Instance
|
|
171
233
|
|
|
172
234
|
Let's say we want to derive a `Show` type class instance for any type of type `A`:
|
|
@@ -1455,7 +1517,7 @@ Now we can use the derived `Gen[Person]` instance to generate random `Person` va
|
|
|
1455
1517
|
|
|
1456
1518
|
```scala
|
|
1457
1519
|
val random = new Random(42) // Seeded for reproducible output
|
|
1458
|
-
// random: Random = scala.util.Random@
|
|
1520
|
+
// random: Random = scala.util.Random@57cbd935
|
|
1459
1521
|
|
|
1460
1522
|
Person.gen.generate(random)
|
|
1461
1523
|
// res14: Person = Person(name = "p", age = -1360544799)
|