@zio.dev/zio-blocks 0.0.27 → 0.0.29

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.
Files changed (38) hide show
  1. package/guides/query-dsl-extending.md +1 -1
  2. package/guides/query-dsl-fluent-builder.md +1 -1
  3. package/guides/query-dsl-reified-optics.md +1 -1
  4. package/guides/query-dsl-sql.md +1 -1
  5. package/guides/zio-schema-migration.md +6 -6
  6. package/index.md +69 -17
  7. package/package.json +1 -1
  8. package/path-interpolator.md +70 -9
  9. package/reference/allows.md +1155 -34
  10. package/reference/binding-resolver.md +469 -0
  11. package/reference/binding.md +1 -1
  12. package/reference/codec.md +8 -8
  13. package/reference/combinators.md +345 -0
  14. package/reference/context.md +639 -67
  15. package/reference/docs.md +1 -1
  16. package/reference/dynamic-optic.md +5 -0
  17. package/reference/dynamic-schema.md +602 -0
  18. package/reference/dynamic-value.md +5 -0
  19. package/reference/http-model.md +1716 -0
  20. package/reference/json-differ.md +320 -0
  21. package/reference/json-patch.md +803 -0
  22. package/reference/json.md +1 -1
  23. package/reference/media-type.md +2 -2
  24. package/reference/patch.md +4 -0
  25. package/reference/resource-management-di/index.md +49 -0
  26. package/reference/resource-management-di/resource.md +1125 -0
  27. package/{scope.md → reference/resource-management-di/scope.md} +92 -10
  28. package/reference/resource-management-di/wire.md +832 -0
  29. package/reference/schema-evolution/as.md +587 -0
  30. package/reference/schema-evolution/index.md +50 -0
  31. package/reference/schema-evolution/into.md +1027 -0
  32. package/reference/schema-expr.md +2 -2
  33. package/reference/structural-types.md +369 -0
  34. package/reference/type-class-derivation.md +31 -31
  35. package/reference/xml.md +606 -45
  36. package/ringbuffer.md +249 -0
  37. package/sidebars.js +31 -1
  38. package/reference/schema-evolution.md +0 -540
@@ -3,34 +3,90 @@ id: allows
3
3
  title: "Allows"
4
4
  ---
5
5
 
6
- `Allows[A, S]` is a compile-time capability token that proves, at the call site, that type `A` satisfies the structural grammar `S`.
6
+ import Tabs from '@theme/Tabs';
7
+ import TabItem from '@theme/TabItem';
8
+
9
+ `Allows[A, S]` is a compile-time capability token that proves, at the call site, that type `A` satisfies the structural grammar `S`. A capability token is a compile-time phantom proof value — it carries no runtime data and exists solely to pass evidence through the type system that a structural constraint has been satisfied.
7
10
 
8
11
  `Allows` does **not** require or use `Schema[A]`. It inspects the Scala type structure of `A` directly at compile time, using nothing but the Scala type system. Any `Schema[A]` that appears alongside `Allows` in examples is the library author's own separate constraint — it is not imposed by `Allows` itself.
9
12
 
10
- ## Motivation
13
+ ```scala
14
+ sealed abstract class Allows[A, S <: Allows.Structural]
15
+ ```
16
+
17
+ ## Overview
11
18
 
12
- ZIO Blocks (ZIO Schema 2) gives library authors a powerful way to build data-oriented DSLs. A library can accept `A: Schema` and use the schema at runtime to serialize, deserialize, query, or transform values of `A`. But `Allows` is useful even without a Schema — it can enforce structural preconditions on *any* generic function.
19
+ The gap `Allows` fills is **structural preconditions** at the call site, at compile time, with precise error messages. Structural preconditions are constraints on the shape of a type's fields (e.g., "all fields must be scalars"), unlike runtime checks which happen during execution and produce exceptions or errors.
13
20
 
14
- The gap is **structural preconditions**. Many generic functions only make sense for a subset of types:
21
+ ## Motivation
15
22
 
16
- - A CSV serializer requires flat records of scalars.
17
- - An RDBMS layer cannot handle nested records as column values.
18
- - An event bus expects a sealed trait of flat record cases.
19
- - A JSON document store allows arbitrarily nested records but not `DynamicValue` leaves.
23
+ ZIO Blocks gives library authors a powerful way to build data-oriented DSLs. A library can accept `A: Schema` and use the schema at runtime to serialize, deserialize, query, or transform values of `A`. A data-oriented DSL is a generic API built around a data description (Schema) rather than a fixed interface, allowing one function to serialize, validate, or transform any conforming type. Many generic functions have **structural preconditions** that don't require a schema.
20
24
 
21
- Today, these constraints can only be checked at runtime, producing confusing errors deep inside library internals.
25
+ Consider these real-world scenarios:
22
26
 
23
- `Allows[A, S]` closes this gap: the constraint is verified at the **call site**, at compile time, with precise, path-aware error messages and concrete fix suggestions.
27
+ - A CSV serializer requires flat records of scalars — nested records should fail at the call site, not deep inside the serializer.
28
+ - An RDBMS layer cannot handle nested records as column values — the error should name the problematic field.
29
+ - An event bus expects a sealed trait of flat record cases — violations should be caught before publishing.
30
+ - A JSON document store allows arbitrarily nested records but not `DynamicValue` leaves — the schema validation should be precise. DynamicValue is the schema-less escape hatch that can hold arbitrary data — a DynamicValue leaf bypasses compile-time checking entirely, making it impossible for the compiler to enforce any structural grammar.
31
+
32
+ Without `Allows`, these constraints can only be checked at runtime, producing confusing errors deep inside library internals. With `Allows[A, S]`, the constraint is verified at the **call site**, at compile time, with precise, path-aware error messages and concrete fix suggestions.
24
33
 
25
34
  ## The Upper Bound Semantics
26
35
 
27
- `Allows[A, S]` is an upper bound. A type `A` that uses only a strict subset of what `S` permits also satisfies it — just as `A <: Foo` does not require that `A` uses every method of `Foo`.
36
+ `Allows[A, S]` is an upper bound. A type `A` that uses only a strict subset of what `S` permits also satisfies it — just as `A <: Foo` does not require that `A` uses every method of `Foo`. Upper bound semantics is the right choice because a lower bound would require using every shape (impractical), exact matching would require naming every shape used (too rigid), whereas upper bound says "your type may use any of these shapes" — a permission, not a mandate.
37
+
38
+ ```scala
39
+ import zio.blocks.schema.comptime.Allows
40
+ import Allows._
41
+
42
+ // Both satisfy Record[Primitive | Optional[Primitive]] — the upper bound
43
+
44
+ case class UserRow(name: String, age: Int)
45
+ // UserRow satisfies the grammar: all fields are Primitive
46
+
47
+ case class UserRowOpt(name: String, age: Int, email: Option[String])
48
+ // UserRowOpt also satisfies the grammar: all fields are Primitive or Optional[Primitive]
49
+
50
+ val ev1: Allows[UserRow, Record[Primitive | Optional[Primitive]]] = implicitly
51
+ val ev2: Allows[UserRowOpt, Record[Primitive | Optional[Primitive]]] = implicitly
52
+ ```
53
+
54
+ ## Creating Instances
55
+
56
+ `Allows[A, S]` is not instantiated directly. Instead, you summon an evidence value at the point where you need the constraint. The macro automatically verifies the constraint at compile time.
57
+
58
+ <Tabs groupId="scala-version" defaultValue="scala2">
59
+ <TabItem value="scala2" label="Scala 2">
60
+
61
+ ```scala
62
+ import zio.blocks.schema.comptime.Allows
63
+ import Allows._
64
+
65
+ def toJson[A](doc: A)(implicit ev: Allows[A, Record[Primitive]]): String = ???
66
+
67
+ // Or summon at the call site:
68
+ val evidence = implicitly[Allows[Int, Primitive]]
69
+ ```
70
+
71
+ </TabItem>
72
+ <TabItem value="scala3" label="Scala 3">
28
73
 
29
74
  ```scala
30
- // Allows[UserRow, Record[Primitive | Optional[Primitive]]] is satisfied even if
31
- // UserRow has no Option fields — the Optional branch is simply never needed.
75
+ import zio.blocks.schema.comptime.Allows
76
+ import Allows._
77
+
78
+ def toJson[A](doc: A)(using Allows[A, Record[Primitive]]): String = ???
79
+
80
+ // Calling the function:
81
+ case class Person(name: String, age: Int)
82
+ val json = toJson(Person("Alice", 30)) // Compiles if Person satisfies Record[Primitive]
32
83
  ```
33
84
 
85
+ </TabItem>
86
+ </Tabs>
87
+
88
+ The constraint is checked once, at the call site. If the type `A` does not satisfy `S`, you get a compile-time error with a precise message showing exactly which field violates the grammar.
89
+
34
90
  ## Grammar Nodes
35
91
 
36
92
  All grammar nodes extend `Allows.Structural`.
