Skip to content
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
9 changes: 9 additions & 0 deletions patch/pointer.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,12 @@ func NewPointerFromString(str string) (Pointer, error) {
continue
}

// parse wildcard
if tok == "*" {
tokens = append(tokens, WildcardToken{})
continue
}

// parse as index
idx, err := strconv.Atoi(tok)
if err == nil {
Expand Down Expand Up @@ -118,6 +124,9 @@ func (p Pointer) String() string {
case IndexToken:
strs = append(strs, fmt.Sprintf("%d", typedToken.Index))

case WildcardToken:
strs = append(strs, "*")

case AfterLastIndexToken:
strs = append(strs, "-")

Expand Down
88 changes: 63 additions & 25 deletions patch/remove_op.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,31 @@ func (op RemoveOp) Apply(doc interface{}) (interface{}, error) {
return nil, fmt.Errorf("Cannot remove entire document")
}

obj := doc
prevUpdate := func(newObj interface{}) { doc = newObj }
ctxStack := []*mutationCtx{&mutationCtx{
PrevUpdate: func(newObj interface{}) { doc = newObj },
I: 0,
Obj: doc,
}}
for len(ctxStack) != 0 {
// Pop the next context off the stack
ctx := ctxStack[len(ctxStack)-1]
ctxStack = ctxStack[:len(ctxStack)-1]

// Terminate if done
if ctx.I+1 >= len(tokens) {
continue
}

for i, token := range tokens[1:] {
isLast := i == len(tokens)-2
token := tokens[ctx.I+1]
isLast := ctx.I == len(tokens)-2

switch typedToken := token.(type) {
case IndexToken:
idx := typedToken.Index

typedObj, ok := obj.([]interface{})
typedObj, ok := ctx.Obj.([]interface{})
if !ok {
return nil, newOpArrayMismatchTypeErr(tokens[:i+2], obj)
return nil, newOpArrayMismatchTypeErr(tokens[:ctx.I+2], ctx.Obj)
}

if idx >= len(typedObj) {
Expand All @@ -38,16 +50,19 @@ func (op RemoveOp) Apply(doc interface{}) (interface{}, error) {
var newAry []interface{}
newAry = append(newAry, typedObj[:idx]...)
newAry = append(newAry, typedObj[idx+1:]...)
prevUpdate(newAry)
ctx.PrevUpdate(newAry)
} else {
obj = typedObj[idx]
prevUpdate = func(newObj interface{}) { typedObj[idx] = newObj }
ctxStack = append(ctxStack, &mutationCtx{
Obj: typedObj[idx],
PrevUpdate: func(newObj interface{}) { typedObj[idx] = newObj },
I: ctx.I + 1,
})
}

case MatchingIndexToken:
typedObj, ok := obj.([]interface{})
typedObj, ok := ctx.Obj.([]interface{})
if !ok {
return nil, newOpArrayMismatchTypeErr(tokens[:i+2], obj)
return nil, newOpArrayMismatchTypeErr(tokens[:ctx.I+2], ctx.Obj)
}

var idxs []int
Expand All @@ -62,11 +77,11 @@ func (op RemoveOp) Apply(doc interface{}) (interface{}, error) {
}

if typedToken.Optional && len(idxs) == 0 {
return doc, nil
continue // don't exit early
}

if len(idxs) != 1 {
return nil, opMultipleMatchingIndexErr{NewPointer(tokens[:i+2]), idxs}
return nil, opMultipleMatchingIndexErr{NewPointer(tokens[:ctx.I+2]), idxs}
}

idx := idxs[0]
Expand All @@ -75,37 +90,60 @@ func (op RemoveOp) Apply(doc interface{}) (interface{}, error) {
var newAry []interface{}
newAry = append(newAry, typedObj[:idx]...)
newAry = append(newAry, typedObj[idx+1:]...)
prevUpdate(newAry)
ctx.PrevUpdate(newAry)
} else {
obj = typedObj[idx]
// no need to change prevUpdate since matching item can only be a map
ctxStack = append(ctxStack, &mutationCtx{
Obj: typedObj[idx],
PrevUpdate: ctx.PrevUpdate, // no need to change prevUpdate since matching item can only be a map
I: ctx.I + 1,
})
}

case KeyToken:
typedObj, ok := obj.(map[interface{}]interface{})
typedObj, ok := ctx.Obj.(map[interface{}]interface{})
if !ok {
return nil, newOpMapMismatchTypeErr(tokens[:i+2], obj)
return nil, newOpMapMismatchTypeErr(tokens[:ctx.I+2], ctx.Obj)
}

var found bool

obj, found = typedObj[typedToken.Key]
o, found := typedObj[typedToken.Key]
if !found {
if typedToken.Optional {
return doc, nil
continue // don't return yet, as it may be present down alternate paths
}

return nil, opMissingMapKeyErr{typedToken.Key, NewPointer(tokens[:i+2]), typedObj}
return nil, opMissingMapKeyErr{typedToken.Key, NewPointer(tokens[:ctx.I+2]), typedObj}
}

if isLast {
delete(typedObj, typedToken.Key)
} else {
prevUpdate = func(newObj interface{}) { typedObj[typedToken.Key] = newObj }
ctxStack = append(ctxStack, &mutationCtx{
Obj: o,
PrevUpdate: func(newObj interface{}) { typedObj[typedToken.Key] = newObj },
I: ctx.I + 1,
})
}

case WildcardToken:
if isLast {
return nil, fmt.Errorf("Wildcard must not be the last token", NewPointer(tokens[:ctx.I+2]))
}

typedObj, ok := ctx.Obj.([]interface{})
if !ok {
return nil, newOpArrayMismatchTypeErr(tokens[:ctx.I+2], ctx.Obj)
}

for idx, o := range typedObj {
ctxStack = append(ctxStack, &mutationCtx{
PrevUpdate: func(newObj interface{}) { typedObj[idx] = newObj },
I: ctx.I + 1,
Obj: o,
})
}

default:
return nil, opUnexpectedTokenErr{token, NewPointer(tokens[:i+2])}
return nil, opUnexpectedTokenErr{token, NewPointer(tokens[:ctx.I+2])}
}
}

Expand Down
34 changes: 34 additions & 0 deletions patch/remove_op_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,40 @@ import (
)

var _ = Describe("RemoveOp.Apply", func() {
Describe("multiple remove", func() {
It("removes many items", func() {
res, err := RemoveOp{Path: MustNewPointerFromString("/instance_groups/*/vm_extensions?")}.Apply(map[interface{}]interface{}{
"instance_groups": []interface{}{
map[interface{}]interface{}{
"name": "foo",
"vm_extensions": []interface{}{"ex1", "ex2"},
},
map[interface{}]interface{}{
"name": "bar",
},
map[interface{}]interface{}{
"name": "baz",
"vm_extensions": []interface{}{"ex1", "ex2", "ex3"},
},
},
})
Expect(err).ToNot(HaveOccurred())
Expect(res).To(Equal(map[interface{}]interface{}{
"instance_groups": []interface{}{
map[interface{}]interface{}{
"name": "foo",
},
map[interface{}]interface{}{
"name": "bar",
},
map[interface{}]interface{}{
"name": "baz",
},
},
}))
})
})

It("returns an error if path is for the entire document", func() {
_, err := RemoveOp{Path: MustNewPointerFromString("")}.Apply("a")
Expect(err).To(HaveOccurred())
Expand Down
Loading