@zio.dev/zio-blocks 0.0.26 → 0.0.28
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/guides/query-dsl-extending.md +1 -1
- package/guides/query-dsl-fluent-builder.md +1 -1
- package/guides/query-dsl-reified-optics.md +1 -1
- package/guides/query-dsl-sql.md +1 -1
- package/guides/zio-schema-migration.md +1195 -0
- package/index.md +21 -12
- package/package.json +1 -1
- package/reference/allows.md +1377 -0
- package/reference/binding-resolver.md +469 -0
- package/reference/binding.md +1 -1
- package/reference/codec.md +10 -10
- package/reference/docs.md +1 -1
- package/reference/dynamic-optic.md +5 -0
- package/reference/dynamic-schema.md +602 -0
- package/reference/dynamic-value.md +5 -0
- package/reference/json-patch.md +803 -0
- package/reference/json.md +1 -1
- package/reference/media-type.md +2 -2
- package/reference/modifier.md +9 -9
- package/reference/patch.md +4 -0
- package/reference/schema-error.md +569 -0
- package/reference/schema-evolution/as.md +587 -0
- package/reference/schema-evolution/index.md +50 -0
- package/reference/schema-evolution/into.md +1027 -0
- package/reference/schema-expr.md +2 -2
- package/reference/schema.md +29 -0
- package/reference/structural-types.md +369 -0
- package/reference/type-class-derivation.md +329 -324
- package/reference/validation.md +1 -1
- package/reference/xml.md +1304 -0
- package/scope.md +241 -17
- package/sidebars.js +21 -1
- package/reference/schema-evolution.md +0 -540
package/reference/schema-expr.md
CHANGED
|
@@ -70,13 +70,13 @@ val result: Either[OpticCheck, Seq[Boolean]] = combined.eval(alice)
|
|
|
70
70
|
## Installation
|
|
71
71
|
|
|
72
72
|
```scala
|
|
73
|
-
libraryDependencies += "dev.zio" %% "zio-blocks-schema" % "0.0.
|
|
73
|
+
libraryDependencies += "dev.zio" %% "zio-blocks-schema" % "0.0.28"
|
|
74
74
|
```
|
|
75
75
|
|
|
76
76
|
For cross-platform (Scala.js):
|
|
77
77
|
|
|
78
78
|
```scala
|
|
79
|
-
libraryDependencies += "dev.zio" %%% "zio-blocks-schema" % "0.0.
|
|
79
|
+
libraryDependencies += "dev.zio" %%% "zio-blocks-schema" % "0.0.28"
|
|
80
80
|
```
|
|
81
81
|
|
|
82
82
|
Supported Scala versions: 2.13.x and 3.x.
|
package/reference/schema.md
CHANGED
|
@@ -618,3 +618,32 @@ object UserId {
|
|
|
618
618
|
.transform(UserId(_), _.value)
|
|
619
619
|
}
|
|
620
620
|
```
|
|
621
|
+
|
|
622
|
+
## Compile-Time Shape Constraints (`Allows`)
|
|
623
|
+
|
|
624
|
+
ZIO Blocks provides `Allows[A, S]` — a phantom-typed capability token that proves, at compile time, that type `A` satisfies the structural grammar `S`. This lets library authors express and enforce structural preconditions on their generic APIs without writing macros themselves.
|
|
625
|
+
|
|
626
|
+
```scala
|
|
627
|
+
import zio.blocks.schema.Schema
|
|
628
|
+
import zio.blocks.schema.comptime.Allows
|
|
629
|
+
import Allows._
|
|
630
|
+
|
|
631
|
+
// Require a flat record of scalars (e.g. for CSV or RDBMS)
|
|
632
|
+
def writeCsv[A: Schema](rows: Seq[A])(using
|
|
633
|
+
Allows[A, Record[Primitive | Optional[Primitive]]]
|
|
634
|
+
): Unit = ???
|
|
635
|
+
|
|
636
|
+
// Sealed traits auto-unwrap: each case must satisfy Record[...] — no Variant node needed
|
|
637
|
+
def publish[A: Schema](event: A)(using
|
|
638
|
+
Allows[A, Record[Primitive | Sequence[Primitive]]]
|
|
639
|
+
): Unit = ???
|
|
640
|
+
|
|
641
|
+
// Recursive grammar (e.g. for a JSON document store)
|
|
642
|
+
def toJson[A: Schema](doc: A)(using
|
|
643
|
+
Allows[A, Record[Primitive | Self | Optional[Primitive | Self] | Sequence[Primitive | Self]]]
|
|
644
|
+
): String = ???
|
|
645
|
+
```
|
|
646
|
+
|
|
647
|
+
When a type does not satisfy the grammar, the user gets a precise compile-time error naming the violating field and suggesting a fix. No runtime surprises.
|
|
648
|
+
|
|
649
|
+
See the [`Allows` reference](./allows.md) for the full grammar node table, union syntax, `Self` for recursive types, newtypes, and error message examples.
|
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: structural-types
|
|
3
|
+
title: "Structural Types"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
<!--
|
|
7
|
+
BLOCKING ISSUE: Scala 3.7.4 Compiler Crash
|
|
8
|
+
============================================
|
|
9
|
+
|
|
10
|
+
This file uses structural type syntax ({ def field: Type }) which triggers a compiler
|
|
11
|
+
crash in Scala 3.7.4 during the erasure phase. Therefore, code blocks here are marked
|
|
12
|
+
as plain `scala` instead of `mdoc:compile-only` to allow the documentation build to pass.
|
|
13
|
+
|
|
14
|
+
WORKAROUND: Code examples are NOT compiled/type-checked until Scala 3.8.x is adopted.
|
|
15
|
+
|
|
16
|
+
TODO (When Scala 3.8.x is adopted):
|
|
17
|
+
1. Change all `\`\`\`scala` blocks back to `\`\`\`scala mdoc:compile-only`
|
|
18
|
+
2. Run `sbt docs/mdoc` to verify compilation succeeds
|
|
19
|
+
3. Delete this comment
|
|
20
|
+
|
|
21
|
+
Reference:
|
|
22
|
+
- Scala 3.7.4 compiler issue: https://github.com/scala/scala3/issues/24598 (or similar)
|
|
23
|
+
- Scala 3.8.x migration: PR #1169 "Preparing to migration on Scala 3.8.x"
|
|
24
|
+
- This PR: Addresses structural-types.md CI failures
|
|
25
|
+
-->
|
|
26
|
+
|
|
27
|
+
Structural types enable **duck typing** with ZIO Blocks schemas. Instead of requiring a nominal type name (like `class Person`), a structural schema validates based on the **shape** of an object — the fields it provides, regardless of how it was defined.
|
|
28
|
+
|
|
29
|
+
## Motivation
|
|
30
|
+
|
|
31
|
+
Consider a common integration scenario:
|
|
32
|
+
|
|
33
|
+
```scala
|
|
34
|
+
// Your system
|
|
35
|
+
case class Person(name: String, age: Int)
|
|
36
|
+
|
|
37
|
+
// External system (same data, different class)
|
|
38
|
+
case class User(name: String, age: Int)
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Without structural types, converting between `Person` and `User` requires manual translation. With structural types, they both have the same structural schema:
|
|
42
|
+
|
|
43
|
+
```scala
|
|
44
|
+
import scala.language.reflectiveCalls
|
|
45
|
+
import zio.blocks.schema.Schema
|
|
46
|
+
|
|
47
|
+
case class Person(name: String, age: Int)
|
|
48
|
+
case class User(name: String, age: Int)
|
|
49
|
+
|
|
50
|
+
val personSchema = Schema.derived[Person]
|
|
51
|
+
val personStructural = personSchema.structural
|
|
52
|
+
// Schema[{ def name: String; def age: Int }]
|
|
53
|
+
|
|
54
|
+
val userSchema = Schema.derived[User]
|
|
55
|
+
val userStructural = userSchema.structural
|
|
56
|
+
// Schema[{ def name: String; def age: Int }]
|
|
57
|
+
|
|
58
|
+
// Both schemas accept the same data shape
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Construction: `Schema#structural`
|
|
62
|
+
|
|
63
|
+
Use the `Schema#structural` method on any schema to get the corresponding structural schema.
|
|
64
|
+
|
|
65
|
+
**Scala 3:** Using transparent inline — the return type is inferred to the full refinement type:
|
|
66
|
+
|
|
67
|
+
```scala
|
|
68
|
+
import zio.blocks.schema.Schema
|
|
69
|
+
|
|
70
|
+
case class Person(name: String, age: Int)
|
|
71
|
+
object Person {
|
|
72
|
+
implicit val schema: Schema[Person] = Schema.derived[Person]
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
val personSchema: Schema[Person] = Schema.derived[Person]
|
|
76
|
+
val structuralSchema: Schema[{ def name: String; def age: Int }] = personSchema.structural
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
**Scala 2:** Implicit derivation — returns `Schema[ts.StructuralType]` (path-dependent type):
|
|
80
|
+
|
|
81
|
+
```scala
|
|
82
|
+
import zio.blocks.schema.Schema
|
|
83
|
+
|
|
84
|
+
case class Person(name: String, age: Int)
|
|
85
|
+
object Person {
|
|
86
|
+
implicit val schema: Schema[Person] = Schema.derived[Person]
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
val personSchema: Schema[Person] = Schema.derived[Person]
|
|
90
|
+
val structuralSchema = personSchema.structural
|
|
91
|
+
// Type: Schema[ts.StructuralType] (structural type inferred from macro)
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Supported Conversions
|
|
95
|
+
|
|
96
|
+
The following type categories can be converted to structural schemas:
|
|
97
|
+
|
|
98
|
+
### Product Types (Case Classes)
|
|
99
|
+
|
|
100
|
+
Both Scala 2 and 3 support structural conversion of case classes:
|
|
101
|
+
|
|
102
|
+
```scala
|
|
103
|
+
import zio.blocks.schema.Schema
|
|
104
|
+
|
|
105
|
+
case class Address(street: String, city: String, zipCode: Int)
|
|
106
|
+
object Address {
|
|
107
|
+
implicit val schema: Schema[Address] = Schema.derived[Address]
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
val schema = Schema.derived[Address]
|
|
111
|
+
val structural = schema.structural
|
|
112
|
+
// Schema[{ def street: String; def city: String; def zipCode: Int }]
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### Tuples
|
|
116
|
+
|
|
117
|
+
Tuples convert to structural records with field names derived from positions:
|
|
118
|
+
|
|
119
|
+
```scala
|
|
120
|
+
import zio.blocks.schema.Schema
|
|
121
|
+
|
|
122
|
+
type StringIntBool = (String, Int, Boolean)
|
|
123
|
+
implicit val schema: Schema[StringIntBool] = Schema.derived[StringIntBool]
|
|
124
|
+
|
|
125
|
+
val tupleSchema = Schema.derived[(String, Int, Boolean)]
|
|
126
|
+
val structuralSchema = tupleSchema.structural
|
|
127
|
+
// Schema[{ def _1: String; def _2: Int; def _3: Boolean }]
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### Nested Products
|
|
131
|
+
|
|
132
|
+
Nested product fields keep their nominal types; only the outer product is structuralized:
|
|
133
|
+
|
|
134
|
+
```scala
|
|
135
|
+
import zio.blocks.schema.Schema
|
|
136
|
+
|
|
137
|
+
case class Address(street: String, city: String)
|
|
138
|
+
object Address {
|
|
139
|
+
implicit val schema: Schema[Address] = Schema.derived[Address]
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
case class Person(name: String, age: Int, address: Address)
|
|
143
|
+
object Person {
|
|
144
|
+
implicit val schema: Schema[Person] = Schema.derived[Person]
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
val personSchema = Schema.derived[Person]
|
|
148
|
+
val structuralSchema = personSchema.structural
|
|
149
|
+
// Schema[{
|
|
150
|
+
// def name: String
|
|
151
|
+
// def age: Int
|
|
152
|
+
// def address: Address
|
|
153
|
+
// }]
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### Opaque Types (Scala 3)
|
|
157
|
+
|
|
158
|
+
Opaque type aliases are unwrapped to their underlying type:
|
|
159
|
+
|
|
160
|
+
```scala
|
|
161
|
+
import zio.blocks.schema.Schema
|
|
162
|
+
|
|
163
|
+
opaque type UserId = String
|
|
164
|
+
|
|
165
|
+
case class User(id: UserId, name: String)
|
|
166
|
+
object User {
|
|
167
|
+
implicit val schema: Schema[User] = Schema.derived[User]
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
val schema = Schema.derived[User]
|
|
171
|
+
val structural = schema.structural
|
|
172
|
+
// Schema[{ def id: String; def name: String }]
|
|
173
|
+
// (UserId unwrapped to String)
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### Sum Types / Sealed Traits (Scala 3)
|
|
177
|
+
|
|
178
|
+
Sealed traits and enums convert to union types with nested method syntax:
|
|
179
|
+
|
|
180
|
+
```scala
|
|
181
|
+
import zio.blocks.schema.Schema
|
|
182
|
+
|
|
183
|
+
sealed trait Shape
|
|
184
|
+
object Shape {
|
|
185
|
+
case class Circle(radius: Double) extends Shape
|
|
186
|
+
case class Rectangle(width: Double, height: Double) extends Shape
|
|
187
|
+
implicit val schema: Schema[Shape] = Schema.derived[Shape]
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
val schema = Schema.derived[Shape]
|
|
191
|
+
val structural = schema.structural
|
|
192
|
+
// Schema[
|
|
193
|
+
// { def Circle: { def radius: Double } } |
|
|
194
|
+
// { def Rectangle: { def height: Double; def width: Double } }
|
|
195
|
+
// ]
|
|
196
|
+
// (cases sorted alphabetically)
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
**Enum syntax** (Scala 3):
|
|
200
|
+
|
|
201
|
+
```scala
|
|
202
|
+
import zio.blocks.schema.Schema
|
|
203
|
+
|
|
204
|
+
enum Color {
|
|
205
|
+
case Red, Green, Blue
|
|
206
|
+
}
|
|
207
|
+
object Color {
|
|
208
|
+
implicit val schema: Schema[Color] = Schema.derived[Color]
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
val schema = Schema.derived[Color]
|
|
212
|
+
val structural = schema.structural
|
|
213
|
+
// Schema[
|
|
214
|
+
// { def Blue: {} } |
|
|
215
|
+
// { def Green: {} } |
|
|
216
|
+
// { def Red: {} }
|
|
217
|
+
// ]
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
Cases appear in **alphabetical order** in the union type. This alphabetical ordering (applied to fields in products and case names in unions) ensures **deterministic, normalized type identity**: two structural types with the same fields but different declaration order produce the same structural type and normalized name. This is essential for predictable schema evolution and cross-system interop.
|
|
221
|
+
|
|
222
|
+
## Direct Structural Derivation (Scala 3)
|
|
223
|
+
|
|
224
|
+
Create a schema directly for a structural type without a nominal base:
|
|
225
|
+
|
|
226
|
+
```scala
|
|
227
|
+
import zio.blocks.schema.Schema
|
|
228
|
+
|
|
229
|
+
// No case class needed — define the schema for the shape directly
|
|
230
|
+
val personStructural = Schema.derived[{ def name: String; def age: Int }]
|
|
231
|
+
|
|
232
|
+
// The schema is ready to use with values matching that structural shape
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
This is only supported in **Scala 3** with the right macro machinery.
|
|
236
|
+
|
|
237
|
+
## Round-tripping Through DynamicValue
|
|
238
|
+
|
|
239
|
+
Structural schemas enable **cross-type conversion through `DynamicValue`** — encode a value of one nominal type and decode it as a *different* nominal type with the same structural shape. This is the core benefit of structural types for system integration.
|
|
240
|
+
|
|
241
|
+
### Motivation
|
|
242
|
+
|
|
243
|
+
In real integrations, you often receive data from an external system shaped like one type, but you need to work with it as a different type in your system. Without structural types, field-by-field translation is required. With structural types, if both types have identical shape, `DynamicValue` acts as the seamless bridge.
|
|
244
|
+
|
|
245
|
+
Common scenarios:
|
|
246
|
+
- **API gateways** — receive a `PersonDTO` from an external API, decode as your internal `Person` type
|
|
247
|
+
- **Message brokers** — consume an event shaped like `UserEvent`, convert to your domain `Account` type
|
|
248
|
+
- **Data pipelines** — records with identical fields but different class names from different services
|
|
249
|
+
|
|
250
|
+
### Cross-type conversion in action
|
|
251
|
+
|
|
252
|
+
Set up two types with identical structural shape:
|
|
253
|
+
|
|
254
|
+
```scala
|
|
255
|
+
import zio.blocks.schema.Schema
|
|
256
|
+
import zio.blocks.schema.SchemaError
|
|
257
|
+
|
|
258
|
+
case class Person(name: String, age: Int)
|
|
259
|
+
object Person {
|
|
260
|
+
implicit val schema: Schema[Person] = Schema.derived[Person]
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
case class Employee(name: String, age: Int)
|
|
264
|
+
object Employee {
|
|
265
|
+
implicit val schema: Schema[Employee] = Schema.derived[Employee]
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
val personSchema = Schema.derived[Person]
|
|
269
|
+
val employeeSchema = Schema.derived[Employee]
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
Now encode a `Person` to `DynamicValue` and decode it as an `Employee`:
|
|
273
|
+
|
|
274
|
+
```scala
|
|
275
|
+
val person = Person("Alice", 30)
|
|
276
|
+
// person: Person = Person(name = "Alice", age = 30)
|
|
277
|
+
val dynamic = personSchema.toDynamicValue(person)
|
|
278
|
+
// dynamic: DynamicValue = Record(
|
|
279
|
+
// IndexedSeq(("name", Primitive(String("Alice"))), ("age", Primitive(Int(30))))
|
|
280
|
+
// )
|
|
281
|
+
|
|
282
|
+
val employee: Either[SchemaError, Employee] =
|
|
283
|
+
employeeSchema.fromDynamicValue(dynamic)
|
|
284
|
+
// employee: Either[SchemaError, Employee] = Right(
|
|
285
|
+
// Employee(name = "Alice", age = 30)
|
|
286
|
+
// )
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
The structural shape guarantee ensures type-safe conversion: at compile time, you know both schemas accept the same fields, so round-tripping through `DynamicValue` is safe and zero-cost.
|
|
290
|
+
|
|
291
|
+
## Integration
|
|
292
|
+
|
|
293
|
+
Structural types integrate seamlessly with ZIO Blocks' broader ecosystem:
|
|
294
|
+
|
|
295
|
+
### With Schema Evolution Macros
|
|
296
|
+
|
|
297
|
+
Structural schemas work with [Schema Evolution](./schema-evolution/into.md) macros for cross-type conversion. When two types share the same structural shape, the conversion machinery can work across type boundaries:
|
|
298
|
+
|
|
299
|
+
```scala
|
|
300
|
+
import zio.blocks.schema.Schema
|
|
301
|
+
|
|
302
|
+
case class Person(name: String, age: Int)
|
|
303
|
+
object Person {
|
|
304
|
+
implicit val schema: Schema[Person] = Schema.derived[Person]
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
case class PersonDTO(name: String, age: Int)
|
|
308
|
+
object PersonDTO {
|
|
309
|
+
implicit val schema: Schema[PersonDTO] = Schema.derived[PersonDTO]
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Both types have identical structural schemas
|
|
313
|
+
val personSchema = Schema.derived[Person]
|
|
314
|
+
val dtoSchema = Schema.derived[PersonDTO]
|
|
315
|
+
|
|
316
|
+
// They share the same structural shape:
|
|
317
|
+
// Schema[{ def name: String; def age: Int }]
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
### With Binding.of (Serialization)
|
|
321
|
+
|
|
322
|
+
Structural types are also supported by the `Binding.of` macro for high-performance serialization via register-based encoding:
|
|
323
|
+
|
|
324
|
+
```scala
|
|
325
|
+
import zio.blocks.schema.binding.Binding
|
|
326
|
+
|
|
327
|
+
// Direct structural type serialization (JVM only)
|
|
328
|
+
val binding = Binding.of[{ def name: String; def age: Int }]
|
|
329
|
+
|
|
330
|
+
// Works with nested structural types
|
|
331
|
+
val nestedBinding = Binding.of[{
|
|
332
|
+
def name: String
|
|
333
|
+
def address: { def street: String; def city: String }
|
|
334
|
+
}]
|
|
335
|
+
|
|
336
|
+
// Works with containers
|
|
337
|
+
val containerBinding = Binding.of[{
|
|
338
|
+
def name: String
|
|
339
|
+
def emails: List[String]
|
|
340
|
+
}]
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
This enables anonymous structural types to benefit from ZIO Blocks' high-performance serialization without requiring nominal case class definitions. Like `Schema#structural`, this is **JVM-only**.
|
|
344
|
+
|
|
345
|
+
See [Binding](./binding.md) for detailed serialization documentation.
|
|
346
|
+
|
|
347
|
+
## Running the Examples
|
|
348
|
+
|
|
349
|
+
Example applications demonstrating structural types are available in `schema-examples`:
|
|
350
|
+
|
|
351
|
+
```sh
|
|
352
|
+
# Simple product type
|
|
353
|
+
sbt "schema-examples/runMain structural.StructuralSimpleProductExample"
|
|
354
|
+
|
|
355
|
+
# Nested products
|
|
356
|
+
sbt "schema-examples/runMain structural.StructuralNestedProductExample"
|
|
357
|
+
|
|
358
|
+
# Sealed trait (Scala 3)
|
|
359
|
+
sbt "schema-examples/runMain structural.StructuralSealedTraitExample"
|
|
360
|
+
|
|
361
|
+
# Enum (Scala 3)
|
|
362
|
+
sbt "schema-examples/runMain structural.StructuralEnumExample"
|
|
363
|
+
|
|
364
|
+
# Tuples
|
|
365
|
+
sbt "schema-examples/runMain structural.StructuralTupleExample"
|
|
366
|
+
|
|
367
|
+
# Integration with Into macro
|
|
368
|
+
sbt "schema-examples/runMain structural.StructuralIntoExample"
|
|
369
|
+
```
|