@zio.dev/zio-blocks 0.0.22 → 0.0.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,669 @@
1
+ ---
2
+ id: schema-expr
3
+ title: "SchemaExpr"
4
+ ---
5
+
6
+ `SchemaExpr[A, +B]` is a **schema-aware expression** that computes a result of type `B` from an input value of type `A`. The input type `A` must be fully described by a [`Schema`](./schema.md), and the expression is built from [optics](./optics.md), literal values, and operators. The fundamental operations are `eval` and `evalDynamic`.
7
+
8
+ `SchemaExpr`:
9
+ - represents expressions as a reified AST, enabling introspection and serialization
10
+ - supports relational (`===`, `>`, `<`, `>=`, `<=`, `!=`), logical (`&&`, `||`, `!`), arithmetic (`+`, `-`, `*`), and string (`concat`, `matches`, `length`) operations
11
+ - evaluates to `Either[OpticCheck, Seq[B]]`, handling failures and multi-valued results from traversals
12
+ - is covariant in `B`, the output type
13
+
14
+ ```scala
15
+ sealed trait SchemaExpr[A, +B] {
16
+ def eval(input: A): Either[OpticCheck, Seq[B]]
17
+ def evalDynamic(input: A): Either[OpticCheck, Seq[DynamicValue]]
18
+ }
19
+ ```
20
+
21
+ :::tip
22
+ For practical walkthroughs of building with `SchemaExpr`, see [Query DSL Part 1: Expressions](../guides/query-dsl-reified-optics.md), [Part 2: SQL Generation](../guides/query-dsl-sql.md), and [Part 3: Extending the Expression Language](../guides/query-dsl-extending.md).
23
+ :::
24
+
25
+ ## Motivation
26
+
27
+ When working with schema-described data, we often need to express computations over that data — comparisons, arithmetic, string operations — in a way that can be both **evaluated at runtime** and **inspected as data**. This is essential for:
28
+
29
+ 1. **Persistence DSLs** — Third-party libraries can translate `SchemaExpr` trees into SQL `WHERE` clauses, NoSQL filters, or other query languages, because the expression structure is reified (not opaque functions).
30
+ 2. **Validation** — Express constraints like "age must be greater than 18" or "name must match a pattern" as composable, inspectable expressions.
31
+ 3. **Data Migration** — Define transformation rules that can be analyzed and optimized before execution.
32
+
33
+ ```text
34
+ SchemaExpr[A, B]
35
+ │
36
+ ┌──────────┬───────────────┼───────────────┬──────────────────┐
37
+ │ │ │ │ │
38
+ Leaf Nodes Unary Ops Binary Ops StringRegexMatch StringLength
39
+ │ │ │
40
+ ┌─────┴─────┐ Not ┌────────┼────────┐
41
+ Literal Optic Relational Logical Arithmetic
42
+ StringConcat
43
+ ```
44
+
45
+ The typical way to build expressions is through the operator syntax on [Optic](./optics.md) values:
46
+
47
+ ```scala
48
+ import zio.blocks.schema._
49
+
50
+ case class Person(name: String, age: Int)
51
+
52
+ object Person extends CompanionOptics[Person] {
53
+ implicit val schema: Schema[Person] = Schema.derived
54
+
55
+ val name: Lens[Person, String] = $(_.name)
56
+ val age: Lens[Person, Int] = $(_.age)
57
+ }
58
+
59
+ // Build expressions using optic operators
60
+ val isAdult: SchemaExpr[Person, Boolean] = Person.age >= 18
61
+ val isAlice: SchemaExpr[Person, Boolean] = Person.name === "Alice"
62
+ val combined: SchemaExpr[Person, Boolean] = isAdult && isAlice
63
+
64
+ // Evaluate against a value
65
+ val alice = Person("Alice", 30)
66
+ val result: Either[OpticCheck, Seq[Boolean]] = combined.eval(alice)
67
+ // Right(Seq(true))
68
+ ```
69
+
70
+ ## Installation
71
+
72
+ ```scala
73
+ libraryDependencies += "dev.zio" %% "zio-blocks-schema" % "0.0.25"
74
+ ```
75
+
76
+ For cross-platform (Scala.js):
77
+
78
+ ```scala
79
+ libraryDependencies += "dev.zio" %%% "zio-blocks-schema" % "0.0.25"
80
+ ```
81
+
82
+ Supported Scala versions: 2.13.x and 3.x.
83
+
84
+ ## Creating Instances
85
+
86
+ `SchemaExpr` instances are typically created through **operator syntax on optics** rather than by constructing AST nodes directly. Each operator on `Optic[S, A]` returns a `SchemaExpr[S, B]`.
87
+
88
+ ### Via Relational Operators on Optics
89
+
90
+ The comparison operators `===`, `>`, `>=`, `<`, `<=`, and `!=` on `Optic[S, A]` create `SchemaExpr.Relational` nodes. Each operator has two overloads — one comparing against a literal value, and one comparing against another optic:
91
+
92
+ ```scala
93
+ import zio.blocks.schema._
94
+
95
+ case class Product(name: String, price: Double, stock: Int)
96
+
97
+ object Product extends CompanionOptics[Product] {
98
+ implicit val schema: Schema[Product] = Schema.derived
99
+
100
+ val name: Lens[Product, String] = $(_.name)
101
+ val price: Lens[Product, Double] = $(_.price)
102
+ val stock: Lens[Product, Int] = $(_.stock)
103
+ }
104
+
105
+ // Compare optic against a literal value
106
+ val expensive: SchemaExpr[Product, Boolean] = Product.price > 100.0
107
+ val inStock: SchemaExpr[Product, Boolean] = Product.stock > 0
108
+ val named: SchemaExpr[Product, Boolean] = Product.name === "Widget"
109
+
110
+ // Compare optic against another optic
111
+ // (e.g., stock > price — contrived, but shows the syntax)
112
+ ```
113
+
114
+ ### Via Logical Operators on Optics
115
+
116
+ The `&&`, `||`, and `!` (unary) operators on boolean-focused optics create `SchemaExpr.Logical` and `SchemaExpr.Not` nodes:
117
+
118
+ ```scala
119
+ import zio.blocks.schema._
120
+
121
+ case class User(name: String, active: Boolean, verified: Boolean)
122
+
123
+ object User extends CompanionOptics[User] {
124
+ implicit val schema: Schema[User] = Schema.derived
125
+
126
+ val name: Lens[User, String] = $(_.name)
127
+ val active: Lens[User, Boolean] = $(_.active)
128
+ val verified: Lens[User, Boolean] = $(_.verified)
129
+ }
130
+
131
+ // Logical operators on boolean optics
132
+ val activeAndVerified: SchemaExpr[User, Boolean] = User.active && User.verified
133
+ val eitherOne: SchemaExpr[User, Boolean] = User.active || User.verified
134
+ val notActive: SchemaExpr[User, Boolean] = !User.active
135
+ ```
136
+
137
+ ### Via Arithmetic Operators on Optics
138
+
139
+ The `+`, `-`, and `*` operators on numeric-focused optics create `SchemaExpr.Arithmetic` nodes. These require an implicit `IsNumeric[A]` instance, which is provided for `Byte`, `Short`, `Int`, `Long`, `Float`, `Double`, `BigInt`, and `BigDecimal`:
140
+
141
+ ```scala
142
+ import zio.blocks.schema._
143
+
144
+ case class Order(quantity: Int, unitPrice: Double)
145
+
146
+ object Order extends CompanionOptics[Order] {
147
+ implicit val schema: Schema[Order] = Schema.derived
148
+
149
+ val quantity : Lens[Order, Int] = $(_.quantity)
150
+ val unitPrice: Lens[Order, Double] = $(_.unitPrice)
151
+ }
152
+
153
+ // Arithmetic on optic values
154
+ val doubled : SchemaExpr[Order, Int] = Order.quantity * 2
155
+ val discounted: SchemaExpr[Order, Double] = Order.unitPrice - 5.0
156
+ val increased : SchemaExpr[Order, Int] = Order.quantity + 1
157
+ ```
158
+
159
+ ### Via String Operators on Optics
160
+
161
+ The `concat`, `matches`, and `length` methods on string-focused optics create `SchemaExpr.StringConcat`, `SchemaExpr.StringRegexMatch`, and `SchemaExpr.StringLength` nodes:
162
+
163
+ ```scala
164
+ import zio.blocks.schema._
165
+
166
+ case class Email(address: String, subject: String)
167
+
168
+ object Email extends CompanionOptics[Email] {
169
+ implicit val schema: Schema[Email] = Schema.derived
170
+
171
+ val address: Lens[Email, String] = $(_.address)
172
+ val subject: Lens[Email, String] = $(_.subject)
173
+ }
174
+
175
+ // String operations
176
+ val withDomain: SchemaExpr[Email, String] = Email.address.concat("@example.com")
177
+ val isValid : SchemaExpr[Email, Boolean] = Email.address.matches("^[^@]+@[^@]+$")
178
+ val subjectLen: SchemaExpr[Email, Int] = Email.subject.length
179
+ ```
180
+
181
+ ### Via Logical Operators on SchemaExpr
182
+
183
+ Boolean-typed `SchemaExpr` values can be combined with `&&` and `||`:
184
+
185
+ ```scala
186
+ import zio.blocks.schema._
187
+
188
+ case class Person(name: String, age: Int)
189
+
190
+ object Person extends CompanionOptics[Person] {
191
+ implicit val schema: Schema[Person] = Schema.derived
192
+
193
+ val name: Lens[Person, String] = $(_.name)
194
+ val age: Lens[Person, Int] = $(_.age)
195
+ }
196
+
197
+ // Compose expressions with && and ||
198
+ val isAdult = Person.age >= 18
199
+ val isAlice = Person.name === "Alice"
200
+
201
+ val adultAlice : SchemaExpr[Person, Boolean] = isAdult && isAlice
202
+ val adultOrAlice: SchemaExpr[Person, Boolean] = isAdult || isAlice
203
+ ```
204
+
205
+ ### Via Direct AST Construction
206
+
207
+ For advanced use cases, we can construct `SchemaExpr` nodes directly:
208
+
209
+ ```scala
210
+ import zio.blocks.schema._
211
+
212
+ case class Item(price: Int)
213
+
214
+ object Item extends CompanionOptics[Item] {
215
+ implicit val schema: Schema[Item] = Schema.derived
216
+
217
+ val price: Lens[Item, Int] = $(_.price)
218
+ }
219
+
220
+ // Construct AST nodes directly
221
+ val lit: SchemaExpr[Item, Int] = new SchemaExpr.Literal(42, Schema[Int])
222
+ val opticExpr: SchemaExpr[Item, Int] = new SchemaExpr.Optic(Item.price)
223
+ val comparison: SchemaExpr[Item, Boolean] = new SchemaExpr.Relational(
224
+ opticExpr,
225
+ lit,
226
+ SchemaExpr.RelationalOperator.GreaterThan
227
+ )
228
+ ```
229
+
230
+ ## Core Operations
231
+
232
+ ### Evaluation
233
+
234
+ #### `eval`
235
+
236
+ Evaluates the expression against an input value, returning the typed result. The result is a `Seq[B]` because traversal-based expressions can produce multiple values.
237
+
238
+ ```scala
239
+ trait SchemaExpr[A, +B] {
240
+ def eval(input: A): Either[OpticCheck, Seq[B]]
241
+ }
242
+ ```
243
+
244
+ ```scala
245
+ import zio.blocks.schema._
246
+
247
+ case class Person(name: String, age: Int)
248
+
249
+ object Person extends CompanionOptics[Person] {
250
+ implicit val schema: Schema[Person] = Schema.derived
251
+
252
+ val age: Lens[Person, Int] = $(_.age)
253
+ }
254
+
255
+ val isAdult = Person.age >= 18
256
+
257
+ val alice = Person("Alice", 30)
258
+ val result = isAdult.eval(alice)
259
+ // Right(List(true))
260
+
261
+ val bob = Person("Bob", 12)
262
+ val result2 = isAdult.eval(bob)
263
+ // Right(List(false))
264
+ ```
265
+
266
+ :::note
267
+ When an expression wraps a `Traversal` optic, `eval` returns multiple values — one per element in the traversed collection. For `Lens`-based expressions, the result is always a single-element `Seq`.
268
+ :::
269
+
270
+ #### `evalDynamic`
271
+
272
+ Like `eval`, but converts the result to [`DynamicValue`](./dynamic-value.md) instances. This is useful for serialization or when working with schema-agnostic code.
273
+
274
+ ```scala
275
+ trait SchemaExpr[A, +B] {
276
+ def evalDynamic(input: A): Either[OpticCheck, Seq[DynamicValue]]
277
+ }
278
+ ```
279
+
280
+ In the following example we are evaluating a simple optic expression to extract the `name` field from a `Person` and retrieving it as a `DynamicValue`:
281
+
282
+ ```scala
283
+ import zio.blocks.schema._
284
+
285
+ case class Person(name: String, age: Int)
286
+
287
+ object Person extends CompanionOptics[Person] {
288
+ implicit val schema: Schema[Person] = Schema.derived
289
+
290
+ val name: Lens[Person, String] = $(_.name)
291
+ }
292
+
293
+ val nameExpr = new SchemaExpr.Optic(Person.name)
294
+ val result = nameExpr.evalDynamic(Person("Alice", 30))
295
+ // Right(List(DynamicValue.Primitive(PrimitiveValue.String("Alice"))))
296
+ ```
297
+
298
+ ### Logical Combination
299
+
300
+ #### `&&`
301
+
302
+ Combines two boolean-typed expressions with logical AND. Both operands must produce `Boolean` results.
303
+
304
+ ```scala
305
+ trait SchemaExpr[A, +B] {
306
+ def &&[B2](that: SchemaExpr[A, B2])(implicit ev: B <:< Boolean, ev2: B2 =:= Boolean): SchemaExpr[A, Boolean]
307
+ }
308
+ ```
309
+
310
+ In the following example we are evaluating a combined expression that checks if a `Person` is an adult (age >= 18) and has the name "Alice". The result of evaluating this expression against a `Person` instance will be `true` if both conditions are met, and `false` otherwise:
311
+
312
+ ```scala
313
+ import zio.blocks.schema._
314
+
315
+ case class Person(name: String, age: Int)
316
+
317
+ object Person extends CompanionOptics[Person] {
318
+ implicit val schema: Schema[Person] = Schema.derived
319
+
320
+ val name: Lens[Person, String] = $(_.name)
321
+ val age: Lens[Person, Int] = $(_.age)
322
+ }
323
+
324
+ val isAdultAlice = (Person.age >= 18) && (Person.name === "Alice")
325
+ val result = isAdultAlice.eval(Person("Alice", 30))
326
+ // Right(List(true))
327
+ ```
328
+
329
+ #### `||`
330
+
331
+ Combines two boolean-typed expressions with logical OR.
332
+
333
+ ```scala
334
+ trait SchemaExpr[A, +B] {
335
+ def ||[B2](that: SchemaExpr[A, B2])(implicit ev: B <:< Boolean, ev2: B2 =:= Boolean): SchemaExpr[A, Boolean]
336
+ }
337
+ ```
338
+
339
+ In the following example we are evaluating a combined expression that checks if a `Person` is an adult (age >= 18) or has the name "Alice". The result of evaluating this expression against a `Person` instance will be `true` if either condition is met, and `false` only if both conditions are not met:
340
+
341
+ ```scala
342
+ import zio.blocks.schema._
343
+
344
+ case class Person(name: String, age: Int)
345
+
346
+ object Person extends CompanionOptics[Person] {
347
+ implicit val schema: Schema[Person] = Schema.derived
348
+
349
+ val name: Lens[Person, String] = $(_.name)
350
+ val age: Lens[Person, Int] = $(_.age)
351
+ }
352
+
353
+ val isAdultOrAlice = (Person.age >= 18) || (Person.name === "Alice")
354
+ val result = isAdultOrAlice.eval(Person("Alice", 12))
355
+ // Right(List(true)) — Alice, even though not adult
356
+ ```
357
+
358
+ ## Subtypes
359
+
360
+ `SchemaExpr` is a sealed trait with a rich set of case classes representing different expression nodes. These form an **expression AST** that can be inspected, serialized, or translated to other query languages.
361
+
362
+ ### Leaf Nodes
363
+
364
+ #### `SchemaExpr.Literal`
365
+
366
+ A constant value that ignores the input and always produces the same result.
367
+
368
+ ```scala
369
+ object SchemaExpr {
370
+ case class Literal[S, A](value: A, schema: Schema[A]) extends SchemaExpr[S, A]
371
+ }
372
+ ```
373
+
374
+ The `Literal#eval` always returns `Right(Seq(value))` regardless of the input. The `Literal#schema` parameter enables conversion to `DynamicValue` via `evalDynamic`.
375
+
376
+ #### `SchemaExpr.Optic`
377
+
378
+ Wraps an [`Optic[A, B]`](./optics.md) to extract values from the input. The behavior depends on the optic type:
379
+
380
+ ```scala
381
+ object SchemaExpr {
382
+ case class Optic[A, B](optic: zio.blocks.schema.Optic[A, B]) extends SchemaExpr[A, B]
383
+ }
384
+ ```
385
+
386
+ | Optic Type | `SchemaExpr.Optic#eval` Behavior |
387
+ |-------------|---------------------------------------------------------------------------------------|
388
+ | `Lens` | Always succeeds with a single value |
389
+ | `Prism` | Succeeds if the input matches the expected case; otherwise returns `Left(OpticCheck)` |
390
+ | `Optional` | Succeeds if the value is present; otherwise returns `Left(OpticCheck)` |
391
+ | `Traversal` | Returns all elements; returns `Left(OpticCheck)` if the collection is empty |
392
+
393
+ ### Unary Operations
394
+
395
+ #### `SchemaExpr.Not`
396
+
397
+ Negates a boolean expression.
398
+
399
+ ```scala
400
+ object SchemaExpr {
401
+ case class Not[A](expr: SchemaExpr[A, Boolean]) extends UnaryOp[A, Boolean](expr)
402
+ }
403
+ ```
404
+
405
+ Created via the `!` (unary negation) operator on boolean optics:
406
+
407
+ ```scala
408
+ import zio.blocks.schema._
409
+
410
+ case class User(active: Boolean)
411
+
412
+ object User extends CompanionOptics[User] {
413
+ implicit val schema: Schema[User] = Schema.derived
414
+
415
+ val active: Lens[User, Boolean] = $(_.active)
416
+ }
417
+
418
+ val inactive: SchemaExpr[User, Boolean] = !User.active
419
+
420
+ val result = inactive.eval(User(active = true))
421
+ // Right(List(false))
422
+ ```
423
+
424
+ ### Binary Operations
425
+
426
+ `Relational`, `Logical`, `Arithmetic`, and `StringConcat` extend `BinaryOp[A, B, C]`, which provides `left` and `right` sub-expressions. `StringRegexMatch` and `StringLength` extend `SchemaExpr` directly — see [Other Operations](#other-operations) below.
427
+
428
+ #### `SchemaExpr.Relational`
429
+
430
+ Compares two expressions using a `RelationalOperator`. Returns a boolean result.
431
+
432
+ ```scala
433
+ object SchemaExpr {
434
+ case class Relational[A, B](
435
+ left: SchemaExpr[A, B],
436
+ right: SchemaExpr[A, B],
437
+ operator: RelationalOperator
438
+ ) extends SchemaExpr[A, Boolean]
439
+ }
440
+ ```
441
+
442
+ The available `RelationalOperator` values are:
443
+
444
+ | Operator | Optic Syntax | Description |
445
+ |----------------------|--------------|-----------------------|
446
+ | `Equal` | `===` | Equality check |
447
+ | `NotEqual` | `!=` | Inequality check |
448
+ | `LessThan` | `<` | Less than |
449
+ | `LessThanOrEqual` | `<=` | Less than or equal |
450
+ | `GreaterThan` | `>` | Greater than |
451
+ | `GreaterThanOrEqual` | `>=` | Greater than or equal |
452
+
453
+ :::note
454
+ Equality and inequality (`===`, `!=`) compare values directly. Ordering operators (`<`, `<=`, `>`, `>=`) compare via `DynamicValue` ordering internally.
455
+ :::
456
+
457
+ #### `SchemaExpr.Logical`
458
+
459
+ Combines two boolean expressions with a `LogicalOperator`.
460
+
461
+ ```scala
462
+ object SchemaExpr {
463
+ case class Logical[A](
464
+ left: SchemaExpr[A, Boolean],
465
+ right: SchemaExpr[A, Boolean],
466
+ operator: LogicalOperator
467
+ ) extends SchemaExpr[A, Boolean]
468
+ }
469
+ ```
470
+
471
+ The available `LogicalOperator` values are:
472
+
473
+ | Operator | Syntax | Description |
474
+ |----------|--------|---------------------|
475
+ | `And` | `&&` | Logical conjunction |
476
+ | `Or` | `\|\|` | Logical disjunction |
477
+
478
+ #### `SchemaExpr.Arithmetic`
479
+
480
+ Performs arithmetic on two numeric expressions using an `ArithmeticOperator`. Requires an `IsNumeric[A]` type class instance.
481
+
482
+ ```scala
483
+ object SchemaExpr {
484
+ case class Arithmetic[S, A](
485
+ left: SchemaExpr[S, A],
486
+ right: SchemaExpr[S, A],
487
+ operator: ArithmeticOperator,
488
+ isNumeric: IsNumeric[A]
489
+ ) extends SchemaExpr[S, A]
490
+ }
491
+ ```
492
+
493
+ The available `ArithmeticOperator` values are:
494
+
495
+ | Operator | Optic Syntax | Description |
496
+ |------------|--------------|----------------|
497
+ | `Add` | `+` | Addition |
498
+ | `Subtract` | `-` | Subtraction |
499
+ | `Multiply` | `*` | Multiplication |
500
+
501
+ Supported numeric types via `IsNumeric`: `Byte`, `Short`, `Int`, `Long`, `Float`, `Double`, `BigInt`, `BigDecimal`.
502
+
503
+ #### `SchemaExpr.StringConcat`
504
+
505
+ Concatenates two string expressions.
506
+
507
+ ```scala
508
+ object SchemaExpr {
509
+ case class StringConcat[A](
510
+ left: SchemaExpr[A, String],
511
+ right: SchemaExpr[A, String]
512
+ ) extends SchemaExpr[A, String]
513
+ }
514
+ ```
515
+
516
+ Created via the `concat` method on string optics:
517
+
518
+ ```scala
519
+ import zio.blocks.schema._
520
+
521
+ case class Greeting(prefix: String)
522
+
523
+ object Greeting extends CompanionOptics[Greeting] {
524
+ implicit val schema: Schema[Greeting] = Schema.derived
525
+
526
+ val prefix: Lens[Greeting, String] = $(_.prefix)
527
+ }
528
+
529
+ val withName = Greeting.prefix.concat(", World!")
530
+ val result = withName.eval(Greeting("Hello"))
531
+ // Right(List("Hello, World!"))
532
+ ```
533
+
534
+ ### Other Operations
535
+
536
+ `StringRegexMatch` and `StringLength` extend `SchemaExpr` directly rather than through `UnaryOp` or `BinaryOp`.
537
+
538
+ #### `SchemaExpr.StringRegexMatch`
539
+
540
+ Tests whether a string matches a regular expression pattern. Despite having two operands (`regex` and `string`), it extends `SchemaExpr[A, Boolean]` directly.
541
+
542
+ ```scala
543
+ object SchemaExpr {
544
+ case class StringRegexMatch[A](
545
+ regex: SchemaExpr[A, String],
546
+ string: SchemaExpr[A, String]
547
+ ) extends SchemaExpr[A, Boolean]
548
+ }
549
+ ```
550
+
551
+ Created via the `matches` method on string optics:
552
+
553
+ ```scala
554
+ import zio.blocks.schema._
555
+
556
+ case class Email(address: String)
557
+
558
+ object Email extends CompanionOptics[Email] {
559
+ implicit val schema: Schema[Email] = Schema.derived
560
+
561
+ val address: Lens[Email, String] = $(_.address)
562
+ }
563
+
564
+ val isValid = Email.address.matches("^[^@]+@[^@]+\\.[^@]+$")
565
+ val result = isValid.eval(Email("alice@example.com"))
566
+ // Right(List(true))
567
+ ```
568
+
569
+ #### `SchemaExpr.StringLength`
570
+
571
+ Computes the length of a string expression. This is a unary operation but extends `SchemaExpr[A, Int]` directly rather than `UnaryOp`.
572
+
573
+ ```scala
574
+ object SchemaExpr {
575
+ case class StringLength[A](
576
+ string: SchemaExpr[A, String]
577
+ ) extends SchemaExpr[A, Int]
578
+ }
579
+ ```
580
+
581
+ Created via the `length` method on string optics:
582
+
583
+ ```scala
584
+ import zio.blocks.schema._
585
+
586
+ case class Message(body: String)
587
+
588
+ object Message extends CompanionOptics[Message] {
589
+ implicit val schema: Schema[Message] = Schema.derived
590
+
591
+ val body: Lens[Message, String] = $(_.body)
592
+ }
593
+
594
+ val bodyLength = Message.body.length
595
+ val result = bodyLength.eval(Message("Hello!"))
596
+ // Right(List(6))
597
+ ```
598
+
599
+ ### Abstract Intermediate Traits
600
+
601
+ Two sealed traits categorize some expressions by arity:
602
+
603
+ - **`UnaryOp[A, B]`** — has a single `expr: SchemaExpr[A, B]`. Extended by `Not`.
604
+ - **`BinaryOp[A, B, C]`** — has `left: SchemaExpr[A, B]` and `right: SchemaExpr[A, B]`. Extended by `Relational`, `Logical`, `Arithmetic`, and `StringConcat`.
605
+
606
+ Not all expression nodes use these traits — `StringRegexMatch` and `StringLength` extend `SchemaExpr` directly. These intermediate traits are useful for pattern matching when you need to generically process the expression tree.
607
+
608
+ ## Error Handling
609
+
610
+ Expression evaluation returns `Either[OpticCheck, Seq[B]]`. The `Left` case contains an [`OpticCheck`](./optics.md) with detailed diagnostic information about what went wrong — for example, an unexpected case in a prism, an empty collection in a traversal, or a missing key.
611
+
612
+ ```scala
613
+ import zio.blocks.schema._
614
+
615
+ case class Shape(kind: String)
616
+
617
+ object Shape extends CompanionOptics[Shape] {
618
+ implicit val schema: Schema[Shape] = Schema.derived
619
+
620
+ val kind: Lens[Shape, String] = $(_.kind)
621
+ }
622
+
623
+ val expr = Shape.kind === "circle"
624
+ val result = expr.eval(Shape("circle"))
625
+
626
+ result match {
627
+ case Right(values) => println(s"Result: ${values.head}")
628
+ case Left(check) => println(s"Error: ${check.message}")
629
+ }
630
+ ```
631
+
632
+ ## Advanced Usage: Building Query DSLs
633
+
634
+ Because `SchemaExpr` is a sealed, inspectable AST, third-party libraries can pattern-match on the expression tree to translate it into other languages. For example, a database library could translate `SchemaExpr` into SQL:
635
+
636
+ ```scala
637
+ // Pseudocode — illustrates the concept
638
+ def toSql[A](expr: SchemaExpr[A, Boolean]): String = expr match {
639
+ case SchemaExpr.Relational(left, right, op) =>
640
+ s"${toSqlValue(left)} ${opToSql(op)} ${toSqlValue(right)}"
641
+ case SchemaExpr.Logical(left, right, SchemaExpr.LogicalOperator.And) =>
642
+ s"(${toSql(left)}) AND (${toSql(right)})"
643
+ case SchemaExpr.Logical(left, right, SchemaExpr.LogicalOperator.Or) =>
644
+ s"(${toSql(left)}) OR (${toSql(right)})"
645
+ case SchemaExpr.Not(inner) =>
646
+ s"NOT (${toSql(inner)})"
647
+ // ...
648
+ }
649
+ ```
650
+
651
+ This is the key advantage of reified expressions over plain functions — the same expression can be evaluated locally *and* translated to a remote query language.
652
+
653
+ ## Integration
654
+
655
+ ### Optics
656
+
657
+ `SchemaExpr` is tightly integrated with [Optics](./optics.md). All operator methods (`===`, `>`, `<`, `>=`, `<=`, `!=`, `&&`, `||`, `!`, `+`, `-`, `*`, `concat`, `matches`, `length`) are defined on `Optic[S, A]` and return `SchemaExpr[S, B]` values. This makes the optic the primary entry point for building expressions.
658
+
659
+ ### Schema
660
+
661
+ [Schema](./schema.md) provides the type information needed by `SchemaExpr.Literal` to convert values to `DynamicValue` via `Literal#evalDynamic`. The `IsNumeric` type class (used by `Arithmetic`) also derives from `Schema`.
662
+
663
+ ### DynamicValue
664
+
665
+ [DynamicValue](./dynamic-value.md) is the output type of `SchemaExpr#evalDynamic`. It provides a schema-less representation that can be serialized, compared, and manipulated uniformly.
666
+
667
+ ### OpticCheck
668
+
669
+ [OpticCheck](./optics.md) is the error type returned when expression evaluation fails. It provides rich diagnostic information including the optic path, expected vs. actual cases, and the actual value encountered.
@@ -389,6 +389,7 @@ Here is an example of how to set and retrieve documentation values:
389
389
 
390
390
  ```scala
391
391
  import zio.blocks.schema._
392
+ import zio.blocks.docs.Doc
392
393
 
393
394
  case class Person(name: String, age: Int)
394
395