@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.
@@ -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