-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathapply.go
More file actions
427 lines (386 loc) · 10.6 KB
/
apply.go
File metadata and controls
427 lines (386 loc) · 10.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
package jsonpatch
import (
"encoding/json"
"fmt"
"reflect"
)
// Apply applies a JSON Patch document to a target JSON document.
// Both arguments must be valid JSON encoded as []byte or string (or any type
// with one of those underlying types). The return type matches the input type.
// Operations are applied sequentially; if any operation fails, the entire
// patch is aborted and an error is returned (atomic semantics per RFC 5789).
func Apply[D Document](docJSON, patchJSON D) (D, error) {
var zero D
patch, err := DecodePatch(patchJSON)
if err != nil {
return zero, err
}
return ApplyPatch(docJSON, patch)
}
// ApplyWithOptions is like Apply but accepts functional options.
func ApplyWithOptions[D Document](docJSON, patchJSON D, opts ...Option) (D, error) {
var zero D
patch, err := DecodePatch(patchJSON)
if err != nil {
return zero, err
}
return ApplyPatchWithOptions(docJSON, patch, opts...)
}
// ApplyPatch applies a decoded Patch to a target JSON document.
// The document can be []byte or string (or any type with one of those
// underlying types). The return type matches the input type.
func ApplyPatch[D Document](docJSON D, patch Patch) (D, error) {
var zero D
result, err := applyPatchInternal(toBytes(docJSON), patch, defaultOptions())
if err != nil {
return zero, err
}
return fromBytes[D](result), nil
}
// ApplyPatchWithOptions is like ApplyPatch but accepts functional options.
func ApplyPatchWithOptions[D Document](docJSON D, patch Patch, opts ...Option) (D, error) {
var zero D
result, err := applyPatchInternal(toBytes(docJSON), patch, buildOptions(opts))
if err != nil {
return zero, err
}
return fromBytes[D](result), nil
}
// applyPatchInternal is the shared implementation for ApplyPatch and ApplyPatchWithOptions.
func applyPatchInternal(docJSON []byte, patch Patch, opts ApplyOptions) ([]byte, error) {
var doc any
if err := json.Unmarshal(docJSON, &doc); err != nil {
return nil, fmt.Errorf("failed to decode target document: %w", err)
}
var err error
for i := range patch {
doc, err = applyOperation(doc, &patch[i], opts)
if err != nil {
return nil, &InvalidOperationError{
Index: i,
Op: patch[i].Op,
Path: patch[i].Path,
Cause: err,
}
}
}
result, err := json.Marshal(doc)
if err != nil {
return nil, fmt.Errorf("failed to marshal result: %w", err)
}
return result, nil
}
// applyOperation applies a single operation to the document.
func applyOperation(doc any, op *Operation, opts ApplyOptions) (any, error) {
switch op.Op {
case OpAdd:
return applyAdd(doc, op, opts)
case OpRemove:
return applyRemove(doc, op, opts)
case OpReplace:
return applyReplace(doc, op, opts)
case OpMove:
return applyMove(doc, op, opts)
case OpCopy:
return applyCopy(doc, op, opts)
case OpTest:
return applyTest(doc, op, opts)
default:
return nil, fmt.Errorf("unknown operation %q", op.Op)
}
}
// applyAdd implements the "add" operation (Section 4.1).
func applyAdd(doc any, op *Operation, opts ApplyOptions) (any, error) {
var path Pointer
if op.cache != nil {
path = op.cache.parsedPath
} else {
var err error
path, err = ParsePointer(op.Path)
if err != nil {
return nil, err
}
}
value, err := op.GetValue()
if err != nil {
return nil, err
}
if opts.EnsurePathExistsOnAdd {
doc = ensurePathExists(doc, path)
}
return path.Set(doc, value)
}
// applyRemove implements the "remove" operation (Section 4.2).
func applyRemove(doc any, op *Operation, opts ApplyOptions) (any, error) {
var path Pointer
if op.cache != nil {
path = op.cache.parsedPath
} else {
var err error
path, err = ParsePointer(op.Path)
if err != nil {
return nil, err
}
}
result, err := path.Remove(doc)
if err != nil && opts.AllowMissingPathOnRemove {
if isMissingTarget(err) {
return doc, nil
}
}
return result, err
}
// isMissingTarget reports whether err represents a missing target location —
// either a PathNotFoundError or an IndexOutOfBoundsError.
// Both are treated as no-ops under AllowMissingPathOnRemove.
func isMissingTarget(err error) bool {
for e := err; e != nil; {
switch e.(type) {
case *PathNotFoundError, *IndexOutOfBoundsError:
return true
}
u, ok := e.(interface{ Unwrap() error })
if !ok {
return false
}
e = u.Unwrap()
}
return false
}
// applyReplace implements the "replace" operation (Section 4.3).
// Functionally identical to a "remove" followed by "add" at the same location.
func applyReplace(doc any, op *Operation, opts ApplyOptions) (any, error) {
var path Pointer
if op.cache != nil {
path = op.cache.parsedPath
} else {
var err error
path, err = ParsePointer(op.Path)
if err != nil {
return nil, err
}
}
// Verify the target exists
if _, err := path.Evaluate(doc); err != nil {
return nil, fmt.Errorf("target location does not exist: %w", err)
}
value, err := op.GetValue()
if err != nil {
return nil, err
}
// For replace on an object member, we set directly (replaces existing).
// For replace on an array element, we need to replace in-place (not insert).
if path.IsRoot() {
return value, nil
}
parent, err := path.Parent().Evaluate(doc)
if err != nil {
return nil, err
}
key := path.Last()
switch node := parent.(type) {
case map[string]any:
node[key] = value
return doc, nil
case []any:
idx, err := resolveArrayIndex(key, len(node))
if err != nil {
return nil, err
}
node[idx] = value
return doc, nil
default:
return nil, fmt.Errorf("cannot replace value in %T", parent)
}
}
// applyMove implements the "move" operation (Section 4.4).
// Functionally identical to "remove" from the source, then "add" at the target.
func applyMove(doc any, op *Operation, opts ApplyOptions) (any, error) {
var fromPtr, pathPtr Pointer
if op.cache != nil {
fromPtr = op.cache.parsedFrom
pathPtr = op.cache.parsedPath
} else {
var err error
fromPtr, err = ParsePointer(op.From)
if err != nil {
return nil, err
}
pathPtr, err = ParsePointer(op.Path)
if err != nil {
return nil, err
}
}
// The "from" location MUST NOT be a proper prefix of the "path" location
if fromPtr.IsPrefixOf(pathPtr) {
return nil, fmt.Errorf("\"from\" location %q must not be a proper prefix of \"path\" location %q",
op.From, op.Path)
}
// Get the value at the "from" location
value, err := fromPtr.Evaluate(doc)
if err != nil {
return nil, fmt.Errorf("\"from\" location does not exist: %w", err)
}
// No deep copy needed: Remove either calls delete(node, key) for maps
// (which doesn't invalidate the value reference) or constructs a new
// backing slice for arrays — in both cases the original reference is valid.
// Remove from the source
doc, err = fromPtr.Remove(doc)
if err != nil {
return nil, err
}
// Add to the target
return pathPtr.Set(doc, value)
}
// applyCopy implements the "copy" operation (Section 4.5).
// Functionally identical to an "add" operation using the value from "from".
func applyCopy(doc any, op *Operation, opts ApplyOptions) (any, error) {
var fromPtr, pathPtr Pointer
if op.cache != nil {
fromPtr = op.cache.parsedFrom
pathPtr = op.cache.parsedPath
} else {
var err error
fromPtr, err = ParsePointer(op.From)
if err != nil {
return nil, err
}
pathPtr, err = ParsePointer(op.Path)
if err != nil {
return nil, err
}
}
// Get the value at the "from" location
value, err := fromPtr.Evaluate(doc)
if err != nil {
return nil, fmt.Errorf("\"from\" location does not exist: %w", err)
}
// Deep copy the value — copy shares a value between two locations, so
// mutation through one path could affect the other.
value = deepCopy(value)
// Add at the target location
return pathPtr.Set(doc, value)
}
// applyTest implements the "test" operation (Section 4.6).
func applyTest(doc any, op *Operation, opts ApplyOptions) (any, error) {
var path Pointer
if op.cache != nil {
path = op.cache.parsedPath
} else {
var err error
path, err = ParsePointer(op.Path)
if err != nil {
return nil, err
}
}
// Get the value at the target location
actual, err := path.Evaluate(doc)
if err != nil {
return nil, fmt.Errorf("target location does not exist: %w", err)
}
// Get the expected value
expected, err := op.GetValue()
if err != nil {
return nil, err
}
// Compare values using deep equality
if !jsonEqual(actual, expected) {
return nil, &TestFailedError{
Path: op.Path,
Expected: expected,
Actual: actual,
}
}
return doc, nil
}
// jsonEqual compares two JSON-compatible values for equality per RFC 6902 Section 4.6.
// All callers are expected to pass values already produced by encoding/json
// (i.e., numbers are float64, maps are map[string]any, etc.).
// Uses a recursive type-switch to avoid reflection overhead.
func jsonEqual(a, b any) bool {
switch av := a.(type) {
case nil:
return b == nil
case bool:
bv, ok := b.(bool)
return ok && av == bv
case float64:
bv, ok := b.(float64)
return ok && av == bv
case string:
bv, ok := b.(string)
return ok && av == bv
case map[string]any:
bv, ok := b.(map[string]any)
if !ok || len(av) != len(bv) {
return false
}
for k, va := range av {
vb, exists := bv[k]
if !exists || !jsonEqual(va, vb) {
return false
}
}
return true
case []any:
bv, ok := b.([]any)
if !ok || len(av) != len(bv) {
return false
}
for i, va := range av {
if !jsonEqual(va, bv[i]) {
return false
}
}
return true
default:
return reflect.DeepEqual(a, b)
}
}
// normalizeJSON normalizes a value by round-tripping through JSON serialization.
// This ensures consistent types (e.g., all numbers become float64).
// Used by CreatePatchFromValues to normalize caller-supplied values.
func normalizeJSON(v any) any {
b, err := json.Marshal(v)
if err != nil {
return v
}
var out any
if err := json.Unmarshal(b, &out); err != nil {
return v
}
return out
}
// ensurePathExists creates intermediate objects along the pointer's parent
// path so that a subsequent Set will not fail due to a missing parent.
// Only object (map) intermediates are created; array intermediates are not.
func ensurePathExists(doc any, ptr Pointer) any {
if ptr.IsRoot() {
return doc
}
if doc == nil {
doc = make(map[string]any)
}
if len(ptr.tokens) <= 1 {
return doc
}
current := doc
// Walk all tokens except the last (which is the key being added).
for _, token := range ptr.tokens[:len(ptr.tokens)-1] {
switch node := current.(type) {
case map[string]any:
next, ok := node[token]
if !ok {
child := make(map[string]any)
node[token] = child
current = child
} else {
current = next
}
default:
// Cannot create intermediates inside arrays or scalars.
return doc
}
}
return doc
}