@zio.dev/zio-blocks 0.0.26 → 0.0.28

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/scope.md CHANGED
@@ -1,4 +1,7 @@
1
- # ZIO Blocks — Scope (`zio.blocks.scope`)
1
+ ---
2
+ id: scope
3
+ title: "Scope"
4
+ ---
2
5
 
3
6
  `zio.blocks.scope` is a **compile-time safe, zero-cost** resource management library for **Scala 3** (and Scala 2.13). It prevents a large class of lifetime bugs by tagging allocated values with an *unnameable*, scope-specific type and restricting how those values may be used.
4
7
 
@@ -153,6 +156,58 @@ Scope.global.scoped { scope =>
153
156
  }
154
157
  ```
155
158
 
159
+ ##### N-ary `$`: accessing multiple scoped values at once
160
+
161
+ When a result depends on **two or more** scoped values simultaneously, use the N-ary overloads (`N = 2..5`):
162
+
163
+ ```scala
164
+ $(sa1, sa2)((v1, v2) => v1.method(v2.result()))
165
+ $(sa1, sa2, sa3)((v1, v2, v3) => v1.query(v2.key()) + v3.tag())
166
+ ```
167
+
168
+ The same receiver-only grammar applies to every parameter: each `vi` may only appear as a method receiver (e.g., `vi.method()`). Feeding the *result* of one parameter to a method of another is permitted:
169
+
170
+ ```scala
171
+ Scope.global.scoped { scope =>
172
+ import scope.*
173
+ val db: $[Database] = Resource.from[Database].allocate
174
+ val cache: $[Cache] = Resource.from[Cache].allocate
175
+
176
+ // d1 and d2 are both receivers; d2.key() produces a plain String arg
177
+ val result: String = $(db, cache)((d1, d2) => d1.query(d2.key()))
178
+ }
179
+ ```
180
+
181
+ Rejected at compile time (same rules as N=1, applied to each parameter independently):
182
+
183
+ ```scala
184
+ $(db, cache)((d1, d2) => d2) // d2 returned directly
185
+ $(db, cache)((d1, d2) => store(d1)) // d1 passed as argument
186
+ $(db, cache)((d1, d2) => d1.method(d2)) // d2 as bare arg (not a receiver)
187
+ $(db, cache)((d1, d2) => () => d2.query()) // d2 captured in closure
188
+ ```
189
+
190
+ The error messages name the offending parameter:
191
+
192
+ ```
193
+ Parameter 2 ('d2') cannot be passed as an argument to a function or method.
194
+ Scoped values may only be used as a method receiver (e.g., d2.method()).
195
+ ```
196
+
197
+ **Infix syntax** (`scope $ sa`) is only available for N=1. For N≥2, use unqualified syntax after `import scope.*`:
198
+
199
+ ```scala
200
+ $(db, cache)((d, c) => d.query(c.key())) // ✓ unqualified
201
+ ```
202
+
203
+ **For N>5**, extract each value in sequence (all results are `Unscoped` strings/values and can be freely combined):
204
+
205
+ ```scala
206
+ val q1 = $(db1)(_.query("a"))
207
+ val q2 = $(db2)(_.query("b"))
208
+ q1 + q2
209
+ ```
210
+
156
211
  ---
157
212
 
158
213
  ### 3) `Resource[A]`: acquisition + finalization
@@ -417,16 +472,52 @@ A `scoped { ... }` block can only return pure data (or `Nothing`). Resources and
417
472
 
418
473
  **Pragmatic safety.** The type-level tagging prevents *accidental* scope misuse in normal code, but it is not a security boundary. A determined developer can bypass it via `leak` (which emits a compiler warning), unsafe casts (`asInstanceOf`), or storing scoped references in mutable state (`var`).
419
474
 
420
- ### Closed-scope defense (runtime)
475
+ ### Closed-scope safety (runtime)
476
+
477
+ If a scope reference escapes its `scoped { }` block and an operation is attempted after closing, Scope throws `IllegalStateException` with a detailed, actionable error message:
478
+
479
+ - **`allocate`** on a closed scope:
480
+
481
+ ```
482
+ ── Scope Error ─────────────────────────────────────────────────────────────────
483
+
484
+ Cannot allocate resource: scope is already closed.
485
+
486
+ Scope: Scope.Child
487
+
488
+ What happened:
489
+ A call to allocate was made on a scope whose finalizers have
490
+ already run. The resource was never acquired.
491
+
492
+ Common causes:
493
+ • A scope reference escaped a scoped { } block (e.g. stored in a
494
+ field, captured in a Future or passed to another thread).
495
+ • close() was called on an OpenScope before all
496
+ allocations inside it completed.
497
+
498
+ Fix:
499
+ Call allocate only inside a live scoped { } block, or before
500
+ calling close() on an OpenScope.
421
501
 
