@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.
- 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 +42 -39
- package/package.json +1 -1
- package/reference/codec.md +20 -18
- package/reference/docs.md +1 -1
- package/reference/dynamic-optic.md +4 -0
- package/reference/json-schema.md +1 -1
- package/reference/json.md +2 -2
- package/reference/media-type.md +460 -0
- package/reference/optics.md +4 -0
- package/reference/schema-expr.md +669 -0
- package/reference/schema.md +1 -0
- package/reference/type-class-derivation.md +50 -12
- package/scope.md +744 -490
- package/sidebars.js +12 -0
- package/undocumented-report.md +331 -0
|
@@ -0,0 +1,494 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: query-dsl-reified-optics
|
|
3
|
+
title: "Query DSL with Reified Optics — Part 1: Expressions"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
In this guide, we will build a type-safe query DSL for filtering, comparing, and computing over domain data using ZIO Blocks' reified optics and schema expressions. By the end, you will have a composable query language that works on any schema-equipped data type, supporting equality checks, comparisons, boolean logic, arithmetic, and string operations.
|
|
7
|
+
|
|
8
|
+
We'll take an incremental approach: starting with simple field-level equality checks, then adding comparison operators, boolean combinators, arithmetic expressions, and string operations until we have a complete, expressive query DSL.
|
|
9
|
+
|
|
10
|
+
**What we'll cover:**
|
|
11
|
+
|
|
12
|
+
- Defining domain types with schemas and optics
|
|
13
|
+
- Building equality and comparison queries using `===`, `>`, `<`, `>=`, `<=`
|
|
14
|
+
- Combining queries with `&&` and `||`
|
|
15
|
+
- Using arithmetic operators (`+`, `-`, `*`) in expressions
|
|
16
|
+
- Working with string operations (`matches`, `concat`, `length`)
|
|
17
|
+
- Querying through nested structures and collections
|
|
18
|
+
- Evaluating queries against data
|
|
19
|
+
|
|
20
|
+
## The Problem
|
|
21
|
+
|
|
22
|
+
When you need to query or filter collections of structured data in Scala, you typically write ad-hoc predicate functions:
|
|
23
|
+
|
|
24
|
+
```scala
|
|
25
|
+
case class Product(name: String, price: Double, category: String, inStock: Boolean)
|
|
26
|
+
|
|
27
|
+
val products: List[Product] = loadProducts()
|
|
28
|
+
|
|
29
|
+
// Filtering with ad-hoc predicates
|
|
30
|
+
val results = products.filter(p =>
|
|
31
|
+
p.category == "Electronics" && p.price < 500.0 && p.inStock
|
|
32
|
+
)
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
This works, but the predicate `p => p.category == "Electronics" && p.price < 500.0 && p.inStock` is an opaque function. You cannot inspect it, serialize it, translate it to SQL, send it to a remote service, or optimize it. It is a black box.
|
|
36
|
+
|
|
37
|
+
If you need to build a query builder for a database, a filter language for an API, or a rule engine, you need queries as **data** -- inspectable, composable, serializable expression trees. Building these by hand means defining an AST, writing an evaluator, and maintaining type safety across all operators -- a significant amount of boilerplate for every domain type.
|
|
38
|
+
|
|
39
|
+
In this guide, we'll solve this by using ZIO Blocks' `SchemaExpr` and reified optics, which give us composable, type-safe query expressions for free, derived directly from your data model's schema.
|
|
40
|
+
|
|
41
|
+
## Prerequisites
|
|
42
|
+
|
|
43
|
+
Add the ZIO Blocks Schema dependency:
|
|
44
|
+
|
|
45
|
+
```scala
|
|
46
|
+
libraryDependencies += "dev.zio" %% "zio-blocks-schema" % "0.0.25"
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
```scala
|
|
50
|
+
import zio.blocks.schema._
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
This guide assumes familiarity with ZIO Blocks schemas and basic optics. See the [Schema](../reference/schema.md) and [Optics](../reference/optics.md) reference pages for background.
|
|
54
|
+
|
|
55
|
+
## Defining Your Domain
|
|
56
|
+
|
|
57
|
+
We'll build a product catalog query DSL. First, define the domain types with schemas and optics:
|
|
58
|
+
|
|
59
|
+
```scala
|
|
60
|
+
case class Product(
|
|
61
|
+
name: String,
|
|
62
|
+
price: Double,
|
|
63
|
+
category: String,
|
|
64
|
+
inStock: Boolean,
|
|
65
|
+
rating: Int
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
object Product extends CompanionOptics[Product] {
|
|
69
|
+
implicit val schema: Schema[Product] = Schema.derived
|
|
70
|
+
|
|
71
|
+
val name: Lens[Product, String] = optic(_.name)
|
|
72
|
+
val price: Lens[Product, Double] = optic(_.price)
|
|
73
|
+
val category: Lens[Product, String] = optic(_.category)
|
|
74
|
+
val inStock: Lens[Product, Boolean] = optic(_.inStock)
|
|
75
|
+
val rating: Lens[Product, Int] = optic(_.rating)
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Three things are required for each queryable type:
|
|
80
|
+
|
|
81
|
+
1. **`Schema.derived`** -- captures the type's structure at runtime
|
|
82
|
+
2. **`CompanionOptics[T]`** -- provides the `optic` macro for deriving lenses
|
|
83
|
+
3. **Named lenses** for each field you want to query on
|
|
84
|
+
|
|
85
|
+
Each lens is a *reified* field accessor: unlike `_.price`, the lens `Product.price` is a first-class value that carries the field name, the source schema, and the focus schema. This metadata is what makes the query DSL possible.
|
|
86
|
+
|
|
87
|
+
## Equality and Comparison Queries
|
|
88
|
+
|
|
89
|
+
Every optic in ZIO Blocks has built-in operators that create `SchemaExpr` values -- expression trees representing queries:
|
|
90
|
+
|
|
91
|
+
```scala
|
|
92
|
+
// Equality check
|
|
93
|
+
val isElectronics: SchemaExpr[Product, Boolean] =
|
|
94
|
+
Product.category === "Electronics"
|
|
95
|
+
|
|
96
|
+
// Greater-than comparison
|
|
97
|
+
val expensiveItems: SchemaExpr[Product, Boolean] =
|
|
98
|
+
Product.price > 100.0
|
|
99
|
+
|
|
100
|
+
// Less-than-or-equal
|
|
101
|
+
val budgetFriendly: SchemaExpr[Product, Boolean] =
|
|
102
|
+
Product.price <= 50.0
|
|
103
|
+
|
|
104
|
+
// Comparison against a literal
|
|
105
|
+
val highRated: SchemaExpr[Product, Boolean] =
|
|
106
|
+
Product.rating >= 4
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
The full set of comparison operators:
|
|
110
|
+
|
|
111
|
+
| Operator | Meaning |
|
|
112
|
+
|----------|------------------------|
|
|
113
|
+
| `===` | Equal to |
|
|
114
|
+
| `!=` | Not equal to |
|
|
115
|
+
| `>` | Greater than |
|
|
116
|
+
| `>=` | Greater than or equal |
|
|
117
|
+
| `<` | Less than |
|
|
118
|
+
| `<=` | Less than or equal |
|
|
119
|
+
|
|
120
|
+
Each operator works in two forms:
|
|
121
|
+
- **Optic vs. literal**: `Product.price > 100.0` -- compare a field to a value
|
|
122
|
+
- **Optic vs. optic**: `Product.rating === Product.rating` -- compare two fields
|
|
123
|
+
|
|
124
|
+
## Evaluating Queries
|
|
125
|
+
|
|
126
|
+
A `SchemaExpr[A, Boolean]` is a predicate over `A`. Evaluate it with `.eval`:
|
|
127
|
+
|
|
128
|
+
```scala
|
|
129
|
+
val laptop = Product("Laptop", 999.99, "Electronics", true, 5)
|
|
130
|
+
val pen = Product("Pen", 2.50, "Office", true, 3)
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
```scala
|
|
134
|
+
isElectronics.eval(laptop)
|
|
135
|
+
// res0: Either[OpticCheck, Seq[Boolean]] = Right(IndexedSeq(true))
|
|
136
|
+
isElectronics.eval(pen)
|
|
137
|
+
// res1: Either[OpticCheck, Seq[Boolean]] = Right(IndexedSeq(false))
|
|
138
|
+
|
|
139
|
+
expensiveItems.eval(laptop)
|
|
140
|
+
// res2: Either[OpticCheck, Seq[Boolean]] = Right(IndexedSeq(true))
|
|
141
|
+
expensiveItems.eval(pen)
|
|
142
|
+
// res3: Either[OpticCheck, Seq[Boolean]] = Right(IndexedSeq(false))
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
The result type is `Either[OpticCheck, Seq[Boolean]]`:
|
|
146
|
+
- `Right(Seq(true))` or `Right(Seq(false))` for successful evaluation
|
|
147
|
+
- `Left(opticCheck)` if the optic path fails (e.g., a prism encounters the wrong variant case)
|
|
148
|
+
|
|
149
|
+
For lenses, evaluation always succeeds because lenses always resolve.
|
|
150
|
+
|
|
151
|
+
## Combining Queries with Boolean Logic
|
|
152
|
+
|
|
153
|
+
Combine queries with `&&` (and), `||` (or), and `!` (not):
|
|
154
|
+
|
|
155
|
+
```scala
|
|
156
|
+
// AND: electronics under $500
|
|
157
|
+
val affordableElectronics: SchemaExpr[Product, Boolean] =
|
|
158
|
+
(Product.category === "Electronics") && (Product.price < 500.0)
|
|
159
|
+
|
|
160
|
+
// OR: either cheap or highly rated
|
|
161
|
+
val goodDeal: SchemaExpr[Product, Boolean] =
|
|
162
|
+
(Product.price < 10.0) || (Product.rating >= 5)
|
|
163
|
+
|
|
164
|
+
// NOT: items that are out of stock
|
|
165
|
+
val outOfStock: SchemaExpr[Product, Boolean] =
|
|
166
|
+
!Product.inStock
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
```scala
|
|
170
|
+
affordableElectronics.eval(laptop)
|
|
171
|
+
// res4: Either[OpticCheck, Seq[Boolean]] = Right(IndexedSeq(false))
|
|
172
|
+
goodDeal.eval(laptop)
|
|
173
|
+
// res5: Either[OpticCheck, Seq[Boolean]] = Right(IndexedSeq(true))
|
|
174
|
+
goodDeal.eval(pen)
|
|
175
|
+
// res6: Either[OpticCheck, Seq[Boolean]] = Right(IndexedSeq(true))
|
|
176
|
+
outOfStock.eval(laptop)
|
|
177
|
+
// res7: Either[OpticCheck, Seq[Boolean]] = Right(IndexedSeq(false))
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Boolean combinators also work on `SchemaExpr` values, not just optics. This means you can build complex compound queries:
|
|
181
|
+
|
|
182
|
+
```scala
|
|
183
|
+
val complexQuery: SchemaExpr[Product, Boolean] =
|
|
184
|
+
((Product.category === "Electronics") && (Product.price < 500.0)) ||
|
|
185
|
+
((Product.category === "Office") && (Product.rating >= 4))
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
```scala
|
|
189
|
+
complexQuery.eval(laptop)
|
|
190
|
+
// res8: Either[OpticCheck, Seq[Boolean]] = Right(IndexedSeq(false))
|
|
191
|
+
complexQuery.eval(pen)
|
|
192
|
+
// res9: Either[OpticCheck, Seq[Boolean]] = Right(IndexedSeq(false))
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
## Arithmetic Expressions
|
|
196
|
+
|
|
197
|
+
Optics on numeric fields support `+`, `-`, and `*`:
|
|
198
|
+
|
|
199
|
+
```scala
|
|
200
|
+
val discountedPrice: SchemaExpr[Product, Double] =
|
|
201
|
+
Product.price * 0.9
|
|
202
|
+
|
|
203
|
+
val priceWithTax: SchemaExpr[Product, Double] =
|
|
204
|
+
Product.price * 1.08
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
```scala
|
|
208
|
+
discountedPrice.eval(laptop)
|
|
209
|
+
// res10: Either[OpticCheck, Seq[Double]] = Right(IndexedSeq(899.991))
|
|
210
|
+
priceWithTax.eval(pen)
|
|
211
|
+
// res11: Either[OpticCheck, Seq[Double]] = Right(IndexedSeq(2.7))
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
Arithmetic operators are available for all numeric types: `Byte`, `Short`, `Int`, `Long`, `Float`, `Double`, `BigInt`, and `BigDecimal`.
|
|
215
|
+
|
|
216
|
+
## String Operations
|
|
217
|
+
|
|
218
|
+
Optics on `String` fields provide string-specific operations:
|
|
219
|
+
|
|
220
|
+
```scala
|
|
221
|
+
// Regex matching
|
|
222
|
+
val startsWithL: SchemaExpr[Product, Boolean] =
|
|
223
|
+
Product.name.matches("L.*")
|
|
224
|
+
|
|
225
|
+
// String concatenation
|
|
226
|
+
val labeledName: SchemaExpr[Product, String] =
|
|
227
|
+
Product.name.concat(" [SALE]")
|
|
228
|
+
|
|
229
|
+
// String length
|
|
230
|
+
val nameLength: SchemaExpr[Product, Int] =
|
|
231
|
+
Product.name.length
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
```scala
|
|
235
|
+
startsWithL.eval(laptop)
|
|
236
|
+
// res12: Either[OpticCheck, Seq[Boolean]] = Right(IndexedSeq(true))
|
|
237
|
+
startsWithL.eval(pen)
|
|
238
|
+
// res13: Either[OpticCheck, Seq[Boolean]] = Right(IndexedSeq(false))
|
|
239
|
+
|
|
240
|
+
labeledName.eval(laptop)
|
|
241
|
+
// res14: Either[OpticCheck, Seq[String]] = Right(IndexedSeq("Laptop [SALE]"))
|
|
242
|
+
|
|
243
|
+
nameLength.eval(laptop)
|
|
244
|
+
// res15: Either[OpticCheck, Seq[Int]] = Right(IndexedSeq(6))
|
|
245
|
+
nameLength.eval(pen)
|
|
246
|
+
// res16: Either[OpticCheck, Seq[Int]] = Right(IndexedSeq(3))
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
| String Operation | Signature | Description |
|
|
250
|
+
|------------------|------------------------------------|---------------------------|
|
|
251
|
+
| `matches` | `(regex: String) => SchemaExpr[S, Boolean]` | Regex match |
|
|
252
|
+
| `concat` | `(suffix: String) => SchemaExpr[S, String]` | Append a string |
|
|
253
|
+
| `length` | `SchemaExpr[S, Int]` | String length |
|
|
254
|
+
|
|
255
|
+
## Dynamic Evaluation
|
|
256
|
+
|
|
257
|
+
Every `SchemaExpr` can also evaluate to `DynamicValue`, which is useful when you need format-agnostic results (e.g., serializing query results to JSON):
|
|
258
|
+
|
|
259
|
+
```scala
|
|
260
|
+
val priceExpr: SchemaExpr[Product, Double] = Product.price * 0.9
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
```scala
|
|
264
|
+
priceExpr.evalDynamic(laptop)
|
|
265
|
+
// res17: Either[OpticCheck, Seq[DynamicValue]] = Right(
|
|
266
|
+
// IndexedSeq(Primitive(Double(899.991)))
|
|
267
|
+
// )
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
The `evalDynamic` method converts results to `DynamicValue` representations, enabling integration with serialization formats without knowing the concrete type.
|
|
271
|
+
|
|
272
|
+
## Querying Nested Structures
|
|
273
|
+
|
|
274
|
+
The real power of reified optics emerges with nested data. Define a richer domain:
|
|
275
|
+
|
|
276
|
+
```scala
|
|
277
|
+
import zio.blocks.schema._
|
|
278
|
+
|
|
279
|
+
case class Address(city: String, country: String)
|
|
280
|
+
object Address {
|
|
281
|
+
implicit val schema: Schema[Address] = Schema.derived
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
case class Seller(name: String, address: Address, rating: Double)
|
|
285
|
+
object Seller extends CompanionOptics[Seller] {
|
|
286
|
+
implicit val schema: Schema[Seller] = Schema.derived
|
|
287
|
+
|
|
288
|
+
val name: Lens[Seller, String] = optic(_.name)
|
|
289
|
+
val rating: Lens[Seller, Double] = optic(_.rating)
|
|
290
|
+
// Compose through nested structure directly
|
|
291
|
+
val city: Lens[Seller, String] = optic(_.address.city)
|
|
292
|
+
val country: Lens[Seller, String] = optic(_.address.country)
|
|
293
|
+
}
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
The `optic(_.address.city)` macro composes a lens from `Seller` to `Address` with a lens from `Address` to `String`, producing a single `Lens[Seller, String]`. Now we can query nested fields as if they were top-level:
|
|
297
|
+
|
|
298
|
+
```scala
|
|
299
|
+
val localSeller: SchemaExpr[Seller, Boolean] =
|
|
300
|
+
(Seller.city === "Berlin") && (Seller.rating >= 4.0)
|
|
301
|
+
|
|
302
|
+
val seller = Seller("TechShop", Address("Berlin", "Germany"), 4.5)
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
```scala
|
|
306
|
+
localSeller.eval(seller)
|
|
307
|
+
// res19: Either[OpticCheck, Seq[Boolean]] = Right(IndexedSeq(true))
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
## Querying Through Collections
|
|
311
|
+
|
|
312
|
+
For collection fields, use traversals to query across all elements:
|
|
313
|
+
|
|
314
|
+
```scala
|
|
315
|
+
import zio.blocks.schema._
|
|
316
|
+
|
|
317
|
+
case class LineItem(sku: String, price: Double, quantity: Int)
|
|
318
|
+
object LineItem {
|
|
319
|
+
implicit val schema: Schema[LineItem] = Schema.derived
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
case class Order(id: String, items: List[LineItem])
|
|
323
|
+
object Order extends CompanionOptics[Order] {
|
|
324
|
+
implicit val schema: Schema[Order] = Schema.derived
|
|
325
|
+
|
|
326
|
+
val id: Lens[Order, String] = optic(_.id)
|
|
327
|
+
val allPrices: Traversal[Order, Double] = optic(_.items.each.price)
|
|
328
|
+
val allSkus: Traversal[Order, String] = optic(_.items.each.sku)
|
|
329
|
+
val allQuantities: Traversal[Order, Int] = optic(_.items.each.quantity)
|
|
330
|
+
}
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
Traversals produce `SchemaExpr` values that evaluate to **multiple results**:
|
|
334
|
+
|
|
335
|
+
```scala
|
|
336
|
+
val order = Order("ORD-1", List(
|
|
337
|
+
LineItem("SKU-A", 29.99, 2),
|
|
338
|
+
LineItem("SKU-B", 149.99, 1),
|
|
339
|
+
LineItem("SKU-C", 9.99, 5)
|
|
340
|
+
))
|
|
341
|
+
|
|
342
|
+
// Each element is evaluated independently
|
|
343
|
+
val hasExpensiveItem: SchemaExpr[Order, Boolean] =
|
|
344
|
+
Order.allPrices > 100.0
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
```scala
|
|
348
|
+
hasExpensiveItem.eval(order)
|
|
349
|
+
// res21: Either[OpticCheck, Seq[Boolean]] = Right(List(false, true, false))
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
:::tip
|
|
353
|
+
When a traversal-based expression produces multiple results, each element in the sequence corresponds to one focused value from the traversal. This makes it straightforward to check whether *any* or *all* elements satisfy a condition by examining the result sequence.
|
|
354
|
+
:::
|
|
355
|
+
|
|
356
|
+
## Filtering a Collection
|
|
357
|
+
|
|
358
|
+
With `SchemaExpr` as a reified predicate, you can build a generic filter function that works with any schema-equipped type:
|
|
359
|
+
|
|
360
|
+
```scala
|
|
361
|
+
import zio.blocks.schema._
|
|
362
|
+
|
|
363
|
+
case class Product(
|
|
364
|
+
name: String,
|
|
365
|
+
price: Double,
|
|
366
|
+
category: String,
|
|
367
|
+
inStock: Boolean,
|
|
368
|
+
rating: Int
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
object Product extends CompanionOptics[Product] {
|
|
372
|
+
implicit val schema: Schema[Product] = Schema.derived
|
|
373
|
+
|
|
374
|
+
val name: Lens[Product, String] = optic(_.name)
|
|
375
|
+
val price: Lens[Product, Double] = optic(_.price)
|
|
376
|
+
val category: Lens[Product, String] = optic(_.category)
|
|
377
|
+
val inStock: Lens[Product, Boolean] = optic(_.inStock)
|
|
378
|
+
val rating: Lens[Product, Int] = optic(_.rating)
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
def filter[A](items: List[A], predicate: SchemaExpr[A, Boolean]): List[A] =
|
|
382
|
+
items.filter(item =>
|
|
383
|
+
predicate.eval(item) match {
|
|
384
|
+
case Right(results) => results.forall(_ == true)
|
|
385
|
+
case Left(_) => false
|
|
386
|
+
}
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
val catalog = List(
|
|
390
|
+
Product("Laptop", 999.99, "Electronics", true, 5),
|
|
391
|
+
Product("Mouse", 29.99, "Electronics", true, 4),
|
|
392
|
+
Product("Pen", 2.50, "Office", true, 3),
|
|
393
|
+
Product("Monitor", 349.99, "Electronics", false, 5),
|
|
394
|
+
Product("Notebook", 5.99, "Office", true, 4)
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
val query = (Product.category === "Electronics") && (Product.inStock === true) && (Product.price < 500.0)
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
```scala
|
|
401
|
+
filter(catalog, query).map(_.name)
|
|
402
|
+
// res23: List[String] = List("Mouse")
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
The `filter` function knows nothing about `Product` -- it works with any `SchemaExpr[A, Boolean]`. The query is data, not a lambda, so it could be serialized, logged, or translated to a database query.
|
|
406
|
+
|
|
407
|
+
## Putting It Together
|
|
408
|
+
|
|
409
|
+
Here is a complete, self-contained example combining all the techniques from this guide:
|
|
410
|
+
|
|
411
|
+
```scala
|
|
412
|
+
import zio.blocks.schema._
|
|
413
|
+
|
|
414
|
+
// --- Domain ---
|
|
415
|
+
|
|
416
|
+
case class Address(city: String, country: String)
|
|
417
|
+
object Address {
|
|
418
|
+
implicit val schema: Schema[Address] = Schema.derived
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
case class Product(
|
|
422
|
+
name: String,
|
|
423
|
+
price: Double,
|
|
424
|
+
category: String,
|
|
425
|
+
inStock: Boolean,
|
|
426
|
+
rating: Int,
|
|
427
|
+
warehouse: Address
|
|
428
|
+
)
|
|
429
|
+
|
|
430
|
+
object Product extends CompanionOptics[Product] {
|
|
431
|
+
implicit val schema: Schema[Product] = Schema.derived
|
|
432
|
+
|
|
433
|
+
val name: Lens[Product, String] = optic(_.name)
|
|
434
|
+
val price: Lens[Product, Double] = optic(_.price)
|
|
435
|
+
val category: Lens[Product, String] = optic(_.category)
|
|
436
|
+
val inStock: Lens[Product, Boolean] = optic(_.inStock)
|
|
437
|
+
val rating: Lens[Product, Int] = optic(_.rating)
|
|
438
|
+
val city: Lens[Product, String] = optic(_.warehouse.city)
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// --- Generic query filter ---
|
|
442
|
+
|
|
443
|
+
def filter[A](items: List[A], predicate: SchemaExpr[A, Boolean]): List[A] =
|
|
444
|
+
items.filter(item =>
|
|
445
|
+
predicate.eval(item) match {
|
|
446
|
+
case Right(results) => results.forall(_ == true)
|
|
447
|
+
case Left(_) => false
|
|
448
|
+
}
|
|
449
|
+
)
|
|
450
|
+
|
|
451
|
+
// --- Usage ---
|
|
452
|
+
|
|
453
|
+
val catalog = List(
|
|
454
|
+
Product("Laptop", 999.99, "Electronics", true, 5, Address("Berlin", "Germany")),
|
|
455
|
+
Product("Mouse", 29.99, "Electronics", true, 4, Address("Berlin", "Germany")),
|
|
456
|
+
Product("Pen", 2.50, "Office", true, 3, Address("London", "UK")),
|
|
457
|
+
Product("Monitor", 349.99, "Electronics", false, 5, Address("Berlin", "Germany")),
|
|
458
|
+
Product("Notebook", 5.99, "Office", true, 4, Address("London", "UK"))
|
|
459
|
+
)
|
|
460
|
+
|
|
461
|
+
// Compose a query: in-stock electronics under $500, from Berlin, highly rated
|
|
462
|
+
val query =
|
|
463
|
+
(Product.category === "Electronics") &&
|
|
464
|
+
(Product.inStock === true) &&
|
|
465
|
+
(Product.price < 500.0) &&
|
|
466
|
+
(Product.city === "Berlin") &&
|
|
467
|
+
(Product.rating >= 4)
|
|
468
|
+
|
|
469
|
+
val results = filter(catalog, query)
|
|
470
|
+
// results: List(Product("Mouse", 29.99, "Electronics", true, 4, Address("Berlin", "Germany")))
|
|
471
|
+
|
|
472
|
+
// String operations
|
|
473
|
+
val searchQuery = Product.name.matches(".*top$")
|
|
474
|
+
val matches = filter(catalog, searchQuery)
|
|
475
|
+
// matches: List(Product("Laptop", ...))
|
|
476
|
+
|
|
477
|
+
// Arithmetic: compute discounted prices
|
|
478
|
+
val discounted = Product.price * 0.8
|
|
479
|
+
catalog.foreach { p =>
|
|
480
|
+
println(s"${Product.name.get(p)}: ${discounted.eval(p)}")
|
|
481
|
+
}
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
## Going Further
|
|
485
|
+
|
|
486
|
+
- **[Part 2: SQL Generation](./query-dsl-sql.md)** -- Translating query expressions to SQL
|
|
487
|
+
- **[Part 3: Extending the Expression Language](./query-dsl-extending.md)** -- Adding custom operators (IN, BETWEEN, aggregates) beyond SchemaExpr
|
|
488
|
+
- **[Part 4: A Fluent SQL Builder](./query-dsl-fluent-builder.md)** -- Type-safe SELECT, UPDATE, INSERT, DELETE with seamless condition mixing
|
|
489
|
+
- **[Optics Reference](../reference/optics.md)** -- Full API coverage of Lens, Prism, Optional, and Traversal
|
|
490
|
+
- **[DynamicOptic Reference](../reference/dynamic-optic.md)** -- Runtime optic paths for programmatic query construction
|
|
491
|
+
- **[Schema Reference](../reference/schema.md)** -- Schema derivation and type-level metadata
|
|
492
|
+
- **[Path Interpolator](../path-interpolator.md)** -- String-based path construction with `p"..."` syntax
|
|
493
|
+
|
|
494
|
+
The `SchemaExpr` expression tree is a sealed trait, making it straightforward to write interpreters that translate queries to SQL, MongoDB filters, Elasticsearch queries, or any other target language. Because each optic carries its `DynamicOptic` path (via `toDynamic`), you can extract field names and paths programmatically for these translations.
|