@zio.dev/zio-blocks 0.0.1 → 0.0.22
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 +163 -15
- package/package.json +5 -2
- package/path-interpolator.md +24 -23
- package/reference/binding.md +8 -14
- package/reference/chunk.md +36 -36
- package/reference/codec.md +384 -0
- package/reference/docs.md +2 -2
- package/reference/dynamic-optic.md +392 -0
- package/reference/dynamic-value.md +34 -34
- package/reference/formats.md +84 -28
- package/reference/json-schema.md +33 -30
- package/reference/json.md +47 -47
- package/reference/lazy.md +361 -0
- package/reference/modifier.md +151 -87
- 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 +27 -27
- package/reference/type-class-derivation.md +1959 -0
- package/scope.md +521 -203
- package/sidebars.js +6 -1
- package/reference/reflect-transform.md +0 -387
- package/reference/type-class-derivation-internals.md +0 -632
package/reference/modifier.md
CHANGED
|
@@ -5,71 +5,143 @@ title: "Modifier"
|
|
|
5
5
|
|
|
6
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
7
|
|
|
8
|
-
|
|
8
|
+
Modifiers are designed to be **pure data** values that can be serialized, making them ideal for runtime introspection and cross-process schema exchange. When deriving schemas, modifiers are collected and attached to the corresponding fields or types, allowing codecs to read and interpret them accordingly. They are extended with `StaticAnnotation` to also support annotation syntax:
|
|
9
9
|
|
|
10
10
|
```scala
|
|
11
11
|
sealed trait Modifier extends StaticAnnotation
|
|
12
|
+
object Modifier {
|
|
13
|
+
sealed trait Term extends Modifier
|
|
14
|
+
// ... term modifiers (transient, rename, alias, config) ...
|
|
15
|
+
sealed trait Reflect extends Modifier
|
|
16
|
+
// ... reflect modifiers (config) ...
|
|
17
|
+
}
|
|
12
18
|
```
|
|
13
19
|
|
|
14
|
-
|
|
20
|
+
Modifiers can be applied in two ways:
|
|
15
21
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
22
|
+
1. **Programmatic API**: Using the `Schema#modifier` and `Schema#modifiers` methods to attach modifiers to the entire schema or, for field-level modifiers, attach them to specific fields using optics when deriving codecs. This approach keeps your domain types clean and allows you to separate schema configuration from your data model:
|
|
23
|
+
|
|
24
|
+
```scala
|
|
25
|
+
import zio.blocks.schema._
|
|
26
|
+
import zio.blocks.schema.json._
|
|
27
|
+
|
|
28
|
+
// Clean domain type - zero dependencies
|
|
29
|
+
case class User(
|
|
30
|
+
id: String,
|
|
31
|
+
name: String,
|
|
32
|
+
cache: Map[String, String] = Map.empty
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
// Modifiers applied separately to schema and codecs
|
|
36
|
+
object User extends CompanionOptics[User] {
|
|
37
|
+
implicit val schema: Schema[User] = Schema
|
|
38
|
+
.derived[User]
|
|
39
|
+
.modifier(Modifier.config("db.table-name", "users"))
|
|
40
|
+
|
|
41
|
+
implicit val jsonCodec: JsonBinaryCodec[User] =
|
|
42
|
+
schema
|
|
43
|
+
.deriving[JsonBinaryCodec](JsonBinaryCodecDeriver)
|
|
44
|
+
.modifier(User.name, Modifier.rename("username"))
|
|
45
|
+
.modifier(User.cache, Modifier.transient())
|
|
46
|
+
.derive
|
|
47
|
+
|
|
48
|
+
lazy val id : Lens[User, String] = $(_.id)
|
|
49
|
+
lazy val name : Lens[User, String] = $(_.name)
|
|
50
|
+
lazy val cache: Lens[User, Map[String, String]] = $(_.cache)
|
|
51
|
+
}
|
|
36
52
|
```
|
|
37
53
|
|
|
38
|
-
|
|
54
|
+
In the above example, we derived a JSON codec for `User` and applied the `rename` and `transient` modifiers to the `name` and `cache` fields respectively, while keeping the domain type free of any schema-related annotations. Now when encoding a `User` to JSON, the `name` field will be serialized as `username`, and the `cache` field will be omitted. During decoding, the codec will look for `username` in the input JSON and populate the `name` field accordingly:
|
|
39
55
|
|
|
40
|
-
|
|
56
|
+
```scala
|
|
57
|
+
val user = User(
|
|
58
|
+
id = "123",
|
|
59
|
+
name = "Alice",
|
|
60
|
+
cache = Map("lastLogin" -> "2024-06-01T12:00:00Z")
|
|
61
|
+
)
|
|
62
|
+
val json: String = User.jsonCodec.encodeToString(user)
|
|
63
|
+
println(json)
|
|
64
|
+
// Prints: {"id":"123","username":"Alice"}
|
|
65
|
+
val decodedUser: Either[SchemaError, User] = User.jsonCodec.decode(json)
|
|
66
|
+
println(decodedUser)
|
|
67
|
+
// Prints: Right(User(123,Alice,Map()))
|
|
68
|
+
```
|
|
41
69
|
|
|
42
|
-
|
|
70
|
+
Please note that when deriving codecs, you can access these modifiers programmatically, allowing you to build custom logic based on the presence of certain modifiers. For example, your SQL codec could check for the presence of `db.table-name` in the schema modifiers to determine which table to read from or write to.
|
|
43
71
|
|
|
44
|
-
|
|
72
|
+
2. **Annotation Syntax**: Using the `@` syntax to annotate fields and cases directly in your case classes and sealed traits. These annotations are processed during schema derivation to attach the corresponding modifiers to the schema elements. At runtime, you can access these modifiers through the `Reflect` structure of the schema.
|
|
45
73
|
|
|
46
|
-
```scala
|
|
74
|
+
```scala
|
|
47
75
|
import zio.blocks.schema._
|
|
76
|
+
import zio.blocks.schema.Modifier._
|
|
48
77
|
|
|
78
|
+
@Modifier.config("db.table-name", "users")
|
|
49
79
|
case class User(
|
|
50
|
-
id:
|
|
51
|
-
name: String,
|
|
52
|
-
@Modifier.transient cache: Map[String, String] = Map.empty
|
|
80
|
+
id: String,
|
|
81
|
+
@Modifier.rename("username") name: String,
|
|
82
|
+
@Modifier.transient() cache: Map[String, String] = Map.empty
|
|
53
83
|
)
|
|
54
84
|
|
|
55
|
-
object User {
|
|
56
|
-
implicit val schema: Schema[User] =
|
|
85
|
+
object User extends CompanionOptics[User] {
|
|
86
|
+
implicit val schema: Schema[User] =
|
|
87
|
+
Schema.derived[User]
|
|
88
|
+
|
|
89
|
+
implicit val jsonCodec: JsonBinaryCodec[User] =
|
|
90
|
+
schema
|
|
91
|
+
.derive[JsonBinaryCodec](JsonBinaryCodecDeriver)
|
|
57
92
|
}
|
|
58
93
|
```
|
|
59
94
|
|
|
60
|
-
|
|
95
|
+
In this example, we applied the same modifiers as in the programmatic example, but using annotation syntax directly on the case class fields. Let's try encoding and decoding a `User` instance:
|
|
96
|
+
|
|
97
|
+
```scala
|
|
98
|
+
val user = User(
|
|
99
|
+
id = "123",
|
|
100
|
+
name = "Alice",
|
|
101
|
+
cache = Map("lastLogin" -> "2024-06-01T12:00:00Z")
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
val json: String = User.jsonCodec.encodeToString(user)
|
|
105
|
+
println(json)
|
|
106
|
+
// Prints: {"id":"123","username":"Alice"}
|
|
107
|
+
val decodedUser: Either[SchemaError, User] = User.jsonCodec.decode(json)
|
|
108
|
+
println(decodedUser)
|
|
109
|
+
// Prints: Right(User(123,Alice,Map()))
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Modifier Hierarchy
|
|
113
|
+
|
|
114
|
+
Modifiers are organized into two main categories:
|
|
115
|
+
|
|
116
|
+
1. **Term modifiers** - annotate record fields or variant cases (the data structure elements)
|
|
117
|
+
2. **Reflect modifiers** - annotate schemas/reflect values themselves (the metadata about types)
|
|
61
118
|
|
|
62
|
-
```json
|
|
63
|
-
{"id": 1, "name": "Alice"}
|
|
64
119
|
```
|
|
120
|
+
Modifier
|
|
121
|
+
├── Modifier.Term (annotates record fields and variant cases)
|
|
122
|
+
│ ├── transient() : exclude from serialization
|
|
123
|
+
│ ├── rename(name) : change serialized name
|
|
124
|
+
│ ├── alias(name) : add alternative name
|
|
125
|
+
│ └── config(key, val) : attach key-value metadata
|
|
126
|
+
└── Modifier.Reflect (annotates reflect values / types)
|
|
127
|
+
└── config(key, val) : attach key-value metadata
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
As you can see, `config` is the only modifier that extends both `Term` and `Reflect`, allowing it to be used on both fields and types.
|
|
131
|
+
|
|
132
|
+
## Term Modifiers
|
|
133
|
+
|
|
134
|
+
Term modifiers annotate record fields and variant cases. They are used to control how individual fields or cases are serialized and deserialized, as well as to attach additional metadata that can be interpreted by codecs or other tools.
|
|
135
|
+
|
|
136
|
+
### transient
|
|
65
137
|
|
|
66
|
-
|
|
138
|
+
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.
|
|
67
139
|
|
|
68
140
|
### rename
|
|
69
141
|
|
|
70
142
|
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
143
|
|
|
72
|
-
```scala
|
|
144
|
+
```scala
|
|
73
145
|
import zio.blocks.schema._
|
|
74
146
|
|
|
75
147
|
case class Person(
|
|
@@ -82,15 +154,9 @@ object Person {
|
|
|
82
154
|
}
|
|
83
155
|
```
|
|
84
156
|
|
|
85
|
-
When encoding a `Person` to JSON:
|
|
86
|
-
|
|
87
|
-
```json
|
|
88
|
-
{"user_name": "Alice", "user_age": 30}
|
|
89
|
-
```
|
|
90
|
-
|
|
91
157
|
You can also use `rename` on variant cases to customize the discriminator value:
|
|
92
158
|
|
|
93
|
-
```scala
|
|
159
|
+
```scala
|
|
94
160
|
import zio.blocks.schema._
|
|
95
161
|
|
|
96
162
|
sealed trait PaymentMethod
|
|
@@ -110,13 +176,15 @@ object PaymentMethod {
|
|
|
110
176
|
|
|
111
177
|
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
178
|
|
|
113
|
-
```scala
|
|
179
|
+
```scala
|
|
114
180
|
import zio.blocks.schema._
|
|
115
181
|
|
|
116
|
-
|
|
117
|
-
@Modifier.
|
|
118
|
-
@Modifier.alias("
|
|
119
|
-
|
|
182
|
+
case class MyClass(
|
|
183
|
+
@Modifier.rename("NewName")
|
|
184
|
+
@Modifier.alias("OldName")
|
|
185
|
+
@Modifier.alias("LegacyName")
|
|
186
|
+
value: String
|
|
187
|
+
)
|
|
120
188
|
|
|
121
189
|
object MyClass {
|
|
122
190
|
implicit val schema: Schema[MyClass] = Schema.derived
|
|
@@ -131,9 +199,9 @@ This pattern is particularly useful when migrating data formats without breaking
|
|
|
131
199
|
|
|
132
200
|
### config
|
|
133
201
|
|
|
134
|
-
The `config` modifier attaches arbitrary key-value metadata to a term. The convention for keys is `<format>.<property>`, allowing format-specific configuration.
|
|
202
|
+
The `config` modifier attaches arbitrary key-value metadata to a term (record fields or variant cases) or a type itself. The convention for keys is `<format>.<property>`, allowing format-specific configuration.
|
|
135
203
|
|
|
136
|
-
```scala
|
|
204
|
+
```scala
|
|
137
205
|
import zio.blocks.schema._
|
|
138
206
|
|
|
139
207
|
case class Event(
|
|
@@ -146,17 +214,17 @@ object Event {
|
|
|
146
214
|
}
|
|
147
215
|
```
|
|
148
216
|
|
|
149
|
-
The `config` modifier extends both `Term` and `Reflect`, making it usable on both fields and types.
|
|
217
|
+
The `config` modifier extends both `Term` and `Reflect`, making it usable on both fields and types. We will discuss using `config` on types in the reflect modifiers section below.
|
|
150
218
|
|
|
151
219
|
## Reflect Modifiers
|
|
152
220
|
|
|
153
221
|
Reflect modifiers annotate reflect values (types themselves). Currently, only `config` is a reflect modifier.
|
|
154
222
|
|
|
155
|
-
###
|
|
223
|
+
### config
|
|
156
224
|
|
|
157
225
|
You can attach configuration to the type itself using the `Schema#modifier` method:
|
|
158
226
|
|
|
159
|
-
```scala
|
|
227
|
+
```scala
|
|
160
228
|
import zio.blocks.schema._
|
|
161
229
|
|
|
162
230
|
case class Person(name: String, age: Int)
|
|
@@ -170,7 +238,7 @@ object Person {
|
|
|
170
238
|
|
|
171
239
|
Or add multiple modifiers at once:
|
|
172
240
|
|
|
173
|
-
```scala
|
|
241
|
+
```scala
|
|
174
242
|
import zio.blocks.schema._
|
|
175
243
|
|
|
176
244
|
case class Person(name: String, age: Int)
|
|
@@ -186,11 +254,25 @@ object Person {
|
|
|
186
254
|
}
|
|
187
255
|
```
|
|
188
256
|
|
|
257
|
+
Or annotate the case class directly:
|
|
258
|
+
|
|
259
|
+
```scala
|
|
260
|
+
import zio.blocks.schema._
|
|
261
|
+
|
|
262
|
+
@Modifier.config("db.table-name", "person_table")
|
|
263
|
+
@Modifier.config("schema.version", "v2")
|
|
264
|
+
case class Person(name: String, age: Int)
|
|
265
|
+
|
|
266
|
+
object Person {
|
|
267
|
+
implicit val schema: Schema[Person] = Schema.derived
|
|
268
|
+
}
|
|
269
|
+
```
|
|
270
|
+
|
|
189
271
|
## Programmatic Modifier Access
|
|
190
272
|
|
|
191
273
|
You can access modifiers programmatically through the `Reflect` structure:
|
|
192
274
|
|
|
193
|
-
```scala
|
|
275
|
+
```scala
|
|
194
276
|
import zio.blocks.schema._
|
|
195
277
|
|
|
196
278
|
case class Person(
|
|
@@ -218,7 +300,7 @@ reflect match {
|
|
|
218
300
|
|
|
219
301
|
All modifier types have built-in `Schema` instances, enabling them to be serialized and deserialized:
|
|
220
302
|
|
|
221
|
-
```scala
|
|
303
|
+
```scala
|
|
222
304
|
import zio.blocks.schema._
|
|
223
305
|
|
|
224
306
|
// Schema instances for individual modifiers
|
|
@@ -233,21 +315,19 @@ Schema[Modifier.Reflect]
|
|
|
233
315
|
Schema[Modifier]
|
|
234
316
|
```
|
|
235
317
|
|
|
236
|
-
This
|
|
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
|
|
318
|
+
This means you can serialize modifiers as part of your schema metadata, allowing you to persist and exchange schema information with full modifier details.
|
|
242
319
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
|
249
|
-
|
|
250
|
-
| `
|
|
320
|
+
[//]: # (## Format Support)
|
|
321
|
+
[//]: # ()
|
|
322
|
+
[//]: # (Different serialization formats interpret modifiers according to their semantics.)
|
|
323
|
+
[//]: # ()
|
|
324
|
+
[//]: # (TODO: Add a table comparing how each modifier is supported across formats like JSON, BSON, Avro, Protobuf, etc. For example:)
|
|
325
|
+
[//]: # (| Modifier | JSON | BSON | Avro | Protobuf |)
|
|
326
|
+
[//]: # (|-------------|----------------------|----------------------|-------------------|-------------------|)
|
|
327
|
+
[//]: # (| `transient` | Field omitted | Field omitted | Field omitted | Field omitted |)
|
|
328
|
+
[//]: # (| `rename` | Custom field name | Custom field name | Custom field name | Custom field name |)
|
|
329
|
+
[//]: # (| `alias` | Accepts alternatives | Accepts alternatives | - | - |)
|
|
330
|
+
[//]: # (| `config` | Format-specific | Format-specific | Format-specific | Format-specific |)
|
|
251
331
|
|
|
252
332
|
## Best Practices
|
|
253
333
|
|
|
@@ -258,19 +338,3 @@ Different serialization formats interpret modifiers according to their semantics
|
|
|
258
338
|
3. **Use `transient` sparingly**: Only mark fields as transient when they are truly derived or temporary. Remember that transient fields need default values.
|
|
259
339
|
|
|
260
340
|
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
|
-
```
|