@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,364 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: binding
|
|
3
|
+
title: "The Binding Data Type"
|
|
4
|
+
sidebar_label: Binding
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
`Binding` is a sealed trait in ZIO Blocks that provides the operational machinery for constructing and deconstructing values of schema-described types. While `Reflect` describes the **structure** of data types, `Binding` provides the **behavior** needed to work with those types at runtime.
|
|
8
|
+
|
|
9
|
+
In other words, a binding is used to attach non-serializable Scala functions, such as **constructors**, **deconstructors**, and **matchers**, to a reflection type.
|
|
10
|
+
|
|
11
|
+
The combination of `Reflect` and `Binding` forms the foundation of `Schema`: a bound reflect (one where `F[_, _] = Binding`) that can both describe and manipulate data.
|
|
12
|
+
|
|
13
|
+
```scala
|
|
14
|
+
sealed trait Binding[T <: BindingType, A]
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
The `Binding` trait is parameterized by:
|
|
18
|
+
|
|
19
|
+
- **`T <: BindingType`**: A phantom type that identifies the kind of binding (Record, Variant, Seq, Map, etc.)
|
|
20
|
+
- **`A`**: The Scala type that this binding operates on
|
|
21
|
+
|
|
22
|
+
## The Role of Binding in the Schema Architecture
|
|
23
|
+
|
|
24
|
+
ZIO Blocks separates operational behavior from structural metadata using two key abstractions:
|
|
25
|
+
|
|
26
|
+
1. **Reflect** (`Reflect[F, A]`): A generic data structure describing the shape of type `A`. It's parameterized by `F[_, _]`, which can be plugged with different "binding strategies".
|
|
27
|
+
2. **Binding**: The concrete binding strategy that embeds construction/deconstruction capabilities within a `Reflect` structure. The binding is the operational behavior attached to the reflect.
|
|
28
|
+
|
|
29
|
+
When `F[_, _] = Binding`, you get a **bound reflect** that can actually construct and deconstruct values. When `F[_, _] = NoBinding`, you get an **unbound reflect** that contains only structural metadata and is fully **serializable**.
|
|
30
|
+
|
|
31
|
+
```scala
|
|
32
|
+
// Type aliases for clarity
|
|
33
|
+
type Reflect.Bound[A] = Reflect[Binding, A] // Can construct/deconstruct A
|
|
34
|
+
type Reflect.Unbound[A] = Reflect[NoBinding, A] // Pure metadata, serializable
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
A `Schema[A]` is simply a wrapper around `Reflect.Bound[A]`:
|
|
38
|
+
|
|
39
|
+
```scala
|
|
40
|
+
final class Schema[A](val reflect: Reflect.Bound[A])
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Binding Variants
|
|
44
|
+
|
|
45
|
+
The `Binding` sealed trait has several case class variants, each corresponding to a different kind of `Reflect` node:
|
|
46
|
+
|
|
47
|
+
### 1. `Binding.Record`
|
|
48
|
+
|
|
49
|
+
Provides construction and deconstruction capabilities for product types—case classes, tuples, and any type composed of multiple named or positional fields.
|
|
50
|
+
|
|
51
|
+
```scala
|
|
52
|
+
final case class Record[A](
|
|
53
|
+
constructor: Constructor[A],
|
|
54
|
+
deconstructor: Deconstructor[A],
|
|
55
|
+
defaultValue: Option[() => A] = None,
|
|
56
|
+
examples: collection.immutable.Seq[A] = Nil
|
|
57
|
+
) extends Binding[BindingType.Record, A]
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
**Components:**
|
|
61
|
+
|
|
62
|
+
- `constructor`: Builds an `A` from primitive components stored in `Registers`
|
|
63
|
+
- `deconstructor`: Breaks down an `A` into primitive components in `Registers`
|
|
64
|
+
- `defaultValue`: An optional thunk `() => A` that produces a default instance. This is useful for formats like Protocol Buffers where missing fields assume default values.
|
|
65
|
+
- `examples`: Sample values for documentation or testing, or OpenAPI schema generation.
|
|
66
|
+
|
|
67
|
+
Here is an example of a `Binding.Record` for a simple `Person` case class:
|
|
68
|
+
|
|
69
|
+
```scala mdoc:compile-only
|
|
70
|
+
import zio.blocks.schema.binding._
|
|
71
|
+
import zio.blocks.schema.binding.RegisterOffset._
|
|
72
|
+
|
|
73
|
+
case class Person(name: String, age: Int)
|
|
74
|
+
|
|
75
|
+
val initialRegisters = RegisterOffset(objects = 1, ints = 1)
|
|
76
|
+
|
|
77
|
+
val personRecord: Binding.Record[Person] =
|
|
78
|
+
Binding.Record[Person](
|
|
79
|
+
constructor = new Constructor[Person] {
|
|
80
|
+
def usedRegisters = initialRegisters
|
|
81
|
+
|
|
82
|
+
def construct(in: Registers, offset: RegisterOffset): Person =
|
|
83
|
+
Person(
|
|
84
|
+
in.getObject(offset).asInstanceOf[String],
|
|
85
|
+
in.getInt(offset + RegisterOffset(objects = 1))
|
|
86
|
+
)
|
|
87
|
+
},
|
|
88
|
+
deconstructor = new Deconstructor[Person] {
|
|
89
|
+
|
|
90
|
+
def usedRegisters = initialRegisters
|
|
91
|
+
|
|
92
|
+
def deconstruct(out: Registers, offset: RegisterOffset, p: Person): Unit = {
|
|
93
|
+
out.setObject(offset, p.name)
|
|
94
|
+
out.setInt(offset + RegisterOffset(objects = 1), p.age)
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
)
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### 2. `Binding.Variant`
|
|
101
|
+
|
|
102
|
+
The `Binding.Variant` data type provides discrimination capabilities and matchers for sum types—sealed traits, Scala 3 enums, and any type that represents one of several possible alternatives:
|
|
103
|
+
|
|
104
|
+
```scala
|
|
105
|
+
final case class Variant[A](
|
|
106
|
+
discriminator: Discriminator[A],
|
|
107
|
+
matchers: Matchers[A],
|
|
108
|
+
defaultValue: Option[() => A] = None,
|
|
109
|
+
examples: collection.immutable.Seq[A] = Nil
|
|
110
|
+
) extends Binding[BindingType.Variant, A]
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
**Discriminator** and **Matchers** are complementary components in ZIO Blocks that work together to handle sum types (ADTs/sealed traits/enums):
|
|
114
|
+
|
|
115
|
+
- `Discriminator`: Given a value of a sum type, determine which case it belongs to by returning a numerical index.
|
|
116
|
+
- `Matchers`: Given a value and a case index, safely downcast the value to the specific case type, or return null if it doesn't match.
|
|
117
|
+
|
|
118
|
+
```scala mdoc:compile-only
|
|
119
|
+
import zio.blocks.schema.binding._
|
|
120
|
+
|
|
121
|
+
sealed trait Shape extends Product with Serializable
|
|
122
|
+
|
|
123
|
+
case class Circle(radius: Double) extends Shape
|
|
124
|
+
|
|
125
|
+
case class Rectangle(width: Double, height: Double) extends Shape
|
|
126
|
+
|
|
127
|
+
val shapeBinding = Binding.Variant[Shape](
|
|
128
|
+
discriminator = new Discriminator[Shape] {
|
|
129
|
+
override def discriminate(a: Shape): Int = a match {
|
|
130
|
+
case Circle(_) => 0
|
|
131
|
+
case Rectangle(_, _) => 1
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
matchers = Matchers(
|
|
135
|
+
new Matcher[Circle] {
|
|
136
|
+
override def downcastOrNull(any: Any): Circle =
|
|
137
|
+
any match {
|
|
138
|
+
case c: Circle => c
|
|
139
|
+
case _ => null.asInstanceOf[Circle]
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
new Matcher[Rectangle] {
|
|
143
|
+
override def downcastOrNull(any: Any): Rectangle =
|
|
144
|
+
any match {
|
|
145
|
+
case r: Rectangle => r
|
|
146
|
+
case _ => null.asInstanceOf[Rectangle]
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
)
|
|
150
|
+
)
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Please note that the `Variant` is only responsible for discrimination and downcasting. The actual construction and deconstruction of each case is handled by the individual case's `Binding.Record` (or other appropriate binding).
|
|
154
|
+
|
|
155
|
+
### 3. Binding.Seq
|
|
156
|
+
|
|
157
|
+
The `Binding.Seq` data type provides efficient construction and deconstruction capabilities for sequence/collection types—`List`, `Vector`, `Array`, `Set`, `Chunk`, and any ordered collection:
|
|
158
|
+
|
|
159
|
+
```scala
|
|
160
|
+
final case class Seq[C[_], A](
|
|
161
|
+
constructor: SeqConstructor[C],
|
|
162
|
+
deconstructor: SeqDeconstructor[C],
|
|
163
|
+
defaultValue: Option[() => C[A]] = None,
|
|
164
|
+
examples: collection.immutable.Seq[C[A]] = Nil
|
|
165
|
+
) extends Binding[BindingType.Seq[C], C[A]]
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
The type parameter is `C[_]` (the collection type constructor) rather than `C[A]` (a specific element type). This allows a single `Binding.Seq[List]` to work for `List[Int]`, `List[String]`, `List[Person]`, etc. Inside the companion object of `Binding.Seq`, there are implementations for standard Scala collections like `List`, `Vector`, `Seq`, `IndexedSeq`, `Set`, and `ArraySeq`.
|
|
169
|
+
|
|
170
|
+
Sequences are ubiquitous in data modeling, and their efficient handling is critical for performance. The `Seq` binding abstracts over the specific collection type using the **builder pattern**, enabling both stacked and heap-allocated construction strategies.
|
|
171
|
+
|
|
172
|
+
- `seqConstructor` is a sequence constructor which provides how to build the sequence of type `C[_]`. It provides collection-specific builder operations. Different collections have different constructor strategies (e.g., `Array` needs size upfront and is stack-allocated, while `List` can be built incrementally on the heap).
|
|
173
|
+
- `seqDeconstructor` is a sequence deconstructor that provides iteration and size information for the collection type `C[_]`, which enables us to tear down the collection into its elements efficiently. The size hints enable codecs to pre-allocate output buffers and builders.
|
|
174
|
+
|
|
175
|
+
Let's take a deep dive into these two main components.
|
|
176
|
+
|
|
177
|
+
#### SeqConstructor
|
|
178
|
+
|
|
179
|
+
The `SeqConstructor` has two main variants:
|
|
180
|
+
|
|
181
|
+
1. **Heap-allocated Constructors** which are for collections like `List`, `Vector`, `Set`, etc. These collections typically use mutable builders under the hood to efficiently accumulate elements before producing an immutable collection. The `Boxed` abstract class is a convenient base class for heap-allocated constructors that uses a single builder type for all primitive types. This makes it easy to implement sequence constructors for collections of heap-allocated objects:
|
|
182
|
+
|
|
183
|
+
```scala
|
|
184
|
+
abstract class Boxed[C[_]] extends SeqConstructor[C] {
|
|
185
|
+
override type BooleanBuilder = ObjectBuilder[Boolean]
|
|
186
|
+
override type ByteBuilder = ObjectBuilder[Byte]
|
|
187
|
+
// ... all primitive builders are aliases to ObjectBuilder
|
|
188
|
+
|
|
189
|
+
def addBoolean(builder: BooleanBuilder, a: Boolean): Unit = addObject(builder, a)
|
|
190
|
+
// primitives get boxed when passed to addObject
|
|
191
|
+
|
|
192
|
+
// ... similarly for other primitive adders
|
|
193
|
+
}
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
To implement a boxed sequence, we have to define just four things: the builder type, how to create a new builder, how to add elements to it, and how to produce the final collection.
|
|
197
|
+
|
|
198
|
+
ZIO Blocks provides `SeqConstructor.Boxed` implementations for standard Scala collections like `List`, `Vector`, `Set`, `IndexedSeq` and `Seq`. Here is an example of a `SeqConstructor.Boxed` for `List`:
|
|
199
|
+
|
|
200
|
+
```scala
|
|
201
|
+
val listConstructor: SeqConstructor[List] = new Boxed[List] {
|
|
202
|
+
type ObjectBuilder[A] = scala.collection.mutable.ListBuffer[A]
|
|
203
|
+
|
|
204
|
+
def newObjectBuilder[A](sizeHint: Int): ObjectBuilder[A] = new ListBuffer[A]
|
|
205
|
+
|
|
206
|
+
def addObject[A](builder: ObjectBuilder[A], a: A): Unit = builder.addOne(a)
|
|
207
|
+
|
|
208
|
+
def resultObject[A](builder: ObjectBuilder[A]): List[A] = builder.toList
|
|
209
|
+
}
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
2. **Stack-allocated Constructors** which are for collections like `Array`, `ArraySeq`, and `IArray`.
|
|
213
|
+
|
|
214
|
+
All the implementations are found in the companion object of the `SeqConstructor` trait.
|
|
215
|
+
|
|
216
|
+
#### SeqDeconstructor
|
|
217
|
+
|
|
218
|
+
The `SeqDeconstructor` has a simple interface with two methods:
|
|
219
|
+
|
|
220
|
+
```scala
|
|
221
|
+
trait SeqDeconstructor[C[_]] {
|
|
222
|
+
def deconstruct[A](c: C[A]): Iterator[A]
|
|
223
|
+
|
|
224
|
+
def size[A](c: C[A]): Int
|
|
225
|
+
}
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
The `deconstruct` method provides an iterator over the elements of the collection, while the `size` method returns the number of elements.
|
|
229
|
+
|
|
230
|
+
Inside the companion object of the `SeqDeconstructor` there are implementations for standard Scala collections like `List`, `Vector`, `Seq`, and `IndexedSeq`.
|
|
231
|
+
|
|
232
|
+
For efficiency, there are specialized implementations for array-based sequences like `Array`, `ArraySeq`, and `IArray` which provide direct access to elements, besides the iterator-based approach. As a result, they are much more performant than the generic iterator-based deconstruction. When an `Optic` needs to access a specific element by index, it can use these specialized methods to avoid the overhead of iterator traversal.
|
|
233
|
+
|
|
234
|
+
This is done through the `SpecializedIndexed` trait:
|
|
235
|
+
|
|
236
|
+
```scala
|
|
237
|
+
sealed trait SpecializedIndexed[C[_]] extends SeqDeconstructor[C] {
|
|
238
|
+
def elementType[A](c: C[A]): RegisterType[A]
|
|
239
|
+
def objectAt[A](c: C[A], index: Int): A
|
|
240
|
+
def booleanAt(c: C[Boolean], index: Int): Boolean
|
|
241
|
+
def byteAt(c: C[Byte], index: Int): Byte
|
|
242
|
+
def shortAt(c: C[Short], index: Int): Short
|
|
243
|
+
def intAt(c: C[Int], index: Int): Int
|
|
244
|
+
def longAt(c: C[Long], index: Int): Long
|
|
245
|
+
def floatAt(c: C[Float], index: Int): Float
|
|
246
|
+
def doubleAt(c: C[Double], index: Int): Double
|
|
247
|
+
def charAt(c: C[Char], index: Int): Char
|
|
248
|
+
}
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
You can find implementations of `SpecializedIndexed` for `Array`, `ArraySeq`, and `IArray` in the companion object of `SeqDeconstructor`.
|
|
252
|
+
|
|
253
|
+
### 4. Binding.Map
|
|
254
|
+
|
|
255
|
+
To describe the construction and deconstruction of key-value collections, use `Binding.Map`:
|
|
256
|
+
|
|
257
|
+
```scala
|
|
258
|
+
final case class Map[M[_, _], K, V](
|
|
259
|
+
constructor: MapConstructor[M],
|
|
260
|
+
deconstructor: MapDeconstructor[M],
|
|
261
|
+
defaultValue: Option[() => M[K, V]] = None,
|
|
262
|
+
examples: collection.immutable.Seq[M[K, V]] = Nil
|
|
263
|
+
) extends Binding[BindingType.Map[M], M[K, V]]
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
It has two main components for building maps and tearing down maps to their key-value pairs:
|
|
267
|
+
|
|
268
|
+
- `mapConstructor`: Builds maps from key-value pairs using builders
|
|
269
|
+
- `mapDeconstructor`: Provides key-value iteration for maps
|
|
270
|
+
|
|
271
|
+
Their interfaces are similar to `SeqConstructor` and `SeqDeconstructor`, but specialized for key-value pairs.
|
|
272
|
+
|
|
273
|
+
ZIO Blocks has an implementation of `Binding.Map` for standard Scala `Map`s in the companion object of `Binding.Map` called `Binding.Map.map`.
|
|
274
|
+
|
|
275
|
+
### 5. Binding.Primitive
|
|
276
|
+
|
|
277
|
+
The `Binding.Primitive` provides metadata for primitive/scalar types—`Int`, `Long`, `Double`, `String`, `Boolean`, `Byte`, `Short`, `Char`, `Float`, `BigInt`, `BigDecimal`, `UUID`, and temporal types.
|
|
278
|
+
|
|
279
|
+
```scala
|
|
280
|
+
final case class Primitive[A](
|
|
281
|
+
defaultValue: Option[() => A] = None,
|
|
282
|
+
examples: collection.immutable.Seq[A] = Nil
|
|
283
|
+
) extends Binding[BindingType.Primitive, A]
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
Primitives require minimal binding logic since they are atomic values that don't need construction from parts. You can access all built-in primitive bindings in the companion object of `Binding.Primitive`, such as `Primitive.int`, `Primitive.string`, etc.
|
|
287
|
+
|
|
288
|
+
### 6. Binding.Wrapper
|
|
289
|
+
|
|
290
|
+
The `Binding.Wrapper` provides wrap/unwrap capabilities for newtype patterns—opaque types, value classes, validated wrappers, and single-field case class wrappers.
|
|
291
|
+
|
|
292
|
+
`Newtypes` are extremely common in well-designed codebases. Instead of passing raw String values for emails, you define `Email` as a distinct type. Instead of `Int` for user IDs, you define `UserId`. This provides type safety without runtime overhead.
|
|
293
|
+
|
|
294
|
+
The Wrapper binding handles the bidirectional transformation between the wrapper type and its underlying representation, with optional validation:
|
|
295
|
+
|
|
296
|
+
```scala
|
|
297
|
+
final case class Wrapper[A, B](
|
|
298
|
+
wrap: B => Either[SchemaError, A],
|
|
299
|
+
unwrap: A => Either[SchemaError, B]
|
|
300
|
+
) extends Binding[BindingType.Wrapper[A, B], A]
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
**Components:**
|
|
304
|
+
|
|
305
|
+
- `wrap`: Converts from the underlying type `B` to the wrapper type `A`, returning `Right(a)` on success or `Left(SchemaError)` on failure
|
|
306
|
+
- `unwrap`: Extracts the underlying `B` from an `A`, returning `Right(b)` on success or `Left(SchemaError)` on failure
|
|
307
|
+
|
|
308
|
+
Here is an example of a `Binding.Wrapper` for an `Email` newtype with validation:
|
|
309
|
+
|
|
310
|
+
```scala mdoc:compile-only
|
|
311
|
+
import zio.blocks.schema._
|
|
312
|
+
import zio.blocks.schema.binding._
|
|
313
|
+
|
|
314
|
+
case class Email(value: String)
|
|
315
|
+
|
|
316
|
+
object Email {
|
|
317
|
+
private val EmailRegex = "^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+$".r
|
|
318
|
+
new Binding.Wrapper[Email, String](
|
|
319
|
+
wrap = {
|
|
320
|
+
case x@EmailRegex(_*) => Right(new Email(x))
|
|
321
|
+
case _ => Left(SchemaError.validationFailed("Expected valid email format"))
|
|
322
|
+
},
|
|
323
|
+
email => Right(email.value)
|
|
324
|
+
)
|
|
325
|
+
}
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
### 7. Binding.Dynamic
|
|
329
|
+
|
|
330
|
+
To bind untyped dynamic values, use `Binding.Dynamic`. It provides a binding for dynamically typed values whose structure is not known at compile time:
|
|
331
|
+
|
|
332
|
+
```scala
|
|
333
|
+
final case class Dynamic(
|
|
334
|
+
defaultValue: Option[() => DynamicValue] = None,
|
|
335
|
+
examples: collection.immutable.Seq[DynamicValue] = Nil
|
|
336
|
+
) extends Binding[BindingType.Dynamic, DynamicValue]
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
Used when the schema of the data is not known at compile time, such as JSON payloads with arbitrary structure.
|
|
340
|
+
|
|
341
|
+
## Binding and Schema Serialization
|
|
342
|
+
|
|
343
|
+
When `F[_, _] = NoBinding` in `Reflect[F[_, _], A]` the `Reflect` structure contains only pure data (no functions) and becomes fully serializable. This enables:
|
|
344
|
+
|
|
345
|
+
1. **Schema serialization**: Convert schemas to JSON Schema or other formats, making them portable
|
|
346
|
+
2. **Schema rebinding**: Deserialize a schema and rebind it using a `TypeRegistry`, so it becomes type-safe and operational again
|
|
347
|
+
|
|
348
|
+
We will cover schema serialization and rebinding in more detail in the `Reflect` data type documentation page.
|
|
349
|
+
|
|
350
|
+
## Summary
|
|
351
|
+
|
|
352
|
+
The `Binding` data type is the operational heart of ZIO Blocks' schema system:
|
|
353
|
+
|
|
354
|
+
| Reflect Node | Binding Variant | Construction and Deconstruction of |
|
|
355
|
+
|---------------------|---------------------|------------------------------------|
|
|
356
|
+
| `Reflect.Record` | `Binding.Record` | Product Types |
|
|
357
|
+
| `Reflect.Variant` | `Binding.Variant` | Sum Types |
|
|
358
|
+
| `Reflect.Sequence` | `Binding.Seq` | Sequence Collection Types |
|
|
359
|
+
| `Reflect.Map` | `Binding.Map` | Key-value Collection Types |
|
|
360
|
+
| `Reflect.Primitive` | `Binding.Primitive` | Primitive Types |
|
|
361
|
+
| `Reflect.Wrapper` | `Binding.Wrapper` | Wrap/unwrap newtypes |
|
|
362
|
+
| `Reflect.Dynamic` | `Binding.Dynamic` | Untyped Dynamic Values |
|
|
363
|
+
|
|
364
|
+
A key innovation is the clean separation between structure (`Reflect`) and behavior (`Binding`), enabling us to achieve serializable schemas with unbound reflects, while the `Reflect` remains pluggable with the `Binding` for runtime operations.
|