422
- If a scope escapes and is used after closing, operations become **no-ops returning default values**:
502
+ // Correct usage:
503
+ Scope.global.scoped { scope =>
504
+ import scope.*
505
+ val db = allocate(Resource(new Database))
506
+ $(db)(_.query("SELECT 1"))
507
+ }
508
+
509
+ ────────────────────────────────────────────────────────────────────────────────
510
+ ```
511
+
512
+ - **`open()`** on a closed scope gives the same treatment, explaining that no child scope was created and directing the user to call `open()` only on a live scope.
513
+
514
+ - **`$`** on a closed scope explains that the resource may have already been released and accessing it would be undefined behaviour.
423
515
 
424
- - `$`, `allocate`, `open`, `lower` return defaults (`null`, `0`, `false`, …)
425
- - `$` does not run the lambda when closed
426
- - `defer` on a closed scope is ignored
427
- - `scoped` creates a born-closed child if the parent is already closed
516
+ The following operations on a closed scope do **not** throw:
428
517
 
429
- This prevents post-close interaction with released resources, but can produce surprising default values if scopes are misused across threads.
518
+ - `defer` — silently ignored (no-op)
519
+ - `scoped` — runs normally but creates a born-closed child scope
520
+ - `lower` — zero-cost cast, no closed check needed
430
521
 
431
522
  ### Thread ownership rule (JVM)
432
523
 
@@ -780,6 +871,112 @@ val appResource: Resource[App] =
780
871
 
781
872
  ---
782
873
 
874
+ ## Common runtime errors (and what they mean)
875
+
876
+ These `IllegalStateException`s are thrown when a scope operation is attempted on a closed scope. Each message identifies the scope type, explains what went wrong, lists common causes, and shows a correct usage example.
877
+
878
+ ### `allocate` on a closed scope
879
+
880
+ ```
881
+ ── Scope Error ─────────────────────────────────────────────────────────────────
882
+
883
+ Cannot allocate resource: scope is already closed.
884
+
885
+ Scope: Scope.Child
886
+
887
+ What happened:
888
+ A call to allocate was made on a scope whose finalizers have
889
+ already run. The resource was never acquired.
890
+
891
+ Common causes:
892
+ • A scope reference escaped a scoped { } block (e.g. stored in a
893
+ field, captured in a Future or passed to another thread).
894
+ • close() was called on an OpenScope before all
895
+ allocations inside it completed.
896
+
897
+ Fix:
898
+ Call allocate only inside a live scoped { } block, or before
899
+ calling close() on an OpenScope.
900
+
901
+ // Correct usage:
902
+ Scope.global.scoped { scope =>
903
+ import scope.*
904
+ val db = allocate(Resource(new Database))
905
+ $(db)(_.query("SELECT 1"))
906
+ }
907
+
908
+ ────────────────────────────────────────────────────────────────────────────────
909
+ ```
910
+
911
+ ### `open()` on a closed scope
912
+
913
+ ```
914
+ ── Scope Error ─────────────────────────────────────────────────────────────────
915
+
916
+ Cannot open child scope: scope is already closed.
917
+
918
+ Scope: Scope.Child
919
+
920
+ What happened:
921
+ A call to open() was made on a scope whose finalizers have
922
+ already run. No child scope was created.
923
+
924
+ Common causes:
925
+ • A scope reference escaped a scoped { } block and open()
926
+ was called after the block exited.
927
+ • close() was called on the parent OpenScope before
928
+ open() was called on it.
929
+
930
+ Fix:
931
+ Call open() only on a live (not yet closed) scope.
932
+
933
+ // Correct usage:
934
+ Scope.global.scoped { scope =>
935
+ import scope.*
936
+ val child = open()
937
+ $(child)(_.scope.allocate(Resource(new Database)))
938
+ }
939
+
940
+ ────────────────────────────────────────────────────────────────────────────────
941
+ ```
942
+
943
+ ### `$` on a closed scope
944
+
945
+ ```
946
+ ── Scope Error ─────────────────────────────────────────────────────────────────
947
+
948
+ Cannot access scoped value: scope is already closed.
949
+
950
+ Scope: Scope.Child
951
+
952
+ What happened:
953
+ The $ operator was called on a scope whose finalizers have
954
+ already run. The underlying resource may have been released.
955
+ Accessing it would be undefined behavior.
956
+
957
+ Common causes:
958
+ • A $[A] value or its owning scope escaped a scoped { }
959
+ block (e.g. captured in a Future, stored in a field, or
960
+ passed to another thread).
961
+ • close() was called on an OpenScope that still has
962
+ live $[A] values being accessed.
963
+
964
+ Fix:
965
+ Ensure all $ calls occur strictly within the scoped { }
966
+ block that owns the value, and that the scope has not been closed.
967
+
968
+ // Correct usage:
969
+ Scope.global.scoped { scope =>
970
+ import scope.*
971
+ val db = allocate(Resource(new Database))
972
+ $(db)(_.query("SELECT 1")) // $ used inside the block
973
+ }
974
+
975
+ ────────────────────────────────────────────────────────────────────────────────
976
+ ```
977
+
978
+ ---
979
+
783
980
  ## Common compile errors (and what they mean)
784
981
 
785
982
  This module produces two kinds of compile-time feedback:
@@ -789,17 +986,33 @@ This module produces two kinds of compile-time feedback:
789
986
 
790
987
  ### Unsafe use inside `$`
791
988
 
792
- Typical messages include:
989
+ All messages name the offending parameter by its 1-based index and source name, and end with the receiver-only reminder. Typical messages:
793
990
 
794
991
  ```
