@zio.dev/zio-blocks 0.0.28 → 0.0.29

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,320 @@
1
+ ---
2
+ id: json-differ
3
+ title: "JsonDiffer"
4
+ ---
5
+
6
+ `JsonDiffer` is a **diff algorithm for JSON values** that computes the minimal [`JsonPatch`](./json-patch.md) transforming one `Json` value into another. It is the foundation of `JsonPatch.diff`.
7
+
8
+ ```scala
9
+ object JsonDiffer {
10
+ def diff(source: Json, target: Json): JsonPatch
11
+ }
12
+ ```
13
+
14
+ ## Overview
15
+
16
+ `JsonDiffer` solves a fundamental problem: given two `Json` values, what is the most compact representation of the changes from one to the other?
17
+
18
+ Instead of transmitting or storing the entire new value, `JsonDiffer` emits a `JsonPatch` containing only the differences. For each type of change — numeric updates, string mutations, array reordering, field additions — it selects the most space-efficient representation:
19
+
20
+ ```
21
+ Original JSON Target JSON
22
+ ┌──────────────────┐ ┌──────────────────┐
23
+ │ { │ │ { │
24
+ │ "name": "Alice"│ │ "name": "Alice"│
25
+ │ "age": 25 │─────→│ "age": 26 │
26
+ │ "city": "NYC" │ diff │ "city": "NYC" │
27
+ │ } │ │ } │
28
+ └──────────────────┘ └──────────────────┘
29
+ │
30
+ ▼
31
+ JsonPatch {
32
+ ObjectEdit(
33
+ Modify("age", NumberDelta(1))
34
+ )
35
+ }
36
+ ```
37
+
38
+ The core guarantee: `JsonDiffer.diff(source, target).apply(source) == Right(target)` always holds.
39
+
40
+ ## Motivation
41
+
42
+ Transmitting or storing entire JSON documents wastes bandwidth, disk space, and network latency when only a fraction of the data changes. `JsonDiffer` enables:
43
+
44
+ - **Efficient data synchronization** — send only what changed, not the entire document
45
+ - **Change tracking and audit logs** — record every mutation as a first-class value
46
+ - **Optimistic concurrency control** — detect conflicts by analyzing patches rather than full documents
47
+ - **Replay and time-travel debugging** — reconstruct any historical state by applying patches in sequence
48
+ - **Compression** — patches are often 10–100× smaller than the full target value
49
+
50
+ ## The Diffing Algorithm
51
+
52
+ `JsonDiffer.diff` adapts its strategy per `Json` type, choosing the most compact representation for each kind of change.
53
+
54
+ ### Type Mismatches
55
+
56
+ When the types differ (e.g., number to string, object to array), `JsonDiffer` emits `Op.Set` to replace the value entirely. This is the only compact choice when the types diverge:
57
+
58
+ ```scala
59
+ import zio.blocks.schema.json.{Json, JsonDiffer}
60
+
61
+ val numberToString = JsonDiffer.diff(Json.Number(42), Json.String("hello"))
62
+ val objectToArray = JsonDiffer.diff(
63
+ Json.Object("x" -> Json.Number(1)),
64
+ Json.Array(Json.Number(1), Json.Number(2))
65
+ )
66
+ ```
67
+
68
+ Both patches consist of a single `Set` operation:
69
+
70
+ ```scala
71
+ import zio.blocks.schema.json.JsonPatch
72
+
73
+ numberToString.apply(Json.Number(42))
74
+ objectToArray.apply(Json.Object("x" -> Json.Number(1)))
75
+ ```
76
+
77
+ ### Numbers
78
+
79
+ For numeric changes, `JsonDiffer` emits `NumberDelta` — representing the difference between old and new values as a delta rather than replacing the entire number:
80
+
81
+ ```scala
82
+ import zio.blocks.schema.json.{Json, JsonDiffer}
83
+
84
+ val increment = JsonDiffer.diff(Json.Number(100), Json.Number(105))
85
+ val decrement = JsonDiffer.diff(Json.Number(50), Json.Number(48))
86
+ ```
87
+
88
+ The patches store deltas, not full new values:
89
+
90
+ ```scala
91
+ import zio.blocks.schema.json.JsonPatch
92
+
93
+ increment.apply(Json.Number(100))
94
+ decrement.apply(Json.Number(50))
95
+ ```
96
+
97
+ ### Strings
98
+
99
+ For strings, `JsonDiffer` chooses between two strategies:
100
+
101
+ 1. **LCS (Longest Common Subsequence) edit operations** — if the edits are more compact than the full new string.
102
+ 2. **Full replacement with `Set`** — if the string has changed so much that storing the entire new value is smaller.
103
+
104
+ The algorithm computes common characters, then emits `Insert` and `Delete` operations (with `Append` and `Modify` existing as supported patch ops but not currently produced by `JsonDiffer`). It only emits the edits if their byte size is smaller than the new string's length:
105
+
106
+ ```scala
107
+ import zio.blocks.schema.json.{Json, JsonDiffer}
108
+
109
+ // Prefix insertion — common suffix preserved
110
+ val addPrefix = JsonDiffer.diff(Json.String("world"), Json.String("hello world"))
111
+
112
+ // Complete replacement — almost no common subsequence
113
+ val replacement = JsonDiffer.diff(Json.String("abc"), Json.String("xyz"))
114
+ ```
115
+
116
+ The first patch uses `StringEdit` (compact), the second uses `Set` (more efficient):
117
+
118
+ ```scala
119
+ addPrefix.apply(Json.String("world"))
120
+ replacement.apply(Json.String("abc"))
121
+ ```
122
+
123
+ ### Arrays
124
+
125
+ For arrays, `JsonDiffer` uses **LCS-aligned insert, delete, and append operations** to transform the old array into the new one. It aligns common elements and generates the minimum sequence of mutations:
126
+
127
+ ```scala
128
+ import zio.blocks.schema.json.{Json, JsonDiffer}
129
+
130
+ // Append elements at the end
131
+ val append = JsonDiffer.diff(
132
+ Json.Array(Json.Number(1), Json.Number(2)),
133
+ Json.Array(Json.Number(1), Json.Number(2), Json.Number(3))
134
+ )
135
+
136
+ // Delete and reorder — LCS finds the common elements
137
+ val reorder = JsonDiffer.diff(
138
+ Json.Array(Json.Number(1), Json.Number(2), Json.Number(3)),
139
+ Json.Array(Json.Number(3), Json.Number(2), Json.Number(1))
140
+ )
141
+ ```
142
+
143
+ Both patches use efficient `ArrayEdit` operations:
144
+
145
+ ```scala
146
+ append.apply(Json.Array(Json.Number(1), Json.Number(2)))
147
+ reorder.apply(Json.Array(Json.Number(1), Json.Number(2), Json.Number(3)))
148
+ ```
149
+
150
+ ### Objects
151
+
152
+ For objects, `JsonDiffer` compares field-by-field:
153
+
154
+ - Fields in the target but not in the source become `Add` operations.
155
+ - Fields in the source but not in the target become `Remove` operations.
156
+ - Fields in both are recursively diffed — if the values differ, a `Modify` with a sub-patch is emitted.
157
+
158
+ ```scala
159
+ import zio.blocks.schema.json.{Json, JsonDiffer}
160
+
161
+ val original = Json.Object(
162
+ "name" -> Json.String("Alice"),
163
+ "age" -> Json.Number(25),
164
+ "city" -> Json.String("NYC")
165
+ )
166
+
167
+ val updated = Json.Object(
168
+ "name" -> Json.String("Alice"),
169
+ "age" -> Json.Number(26),
170
+ "email" -> Json.String("alice@example.com")
171
+ )
172
+
173
+ val patch = JsonDiffer.diff(original, updated)
174
+ ```
175
+
176
+ The patch contains `Add`, `Remove`, and `Modify` operations:
177
+
178
+ ```scala
179
+ patch.apply(original)
180
+ ```
181
+
182
+ ### Nested Structures
183
+
184
+ `JsonDiffer` handles deeply nested objects and arrays recursively. Each field or element is diffed independently, producing compact patches at every level:
185
+
186
+ ```scala
187
+ import zio.blocks.schema.json.{Json, JsonDiffer}
188
+
189
+ val original = Json.Object(
190
+ "user" -> Json.Object(
191
+ "name" -> Json.String("Alice"),
192
+ "scores" -> Json.Array(Json.Number(95), Json.Number(87))
193
+ )
194
+ )
195
+
196
+ val updated = Json.Object(
197
+ "user" -> Json.Object(
198
+ "name" -> Json.String("Alice"),
199
+ "scores" -> Json.Array(Json.Number(95), Json.Number(88), Json.Number(92))
200
+ )
201
+ )
202
+
203
+ val patch = JsonDiffer.diff(original, updated)
204
+ ```
205
+
206
+ The patch navigates to the nested array and emits only the array changes:
207
+
208
+ ```scala
209
+ patch.apply(original)
210
+ ```
211
+
212
+ ## Core Operations
213
+
214
+ `JsonDiffer` exposes a single public operation: `diff`, which computes the minimal `JsonPatch` that transforms `source` into `target`. Returns an empty patch if the values are already equal:
215
+
216
+ ```scala
217
+ object JsonDiffer {
218
+ def diff(source: Json, target: Json): JsonPatch
219
+ }
220
+ ```
221
+
222
+ The roundtrip property always holds — applying the patch to the source always yields the target:
223
+
224
+ ```scala
225
+ import zio.blocks.schema.json.{Json, JsonDiffer}
226
+
227
+ val source = Json.Object("x" -> Json.Number(10), "y" -> Json.Number(20))
228
+ val target = Json.Object("x" -> Json.Number(10), "y" -> Json.Number(21))
229
+ val patch = JsonDiffer.diff(source, target)
230
+ ```
231
+
232
+ ```scala
233
+ patch.apply(source) == Right(target)
234
+ // res13: Boolean = true
235
+ ```
236
+
237
+ `JsonDiffer.diff` is also available as `JsonPatch.diff` and as the `Json#diff` extension method:
238
+
239
+ ```scala
240
+ import zio.blocks.schema.json.{Json, JsonPatch}
241
+
242
+ // Via JsonPatch companion
243
+ val p1 = JsonPatch.diff(Json.Number(1), Json.Number(2))
244
+
245
+ // Via Json extension method
246
+ val p2 = Json.Number(1).diff(Json.Number(2))
247
+ ```
248
+
249
+ ## Integration
250
+
251
+ `JsonDiffer` is the implementation behind the public `JsonPatch.diff` API. You typically interact with it through `JsonPatch.diff` or the `Json#diff` extension method rather than calling `JsonDiffer.diff` directly.
252
+
253
+ The relationship is simple: `JsonPatch.diff` delegates to `JsonDiffer.diff` and wraps the result in a `JsonPatch` for further composition and application.
254
+
255
+ Once you have a `JsonPatch` from `JsonDiffer`, use the full `JsonPatch` API to apply it with different modes, compose multiple patches, or convert to/from `DynamicPatch`:
256
+
257
+ ```scala
258
+ import zio.blocks.schema.json.{Json, JsonDiffer, JsonPatch}
259
+ import zio.blocks.schema.patch.PatchMode
260
+
261
+ val source = Json.Object("count" -> Json.Number(0))
262
+ val target = Json.Object("count" -> Json.Number(1))
263
+ val patch = JsonDiffer.diff(source, target)
264
+ ```
265
+
266
+ Apply with different failure-handling modes:
267
+
268
+ ```scala
269
+ patch.apply(source, PatchMode.Strict)
270
+ patch.apply(source, PatchMode.Lenient)
271
+ patch.apply(source, PatchMode.Clobber)
272
+ ```
273
+
274
+ Compose patches with `++`:
275
+
276
+ ```scala
277
+ val patch2 = JsonDiffer.diff(target, Json.Object("count" -> Json.Number(2)))
278
+ val combined = patch ++ patch2
279
+ ```
280
+
281
+ ## Implementation Notes
282
+
283
+ `JsonDiffer.diff` uses the LCS (Longest Common Subsequence) algorithm for both strings and arrays, ensuring that the number of edit operations is minimized. For strings, it compares character sequences; for arrays, it compares JSON elements by structural equality.
284
+
285
+ The LCS-based approach is particularly effective for arrays and strings with significant common subsequences — a common pattern in real-world data mutation scenarios (e.g., adding an item to a list, inserting a few characters into a string).
286
+
287
+ :::note
288
+ The LCS algorithm has O(n·m) time complexity, where n and m are the lengths of the two sequences. For very large strings or arrays, consider whether you need the minimal patch or can accept a faster approximation.
289
+ :::
290
+
291
+ ## Running the Examples
292
+
293
+ `JsonDiffer` is the foundation of the `JsonPatch` API. Runnable examples demonstrating `JsonPatch.diff` (which internally uses `JsonDiffer`) are available in the `schema-examples` module.
294
+
295
+ **1. Clone the repository and navigate to the project:**
296
+
297
+ ```bash
298
+ git clone https://github.com/zio/zio-blocks.git
299
+ cd zio-blocks
300
+ ```
301
+
302
+ **2. Run JsonPatch examples with sbt:**
303
+
304
+ These examples demonstrate how `JsonDiffer.diff` computes minimal patches through the public `JsonPatch.diff` API:
305
+
306
+ ```bash
307
+ sbt "schema-examples/runMain jsonpatch.JsonPatchDiffAndApplyExample"
308
+ ```
309
+
310
+ ```bash
311
+ sbt "schema-examples/runMain jsonpatch.JsonPatchOperationsExample"
312
+ ```
313
+
314
+ ```bash
315
+ sbt "schema-examples/runMain jsonpatch.JsonPatchCompositionExample"
316
+ ```
317
+
318
+ ```bash
319
+ sbt "schema-examples/runMain jsonpatch.CompleteJsonPatchExample"
320
+ ```
@@ -640,7 +640,7 @@ You rarely need to construct `Nested` manually; it is primarily an internal opti
640
640
 
