@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,521 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: reflect
|
|
3
|
+
title: "Reflect"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
The `Reflect` data type is the foundational data structure underlying ZIO Blocks. While `Schema[A]` is the user-facing API that wraps a `Reflect`, the `Reflect` type itself contains the actual reflective metadata that describes the structure of Scala data types at runtime:
|
|
7
|
+
|
|
8
|
+
```scala
|
|
9
|
+
// Simplified definition of Schema and Reflect
|
|
10
|
+
final case class Schema[A](reflect: Reflect.Bound[A])
|
|
11
|
+
|
|
12
|
+
sealed trait Reflect[F[_, _], A]
|
|
13
|
+
object Reflect {
|
|
14
|
+
case class Record [F[_, _], A](???) extends Reflect[F, A]
|
|
15
|
+
case class Variant [F[_, _], A](???) extends Reflect[F, A]
|
|
16
|
+
case class Sequence [F[_, _], A, C[_]](???) extends Reflect[F, C[A]]
|
|
17
|
+
case class Map [F[_, _], K, V, M[_, _]](???) extends Reflect[F, M[K, V]]
|
|
18
|
+
case class Dynamic [F[_, _]](???) extends Reflect[F, DynamicValue]
|
|
19
|
+
case class Primitive[F[_, _], A](???) extends Reflect[F, A]
|
|
20
|
+
case class Wrapper [F[_, _], A, B](???) extends Reflect[F, A]
|
|
21
|
+
case class Deferred [F[_, _], A](???) extends Reflect[F, A]
|
|
22
|
+
}
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
The `Reflect` type has two type parameters:
|
|
26
|
+
|
|
27
|
+
1. **`F[_, _]`** The binding type constructor. When `F = Binding`, the Reflect is "bound" and contains runtime functions for construction/deconstruction. When `F = NoBinding`, the Reflect is "unbound" and contains only pure structural data.
|
|
28
|
+
2. **`A`** The Scala type that this Reflect describes.
|
|
29
|
+
|
|
30
|
+
## Bound and Unbound Reflects
|
|
31
|
+
|
|
32
|
+
Each of the eight case class nodes of `Reflect` corresponds to a different category of Scala types (records, variants, sequences, maps, primitives, ... types). They may contain a field of type `F[BindingType.X, A]` that holds the runtime binding information for that type. For example the `Record` variant contains a `F[BindingType.Record, A]` instance that holds the `Constructor[A]` and `Deconstructor[A]` functions:
|
|
33
|
+
|
|
34
|
+
```scala
|
|
35
|
+
case class Record[F[_, _], A](
|
|
36
|
+
fields: IndexedSeq[Term[F, A, ?]],
|
|
37
|
+
typeName: TypeName[A],
|
|
38
|
+
recordBinding: F[BindingType.Record, A], // Binding info
|
|
39
|
+
doc: Doc = Doc.Empty,
|
|
40
|
+
modifiers: Seq[Modifier.Reflect] = Nil
|
|
41
|
+
) extends Reflect[F, A]
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
We have discussed the binding system in detail on [the Binding System page](./binding.md).
|
|
45
|
+
|
|
46
|
+
This dual-nature design enables schema serialization:
|
|
47
|
+
|
|
48
|
+
- **Bound Reflect (`Reflect.Bound[A]`)**: Contains runtime bindings (constructors, deconstructors, functions) that allow actual construction and deconstruction of values. This is what `Schema` wraps.
|
|
49
|
+
- **Unbound Reflect**: Contains only pure data with no functions or closures. Can be serialized and deserialized identically, enabling transmission of schemas across the wire.
|
|
50
|
+
|
|
51
|
+
```scala
|
|
52
|
+
type Reflect.Bound[A] = Reflect[Binding, A]
|
|
53
|
+
type Reflect.Unbound[A] = Reflect[NoBinding, A]
|
|
54
|
+
|
|
55
|
+
// Schemas are always bound
|
|
56
|
+
final case class Schema[A](reflect: Reflect.Bound[A])
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Reflect Nodes
|
|
60
|
+
|
|
61
|
+
`Reflect` is a sealed trait with eight nodes, each representing a different category of Scala types.
|
|
62
|
+
|
|
63
|
+
### 1. Record
|
|
64
|
+
|
|
65
|
+
`Reflect.Record` represents case classes and other product types:
|
|
66
|
+
|
|
67
|
+
```scala
|
|
68
|
+
case class Record[F[_, _], A](
|
|
69
|
+
fields: IndexedSeq[Term[F, A, ?]],
|
|
70
|
+
typeName: TypeName[A],
|
|
71
|
+
recordBinding: F[BindingType.Record, A],
|
|
72
|
+
doc: Doc = Doc.Empty,
|
|
73
|
+
modifiers: Seq[Modifier.Record] = Nil
|
|
74
|
+
) extends Reflect[F, A]
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
**Key Components:**
|
|
78
|
+
|
|
79
|
+
- **fields**: An indexed sequence of `Term` objects, each describing a field with its name, type, and nested `Reflect`.
|
|
80
|
+
- **typeName**: The fully qualified type name including namespace.
|
|
81
|
+
- **recordBinding**: Contains the `Constructor[A]` and `Deconstructor[A]` for building and tearing apart values.
|
|
82
|
+
- **doc**: Optional documentation.
|
|
83
|
+
- **modifiers**: Metadata modifiers for customization.
|
|
84
|
+
|
|
85
|
+
The following example shows a `Person` case class represented as a `Reflect.Record`:
|
|
86
|
+
|
|
87
|
+
```scala mdoc:compile-only
|
|
88
|
+
import zio.blocks.schema._
|
|
89
|
+
import zio.blocks.schema.binding.RegisterOffset._
|
|
90
|
+
import zio.blocks.schema.binding._
|
|
91
|
+
import zio.blocks.typeid.TypeId
|
|
92
|
+
|
|
93
|
+
case class Person(
|
|
94
|
+
name: String,
|
|
95
|
+
email: String,
|
|
96
|
+
age: Int,
|
|
97
|
+
height: Double,
|
|
98
|
+
weight: Double
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
object Person {
|
|
102
|
+
implicit val schema: Schema[Person] =
|
|
103
|
+
Schema {
|
|
104
|
+
Reflect.Record[Binding, Person](
|
|
105
|
+
fields = Vector(
|
|
106
|
+
Term("name", Schema.string.reflect),
|
|
107
|
+
Term("email", Schema.string.reflect),
|
|
108
|
+
Term("age", Schema.int.reflect),
|
|
109
|
+
Term("height", Schema.double.reflect),
|
|
110
|
+
Term("weight", Schema.double.reflect)
|
|
111
|
+
),
|
|
112
|
+
typeId = TypeId.of[Person],
|
|
113
|
+
recordBinding = Binding.Record[Person](
|
|
114
|
+
constructor = new Constructor[Person] {
|
|
115
|
+
override def usedRegisters: RegisterOffset =
|
|
116
|
+
RegisterOffset(objects = 2, ints = 1, doubles = 2)
|
|
117
|
+
override def construct(in: Registers, offset: RegisterOffset): Person =
|
|
118
|
+
Person(
|
|
119
|
+
in.getObject(offset).asInstanceOf[String],
|
|
120
|
+
in.getObject(offset + RegisterOffset(objects = 1)).asInstanceOf[String],
|
|
121
|
+
in.getInt(offset + RegisterOffset.Zero),
|
|
122
|
+
in.getDouble(offset + RegisterOffset(ints = 1)),
|
|
123
|
+
in.getDouble(offset + RegisterOffset(ints = 1, doubles = 1))
|
|
124
|
+
)
|
|
125
|
+
},
|
|
126
|
+
deconstructor = new Deconstructor[Person] {
|
|
127
|
+
override def usedRegisters: RegisterOffset =
|
|
128
|
+
RegisterOffset(objects = 2, ints = 1, doubles = 2)
|
|
129
|
+
override def deconstruct(out: Registers, offset: RegisterOffset, in: Person): Unit = {
|
|
130
|
+
out.setObject(offset, in.name)
|
|
131
|
+
out.setObject(offset + RegisterOffset(objects = 1), in.email)
|
|
132
|
+
out.setInt(offset, in.age)
|
|
133
|
+
out.setDouble(offset + RegisterOffset(ints = 1), in.height)
|
|
134
|
+
out.setDouble(offset + RegisterOffset(ints = 1, doubles = 1), in.weight)
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
)
|
|
138
|
+
)
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### 2. Variant
|
|
144
|
+
|
|
145
|
+
`Reflect.Variant` represents sealed traits, enums, and other sum types—data structures that can be one of several cases:
|
|
146
|
+
|
|
147
|
+
```scala
|
|
148
|
+
case class Variant[F[_, _], A](
|
|
149
|
+
cases: IndexedSeq[Term[F, A, ?]],
|
|
150
|
+
typeName: TypeName[A],
|
|
151
|
+
variantBinding: F[BindingType.Variant, A],
|
|
152
|
+
doc: Doc = Doc.Empty,
|
|
153
|
+
modifiers: Seq[Modifier.Variant] = Nil
|
|
154
|
+
) extends Reflect[F, A]
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
**Key Components:**
|
|
158
|
+
|
|
159
|
+
- **cases**: An indexed sequence of `Term` objects, each describing one of the possible cases with its name, type, and nested `Reflect`.
|
|
160
|
+
- **typeName**: The fully qualified type name including namespace.
|
|
161
|
+
- **variantBinding**: Contains the binding information for the variant, such as a discriminator and matcher functions.
|
|
162
|
+
|
|
163
|
+
The following example shows a `Shape` sealed trait represented as a `Reflect.Variant`. We assume the schema for the `Circle` and `Rectangle` case classes are defined elsewhere:
|
|
164
|
+
|
|
165
|
+
```scala
|
|
166
|
+
sealed trait Shape extends Product with Serializable
|
|
167
|
+
|
|
168
|
+
object Shape {
|
|
169
|
+
case class Circle(radius: Double) extends Shape
|
|
170
|
+
object Circle {
|
|
171
|
+
implicit val schema: Schema[Circle] = ???
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
case class Rectangle(width: Double, height: Double) extends Shape
|
|
175
|
+
object Rectangle {
|
|
176
|
+
implicit val schema: Schema[Rectangle] = ???
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
implicit val schema: Schema[Shape] =
|
|
180
|
+
Schema[Shape] {
|
|
181
|
+
Reflect.Variant[Binding, Shape](
|
|
182
|
+
cases = IndexedSeq(
|
|
183
|
+
Term(
|
|
184
|
+
name = "Circle",
|
|
185
|
+
value = Circle.schema.reflect
|
|
186
|
+
),
|
|
187
|
+
Term(
|
|
188
|
+
name = "Rectangle",
|
|
189
|
+
value = Rectangle.schema.reflect
|
|
190
|
+
)
|
|
191
|
+
),
|
|
192
|
+
typeName = TypeName(namespace = Namespace(Seq.empty), name = "Shape"),
|
|
193
|
+
variantBinding = Binding.Variant[Shape](
|
|
194
|
+
discriminator = new Discriminator[Shape] {
|
|
195
|
+
override def discriminate(a: Shape): Int = a match {
|
|
196
|
+
case Circle(_) => 0
|
|
197
|
+
case Rectangle(_, _) => 1
|
|
198
|
+
}
|
|
199
|
+
},
|
|
200
|
+
matchers = Matchers(
|
|
201
|
+
new Matcher[Circle] {
|
|
202
|
+
override def downcastOrNull(any: Any): Circle =
|
|
203
|
+
any match {
|
|
204
|
+
case c: Circle => c
|
|
205
|
+
case _ => null.asInstanceOf[Circle]
|
|
206
|
+
}
|
|
207
|
+
},
|
|
208
|
+
new Matcher[Rectangle] {
|
|
209
|
+
override def downcastOrNull(any: Any): Rectangle =
|
|
210
|
+
any match {
|
|
211
|
+
case r: Rectangle => r
|
|
212
|
+
case _ => null.asInstanceOf[Rectangle]
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
)
|
|
216
|
+
),
|
|
217
|
+
doc = Doc("A geometric shape"),
|
|
218
|
+
modifiers = Seq(Modifier.config("protobuf.field-id", "1"))
|
|
219
|
+
)
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
### 3. Sequence
|
|
225
|
+
|
|
226
|
+
`Reflect.Sequence` represents sequential collections like `List`, `Vector`, `Array`, `Set`, and other `Iterable` types:
|
|
227
|
+
|
|
228
|
+
```scala
|
|
229
|
+
case class Sequence[F[_, _], A, C[_]](
|
|
230
|
+
element: Reflect[F, A],
|
|
231
|
+
typeName: TypeName[C[A]],
|
|
232
|
+
seqBinding: F[BindingType.Seq[C], C[A]],
|
|
233
|
+
doc: Doc = Doc.Empty,
|
|
234
|
+
modifiers: scala.Seq[Modifier.Reflect] = Nil
|
|
235
|
+
) extends Reflect[F, C[A]]
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
**Key components**:
|
|
239
|
+
- **element**: The `Reflect` describing the element type.
|
|
240
|
+
- **seqBinding**: Contains the corresponding sequence binding
|
|
241
|
+
|
|
242
|
+
### 4. Map
|
|
243
|
+
|
|
244
|
+
`Reflect.Map` represents key-value collections like `Map` etc:
|
|
245
|
+
|
|
246
|
+
```scala
|
|
247
|
+
case class Map[F[_, _], K, V, M[_, _]](
|
|
248
|
+
key: Reflect[F, K],
|
|
249
|
+
value: Reflect[F, V],
|
|
250
|
+
typeName: TypeName[M[K, V]],
|
|
251
|
+
mapBinding: F[BindingType.Map[M], M[K, V]],
|
|
252
|
+
doc: Doc = Doc.Empty,
|
|
253
|
+
modifiers: Seq[Modifier.Reflect] = Nil
|
|
254
|
+
) extends Reflect[F, M[K, V]]
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
**Key Components:**
|
|
258
|
+
|
|
259
|
+
- **key**: The `Reflect` describing the key type.
|
|
260
|
+
- **value**: The `Reflect` describing the value type.
|
|
261
|
+
- **mapBinding**: Contains the corresponding map binding.
|
|
262
|
+
|
|
263
|
+
### 5. Dynamic
|
|
264
|
+
|
|
265
|
+
`Reflect.Dynamic` represents values whose types are not known at compile time. This is essential for handling JSON payloads, schemaless data, or any scenario where the structure is determined at runtime.
|
|
266
|
+
|
|
267
|
+
```scala
|
|
268
|
+
case class Dynamic[F[_, _]](
|
|
269
|
+
dynamicBinding: F[BindingType.Dynamic, DynamicValue],
|
|
270
|
+
doc: Doc = Doc.Empty,
|
|
271
|
+
modifiers: Seq[Modifier.Dynamic] = Nil
|
|
272
|
+
) extends Reflect[F, DynamicValue]
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
### 6. Primitive
|
|
276
|
+
|
|
277
|
+
`Reflect.Primitive` represents primitive and scalar types. This includes numeric types (`Byte`, `Short`, `Int`, `Long`, `Float`, `Double`, `BigInt`, `BigDecimal`), text types (`String`, `Char`), and `Boolean`. It also covers the full range of `java.time` temporal types: instants and dates (`Instant`, `LocalDate`, `LocalDateTime`, `LocalTime`, `OffsetDateTime`, `OffsetTime`, `ZonedDateTime`), durations and periods (`Duration`, `Period`), calendar components (`Year`, `YearMonth`, `Month`, `MonthDay`, `DayOfWeek`), and time zones (`ZoneId`, `ZoneOffset`). Additionally, it supports `Currency`, `UUID`, and `Unit`, and can be extended to support custom primitive types as needed.
|
|
278
|
+
|
|
279
|
+
```scala
|
|
280
|
+
case class Primitive[F[_, _], A](
|
|
281
|
+
primitiveType: PrimitiveType[A],
|
|
282
|
+
typeName: TypeName[A],
|
|
283
|
+
primitiveBinding: F[BindingType.Primitive, A],
|
|
284
|
+
doc: Doc = Doc.Empty,
|
|
285
|
+
modifiers: Seq[Modifier.Reflect] = Nil
|
|
286
|
+
) extends Reflect[F, A]
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
You can access all built-in primitive types inside companion object of `Reflect` data type, e.g. `Reflect.int`, `Reflect.string`, `Reflect.instant`, etc.
|
|
290
|
+
|
|
291
|
+
### 7. Wrapper
|
|
292
|
+
|
|
293
|
+
Modern Scala development often involves creating domain-specific types that add semantic meaning or validation to primitive types:
|
|
294
|
+
|
|
295
|
+
```scala
|
|
296
|
+
// Opaque type in Scala 3
|
|
297
|
+
opaque type Age = Int
|
|
298
|
+
|
|
299
|
+
// Newtype pattern
|
|
300
|
+
case class Email(value: String)
|
|
301
|
+
|
|
302
|
+
// ZIO Prelude newtypes
|
|
303
|
+
object UserId extends Newtype[String]
|
|
304
|
+
type UserId = UserId.Type
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
Each of these patterns shares a common characteristic: they wrap an underlying type (`Int`, `String`) to create a new type with distinct semantics.
|
|
308
|
+
|
|
309
|
+
`Reflect.Wrapper` is a specialized node type that models the relationship between a wrapper type and its underlying representation. It provides a unified abstraction for opaque types, newtypes, wrapper classes, and similar patterns where one type wraps another with optional validation logic:
|
|
310
|
+
|
|
311
|
+
```
|
|
312
|
+
case class Wrapper[F[_, _], A, B](
|
|
313
|
+
wrapped: Reflect[F, B],
|
|
314
|
+
typeId: TypeId[A],
|
|
315
|
+
wrapperBinding: F[BindingType.Wrapper[A, B], A],
|
|
316
|
+
doc: Doc = Doc.Empty,
|
|
317
|
+
modifiers: Seq[Modifier.Reflect] = Nil
|
|
318
|
+
) extends Reflect[F, A]
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
The `Wrapper` has three type parameters:
|
|
322
|
+
|
|
323
|
+
- **`F[_, _]`**: The binding type constructor (typically `Binding` for bound schemas)
|
|
324
|
+
- **`A`**: The wrapper type (e.g., `Age`, `Email`)
|
|
325
|
+
- **`B`**: The underlying/wrapped type (e.g., `Int`, `String`)
|
|
326
|
+
|
|
327
|
+
So we can say a wrapper of type `Wrapper[F[_, _], A, B]` wraps a type `B` (described by `wrapped: Reflect[F, B]`) to create a new type `A`.
|
|
328
|
+
|
|
329
|
+
Assume we have a positive integer type `PosInt` that wraps an `Int` but enforces a validation rule that the value must be non-negative. We can define its schema using `Reflect.Wrapper` as follows:
|
|
330
|
+
|
|
331
|
+
```scala
|
|
332
|
+
import zio.blocks.schema._
|
|
333
|
+
import zio.blocks.schema.binding._
|
|
334
|
+
|
|
335
|
+
case class PosInt private (value: Int) extends AnyVal
|
|
336
|
+
|
|
337
|
+
object PosInt {
|
|
338
|
+
def apply(value: Int): Either[String, PosInt] =
|
|
339
|
+
if (value >= 0) Right(new PosInt(value))
|
|
340
|
+
else Left("Expected positive value")
|
|
341
|
+
|
|
342
|
+
implicit val schema: Schema[PosInt] = Schema(
|
|
343
|
+
Reflect.Wrapper(
|
|
344
|
+
wrapped = Schema[Int].reflect,
|
|
345
|
+
typeId = TypeId.of[PosInt],
|
|
346
|
+
wrapperBinding = Binding.Wrapper[PosInt, Int](
|
|
347
|
+
wrap = v => PosInt(v),
|
|
348
|
+
unwrap = _.value
|
|
349
|
+
)
|
|
350
|
+
)
|
|
351
|
+
)
|
|
352
|
+
}
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
To create schemas for wrapper types, use `transform`:
|
|
356
|
+
|
|
357
|
+
```scala
|
|
358
|
+
import zio.blocks.schema.Schema
|
|
359
|
+
|
|
360
|
+
case class PosInt private (value: Int) extends AnyVal
|
|
361
|
+
|
|
362
|
+
object PosInt {
|
|
363
|
+
def unsafeApply(value: Int): PosInt =
|
|
364
|
+
if (value >= 0) new PosInt(value)
|
|
365
|
+
else throw SchemaError.validationFailed("Expected positive value")
|
|
366
|
+
|
|
367
|
+
implicit val schema: Schema[PosInt] =
|
|
368
|
+
Schema[Int].transform(PosInt.unsafeApply, _.value)
|
|
369
|
+
}
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
### 8. Deferred
|
|
373
|
+
|
|
374
|
+
`Reflect.Deferred` introduces laziness to handle recursive and mutually recursive data types. Without deferred evaluation, recursive types would cause infinite loops:
|
|
375
|
+
|
|
376
|
+
```scala
|
|
377
|
+
case class Deferred[F[_, _], A](
|
|
378
|
+
deferred: () => Reflect[F, A]
|
|
379
|
+
) extends Reflect[F, A]
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
For example, if we have a recursive data type like below:
|
|
383
|
+
|
|
384
|
+
```scala
|
|
385
|
+
case class Tree(value: Int, children: List[Tree])
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
We can define its schema using `Reflect.Deferred` as follows:
|
|
389
|
+
|
|
390
|
+
```scala mdoc:compile-only
|
|
391
|
+
import zio.blocks.schema._
|
|
392
|
+
import zio.blocks.schema.binding.RegisterOffset.RegisterOffset
|
|
393
|
+
import zio.blocks.schema.binding._
|
|
394
|
+
import zio.blocks.typeid.TypeId
|
|
395
|
+
|
|
396
|
+
// Recursive data type
|
|
397
|
+
case class Tree(value: Int, children: List[Tree])
|
|
398
|
+
|
|
399
|
+
object Tree {
|
|
400
|
+
implicit val schema: Schema[Tree] = {
|
|
401
|
+
lazy val treeReflect: Reflect.Bound[Tree] = Reflect.Record[Binding, Tree](
|
|
402
|
+
fields = Vector(
|
|
403
|
+
Schema[Int].reflect.asTerm("value"),
|
|
404
|
+
Reflect.Deferred(() => Schema.list(new Schema(treeReflect)).reflect).asTerm("children")
|
|
405
|
+
),
|
|
406
|
+
typeId = TypeId.of[Tree],
|
|
407
|
+
recordBinding = Binding.Record(
|
|
408
|
+
constructor = new Constructor[Tree] {
|
|
409
|
+
def usedRegisters: RegisterOffset = RegisterOffset(ints = 1, objects = 1)
|
|
410
|
+
|
|
411
|
+
def construct(in: Registers, offset: RegisterOffset): Tree =
|
|
412
|
+
Tree(
|
|
413
|
+
in.getInt(offset),
|
|
414
|
+
in.getObject(offset).asInstanceOf[List[Tree]]
|
|
415
|
+
)
|
|
416
|
+
},
|
|
417
|
+
deconstructor = new Deconstructor[Tree] {
|
|
418
|
+
def usedRegisters: RegisterOffset = RegisterOffset(ints = 1, objects = 1)
|
|
419
|
+
|
|
420
|
+
def deconstruct(out: Registers, offset: RegisterOffset, in: Tree): Unit = {
|
|
421
|
+
out.setInt(offset, in.value)
|
|
422
|
+
out.setObject(offset, in.children)
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
)
|
|
426
|
+
)
|
|
427
|
+
|
|
428
|
+
new Schema(treeReflect)
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
## Debug-Friendly toString
|
|
434
|
+
|
|
435
|
+
`Reflect` types have a custom `toString` that produces a human-readable SDL (Schema Definition Language) format. This makes debugging schemas much easier compared to the default case class output.
|
|
436
|
+
|
|
437
|
+
```scala
|
|
438
|
+
import zio.blocks.schema._
|
|
439
|
+
|
|
440
|
+
case class Person(name: String, age: Int, address: Address)
|
|
441
|
+
case class Address(street: String, city: String)
|
|
442
|
+
|
|
443
|
+
object Person {
|
|
444
|
+
implicit val schema: Schema[Person] = Schema.derived
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
println(Schema[Person].reflect)
|
|
448
|
+
// Output:
|
|
449
|
+
// record Person {
|
|
450
|
+
// name: String
|
|
451
|
+
// age: Int
|
|
452
|
+
// address: record Address {
|
|
453
|
+
// street: String
|
|
454
|
+
// city: String
|
|
455
|
+
// }
|
|
456
|
+
// }
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
**Format by Reflect type:**
|
|
460
|
+
|
|
461
|
+
| Type | Format |
|
|
462
|
+
|------|--------|
|
|
463
|
+
| `Primitive` | Type name (e.g., `String`, `Int`, `java.time.Instant`) |
|
|
464
|
+
| `Record` | `record Name { fields... }` |
|
|
465
|
+
| `Variant` | `variant Name { \| Case1 \| Case2... }` |
|
|
466
|
+
| `Sequence` | `sequence List[Element]` or multiline for complex elements |
|
|
467
|
+
| `Map` | `map Map[Key, Value]` or multiline for complex types |
|
|
468
|
+
| `Wrapper` | `wrapper Name(underlying)` |
|
|
469
|
+
| `Deferred` | `deferred => TypeName` (breaks recursive cycles) |
|
|
470
|
+
| `Dynamic` | `DynamicValue` |
|
|
471
|
+
|
|
472
|
+
**Variant example:**
|
|
473
|
+
|
|
474
|
+
```scala
|
|
475
|
+
sealed trait PaymentMethod
|
|
476
|
+
case object Cash extends PaymentMethod
|
|
477
|
+
case class CreditCard(number: String, cvv: String) extends PaymentMethod
|
|
478
|
+
|
|
479
|
+
// toString output:
|
|
480
|
+
// variant PaymentMethod {
|
|
481
|
+
// | Cash
|
|
482
|
+
// | CreditCard(
|
|
483
|
+
// number: String,
|
|
484
|
+
// cvv: String
|
|
485
|
+
// )
|
|
486
|
+
// }
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
**Recursive type example:**
|
|
490
|
+
|
|
491
|
+
```scala
|
|
492
|
+
case class Tree(value: Int, children: List[Tree])
|
|
493
|
+
|
|
494
|
+
// toString output:
|
|
495
|
+
// record Tree {
|
|
496
|
+
// value: Int
|
|
497
|
+
// children: sequence List[
|
|
498
|
+
// deferred => Tree
|
|
499
|
+
// ]
|
|
500
|
+
// }
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
## Auto-Derivation
|
|
504
|
+
|
|
505
|
+
While you can manually construct `Reflect` instances as shown in the examples above, ZIO Blocks provides powerful auto-derivation capabilities that can automatically generate `Schema` instances (and thus `Reflect` instances) for most Scala types using macros and implicit resolution.
|
|
506
|
+
|
|
507
|
+
The auto-derivation mechanism inspects the structure of your data types at compile time and generates the appropriate `Reflect` representation, including nested types, collections, options, and more.
|
|
508
|
+
|
|
509
|
+
To leverage auto-derivation, simply define an implicit `Schema` for your type using `Schema.derived`:
|
|
510
|
+
|
|
511
|
+
```scala mdoc:compile-only
|
|
512
|
+
import zio.blocks.schema.Schema
|
|
513
|
+
|
|
514
|
+
case class Person(name: String, age: Int)
|
|
515
|
+
|
|
516
|
+
object Person {
|
|
517
|
+
implicit val schema: Schema[Person] = Schema.derived
|
|
518
|
+
}
|
|
519
|
+
```
|
|
520
|
+
|
|
521
|
+
The above will automatically generate a `Reflect.Record` for the `Person` case class, including fields for `name` and `age`, along with the necessary bindings for construction and deconstruction. The same applies to more complex types, including variants, collections, and recursive structures.
|