@zio.dev/zio-blocks 0.0.21 → 0.0.24
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 +758 -0
- package/guides/query-dsl-fluent-builder.md +1287 -0
- package/guides/query-dsl-reified-optics.md +494 -0
- package/guides/query-dsl-sql.md +680 -0
- package/index.md +57 -34
- package/package.json +1 -1
- package/path-interpolator.md +24 -23
- package/reference/codec.md +386 -0
- package/reference/docs.md +1 -1
- package/reference/dynamic-optic.md +396 -0
- package/reference/formats.md +68 -12
- package/reference/json-schema.md +14 -11
- package/reference/json.md +2 -2
- package/reference/lazy.md +361 -0
- package/reference/media-type.md +460 -0
- package/reference/modifier.md +340 -0
- package/reference/optics.md +4 -0
- package/reference/schema-expr.md +669 -0
- package/reference/schema.md +1 -0
- package/reference/syntax.md +11 -11
- package/reference/type-class-derivation.md +1960 -0
- package/scope.md +754 -502
- package/sidebars.js +18 -1
- package/undocumented-report.md +331 -0
|
@@ -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.24"
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
For cross-platform (Scala.js):
|
|
77
|
+
|
|
78
|
+
```scala
|
|
79
|
+
libraryDependencies += "dev.zio" %%% "zio-blocks-schema" % "0.0.24"
|
|
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.
|
package/reference/schema.md
CHANGED
package/reference/syntax.md
CHANGED
|
@@ -394,16 +394,16 @@ The API is the same—just import `zio.blocks.schema._` and the appropriate synt
|
|
|
394
394
|
|
|
395
395
|
## Method Reference
|
|
396
396
|
|
|
397
|
-
| Method
|
|
398
|
-
|
|
399
|
-
| `toJson`
|
|
400
|
-
| `toJsonString`
|
|
401
|
-
| `toJsonBytes`
|
|
402
|
-
| `fromJson[A]`
|
|
403
|
-
| `fromJson[A]`
|
|
404
|
-
| `show`
|
|
405
|
-
| `diff`
|
|
406
|
-
| `applyPatch`
|
|
407
|
-
| `applyPatchStrict` | `A`
|
|
397
|
+
| Method | Receiver | Return Type | Description |
|
|
398
|
+
|--------------------|---------------|--------------------------|--------------------------------|
|
|
399
|
+
| `toJson` | `A` | `Json` | Convert to JSON AST |
|
|
400
|
+
| `toJsonString` | `A` | `String` | Convert to JSON string |
|
|
401
|
+
| `toJsonBytes` | `A` | `Array[Byte]` | Convert to UTF-8 bytes |
|
|
402
|
+
| `fromJson[A]` | `String` | `Either[SchemaError, A]` | Parse JSON string |
|
|
403
|
+
| `fromJson[A]` | `Array[Byte]` | `Either[SchemaError, A]` | Parse JSON bytes |
|
|
404
|
+
| `show` | `A` | `String` | Pretty-print via DynamicValue |
|
|
405
|
+
| `diff` | `A` | `Patch[A]` | Compute patch to another value |
|
|
406
|
+
| `applyPatch` | `A` | `A` | Apply patch (lenient) |
|
|
407
|
+
| `applyPatchStrict` | `A` | `Either[SchemaError, A]` | Apply patch (strict) |
|
|
408
408
|
|
|
409
409
|
All methods require an implicit/given `Schema[A]` in scope.
|