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

Support nested array wildcard deletion #113

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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
190 changes: 123 additions & 67 deletions gabs.go
Original file line number Diff line number Diff line change
Expand Up @@ -137,46 +137,87 @@ func (g *Container) Data() interface{} {

//------------------------------------------------------------------------------

func (g *Container) searchStrict(allowWildcard bool, hierarchy ...string) (*Container, error) {
object := g.Data()
for target := 0; target < len(hierarchy); target++ {
pathSeg := hierarchy[target]
if mmap, ok := object.(map[string]interface{}); ok {
object, ok = mmap[pathSeg]
if !ok {
return nil, fmt.Errorf("failed to resolve path segment '%v': key '%v' was not found", target, pathSeg)
}
} else if marray, ok := object.([]interface{}); ok {
if allowWildcard && pathSeg == "*" {
tmpArray := []interface{}{}
for _, val := range marray {
if (target + 1) >= len(hierarchy) {
tmpArray = append(tmpArray, val)
} else if res := Wrap(val).Search(hierarchy[target+1:]...); res != nil {
tmpArray = append(tmpArray, res.Data())
const wildcard = "*"

type path struct {
hierarchy []string
object interface{}
}

type paths []path

func (ps paths) Container() *Container {
if len(ps) == 0 {
return nil
}

if len(ps) == 1 {
return &Container{object: ps[0].object}
}

s := make([]interface{}, len(ps))
for i, p := range ps {
s[i] = p.object
}
return &Container{object: s}
}

func (g *Container) searchPaths(allowWildcard bool, hierarchy ...string) (paths, error) {
ps := paths{{object: g.Data()}}

for i, pathSeg := range hierarchy {
var tmpPs paths

for _, p := range ps {
switch val := p.object.(type) {
case map[string]interface{}:
obj, ok := val[pathSeg]
if !ok {
return nil, fmt.Errorf("failed to resolve path segment '%d': key '%v' was not found", i, pathSeg)
}
tmpPs = append(tmpPs, path{
hierarchy: append(p.hierarchy, pathSeg),
object: obj,
})
case []interface{}:
if allowWildcard && pathSeg == wildcard {
for j, v := range val {
newPath := path{
hierarchy: append([]string{}, p.hierarchy...),
object: v,
}
newPath.hierarchy = append(newPath.hierarchy, strconv.Itoa(j))
tmpPs = append(tmpPs, newPath)
}
continue
}
if len(tmpArray) == 0 {
return nil, nil
index, err := strconv.Atoi(pathSeg)
if err != nil {
return nil, fmt.Errorf("failed to resolve path segment '%d': found array but segment value '%s' could not be parsed into array index: %v", i, pathSeg, err)
}
return &Container{tmpArray}, nil
}
index, err := strconv.Atoi(pathSeg)
if err != nil {
return nil, fmt.Errorf("failed to resolve path segment '%v': found array but segment value '%v' could not be parsed into array index: %v", target, pathSeg, err)
}
if index < 0 {
return nil, fmt.Errorf("failed to resolve path segment '%v': found array but index '%v' is invalid", target, pathSeg)
}
if len(marray) <= index {
return nil, fmt.Errorf("failed to resolve path segment '%v': found array but index '%v' exceeded target array size of '%v'", target, pathSeg, len(marray))
if index < 0 {
return nil, fmt.Errorf("failed to resolve path segment '%d': found array but index '%s' is invalid", i, pathSeg)
}
if len(val) <= index {
return nil, fmt.Errorf("failed to resolve path segment '%d': found array but index '%s' exceeded target array size of '%v'", i, pathSeg, len(val))
}
tmpPs = append(tmpPs, path{
hierarchy: append(p.hierarchy, pathSeg),
object: val[index],
})
default:
return nil, fmt.Errorf("failed to resolve path segment '%d': field '%s' was not found", i, pathSeg)
}
object = marray[index]
} else {
return nil, fmt.Errorf("failed to resolve path segment '%v': field '%v' was not found", target, pathSeg)
}

if tmpPs == nil {
return nil, nil
}

ps = tmpPs
}
return &Container{object}, nil

return ps, nil
}

// Search attempts to find and return an object within the wrapped structure by
Expand All @@ -187,8 +228,8 @@ func (g *Container) searchStrict(allowWildcard bool, hierarchy ...string) (*Cont
// character '*', in which case all elements are searched with the remaining
// search hierarchy and the results returned within an array.
func (g *Container) Search(hierarchy ...string) *Container {
c, _ := g.searchStrict(true, hierarchy...)
return c
paths, _ := g.searchPaths(true, hierarchy...)
return paths.Container()
}

// Path searches the wrapped structure following a path in dot notation,
Expand All @@ -213,7 +254,11 @@ func (g *Container) JSONPointer(path string) (*Container, error) {
if err != nil {
return nil, err
}
return g.searchStrict(false, hierarchy...)
paths, err := g.searchPaths(false, hierarchy...)
if err != nil {
return nil, err
}
return paths.Container(), nil
}

// S is a shorthand alias for Search.
Expand Down Expand Up @@ -439,8 +484,7 @@ func (g *Container) ArrayOfSizeI(size, index int) (*Container, error) {
}

// Delete an element at a path, an error is returned if the element does not
// exist or is not an object. In order to remove an array element please use
// ArrayRemove.
// exist or is not an object.
func (g *Container) Delete(hierarchy ...string) error {
if g == nil || g.object == nil {
return ErrNotObj
Expand All @@ -449,38 +493,50 @@ func (g *Container) Delete(hierarchy ...string) error {
return ErrInvalidQuery
}

object := g.object
target := hierarchy[len(hierarchy)-1]
if len(hierarchy) > 1 {
object = g.Search(hierarchy[:len(hierarchy)-1]...).Data()
}

if obj, ok := object.(map[string]interface{}); ok {
if _, ok = obj[target]; !ok {
return ErrNotFound
wildcarded := false
for _, pathSeg := range hierarchy {
if pathSeg == wildcard {
wildcarded = true
break
}
delete(obj, target)
return nil
}
if array, ok := object.([]interface{}); ok {
if len(hierarchy) < 2 {
return errors.New("unable to delete array index at root of path")
}
index, err := strconv.Atoi(target)
if err != nil {
return fmt.Errorf("failed to parse array index '%v': %v", target, err)
}
if index >= len(array) {
return ErrOutOfBounds
}
if index < 0 {
return ErrOutOfBounds

target := hierarchy[len(hierarchy)-1]
paths, _ := g.searchPaths(true, hierarchy[:len(hierarchy)-1]...)

for _, p := range paths {
switch val := p.object.(type) {
case map[string]interface{}:
if _, ok := val[target]; !ok && !wildcarded {
return ErrNotFound
}
delete(val, target)
case []interface{}:
if target == wildcard {
g.Set([]interface{}{}, p.hierarchy...)
continue
}
index, err := strconv.Atoi(target)
if err != nil {
return fmt.Errorf("failed to parse array index '%v': %v", target, err)
}
if index < 0 {
return ErrOutOfBounds
}
if index >= len(val) {
if !wildcarded {
return ErrOutOfBounds
}
continue
}
val = append(val[:index], val[index+1:]...)
g.Set(val, p.hierarchy...)
default:
return ErrNotObjOrArray
}
array = append(array[:index], array[index+1:]...)
g.Set(array, hierarchy[:len(hierarchy)-1]...)
return nil
}
return ErrNotObjOrArray

return nil
}

// DeleteP deletes an element at a path using dot notation, an error is returned
Expand Down
84 changes: 84 additions & 0 deletions gabs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -582,6 +582,90 @@ func TestDeletes(t *testing.T) {
}
}

func TestDeletesTable(t *testing.T) {
testCases := []struct {
name string
path string
input string
output string
}{
{
name: "top level array",
path: `*`,
input: `[{"a":"foo"},{"b":"bar"}]`,
output: `[]`,
},
{
name: "field in object",
path: `b`,
input: `{"a":"foo","b":"bar"}`,
output: `{"a":"foo"}`,
},
{
name: "field in nested object",
path: `b.d`,
input: `{"a":"foo","b":{"c":"bar","d":"baz"}}`,
output: `{"a":"foo","b":{"c":"bar"}}`,
},
{
name: "field in array nested object",
path: `0.b.d`,
input: `[{"a":"foo","b":{"c":"bar","d":"baz"}},{"e":"buz"}]`,
output: `[{"a":"foo","b":{"c":"bar"}},{"e":"buz"}]`,
},
{
name: "field in array of array objects",
path: `0.1.b`,
input: `[[{"a":"foo"},{"b":"bar"}],[{"c":"baz"},{"d":"buz"}]]`,
output: `[[{"a":"foo"},{}],[{"c":"baz"},{"d":"buz"}]]`,
},
{
name: "field in array of array objects",
path: `0.1.b`,
input: `[[{"a":"foo"},{"b":"bar"}],[{"c":"baz"},{"d":"buz"}]]`,
output: `[[{"a":"foo"},{}],[{"c":"baz"},{"d":"buz"}]]`,
},
{
name: "field in array wildcard of array objects",
path: `*.1.b`,
input: `[[{"a":"foo"},{"b":"bar"}],[{"c":"baz"},{"d":"buz"}]]`,
output: `[[{"a":"foo"},{}],[{"c":"baz"},{"d":"buz"}]]`,
},
{
name: "field in array of array wildcard objects",
path: `0.*.b`,
input: `[[{"a":"foo"},{"b":"bar"}],[{"c":"baz"},{"d":"buz"}]]`,
output: `[[{"a":"foo"},{}],[{"c":"baz"},{"d":"buz"}]]`,
},
{
name: "field in array wildcard of array wildcard objects",
path: `*.*.b`,
input: `[[{"a":"foo"},{"b":"bar"}],[{"c":"baz"},{"d":"buz"}]]`,
output: `[[{"a":"foo"},{}],[{"c":"baz"},{"d":"buz"}]]`,
},
{
name: "wildcard in array wildcard of array wildcard objects",
path: `*.*.*`,
input: `[[[{"a":"foo"},{"b":"bar"}]],[[{"c":"baz"},{"d":"buz"}]]]`,
output: `[[[]],[[]]]`,
},
}

for _, test := range testCases {
test := test
t.Run(test.name, func(t *testing.T) {
parsed, err := ParseJSON([]byte(test.input))
if err != nil {
t.Fatal(err)
}
parsed.DeleteP(test.path)
if exp, act := test.output, parsed.String(); exp != act {
t.Errorf("wrong output, expect: %v, actual: %v", exp, act)
}
})
}
}

func TestDeletesWithArrays(t *testing.T) {
rawJSON := `{
"outter":[
Expand Down