Skip to content

Commit 147e2b4

Browse files
authored
Merge pull request evanphx#123 from vassilvk/122-extra-support-for-negative-indices
Implement extra support for negative indices
2 parents ce26fd4 + a764fc2 commit 147e2b4

File tree

2 files changed

+77
-22
lines changed

2 files changed

+77
-22
lines changed

v5/patch.go

Lines changed: 42 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,8 @@ type partialDoc map[string]*lazyNode
5555
type partialArray []*lazyNode
5656

5757
type container interface {
58-
get(key string) (*lazyNode, error)
59-
set(key string, val *lazyNode) error
58+
get(key string, options *ApplyOptions) (*lazyNode, error)
59+
set(key string, val *lazyNode, options *ApplyOptions) error
6060
add(key string, val *lazyNode, options *ApplyOptions) error
6161
remove(key string, options *ApplyOptions) error
6262
}
@@ -376,7 +376,7 @@ Loop:
376376
return false
377377
}
378378

379-
func findObject(pd *container, path string) (container, string) {
379+
func findObject(pd *container, path string, options *ApplyOptions) (container, string) {
380380
doc := *pd
381381

382382
split := strings.Split(path, "/")
@@ -393,7 +393,7 @@ func findObject(pd *container, path string) (container, string) {
393393

394394
for _, part := range parts {
395395

396-
next, ok := doc.get(decodePatchKey(part))
396+
next, ok := doc.get(decodePatchKey(part), options)
397397

398398
if next == nil || ok != nil {
399399
return nil, ""
@@ -417,7 +417,7 @@ func findObject(pd *container, path string) (container, string) {
417417
return doc, decodePatchKey(key)
418418
}
419419

420-
func (d *partialDoc) set(key string, val *lazyNode) error {
420+
func (d *partialDoc) set(key string, val *lazyNode, options *ApplyOptions) error {
421421
(*d)[key] = val
422422
return nil
423423
}
@@ -427,7 +427,7 @@ func (d *partialDoc) add(key string, val *lazyNode, options *ApplyOptions) error
427427
return nil
428428
}
429429

430-
func (d *partialDoc) get(key string) (*lazyNode, error) {
430+
func (d *partialDoc) get(key string, options *ApplyOptions) (*lazyNode, error) {
431431
v, ok := (*d)[key]
432432
if !ok {
433433
return v, errors.Wrapf(ErrMissing, "unable to get nonexistent key: %s", key)
@@ -450,12 +450,22 @@ func (d *partialDoc) remove(key string, options *ApplyOptions) error {
450450

451451
// set should only be used to implement the "replace" operation, so "key" must
452452
// be an already existing index in "d".
453-
func (d *partialArray) set(key string, val *lazyNode) error {
453+
func (d *partialArray) set(key string, val *lazyNode, options *ApplyOptions) error {
454454
idx, err := strconv.Atoi(key)
455455
if err != nil {
456456
return err
457457
}
458458

459+
if idx < 0 {
460+
if !options.SupportNegativeIndices {
461+
return errors.Wrapf(ErrInvalidIndex, "Unable to access invalid index: %d", idx)
462+
}
463+
if idx < -len(*d) {
464+
return errors.Wrapf(ErrInvalidIndex, "Unable to access invalid index: %d", idx)
465+
}
466+
idx += len(*d)
467+
}
468+
459469
(*d)[idx] = val
460470
return nil
461471
}
@@ -499,13 +509,23 @@ func (d *partialArray) add(key string, val *lazyNode, options *ApplyOptions) err
499509
return nil
500510
}
501511

502-
func (d *partialArray) get(key string) (*lazyNode, error) {
512+
func (d *partialArray) get(key string, options *ApplyOptions) (*lazyNode, error) {
503513
idx, err := strconv.Atoi(key)
504514

505515
if err != nil {
506516
return nil, err
507517
}
508518

519+
if idx < 0 {
520+
if !options.SupportNegativeIndices {
521+
return nil, errors.Wrapf(ErrInvalidIndex, "Unable to access invalid index: %d", idx)
522+
}
523+
if idx < -len(*d) {
524+
return nil, errors.Wrapf(ErrInvalidIndex, "Unable to access invalid index: %d", idx)
525+
}
526+
idx += len(*d)
527+
}
528+
509529
if idx >= len(*d) {
510530
return nil, errors.Wrapf(ErrInvalidIndex, "Unable to access invalid index: %d", idx)
511531
}
@@ -560,7 +580,7 @@ func (p Patch) add(doc *container, op Operation, options *ApplyOptions) error {
560580
ensurePathExists(doc, path, options)
561581
}
562582

563-
con, key := findObject(doc, path)
583+
con, key := findObject(doc, path, options)
564584

565585
if con == nil {
566586
return errors.Wrapf(ErrMissing, "add operation does not apply: doc is missing path: \"%s\"", path)
@@ -598,7 +618,7 @@ func ensurePathExists(pd *container, path string, options *ApplyOptions) error {
598618
return nil
599619
}
600620

601-
target, ok := doc.get(decodePatchKey(part))
621+
target, ok := doc.get(decodePatchKey(part), options)
602622

603623
if target == nil || ok != nil {
604624

@@ -661,7 +681,7 @@ func (p Patch) remove(doc *container, op Operation, options *ApplyOptions) error
661681
return errors.Wrapf(ErrMissing, "remove operation failed to decode path")
662682
}
663683

664-
con, key := findObject(doc, path)
684+
con, key := findObject(doc, path, options)
665685

666686
if con == nil {
667687
if options.AllowMissingPathOnRemove {
@@ -684,18 +704,18 @@ func (p Patch) replace(doc *container, op Operation, options *ApplyOptions) erro
684704
return errors.Wrapf(err, "replace operation failed to decode path")
685705
}
686706

687-
con, key := findObject(doc, path)
707+
con, key := findObject(doc, path, options)
688708

689709
if con == nil {
690710
return errors.Wrapf(ErrMissing, "replace operation does not apply: doc is missing path: %s", path)
691711
}
692712

693-
_, ok := con.get(key)
713+
_, ok := con.get(key, options)
694714
if ok != nil {
695715
return errors.Wrapf(ErrMissing, "replace operation does not apply: doc is missing key: %s", path)
696716
}
697717

698-
err = con.set(key, op.value())
718+
err = con.set(key, op.value(), options)
699719
if err != nil {
700720
return errors.Wrapf(err, "error in remove for path: '%s'", path)
701721
}
@@ -709,13 +729,13 @@ func (p Patch) move(doc *container, op Operation, options *ApplyOptions) error {
709729
return errors.Wrapf(err, "move operation failed to decode from")
710730
}
711731

712-
con, key := findObject(doc, from)
732+
con, key := findObject(doc, from, options)
713733

714734
if con == nil {
715735
return errors.Wrapf(ErrMissing, "move operation does not apply: doc is missing from path: %s", from)
716736
}
717737

718-
val, err := con.get(key)
738+
val, err := con.get(key, options)
719739
if err != nil {
720740
return errors.Wrapf(err, "error in move for path: '%s'", key)
721741
}
@@ -730,7 +750,7 @@ func (p Patch) move(doc *container, op Operation, options *ApplyOptions) error {
730750
return errors.Wrapf(err, "move operation failed to decode path")
731751
}
732752

733-
con, key = findObject(doc, path)
753+
con, key = findObject(doc, path, options)
734754

735755
if con == nil {
736756
return errors.Wrapf(ErrMissing, "move operation does not apply: doc is missing destination path: %s", path)
@@ -750,13 +770,13 @@ func (p Patch) test(doc *container, op Operation, options *ApplyOptions) error {
750770
return errors.Wrapf(err, "test operation failed to decode path")
751771
}
752772

753-
con, key := findObject(doc, path)
773+
con, key := findObject(doc, path, options)
754774

755775
if con == nil {
756776
return errors.Wrapf(ErrMissing, "test operation does not apply: is missing path: %s", path)
757777
}
758778

759-
val, err := con.get(key)
779+
val, err := con.get(key, options)
760780
if err != nil && errors.Cause(err) != ErrMissing {
761781
return errors.Wrapf(err, "error in test for path: '%s'", path)
762782
}
@@ -783,13 +803,13 @@ func (p Patch) copy(doc *container, op Operation, accumulatedCopySize *int64, op
783803
return errors.Wrapf(err, "copy operation failed to decode from")
784804
}
785805

786-
con, key := findObject(doc, from)
806+
con, key := findObject(doc, from, options)
787807

788808
if con == nil {
789809
return errors.Wrapf(ErrMissing, "copy operation does not apply: doc is missing from path: %s", from)
790810
}
791811

792-
val, err := con.get(key)
812+
val, err := con.get(key, options)
793813
if err != nil {
794814
return errors.Wrapf(err, "error in copy for from: '%s'", from)
795815
}
@@ -799,7 +819,7 @@ func (p Patch) copy(doc *container, op Operation, accumulatedCopySize *int64, op
799819
return errors.Wrapf(ErrMissing, "copy operation failed to decode path")
800820
}
801821

802-
con, key = findObject(doc, path)
822+
con, key = findObject(doc, path, options)
803823

804824
if con == nil {
805825
return errors.Wrapf(ErrMissing, "copy operation does not apply: doc is missing destination path: %s", path)

v5/patch_test.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,20 @@ var Cases = []Case{
118118
false,
119119
false,
120120
},
121+
{
122+
`{ "foo": [ "bar", "qux", "baz" ] }`,
123+
`[ { "op": "remove", "path": "/foo/-1" } ]`,
124+
`{ "foo": [ "bar", "qux" ] }`,
125+
false,
126+
false,
127+
},
128+
{
129+
`{ "foo": [ "bar", "qux", {"a": "abc", "b": "xyz" } ] }`,
130+
`[ { "op": "remove", "path": "/foo/-1/a" } ]`,
131+
`{ "foo": [ "bar", "qux", {"b": "xyz" } ] }`,
132+
false,
133+
false,
134+
},
121135
{
122136
`{ "baz": "qux", "foo": "bar" }`,
123137
`[ { "op": "replace", "path": "/baz", "value": "boo" } ]`,
@@ -197,6 +211,20 @@ var Cases = []Case{
197211
false,
198212
false,
199213
},
214+
{
215+
`{ "foo": ["bar"]}`,
216+
`[ { "op": "replace", "path": "/foo/-1", "value": "baz"}]`,
217+
`{ "foo": ["baz"]}`,
218+
false,
219+
false,
220+
},
221+
{
222+
`{ "foo": [{"bar": "x"}]}`,
223+
`[ { "op": "replace", "path": "/foo/-1/bar", "value": "baz"}]`,
224+
`{ "foo": [{"bar": "baz"}]}`,
225+
false,
226+
false,
227+
},
200228
{
201229
`{ "foo": ["bar","baz"]}`,
202230
`[ { "op": "replace", "path": "/foo/0", "value": "bum"}]`,
@@ -417,6 +445,13 @@ var Cases = []Case{
417445
false,
418446
true,
419447
},
448+
{
449+
`{"a": [{}]}`,
450+
`[ { "op": "add", "path": "/a/-1/b/c", "value": "hello" } ]`,
451+
`{"a": [{"b": {"c": "hello"}}]}`,
452+
false,
453+
true,
454+
},
420455
{
421456
`{"a": [{"b": "whatever"}]}`,
422457
`[ { "op": "add", "path": "/a/2/b/c", "value": "hello" } ]`,

0 commit comments

Comments
 (0)