@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.
@@ -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.