@@ -53,16 +109,29 @@ All grammar nodes extend `Allows.Structural`.
53
109
  | `Primitive.UUID` | `java.util.UUID` only |
54
110
  | `Primitive.Currency` | `java.util.Currency` only |
55
111
  | `Primitive.Instant` / `LocalDate` / `LocalDateTime` / … | Each specific `java.time.*` type |
112
+ | | |
56
113
  | `Record[A]` | A case class / product type whose every field satisfies `A`. Vacuously true for zero-field records. Sealed traits and enums are **automatically unwrapped**: each case is checked individually, so no `Variant` node is needed. |
57
- | `Sequence[A]` | `List`, `Vector`, `Set`, `Array`, `Chunk`, … whose element type satisfies `A` |
114
+ | `Sequence[A]` | Any collection (`List`, `Vector`, `Set`, `Array`, `Chunk`, …) whose element type satisfies `A` |
58
115
  | `Map[K, V]` | `Map`, `HashMap`, … whose key satisfies `K` and value satisfies `V` |
59
116
  | `Optional[A]` | `Option[X]` where the inner type `X` satisfies `A` |
60
117
  | `Wrapped[A]` | A ZIO Prelude `Newtype`/`Subtype` wrapper whose underlying type satisfies `A` |
61
- | `Dynamic` | `DynamicValue` — the schema-less escape hatch |
118
+ | | |
62
119
  | `Self` | Recursive self-reference back to the entire enclosing `Allows[A, S]` grammar |
120
+ | `Dynamic` | `DynamicValue` — the schema-less escape hatch |
121
+ | `IsType[A]` | Exact nominal type match: satisfied only when the checked type is exactly `A` (`=:=`) |
63
122
  | `` `\|` `` | Union of two grammar nodes: `A \| B`. In Scala 2 write `` A `\|` B `` in infix position. |
64
123
 
65
- Every specific `Primitive.Xxx` node also satisfies the catch-all `Primitive`. This means a type annotated with `Primitive.Int` is valid wherever `Primitive` or `Primitive | Primitive.Long` is required.
124
+ Every specific `Primitive.Xxx` node also satisfies the top-level `Primitive` node (which matches any of the 30 primitive types). This means a type annotated with `Primitive.Int` is valid wherever `Primitive` or `Primitive | Primitive.Long` is required.
125
+
126
+ ## Core Operations
127
+
128
+ `Allows[A, S]` is a **proof token**, not an ordinary value. It carries zero public methods that you call directly. Instead, you use it in three ways:
129
+
130
+ 1. **As a constraint in function signatures** — Declare `Allows[A, S]` as an implicit/using parameter to require that callers pass only types satisfying the grammar.
131
+ 2. **To summon evidence** — Use `implicitly[Allows[A, S]]` (Scala 2) or `summon[Allows[A, S]]` (Scala 3) at a call site to check the constraint and get an error message if it fails.
132
+ 3. **In type aliases** — Define type aliases like `type FlatRecord = Allows[_, Record[Primitive | Optional[Primitive]]]` to name constraints and reuse them across functions.
133
+
134
+ The macro that powers `Allows` checks the constraint **at compile time** and emits nothing but a reference to a single private singleton at runtime, so there is zero per-call-site overhead.
66
135
 
67
136
  ## Specific Primitives
68
137
 
@@ -118,7 +187,7 @@ def toJson[A](doc: A)(using Allows[A, Json]): String = ???
118
187
 
119
188
  `Self` recurses back to `Json` at every nested position, so `List[String]` satisfies `Sequence[JsonPrimitive | Self]` (String is JsonPrimitive), `List[Author]` satisfies it too (Author satisfies `Record[JsonPrimitive | Self]` via Self), and top-level arrays work directly.
120
189
 
121
- A type with a UUID or Instant field fails at compile time:
190
+ A type with a UUID or Instant field fails at compile time with this error:
122
191
 
123
192
  ```
124
193
  [error] Schema shape violation at WithUUID.id: found Primitive(java.util.UUID),
@@ -130,28 +199,37 @@ A type with a UUID or Instant field fails at compile time:
130
199
 
131
200
  Union types express "or" in the grammar.
132
201
 
133
- **Scala 3** uses native union type syntax:
202
+ <Tabs groupId="scala-version" defaultValue="scala2">
203
+ <TabItem value="scala2" label="Scala 2">
204
+
205
+ Uses the infix operator `` Primitive `|` Optional[Primitive] `` from `Allows`:
134
206
 
135
207
  ```scala
136
208
  import zio.blocks.schema.comptime.Allows
137
209
  import Allows._
138
210
 
