@zio.dev/zio-blocks 0.0.1
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/index.md +426 -0
- package/package.json +6 -0
- package/path-interpolator.md +645 -0
- package/reference/binding.md +364 -0
- package/reference/chunk.md +576 -0
- package/reference/context.md +157 -0
- package/reference/docs.md +524 -0
- package/reference/dynamic-value.md +823 -0
- package/reference/formats.md +640 -0
- package/reference/json-schema.md +626 -0
- package/reference/json.md +979 -0
- package/reference/modifier.md +276 -0
- package/reference/optics.md +1613 -0
- package/reference/patch.md +631 -0
- package/reference/reflect-transform.md +387 -0
- package/reference/reflect.md +521 -0
- package/reference/registers.md +282 -0
- package/reference/schema-evolution.md +540 -0
- package/reference/schema.md +619 -0
- package/reference/syntax.md +409 -0
- package/reference/type-class-derivation-internals.md +632 -0
- package/reference/typeid.md +900 -0
- package/reference/validation.md +458 -0
- package/scope.md +627 -0
- package/sidebars.js +30 -0
|
@@ -0,0 +1,645 @@
|
|
|
1
|
+
# Path Interpolator
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
The path interpolator `p"..."` is a compile-time string interpolator for constructing `DynamicOptic` instances in ZIO Blocks. It provides a clean, concise syntax for building optic paths that navigate through complex data structures, with all parsing and validation happening at compile time for zero runtime overhead.
|
|
6
|
+
|
|
7
|
+
**Why use the path interpolator?**
|
|
8
|
+
|
|
9
|
+
Instead of manually constructing optics like this:
|
|
10
|
+
|
|
11
|
+
```scala
|
|
12
|
+
DynamicOptic(Vector(
|
|
13
|
+
DynamicOptic.Node.Field("users"),
|
|
14
|
+
DynamicOptic.Node.Elements,
|
|
15
|
+
DynamicOptic.Node.Field("email")
|
|
16
|
+
))
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
You can write:
|
|
20
|
+
|
|
21
|
+
```scala
|
|
22
|
+
p".users[*].email"
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
The interpolator is **type-safe**, **compile-time validated**, and **performance-optimized** with zero runtime parsing overhead.
|
|
26
|
+
|
|
27
|
+
## Getting Started
|
|
28
|
+
|
|
29
|
+
Import the schema package to enable the path interpolator:
|
|
30
|
+
|
|
31
|
+
```scala
|
|
32
|
+
import zio.blocks.schema._
|
|
33
|
+
|
|
34
|
+
// Now you can use p"..." anywhere
|
|
35
|
+
val path = p".users[0].name"
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Key Features
|
|
39
|
+
|
|
40
|
+
- **✅ Zero Runtime Overhead**: All parsing happens at compile time
|
|
41
|
+
- **✅ Cross-Platform**: Works on Scala 2.13.x and Scala 3.x
|
|
42
|
+
- **✅ Compile-Time Safety**: Invalid paths are rejected during compilation
|
|
43
|
+
- **✅ No Runtime Interpolation**: Prevents accidental use of runtime values
|
|
44
|
+
- **✅ Rich Syntax**: Supports all `DynamicOptic` operations
|
|
45
|
+
|
|
46
|
+
## Syntax Reference
|
|
47
|
+
|
|
48
|
+
### Field Access
|
|
49
|
+
|
|
50
|
+
Access fields in records using dot notation. The leading dot is optional.
|
|
51
|
+
|
|
52
|
+
```scala
|
|
53
|
+
// With leading dot
|
|
54
|
+
p".name" // Field("name")
|
|
55
|
+
p".firstName" // Field("firstName")
|
|
56
|
+
|
|
57
|
+
// Without leading dot
|
|
58
|
+
p"name" // Field("name")
|
|
59
|
+
p"firstName" // Field("firstName")
|
|
60
|
+
|
|
61
|
+
// Chained fields
|
|
62
|
+
p".user.address.street"
|
|
63
|
+
// Equivalent to: Field("user") → Field("address") → Field("street")
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
**Special cases:**
|
|
67
|
+
|
|
68
|
+
```scala
|
|
69
|
+
p"._private" // Fields starting with underscore
|
|
70
|
+
p".field123" // Fields with digits
|
|
71
|
+
p".café" // Unicode field names
|
|
72
|
+
p".true" // Keywords as field names (true, false, null)
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Index Access
|
|
76
|
+
|
|
77
|
+
Access sequence elements by index, multiple indices, or ranges.
|
|
78
|
+
|
|
79
|
+
**Single index:**
|
|
80
|
+
|
|
81
|
+
```scala
|
|
82
|
+
p"[0]" // AtIndex(0)
|
|
83
|
+
p"[42]" // AtIndex(42)
|
|
84
|
+
p"[2147483647]" // AtIndex(Int.MaxValue)
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
**Multiple indices:**
|
|
88
|
+
|
|
89
|
+
```scala
|
|
90
|
+
p"[0,1,2]" // AtIndices(Seq(0, 1, 2))
|
|
91
|
+
p"[0, 2, 5]" // AtIndices(Seq(0, 2, 5)) - spaces allowed
|
|
92
|
+
p"[5,2,8,1]" // Order preserved
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
**Ranges:**
|
|
96
|
+
|
|
97
|
+
```scala
|
|
98
|
+
p"[0:5]" // AtIndices(Seq(0, 1, 2, 3, 4))
|
|
99
|
+
p"[5:8]" // AtIndices(Seq(5, 6, 7))
|
|
100
|
+
p"[3:4]" // AtIndices(Seq(3)) - single element
|
|
101
|
+
p"[5:5]" // AtIndices(Seq.empty) - empty range
|
|
102
|
+
p"[10:5]" // AtIndices(Seq.empty) - inverted range
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Element Selectors
|
|
106
|
+
|
|
107
|
+
Select all elements in a sequence using wildcard syntax.
|
|
108
|
+
|
|
109
|
+
```scala
|
|
110
|
+
p"[*]" // Elements - all elements
|
|
111
|
+
p"[:*]" // Elements - alternative syntax
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
**Chained selectors:**
|
|
115
|
+
|
|
116
|
+
```scala
|
|
117
|
+
p"[*][*]" // Nested sequences: all elements of all elements
|
|
118
|
+
p"[*][0]" // First element of each sequence
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### Map Access
|
|
122
|
+
|
|
123
|
+
Access map values by key, where keys can be strings, integers, booleans, or characters.
|
|
124
|
+
|
|
125
|
+
**String keys:**
|
|
126
|
+
|
|
127
|
+
```scala
|
|
128
|
+
p"""{"host"}""" // AtMapKey(String("host"))
|
|
129
|
+
p"""{"foo bar"}""" // Keys with spaces
|
|
130
|
+
p"""{"日本語"}""" // Unicode keys
|
|
131
|
+
p"""{"🎉"}""" // Emoji keys
|
|
132
|
+
p"""{""}""" // Empty string key
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
**Integer keys:**
|
|
136
|
+
|
|
137
|
+
```scala
|
|
138
|
+
p"{42}" // AtMapKey(Int(42))
|
|
139
|
+
p"{0}" // AtMapKey(Int(0))
|
|
140
|
+
p"{-42}" // AtMapKey(Int(-42))
|
|
141
|
+
p"{2147483647}" // AtMapKey(Int.MaxValue)
|
|
142
|
+
p"{-2147483648}" // AtMapKey(Int.MinValue)
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
**Boolean keys:**
|
|
146
|
+
|
|
147
|
+
```scala
|
|
148
|
+
p"{true}" // AtMapKey(Boolean(true))
|
|
149
|
+
p"{false}" // AtMapKey(Boolean(false))
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
**Char keys:**
|
|
153
|
+
|
|
154
|
+
```scala
|
|
155
|
+
p"{'a'}" // AtMapKey(Char('a'))
|
|
156
|
+
p"{' '}" // AtMapKey(Char(' '))
|
|
157
|
+
p"{'9'}" // AtMapKey(Char('9'))
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
**Multiple keys:**
|
|
161
|
+
|
|
162
|
+
```scala
|
|
163
|
+
p"""{"foo", "bar", "baz"}""" // AtMapKeys(Seq(...))
|
|
164
|
+
p"{1, 2, 3}" // Multiple integer keys
|
|
165
|
+
p"{true, false}" // Multiple boolean keys
|
|
166
|
+
|
|
167
|
+
// Mixed types
|
|
168
|
+
p"""{"foo", 42}""" // AtMapKeys(Seq(String("foo"), Int(42)))
|
|
169
|
+
p"""{"s", 'c', 42, true}""" // All supported types
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### Map Selectors
|
|
173
|
+
|
|
174
|
+
Select all keys or all values in a map.
|
|
175
|
+
|
|
176
|
+
```scala
|
|
177
|
+
p"{*}" // MapValues - all values
|
|
178
|
+
p"{:*}" // MapValues - alternative syntax
|
|
179
|
+
p"{*:}" // MapKeys - all keys
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
**Examples:**
|
|
183
|
+
|
|
184
|
+
```scala
|
|
185
|
+
p"{*}{*}" // Nested maps: all values of all values
|
|
186
|
+
p"{*:}{*:}" // All keys of all keys
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### Variant Case Access
|
|
190
|
+
|
|
191
|
+
Navigate into a specific variant case using angle brackets.
|
|
192
|
+
|
|
193
|
+
```scala
|
|
194
|
+
p"<Left>" // Case("Left")
|
|
195
|
+
p"<Right>" // Case("Right")
|
|
196
|
+
p"<Some>" // Case("Some")
|
|
197
|
+
p"<None>" // Case("None")
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
**Special cases:**
|
|
201
|
+
|
|
202
|
+
```scala
|
|
203
|
+
p"<_Empty>" // Cases starting with underscore
|
|
204
|
+
p"<Case1>" // Cases with digits
|
|
205
|
+
p"<café>" // Unicode case names
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
**Chained cases:**
|
|
209
|
+
|
|
210
|
+
```scala
|
|
211
|
+
p"<A><B><C>" // Nested variants
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
## Escape Sequences
|
|
215
|
+
|
|
216
|
+
String and character literals support standard escape sequences:
|
|
217
|
+
|
|
218
|
+
| Escape | Result | Description |
|
|
219
|
+
|--------|--------|-------------|
|
|
220
|
+
| `\n` | newline | Line feed |
|
|
221
|
+
| `\t` | tab | Horizontal tab |
|
|
222
|
+
| `\r` | return | Carriage return |
|
|
223
|
+
| `\'` | `'` | Single quote |
|
|
224
|
+
| `\"` | `"` | Double quote |
|
|
225
|
+
| `\\` | `\` | Backslash |
|
|
226
|
+
|
|
227
|
+
**Examples:**
|
|
228
|
+
|
|
229
|
+
```scala
|
|
230
|
+
p"""{"foo\nbar"}""" // String key with newline
|
|
231
|
+
p"""{"foo\tbar"}""" // String key with tab
|
|
232
|
+
p"""{'\n'}""" // Char key with newline
|
|
233
|
+
p"""{"foo\"bar"}""" // Escaped quote in string
|
|
234
|
+
p"""{"foo\\bar"}""" // Escaped backslash in string
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
## Combined Paths
|
|
238
|
+
|
|
239
|
+
Combine different path elements to navigate complex nested structures.
|
|
240
|
+
|
|
241
|
+
### Field → Sequence
|
|
242
|
+
|
|
243
|
+
```scala
|
|
244
|
+
p".items[0]" // First item
|
|
245
|
+
p".items[*]" // All items
|
|
246
|
+
p".items[0,1,2]" // Items at indices 0, 1, 2
|
|
247
|
+
p".items[0:5]" // Items 0 through 4
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
### Field → Map
|
|
251
|
+
|
|
252
|
+
```scala
|
|
253
|
+
p""".config{"host"}""" // Map lookup
|
|
254
|
+
p".settings{42}" // Integer key
|
|
255
|
+
p".lookup{*}" // All map values
|
|
256
|
+
p".lookup{*:}" // All map keys
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
### Field → Variant
|
|
260
|
+
|
|
261
|
+
```scala
|
|
262
|
+
p".result<Success>" // Variant case
|
|
263
|
+
p".response<Ok>" // HTTP response variant
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
### Nested Structures
|
|
267
|
+
|
|
268
|
+
```scala
|
|
269
|
+
// Record in sequence
|
|
270
|
+
p".users[0].name"
|
|
271
|
+
// Equivalent to: Field("users") → AtIndex(0) → Field("name")
|
|
272
|
+
|
|
273
|
+
// All elements then field
|
|
274
|
+
p".users[*].email"
|
|
275
|
+
// Equivalent to: Field("users") → Elements → Field("email")
|
|
276
|
+
|
|
277
|
+
// Map values then field
|
|
278
|
+
p".lookup{*}.value"
|
|
279
|
+
// Equivalent to: Field("lookup") → MapValues → Field("value")
|
|
280
|
+
|
|
281
|
+
// Variant then field
|
|
282
|
+
p".response<Ok>.body"
|
|
283
|
+
// Equivalent to: Field("response") → Case("Ok") → Field("body")
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
### Deeply Nested Paths
|
|
287
|
+
|
|
288
|
+
```scala
|
|
289
|
+
// Complex nested navigation
|
|
290
|
+
p""".root.children[*].metadata{"tags"}[0]"""
|
|
291
|
+
// Field("root") → Field("children") → Elements →
|
|
292
|
+
// Field("metadata") → AtMapKey("tags") → AtIndex(0)
|
|
293
|
+
|
|
294
|
+
// All node types in one path
|
|
295
|
+
p""".a[0]{"k"}<V>.b[*]{*}.c{*:}"""
|
|
296
|
+
// Field("a") → AtIndex(0) → AtMapKey("k") → Case("V") →
|
|
297
|
+
// Field("b") → Elements → MapValues → Field("c") → MapKeys
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
## Root and Empty Paths
|
|
301
|
+
|
|
302
|
+
```scala
|
|
303
|
+
p"" // Empty path = root
|
|
304
|
+
// Equivalent to: DynamicOptic.root
|
|
305
|
+
// Equivalent to: DynamicOptic(Vector.empty)
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
## Compile-Time Safety
|
|
309
|
+
|
|
310
|
+
The path interpolator **rejects runtime interpolation** to prevent unsafe dynamic path construction.
|
|
311
|
+
|
|
312
|
+
**❌ This will fail to compile:**
|
|
313
|
+
|
|
314
|
+
```scala
|
|
315
|
+
val fieldName = "email"
|
|
316
|
+
val path = p".$fieldName"
|
|
317
|
+
// Error: Path interpolator does not support runtime arguments.
|
|
318
|
+
// Use only literal strings like p".field[0]"
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
**❌ This will also fail:**
|
|
322
|
+
|
|
323
|
+
```scala
|
|
324
|
+
val idx = 5
|
|
325
|
+
val path = p"[$idx]"
|
|
326
|
+
// Error: Path interpolator does not support runtime arguments.
|
|
327
|
+
// Use only literal strings like p".field[0]"
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
**✅ Use only literal strings:**
|
|
331
|
+
|
|
332
|
+
```scala
|
|
333
|
+
val path = p".users[0].email" // ✓ Works
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
### Parse Error Examples
|
|
337
|
+
|
|
338
|
+
Invalid syntax is caught at compile time:
|
|
339
|
+
|
|
340
|
+
```scala
|
|
341
|
+
// Unterminated string
|
|
342
|
+
p"""{"foo"""
|
|
343
|
+
// Error: Unterminated string literal starting at position 1
|
|
344
|
+
|
|
345
|
+
// Invalid escape sequence
|
|
346
|
+
p"""{"foo\x"}"""
|
|
347
|
+
// Error: Invalid escape sequence '\x' at position 6
|
|
348
|
+
|
|
349
|
+
// Unexpected character
|
|
350
|
+
p".field@"
|
|
351
|
+
// Error: Unexpected character '@' at position 6
|
|
352
|
+
|
|
353
|
+
// Invalid identifier
|
|
354
|
+
p"."
|
|
355
|
+
// Error: Invalid identifier at position 1
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
## Performance
|
|
359
|
+
|
|
360
|
+
**Zero Runtime Overhead**
|
|
361
|
+
|
|
362
|
+
All path parsing and validation occurs at **compile time**. The interpolator generates the exact same bytecode as manual `DynamicOptic` construction:
|
|
363
|
+
|
|
364
|
+
```scala
|
|
365
|
+
// These produce identical bytecode:
|
|
366
|
+
p".users[*].email"
|
|
367
|
+
|
|
368
|
+
DynamicOptic(Vector(
|
|
369
|
+
DynamicOptic.Node.Field("users"),
|
|
370
|
+
DynamicOptic.Node.Elements,
|
|
371
|
+
DynamicOptic.Node.Field("email")
|
|
372
|
+
))
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
There is **no runtime parsing**, **no reflection**, and **no performance penalty**.
|
|
376
|
+
|
|
377
|
+
## Examples
|
|
378
|
+
|
|
379
|
+
### Accessing Nested Fields
|
|
380
|
+
|
|
381
|
+
```scala
|
|
382
|
+
import zio.blocks.schema._
|
|
383
|
+
|
|
384
|
+
case class Address(street: String, city: String, zipCode: String)
|
|
385
|
+
case class Person(name: String, age: Int, address: Address)
|
|
386
|
+
|
|
387
|
+
// Access nested street field
|
|
388
|
+
val streetPath = p".address.street"
|
|
389
|
+
|
|
390
|
+
// Use with DynamicValue
|
|
391
|
+
val person = DynamicValue.fromPerson(...)
|
|
392
|
+
val street = person.get(streetPath)
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
### Working with Collections
|
|
396
|
+
|
|
397
|
+
```scala
|
|
398
|
+
case class User(id: Int, email: String, tags: Seq[String])
|
|
399
|
+
case class Company(name: String, users: Seq[User])
|
|
400
|
+
|
|
401
|
+
// Get all user emails
|
|
402
|
+
val emailsPath = p".users[*].email"
|
|
403
|
+
|
|
404
|
+
// Get first user's first tag
|
|
405
|
+
val firstTagPath = p".users[0].tags[0]"
|
|
406
|
+
|
|
407
|
+
// Get specific users by index
|
|
408
|
+
val specificUsersPath = p".users[0,2,5]"
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
### Map Lookups
|
|
412
|
+
|
|
413
|
+
```scala
|
|
414
|
+
case class Config(
|
|
415
|
+
settings: Map[String, String],
|
|
416
|
+
ports: Map[Int, String]
|
|
417
|
+
)
|
|
418
|
+
|
|
419
|
+
// Lookup by string key
|
|
420
|
+
val hostPath = p"""settings{"host"}"""
|
|
421
|
+
|
|
422
|
+
// Lookup by integer key
|
|
423
|
+
val httpPortPath = p"ports{80}"
|
|
424
|
+
|
|
425
|
+
// Get all config values
|
|
426
|
+
val allValuesPath = p"settings{*}"
|
|
427
|
+
|
|
428
|
+
// Get all port numbers (keys)
|
|
429
|
+
val allPortsPath = p"ports{*:}"
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
### Variant Case Handling
|
|
433
|
+
|
|
434
|
+
```scala
|
|
435
|
+
sealed trait Result[+A]
|
|
436
|
+
case class Success[A](value: A) extends Result[A]
|
|
437
|
+
case class Failure(error: String) extends Result[Nothing]
|
|
438
|
+
|
|
439
|
+
case class Response(result: Result[User])
|
|
440
|
+
|
|
441
|
+
// Navigate into Success case
|
|
442
|
+
val successValuePath = p".result<Success>.value"
|
|
443
|
+
|
|
444
|
+
// Navigate into Failure case
|
|
445
|
+
val errorPath = p".result<Failure>.error"
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
### Real-World Example: API Response
|
|
449
|
+
|
|
450
|
+
```scala
|
|
451
|
+
case class Metadata(tags: Seq[String], version: Int)
|
|
452
|
+
case class Item(id: String, data: String, metadata: Metadata)
|
|
453
|
+
case class ApiResponse(
|
|
454
|
+
status: String,
|
|
455
|
+
items: Seq[Item],
|
|
456
|
+
config: Map[String, String]
|
|
457
|
+
)
|
|
458
|
+
|
|
459
|
+
// Get the version from the first item's metadata
|
|
460
|
+
val versionPath = p".items[0].metadata.version"
|
|
461
|
+
|
|
462
|
+
// Get all item IDs
|
|
463
|
+
val allIdsPath = p".items[*].id"
|
|
464
|
+
|
|
465
|
+
// Get the first tag of each item
|
|
466
|
+
val firstTagsPath = p".items[*].metadata.tags[0]"
|
|
467
|
+
|
|
468
|
+
// Lookup config value
|
|
469
|
+
val apiKeyPath = p"""config{"api_key"}"""
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
## Before & After Comparison
|
|
473
|
+
|
|
474
|
+
### Manual Construction (Before)
|
|
475
|
+
|
|
476
|
+
```scala
|
|
477
|
+
import zio.blocks.schema.DynamicOptic
|
|
478
|
+
import zio.blocks.schema.DynamicOptic.Node
|
|
479
|
+
import zio.blocks.schema.DynamicValue
|
|
480
|
+
import zio.blocks.schema.PrimitiveValue
|
|
481
|
+
|
|
482
|
+
// Simple path - verbose and error-prone
|
|
483
|
+
val path1 = DynamicOptic(Vector(
|
|
484
|
+
Node.Field("users"),
|
|
485
|
+
Node.AtIndex(0),
|
|
486
|
+
Node.Field("email")
|
|
487
|
+
))
|
|
488
|
+
|
|
489
|
+
// Complex path - extremely verbose
|
|
490
|
+
val path2 = DynamicOptic(Vector(
|
|
491
|
+
Node.Field("root"),
|
|
492
|
+
Node.Field("children"),
|
|
493
|
+
Node.Elements,
|
|
494
|
+
Node.Field("metadata"),
|
|
495
|
+
Node.AtMapKey(DynamicValue.Primitive(PrimitiveValue.String("tags"))),
|
|
496
|
+
Node.AtIndex(0)
|
|
497
|
+
))
|
|
498
|
+
|
|
499
|
+
// Map with multiple keys
|
|
500
|
+
val path3 = DynamicOptic(Vector(
|
|
501
|
+
Node.Field("data"),
|
|
502
|
+
Node.AtMapKeys(Seq(
|
|
503
|
+
DynamicValue.Primitive(PrimitiveValue.String("foo")),
|
|
504
|
+
DynamicValue.Primitive(PrimitiveValue.String("bar")),
|
|
505
|
+
DynamicValue.Primitive(PrimitiveValue.Int(42))
|
|
506
|
+
))
|
|
507
|
+
))
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
### Path Interpolator (After)
|
|
511
|
+
|
|
512
|
+
```scala
|
|
513
|
+
import zio.blocks.schema._
|
|
514
|
+
|
|
515
|
+
// Simple path - clean and readable
|
|
516
|
+
val path1 = p".users[0].email"
|
|
517
|
+
|
|
518
|
+
// Complex path - still clean and readable
|
|
519
|
+
val path2 = p""".root.children[*].metadata{"tags"}[0]"""
|
|
520
|
+
|
|
521
|
+
// Map with multiple keys - concise
|
|
522
|
+
val path3 = p"""data{"foo", "bar", 42}"""
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
**Benefits:**
|
|
526
|
+
|
|
527
|
+
- **90% less code** for typical paths
|
|
528
|
+
- **Easier to read** and understand intent
|
|
529
|
+
- **Easier to write** and maintain
|
|
530
|
+
- **Compile-time validated** - catches errors immediately
|
|
531
|
+
- **No performance difference** - identical bytecode
|
|
532
|
+
|
|
533
|
+
## Practical Usage Patterns
|
|
534
|
+
|
|
535
|
+
### Building Paths Dynamically (at Compile Time)
|
|
536
|
+
|
|
537
|
+
```scala
|
|
538
|
+
// You can't use runtime variables, but you can compose literal paths:
|
|
539
|
+
val basePath = p".data.items"
|
|
540
|
+
val emailPath = basePath(p"[*].email")
|
|
541
|
+
// Same as: p".data.items[*].email"
|
|
542
|
+
```
|
|
543
|
+
|
|
544
|
+
### Working with DynamicValue
|
|
545
|
+
|
|
546
|
+
```scala
|
|
547
|
+
import zio.blocks.schema._
|
|
548
|
+
|
|
549
|
+
val data: DynamicValue = ...
|
|
550
|
+
|
|
551
|
+
// Navigate and extract
|
|
552
|
+
val value = data.get(p".users[0].email")
|
|
553
|
+
|
|
554
|
+
// Update at path
|
|
555
|
+
val updated = data.set(p".users[0].age", DynamicValue.fromInt(30))
|
|
556
|
+
```
|
|
557
|
+
|
|
558
|
+
### Integration with Schema Optics
|
|
559
|
+
|
|
560
|
+
```scala
|
|
561
|
+
import zio.blocks.schema._
|
|
562
|
+
|
|
563
|
+
case class User(name: String, email: String)
|
|
564
|
+
object User extends CompanionOptics[User] {
|
|
565
|
+
implicit val schema: Schema[User] = Schema.derived
|
|
566
|
+
|
|
567
|
+
// Use path interpolator for complex lenses
|
|
568
|
+
val email = $(_.email)
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// DynamicOptic can be used for runtime path resolution
|
|
572
|
+
val dynamicPath = p".email"
|
|
573
|
+
```
|
|
574
|
+
|
|
575
|
+
## Tips and Best Practices
|
|
576
|
+
|
|
577
|
+
1. **Use the leading dot for clarity**: While optional, `p".field"` is more explicit than `p"field"`
|
|
578
|
+
|
|
579
|
+
2. **Leverage compile-time validation**: Let the compiler catch typos and syntax errors early
|
|
580
|
+
|
|
581
|
+
3. **Compose paths when needed**: Break complex paths into reusable components
|
|
582
|
+
```scala
|
|
583
|
+
val userPath = p".users[0]"
|
|
584
|
+
val emailPath = userPath(p".email")
|
|
585
|
+
```
|
|
586
|
+
|
|
587
|
+
4. **Use raw strings for map keys**: Triple-quoted strings avoid escape hell
|
|
588
|
+
```scala
|
|
589
|
+
p"""config{"api.key"}""" // Better than p"config{\"api.key\"}"
|
|
590
|
+
```
|
|
591
|
+
|
|
592
|
+
5. **Document complex paths**: Add comments explaining what nested paths navigate
|
|
593
|
+
```scala
|
|
594
|
+
// Get the first tag from each user's metadata
|
|
595
|
+
val tagsPath = p".users[*].metadata.tags[0]"
|
|
596
|
+
```
|
|
597
|
+
|
|
598
|
+
## Limitations
|
|
599
|
+
|
|
600
|
+
- **No runtime interpolation**: You cannot use variables in paths (this is by design for safety)
|
|
601
|
+
- **No arithmetic in ranges**: Ranges must be literal integers (e.g., `[0:5]` not `[0:n]`)
|
|
602
|
+
- **No string interpolation**: Only literal strings work with the interpolator
|
|
603
|
+
- **Map keys limited to primitives**: Only String, Int, Char, and Boolean keys are supported
|
|
604
|
+
|
|
605
|
+
These limitations ensure compile-time safety and zero runtime overhead.
|
|
606
|
+
|
|
607
|
+
## Debug-Friendly toString
|
|
608
|
+
|
|
609
|
+
`DynamicOptic` instances have a custom `toString` that produces output matching the `p"..."` interpolator syntax. This makes debugging easier because you can copy the output directly into your code:
|
|
610
|
+
|
|
611
|
+
```scala
|
|
612
|
+
val optic = DynamicOptic.root.field("users").elements.field("email")
|
|
613
|
+
println(optic) // Output: .users[*].email
|
|
614
|
+
|
|
615
|
+
// The output can be copy-pasted into p"..."
|
|
616
|
+
val same = p".users[*].email"
|
|
617
|
+
```
|
|
618
|
+
|
|
619
|
+
**Examples:**
|
|
620
|
+
|
|
621
|
+
| DynamicOptic Construction | toString Output |
|
|
622
|
+
|---------------------------|-----------------|
|
|
623
|
+
| `DynamicOptic.root.field("name")` | `.name` |
|
|
624
|
+
| `DynamicOptic.root.field("address").field("street")` | `.address.street` |
|
|
625
|
+
| `DynamicOptic.root.caseOf("Some")` | `<Some>` |
|
|
626
|
+
| `DynamicOptic.root.at(0)` | `[0]` |
|
|
627
|
+
| `DynamicOptic.root.atIndices(0, 2, 5)` | `[0,2,5]` |
|
|
628
|
+
| `DynamicOptic.elements` | `[*]` |
|
|
629
|
+
| `DynamicOptic.root.atKey("host")` | `{"host"}` |
|
|
630
|
+
| `DynamicOptic.root.atKey(80)` | `{80}` |
|
|
631
|
+
| `DynamicOptic.mapValues` | `{*}` |
|
|
632
|
+
| `DynamicOptic.mapKeys` | `{*:}` |
|
|
633
|
+
| `DynamicOptic.wrapped` | `.~` |
|
|
634
|
+
|
|
635
|
+
## Summary
|
|
636
|
+
|
|
637
|
+
The `p"..."` path interpolator provides:
|
|
638
|
+
|
|
639
|
+
- **Concise syntax** for building optic paths
|
|
640
|
+
- **Compile-time parsing** with zero runtime overhead
|
|
641
|
+
- **Type-safe navigation** through complex data structures
|
|
642
|
+
- **Cross-platform support** for Scala 2 and Scala 3
|
|
643
|
+
- **Rich feature set** covering all DynamicOptic operations
|
|
644
|
+
|
|
645
|
+
Use it whenever you need to construct `DynamicOptic` paths for navigating dynamic data structures in ZIO Blocks.
|