641
641
  ## Diffing Algorithm
642
642
 
643
- `JsonPatch.diff` (and its alias `Json#diff`) delegate to `JsonDiffer.diff`, which selects the most compact representation for each type of change:
643
+ `JsonPatch.diff` (and its alias `Json#diff`) delegate to [`JsonDiffer.diff`](./json-differ.md), which selects the most compact representation for each type of change:
644
644
 
645
645
  | Value type | Change | Strategy |
646
646
  |------------|--------|----------|
@@ -75,13 +75,13 @@ textAny.matches(html) // true
75
75
  Add the following to your `build.sbt`:
76
76
 
77
77
  ```scala
78
- libraryDependencies += "dev.zio" %% "zio-blocks-mediatype" % "0.0.28"
78
+ libraryDependencies += "dev.zio" %% "zio-blocks-mediatype" % "0.0.29"
79
79
  ```
80
80
 
81
81
  For cross-platform projects (Scala.js):
82
82
 
83
83
  ```scala
84
- libraryDependencies += "dev.zio" %%% "zio-blocks-mediatype" % "0.0.28"
84
+ libraryDependencies += "dev.zio" %%% "zio-blocks-mediatype" % "0.0.29"
85
85
  ```
86
86
 
87
87
  Supported Scala versions: 2.13.x and 3.x.
