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