795
- Unsafe use of scoped value: the lambda parameter cannot be passed as an argument to a function or method.
992
+ Parameter 1 ('d') cannot be passed as an argument to a function or method.
993
+ Scoped values may only be used as a method receiver (e.g., d.method()).
796
994
  ```
797
995
 
798
- Other variants:
996
+ ```
997
+ Parameter 1 ('d') must only be used as a method receiver.
998
+ It cannot be returned, stored, passed as an argument, or captured.
999
+ Scoped values may only be used as a method receiver (e.g., d.method()).
1000
+ ```
799
1001
 
800
- - `Unsafe use of scoped value: the lambda parameter cannot be captured in a nested lambda or closure.`
801
- - `Unsafe use of scoped value: the lambda parameter must only be used as a method receiver ...`
802
- - `$ requires a lambda literal: (scope $ x)(a => a.method()). Method references and variables are not supported.`
1002
+ ```
1003
+ Parameter 1 ('d') cannot be captured in a nested lambda, def, or anonymous class.
1004
+ Scoped values may only be used as a method receiver (e.g., d.method()).
1005
+ ```
1006
+
1007
+ ```
1008
+ Parameter 2 ('cache') cannot be passed as an argument to a function or method.
1009
+ Scoped values may only be used as a method receiver (e.g., cache.method()).
1010
+ ```
1011
+
1012
+ ```
1013
+ $ requires a lambda literal, e.g. $(x)(a => a.method()).
1014
+ Method references and variables are not supported.
1015
+ ```
803
1016
 
804
1017
  ### Not a class (`Wire.shared/unique` on a trait / abstract)
805
1018
 
@@ -1013,8 +1226,15 @@ def scoped[A](f: (child: Scope.Child[this.type]) => A)(using Unscoped[A]): A
1013
1226
  def allocate[A](resource: Resource[A]): $[A]
1014
1227
  def allocate[A <: AutoCloseable](value: => A): $[A]
1015
1228
 
1229
+ // N=1 (infix available: `scope $ sa`)
1016
1230
  infix transparent inline def $[A, B](sa: $[A])(inline f: A => B): B | $[B]
1017
1231
 
1232
+ // N=2..5 (unqualified syntax: `$(sa1, sa2)(f)` after `import scope.*`)
1233
+ transparent inline def $[A1, A2, B](sa1: $[A1], sa2: $[A2])(inline f: (A1, A2) => B): B | $[B]
1234
+ transparent inline def $[A1, A2, A3, B](sa1: $[A1], sa2: $[A2], sa3: $[A3])(inline f: (A1, A2, A3) => B): B | $[B]
1235
+ transparent inline def $[A1, A2, A3, A4, B](sa1: $[A1], sa2: $[A2], sa3: $[A3], sa4: $[A4])(inline f: (A1, A2, A3, A4) => B): B | $[B]
1236
+ transparent inline def $[A1, A2, A3, A4, A5, B](sa1: $[A1], sa2: $[A2], sa3: $[A3], sa4: $[A4], sa5: $[A5])(inline f: (A1, A2, A3, A4, A5) => B): B | $[B]
1237
+
1018
1238
  def lower[A](value: parent.$[A]): $[A]
1019
1239
 
1020
1240
  override def defer(f: => Unit): DeferHandle
@@ -1026,9 +1246,11 @@ inline def leak[A](inline sa: $[A]): A
1026
1246
 
1027
1247
  Notes:
1028
1248
 
1029
- - `$` requires a **lambda literal** and enforces safe receiver-only usage.
1249
+ - `$` (all arities) requires a **lambda literal** and enforces safe receiver-only usage at compile time.
1030
1250
  - `$` returns `B` if `Unscoped[B]` exists; otherwise returns `$[B]`.
1031
- - If the scope is closed, `$` / `allocate` / `open` / `lower` return default values and perform no work.
1251
+ - N=1 is `infix`; N≥2 are not — use unqualified syntax after `import scope.*`.
1252
+ - For N>5, call `$` once per resource and combine the resulting plain (Unscoped) values.
1253
+ - If the scope is closed, `$`, `allocate`, and `open` throw `IllegalStateException` with a detailed error message. `defer` and `lower` are unaffected.
1032
1254
 
1033
1255
  Syntax enrichments available after `import scope.*` inside a scope:
1034
1256
 
@@ -1191,7 +1413,9 @@ object Unscoped:
1191
1413
  ## Practical guidance (summary)
1192
1414
 
1193
1415
  - Allocate in a scope: `resource.allocate` (inside `Scope.global.scoped { scope => import scope.* ... }`)
1194
- - Use scoped values only through: `(scope $ value)(...)`
1416
+ - Access one scoped value: `$(sa)(v => v.method())` — parameter can only be a receiver
1417
+ - Access two or more scoped values simultaneously: `$(sa1, sa2)((v1, v2) => v1.method(v2.result()))` (N=2..5)
1418
+ - For N>5: call `$` once per resource, combine the plain results
1195
1419
  - Return only `Unscoped` data from `scoped` blocks
1196
1420
  - Use `lower` to use parent values inside a child
1197
1421
  - If `$` returns `$[Resource[A]]`, call `.allocate` on it (scoped resource chaining)
package/sidebars.js CHANGED
@@ -7,13 +7,20 @@ const sidebars = {
7
7
  link: { type: "doc", id: "index" },
8
8
  items: [
9
9
  "reference/schema",
10
+ "reference/allows",
10
11
  "reference/reflect",
11
12
  "reference/binding",
13
+ "reference/binding-resolver",
12
14
  "reference/registers",
13
15
  "reference/typeid",
16
+ "reference/allows",
14
17
  "reference/modifier",
15
18
  "reference/dynamic-value",
19
+ "reference/dynamic-schema",
20
+ "reference/lazy",
21
+ "reference/structural-types",
16
22
  "reference/optics",
23
+ "reference/patch",
17
24
  "reference/schema-expr",
18
25
  "reference/dynamic-optic",
19
26
  "reference/type-class-derivation",
@@ -21,12 +28,24 @@ const sidebars = {
21
28
  "reference/formats",
22
29
  "path-interpolator",
23
30
  "reference/chunk",
31
+ "reference/schema-error",
24
32
  "reference/validation",
25
- "reference/schema-evolution",
33
+ {
34
+ type: "category",
35
+ label: "Schema Evolution",
36
+ link: { type: "doc", id: "reference/schema-evolution/index" },
37
+ items: [
38
+ "reference/schema-evolution/into",
39
+ "reference/schema-evolution/as",
40
+ ]
41
+ },
26
42
  "reference/context",
43
+ "scope",
27
44
  "reference/docs",
28
45
  "reference/json",
46
+ "reference/json-patch",
29
47
  "reference/json-schema",
48
+ "reference/xml",
30
49
  "reference/syntax",
31
50
  "reference/media-type",
32
51
  ]
@@ -35,6 +54,7 @@ const sidebars = {
35
54
  type: "category",
36
55
  label: "Guides",
37
56
  items: [
57
+ "guides/zio-schema-migration",
38
58
  "guides/query-dsl-reified-optics",
39
59
  "guides/query-dsl-sql",
40
60
  "guides/query-dsl-extending",