@@ -0,0 +1,49 @@
1
+ ---
2
+ id: index
3
+ title: "Resource Management & Dependency Injection"
4
+ ---
5
+
6
+ ## Introduction
7
+
8
+ Resource management and dependency injection are fundamental to building reliable, maintainable applications. ZIO Blocks provides three complementary types that work together to eliminate common lifetime bugs while enabling powerful composition patterns: **Scope** provides compile-time safe resource boundaries, **Resource** encapsulates acquisition and cleanup with automatic finalization, and **Wire** describes dependency graphs with type-safe construction recipes. Together, they form a cohesive system for managing object lifecycles, preventing resource leaks, and building dependency-injected architectures.
9
+
10
+ **Related Types:**
11
+ - [`Resource`](./resource.md) — Lazy recipe for managing resource lifecycles with automatic cleanup
12
+ - [`Scope`](./scope.md) — Compile-time safe resource management and scoped value access
13
+ - [`Wire`](./wire.md) — Type-safe recipes for constructing services and their dependencies
14
+
15
+ ## Overview
16
+
17
+ These three types solve the fundamental problem of managing resources and dependencies in concurrent, long-lived applications:
18
+
19
+ **Scope** is the foundation — it provides a compile-time safe boundary that prevents resources from escaping their intended lifetime. Using path-dependent types, Scope ensures that values allocated in one scope cannot accidentally be used in another scope, catching lifetime violations at compile time rather than causing runtime bugs.
20
+
21
+ **Resource** builds on Scope to describe how to acquire and finalize resources. Rather than executing immediately, a Resource is a lazy recipe that composes naturally with `map`, `flatMap`, and `zip`. When allocated within a scope, finalizers run automatically in LIFO order, ensuring cleanup happens even when errors occur.
22
+
23
+ **Wire** brings it all together by describing how to construct services and their dependencies. The Wire macro automatically handles dependency resolution, cycle detection, and AutoCloseable registration, letting you declaratively specify a dependency graph that the compiler validates.
24
+
25
+ ### How They Work Together
26
+
27
+ The typical flow is:
28
+
29
+ 1. **Define** dependencies using `Wire.shared[T]` or `Wire.unique[T]` — the macro inspects constructor parameters and generates a wire
30
+ 2. **Compose** wires together using `Resource.from[App](wire1, wire2, ...)` — builds the dependency graph
31
+ 3. **Allocate** within a scope using `scope.allocate(resource)` — acquires resources and registers finalizers
32
+ 4. **Use** scoped values via the `$` accessor — compile-time ensures they can't escape
33
+ 5. **Cleanup** happens automatically when the scope exits — finalizers run in reverse order (LIFO)
34
+
35
+ ### Common Patterns
36
+
37
+ **Shared Singletons** — Use `Wire.shared[T]` for expensive resources (database connections, thread pools) that should be created once and reused across the application.
38
+
39
+ **Per-Request Instances** — Use `Wire.unique[T]` for request-scoped state that should be fresh for each request or operation.
40
+
41
+ **Manual Construction** — Use `Wire.Shared.fromFunction` or `Wire.Unique.fromFunction` when macro derivation doesn't fit your use case (custom initialization logic, special parameters).
42
+
43
+ **Resource Composition** — Use `Resource.map`, `Resource.flatMap`, and `Resource.zip` to build complex dependency chains where later resources depend on earlier ones.
44
+
45
+ ### Integration Points
46
+
47
+ - **Wire** uses **Resource** to manage lifecycles of constructed services
48
+ - **Resource** uses **Scope** for finalization and scoped value boundaries
49
+ - Both **Wire** and **Resource** produce values that are usable only within a **Scope** context