@zio.dev/zio-blocks 0.0.1 → 0.0.21
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/index.md +143 -15
- package/package.json +5 -2
- package/reference/binding.md +8 -14
- package/reference/chunk.md +36 -36
- package/reference/docs.md +2 -2
- package/reference/dynamic-value.md +34 -34
- package/reference/formats.md +16 -16
- package/reference/json-schema.md +20 -20
- package/reference/json.md +47 -47
- package/reference/optics.md +51 -37
- package/reference/reflect.md +3 -3
- package/reference/registers.md +7 -7
- package/reference/schema.md +18 -18
- package/reference/syntax.md +16 -16
- package/scope.md +481 -161
- package/reference/modifier.md +0 -276
- package/reference/reflect-transform.md +0 -387
- package/reference/type-class-derivation-internals.md +0 -632
package/reference/modifier.md
DELETED
|
@@ -1,276 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
id: modifier
|
|
3
|
-
title: "Modifier"
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
`Modifier` is a sealed trait that provides a mechanism to attach metadata and configuration to schema elements. Modifiers serve as annotations for record fields, variant cases, and reflect values, enabling format-specific customization without polluting domain types.
|
|
7
|
-
|
|
8
|
-
Unlike Scala annotations, modifiers are **pure data** values that can be serialized, making them ideal for runtime introspection and cross-process schema exchange.
|
|
9
|
-
|
|
10
|
-
```scala
|
|
11
|
-
sealed trait Modifier extends StaticAnnotation
|
|
12
|
-
```
|
|
13
|
-
|
|
14
|
-
## Modifier Hierarchy
|
|
15
|
-
|
|
16
|
-
```
|
|
17
|
-
┌────────────────────────────────────────────────────────────────┐
|
|
18
|
-
│ Modifier │
|
|
19
|
-
├────────────────────────────────────────────────────────────────┤
|
|
20
|
-
│ ┌─────────────────────────────────────────────────────────┐ │
|
|
21
|
-
│ │ Modifier.Term │ │
|
|
22
|
-
│ │ (annotates record fields and variant cases) │ │
|
|
23
|
-
│ │ │ │
|
|
24
|
-
│ │ - transient() : exclude from serialization │ │
|
|
25
|
-
│ │ - rename(name) : change serialized name │ │
|
|
26
|
-
│ │ - alias(name) : add alternative name │ │
|
|
27
|
-
│ │ - config(key, val) : attach key-value metadata │ │
|
|
28
|
-
│ └─────────────────────────────────────────────────────────┘ │
|
|
29
|
-
│ ┌─────────────────────────────────────────────────────────┐ │
|
|
30
|
-
│ │ Modifier.Reflect │ │
|
|
31
|
-
│ │ (annotates reflect values / types) │ │
|
|
32
|
-
│ │ │ │
|
|
33
|
-
│ │ - config(key, val) : attach key-value metadata │ │
|
|
34
|
-
│ └─────────────────────────────────────────────────────────┘ │
|
|
35
|
-
└────────────────────────────────────────────────────────────────┘
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-
## Term Modifiers
|
|
39
|
-
|
|
40
|
-
Term modifiers annotate record fields and variant cases. They are used during schema derivation to customize how fields are serialized and deserialized.
|
|
41
|
-
|
|
42
|
-
### transient
|
|
43
|
-
|
|
44
|
-
The `transient` modifier marks a field as transient, meaning it will be excluded from serialization. This is useful for computed fields, caches, or sensitive data that shouldn't be persisted.
|
|
45
|
-
|
|
46
|
-
```scala mdoc:compile-only
|
|
47
|
-
import zio.blocks.schema._
|
|
48
|
-
|
|
49
|
-
case class User(
|
|
50
|
-
id: Long,
|
|
51
|
-
name: String,
|
|
52
|
-
@Modifier.transient cache: Map[String, String] = Map.empty
|
|
53
|
-
)
|
|
54
|
-
|
|
55
|
-
object User {
|
|
56
|
-
implicit val schema: Schema[User] = Schema.derived
|
|
57
|
-
}
|
|
58
|
-
```
|
|
59
|
-
|
|
60
|
-
When encoding a `User` to JSON, the `cache` field will be omitted:
|
|
61
|
-
|
|
62
|
-
```json
|
|
63
|
-
{"id": 1, "name": "Alice"}
|
|
64
|
-
```
|
|
65
|
-
|
|
66
|
-
During decoding, transient fields use their default values. Therefore, **transient fields must have default values** defined in the case class.
|
|
67
|
-
|
|
68
|
-
### rename
|
|
69
|
-
|
|
70
|
-
The `rename` modifier changes the serialized name of a field or variant case. This is useful when the field name in your Scala code differs from the expected name in the serialized format.
|
|
71
|
-
|
|
72
|
-
```scala mdoc:compile-only
|
|
73
|
-
import zio.blocks.schema._
|
|
74
|
-
|
|
75
|
-
case class Person(
|
|
76
|
-
@Modifier.rename("user_name") name: String,
|
|
77
|
-
@Modifier.rename("user_age") age: Int
|
|
78
|
-
)
|
|
79
|
-
|
|
80
|
-
object Person {
|
|
81
|
-
implicit val schema: Schema[Person] = Schema.derived
|
|
82
|
-
}
|
|
83
|
-
```
|
|
84
|
-
|
|
85
|
-
When encoding a `Person` to JSON:
|
|
86
|
-
|
|
87
|
-
```json
|
|
88
|
-
{"user_name": "Alice", "user_age": 30}
|
|
89
|
-
```
|
|
90
|
-
|
|
91
|
-
You can also use `rename` on variant cases to customize the discriminator value:
|
|
92
|
-
|
|
93
|
-
```scala mdoc:compile-only
|
|
94
|
-
import zio.blocks.schema._
|
|
95
|
-
|
|
96
|
-
sealed trait PaymentMethod
|
|
97
|
-
|
|
98
|
-
object PaymentMethod {
|
|
99
|
-
@Modifier.rename("credit_card")
|
|
100
|
-
case class CreditCard(number: String, cvv: String) extends PaymentMethod
|
|
101
|
-
|
|
102
|
-
@Modifier.rename("bank_transfer")
|
|
103
|
-
case class BankTransfer(iban: String) extends PaymentMethod
|
|
104
|
-
|
|
105
|
-
implicit val schema: Schema[PaymentMethod] = Schema.derived
|
|
106
|
-
}
|
|
107
|
-
```
|
|
108
|
-
|
|
109
|
-
### alias
|
|
110
|
-
|
|
111
|
-
The `alias` modifier provides an alternative name for a term during decoding. This is useful for supporting multiple names during schema evolution or data migration.
|
|
112
|
-
|
|
113
|
-
```scala mdoc:compile-only
|
|
114
|
-
import zio.blocks.schema._
|
|
115
|
-
|
|
116
|
-
@Modifier.rename("NewName")
|
|
117
|
-
@Modifier.alias("OldName")
|
|
118
|
-
@Modifier.alias("LegacyName")
|
|
119
|
-
case class MyClass(value: String)
|
|
120
|
-
|
|
121
|
-
object MyClass {
|
|
122
|
-
implicit val schema: Schema[MyClass] = Schema.derived
|
|
123
|
-
}
|
|
124
|
-
```
|
|
125
|
-
|
|
126
|
-
With this configuration:
|
|
127
|
-
- **Encoding** always uses the `rename` value: `"NewName"`
|
|
128
|
-
- **Decoding** accepts any of: `"NewName"`, `"OldName"`, or `"LegacyName"`
|
|
129
|
-
|
|
130
|
-
This pattern is particularly useful when migrating data formats without breaking compatibility with existing data.
|
|
131
|
-
|
|
132
|
-
### config
|
|
133
|
-
|
|
134
|
-
The `config` modifier attaches arbitrary key-value metadata to a term. The convention for keys is `<format>.<property>`, allowing format-specific configuration.
|
|
135
|
-
|
|
136
|
-
```scala mdoc:compile-only
|
|
137
|
-
import zio.blocks.schema._
|
|
138
|
-
|
|
139
|
-
case class Event(
|
|
140
|
-
@Modifier.config("protobuf.field-id", "1") id: Long,
|
|
141
|
-
@Modifier.config("protobuf.field-id", "2") name: String
|
|
142
|
-
)
|
|
143
|
-
|
|
144
|
-
object Event {
|
|
145
|
-
implicit val schema: Schema[Event] = Schema.derived
|
|
146
|
-
}
|
|
147
|
-
```
|
|
148
|
-
|
|
149
|
-
The `config` modifier extends both `Term` and `Reflect`, making it usable on both fields and types.
|
|
150
|
-
|
|
151
|
-
## Reflect Modifiers
|
|
152
|
-
|
|
153
|
-
Reflect modifiers annotate reflect values (types themselves). Currently, only `config` is a reflect modifier.
|
|
154
|
-
|
|
155
|
-
### Using config on Types
|
|
156
|
-
|
|
157
|
-
You can attach configuration to the type itself using the `Schema#modifier` method:
|
|
158
|
-
|
|
159
|
-
```scala mdoc:compile-only
|
|
160
|
-
import zio.blocks.schema._
|
|
161
|
-
|
|
162
|
-
case class Person(name: String, age: Int)
|
|
163
|
-
|
|
164
|
-
object Person {
|
|
165
|
-
implicit val schema: Schema[Person] = Schema.derived
|
|
166
|
-
.modifier(Modifier.config("db.table-name", "person_table"))
|
|
167
|
-
.modifier(Modifier.config("schema.version", "v2"))
|
|
168
|
-
}
|
|
169
|
-
```
|
|
170
|
-
|
|
171
|
-
Or add multiple modifiers at once:
|
|
172
|
-
|
|
173
|
-
```scala mdoc:compile-only
|
|
174
|
-
import zio.blocks.schema._
|
|
175
|
-
|
|
176
|
-
case class Person(name: String, age: Int)
|
|
177
|
-
|
|
178
|
-
object Person {
|
|
179
|
-
implicit val schema: Schema[Person] = Schema.derived
|
|
180
|
-
.modifiers(
|
|
181
|
-
Seq(
|
|
182
|
-
Modifier.config("db.table-name", "person_table"),
|
|
183
|
-
Modifier.config("schema.version", "v2")
|
|
184
|
-
)
|
|
185
|
-
)
|
|
186
|
-
}
|
|
187
|
-
```
|
|
188
|
-
|
|
189
|
-
## Programmatic Modifier Access
|
|
190
|
-
|
|
191
|
-
You can access modifiers programmatically through the `Reflect` structure:
|
|
192
|
-
|
|
193
|
-
```scala mdoc:compile-only
|
|
194
|
-
import zio.blocks.schema._
|
|
195
|
-
|
|
196
|
-
case class Person(
|
|
197
|
-
@Modifier.rename("full_name") name: String,
|
|
198
|
-
@Modifier.transient cache: String = ""
|
|
199
|
-
)
|
|
200
|
-
|
|
201
|
-
object Person {
|
|
202
|
-
implicit val schema: Schema[Person] = Schema.derived
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
// Access field modifiers through the reflect
|
|
206
|
-
val reflect = Schema[Person].reflect
|
|
207
|
-
reflect match {
|
|
208
|
-
case record: Reflect.Record[_, _] =>
|
|
209
|
-
record.fields.foreach { field =>
|
|
210
|
-
println(s"Field: ${field.name}")
|
|
211
|
-
println(s"Modifiers: ${field.modifiers}")
|
|
212
|
-
}
|
|
213
|
-
case _ => ()
|
|
214
|
-
}
|
|
215
|
-
```
|
|
216
|
-
|
|
217
|
-
## Built-in Schema Support
|
|
218
|
-
|
|
219
|
-
All modifier types have built-in `Schema` instances, enabling them to be serialized and deserialized:
|
|
220
|
-
|
|
221
|
-
```scala mdoc:compile-only
|
|
222
|
-
import zio.blocks.schema._
|
|
223
|
-
|
|
224
|
-
// Schema instances for individual modifiers
|
|
225
|
-
Schema[Modifier.transient]
|
|
226
|
-
Schema[Modifier.rename]
|
|
227
|
-
Schema[Modifier.alias]
|
|
228
|
-
Schema[Modifier.config]
|
|
229
|
-
|
|
230
|
-
// Schema instances for modifier traits
|
|
231
|
-
Schema[Modifier.Term]
|
|
232
|
-
Schema[Modifier.Reflect]
|
|
233
|
-
Schema[Modifier]
|
|
234
|
-
```
|
|
235
|
-
|
|
236
|
-
This enables scenarios like:
|
|
237
|
-
- Serializing schema metadata across process boundaries
|
|
238
|
-
- Storing schema configuration in databases
|
|
239
|
-
- Building schema registries with full modifier information
|
|
240
|
-
|
|
241
|
-
## Format Support
|
|
242
|
-
|
|
243
|
-
Different serialization formats interpret modifiers according to their semantics:
|
|
244
|
-
|
|
245
|
-
| Modifier | JSON | BSON | Avro | Protobuf |
|
|
246
|
-
|-------------|------|------|------|----------|
|
|
247
|
-
| `transient` | Field omitted | Field omitted | Field omitted | Field omitted |
|
|
248
|
-
| `rename` | Custom field name | Custom field name | Custom field name | Custom field name |
|
|
249
|
-
| `alias` | Accepts alternatives | Accepts alternatives | - | - |
|
|
250
|
-
| `config` | Format-specific | Format-specific | Format-specific | Format-specific |
|
|
251
|
-
|
|
252
|
-
## Best Practices
|
|
253
|
-
|
|
254
|
-
1. **Use `rename` for external APIs**: When integrating with external systems that use different naming conventions (snake_case vs camelCase), use `rename` to match the expected format.
|
|
255
|
-
|
|
256
|
-
2. **Use `alias` for migrations**: When evolving your data model, add `alias` modifiers to support reading old data while writing with new names.
|
|
257
|
-
|
|
258
|
-
3. **Use `transient` sparingly**: Only mark fields as transient when they are truly derived or temporary. Remember that transient fields need default values.
|
|
259
|
-
|
|
260
|
-
4. **Use namespaced keys for `config`**: Follow the `<format>.<property>` convention to avoid conflicts between different formats or tools.
|
|
261
|
-
|
|
262
|
-
5. **Combine modifiers**: You can apply multiple modifiers to the same field:
|
|
263
|
-
|
|
264
|
-
```scala mdoc:compile-only
|
|
265
|
-
import zio.blocks.schema._
|
|
266
|
-
|
|
267
|
-
case class Document(
|
|
268
|
-
@Modifier.rename("doc_id")
|
|
269
|
-
@Modifier.config("protobuf.field-id", "1")
|
|
270
|
-
id: String
|
|
271
|
-
)
|
|
272
|
-
|
|
273
|
-
object Document {
|
|
274
|
-
implicit val schema: Schema[Document] = Schema.derived
|
|
275
|
-
}
|
|
276
|
-
```
|
|
@@ -1,387 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
id: reflect-transform
|
|
3
|
-
title: "Reflect Transform"
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# The Transform Method: Architectural Pattern and Design Guide
|
|
7
|
-
|
|
8
|
-
The `transform` method is a fundamental operation on `Reflect` that enables systematic transformation of schema trees. This document explains the architectural patterns, design decisions, and provides a comprehensive guide for understanding and using `transform`.
|
|
9
|
-
|
|
10
|
-
## Overview
|
|
11
|
-
|
|
12
|
-
The `transform` method converts a `Reflect[F, A]` into a `Reflect[G, A]` by recursively traversing the schema tree and applying a `ReflectTransformer[F, G]` at each node:
|
|
13
|
-
|
|
14
|
-
```scala
|
|
15
|
-
def transform[G[_, _]](path: DynamicOptic, f: ReflectTransformer[F, G]): Lazy[Reflect[G, A]]
|
|
16
|
-
```
|
|
17
|
-
|
|
18
|
-
**Key Components:**
|
|
19
|
-
- **`path: DynamicOptic`**: Tracks the current location in the schema tree
|
|
20
|
-
- **`f: ReflectTransformer[F, G]`**: The transformation strategy to apply
|
|
21
|
-
- **`Lazy[...]`**: Enables handling of recursive schemas without stack overflow
|
|
22
|
-
|
|
23
|
-
## Architectural Pattern: Visitor with Lazy Evaluation
|
|
24
|
-
|
|
25
|
-
The `transform` method implements a variant of the **Visitor Pattern** combined with **Lazy Evaluation** to handle the unique challenges of schema transformation:
|
|
26
|
-
|
|
27
|
-
```
|
|
28
|
-
┌─────────────────────────────────────────────────────────────┐
|
|
29
|
-
│ Transform Architecture │
|
|
30
|
-
├─────────────────────────────────────────────────────────────┤
|
|
31
|
-
│ │
|
|
32
|
-
│ Reflect[F, A] │
|
|
33
|
-
│ │ │
|
|
34
|
-
│ ▼ │
|
|
35
|
-
│ ┌─────────────┐ ┌──────────────────────┐ │
|
|
36
|
-
│ │ transform │───▶│ ReflectTransformer │ │
|
|
37
|
-
│ │ method │ │ [F, G] │ │
|
|
38
|
-
│ └─────────────┘ └──────────────────────┘ │
|
|
39
|
-
│ │ │ │
|
|
40
|
-
│ │ ▼ │
|
|
41
|
-
│ │ ┌──────────────────┐ │
|
|
42
|
-
│ │ │ transformRecord │ │
|
|
43
|
-
│ │ │ transformVariant │ │
|
|
44
|
-
│ │ │ transformSeq │ │
|
|
45
|
-
│ │ │ ... │ │
|
|
46
|
-
│ │ └──────────────────┘ │
|
|
47
|
-
│ │ │ │
|
|
48
|
-
│ ▼ ▼ │
|
|
49
|
-
│ Lazy[Reflect[G, A]] │
|
|
50
|
-
│ │
|
|
51
|
-
└─────────────────────────────────────────────────────────────┘
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
### Why This Pattern?
|
|
55
|
-
|
|
56
|
-
1. **Separation of Concerns**: The traversal logic stays in `Reflect`, while transformation logic lives in `ReflectTransformer`
|
|
57
|
-
2. **Extensibility**: New transformations can be added without modifying `Reflect`
|
|
58
|
-
3. **Type Safety**: The type system ensures consistent transformation of the binding type parameter
|
|
59
|
-
4. **Composability**: Transformers can be composed and reused
|
|
60
|
-
|
|
61
|
-
## Design Decisions
|
|
62
|
-
|
|
63
|
-
### Decision 1: Two-Phase Transformation (Children First)
|
|
64
|
-
|
|
65
|
-
Each `transform` implementation follows a **children-first** pattern:
|
|
66
|
-
|
|
67
|
-
```scala
|
|
68
|
-
// From Reflect.Record.transform
|
|
69
|
-
def transform[G[_, _]](path: DynamicOptic, f: ReflectTransformer[F, G]): Lazy[Record[G, A]] =
|
|
70
|
-
for {
|
|
71
|
-
// Phase 1: Transform all children first
|
|
72
|
-
fields <- Lazy.foreach(fields)(_.transform(path, Term.Type.Record, f))
|
|
73
|
-
// Phase 2: Then transform the current node with already-transformed children
|
|
74
|
-
record <- f.transformRecord(path, fields, typeId, recordBinding, doc, modifiers, ...)
|
|
75
|
-
} yield record
|
|
76
|
-
```
|
|
77
|
-
|
|
78
|
-
**Rationale:**
|
|
79
|
-
- Ensures children are transformed before parents need them
|
|
80
|
-
- Enables bottom-up construction of the transformed tree
|
|
81
|
-
- Allows transformers to inspect already-transformed children
|
|
82
|
-
|
|
83
|
-
### Decision 2: Path Tracking with DynamicOptic
|
|
84
|
-
|
|
85
|
-
The `path` parameter tracks the current location in the schema tree:
|
|
86
|
-
|
|
87
|
-
```scala
|
|
88
|
-
// Sequence appends 'elements' to the path before transforming its element
|
|
89
|
-
def transform[G[_, _]](path: DynamicOptic, f: ReflectTransformer[F, G]): Lazy[Sequence[G, A, C]] =
|
|
90
|
-
for {
|
|
91
|
-
element <- element.transform(path(DynamicOptic.elements), f) // path + /elements
|
|
92
|
-
sequence <- f.transformSequence(path, element, ...)
|
|
93
|
-
} yield sequence
|
|
94
|
-
|
|
95
|
-
// Map tracks both keys and values separately
|
|
96
|
-
def transform[G[_, _]](path: DynamicOptic, f: ReflectTransformer[F, G]): Lazy[Map[G, K, V, M]] =
|
|
97
|
-
for {
|
|
98
|
-
key <- key.transform(path(DynamicOptic.mapKeys), f) // path + /mapKeys
|
|
99
|
-
value <- value.transform(path(DynamicOptic.mapValues), f) // path + /mapValues
|
|
100
|
-
map <- f.transformMap(path, key, value, ...)
|
|
101
|
-
} yield map
|
|
102
|
-
```
|
|
103
|
-
|
|
104
|
-
**Use Cases for Path Tracking:**
|
|
105
|
-
- Applying different transformations based on location
|
|
106
|
-
- Error reporting with precise location information
|
|
107
|
-
- Conditional transformation rules
|
|
108
|
-
- Auditing and logging transformations
|
|
109
|
-
|
|
110
|
-
### Decision 3: Lazy Evaluation for Recursion
|
|
111
|
-
|
|
112
|
-
The return type `Lazy[Reflect[G, A]]` is critical for handling recursive types:
|
|
113
|
-
|
|
114
|
-
```scala
|
|
115
|
-
// Deferred uses caching to handle recursive schemas
|
|
116
|
-
case class Deferred[F[_, _], A](...) extends Reflect[F, A] {
|
|
117
|
-
def transform[G[_, _]](path: DynamicOptic, f: ReflectTransformer[F, G]): Lazy[Reflect[G, A]] =
|
|
118
|
-
Lazy {
|
|
119
|
-
val c = cache.get
|
|
120
|
-
val key = new IdentityTuple(this, f)
|
|
121
|
-
val cached = c.get(key)
|
|
122
|
-
if (cached ne null) cached.asInstanceOf[Reflect[G, A]]
|
|
123
|
-
else {
|
|
124
|
-
// Create deferred wrapper and cache BEFORE recursing
|
|
125
|
-
val result = Deferred(() => value.transform(path, f).force, ...)
|
|
126
|
-
c.put(key, result)
|
|
127
|
-
result
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
```
|
|
132
|
-
|
|
133
|
-
**How Recursion is Handled:**
|
|
134
|
-
1. `Deferred` nodes check a thread-local cache before transforming
|
|
135
|
-
2. If found, return the cached (transformed) `Deferred` immediately
|
|
136
|
-
3. If not found, create a new `Deferred` and cache it BEFORE recursing
|
|
137
|
-
4. The lazy thunk in the new `Deferred` will eventually call `transform` on the wrapped value
|
|
138
|
-
|
|
139
|
-
This pattern breaks infinite recursion by returning a cached reference.
|
|
140
|
-
|
|
141
|
-
### Decision 4: Node-Specific Transform Methods
|
|
142
|
-
|
|
143
|
-
Each `Reflect` node type has its own implementation:
|
|
144
|
-
|
|
145
|
-
| Node Type | Transforms Children | Path Updates |
|
|
146
|
-
|--------------|-----------------------------|-----------------------|
|
|
147
|
-
| `Record` | All fields | Field names |
|
|
148
|
-
| `Variant` | All cases | Case names |
|
|
149
|
-
| `Sequence` | Element type | `/elements` |
|
|
150
|
-
| `Map` | Key and value types | `/mapKeys`, `/mapValues` |
|
|
151
|
-
| `Wrapper` | Wrapped type | (none) |
|
|
152
|
-
| `Primitive` | (none - leaf node) | (none) |
|
|
153
|
-
| `Dynamic` | (none - leaf node) | (none) |
|
|
154
|
-
| `Deferred` | Wrapped value (lazy) | (preserved) |
|
|
155
|
-
|
|
156
|
-
## The ReflectTransformer Trait
|
|
157
|
-
|
|
158
|
-
The `ReflectTransformer[F, G]` trait defines the transformation strategy:
|
|
159
|
-
|
|
160
|
-
```scala
|
|
161
|
-
trait ReflectTransformer[-F[_, _], G[_, _]] {
|
|
162
|
-
def transformRecord[A](
|
|
163
|
-
path: DynamicOptic,
|
|
164
|
-
fields: IndexedSeq[Term[G, A, ?]], // Already transformed
|
|
165
|
-
typeId: TypeId[A],
|
|
166
|
-
metadata: F[BindingType.Record, A], // Original binding
|
|
167
|
-
doc: Doc,
|
|
168
|
-
modifiers: Seq[Modifier.Reflect],
|
|
169
|
-
storedDefaultValue: Option[DynamicValue],
|
|
170
|
-
storedExamples: collection.immutable.Seq[DynamicValue]
|
|
171
|
-
): Lazy[Reflect.Record[G, A]]
|
|
172
|
-
|
|
173
|
-
def transformVariant[A](...): Lazy[Reflect.Variant[G, A]]
|
|
174
|
-
def transformSequence[A, C[_]](...): Lazy[Reflect.Sequence[G, A, C]]
|
|
175
|
-
def transformMap[K, V, M[_, _]](...): Lazy[Reflect.Map[G, K, V, M]]
|
|
176
|
-
def transformDynamic(...): Lazy[Reflect.Dynamic[G]]
|
|
177
|
-
def transformPrimitive[A](...): Lazy[Reflect.Primitive[G, A]]
|
|
178
|
-
def transformWrapper[A, B](...): Lazy[Reflect.Wrapper[G, A, B]]
|
|
179
|
-
}
|
|
180
|
-
```
|
|
181
|
-
|
|
182
|
-
**Key Observation:** Each method receives already-transformed children plus the original metadata. This enables:
|
|
183
|
-
- Inspecting transformed children before deciding how to transform the current node
|
|
184
|
-
- Combining information from multiple children
|
|
185
|
-
- Conditional transformation based on child structure
|
|
186
|
-
|
|
187
|
-
### The OnlyMetadata Helper
|
|
188
|
-
|
|
189
|
-
For transformations that only need to change the binding type:
|
|
190
|
-
|
|
191
|
-
```scala
|
|
192
|
-
abstract class OnlyMetadata[F[_, _], G[_, _]] extends ReflectTransformer[F, G] {
|
|
193
|
-
// Only this method needs implementation
|
|
194
|
-
def transformMetadata[K, A](f: F[K, A]): Lazy[G[K, A]]
|
|
195
|
-
|
|
196
|
-
// All node-specific methods are provided automatically
|
|
197
|
-
def transformRecord[A](
|
|
198
|
-
path: DynamicOptic,
|
|
199
|
-
fields: IndexedSeq[Term[G, A, ?]],
|
|
200
|
-
typeId: TypeId[A],
|
|
201
|
-
metadata: F[BindingType.Record, A],
|
|
202
|
-
...
|
|
203
|
-
): Lazy[Reflect.Record[G, A]] =
|
|
204
|
-
for {
|
|
205
|
-
binding <- transformMetadata(metadata)
|
|
206
|
-
} yield new Reflect.Record(fields, typeId, binding, ...)
|
|
207
|
-
|
|
208
|
-
// ... similar for other node types
|
|
209
|
-
}
|
|
210
|
-
```
|
|
211
|
-
|
|
212
|
-
## Built-in Transformers
|
|
213
|
-
|
|
214
|
-
### noBinding(): Remove Runtime Bindings
|
|
215
|
-
|
|
216
|
-
The most common built-in transformer strips all runtime bindings:
|
|
217
|
-
|
|
218
|
-
```scala
|
|
219
|
-
// Definition
|
|
220
|
-
def noBinding[F[_, _]](): ReflectTransformer[F, NoBinding]
|
|
221
|
-
|
|
222
|
-
// Usage
|
|
223
|
-
val boundSchema: Reflect[Binding, Person] = Schema[Person].reflect
|
|
224
|
-
val unboundSchema: Reflect[NoBinding, Person] =
|
|
225
|
-
boundSchema.transform(DynamicOptic.root, ReflectTransformer.noBinding()).force
|
|
226
|
-
|
|
227
|
-
// Or use the convenience property
|
|
228
|
-
val unboundSchema: Reflect[NoBinding, Person] = boundSchema.noBinding
|
|
229
|
-
```
|
|
230
|
-
|
|
231
|
-
**Use Cases:**
|
|
232
|
-
- Schema serialization (bindings contain functions that can't be serialized)
|
|
233
|
-
- Schema comparison (compare structure without runtime details)
|
|
234
|
-
- Schema transmission over the network
|
|
235
|
-
|
|
236
|
-
## Practical Example: Custom Transformer
|
|
237
|
-
|
|
238
|
-
Here's a complete example of a custom transformer that adds documentation to all fields:
|
|
239
|
-
|
|
240
|
-
```scala
|
|
241
|
-
import zio.blocks.schema._
|
|
242
|
-
import zio.blocks.schema.binding._
|
|
243
|
-
|
|
244
|
-
object DocumentingTransformer extends ReflectTransformer.OnlyMetadata[Binding, Binding] {
|
|
245
|
-
// For this transformer, we keep bindings unchanged
|
|
246
|
-
def transformMetadata[K, A](f: Binding[K, A]): Lazy[Binding[K, A]] = Lazy(f)
|
|
247
|
-
|
|
248
|
-
// Override record transformation to add docs
|
|
249
|
-
override def transformRecord[A](
|
|
250
|
-
path: DynamicOptic,
|
|
251
|
-
fields: IndexedSeq[Term[Binding, A, ?]],
|
|
252
|
-
typeId: TypeId[A],
|
|
253
|
-
metadata: Binding[BindingType.Record, A],
|
|
254
|
-
doc: Doc,
|
|
255
|
-
modifiers: Seq[Modifier.Reflect],
|
|
256
|
-
storedDefaultValue: Option[DynamicValue],
|
|
257
|
-
storedExamples: collection.immutable.Seq[DynamicValue]
|
|
258
|
-
): Lazy[Reflect.Record[Binding, A]] = {
|
|
259
|
-
// Add path-based documentation
|
|
260
|
-
val enhancedDoc = doc match {
|
|
261
|
-
case Doc.Empty => Doc.Text(s"Record at path: ${path}")
|
|
262
|
-
case existing => existing
|
|
263
|
-
}
|
|
264
|
-
Lazy(new Reflect.Record(
|
|
265
|
-
fields, typeId, metadata, enhancedDoc, modifiers,
|
|
266
|
-
storedDefaultValue, storedExamples
|
|
267
|
-
))
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
// Usage
|
|
272
|
-
val documented = schema.reflect.transform(DynamicOptic.root, DocumentingTransformer).force
|
|
273
|
-
```
|
|
274
|
-
|
|
275
|
-
## Transform Flow Visualization
|
|
276
|
-
|
|
277
|
-
Here's how transform flows through a nested schema:
|
|
278
|
-
|
|
279
|
-
```
|
|
280
|
-
Input: Reflect[Binding, Person]
|
|
281
|
-
where Person(name: String, address: Address)
|
|
282
|
-
Address(street: String, city: String)
|
|
283
|
-
|
|
284
|
-
Transform Flow:
|
|
285
|
-
═══════════════
|
|
286
|
-
|
|
287
|
-
1. Person.transform(root, f)
|
|
288
|
-
│
|
|
289
|
-
├─▶ 2. name.transform(root/name, f)
|
|
290
|
-
│ └─▶ f.transformPrimitive(root/name, String, ...)
|
|
291
|
-
│ └─▶ Lazy[Reflect.Primitive[G, String]]
|
|
292
|
-
│
|
|
293
|
-
├─▶ 3. address.transform(root/address, f)
|
|
294
|
-
│ │
|
|
295
|
-
│ ├─▶ 4. street.transform(root/address/street, f)
|
|
296
|
-
│ │ └─▶ f.transformPrimitive(root/address/street, String, ...)
|
|
297
|
-
│ │ └─▶ Lazy[Reflect.Primitive[G, String]]
|
|
298
|
-
│ │
|
|
299
|
-
│ ├─▶ 5. city.transform(root/address/city, f)
|
|
300
|
-
│ │ └─▶ f.transformPrimitive(root/address/city, String, ...)
|
|
301
|
-
│ │ └─▶ Lazy[Reflect.Primitive[G, String]]
|
|
302
|
-
│ │
|
|
303
|
-
│ └─▶ 6. f.transformRecord(root/address, [street', city'], Address, ...)
|
|
304
|
-
│ └─▶ Lazy[Reflect.Record[G, Address]]
|
|
305
|
-
│
|
|
306
|
-
└─▶ 7. f.transformRecord(root, [name', address'], Person, ...)
|
|
307
|
-
└─▶ Lazy[Reflect.Record[G, Person]]
|
|
308
|
-
|
|
309
|
-
Output: Lazy[Reflect[G, Person]]
|
|
310
|
-
```
|
|
311
|
-
|
|
312
|
-
## Best Practices
|
|
313
|
-
|
|
314
|
-
### 1. Always Start from Root
|
|
315
|
-
|
|
316
|
-
```scala
|
|
317
|
-
// Good: Start with root path
|
|
318
|
-
schema.reflect.transform(DynamicOptic.root, transformer)
|
|
319
|
-
|
|
320
|
-
// Also good: Use the noBinding convenience
|
|
321
|
-
schema.reflect.noBinding
|
|
322
|
-
```
|
|
323
|
-
|
|
324
|
-
### 2. Handle Lazy Properly
|
|
325
|
-
|
|
326
|
-
```scala
|
|
327
|
-
// Be careful with when you force
|
|
328
|
-
val lazy = schema.reflect.transform(path, f)
|
|
329
|
-
|
|
330
|
-
// Good: Force when you need the result
|
|
331
|
-
val result = lazy.force
|
|
332
|
-
|
|
333
|
-
// Good: Chain transformations before forcing
|
|
334
|
-
val chained = for {
|
|
335
|
-
step1 <- schema.reflect.transform(path, f1)
|
|
336
|
-
step2 <- step1.transform(path, f2)
|
|
337
|
-
} yield step2
|
|
338
|
-
val result = chained.force
|
|
339
|
-
```
|
|
340
|
-
|
|
341
|
-
### 3. Use OnlyMetadata When Possible
|
|
342
|
-
|
|
343
|
-
```scala
|
|
344
|
-
// If you only need to transform bindings, use OnlyMetadata
|
|
345
|
-
object MyTransformer extends ReflectTransformer.OnlyMetadata[Binding, MyBinding] {
|
|
346
|
-
def transformMetadata[K, A](f: Binding[K, A]): Lazy[MyBinding[K, A]] =
|
|
347
|
-
Lazy(convertBinding(f))
|
|
348
|
-
}
|
|
349
|
-
```
|
|
350
|
-
|
|
351
|
-
### 4. Consider Path for Context-Aware Transformations
|
|
352
|
-
|
|
353
|
-
```scala
|
|
354
|
-
override def transformPrimitive[A](
|
|
355
|
-
path: DynamicOptic,
|
|
356
|
-
primitiveType: PrimitiveType[A],
|
|
357
|
-
...
|
|
358
|
-
): Lazy[Reflect.Primitive[G, A]] = {
|
|
359
|
-
// Different transformation based on where we are in the schema
|
|
360
|
-
val pathStr = path.toString
|
|
361
|
-
if (pathStr.contains("password") || pathStr.contains("secret")) {
|
|
362
|
-
// Add security modifier for sensitive fields
|
|
363
|
-
Lazy(new Reflect.Primitive(..., modifiers = modifiers :+ Modifier.sensitive))
|
|
364
|
-
} else {
|
|
365
|
-
Lazy(new Reflect.Primitive(...))
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
```
|
|
369
|
-
|
|
370
|
-
## Summary
|
|
371
|
-
|
|
372
|
-
The `transform` method is a powerful mechanism for systematic schema transformation that:
|
|
373
|
-
|
|
374
|
-
| Aspect | Description |
|
|
375
|
-
|--------|-------------|
|
|
376
|
-
| **Pattern** | Visitor pattern with lazy evaluation |
|
|
377
|
-
| **Traversal** | Bottom-up (children first, then parent) |
|
|
378
|
-
| **Recursion** | Handled via `Lazy` and `Deferred` caching |
|
|
379
|
-
| **Path Tracking** | Via `DynamicOptic` for context-aware transforms |
|
|
380
|
-
| **Extensibility** | Custom `ReflectTransformer` implementations |
|
|
381
|
-
| **Type Safety** | Preserves schema type `A` while changing binding `F` to `G` |
|
|
382
|
-
|
|
383
|
-
Understanding `transform` is essential for:
|
|
384
|
-
- Implementing custom schema codecs
|
|
385
|
-
- Building schema analysis tools
|
|
386
|
-
- Creating schema-to-schema transformations
|
|
387
|
-
- Understanding how ZIO Blocks handles schema serialization
|