Skip to content

Commit 880a8c8

Browse files
committed
Set,Delete: fix missing materialization
Apply materialization when modifying a document based on raw JSON (json.RawMessage, json.Decoder). This fixes Set/Delete that didn't properly apply the modification from root. Note that, for now, documents are fully decoded. We do not apply layered decoding yet.
1 parent 2e9ac93 commit 880a8c8

2 files changed

Lines changed: 161 additions & 3 deletions

File tree

‎jsonptr.go‎

Lines changed: 71 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -268,8 +268,70 @@ func Get(doc interface{}, ptr string) (interface{}, error) {
268268
return doc, err
269269
}
270270

271+
// materialize walks *pdoc along ptr[p:] (ptr[:p] is the already walked part)
272+
// and replaces in place any [encoding/json.RawMessage] or JSONDecoder found on
273+
// the way (including at the end of the path) by its decoded value.
274+
// Once done, the value at ptr (if any) is attached to the tree at *pdoc and
275+
// can be modified in place.
276+
//
277+
// Only JSON decoding errors are reported: navigation errors are left for Get
278+
// to report.
279+
func materialize(pdoc *interface{}, ptr string, p int) error {
280+
switch raw := (*pdoc).(type) {
281+
case json.RawMessage:
282+
var v interface{}
283+
if err := json.Unmarshal(raw, &v); err != nil {
284+
return jsonError(ptr[:p], err)
285+
}
286+
*pdoc = v
287+
case JSONDecoder:
288+
var v interface{}
289+
if err := raw.Decode(&v); err != nil {
290+
return jsonError(ptr[:p], err)
291+
}
292+
*pdoc = v
293+
}
294+
295+
if p >= len(ptr) {
296+
return nil
297+
}
298+
// ptr[p] == '/'
299+
p++
300+
q := strings.IndexByte(ptr[p:], '/')
301+
if q == -1 {
302+
q = len(ptr) - p
303+
}
304+
token := ptr[p : p+q]
305+
p += q
306+
307+
switch here := (*pdoc).(type) {
308+
case map[string]interface{}:
309+
key, err := UnescapeString(token)
310+
if err != nil {
311+
return nil
312+
}
313+
v, ok := here[key]
314+
if !ok {
315+
return nil
316+
}
317+
err = materialize(&v, ptr, p)
318+
here[key] = v
319+
return err
320+
case []interface{}:
321+
n, err := arrayIndex(token)
322+
if err != nil || n < 0 || n >= len(here) {
323+
return nil
324+
}
325+
return materialize(&here[n], ptr, p)
326+
}
327+
return nil
328+
}
329+
271330
// Set modifies a JSON-like data tree.
272331
//
332+
// Any [encoding/json.RawMessage] or JSONDecoder on the path to the value
333+
// is replaced in the tree by its decoded value.
334+
//
273335
// In case of error a PtrError is returned.
274336
func Set(doc *interface{}, ptr string, value interface{}) error {
275337
if len(ptr) == 0 {
@@ -283,13 +345,13 @@ func Set(doc *interface{}, ptr string, value interface{}) error {
283345
prop := ptr[p+1:]
284346
parentPtr := ptr[:p]
285347

348+
if err := materialize(doc, parentPtr, 0); err != nil {
349+
return err
350+
}
286351
parent, err := Get(*doc, parentPtr)
287352
if err != nil {
288353
return err
289354
}
290-
if len(parentPtr) == 0 {
291-
*doc = parent
292-
}
293355

294356
switch parent := (parent).(type) {
295357
case map[string]interface{}:
@@ -336,6 +398,9 @@ func Set(doc *interface{}, ptr string, value interface{}) error {
336398

337399
// Delete removes an object property or an array element (and shifts remaining ones).
338400
// It can't be applied on root.
401+
//
402+
// Any [encoding/json.RawMessage] or JSONDecoder on the path to the value
403+
// is replaced in the tree by its decoded value.
339404
func Delete(pdoc *interface{}, ptr string) (interface{}, error) {
340405
if len(ptr) == 0 {
341406
return nil, &BadPointerError{ptr, ErrDeleteRoot}
@@ -348,6 +413,9 @@ func Delete(pdoc *interface{}, ptr string) (interface{}, error) {
348413
prop := ptr[p+1:]
349414
parentPtr := ptr[:p]
350415

416+
if err := materialize(pdoc, parentPtr, 0); err != nil {
417+
return nil, err
418+
}
351419
parent, err := Get(*pdoc, parentPtr)
352420
if err != nil {
353421
return nil, err

‎jsonptr_test.go‎

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,4 +237,94 @@ func TestSet(t *testing.T) {
237237
checkSet(t, `[0,1]`, `/-`, true, `[0,1,true]`)
238238
checkSet(t, `{}`, `/ok`, true, `{"ok":true}`)
239239
checkSet(t, `{"x":[]}`, `/x/-`, true, `{"x":[true]}`)
240+
241+
// json.RawMessage nested in the tree: it must be decoded and stored in
242+
// the tree before being modified
243+
checkSet(t, map[string]interface{}{"a": json.RawMessage(`{}`)}, `/a/b`, 1, `{"a":{"b":1}}`)
244+
checkSet(t, map[string]interface{}{"a": json.RawMessage(`{"x":{"y":[]}}`)}, `/a/x/y/-`, true, `{"a":{"x":{"y":[true]}}}`)
245+
checkSet(t, map[string]interface{}{"a": json.RawMessage(`[]`)}, `/a/-`, true, `{"a":[true]}`)
246+
checkSet(t, []interface{}{json.RawMessage(`[1]`)}, `/0/-`, 2, `[[1,2]]`)
247+
checkSet(t, []interface{}{json.RawMessage(`[1]`)}, `/0/0`, 2, `[[2]]`)
248+
checkSet(t, map[string]interface{}{"a": json.RawMessage(`{"b":{"c":1}}`)}, `/a/b/c`, 2, `{"a":{"b":{"c":2}}}`)
249+
// Replacing the RawMessage itself
250+
checkSet(t, map[string]interface{}{"a": json.RawMessage(`{}`)}, `/a`, 1, `{"a":1}`)
251+
// Nested streamed decoder
252+
checkSet(t, map[string]interface{}{"a": json.NewDecoder(strings.NewReader(`{"x":[1]}`))}, `/a/x/-`, 2, `{"a":{"x":[1,2]}}`)
253+
// Nested in a RawMessage root
254+
checkSet(t, `{"a":{"b":[]}}`, `/a/b/0`, true, `{"a":{"b":[true]}}`)
255+
256+
// Invalid JSON in a nested RawMessage
257+
doc := interface{}(map[string]interface{}{"a": json.RawMessage(`{`)})
258+
err := jsonptr.Set(&doc, `/a/b`, 1)
259+
var docErr *jsonptr.DocumentError
260+
if !errors.As(err, &docErr) {
261+
t.Errorf("invalid nested JSON: got %T %v, want *DocumentError", err, err)
262+
}
263+
}
264+
265+
func checkDelete(t *testing.T, data interface{}, ptr string, expectedValue interface{}, jsonOut string) {
266+
t.Logf("%#v - \"%v\"", data, ptr)
267+
got, err := jsonptr.Delete(&data, ptr)
268+
if err != nil {
269+
t.Errorf(" unexpected error: %s", err)
270+
return
271+
}
272+
if !reflect.DeepEqual(got, expectedValue) {
273+
t.Errorf(" deleted value: got %T %v, want %T %v", got, got, expectedValue, expectedValue)
274+
}
275+
out, err := json.Marshal(data)
276+
if err != nil {
277+
t.Errorf(" can't marshal output: %s", err)
278+
return
279+
}
280+
if string(out) != jsonOut {
281+
t.Errorf(" got %s, want %s", out, jsonOut)
282+
}
283+
}
284+
285+
func TestDelete(t *testing.T) {
286+
checkDelete(t, map[string]interface{}{"a": 1, "b": 2}, `/a`, 1, `{"b":2}`)
287+
checkDelete(t, []interface{}{1, 2, 3}, `/1`, 2, `[1,3]`)
288+
checkDelete(t, []interface{}{1, 2, 3}, `/2`, 3, `[1,2]`)
289+
checkDelete(t, []interface{}{1}, `/0`, 1, `[]`)
290+
checkDelete(t, map[string]interface{}{"a": []interface{}{1, 2}}, `/a/0`, 1, `{"a":[2]}`)
291+
292+
// json.RawMessage in the tree, at root or nested
293+
checkDelete(t, json.RawMessage(`{"a":1,"b":2}`), `/a`, float64(1), `{"b":2}`)
294+
checkDelete(t, json.RawMessage(`[1,2,3]`), `/1`, float64(2), `[1,3]`)
295+
checkDelete(t, map[string]interface{}{"a": json.RawMessage(`{"b":1,"c":2}`)}, `/a/b`, float64(1), `{"a":{"c":2}}`)
296+
checkDelete(t, map[string]interface{}{"a": json.RawMessage(`[1,2,3]`)}, `/a/1`, float64(2), `{"a":[1,3]}`)
297+
checkDelete(t, map[string]interface{}{"a": json.RawMessage(`{"b":{"c":[true]}}`)}, `/a/b/c/0`, true, `{"a":{"b":{"c":[]}}}`)
298+
checkDelete(t, map[string]interface{}{"a": json.NewDecoder(strings.NewReader(`{"b":1,"c":2}`))}, `/a/b`, float64(1), `{"a":{"c":2}}`)
299+
300+
// Errors
301+
for _, test := range []struct {
302+
doc interface{}
303+
ptr string
304+
err error
305+
}{
306+
{map[string]interface{}{}, ``, jsonptr.ErrDeleteRoot},
307+
{map[string]interface{}{}, `a`, jsonptr.ErrSyntax},
308+
{map[string]interface{}{}, `/a`, jsonptr.ErrProperty},
309+
{map[string]interface{}{"a": 1}, `/a/b`, nil}, // DocumentError
310+
{[]interface{}{1}, `/1`, jsonptr.ErrIndex},
311+
{[]interface{}{1}, `/-`, jsonptr.ErrIndex},
312+
{[]interface{}{1}, `/x`, jsonptr.ErrSyntax},
313+
{map[string]interface{}{"a": json.RawMessage(`{`)}, `/a/b`, nil}, // DocumentError
314+
} {
315+
doc := test.doc
316+
_, err := jsonptr.Delete(&doc, test.ptr)
317+
if err == nil {
318+
t.Errorf("%#v - %q: expected error", test.doc, test.ptr)
319+
continue
320+
}
321+
if test.err == nil {
322+
var docErr *jsonptr.DocumentError
323+
if !errors.As(err, &docErr) {
324+
t.Errorf("%#v - %q: got %T %v, want *DocumentError", test.doc, test.ptr, err, err)
325+
}
326+
} else if !errors.Is(err, test.err) {
327+
t.Errorf("%#v - %q: got %T %v, want %v", test.doc, test.ptr, err, err, test.err)
328+
}
329+
}
240330
}

0 commit comments

Comments
 (0)