Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement extra support for negative indices #123

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 42 additions & 22 deletions v5/patch.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,8 @@ type partialDoc map[string]*lazyNode
type partialArray []*lazyNode

type container interface {
get(key string) (*lazyNode, error)
set(key string, val *lazyNode) error
get(key string, options *ApplyOptions) (*lazyNode, error)
set(key string, val *lazyNode, options *ApplyOptions) error
add(key string, val *lazyNode, options *ApplyOptions) error
remove(key string, options *ApplyOptions) error
}
Expand Down Expand Up @@ -376,7 +376,7 @@ Loop:
return false
}

func findObject(pd *container, path string) (container, string) {
func findObject(pd *container, path string, options *ApplyOptions) (container, string) {
doc := *pd

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

for _, part := range parts {

next, ok := doc.get(decodePatchKey(part))
next, ok := doc.get(decodePatchKey(part), options)

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

func (d *partialDoc) set(key string, val *lazyNode) error {
func (d *partialDoc) set(key string, val *lazyNode, options *ApplyOptions) error {
(*d)[key] = val
return nil
}
Expand All @@ -427,7 +427,7 @@ func (d *partialDoc) add(key string, val *lazyNode, options *ApplyOptions) error
return nil
}

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

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

if idx < 0 {
if !options.SupportNegativeIndices {
return errors.Wrapf(ErrInvalidIndex, "Unable to access invalid index: %d", idx)
}
if idx < -len(*d) {
return errors.Wrapf(ErrInvalidIndex, "Unable to access invalid index: %d", idx)
}
idx += len(*d)
}

(*d)[idx] = val
return nil
}
Expand Down Expand Up @@ -499,13 +509,23 @@ func (d *partialArray) add(key string, val *lazyNode, options *ApplyOptions) err
return nil
}

func (d *partialArray) get(key string) (*lazyNode, error) {
func (d *partialArray) get(key string, options *ApplyOptions) (*lazyNode, error) {
idx, err := strconv.Atoi(key)

if err != nil {
return nil, err
}

if idx < 0 {
if !options.SupportNegativeIndices {
return nil, errors.Wrapf(ErrInvalidIndex, "Unable to access invalid index: %d", idx)
}
if idx < -len(*d) {
return nil, errors.Wrapf(ErrInvalidIndex, "Unable to access invalid index: %d", idx)
}
idx += len(*d)
}

if idx >= len(*d) {
return nil, errors.Wrapf(ErrInvalidIndex, "Unable to access invalid index: %d", idx)
}
Expand Down Expand Up @@ -560,7 +580,7 @@ func (p Patch) add(doc *container, op Operation, options *ApplyOptions) error {
ensurePathExists(doc, path, options)
}

con, key := findObject(doc, path)
con, key := findObject(doc, path, options)

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

target, ok := doc.get(decodePatchKey(part))
target, ok := doc.get(decodePatchKey(part), options)

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

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

con, key := findObject(doc, path)
con, key := findObject(doc, path, options)

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

con, key := findObject(doc, path)
con, key := findObject(doc, path, options)

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

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

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

con, key := findObject(doc, from)
con, key := findObject(doc, from, options)

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

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

con, key = findObject(doc, path)
con, key = findObject(doc, path, options)

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

con, key := findObject(doc, path)
con, key := findObject(doc, path, options)

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

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

con, key := findObject(doc, from)
con, key := findObject(doc, from, options)

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

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

con, key = findObject(doc, path)
con, key = findObject(doc, path, options)

if con == nil {
return errors.Wrapf(ErrMissing, "copy operation does not apply: doc is missing destination path: %s", path)
Expand Down
35 changes: 35 additions & 0 deletions v5/patch_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,20 @@ var Cases = []Case{
false,
false,
},
{
`{ "foo": [ "bar", "qux", "baz" ] }`,
`[ { "op": "remove", "path": "/foo/-1" } ]`,
`{ "foo": [ "bar", "qux" ] }`,
false,
false,
},
{
`{ "foo": [ "bar", "qux", {"a": "abc", "b": "xyz" } ] }`,
`[ { "op": "remove", "path": "/foo/-1/a" } ]`,
`{ "foo": [ "bar", "qux", {"b": "xyz" } ] }`,
false,
false,
},
{
`{ "baz": "qux", "foo": "bar" }`,
`[ { "op": "replace", "path": "/baz", "value": "boo" } ]`,
Expand Down Expand Up @@ -197,6 +211,20 @@ var Cases = []Case{
false,
false,
},
{
`{ "foo": ["bar"]}`,
`[ { "op": "replace", "path": "/foo/-1", "value": "baz"}]`,
`{ "foo": ["baz"]}`,
false,
false,
},
{
`{ "foo": [{"bar": "x"}]}`,
`[ { "op": "replace", "path": "/foo/-1/bar", "value": "baz"}]`,
`{ "foo": [{"bar": "baz"}]}`,
false,
false,
},
{
`{ "foo": ["bar","baz"]}`,
`[ { "op": "replace", "path": "/foo/0", "value": "bum"}]`,
Expand Down Expand Up @@ -417,6 +445,13 @@ var Cases = []Case{
false,
true,
},
{
`{"a": [{}]}`,
`[ { "op": "add", "path": "/a/-1/b/c", "value": "hello" } ]`,
`{"a": [{"b": {"c": "hello"}}]}`,
false,
true,
},
{
`{"a": [{"b": "whatever"}]}`,
`[ { "op": "add", "path": "/a/2/b/c", "value": "hello" } ]`,
Expand Down