-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathapply.go
More file actions
252 lines (215 loc) · 6.48 KB
/
apply.go
File metadata and controls
252 lines (215 loc) · 6.48 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
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)
}
// 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
var doc interface{}
if err := json.Unmarshal(toBytes(docJSON), &doc); err != nil {
return zero, fmt.Errorf("failed to decode target document: %w", err)
}
var err error
for i, op := range patch {
doc, err = applyOperation(doc, op)
if err != nil {
return zero, fmt.Errorf("operation %d (%s %s) failed: %w", i, op.Op, op.Path, err)
}
}
result, err := json.Marshal(doc)
if err != nil {
return zero, fmt.Errorf("failed to marshal result: %w", err)
}
return fromBytes[D](result), nil
}
// applyOperation applies a single operation to the document.
func applyOperation(doc interface{}, op Operation) (interface{}, error) {
switch op.Op {
case OpAdd:
return applyAdd(doc, op)
case OpRemove:
return applyRemove(doc, op)
case OpReplace:
return applyReplace(doc, op)
case OpMove:
return applyMove(doc, op)
case OpCopy:
return applyCopy(doc, op)
case OpTest:
return applyTest(doc, op)
default:
return nil, fmt.Errorf("unknown operation %q", op.Op)
}
}
// applyAdd implements the "add" operation (Section 4.1).
func applyAdd(doc interface{}, op Operation) (interface{}, error) {
path, err := ParsePointer(op.Path)
if err != nil {
return nil, err
}
value, err := op.GetValue()
if err != nil {
return nil, err
}
return path.Set(doc, value)
}
// applyRemove implements the "remove" operation (Section 4.2).
func applyRemove(doc interface{}, op Operation) (interface{}, error) {
path, err := ParsePointer(op.Path)
if err != nil {
return nil, err
}
return path.Remove(doc)
}
// applyReplace implements the "replace" operation (Section 4.3).
// Functionally identical to a "remove" followed by "add" at the same location.
func applyReplace(doc interface{}, op Operation) (interface{}, 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]interface{}:
node[key] = value
return doc, nil
case []interface{}:
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 interface{}, op Operation) (interface{}, 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)
}
// Deep copy the value to avoid mutation issues
value = deepCopy(value)
// 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 interface{}, op Operation) (interface{}, 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
value = deepCopy(value)
// Add at the target location
return pathPtr.Set(doc, value)
}
// applyTest implements the "test" operation (Section 4.6).
func applyTest(doc interface{}, op Operation) (interface{}, 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, fmt.Errorf("test failed: value at %q does not match: got %v, expected %v",
op.Path, actual, expected)
}
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]interface{}, etc.).
func jsonEqual(a, b interface{}) bool {
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 interface{}) interface{} {
b, err := json.Marshal(v)
if err != nil {
return v
}
var out interface{}
if err := json.Unmarshal(b, &out); err != nil {
return v
}
return out
}