@zio.dev/zio-blocks 0.0.1
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 +426 -0
- package/package.json +6 -0
- package/path-interpolator.md +645 -0
- package/reference/binding.md +364 -0
- package/reference/chunk.md +576 -0
- package/reference/context.md +157 -0
- package/reference/docs.md +524 -0
- package/reference/dynamic-value.md +823 -0
- package/reference/formats.md +640 -0
- package/reference/json-schema.md +626 -0
- package/reference/json.md +979 -0
- package/reference/modifier.md +276 -0
- package/reference/optics.md +1613 -0
- package/reference/patch.md +631 -0
- package/reference/reflect-transform.md +387 -0
- package/reference/reflect.md +521 -0
- package/reference/registers.md +282 -0
- package/reference/schema-evolution.md +540 -0
- package/reference/schema.md +619 -0
- package/reference/syntax.md +409 -0
- package/reference/type-class-derivation-internals.md +632 -0
- package/reference/typeid.md +900 -0
- package/reference/validation.md +458 -0
- package/scope.md +627 -0
- package/sidebars.js +30 -0
|
@@ -0,0 +1,619 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: schema
|
|
3
|
+
title: "Schema"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
`Schema[A]` is the primary data type in ZIO Blocks (ZIO Schema 2) that contains reified information about the structure of a Scala data type `A`, together with the ability to tear down and build up values of that type.
|
|
7
|
+
|
|
8
|
+
```scala
|
|
9
|
+
final case class Schema[A](reflect: Reflect.Bound[A])
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
┌────────────────────────────────────────────────────────────────┐
|
|
14
|
+
│ Schema[A] │
|
|
15
|
+
├────────────────────────────────────────────────────────────────┤
|
|
16
|
+
│ Reflect.Bound[A] (Reflect[Binding, A]) │
|
|
17
|
+
│ ┌─────────────────────────────────────────────────────┐ │
|
|
18
|
+
│ │ Structure │ TypeName[A], DynamicValue │ │
|
|
19
|
+
│ │ (ADT nodes) │ Doc, Examples, Default Value │ │
|
|
20
|
+
│ │ │ Modifiers, Metadata │ │
|
|
21
|
+
│ └─────────────────────────────────────────────────────┘ │
|
|
22
|
+
│ ┌─────────────────────────────────────────────────────┐ │
|
|
23
|
+
│ │ Binding[T, A] │ │
|
|
24
|
+
│ │ - Constructor[A] (build values) │ │
|
|
25
|
+
│ │ - Deconstructor[A] (tear down values) │ │
|
|
26
|
+
│ └─────────────────────────────────────────────────────┘ │
|
|
27
|
+
└────────────────────────────────────────────────────────────────┘
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Reflect: Structure vs Binding
|
|
31
|
+
|
|
32
|
+
The [`Reflect`](./reflect.md) data type represents the structure of Scala types. It is parameterized by `F[_, _]` which allows plugging in different "binding" strategies:
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
┌────────────────────────────────────────────────────────────────┐
|
|
36
|
+
│ Reflect[F[_, _], A] │
|
|
37
|
+
├────────────────────────────────────────────────────────────────┤
|
|
38
|
+
│ F = Binding → Reflect.Bound[A] (with construction/ │
|
|
39
|
+
│ deconstruction) │
|
|
40
|
+
│ F = NoBinding → Reflect.Unbound[A] (pure data, │
|
|
41
|
+
│ serializable) │
|
|
42
|
+
└────────────────────────────────────────────────────────────────┘
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
- **Bound Schema**: Contains functions for constructing/deconstructing values (not serializable)
|
|
46
|
+
- **Unbound Schema**: Pure data representation (can be serialized across the wire)
|
|
47
|
+
|
|
48
|
+
Schema is a bound Reflect, meaning that other than structural information, it also contains construction and deconstruction capabilities via the `Binding` type:
|
|
49
|
+
|
|
50
|
+
```scala
|
|
51
|
+
// Schema wraps a bound Reflect
|
|
52
|
+
final case class Schema[A](reflect: Reflect.Bound[A])
|
|
53
|
+
|
|
54
|
+
// Reflect.Bound is a type alias
|
|
55
|
+
type Bound[A] = Reflect[Binding, A]
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Schema Derivation
|
|
59
|
+
|
|
60
|
+
ZIO Blocks provides both automatic schema derivation and also ways to manually create schemas. As an end user, you will mostly use automatic derivation. So we will focus on automatic derivation here. If you need to go deeper and create schemas manually, check out the [Reflect](./reflect.md) and [Binding](./binding.md) documentation pages.
|
|
61
|
+
|
|
62
|
+
To leverage auto-derivation, simply define an implicit `Schema` for your type using `Schema.derived`:
|
|
63
|
+
|
|
64
|
+
```scala mdoc:compile-only
|
|
65
|
+
import zio.blocks.schema.Schema
|
|
66
|
+
|
|
67
|
+
case class Person(name: String, age: Int)
|
|
68
|
+
|
|
69
|
+
object Person {
|
|
70
|
+
implicit val schema: Schema[Person] = Schema.derived
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
It will automatically derive the schema for `Person` based on its structure. After that, you can summon the schema using `Schema[Person]` anywhere in your code.
|
|
75
|
+
|
|
76
|
+
For sealed traits (ADTs), it will derive the schema for all subtypes as well:
|
|
77
|
+
|
|
78
|
+
```scala mdoc:compile-only
|
|
79
|
+
import zio.blocks.schema.Schema
|
|
80
|
+
|
|
81
|
+
sealed trait Shape
|
|
82
|
+
object Shape {
|
|
83
|
+
case class Circle(radius: Double) extends Shape
|
|
84
|
+
case class Rectangle(width: Double, height: Double) extends Shape
|
|
85
|
+
|
|
86
|
+
implicit val schema: Schema[Shape] = Schema.derived
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Under the hood, the derivation process includes deriving schemas for cases of enum (i.e. `Circle` and `Rectangle`) and finally creating a schema for the `Shape` trait itself.
|
|
91
|
+
|
|
92
|
+
## Built-in Schemas
|
|
93
|
+
|
|
94
|
+
ZIO Blocks ships with a comprehensive set of pre-defined schemas for standard Scala and Java types, eliminating boilerplate and ensuring consistent behavior across your application.
|
|
95
|
+
|
|
96
|
+
### Primitive Types
|
|
97
|
+
|
|
98
|
+
The foundational building blocks for all data types. These schemas leverage the [register-based architecture](registers.md) for zero-allocation performance:
|
|
99
|
+
|
|
100
|
+
```scala mdoc:compile-only
|
|
101
|
+
import zio.blocks.schema.Schema
|
|
102
|
+
|
|
103
|
+
Schema[Unit] // The unit type (singleton value)
|
|
104
|
+
Schema[Boolean] // Boolean values (true/false)
|
|
105
|
+
Schema[Byte] // 8-bit signed integer (-128 to 127)
|
|
106
|
+
Schema[Short] // 16-bit signed integer
|
|
107
|
+
Schema[Int] // 32-bit signed integer
|
|
108
|
+
Schema[Long] // 64-bit signed integer
|
|
109
|
+
Schema[Float] // 32-bit IEEE 754 floating point
|
|
110
|
+
Schema[Double] // 64-bit IEEE 754 floating point
|
|
111
|
+
Schema[Char] // 16-bit Unicode character
|
|
112
|
+
Schema[String] // Immutable character sequence
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### Arbitrary Precision Numbers
|
|
116
|
+
|
|
117
|
+
For financial calculations and scenarios requiring exact decimal representation:
|
|
118
|
+
|
|
119
|
+
```scala mdoc:compile-only
|
|
120
|
+
import zio.blocks.schema.Schema
|
|
121
|
+
|
|
122
|
+
Schema[BigInt] // Arbitrary precision integer
|
|
123
|
+
Schema[BigDecimal] // Arbitrary precision decimal
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Temporal Types (java.time)
|
|
127
|
+
|
|
128
|
+
Complete coverage of the modern Java Time API for robust date/time handling:
|
|
129
|
+
|
|
130
|
+
```scala mdoc:compile-only
|
|
131
|
+
import zio.blocks.schema.Schema
|
|
132
|
+
|
|
133
|
+
// Date components
|
|
134
|
+
Schema[java.time.LocalDate] // Date without time (2024-01-15)
|
|
135
|
+
Schema[java.time.LocalTime] // Time without date (14:30:00)
|
|
136
|
+
Schema[java.time.LocalDateTime] // Date and time without timezone
|
|
137
|
+
|
|
138
|
+
// Timezone-aware types
|
|
139
|
+
Schema[java.time.ZonedDateTime] // Full date-time with timezone
|
|
140
|
+
Schema[java.time.OffsetDateTime] // Date-time with UTC offset
|
|
141
|
+
Schema[java.time.OffsetTime] // Time with UTC offset
|
|
142
|
+
Schema[java.time.ZoneId] // Timezone identifier (e.g., "America/New_York")
|
|
143
|
+
Schema[java.time.ZoneOffset] // Fixed UTC offset (e.g., +05:30)
|
|
144
|
+
|
|
145
|
+
// Duration and period
|
|
146
|
+
Schema[java.time.Duration] // Time-based duration (hours, minutes, seconds)
|
|
147
|
+
Schema[java.time.Period] // Date-based period (years, months, days)
|
|
148
|
+
Schema[java.time.Instant] // Point on the timeline (Unix timestamp)
|
|
149
|
+
|
|
150
|
+
// Calendar components
|
|
151
|
+
Schema[java.time.Year] // Year value (2024)
|
|
152
|
+
Schema[java.time.Month] // Month of year (JANUARY..DECEMBER)
|
|
153
|
+
Schema[java.time.DayOfWeek] // Day of week (MONDAY..SUNDAY)
|
|
154
|
+
Schema[java.time.YearMonth] // Year and month combination
|
|
155
|
+
Schema[java.time.MonthDay] // Month and day combination
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### Common Java Utility Types
|
|
159
|
+
|
|
160
|
+
There are also schemas for frequently used Java utility types, UUID and Currency:
|
|
161
|
+
|
|
162
|
+
```scala mdoc:compile-only
|
|
163
|
+
import zio.blocks.schema.Schema
|
|
164
|
+
|
|
165
|
+
Schema[java.util.UUID] // 128-bit universally unique identifier
|
|
166
|
+
Schema[java.util.Currency] // ISO 4217 currency code
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### Optional Values
|
|
170
|
+
|
|
171
|
+
ZIO Blocks provides specialized `Option` schemas optimized for primitive types. These avoid boxing overhead by storing primitive values directly in registers:
|
|
172
|
+
|
|
173
|
+
```scala mdoc:compile-only
|
|
174
|
+
import zio.blocks.schema.Schema
|
|
175
|
+
|
|
176
|
+
import zio.blocks.schema.Schema
|
|
177
|
+
|
|
178
|
+
// Specialized primitive options (no boxing overhead)
|
|
179
|
+
Schema[Option[Boolean]] // Also: Byte, Short, Int, Long, Float, Double, Char, Unit
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Other than primitive types, ZIO Blocks uses a generic representation for `Option[A]` which works for all reference types:
|
|
183
|
+
|
|
184
|
+
```scala
|
|
185
|
+
import zio.blocks.schema.Schema
|
|
186
|
+
|
|
187
|
+
// Reference type options (requires A <: AnyRef)
|
|
188
|
+
Schema[Option[A]] // Generic option for reference types
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
### Collection Types
|
|
192
|
+
|
|
193
|
+
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):
|
|
194
|
+
|
|
195
|
+
```scala
|
|
196
|
+
Schema[List[A]] // Immutable singly-linked list
|
|
197
|
+
Schema[Vector[A]] // Immutable indexed sequence (efficient random access)
|
|
198
|
+
Schema[Set[A]] // Immutable set (unique elements)
|
|
199
|
+
Schema[Seq[A]] // General immutable sequence
|
|
200
|
+
Schema[IndexedSeq[A]] // Indexed sequence
|
|
201
|
+
Schema[Map[K, V]] // Immutable key-value mapping
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
To learn how to create custom collection schemas, check out the [`Sequence`](./reflect.md#3-sequence) and [`Map`](./reflect.md#4-map) nodes on the documentation of [`Reflect`](./reflect.md) data type.
|
|
205
|
+
|
|
206
|
+
### DynamicValue
|
|
207
|
+
|
|
208
|
+
ZIO Blocks includes a built-in schema for `DynamicValue`, a semi-structured data representation that serves as a superset of JSON:
|
|
209
|
+
|
|
210
|
+
```scala mdoc:compile-only
|
|
211
|
+
import zio.blocks.schema._
|
|
212
|
+
|
|
213
|
+
val schema = Schema[DynamicValue] // Semi-structured data (superset of JSON)
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
Having the schema for `DynamicValue` allows seamless encoding/decoding between `DynamicValue` and other formats, such as JSON, Avro, Protobuf, etc. It enables us to convert our type-safe data into a semi-structured representation and serialize it into any desired format.
|
|
217
|
+
|
|
218
|
+
### DynamicValue toString (EJSON Format)
|
|
219
|
+
|
|
220
|
+
`DynamicValue` has a custom `toString` that produces EJSON (Extended JSON) format - a superset of JSON that handles non-string keys, tagged variants, and typed primitives:
|
|
221
|
+
|
|
222
|
+
```scala
|
|
223
|
+
import zio.blocks.schema._
|
|
224
|
+
|
|
225
|
+
// Records have unquoted keys
|
|
226
|
+
val record = DynamicValue.Record(Vector(
|
|
227
|
+
"name" -> DynamicValue.Primitive(PrimitiveValue.String("Alice")),
|
|
228
|
+
"age" -> DynamicValue.Primitive(PrimitiveValue.Int(30))
|
|
229
|
+
))
|
|
230
|
+
println(record)
|
|
231
|
+
// {
|
|
232
|
+
// name: "Alice",
|
|
233
|
+
// age: 30
|
|
234
|
+
// }
|
|
235
|
+
|
|
236
|
+
// Maps have quoted string keys
|
|
237
|
+
val map = DynamicValue.Map(Vector(
|
|
238
|
+
DynamicValue.Primitive(PrimitiveValue.String("key")) ->
|
|
239
|
+
DynamicValue.Primitive(PrimitiveValue.String("value"))
|
|
240
|
+
))
|
|
241
|
+
println(map)
|
|
242
|
+
// {
|
|
243
|
+
// "key": "value"
|
|
244
|
+
// }
|
|
245
|
+
|
|
246
|
+
// Variants use @ metadata
|
|
247
|
+
val variant = DynamicValue.Variant("Some", DynamicValue.Record(Vector(
|
|
248
|
+
"value" -> DynamicValue.Primitive(PrimitiveValue.Int(42))
|
|
249
|
+
)))
|
|
250
|
+
println(variant)
|
|
251
|
+
// {
|
|
252
|
+
// value: 42
|
|
253
|
+
// } @ {tag: "Some"}
|
|
254
|
+
|
|
255
|
+
// Typed primitives use @ metadata
|
|
256
|
+
val instant = DynamicValue.Primitive(PrimitiveValue.Instant(java.time.Instant.now))
|
|
257
|
+
println(instant)
|
|
258
|
+
// 1705312800000 @ {type: "instant"}
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
**Key EJSON properties:**
|
|
262
|
+
- **Records**: unquoted keys (`{ name: "John" }`)
|
|
263
|
+
- **Maps**: quoted string keys (`{ "name": "John" }`) or unquoted non-string keys (`{ 42: "value" }`)
|
|
264
|
+
- **Variants**: postfix `@ {tag: "CaseName"}`
|
|
265
|
+
- **Typed primitives**: postfix `@ {type: "instant"}` for types that would lose precision as JSON
|
|
266
|
+
|
|
267
|
+
## Debug-Friendly toString
|
|
268
|
+
|
|
269
|
+
`Schema` has a custom `toString` that wraps the underlying `Reflect` output in a `Schema { ... }` block, providing a complete structural view of your data types:
|
|
270
|
+
|
|
271
|
+
```scala
|
|
272
|
+
import zio.blocks.schema._
|
|
273
|
+
|
|
274
|
+
case class Person(name: String, age: Int)
|
|
275
|
+
object Person {
|
|
276
|
+
implicit val schema: Schema[Person] = Schema.derived
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
println(Schema[Person])
|
|
280
|
+
// Output:
|
|
281
|
+
// Schema {
|
|
282
|
+
// record Person {
|
|
283
|
+
// name: String
|
|
284
|
+
// age: Int
|
|
285
|
+
// }
|
|
286
|
+
// }
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
For primitive schemas:
|
|
290
|
+
|
|
291
|
+
```scala
|
|
292
|
+
println(Schema[Int])
|
|
293
|
+
// Output:
|
|
294
|
+
// Schema {
|
|
295
|
+
// Int
|
|
296
|
+
// }
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
This format makes it easy to inspect complex nested schemas during debugging. See the [Reflect documentation](./reflect.md#debug-friendly-tostring) for details on the SDL format used for the inner structure.
|
|
300
|
+
|
|
301
|
+
## Encoding and Decoding
|
|
302
|
+
|
|
303
|
+
The `Schema[A]` provides methods to encode and decode values of type `A` to/from various formats using the `Format` abstraction:
|
|
304
|
+
|
|
305
|
+
```scala
|
|
306
|
+
case class Schema[A](reflect: Reflect.Bound[A]) {
|
|
307
|
+
def encode[F <: codec.Format](format: F)(output: format.EncodeOutput)(value: A): Unit = ???
|
|
308
|
+
def decode[F <: codec.Format](format: F)(decodeInput: format.DecodeInput): Either[SchemaError, A] = ???
|
|
309
|
+
}
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
The `Format` is the base abstraction for serialization formats in ZIO Blocks, such as Avro, JSON, Protobuf, etc. Each format associates with a specific codec type class which defines the interface for encoding and decoding values and a deriver for deriving codecs for specific types.
|
|
313
|
+
|
|
314
|
+
ZIO Blocks has built-in support for several popular formats, currently `AvroFormat` and `JsonFormat`. You can also implement your own custom formats by extending the `Format` trait.
|
|
315
|
+
|
|
316
|
+
The following example demonstrates encoding and decoding a `Person` case class to/from JSON using the built-in `JsonFormat`:
|
|
317
|
+
|
|
318
|
+
```scala mdoc:compile-only
|
|
319
|
+
import java.nio.ByteBuffer
|
|
320
|
+
|
|
321
|
+
import zio.blocks.schema._
|
|
322
|
+
import zio.blocks.schema.json.JsonFormat
|
|
323
|
+
|
|
324
|
+
case class Person(name: String, age: Int)
|
|
325
|
+
|
|
326
|
+
object Person {
|
|
327
|
+
implicit val schema: Schema[Person] = Schema.derived
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
object EncodeDecodeExample extends App {
|
|
331
|
+
val person = Person("John", 42)
|
|
332
|
+
|
|
333
|
+
// Encode to JSON
|
|
334
|
+
val encodedBuffer = {
|
|
335
|
+
val buffer = ByteBuffer.allocate(1024)
|
|
336
|
+
Schema[Person].encode(JsonFormat)(buffer)(person)
|
|
337
|
+
buffer.flip()
|
|
338
|
+
buffer
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Extract JSON string
|
|
342
|
+
val jsonString = new String(encodedBuffer.duplicate().array())
|
|
343
|
+
println(s"Encoded JSON: $jsonString")
|
|
344
|
+
|
|
345
|
+
// Decode back to Person
|
|
346
|
+
val decodedPerson = Schema[Person].decode(JsonFormat)(encodedBuffer.duplicate())
|
|
347
|
+
println(s"Decoded Person: $decodedPerson")
|
|
348
|
+
}
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
Please note that both `Schema#encode` and `Schema#decode` cache instances, so using encode and decode in multiple places only performs the derivation process once.
|
|
352
|
+
|
|
353
|
+
## Type Class Derivation
|
|
354
|
+
|
|
355
|
+
To derive type class instances for a type `A` based on its schema, you can use the `derive` method on `Schema[A]`. This method takes a `Deriver` for the desired type class and produces an instance of that type class for `A`.
|
|
356
|
+
|
|
357
|
+
In the following example, we derive a JSON codec for the `Person` case class using the `JsonFormat` deriver:
|
|
358
|
+
|
|
359
|
+
```scala
|
|
360
|
+
import zio.blocks.schema.Schema
|
|
361
|
+
import zio.blocks.schema.json.{JsonFormat, JsonBinaryCodec}
|
|
362
|
+
|
|
363
|
+
case class Person(name: String, age: Int)
|
|
364
|
+
|
|
365
|
+
object Person {
|
|
366
|
+
implicit val schema: Schema[Person] = Schema.derived
|
|
367
|
+
val codec: JsonBinaryCodec[Person] = schema.derive(JsonFormat)
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
val person = Person("John", 42)
|
|
371
|
+
|
|
372
|
+
// Encode to JSON
|
|
373
|
+
val json = Person.codec.encode(person)
|
|
374
|
+
|
|
375
|
+
// Extract JSON string
|
|
376
|
+
val jsonString = new String(json)
|
|
377
|
+
println(s"Encoded JSON: $jsonString")
|
|
378
|
+
|
|
379
|
+
// Decode back to Person
|
|
380
|
+
val decodedPerson = Person.codec.decode(json)
|
|
381
|
+
println(s"Decoded Person: $decodedPerson")
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
## Metadata Operations
|
|
385
|
+
|
|
386
|
+
ZIO Blocks allows attaching metadata to schemas and their fields, such as documentation, example values, and default values. This metadata can be useful for generating API documentation, client code, or providing hints to serialization formats.
|
|
387
|
+
|
|
388
|
+
Here is an example of how to set and retrieve documentation values:
|
|
389
|
+
|
|
390
|
+
```scala mdoc:silent
|
|
391
|
+
import zio.blocks.schema._
|
|
392
|
+
|
|
393
|
+
case class Person(name: String, age: Int)
|
|
394
|
+
|
|
395
|
+
object Person extends CompanionOptics[Person]{
|
|
396
|
+
implicit val schema: Schema[Person] = Schema.derived
|
|
397
|
+
|
|
398
|
+
val name: Lens[Person, String] = optic(_.name)
|
|
399
|
+
val age: Lens[Person, Int] = optic(_.age)
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// Add documentation to the schema
|
|
403
|
+
val documented: Schema[Person] = Schema[Person].doc("A person entity")
|
|
404
|
+
|
|
405
|
+
// Get documentation
|
|
406
|
+
val doc: Doc = Schema[Person].doc
|
|
407
|
+
|
|
408
|
+
// Add documentation to a field using optics
|
|
409
|
+
val fieldDoc: Schema[Person] = Schema[Person].doc(Person.name, "The person's name")
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
The important thing to note here is that we can use optics to target specific fields within the schema when adding or retrieving metadata. In the last example, we added documentation specifically to the `name` field of the `Person` schema.
|
|
413
|
+
|
|
414
|
+
Similarly, you can set and get example values:
|
|
415
|
+
|
|
416
|
+
```scala mdoc:compile-only
|
|
417
|
+
import zio.blocks.schema._
|
|
418
|
+
|
|
419
|
+
// Add example values
|
|
420
|
+
val withExamples: Schema[Person] =
|
|
421
|
+
Schema[Person].examples(Person("Alice", 30), Person("Bob", 25))
|
|
422
|
+
|
|
423
|
+
// Get examples
|
|
424
|
+
val examples: Seq[Person] = Schema[Person].examples
|
|
425
|
+
|
|
426
|
+
// Add examples to a specific field
|
|
427
|
+
val fieldExamples: Schema[Person] =
|
|
428
|
+
Schema[Person].examples(Person.name, "Alice", "Bob", "Charlie")
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
There are also methods for setting and getting default values:
|
|
432
|
+
|
|
433
|
+
```scala mdoc:silent
|
|
434
|
+
// Add default value to schema
|
|
435
|
+
val withDefault: Schema[Person] = Schema[Person]
|
|
436
|
+
.defaultValue(Person("Unknown", 0))
|
|
437
|
+
|
|
438
|
+
// Get default value
|
|
439
|
+
val default: Option[Person] = Schema[Person].getDefaultValue
|
|
440
|
+
|
|
441
|
+
// Add default to a specific field
|
|
442
|
+
val fieldDefault: Schema[Person] = Schema[Person]
|
|
443
|
+
.defaultValue(Person.age, 18)
|
|
444
|
+
|
|
445
|
+
// Get default for a field
|
|
446
|
+
val ageDefault: Option[Int] =
|
|
447
|
+
Schema[Person].getDefaultValue(Person.age)
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
## Accessing and Updating Partial Schemas
|
|
451
|
+
|
|
452
|
+
To access a specific part of a schema, we can use the `Schema#get` method, which takes an optic and returns the reflection of the targeted field.
|
|
453
|
+
|
|
454
|
+
```scala mdoc:silent:reset
|
|
455
|
+
import zio.blocks.schema._
|
|
456
|
+
|
|
457
|
+
case class Person(name: String, address: Address)
|
|
458
|
+
|
|
459
|
+
object Person extends CompanionOptics[Person]{
|
|
460
|
+
implicit val schema: Schema[Person] = Schema.derived
|
|
461
|
+
|
|
462
|
+
val name: Lens[Person, String] = optic(_.name)
|
|
463
|
+
val address: Lens[Person, Address] = optic(_.address)
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
case class Address(street: String, city: String)
|
|
467
|
+
|
|
468
|
+
object Address extends CompanionOptics[Address]{
|
|
469
|
+
implicit val schema: Schema[Address] = Schema.derived
|
|
470
|
+
|
|
471
|
+
val street: Lens[Address, String] = optic(_.street)
|
|
472
|
+
val city : Lens[Address, String] = optic(_.city)
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// Get schema for a nested field
|
|
476
|
+
val addressSchema: Option[Reflect.Bound[Address]] =
|
|
477
|
+
Schema[Person].get(Person.address)
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
This method enables us to retrieve the schema for any nested field using optics. For example, to get the schema for the `street` field inside the `address` of a `Person`, we can do as follows:
|
|
481
|
+
|
|
482
|
+
```scala mdoc:compile-only
|
|
483
|
+
val streetSchema: Option[Reflect.Bound[String]] =
|
|
484
|
+
Schema[Person].get(Person.address(Address.street))
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
### Updating Nested Schemas
|
|
488
|
+
|
|
489
|
+
To update a specific part of a schema, we can use the `Schema#updated` method, which takes an optic and a function to transform the targeted schema:
|
|
490
|
+
|
|
491
|
+
```scala
|
|
492
|
+
case class Schema[A](reflect: Reflect.Bound[A]) {
|
|
493
|
+
def updated[B](optic: Optic[A, B])(
|
|
494
|
+
f: Reflect.Bound[B] => Reflect.Bound[B]
|
|
495
|
+
): Option[Schema[A]]
|
|
496
|
+
}
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
Here is an example of updating the documentation of a nested field:
|
|
500
|
+
|
|
501
|
+
```scala mdoc:compile-only
|
|
502
|
+
// Update schema at a specific path
|
|
503
|
+
val updated: Option[Schema[Person]] = Schema[Person]
|
|
504
|
+
.updated(Person.address)(_.doc("Mailing address"))
|
|
505
|
+
```
|
|
506
|
+
|
|
507
|
+
## Schema Aspects
|
|
508
|
+
|
|
509
|
+
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:
|
|
510
|
+
|
|
511
|
+
```scala
|
|
512
|
+
trait SchemaAspect[-Upper, +Lower, F[_, _]] {
|
|
513
|
+
def apply[A >: Lower <: Upper](reflect: Reflect[F, A]): Reflect[F, A]
|
|
514
|
+
def recursive(implicit ev1: Any <:< Upper, ev2: Lower <:< Nothing): SchemaAspect[Upper, Lower, F]
|
|
515
|
+
}
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
The `Schema` data type has a `@@` method used for applying schema aspects:
|
|
519
|
+
|
|
520
|
+
```scala
|
|
521
|
+
case class Schema[A](reflect: Reflect.Bound[A]) {
|
|
522
|
+
def @@[Min >: A, Max <: A](aspect: SchemaAspect[Min, Max, Binding]): Schema[A] = ???
|
|
523
|
+
def @@[B](part: Optic[A, B], aspect: SchemaAspect[B, B, Binding]) = ???
|
|
524
|
+
}
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
These methods enable us to use `@@` syntax for applying aspects to either the entire schema or a specific path within the schema using optics:
|
|
528
|
+
|
|
529
|
+
```scala mdoc:compile-only
|
|
530
|
+
import zio.blocks.schema._
|
|
531
|
+
|
|
532
|
+
// Apply aspect to entire schema
|
|
533
|
+
val documented: Schema[Person] = Schema[Person] @@ SchemaAspect.doc("A person")
|
|
534
|
+
|
|
535
|
+
// Apply aspect to specific path
|
|
536
|
+
val fieldDoc: Schema[Person] = Schema[Person] @@ (
|
|
537
|
+
Person.name,
|
|
538
|
+
SchemaAspect.examples("Alice", "Bob")
|
|
539
|
+
)
|
|
540
|
+
```
|
|
541
|
+
|
|
542
|
+
Currently, ZIO Blocks provides the following built-in schema aspects:
|
|
543
|
+
|
|
544
|
+
- `SchemaAspect.identity`: No-op transformation
|
|
545
|
+
- `SchemaAspect.doc`: Attach documentation to schema or field
|
|
546
|
+
- `SchemaAspect.examples`: Attach example values to schema or field
|
|
547
|
+
|
|
548
|
+
## Modifiers
|
|
549
|
+
|
|
550
|
+
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.
|
|
551
|
+
|
|
552
|
+
The `Schema.modifier` and `Schema.modifiers` methods allow adding one or more modifiers to a schema:
|
|
553
|
+
|
|
554
|
+
```scala
|
|
555
|
+
final case class Schema[A](reflect: Reflect.Bound[A]) {
|
|
556
|
+
def modifier(modifier: Modifier.Reflect) : Schema[A] = ???
|
|
557
|
+
def modifiers(modifiers: Iterable[Modifier.Reflect]): Schema[A] = ???
|
|
558
|
+
}
|
|
559
|
+
```
|
|
560
|
+
|
|
561
|
+
Here is an example of adding modifiers to a schema:
|
|
562
|
+
|
|
563
|
+
```scala mdoc:compile-only
|
|
564
|
+
// Add modifier to schema
|
|
565
|
+
val modified: Schema[Person] = Schema[Person]
|
|
566
|
+
.modifier(Modifier.config("db.table-name", "person_table"))
|
|
567
|
+
.modifier(Modifier.config("schema.version", "v2"))
|
|
568
|
+
|
|
569
|
+
// Add multiple modifiers
|
|
570
|
+
val multiModified: Schema[Person] = Schema[Person]
|
|
571
|
+
.modifiers(
|
|
572
|
+
Seq(
|
|
573
|
+
Modifier.config("db.table-name", "person_table"),
|
|
574
|
+
Modifier.config("schema.version", "v2")
|
|
575
|
+
)
|
|
576
|
+
)
|
|
577
|
+
```
|
|
578
|
+
|
|
579
|
+
## Wrapper Types
|
|
580
|
+
|
|
581
|
+
ZIO Blocks provides the `transform` method for creating schemas for wrapper types, such as newtypes, opaque types and value classes:
|
|
582
|
+
|
|
583
|
+
```scala
|
|
584
|
+
final case class Schema[A](reflect: Reflect.Bound[A]) {
|
|
585
|
+
def transform[B](to: A => B, from: B => A): Schema[B] = ???
|
|
586
|
+
}
|
|
587
|
+
```
|
|
588
|
+
|
|
589
|
+
The `transform` method allows you to define transformations that can fail by throwing `SchemaError` exceptions. Use it for both simple wrapper types and types with validation requirements.
|
|
590
|
+
|
|
591
|
+
Here are examples of both:
|
|
592
|
+
|
|
593
|
+
```scala mdoc:compile-only
|
|
594
|
+
import zio.blocks.schema.{Schema, SchemaError}
|
|
595
|
+
|
|
596
|
+
// For types with validation (may fail)
|
|
597
|
+
case class Email(value: String)
|
|
598
|
+
|
|
599
|
+
object Email {
|
|
600
|
+
private val EmailRegex = "^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+$".r
|
|
601
|
+
|
|
602
|
+
implicit val schema: Schema[Email] = Schema[String]
|
|
603
|
+
.transform(
|
|
604
|
+
{
|
|
605
|
+
case x @ EmailRegex(_*) => Email(x)
|
|
606
|
+
case _ => throw SchemaError.validationFailed("Invalid email format")
|
|
607
|
+
},
|
|
608
|
+
_.value
|
|
609
|
+
)
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
// For total transformations (never fail)
|
|
613
|
+
case class UserId(value: Long)
|
|
614
|
+
|
|
615
|
+
object UserId {
|
|
616
|
+
implicit val schema: Schema[UserId] = Schema[Long]
|
|
617
|
+
.transform(UserId(_), _.value)
|
|
618
|
+
}
|
|
619
|
+
```
|