@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.
@@ -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
- Unlike Scala annotations, modifiers are **pure data** values that can be serialized, making them ideal for runtime introspection and cross-process schema exchange.
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
- ## Modifier Hierarchy
20
+ Modifiers can be applied in two ways:
15
21
 
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
- └────────────────────────────────────────────────────────────────┘
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
- ## Term Modifiers
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
- Term modifiers annotate record fields and variant cases. They are used during schema derivation to customize how fields are serialized and deserialized.
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
- ### transient
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
- 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.
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 mdoc:compile-only
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: Long,
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] = Schema.derived
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
- When encoding a `User` to JSON, the `cache` field will be omitted:
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
- During decoding, transient fields use their default values. Therefore, **transient fields must have default values** defined in the case class.
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 mdoc:compile-only
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 mdoc:compile-only
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 mdoc:compile-only
179
+ ```scala
114
180
  import zio.blocks.schema._
115
181
 
116
- @Modifier.rename("NewName")
117
- @Modifier.alias("OldName")
118
- @Modifier.alias("LegacyName")
119
- case class MyClass(value: String)
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 mdoc:compile-only
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
- ### Using config on Types
223
+ ### config
156
224
 
157
225
  You can attach configuration to the type itself using the `Schema#modifier` method:
158
226
 
159
- ```scala mdoc:compile-only
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 mdoc:compile-only
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 mdoc:compile-only
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 mdoc:compile-only
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 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
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
- 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 |
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
- ```