139
- def writeCsv[A](rows: Seq[A])(using
140
- Allows[A, Record[Primitive | Optional[Primitive]]]
211
+ def writeCsv[A](rows: Seq[A])(implicit
212
+ ev: Allows[A, Record[Primitive | Optional[Primitive]]]
141
213
  ): Unit = ???
142
214
  ```
143
215
 
144
- **Scala 2** uses the `` `\|` `` infix operator from `Allows`:
216
+ </TabItem>
217
+ <TabItem value="scala3" label="Scala 3">
218
+
219
+ Uses native union type syntax:
145
220
 
146
221
  ```scala
147
222
  import zio.blocks.schema.comptime.Allows
148
223
  import Allows._
149
224
 
150
- def writeCsv[A](rows: Seq[A])(implicit
151
- ev: Allows[A, Record[Primitive | Optional[Primitive]]]
225
+ def writeCsv[A](rows: Seq[A])(using
226
+ Allows[A, Record[Primitive | Optional[Primitive]]]
152
227
  ): Unit = ???
153
228
  ```
154
229
 
230
+ </TabItem>
231
+ </Tabs>
232
+
155
233
  Both spellings compile and produce the same semantic behavior. The grammar is identical — the only difference is how the union type is expressed.
156
234
 
157
235
  ## Use Cases
@@ -174,7 +252,7 @@ def insert[A: Schema](value: A)(using
174
252
  ): String = ???
175
253
  ```
176
254
 
177
- If a user passes a type with nested records, they get a precise compile-time error:
255
+ If a user passes a type with nested records, they get a precise compile-time error like this:
178
256
 
179
257
  ```
180
258
  [error] Schema shape violation at UserWithAddress.address: found Record(Address),
@@ -196,7 +274,7 @@ def publish[A: Schema](event: A)(using
196
274
  ): Unit = ???
197
275
  ```
198
276
 
199
- If a case of the sealed trait has a nested record field, the error names that case and field:
277
+ If a case of the sealed trait has a nested record field, the error names that case and field like this:
200
278
 
201
279
  ```
202
280
  [error] Schema shape violation at DomainEvent.OrderPlaced.items.<element>:
@@ -215,7 +293,7 @@ import Allows._
215
293
  type JsonDocument =
216
294
  Record[Primitive | Self | Optional[Primitive | Self] | Sequence[Primitive | Self] | Allows.Map[Primitive, Primitive | Self]]
217
295
 
218
- def toJson[A: Schema](doc: A)(implicit ev: Allows[A, JsonDocument]): String = ???
296
+ def toJson[A: Schema](doc: A)(using Allows[A, JsonDocument]): String = ???
219
297
  ```
220
298
 
221
299
  This grammar allows:
@@ -243,13 +321,112 @@ object TreeNode { implicit val schema: Schema[TreeNode] = Schema.derived }
243
321
  // graphqlType[TreeNode]() — compiles fine
244
322
  ```
245
323
 
324
+ ## Sequence Subtypes
325
+
326
+ The `Sequence[A]` node accepts any collection type. When a DSL needs to restrict to a specific kind of collection — for example, a DynamoDB `Set` operation that is only valid on sets, not lists — use the `Sequence` subtypes:
327
+
328
+ ```scala
329
+ import zio.blocks.schema.comptime.Allows
330
+ import Allows._
331
+
332
+ // Only an immutable List is accepted
333
+ val listOnly: Allows[List[Int], Sequence.List[Primitive]] = implicitly
334
+
335
+ // Only an immutable Set is accepted
336
+ val setOnly: Allows[Set[Int], Sequence.Set[Primitive]] = implicitly
337
+
338
+ // Only a Vector
339
+ val vecOnly: Allows[Vector[String], Sequence.Vector[Primitive]] = implicitly
340
+
341
+ // Only an Array
342
+ val arrOnly: Allows[Array[Int], Sequence.Array[Primitive]] = implicitly
343
+
344
+ // Only a Chunk
345
+ import zio.blocks.chunk.Chunk
346
+ val chkOnly: Allows[Chunk[String], Sequence.Chunk[Primitive]] = implicitly
347
+ ```
348
+
349
+ Each subtype extends `Sequence[A]`, so a grammar written with the parent `Sequence` still accepts all collection kinds. A grammar written with a subtype rejects other kinds at compile time:
350
+
351
+ ```
352
+ // Set[Int] does NOT satisfy Sequence.List[Primitive] — compile error:
353
+ [error] Shape violation at Set: found Sequence[scala.Int], required Sequence.List[...]
354
+ val bad: Allows[Set[Int], Sequence.List[Primitive]] = implicitly
355
+ ```
356
+
357
+ ### DynamoDB-style set operations
358
+
359
+ A DynamoDB grammar can encode the distinction between set types and list types exactly, without any additional runtime proof. We use `Sequence.Set` to narrow the grammar to sets-only operations:
360
+
361
+ ```scala
362
+ import zio.blocks.schema.comptime.Allows
363
+ import Allows._
364
+
365
+ type N = Primitive.Int | Primitive.Long | Primitive.Float | Primitive.Double | Primitive.Short
366
+ type S = Primitive.String
367
+ type NS = Sequence.Set[N | Wrapped[N]]
368
+ type SS = Sequence.Set[S | Wrapped[S]]
369
+
370
+ // addSet is only callable with a Set — List[Int] or Vector[Int] would fail at compile time
371
+ def addSet[A](set: scala.collection.immutable.Set[A])(implicit
372
+ ev: Allows[scala.collection.immutable.Set[A], NS | SS]
373
+ ): String = "ok"
374
+ ```
375
+
376
+ ## `IsType[A]`
377
+
378
+ `IsType[A]` is a nominal type predicate. It is satisfied only when the checked Scala type is exactly `A` (i.e. `checked =:= A`). It is most useful as an element constraint inside `Sequence` subtypes, where it aligns the element type of a collection with a polymorphic method type parameter.
379
+
380
+ The primary motivation (GitHub issue #1172) is DSL methods that must constrain both the container kind and the element type in a single `Allows` expression. Without `IsType`, a separate type class (like `Containable`) is needed to connect `A` in `contains[A]` to the element type of the collection. With `IsType`, the connection is expressed directly in the grammar.
381
+
382
+ To use `IsType[A]` with a polymorphic `A`, require `IsNominalType[A]` from `zio-blocks-typeid` at the call site. This ensures the macro always sees a concrete type when it evaluates `IsType[A]`:
383
+
384
+ ```scala
385
+ import zio.blocks.schema.comptime.Allows
386
+ import Allows._
387
+ import zio.blocks.typeid.IsNominalType
388
+
389
+ // `To` must be a Set whose element type is exactly `A`.
390
+ // IsNominalType[A] ensures A is concrete at the call site — an unresolved
391
+ // type parameter would fail to produce IsNominalType and the call site
392
+ // would not compile.
393
+ def contains[To, From, A: IsNominalType](a: A)(implicit
394
+ ev: Allows[To, Sequence.Set[IsType[A]]]
395
+ ): Boolean = ev.ne(null)
396
+
397
+ // Compiles: Set[Int] satisfies Sequence.Set[IsType[Int]]
398
+ val r1: Boolean = contains[Set[Int], Nothing, Int](42)
399
+
400
+ // Compiles: Set[String] satisfies Sequence.Set[IsType[String]]
401
+ val r2: Boolean = contains[Set[String], Nothing, String]("hello")
402
+ ```
403
+
404
+ A mismatch between the element type and `A` is a compile-time error:
405
+
406
+ ```
407
+ [error] Shape violation at ...<element>: found Primitive(java.lang.String),
408
+ required IsType[Int]
409
+ ```
410
+
411
+ `IsType[A]` can also appear as a standalone constraint, or anywhere a grammar node is accepted:
412
+
413
+ ```scala
414
+ import zio.blocks.schema.comptime.Allows
415
+
416
+ // Int satisfies IsType[Int] exactly
417
+ val ev: Allows[Int, Allows.IsType[Int]] = implicitly
418
+
419
+ // List[String] satisfies Sequence[IsType[String]]
420
+ val ev2: Allows[List[String], Allows.Sequence[Allows.IsType[String]]] = implicitly
421
+ ```
422
+
246
423
  ## The `Self` Grammar Node
247
424
 
248
425
  `Self` refers back to the entire enclosing `Allows[A, S]` grammar. It allows the grammar to describe recursive data structures.
249
426
 
250
427
  **Non-recursive types** satisfy `Self`-containing grammars without issue: if no field ever recurses back to the root type, the `Self` position is never reached, and the constraint is vacuously satisfied.
251
428
 
252
- **Mutual recursion** between two or more distinct types is a compile-time error:
429
+ **Mutual recursion** between two or more distinct types is a compile-time error reported as:
253
430
 
254
431
  ```
255
432
  [error] Mutually recursive types are not supported by Allows.
@@ -258,11 +435,15 @@ object TreeNode { implicit val schema: Schema[TreeNode] = Schema.derived }
258
435
 
259
436
  ## `Wrapped[A]` and Newtypes
260
437
 
261
- The `Wrapped[A]` node matches ZIO Prelude `Newtype` and `Subtype` wrappers. The underlying type must satisfy `A`.
438
+ The `Wrapped[A]` node matches ZIO Prelude `Newtype` and `Subtype` wrappers. The underlying type must satisfy `A`. Here's an example:
262
439
 
263
440
  ```scala
264
- // ZIO Prelude Newtype pattern:
265
441
  import zio.prelude.Newtype
442
+ import zio.blocks.schema.Schema
443
+ import zio.blocks.schema.comptime.Allows
444
+ import Allows._
445
+
446
+ // ZIO Prelude Newtype pattern:
266
447
  object ProductCode extends Newtype[String]
267
448
  type ProductCode = ProductCode.Type
268
449
 
@@ -273,11 +454,10 @@ given Schema[ProductCode] =
273
454
  val ev: Allows[ProductCode, Wrapped[Primitive]] = implicitly
274
455
  ```
275
456
 
276
- **Scala 3 opaque types** are resolved to their underlying type by the macro (they are transparent), so `opaque type UserId = UUID` satisfies `Primitive` (not `Wrapped[Primitive]`):
457
+ **Scala 3 opaque types** are resolved to their underlying type by the macro (they are transparent), so an opaque alias like this satisfies `Primitive` directly:
277
458
 
278
459
  ```scala
279
460
  opaque type UserId = java.util.UUID
280
- // UserId satisfies Allows[UserId, Primitive] — resolved to UUID (a primitive)
281
461
  ```
282
462
 
283
463
  ## Sealed Traits and Enums (Auto-Unwrap)
@@ -310,9 +490,7 @@ When a type does not satisfy the grammar, the macro reports:
310
490
  3. **What was required**: `Primitive | Sequence[Primitive]`
311
491
  4. **A hint** where applicable
312
492
 
313
- Multiple violations are reported in a single compilation pass — the user sees all problems at once.
314
-
315
- Example:
493
+ Multiple violations are reported in a single compilation pass — the user sees all problems at once, for example:
316
494
 
317
495
  ```
318
496
  [error] Schema shape violation at UserWithAddress.address: found Record(Address),
@@ -350,3 +528,946 @@ val ev: Allows[EmptyEvent.type, Record[Primitive]] = implicitly // vacuously tr
350
528
  | Derivation keyword | `Schema.derived` implicit | `Schema.derived` or `derives Schema` |
351
529
 
352
530
  Both Scala versions produce the same macro behavior and the same error messages.
531
+
532
+ ## Integration with Schema
533
+
534
+ `Allows` and `Schema` are complementary but independent:
535
+
536
+ - **`Schema[A]`** describes what an `A` looks like at runtime — how to serialize, deserialize, introspect, or transform it. It requires explicit derivation and handles the full type signature.
537
+ - **`Allows[A, S]`** describes what an `A` *may* look like at compile time — a structural grammar that `A` must satisfy. It requires no schema and uses only the Scala type system.
538
+
539
+ You can use `Allows` **without** `Schema`:
540
+
541
+ ```scala
542
+ import zio.blocks.schema.comptime.Allows
543
+ import Allows._
544
+
545
+ // Pure shape constraint, no Schema required
546
+ def writeCsv[A](rows: Seq[A])(using Allows[A, Record[Primitive | Optional[Primitive]]]): Unit = ???
547
+ ```
548
+
549
+ Or combine them when runtime encoding **and** shape validation are both needed:
550
+
551
+ ```scala
552
+ import zio.blocks.schema.Schema
553
+ import zio.blocks.schema.comptime.Allows
554
+ import Allows._
555
+
556
+ // Shape constraint + runtime encoding
557
+ def writeCsv[A: Schema](rows: Seq[A])(using
558
+ Allows[A, Record[Primitive | Optional[Primitive]]]
559
+ ): Unit = ???
560
+ ```
561
+
562
+ When combined, `Allows` enforces the structural guarantee that `Schema` can use — for example, a CSV serializer can assume that every field is a primitive or optional primitive and skip defensive type checks.
563
+
564
+ See [Schema](./schema.md) for more on runtime encoding and decoding with schemas.
565
+
566
+ ## Running the Examples
567
+
568
+ All code from this guide is available as runnable examples in the `schema-examples` module.
569
+
570
+ **1. Clone the repository and navigate to the project:**
571
+
572
+ ```bash
573
+ git clone https://github.com/zio/zio-blocks.git
574
+ cd zio-blocks
575
+ ```
576
+
577
+ **2. Run individual examples with sbt:**
578
+
579
+ **CSV serializer with flat record compile-time constraints**
580
+ ([source](https://github.com/zio/zio-blocks/blob/main/schema-examples/src/main/scala/comptime/AllowsCsvExample.scala))
581
+
582
+ ```bash
583
+ sbt "schema-examples/runMain comptime.AllowsCsvExample"
584
+ ```
585
+
586
+ ```scala title="schema-examples/src/main/scala/comptime/AllowsCsvExample.scala"
587
+ /*
588
+ * Copyright 2024-2026 John A. De Goes and the ZIO Contributors
589
+ *
590
+ * Licensed under the Apache License, Version 2.0 (the "License");
591
+ * you may not use this file except in compliance with the License.
592
+ * You may obtain a copy of the License at
593
+ *
594
+ * http://www.apache.org/licenses/LICENSE-2.0
595
+ *
596
+ * Unless required by applicable law or agreed to in writing, software
597
+ * distributed under the License is distributed on an "AS IS" BASIS,
598
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
599
+ * See the License for the specific language governing permissions and
600
+ * limitations under the License.
601
+ */
602
+
603
+ package comptime
604
+
605
+ import zio.blocks.schema._
606
+ import zio.blocks.schema.comptime.Allows
607
+ import Allows.{Primitive, Record, `|`}
608
+ import Allows.{Optional => AOptional}
609
+ import util.ShowExpr.show
610
+
611
+ // ---------------------------------------------------------------------------
612
+ // CSV serializer example using Allows[A, S] compile-time shape constraints
613
+ //
614
+ // A CSV row is a flat record: every field must be a primitive scalar or an
615
+ // optional primitive (for nullable columns). Nested records, sequences, and
616
+ // maps are all rejected at compile time.
617
+ // ---------------------------------------------------------------------------
618
+
619
+ // Compatible: flat record of primitives and optional primitives
620
+ case class Employee(name: String, department: String, salary: BigDecimal, active: Boolean)
621
+ object Employee { implicit val schema: Schema[Employee] = Schema.derived }
622
+
623
+ case class SensorReading(sensorId: String, timestamp: Long, value: Double, unit: Option[String])
624
+ object SensorReading { implicit val schema: Schema[SensorReading] = Schema.derived }
625
+
626
+ object CsvSerializer {
627
+
628
+ type FlatRow = Primitive | AOptional[Primitive]
629
+
630
+ /** Serialize a sequence of flat records to CSV format. */
631
+ def toCsv[A](rows: Seq[A])(implicit schema: Schema[A], ev: Allows[A, Record[FlatRow]]): String = {
632
+ val reflect = schema.reflect.asRecord.get
633
+ val header = reflect.fields.map(_.name).mkString(",")
634
+ val lines = rows.map { row =>
635
+ val dv = schema.toDynamicValue(row)
636
+ dv match {
637
+ case DynamicValue.Record(fields) =>
638
+ fields.map { case (_, v) => csvEscape(dvToString(v)) }.mkString(",")
639
+ case _ => ""
640
+ }
641
+ }
642
+ (header +: lines).mkString("\n")
643
+ }
644
+
645
+ private def dvToString(dv: DynamicValue): String = dv match {
646
+ case DynamicValue.Primitive(PrimitiveValue.String(s)) => s
647
+ case DynamicValue.Primitive(PrimitiveValue.Boolean(b)) => b.toString
648
+ case DynamicValue.Primitive(PrimitiveValue.Int(n)) => n.toString
649
+ case DynamicValue.Primitive(PrimitiveValue.Long(n)) => n.toString
650
+ case DynamicValue.Primitive(PrimitiveValue.Double(n)) => n.toString
651
+ case DynamicValue.Primitive(PrimitiveValue.Float(n)) => n.toString
652
+ case DynamicValue.Primitive(PrimitiveValue.BigDecimal(n)) => n.toString
653
+ case DynamicValue.Primitive(v) => v.toString
654
+ case DynamicValue.Null => ""
655
+ case DynamicValue.Variant(tag, inner) if tag == "Some" => dvToString(inner)
656
+ case DynamicValue.Variant(tag, _) if tag == "None" => ""
657
+ case DynamicValue.Record(fields) =>
658
+ fields.headOption.map { case (_, v) => dvToString(v) }.getOrElse("")
659
+ case other => other.toString
660
+ }
661
+
662
+ private def csvEscape(s: String): String =
663
+ if (s.contains(",") || s.contains("\"") || s.contains("\n"))
664
+ "\"" + s.replace("\"", "\"\"") + "\""
665
+ else s
666
+ }
667
+
668
+ // ---------------------------------------------------------------------------
669
+ // Demonstration
670
+ // ---------------------------------------------------------------------------
671
+
672
+ object AllowsCsvExample extends App {
673
+
674
+ // Flat records of primitives — compiles fine
675
+ val employees = Seq(
676
+ Employee("Alice", "Engineering", BigDecimal("120000.00"), true),
677
+ Employee("Bob", "Marketing", BigDecimal("95000.50"), true),
678
+ Employee("Carol", "Engineering", BigDecimal("115000.00"), false)
679
+ )
680
+
681
+ // CSV output for a flat record of primitives
682
+ show(CsvSerializer.toCsv(employees))
683
+
684
+ // Flat record with optional fields — also compiles
685
+ val readings = Seq(
686
+ SensorReading("temp-01", 1709712000L, 23.5, Some("celsius")),
687
+ SensorReading("temp-02", 1709712060L, 72.1, None)
688
+ )
689
+
690
+ // Optional fields become empty CSV cells when None
691
+ show(CsvSerializer.toCsv(readings))
692
+
693
+ // The following would NOT compile — uncomment to see the error:
694
+ //
695
+ // case class Nested(name: String, address: Address)
696
+ // object Nested { implicit val schema: Schema[Nested] = Schema.derived }
697
+ // CsvSerializer.toCsv(Seq(Nested("Alice", Address("1 Main St", "NY", "10001"))))
698
+ // [error] Schema shape violation at Nested.address: found Record(Address),
699
+ // required Primitive | Optional[Primitive]
700
+ }
701
+ ```
702
+
703
+ **Event bus with sealed trait auto-unwrap and nested hierarchies**
704
+ ([source](https://github.com/zio/zio-blocks/blob/main/schema-examples/src/main/scala/comptime/AllowsEventBusExample.scala))
705
+
706
+ ```bash
707
+ sbt "schema-examples/runMain comptime.AllowsEventBusExample"
708
+ ```
709
+
710
+ ```scala title="schema-examples/src/main/scala/comptime/AllowsEventBusExample.scala"
711
+ /*
712
+ * Copyright 2024-2026 John A. De Goes and the ZIO Contributors
713
+ *
714
+ * Licensed under the Apache License, Version 2.0 (the "License");
715
+ * you may not use this file except in compliance with the License.
716
+ * You may obtain a copy of the License at
717
+ *
718
+ * http://www.apache.org/licenses/LICENSE-2.0
719
+ *
720
+ * Unless required by applicable law or agreed to in writing, software
721
+ * distributed under the License is distributed on an "AS IS" BASIS,
722
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
723
+ * See the License for the specific language governing permissions and
724
+ * limitations under the License.
725
+ */
726
+
727
+ package comptime
728
+
729
+ import zio.blocks.schema._
730
+ import zio.blocks.schema.comptime.Allows
731
+ import Allows.{Primitive, Record, Sequence, `|`}
732
+ import Allows.{Optional => AOptional}
733
+ import util.ShowExpr.show
734
+
735
+ // ---------------------------------------------------------------------------
736
+ // Event bus / message broker example using Allows[A, S]
737
+ //
738
+ // Published events are typically sealed traits of flat record cases. Sealed
739
+ // traits are automatically unwrapped by the Allows macro — each case is
740
+ // checked individually against the grammar. No Variant node is needed.
741
+ //
742
+ // This example also shows nested sealed traits (auto-unwrap is recursive).
743
+ // ---------------------------------------------------------------------------
744
+
745
+ // Domain events — a sealed trait hierarchy
746
+ sealed trait AccountEvent
747
+ case class AccountOpened(accountId: String, owner: String, initialBalance: BigDecimal) extends AccountEvent
748
+ case class FundsDeposited(accountId: String, amount: BigDecimal) extends AccountEvent
749
+ case class FundsWithdrawn(accountId: String, amount: BigDecimal) extends AccountEvent
750
+ case class AccountClosed(accountId: String, reason: Option[String]) extends AccountEvent
751
+ object AccountEvent { implicit val schema: Schema[AccountEvent] = Schema.derived }
752
+
753
+ // Nested sealed trait — InventoryEvent has a sub-hierarchy
754
+ sealed trait InventoryEvent
755
+ case class ItemAdded(sku: String, quantity: Int) extends InventoryEvent
756
+ case class ItemRemoved(sku: String, quantity: Int) extends InventoryEvent
757
+
758
+ sealed trait InventoryAlert extends InventoryEvent
759
+ case class LowStock(sku: String, remaining: Int) extends InventoryAlert
760
+ case class OutOfStock(sku: String) extends InventoryAlert
761
+
762
+ object InventoryEvent { implicit val schema: Schema[InventoryEvent] = Schema.derived }
763
+
764
+ // Event with sequence fields (e.g. tags or batch items)
765
+ sealed trait BatchEvent
766
+ case class BatchImport(batchId: String, itemIds: List[String]) extends BatchEvent
767
+ case class BatchComplete(batchId: String, count: Int) extends BatchEvent
768
+ object BatchEvent { implicit val schema: Schema[BatchEvent] = Schema.derived }
769
+
770
+ object EventBus {
771
+
772
+ type EventShape = Primitive | AOptional[Primitive]
773
+
774
+ /**
775
+ * Publish a domain event. All cases of the sealed trait must be flat records.
776
+ */
777
+ def publish[A](event: A)(implicit schema: Schema[A], ev: Allows[A, Record[EventShape]]): String = {
778
+ val dv = schema.toDynamicValue(event)
779
+ val (typeName, payload) = dv match {
780
+ case DynamicValue.Variant(name, inner) => (name, inner.toJson.toString)
781
+ case _ => (schema.reflect.typeId.name, dv.toJson.toString)
782
+ }
783
+ s"PUBLISH topic=${schema.reflect.typeId.name} type=$typeName payload=$payload"
784
+ }
785
+
786
+ /**
787
+ * Publish events that may contain sequence fields (e.g. batch operations).
788
+ */
789
+ def publishBatch[A](event: A)(implicit
790
+ schema: Schema[A],
791
+ ev: Allows[A, Record[Primitive | Sequence[Primitive]]]
792
+ ): String = {
793
+ val dv = schema.toDynamicValue(event)
794
+ val (typeName, payload) = dv match {
795
+ case DynamicValue.Variant(name, inner) => (name, inner.toJson.toString)
796
+ case _ => (schema.reflect.typeId.name, dv.toJson.toString)
797
+ }
798
+ s"PUBLISH topic=${schema.reflect.typeId.name} type=$typeName payload=$payload"
799
+ }
800
+ }
801
+
802
+ // ---------------------------------------------------------------------------
803
+ // Demonstration
804
+ // ---------------------------------------------------------------------------
805
+
806
+ object AllowsEventBusExample extends App {
807
+
808
+ // Flat sealed trait — all cases are records of primitives/optionals
809
+ show(EventBus.publish[AccountEvent](AccountOpened("acc-001", "Alice", BigDecimal("1000.00"))))
810
+ show(EventBus.publish[AccountEvent](FundsDeposited("acc-001", BigDecimal("500.00"))))
811
+ show(EventBus.publish[AccountEvent](AccountClosed("acc-001", Some("customer request"))))
812
+
813
+ // Nested sealed trait — auto-unwrap is recursive
814
+ // InventoryAlert extends InventoryEvent, both are unwrapped
815
+ show(EventBus.publish[InventoryEvent](ItemAdded("SKU-100", 50)))
816
+ show(EventBus.publish[InventoryEvent](LowStock("SKU-100", 3)))
817
+ show(EventBus.publish[InventoryEvent](OutOfStock("SKU-100")))
818
+
819
+ // Events with sequence fields use a wider grammar
820
+ show(EventBus.publishBatch[BatchEvent](BatchImport("batch-42", List("item-1", "item-2", "item-3"))))
821
+ show(EventBus.publishBatch[BatchEvent](BatchComplete("batch-42", 3)))
822
+ }
823
+ ```
824
+
825
+ **GraphQL / tree structures using Self for recursive grammars**
826
+ ([source](https://github.com/zio/zio-blocks/blob/main/schema-examples/src/main/scala/comptime/AllowsGraphQLTreeExample.scala))
827
+
828
+ ```bash
829
+ sbt "schema-examples/runMain comptime.AllowsGraphQLTreeExample"
830
+ ```
831
+
832
+ ```scala title="schema-examples/src/main/scala/comptime/AllowsGraphQLTreeExample.scala"
833
+ /*
834
+ * Copyright 2024-2026 John A. De Goes and the ZIO Contributors
835
+ *
836
+ * Licensed under the Apache License, Version 2.0 (the "License");
837
+ * you may not use this file except in compliance with the License.
838
+ * You may obtain a copy of the License at
839
+ *
840
+ * http://www.apache.org/licenses/LICENSE-2.0
841
+ *
842
+ * Unless required by applicable law or agreed to in writing, software
843
+ * distributed under the License is distributed on an "AS IS" BASIS,
844
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
845
+ * See the License for the specific language governing permissions and
846
+ * limitations under the License.
847
+ */
848
+
849
+ package comptime
850
+
851
+ import zio.blocks.schema._
852
+ import zio.blocks.schema.comptime.Allows
853
+ import Allows.{Primitive, Record, Sequence, `|`}
854
+ import Allows.{Optional => AOptional, Self => ASelf}
855
+ import util.ShowExpr.show
856
+
857
+ // ---------------------------------------------------------------------------
858
+ // GraphQL / tree structure example using Self for recursive grammars
859
+ //
860
+ // Self refers back to the entire enclosing Allows[A, S] grammar, allowing
861
+ // the constraint to describe recursive data structures like trees, linked
862
+ // lists, and nested menus.
863
+ //
864
+ // Non-recursive types also satisfy Self-containing grammars — the Self
865
+ // position is never reached, so the constraint is vacuously satisfied.
866
+ // ---------------------------------------------------------------------------
867
+
868
+ // Recursive tree: children reference the same type
869
+ case class TreeNode(value: Int, children: List[TreeNode])
870
+ object TreeNode { implicit val schema: Schema[TreeNode] = Schema.derived }
871
+
872
+ // Recursive category hierarchy (common in e-commerce, CMS, etc.)
873
+ case class NavCategory(name: String, slug: String, subcategories: List[NavCategory])
874
+ object NavCategory { implicit val schema: Schema[NavCategory] = Schema.derived }
875
+
876
+ // Linked list via Optional[Self]
877
+ case class Chain(label: String, next: Option[Chain])
878
+ object Chain { implicit val schema: Schema[Chain] = Schema.derived }
879
+
880
+ // Non-recursive type — satisfies Self-containing grammars vacuously
881
+ case class FlatNode(id: Int, label: String)
882
+ object FlatNode { implicit val schema: Schema[FlatNode] = Schema.derived }
883
+
884
+ object GraphQL {
885
+
886
+ type TreeShape = Primitive | Sequence[ASelf] | AOptional[ASelf]
887
+
888
+ /** Generate a simplified GraphQL type definition for a recursive type. */
889
+ def graphqlType[A](implicit schema: Schema[A], ev: Allows[A, Record[TreeShape]]): String = {
890
+ val reflect = schema.reflect.asRecord.get
891
+ val fields = reflect.fields.map { f =>
892
+ s" ${f.name}: ${gqlType(resolve(f.value), schema.reflect.typeId.name)}"
893
+ }
894
+ s"type ${schema.reflect.typeId.name} {\n${fields.mkString("\n")}\n}"
895
+ }
896
+
897
+ /** Unwrap Deferred to get the actual Reflect node. */
898
+ private def resolve(r: Reflect.Bound[_]): Reflect.Bound[_] = r match {
899
+ case d: Reflect.Deferred[_, _] => resolve(d.value.asInstanceOf[Reflect.Bound[_]])
900
+ case other => other
901
+ }
902
+
903
+ private def gqlType(r: Reflect.Bound[_], selfName: String): String = r match {
904
+ case _: Reflect.Sequence[_, _, _] => s"[$selfName]"
905
+ case p: Reflect.Primitive[_, _] =>
906
+ p.primitiveType match {
907
+ case PrimitiveType.Int(_) => "Int"
908
+ case PrimitiveType.Long(_) => "Int"
909
+ case PrimitiveType.Float(_) => "Float"
910
+ case PrimitiveType.Double(_) => "Float"
911
+ case PrimitiveType.String(_) => "String"
912
+ case PrimitiveType.Boolean(_) => "Boolean"
913
+ case _ => "String"
914
+ }
915
+ case _ => selfName
916
+ }
917
+ }
918
+
919
+ // ---------------------------------------------------------------------------
920
+ // Demonstration
921
+ // ---------------------------------------------------------------------------
922
+
923
+ object AllowsGraphQLTreeExample extends App {
924
+
925
+ // Recursive tree with Sequence[Self]
926
+ show(GraphQL.graphqlType[TreeNode])
927
+
928
+ // Recursive categories — same grammar, different domain
929
+ show(GraphQL.graphqlType[NavCategory])
930
+
931
+ // Linked list via Optional[Self]
932
+ show(GraphQL.graphqlType[Chain])
933
+
934
+ // Non-recursive type also satisfies the grammar (vacuously — Self is never reached)
935
+ show(GraphQL.graphqlType[FlatNode])
936
+
937
+ // Show that recursive data actually works at runtime
938
+ val tree = TreeNode(
939
+ 1,
940
+ List(
941
+ TreeNode(2, List(TreeNode(4, Nil), TreeNode(5, Nil))),
942
+ TreeNode(3, Nil)
943
+ )
944
+ )
945
+ show(Schema[TreeNode].toDynamicValue(tree).toJson.toString)
946
+
947
+ val nav = NavCategory(
948
+ "Electronics",
949
+ "electronics",
950
+ List(
951
+ NavCategory("Phones", "phones", Nil),
952
+ NavCategory(
953
+ "Laptops",
954
+ "laptops",
955
+ List(
956
+ NavCategory("Gaming", "gaming", Nil)
957
+ )
958
+ )
959
+ )
960
+ )
961
+ show(Schema[NavCategory].toDynamicValue(nav).toJson.toString)
962
+ }
963
+ ```
964
+
965
+ **Sealed trait auto-unwrap with nested hierarchies and case objects**
966
+ ([source](https://github.com/zio/zio-blocks/blob/main/schema-examples/src/main/scala/comptime/AllowsSealedTraitExample.scala))
967
+
968
+ ```bash
969
+ sbt "schema-examples/runMain comptime.AllowsSealedTraitExample"
970
+ ```
971
+
972
+ ```scala title="schema-examples/src/main/scala/comptime/AllowsSealedTraitExample.scala"
973
+ /*
974
+ * Copyright 2024-2026 John A. De Goes and the ZIO Contributors
975
+ *
976
+ * Licensed under the Apache License, Version 2.0 (the "License");
977
+ * you may not use this file except in compliance with the License.
978
+ * You may obtain a copy of the License at
979
+ *
980
+ * http://www.apache.org/licenses/LICENSE-2.0
981
+ *
982
+ * Unless required by applicable law or agreed to in writing, software
983
+ * distributed under the License is distributed on an "AS IS" BASIS,
984
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
985
+ * See the License for the specific language governing permissions and
986
+ * limitations under the License.
987
+ */
988
+
989
+ package comptime
990
+
991
+ import zio.blocks.schema._
992
+ import zio.blocks.schema.comptime.Allows
993
+ import Allows.{Primitive, Record}
994
+ import util.ShowExpr.show
995
+
996
+ // ---------------------------------------------------------------------------
997
+ // Sealed trait auto-unwrap example
998
+ //
999
+ // Sealed traits and enums are automatically unwrapped by the Allows macro.
1000
+ // Each case is checked individually against the grammar — no Variant node
1001
+ // is needed.
1002
+ //
1003
+ // Auto-unwrap is recursive: if a case is itself a sealed trait, its cases
1004
+ // are unwrapped too, to any depth.
1005
+ //
1006
+ // Zero-field records (case objects) are vacuously true for any Record[A]
1007
+ // constraint.
1008
+ // ---------------------------------------------------------------------------
1009
+
1010
+ // Simple sealed trait with case classes and a case object
1011
+ sealed trait Shape
1012
+ case class Circle(radius: Double) extends Shape
1013
+ case class Rectangle(width: Double, height: Double) extends Shape
1014
+ case object Point extends Shape
1015
+ object Shape { implicit val schema: Schema[Shape] = Schema.derived }
1016
+
1017
+ // Nested sealed trait hierarchy — two levels deep
1018
+ sealed trait Expr
1019
+ sealed trait BinaryOp extends Expr
1020
+ case class Add(left: Double, right: Double) extends BinaryOp
1021
+ case class Multiply(left: Double, right: Double) extends BinaryOp
1022
+ case class Literal(value: Double) extends Expr
1023
+ case object Zero extends Expr
1024
+ object Expr { implicit val schema: Schema[Expr] = Schema.derived }
1025
+
1026
+ // All-singleton enum (all case objects)
1027
+ sealed trait Color
1028
+ case object Red extends Color
1029
+ case object Green extends Color
1030
+ case object Blue extends Color
1031
+ object Color { implicit val schema: Schema[Color] = Schema.derived }
1032
+
1033
+ object SealedTraitValidator {
1034
+
1035
+ /** Validate that a value's type has a flat record structure. */
1036
+ def validate[A](value: A)(implicit schema: Schema[A], ev: Allows[A, Record[Primitive]]): String = {
1037
+ val dv = schema.toDynamicValue(value)
1038
+ dv match {
1039
+ case DynamicValue.Variant(caseName, inner) =>
1040
+ s"Valid variant case '$caseName': ${inner.toJson}"
1041
+ case DynamicValue.Record(fields) =>
1042
+ s"Valid record with ${fields.size} field(s): ${fields.map(_._1).mkString(", ")}"
1043
+ case _ =>
1044
+ s"Valid: ${dv.toJson}"
1045
+ }
1046
+ }
1047
+ }
1048
+
1049
+ // ---------------------------------------------------------------------------
1050
+ // Demonstration
1051
+ // ---------------------------------------------------------------------------
1052
+
1053
+ object AllowsSealedTraitExample extends App {
1054
+
1055
+ // Simple sealed trait — all cases checked against Record[Primitive]
1056
+ // Circle: Record(radius: Double) — satisfies Record[Primitive]
1057
+ // Rectangle: Record(width: Double, height: Double) — satisfies Record[Primitive]
1058
+ // Point: zero-field case object — vacuously true
1059
+ show(SealedTraitValidator.validate[Shape](Circle(3.14)))
1060
+ show(SealedTraitValidator.validate[Shape](Rectangle(4.0, 5.0)))
1061
+ show(SealedTraitValidator.validate[Shape](Point))
1062
+
1063
+ // Nested sealed trait — auto-unwrap is recursive
1064
+ // BinaryOp is itself sealed with Add and Multiply
1065
+ // All leaf cases have only Double fields — satisfies Record[Primitive]
1066
+ show(SealedTraitValidator.validate[Expr](Add(1.0, 2.0)))
1067
+ show(SealedTraitValidator.validate[Expr](Multiply(3.0, 4.0)))
1068
+ show(SealedTraitValidator.validate[Expr](Literal(42.0)))
1069
+ show(SealedTraitValidator.validate[Expr](Zero))
1070
+
1071
+ // All-singleton enum — every case is a zero-field record (vacuously true)
1072
+ show(SealedTraitValidator.validate[Color](Red))
1073
+ show(SealedTraitValidator.validate[Color](Green))
1074
+ show(SealedTraitValidator.validate[Color](Blue))
1075
+ }
1076
+ ```
1077
+
1078
+ **RDBMS library with CREATE TABLE and INSERT using flat record constraints** (compile-only)
1079
+ ([source](https://github.com/zio/zio-blocks/blob/main/schema-examples/src/main/scala/comptime/RdbmsExample.scala))
1080
+
1081
+ Demonstrates how Allows constraints are verified at compile time — the code below shows valid examples that compile successfully, and includes comments showing which patterns would be rejected:
1082
+
1083
+ ```scala title="schema-examples/src/main/scala/comptime/RdbmsExample.scala"
1084
+ /*
1085
+ * Copyright 2024-2026 John A. De Goes and the ZIO Contributors
1086
+ *
1087
+ * Licensed under the Apache License, Version 2.0 (the "License");
1088
+ * you may not use this file except in compliance with the License.
1089
+ * You may obtain a copy of the License at
1090
+ *
1091
+ * http://www.apache.org/licenses/LICENSE-2.0
1092
+ *
1093
+ * Unless required by applicable law or agreed to in writing, software
1094
+ * distributed under the License is distributed on an "AS IS" BASIS,
1095
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1096
+ * See the License for the specific language governing permissions and
1097
+ * limitations under the License.
1098
+ */
1099
+
1100
+ package comptime
1101
+
1102
+ import zio.blocks.schema._
1103
+ import zio.blocks.schema.comptime.Allows
1104
+ import Allows.{Primitive, Record, `|`}
1105
+ import Allows.{Map => AMap, Optional => AOptional}
1106
+
1107
+ // ---------------------------------------------------------------------------
1108
+ // Realistic RDBMS example using Allows[A, S] compile-time shape constraints
1109
+ //
1110
+ // Demonstrates how a library can require that user-supplied types have a
1111
+ // structure compatible with what a relational database can represent:
1112
+ // - Flat records of primitives, optional primitives, or primitive-valued maps
1113
+ // - Top-level variants (enum tables) whose cases are flat records
1114
+ //
1115
+ // Incompatible types (nested records, sequences of records, etc.) are
1116
+ // rejected at the call site with a precise compile-time error message.
1117
+ // ---------------------------------------------------------------------------
1118
+
1119
+ // ---------------------------------------------------------------------------
1120
+ // Domain types — vary from compatible to incompatible
1121
+ // ---------------------------------------------------------------------------
1122
+
1123
+ // Compatible: flat record of primitives and optional primitives
1124
+ case class UserRow(
1125
+ id: java.util.UUID,
1126
+ name: String,
1127
+ email: Option[String],
1128
+ age: Int,
1129
+ active: Boolean
1130
+ )
1131
+ object UserRow {
1132
+ implicit val schema: Schema[UserRow] = Schema.derived
1133
+ }
1134
+
1135
+ // Compatible: flat record with a string-keyed map column (stored as JSON/JSONB)
1136
+ case class ProductRow(
1137
+ id: java.util.UUID,
1138
+ name: String,
1139
+ price: BigDecimal,
1140
+ attributes: scala.collection.immutable.Map[String, String]
1141
+ )
1142
+ object ProductRow {
1143
+ implicit val schema: Schema[ProductRow] = Schema.derived
1144
+ }
1145
+
1146
+ // Compatible: event table — a variant (sealed trait) of flat record cases
1147
+ sealed trait DomainEvent
1148
+ case class UserCreated(id: java.util.UUID, name: String, email: String) extends DomainEvent
1149
+ case class UserDeleted(id: java.util.UUID) extends DomainEvent
1150
+ case class OrderPlaced(id: java.util.UUID, userId: java.util.UUID, total: BigDecimal) extends DomainEvent
1151
+ object DomainEvent {
1152
+ implicit val schema: Schema[DomainEvent] = Schema.derived
1153
+ }
1154
+
1155
+ // Incompatible: contains a nested record (Address is not a primitive)
1156
+ case class Address(street: String, city: String, zip: String)
1157
+ object Address { implicit val schema: Schema[Address] = Schema.derived }
1158
+
1159
+ case class UserWithAddress(
1160
+ id: java.util.UUID,
1161
+ name: String,
1162
+ address: Address // ← incompatible: nested record
1163
+ )
1164
+ object UserWithAddress {
1165
+ implicit val schema: Schema[UserWithAddress] = Schema.derived
1166
+ }
1167
+
1168
+ // ---------------------------------------------------------------------------
1169
+ // Simulated RDBMS library API
1170
+ //
1171
+ // The Allows constraint is checked at the CALL SITE — the library author
1172
+ // writes these signatures once. Users get a compile-time error if their type
1173
+ // doesn't match, with a message pointing to the exact violating field.
1174
+ // ---------------------------------------------------------------------------
1175
+
1176
+ object Rdbms {
1177
+
1178
+ // A flat record row: primitives, optional primitives, or string-keyed maps
1179
+ type FlatRow = Primitive | AOptional[Primitive] | AMap[Primitive, Primitive]
1180
+
1181
+ /** Generate a CREATE TABLE DDL statement for a flat record type. */
1182
+ def createTable[A](implicit
1183
+ schema: Schema[A],
1184
+ ev: Allows[A, Record[FlatRow]]
1185
+ ): String = {
1186
+ val fields = schema.reflect.asRecord.get.fields
1187
+ val cols = fields.map { f =>
1188
+ val tpe = sqlType(f.value)
1189
+ s" ${f.name} $tpe"
1190
+ }
1191
+ s"CREATE TABLE ${tableName(schema)} (\n${cols.mkString(",\n")}\n)"
1192
+ }
1193
+
1194
+ /** Generate an INSERT statement for a single flat record row. */
1195
+ def insert[A](value: A)(implicit
1196
+ schema: Schema[A],
1197
+ ev: Allows[A, Record[FlatRow]]
1198
+ ): String = {
1199
+ val dv = schema.toDynamicValue(value)
1200
+ dv match {
1201
+ case DynamicValue.Record(fields) =>
1202
+ val cols = fields.map(_._1).mkString(", ")
1203
+ val vals = fields.map { case (_, v) => sqlLiteralDv(v) }.mkString(", ")
1204
+ s"INSERT INTO ${tableName(schema)} ($cols) VALUES ($vals)"
1205
+ case _ => s"INSERT INTO ${tableName(schema)} VALUES (?)"
1206
+ }
1207
+ }
1208
+
1209
+ /**
1210
+ * Insert an event into an event-sourcing table.
1211
+ *
1212
+ * The type must be a sealed trait / enum whose cases are flat records. No
1213
+ * explicit Variant node is needed — sealed traits are auto-unwrapped by the
1214
+ * macro.
1215
+ */
1216
+ def insertEvent[A](event: A)(implicit
1217
+ schema: Schema[A],
1218
+ ev: Allows[A, Record[FlatRow]]
1219
+ ): String = {
1220
+ val typeName = schema.toDynamicValue(event) match {
1221
+ case DynamicValue.Variant(caseName, _) => caseName
1222
+ case _ => schema.reflect.typeId.name
1223
+ }
1224
+ val payload = schema.toDynamicValue(event).toJson.toString
1225
+ s"INSERT INTO events (type, payload) VALUES ('$typeName', '$payload')"
1226
+ }
1227
+
1228
+ // ---------------------------------------------------------------------------
1229
+ // Helpers
1230
+ // ---------------------------------------------------------------------------
1231
+
1232
+ private def tableName[A](schema: Schema[A]): String =
1233
+ schema.reflect.modifiers.collectFirst {
1234
+ case Modifier.config(k, v) if k == "sql.table_name" => v
1235
+ }.getOrElse(schema.reflect.typeId.name.toLowerCase + "s")
1236
+
1237
+ private def sqlLiteralDv(dv: DynamicValue): String = dv match {
1238
+ case DynamicValue.Primitive(PrimitiveValue.String(s)) => s"'${s.replace("'", "''")}'"
1239
+ case DynamicValue.Primitive(PrimitiveValue.Boolean(b)) => if (b) "TRUE" else "FALSE"
1240
+ case DynamicValue.Primitive(v) => v.toString
1241
+ case DynamicValue.Null => "NULL"
1242
+ case other => s"'${other.toString.replace("'", "''")}'"
1243
+ }
1244
+
1245
+ private def sqlType(reflect: Reflect.Bound[_]): String = reflect match {
1246
+ case p: Reflect.Primitive[_, _] =>
1247
+ p.primitiveType match {
1248
+ case PrimitiveType.Int(_) => "INTEGER"
1249
+ case PrimitiveType.Long(_) => "BIGINT"
1250
+ case PrimitiveType.String(_) => "TEXT"
1251
+ case PrimitiveType.Boolean(_) => "BOOLEAN"
1252
+ case PrimitiveType.Double(_) => "DOUBLE PRECISION"
1253
+ case PrimitiveType.Float(_) => "REAL"
1254
+ case PrimitiveType.BigDecimal(_) => "NUMERIC"
1255
+ case PrimitiveType.UUID(_) => "UUID"
1256
+ case PrimitiveType.Instant(_) => "TIMESTAMPTZ"
1257
+ case PrimitiveType.LocalDate(_) => "DATE"
1258
+ case PrimitiveType.LocalDateTime(_) => "TIMESTAMP"
1259
+ case _ => "TEXT"
1260
+ }
1261
+ case _: Reflect.Map[_, _, _, _] => "JSONB"
1262
+ case _: Reflect.Sequence[_, _, _] => "TEXT[]"
1263
+ case _ => "TEXT"
1264
+ }
1265
+ }
1266
+
1267
+ // ---------------------------------------------------------------------------
1268
+ // Demonstration — these all compile
1269
+ // ---------------------------------------------------------------------------
1270
+
1271
+ object RdbmsDemo {
1272
+
1273
+ // Flat rows compile fine
1274
+ val createUser: String = Rdbms.createTable[UserRow]
1275
+ val createProduct: String = Rdbms.createTable[ProductRow]
1276
+ val insertUser: String = Rdbms.insert(UserRow(new java.util.UUID(0, 0), "Alice", Some("a@b.com"), 30, true))
1277
+ val insertEvent: String = Rdbms.insertEvent[DomainEvent](UserCreated(new java.util.UUID(0, 0), "Alice", "a@b.com"))
1278
+
1279
+ // The following would NOT compile — uncomment to see the error:
1280
+ //
1281
+ // val bad = Rdbms.createTable[UserWithAddress]
1282
+ // [error] Schema shape violation at UserWithAddress.address: found Record(Address), required
1283
+ // Primitive | Optional[Primitive] | Map[Primitive, Primitive]
1284
+ }
1285
+ ```
1286
+
1287
+ **JSON document store with specific primitives and recursive Self grammar** (compile-only)
1288
+ ([source](https://github.com/zio/zio-blocks/blob/main/schema-examples/src/main/scala/comptime/DocumentStoreExample.scala))
1289
+
1290
+ Demonstrates how Allows enforces recursive schema constraints at compile time:
1291
+
1292
+ ```scala title="schema-examples/src/main/scala/comptime/DocumentStoreExample.scala"
1293
+ /*
1294
+ * Copyright 2024-2026 John A. De Goes and the ZIO Contributors
1295
+ *
1296
+ * Licensed under the Apache License, Version 2.0 (the "License");
1297
+ * you may not use this file except in compliance with the License.
1298
+ * You may obtain a copy of the License at
1299
+ *
1300
+ * http://www.apache.org/licenses/LICENSE-2.0
1301
+ *
1302
+ * Unless required by applicable law or agreed to in writing, software
1303
+ * distributed under the License is distributed on an "AS IS" BASIS,
1304
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1305
+ * See the License for the specific language governing permissions and
1306
+ * limitations under the License.
1307
+ */
1308
+
1309
+ package comptime
1310
+
1311
+ import zio.blocks.schema._
1312
+ import zio.blocks.schema.comptime.Allows
1313
+ import Allows.{Record, Sequence, `|`}
1314
+ import Allows.{Optional => AOptional, Self => ASelf}
1315
+
1316
+ // ---------------------------------------------------------------------------
1317
+ // Realistic JSON document-store example using Allows[A, S]
1318
+ //
1319
+ // JSON has a limited primitive value set:
1320
+ // - null → Unit / Option
1321
+ // - boolean → Boolean
1322
+ // - number → Int, Long, Float, Double, BigDecimal, BigInt
1323
+ // - string → String
1324
+ //
1325
+ // Notably absent from JSON: Char, Byte, Short, UUID, Currency,
1326
+ // and ALL java.time.* types. A JSON library should use SPECIFIC primitive
1327
+ // nodes to reject non-JSON scalars at the call site.
1328
+ //
1329
+ // The grammar is simply:
1330
+ // type Json = Record[JsonPrimitive | Self] | Sequence[JsonPrimitive | Self]
1331
+ //
1332
+ // Self handles all nesting: a field may be a primitive or another Json value.
1333
+ // Sequences of primitives (List[String]) and sequences of records
1334
+ // (List[Author]) both satisfy Sequence[JsonPrimitive | Self].
1335
+ // Optional fields (Option[X]) satisfy Record[...] because Option is recognised
1336
+ // by the Optional grammar node which falls through to the field constraint.
1337
+ // ---------------------------------------------------------------------------
1338
+
1339
+ // ---------------------------------------------------------------------------
1340
+ // Domain types
1341
+ // ---------------------------------------------------------------------------
1342
+
1343
+ // Compatible: flat document
1344
+ case class Author(name: String, email: String)
1345
+ object Author { implicit val schema: Schema[Author] = Schema.derived }
1346
+
1347
+ // Compatible: document with nested documents and sequences
1348
+ case class BookChapter(title: String, wordCount: Int)
1349
+ object BookChapter { implicit val schema: Schema[BookChapter] = Schema.derived }
1350
+
1351
+ case class Book(
1352
+ title: String,
1353
+ author: Author, // nested record — satisfied via Self
1354
+ chapters: List[BookChapter], // sequence of records — satisfied via Sequence[Self]
1355
+ tags: List[String], // sequence of primitives — satisfied via Sequence[JsonPrimitive]
1356
+ rating: Option[Double] // optional primitive — satisfied via Optional[JsonPrimitive | Self]
1357
+ )
1358
+ object Book { implicit val schema: Schema[Book] = Schema.derived }
1359
+
1360
+ // Compatible: recursive document
1361
+ case class Category(name: String, subcategories: List[Category])
1362
+ object Category { implicit val schema: Schema[Category] = Schema.derived }
1363
+
1364
+ // Compatible: variant of search results (for indexing)
1365
+ sealed trait SearchResult
1366
+ case class BookResult(title: String, score: Double) extends SearchResult
1367
+ case class AuthorResult(name: String, bookCount: Int) extends SearchResult
1368
+ object SearchResult { implicit val schema: Schema[SearchResult] = Schema.derived }
1369
+
1370
+ // INCOMPATIBLE: UUID is not a JSON-native scalar
1371
+ case class WithUUID(id: java.util.UUID, name: String)
1372
+ object WithUUID { implicit val schema: Schema[WithUUID] = Schema.derived }
1373
+
1374
+ // INCOMPATIBLE: Instant is not a JSON-native scalar
1375
+ case class WithTimestamp(name: String, createdAt: java.time.Instant)
1376
+ object WithTimestamp { implicit val schema: Schema[WithTimestamp] = Schema.derived }
1377
+
1378
+ // ---------------------------------------------------------------------------
1379
+ // Document store library API
1380
+ // ---------------------------------------------------------------------------
1381
+
1382
+ object DocumentStore {
1383
+
1384
+ /**
1385
+ * JSON-representable scalar types.
1386
+ *
1387
+ * Excludes Char, Byte, Short, UUID, Currency, and all java.time.* types —
1388
+ * none of these have a native JSON encoding. Authors who need java.time
1389
+ * values in JSON should store them as Primitive.String (ISO-8601 etc.).
1390
+ */
1391
+ type JsonPrimitive =
1392
+ Allows.Primitive.Boolean | Allows.Primitive.Int | Allows.Primitive.Long | Allows.Primitive.Float |
1393
+ Allows.Primitive.Double | Allows.Primitive.String | Allows.Primitive.BigDecimal | Allows.Primitive.BigInt |
1394
+ Allows.Primitive.Unit
1395
+
1396
+ /**
1397
+ * A JSON value is either a JSON object (Record) or a JSON array (Sequence).
1398
+ * Self recurses back to this same grammar, so nesting works at any depth.
1399
+ * Optional covers nullable fields (JSON null / absent key).
1400
+ */
1401
+ type Json = Record[JsonPrimitive | AOptional[JsonPrimitive | ASelf] | ASelf] | Sequence[JsonPrimitive | ASelf]
1402
+
1403
+ /**
1404
+ * Encode a value to its JSON string representation.
1405
+ *
1406
+ * Accepts both JSON objects (records) and JSON arrays (sequences) at the top
1407
+ * level. Fields may be primitives, nested documents, sequences, or optionals
1408
+ * — all handled via Self and the JsonPrimitive constraint. Types containing
1409
+ * UUID, Instant, Char etc. are rejected at compile time.
1410
+ */
1411
+ def toJson[A: Schema](doc: A)(implicit ev: Allows[A, Json]): String =
1412
+ Schema[A].toDynamicValue(doc).toJson.toString
1413
+
1414
+ /** Serialize to DynamicValue for further processing. */
1415
+ def serialize[A: Schema](doc: A)(implicit ev: Allows[A, Json]): DynamicValue =
1416
+ Schema[A].toDynamicValue(doc)
1417
+
1418
+ /**
1419
+ * Index a search result. The type must be a sealed trait of flat JSON
1420
+ * records. No explicit Variant node needed — sealed traits are
1421
+ * auto-unwrapped.
1422
+ */
1423
+ def index[A: Schema](result: A)(implicit
1424
+ ev: Allows[A, Record[JsonPrimitive | AOptional[JsonPrimitive | ASelf] | Sequence[JsonPrimitive | ASelf]]]
1425
+ ): String = {
1426
+ val typeName = Schema[A].toDynamicValue(result) match {
1427
+ case DynamicValue.Variant(name, _) => name
1428
+ case _ => Schema[A].reflect.typeId.name
1429
+ }
1430
+ val payload = Schema[A].toDynamicValue(result).toJson.toString
1431
+ s"""{"_type":"$typeName","_doc":$payload}"""
1432
+ }
1433
+ }
1434
+
1435
+ // ---------------------------------------------------------------------------
1436
+ // Demonstration
1437
+ // ---------------------------------------------------------------------------
1438
+
1439
+ object DocumentStoreDemo {
1440
+
1441
+ // JSON objects compile fine
1442
+ val bookJson: String = DocumentStore.toJson(
1443
+ Book(
1444
+ "ZIO Blocks",
1445
+ Author("John", "john@example.com"),
1446
+ List(BookChapter("Intro", 1000)),
1447
+ List("scala", "zio"),
1448
+ Some(4.9)
1449
+ )
1450
+ )
1451
+
1452
+ val categoryJson: String = DocumentStore.toJson(
1453
+ Category("Programming", List(Category("Scala", Nil)))
1454
+ )
1455
+
1456
+ // JSON arrays also satisfy Json (top-level Sequence)
1457
+ val tagListJson: String = DocumentStore.toJson(List("scala", "zio", "functional"))
1458
+ val authorListJson: String = DocumentStore.toJson(List(Author("Alice", "a@b.com"), Author("Bob", "b@b.com")))
1459
+
1460
+ val indexed: String = DocumentStore.index[SearchResult](BookResult("ZIO Blocks", 0.99))
1461
+
1462
+ // The following would NOT compile — uncomment to see the errors:
1463
+ //
1464
+ // DocumentStore.toJson(WithUUID(new java.util.UUID(0, 0), "Alice"))
1465
+ // [error] Schema shape violation at WithUUID.id: found Primitive(java.util.UUID),
1466
+ // required JsonPrimitive (Boolean | Int | Long | Float | Double | String | ...)
1467
+ // UUID is not a JSON-native type — encode it as Primitive.String.
1468
+ //
1469
+ // DocumentStore.toJson(WithTimestamp("Alice", java.time.Instant.EPOCH))
1470
+ // [error] Schema shape violation at WithTimestamp.createdAt: found Primitive(java.time.Instant)
1471
+ // Instant is not a JSON-native type — encode it as Primitive.String (ISO-8601).
1472
+ }
1473
+ ```