@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,631 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: patch
|
|
3
|
+
title: "Patching"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
The Patching system in ZIO Blocks provides a type-safe, serializable way to describe and apply transformations to data structures. Unlike direct mutations or lens-based updates, patches are **first-class values** that can be serialized, transmitted over the network, stored for audit logs, and composed together.
|
|
7
|
+
|
|
8
|
+
## Overview
|
|
9
|
+
|
|
10
|
+
A `Patch[S]` represents a sequence of operations that transform a value of type `S`. Because patches use serializable operations and reflective optics for navigation, they enable powerful use cases:
|
|
11
|
+
|
|
12
|
+
- **Remote Patching** — Send patches over the network to update remote state without transmitting entire objects
|
|
13
|
+
- **Audit Logs** — Record patches as a log of changes for compliance, debugging, or undo functionality
|
|
14
|
+
- **CRDT-like Operations** — Use commutative operations like `increment` that can be safely applied in any order
|
|
15
|
+
- **Optimistic Updates** — Apply patches locally while syncing with a server
|
|
16
|
+
- **Schema Evolution** — Patches work with the schema system, adapting as data structures evolve
|
|
17
|
+
|
|
18
|
+
```scala
|
|
19
|
+
import zio.blocks.schema._
|
|
20
|
+
import zio.blocks.schema.patch._
|
|
21
|
+
|
|
22
|
+
case class Person(name: String, age: Int)
|
|
23
|
+
object Person extends CompanionOptics[Person] {
|
|
24
|
+
implicit val schema: Schema[Person] = Schema.derived
|
|
25
|
+
val name: Lens[Person, String] = optic(_.name)
|
|
26
|
+
val age: Lens[Person, Int] = optic(_.age)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Create patches using smart constructors
|
|
30
|
+
val patch1 = Patch.set(Person.name, "John")
|
|
31
|
+
val patch2 = Patch.increment(Person.age, 1)
|
|
32
|
+
|
|
33
|
+
// Compose patches
|
|
34
|
+
val combined = patch1 ++ patch2
|
|
35
|
+
|
|
36
|
+
// Apply the patch
|
|
37
|
+
val jane = Person("Jane", 25)
|
|
38
|
+
val result = combined(jane) // Person("John", 26)
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Creating Patches
|
|
42
|
+
|
|
43
|
+
Patches are created using smart constructors on the `Patch` companion object. Each constructor takes an optic to specify the target location and the operation parameters.
|
|
44
|
+
|
|
45
|
+
### Setting Values
|
|
46
|
+
|
|
47
|
+
The `set` operation replaces a value at the specified location:
|
|
48
|
+
|
|
49
|
+
```scala
|
|
50
|
+
case class Address(street: String, city: String, zip: String)
|
|
51
|
+
object Address extends CompanionOptics[Address] {
|
|
52
|
+
implicit val schema: Schema[Address] = Schema.derived
|
|
53
|
+
val street: Lens[Address, String] = optic(_.street)
|
|
54
|
+
val city: Lens[Address, String] = optic(_.city)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
case class Person(name: String, address: Address)
|
|
58
|
+
object Person extends CompanionOptics[Person] {
|
|
59
|
+
implicit val schema: Schema[Person] = Schema.derived
|
|
60
|
+
val name: Lens[Person, String] = optic(_.name)
|
|
61
|
+
val address: Lens[Person, Address] = optic(_.address)
|
|
62
|
+
val city: Lens[Person, String] = optic(_.address.city)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Set a simple field
|
|
66
|
+
val setName = Patch.set(Person.name, "Alice")
|
|
67
|
+
|
|
68
|
+
// Set a nested field
|
|
69
|
+
val setCity = Patch.set(Person.city, "San Francisco")
|
|
70
|
+
|
|
71
|
+
// Set an entire nested record
|
|
72
|
+
val newAddress = Address("123 Main St", "NYC", "10001")
|
|
73
|
+
val setAddress = Patch.set(Person.address, newAddress)
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
The `set` operation works with all optic types:
|
|
77
|
+
- **Lens** — Sets a single field
|
|
78
|
+
- **Optional** — Sets a value if the path exists
|
|
79
|
+
- **Traversal** — Sets all matching elements to the same value
|
|
80
|
+
- **Prism** — Sets a variant case
|
|
81
|
+
|
|
82
|
+
### Numeric Increments
|
|
83
|
+
|
|
84
|
+
The `increment` operation adds a delta to numeric fields. This is a **commutative operation** — applying increments in any order produces the same result, making it ideal for distributed systems:
|
|
85
|
+
|
|
86
|
+
```scala
|
|
87
|
+
case class Counter(count: Int, total: Long, balance: BigDecimal)
|
|
88
|
+
object Counter extends CompanionOptics[Counter] {
|
|
89
|
+
implicit val schema: Schema[Counter] = Schema.derived
|
|
90
|
+
val count: Lens[Counter, Int] = optic(_.count)
|
|
91
|
+
val total: Lens[Counter, Long] = optic(_.total)
|
|
92
|
+
val balance: Lens[Counter, BigDecimal] = optic(_.balance)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Increment various numeric types
|
|
96
|
+
val addOne = Patch.increment(Counter.count, 1)
|
|
97
|
+
val addTen = Patch.increment(Counter.count, 10)
|
|
98
|
+
val subtractFive = Patch.increment(Counter.count, -5)
|
|
99
|
+
|
|
100
|
+
// Works with all numeric types
|
|
101
|
+
val addToTotal = Patch.increment(Counter.total, 1000L)
|
|
102
|
+
val addToBalance = Patch.increment(Counter.balance, BigDecimal("99.99"))
|
|
103
|
+
|
|
104
|
+
// Compose multiple increments
|
|
105
|
+
val combined = addOne ++ addTen ++ subtractFive
|
|
106
|
+
val counter = Counter(0, 0L, BigDecimal(0))
|
|
107
|
+
combined(counter) // Counter(6, 0, 0)
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Supported numeric types:
|
|
111
|
+
- `Int`, `Long`, `Short`, `Byte`
|
|
112
|
+
- `Float`, `Double`
|
|
113
|
+
- `BigInt`, `BigDecimal`
|
|
114
|
+
|
|
115
|
+
### Temporal Operations
|
|
116
|
+
|
|
117
|
+
Patches support temporal arithmetic with `addDuration` and `addPeriod`:
|
|
118
|
+
|
|
119
|
+
```scala
|
|
120
|
+
import java.time._
|
|
121
|
+
|
|
122
|
+
case class Event(
|
|
123
|
+
timestamp: Instant,
|
|
124
|
+
scheduledDate: LocalDate,
|
|
125
|
+
scheduledTime: LocalDateTime,
|
|
126
|
+
duration: Duration
|
|
127
|
+
)
|
|
128
|
+
object Event extends CompanionOptics[Event] {
|
|
129
|
+
implicit val schema: Schema[Event] = Schema.derived
|
|
130
|
+
val timestamp: Lens[Event, Instant] = optic(_.timestamp)
|
|
131
|
+
val scheduledDate: Lens[Event, LocalDate] = optic(_.scheduledDate)
|
|
132
|
+
val scheduledTime: Lens[Event, LocalDateTime] = optic(_.scheduledTime)
|
|
133
|
+
val duration: Lens[Event, Duration] = optic(_.duration)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Add duration to an Instant
|
|
137
|
+
val postpone1Hour = Patch.addDuration(Event.timestamp, Duration.ofHours(1))
|
|
138
|
+
|
|
139
|
+
// Add period to a LocalDate
|
|
140
|
+
val postpone1Week = Patch.addPeriod(Event.scheduledDate, Period.ofWeeks(1))
|
|
141
|
+
|
|
142
|
+
// Add both period and duration to a LocalDateTime
|
|
143
|
+
val postpone = Patch.addPeriodAndDuration(
|
|
144
|
+
Event.scheduledTime,
|
|
145
|
+
Period.ofDays(1),
|
|
146
|
+
Duration.ofHours(2)
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
// Add duration to a Duration field
|
|
150
|
+
import Patch.DurationDummy.ForDuration
|
|
151
|
+
val extendDuration = Patch.addDuration(Event.duration, Duration.ofMinutes(30))
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### String Edits
|
|
155
|
+
|
|
156
|
+
The `editString` operation applies character-level edits to strings:
|
|
157
|
+
|
|
158
|
+
```scala
|
|
159
|
+
case class Document(content: String)
|
|
160
|
+
object Document extends CompanionOptics[Document] {
|
|
161
|
+
implicit val schema: Schema[Document] = Schema.derived
|
|
162
|
+
val content: Lens[Document, String] = optic(_.content)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Insert text at position
|
|
166
|
+
val insertHello = Patch.editString(
|
|
167
|
+
Document.content,
|
|
168
|
+
Vector(Patch.StringOp.Insert(0, "Hello "))
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
// Delete characters
|
|
172
|
+
val deleteFirst5 = Patch.editString(
|
|
173
|
+
Document.content,
|
|
174
|
+
Vector(Patch.StringOp.Delete(0, 5))
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
// Append text
|
|
178
|
+
val appendBang = Patch.editString(
|
|
179
|
+
Document.content,
|
|
180
|
+
Vector(Patch.StringOp.Append("!"))
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
// Replace a range (delete then insert)
|
|
184
|
+
val replaceWorld = Patch.editString(
|
|
185
|
+
Document.content,
|
|
186
|
+
Vector(Patch.StringOp.Modify(6, 5, "Universe"))
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
// Combine multiple edits (applied in sequence)
|
|
190
|
+
val doc = Document("Hello World")
|
|
191
|
+
val edits = Patch.editString(
|
|
192
|
+
Document.content,
|
|
193
|
+
Vector(
|
|
194
|
+
Patch.StringOp.Delete(5, 6), // "Hello"
|
|
195
|
+
Patch.StringOp.Append(" there!") // "Hello there!"
|
|
196
|
+
)
|
|
197
|
+
)
|
|
198
|
+
edits(doc) // Document("Hello there!")
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
String edit operations:
|
|
202
|
+
- `Insert(index, text)` — Insert text at the given position
|
|
203
|
+
- `Delete(index, length)` — Delete characters starting at index
|
|
204
|
+
- `Append(text)` — Append text to the end
|
|
205
|
+
- `Modify(index, length, text)` — Replace a range with new text
|
|
206
|
+
|
|
207
|
+
## Working with Collections
|
|
208
|
+
|
|
209
|
+
### Appending Elements
|
|
210
|
+
|
|
211
|
+
The `append` operation adds elements to the end of a sequence:
|
|
212
|
+
|
|
213
|
+
```scala
|
|
214
|
+
case class TodoList(items: Vector[String])
|
|
215
|
+
object TodoList extends CompanionOptics[TodoList] {
|
|
216
|
+
implicit val schema: Schema[TodoList] = Schema.derived
|
|
217
|
+
val items: Lens[TodoList, Vector[String]] = optic(_.items)
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
import Patch.CollectionDummy.ForVector
|
|
221
|
+
|
|
222
|
+
val addItems = Patch.append(
|
|
223
|
+
TodoList.items,
|
|
224
|
+
Vector("Buy groceries", "Walk the dog")
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
val list = TodoList(Vector("Existing task"))
|
|
228
|
+
addItems(list) // TodoList(Vector("Existing task", "Buy groceries", "Walk the dog"))
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
Works with `Vector`, `List`, `Seq`, `IndexedSeq`, and `LazyList`. Use the appropriate implicit:
|
|
232
|
+
|
|
233
|
+
```scala
|
|
234
|
+
import Patch.CollectionDummy.ForList
|
|
235
|
+
import Patch.CollectionDummy.ForSeq
|
|
236
|
+
import Patch.CollectionDummy.ForIndexedSeq
|
|
237
|
+
import Patch.CollectionDummy.ForLazyList
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
### Inserting at Index
|
|
241
|
+
|
|
242
|
+
The `insertAt` operation inserts elements at a specific position:
|
|
243
|
+
|
|
244
|
+
```scala
|
|
245
|
+
import Patch.CollectionDummy.ForVector
|
|
246
|
+
|
|
247
|
+
val insertAtStart = Patch.insertAt(
|
|
248
|
+
TodoList.items,
|
|
249
|
+
0,
|
|
250
|
+
Vector("First priority")
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
val insertInMiddle = Patch.insertAt(
|
|
254
|
+
TodoList.items,
|
|
255
|
+
1,
|
|
256
|
+
Vector("Second item", "Third item")
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
val list = TodoList(Vector("A", "B", "C"))
|
|
260
|
+
insertInMiddle(list) // TodoList(Vector("A", "Second item", "Third item", "B", "C"))
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
### Deleting Elements
|
|
264
|
+
|
|
265
|
+
The `deleteAt` operation removes elements starting at an index:
|
|
266
|
+
|
|
267
|
+
```scala
|
|
268
|
+
import Patch.CollectionDummy.ForVector
|
|
269
|
+
|
|
270
|
+
// Delete one element at index 1
|
|
271
|
+
val deleteOne = Patch.deleteAt(TodoList.items, 1, 1)
|
|
272
|
+
|
|
273
|
+
// Delete three elements starting at index 0
|
|
274
|
+
val deleteThree = Patch.deleteAt(TodoList.items, 0, 3)
|
|
275
|
+
|
|
276
|
+
val list = TodoList(Vector("A", "B", "C", "D"))
|
|
277
|
+
deleteOne(list) // TodoList(Vector("A", "C", "D"))
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
### Modifying Elements
|
|
281
|
+
|
|
282
|
+
The `modifyAt` operation applies a nested patch to an element at a specific index:
|
|
283
|
+
|
|
284
|
+
```scala
|
|
285
|
+
case class Task(title: String, priority: Int)
|
|
286
|
+
object Task extends CompanionOptics[Task] {
|
|
287
|
+
implicit val schema: Schema[Task] = Schema.derived
|
|
288
|
+
val title: Lens[Task, String] = optic(_.title)
|
|
289
|
+
val priority: Lens[Task, Int] = optic(_.priority)
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
case class Project(tasks: Vector[Task])
|
|
293
|
+
object Project extends CompanionOptics[Project] {
|
|
294
|
+
implicit val schema: Schema[Project] = Schema.derived
|
|
295
|
+
val tasks: Lens[Project, Vector[Task]] = optic(_.tasks)
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
import Patch.CollectionDummy.ForVector
|
|
299
|
+
|
|
300
|
+
// Create a patch for the nested Task type
|
|
301
|
+
val increasePriority = Patch.increment(Task.priority, 1)
|
|
302
|
+
|
|
303
|
+
// Modify the task at index 0
|
|
304
|
+
val modifyFirst = Patch.modifyAt(Project.tasks, 0, increasePriority)
|
|
305
|
+
|
|
306
|
+
val project = Project(Vector(
|
|
307
|
+
Task("Build feature", 1),
|
|
308
|
+
Task("Write tests", 2)
|
|
309
|
+
))
|
|
310
|
+
modifyFirst(project)
|
|
311
|
+
// Project(Vector(Task("Build feature", 2), Task("Write tests", 2)))
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
## Working with Maps
|
|
315
|
+
|
|
316
|
+
### Adding Keys
|
|
317
|
+
|
|
318
|
+
The `addKey` operation adds a new key-value pair to a map:
|
|
319
|
+
|
|
320
|
+
```scala
|
|
321
|
+
case class Config(settings: Map[String, Int])
|
|
322
|
+
object Config extends CompanionOptics[Config] {
|
|
323
|
+
implicit val schema: Schema[Config] = Schema.derived
|
|
324
|
+
val settings: Lens[Config, Map[String, Int]] = optic(_.settings)
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
val addTimeout = Patch.addKey(Config.settings, "timeout", 30)
|
|
328
|
+
val addRetries = Patch.addKey(Config.settings, "retries", 3)
|
|
329
|
+
|
|
330
|
+
val config = Config(Map("port" -> 8080))
|
|
331
|
+
val combined = addTimeout ++ addRetries
|
|
332
|
+
combined(config) // Config(Map("port" -> 8080, "timeout" -> 30, "retries" -> 3))
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
### Removing Keys
|
|
336
|
+
|
|
337
|
+
The `removeKey` operation removes a key from a map:
|
|
338
|
+
|
|
339
|
+
```scala
|
|
340
|
+
val removePort = Patch.removeKey(Config.settings, "port")
|
|
341
|
+
|
|
342
|
+
val config = Config(Map("port" -> 8080, "timeout" -> 30))
|
|
343
|
+
removePort(config) // Config(Map("timeout" -> 30))
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
### Modifying Values
|
|
347
|
+
|
|
348
|
+
The `modifyKey` operation applies a nested patch to the value at a specific key:
|
|
349
|
+
|
|
350
|
+
```scala
|
|
351
|
+
case class UserSettings(preferences: Map[String, Int])
|
|
352
|
+
object UserSettings extends CompanionOptics[UserSettings] {
|
|
353
|
+
implicit val schema: Schema[UserSettings] = Schema.derived
|
|
354
|
+
val preferences: Lens[UserSettings, Map[String, Int]] = optic(_.preferences)
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Create a patch that increments an Int
|
|
358
|
+
val incrementValue: Patch[Int] = {
|
|
359
|
+
implicit val intSchema: Schema[Int] = Schema[Int]
|
|
360
|
+
Patch(
|
|
361
|
+
DynamicPatch.root(Patch.Operation.PrimitiveDelta(Patch.PrimitiveOp.IntDelta(10))),
|
|
362
|
+
intSchema
|
|
363
|
+
)
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
val increaseVolume = Patch.modifyKey(UserSettings.preferences, "volume", incrementValue)
|
|
367
|
+
|
|
368
|
+
val settings = UserSettings(Map("volume" -> 50, "brightness" -> 80))
|
|
369
|
+
increaseVolume(settings) // UserSettings(Map("volume" -> 60, "brightness" -> 80))
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
## Composing Patches
|
|
373
|
+
|
|
374
|
+
Patches compose with the `++` operator. The result applies the first patch, then the second:
|
|
375
|
+
|
|
376
|
+
```scala
|
|
377
|
+
case class Account(name: String, balance: Int, active: Boolean)
|
|
378
|
+
object Account extends CompanionOptics[Account] {
|
|
379
|
+
implicit val schema: Schema[Account] = Schema.derived
|
|
380
|
+
val name: Lens[Account, String] = optic(_.name)
|
|
381
|
+
val balance: Lens[Account, Int] = optic(_.balance)
|
|
382
|
+
val active: Lens[Account, Boolean] = optic(_.active)
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
val rename = Patch.set(Account.name, "Premium Account")
|
|
386
|
+
val deposit = Patch.increment(Account.balance, 100)
|
|
387
|
+
val activate = Patch.set(Account.active, true)
|
|
388
|
+
|
|
389
|
+
// Compose all patches
|
|
390
|
+
val upgrade = rename ++ deposit ++ activate
|
|
391
|
+
|
|
392
|
+
val account = Account("Basic", 50, false)
|
|
393
|
+
upgrade(account) // Account("Premium Account", 150, true)
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
Composition is associative: `(a ++ b) ++ c` equals `a ++ (b ++ c)`.
|
|
397
|
+
|
|
398
|
+
The empty patch acts as an identity element:
|
|
399
|
+
|
|
400
|
+
```scala
|
|
401
|
+
val empty = Patch.empty[Account]
|
|
402
|
+
val patch = Patch.increment(Account.balance, 100)
|
|
403
|
+
|
|
404
|
+
(patch ++ empty)(account) == patch(account) // true
|
|
405
|
+
(empty ++ patch)(account) == patch(account) // true
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
## Applying Patches
|
|
409
|
+
|
|
410
|
+
### Basic Application
|
|
411
|
+
|
|
412
|
+
The simplest way to apply a patch uses the `apply` method, which uses `Lenient` mode and returns the original value on failure:
|
|
413
|
+
|
|
414
|
+
```scala
|
|
415
|
+
val patch = Patch.increment(Account.balance, 100)
|
|
416
|
+
val account = Account("Test", 50, true)
|
|
417
|
+
|
|
418
|
+
val result: Account = patch(account) // Account("Test", 150, true)
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
### Application Modes
|
|
422
|
+
|
|
423
|
+
For more control, use the overload that takes a `PatchMode`:
|
|
424
|
+
|
|
425
|
+
```scala
|
|
426
|
+
val result: Either[SchemaError, Account] = patch(account, PatchMode.Strict)
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
#### PatchMode.Strict
|
|
430
|
+
|
|
431
|
+
Fails immediately if any operation cannot be applied:
|
|
432
|
+
|
|
433
|
+
```scala
|
|
434
|
+
case class Data(items: Vector[Int])
|
|
435
|
+
object Data extends CompanionOptics[Data] {
|
|
436
|
+
implicit val schema: Schema[Data] = Schema.derived
|
|
437
|
+
val items: Lens[Data, Vector[Int]] = optic(_.items)
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
import Patch.CollectionDummy.ForVector
|
|
441
|
+
|
|
442
|
+
// Try to delete at an invalid index
|
|
443
|
+
val badDelete = Patch.deleteAt(Data.items, 10, 1)
|
|
444
|
+
val data = Data(Vector(1, 2, 3))
|
|
445
|
+
|
|
446
|
+
badDelete(data, PatchMode.Strict)
|
|
447
|
+
// Left(SchemaError(...index out of bounds...))
|
|
448
|
+
|
|
449
|
+
badDelete(data, PatchMode.Lenient)
|
|
450
|
+
// Right(Data(Vector(1, 2, 3))) // Operation skipped
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
Use `Strict` when you need to know if every operation succeeded.
|
|
454
|
+
|
|
455
|
+
#### PatchMode.Lenient
|
|
456
|
+
|
|
457
|
+
Skips operations that fail preconditions and continues with the rest:
|
|
458
|
+
|
|
459
|
+
```scala
|
|
460
|
+
val patch1 = Patch.deleteAt(Data.items, 10, 1) // Will fail
|
|
461
|
+
val patch2 = Patch.increment(Counter.count, 5) // Will succeed
|
|
462
|
+
|
|
463
|
+
val combined = patch1 ++ patch2
|
|
464
|
+
|
|
465
|
+
combined(data, PatchMode.Lenient)
|
|
466
|
+
// Skips the invalid delete, applies the increment
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
Use `Lenient` for best-effort patching where partial success is acceptable.
|
|
470
|
+
|
|
471
|
+
#### PatchMode.Clobber
|
|
472
|
+
|
|
473
|
+
Attempts to force operations through, using fallback behaviors:
|
|
474
|
+
|
|
475
|
+
```scala
|
|
476
|
+
// Adding a key that already exists
|
|
477
|
+
val addExisting = Patch.addKey(Config.settings, "port", 9000)
|
|
478
|
+
val config = Config(Map("port" -> 8080))
|
|
479
|
+
|
|
480
|
+
addExisting(config, PatchMode.Strict)
|
|
481
|
+
// Left(SchemaError(...key already exists...))
|
|
482
|
+
|
|
483
|
+
addExisting(config, PatchMode.Clobber)
|
|
484
|
+
// Right(Config(Map("port" -> 9000))) // Overwrites existing key
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
Use `Clobber` when you want to force updates regardless of preconditions.
|
|
488
|
+
|
|
489
|
+
### Option-Based Application
|
|
490
|
+
|
|
491
|
+
Use `applyOption` for a simpler API that returns `None` on any failure:
|
|
492
|
+
|
|
493
|
+
```scala
|
|
494
|
+
val patch = Patch.increment(Account.balance, 100)
|
|
495
|
+
val result: Option[Account] = patch.applyOption(account)
|
|
496
|
+
// Some(Account("Test", 150, true))
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
## Diffing Values
|
|
500
|
+
|
|
501
|
+
The `diff` method computes a minimal patch that transforms one value into another:
|
|
502
|
+
|
|
503
|
+
```scala
|
|
504
|
+
val old = Person("Alice", 25)
|
|
505
|
+
val new = Person("Alice", 26)
|
|
506
|
+
|
|
507
|
+
// Compute the difference
|
|
508
|
+
val dynamicOld = Schema[Person].toDynamicValue(old)
|
|
509
|
+
val dynamicNew = Schema[Person].toDynamicValue(new)
|
|
510
|
+
val patch = dynamicOld.diff(dynamicNew)
|
|
511
|
+
|
|
512
|
+
// Apply to transform old into new
|
|
513
|
+
patch(dynamicOld, PatchMode.Strict)
|
|
514
|
+
// Right(dynamicNew)
|
|
515
|
+
```
|
|
516
|
+
|
|
517
|
+
The differ produces minimal patches using type-appropriate operations:
|
|
518
|
+
- **Numeric fields** — Uses delta operations (`+1` instead of `set 26`)
|
|
519
|
+
- **Strings** — Uses edit operations when more compact than replacement
|
|
520
|
+
- **Records** — Only includes changed fields
|
|
521
|
+
- **Sequences** — Uses LCS algorithm to compute minimal insert/delete operations
|
|
522
|
+
- **Maps** — Produces add/remove/modify operations for changed entries
|
|
523
|
+
|
|
524
|
+
### Diff Example
|
|
525
|
+
|
|
526
|
+
```scala
|
|
527
|
+
case class User(
|
|
528
|
+
name: String,
|
|
529
|
+
scores: Vector[Int],
|
|
530
|
+
metadata: Map[String, String]
|
|
531
|
+
)
|
|
532
|
+
object User {
|
|
533
|
+
implicit val schema: Schema[User] = Schema.derived
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
val v1 = User(
|
|
537
|
+
"Alice",
|
|
538
|
+
Vector(10, 20, 30),
|
|
539
|
+
Map("level" -> "1", "status" -> "active")
|
|
540
|
+
)
|
|
541
|
+
|
|
542
|
+
val v2 = User(
|
|
543
|
+
"Alice",
|
|
544
|
+
Vector(10, 25, 30, 40),
|
|
545
|
+
Map("level" -> "2", "status" -> "active")
|
|
546
|
+
)
|
|
547
|
+
|
|
548
|
+
val d1 = Schema[User].toDynamicValue(v1)
|
|
549
|
+
val d2 = Schema[User].toDynamicValue(v2)
|
|
550
|
+
val patch = d1.diff(d2)
|
|
551
|
+
|
|
552
|
+
// The patch contains:
|
|
553
|
+
// - scores: delete at index 1, insert 25 at index 1, append 40
|
|
554
|
+
// - metadata["level"]: increment from "1" to "2" (or set if strings)
|
|
555
|
+
```
|
|
556
|
+
|
|
557
|
+
## Serializable Operations
|
|
558
|
+
|
|
559
|
+
All patch operations are serializable through the schema system. Each operation type has an implicit `Schema` instance:
|
|
560
|
+
|
|
561
|
+
```scala
|
|
562
|
+
// Patches can be converted to/from DynamicValue
|
|
563
|
+
val patch = Patch.increment(Account.balance, 100)
|
|
564
|
+
val dynamicPatch = Schema[DynamicPatch].toDynamicValue(patch.dynamicPatch)
|
|
565
|
+
|
|
566
|
+
// Serialize to JSON, Avro, MessagePack, etc.
|
|
567
|
+
import zio.blocks.schema.json._
|
|
568
|
+
val json = JsonEncoder.encode(patch.dynamicPatch)
|
|
569
|
+
```
|
|
570
|
+
|
|
571
|
+
This enables storing patches in databases, sending them over APIs, or logging them for audit purposes.
|
|
572
|
+
|
|
573
|
+
## Operation Reference
|
|
574
|
+
|
|
575
|
+
### Value Operations
|
|
576
|
+
|
|
577
|
+
| Operation | Description |
|
|
578
|
+
|-----------|-------------|
|
|
579
|
+
| `Patch.set(optic, value)` | Set a value at the optic path |
|
|
580
|
+
| `Patch.replace(optic, value)` | Alias for `set` |
|
|
581
|
+
| `Patch.empty[S]` | Empty patch (identity) |
|
|
582
|
+
|
|
583
|
+
### Numeric Operations
|
|
584
|
+
|
|
585
|
+
| Operation | Description |
|
|
586
|
+
|-----------|-------------|
|
|
587
|
+
| `Patch.increment(optic, delta)` | Add delta to numeric field |
|
|
588
|
+
|
|
589
|
+
### Temporal Operations
|
|
590
|
+
|
|
591
|
+
| Operation | Description |
|
|
592
|
+
|-----------|-------------|
|
|
593
|
+
| `Patch.addDuration(optic, duration)` | Add duration to `Instant` or `Duration` |
|
|
594
|
+
| `Patch.addPeriod(optic, period)` | Add period to `LocalDate` or `Period` |
|
|
595
|
+
| `Patch.addPeriodAndDuration(optic, period, duration)` | Add both to `LocalDateTime` |
|
|
596
|
+
|
|
597
|
+
### String Operations
|
|
598
|
+
|
|
599
|
+
| Operation | Description |
|
|
600
|
+
|-----------|-------------|
|
|
601
|
+
| `Patch.editString(optic, edits)` | Apply string edit operations |
|
|
602
|
+
| `StringOp.Insert(index, text)` | Insert text at position |
|
|
603
|
+
| `StringOp.Delete(index, length)` | Delete characters |
|
|
604
|
+
| `StringOp.Append(text)` | Append text |
|
|
605
|
+
| `StringOp.Modify(index, length, text)` | Replace range |
|
|
606
|
+
|
|
607
|
+
### Sequence Operations
|
|
608
|
+
|
|
609
|
+
| Operation | Description |
|
|
610
|
+
|-----------|-------------|
|
|
611
|
+
| `Patch.append(optic, elements)` | Append elements to end |
|
|
612
|
+
| `Patch.insertAt(optic, index, elements)` | Insert at index |
|
|
613
|
+
| `Patch.deleteAt(optic, index, count)` | Delete elements |
|
|
614
|
+
| `Patch.modifyAt(optic, index, patch)` | Apply nested patch at index |
|
|
615
|
+
|
|
616
|
+
### Map Operations
|
|
617
|
+
|
|
618
|
+
| Operation | Description |
|
|
619
|
+
|-----------|-------------|
|
|
620
|
+
| `Patch.addKey(optic, key, value)` | Add key-value pair |
|
|
621
|
+
| `Patch.removeKey(optic, key)` | Remove key |
|
|
622
|
+
| `Patch.modifyKey(optic, key, patch)` | Apply nested patch to value |
|
|
623
|
+
|
|
624
|
+
## Best Practices
|
|
625
|
+
|
|
626
|
+
1. **Use increment for counters** — Increment operations are commutative and merge-friendly
|
|
627
|
+
2. **Prefer fine-grained patches** — Instead of replacing entire records, patch individual fields
|
|
628
|
+
3. **Check isEmpty before applying** — Skip no-op patches for efficiency
|
|
629
|
+
4. **Use Strict mode for validation** — Catch errors early in development
|
|
630
|
+
5. **Use Lenient mode for resilience** — Handle partial updates gracefully in production
|
|
631
|
+
6. **Serialize patches for audit** — Store the patch representation, not just before/after snapshots
|