diff --git a/patch/errs.go b/patch/errs.go index c0aa412..67ece12 100644 --- a/patch/errs.go +++ b/patch/errs.go @@ -51,12 +51,13 @@ func (e opMissingMapKeyErr) siblingKeysErrStr() string { } type opMissingIndexErr struct { - idx int - obj []interface{} + path Pointer + idx int + obj []interface{} } func (e opMissingIndexErr) Error() string { - return fmt.Sprintf("Expected to find array index '%d' but found array of length '%d'", e.idx, len(e.obj)) + return fmt.Sprintf("Expected to find array index '%d' but found array of length '%d' for path '%s'", e.idx, len(e.obj), e.path) } type opMultipleMatchingIndexErr struct { diff --git a/patch/find_op.go b/patch/find_op.go index ea59e2e..dc4c352 100644 --- a/patch/find_op.go +++ b/patch/find_op.go @@ -1,9 +1,5 @@ package patch -import ( - "fmt" -) - type FindOp struct { Path Pointer } @@ -15,105 +11,10 @@ func (op FindOp) Apply(doc interface{}) (interface{}, error) { return doc, nil } - obj := doc - - for i, token := range tokens[1:] { - isLast := i == len(tokens)-2 - - switch typedToken := token.(type) { - case IndexToken: - idx := typedToken.Index - - typedObj, ok := obj.([]interface{}) - if !ok { - return nil, newOpArrayMismatchTypeErr(tokens[:i+2], obj) - } - - if idx >= len(typedObj) { - return nil, opMissingIndexErr{idx, typedObj} - } - - if isLast { - return typedObj[idx], nil - } else { - obj = typedObj[idx] - } - - case AfterLastIndexToken: - errMsg := "Expected not to find after last index token in path '%s' (not supported in find operations)" - return nil, fmt.Errorf(errMsg, op.Path) - - case MatchingIndexToken: - typedObj, ok := obj.([]interface{}) - if !ok { - return nil, newOpArrayMismatchTypeErr(tokens[:i+2], obj) - } - - var idxs []int - - for itemIdx, item := range typedObj { - typedItem, ok := item.(map[interface{}]interface{}) - if ok { - if typedItem[typedToken.Key] == typedToken.Value { - idxs = append(idxs, itemIdx) - } - } - } - - if typedToken.Optional && len(idxs) == 0 { - obj = map[interface{}]interface{}{typedToken.Key: typedToken.Value} - - if isLast { - return obj, nil - } - } else { - if len(idxs) != 1 { - return nil, opMultipleMatchingIndexErr{NewPointer(tokens[:i+2]), idxs} - } - - idx := idxs[0] - - if isLast { - return typedObj[idx], nil - } else { - obj = typedObj[idx] - } - } - - case KeyToken: - typedObj, ok := obj.(map[interface{}]interface{}) - if !ok { - return nil, newOpMapMismatchTypeErr(tokens[:i+2], obj) - } - - var found bool - - obj, found = typedObj[typedToken.Key] - if !found && !typedToken.Optional { - return nil, opMissingMapKeyErr{typedToken.Key, NewPointer(tokens[:i+2]), typedObj} - } - - if isLast { - return typedObj[typedToken.Key], nil - } else { - if !found { - // Determine what type of value to create based on next token - switch tokens[i+2].(type) { - case MatchingIndexToken: - obj = []interface{}{} - case KeyToken: - obj = map[interface{}]interface{}{} - default: - errMsg := "Expected to find key or matching index token at path '%s'" - return nil, fmt.Errorf(errMsg, NewPointer(tokens[:i+3])) - } - } - } - - default: - return nil, opUnexpectedTokenErr{token, NewPointer(tokens[:i+2])} - } - } - - return doc, nil + return (&tokenContext{ + Tokens: tokens, + TokenIndex: 0, + Node: doc, + Method: methodFind, + }).Descend() } diff --git a/patch/find_op_test.go b/patch/find_op_test.go index a72ce0d..4bd9228 100644 --- a/patch/find_op_test.go +++ b/patch/find_op_test.go @@ -63,12 +63,12 @@ var _ = Describe("FindOp.Apply", func() { _, err := FindOp{Path: MustNewPointerFromString("/1")}.Apply([]interface{}{}) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(Equal( - "Expected to find array index '1' but found array of length '0'")) + "Expected to find array index '1' but found array of length '0' for path '/1'")) _, err = FindOp{Path: MustNewPointerFromString("/1/1")}.Apply([]interface{}{}) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(Equal( - "Expected to find array index '1' but found array of length '0'")) + "Expected to find array index '1' but found array of length '0' for path '/1'")) }) }) @@ -315,7 +315,7 @@ var _ = Describe("FindOp.Apply", func() { _, err := FindOp{Path: MustNewPointerFromString("/abc?/0")}.Apply(doc) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(Equal( - "Expected to find key or matching index token at path '/abc?/0'")) + "Expected to find array index '0' but found array of length '0' for path '/abc?/0'")) }) It("returns an error if it's not a map when key is being accessed", func() { diff --git a/patch/pointer.go b/patch/pointer.go index a9888df..78597ee 100644 --- a/patch/pointer.go +++ b/patch/pointer.go @@ -52,6 +52,18 @@ func NewPointerFromString(str string) (Pointer, error) { continue } + // parse as before first index + if isLast && tok == "+" { + tokens = append(tokens, BeforeFirstIndexToken{}) + continue + } + + // parse wildcard + if tok == "*" { + tokens = append(tokens, WildcardToken{}) + continue + } + // parse as index idx, err := strconv.Atoi(tok) if err == nil { @@ -108,47 +120,17 @@ func (p Pointer) IsSet() bool { return len(p.tokens) > 0 } func (p Pointer) String() string { var strs []string - optional := false - + seenOptional := false for _, token := range p.tokens { - switch typedToken := token.(type) { - case RootToken: - strs = append(strs, "") - - case IndexToken: - strs = append(strs, fmt.Sprintf("%d", typedToken.Index)) - - case AfterLastIndexToken: - strs = append(strs, "-") - - case MatchingIndexToken: - key := rfc6901Encoder.Replace(typedToken.Key) - val := rfc6901Encoder.Replace(typedToken.Value) - - if typedToken.Optional { - if !optional { - val += "?" - optional = true - } + s := token.String() + if strings.HasSuffix(s, "?") { + if seenOptional { + s = s[:len(s)-1] + } else { + seenOptional = true } - - strs = append(strs, fmt.Sprintf("%s=%s", key, val)) - - case KeyToken: - str := rfc6901Encoder.Replace(typedToken.Key) - - if typedToken.Optional { // /key?/key2/key3 - if !optional { - str += "?" - optional = true - } - } - - strs = append(strs, str) - - default: - panic(fmt.Sprintf("Unknown token type '%T'", typedToken)) } + strs = append(strs, s) } return strings.Join(strs, "/") diff --git a/patch/remove_op.go b/patch/remove_op.go index 9d175e3..701f99d 100644 --- a/patch/remove_op.go +++ b/patch/remove_op.go @@ -15,98 +15,15 @@ func (op RemoveOp) Apply(doc interface{}) (interface{}, error) { return nil, fmt.Errorf("Cannot remove entire document") } - obj := doc - prevUpdate := func(newObj interface{}) { doc = newObj } - - for i, token := range tokens[1:] { - isLast := i == len(tokens)-2 - - switch typedToken := token.(type) { - case IndexToken: - idx := typedToken.Index - - typedObj, ok := obj.([]interface{}) - if !ok { - return nil, newOpArrayMismatchTypeErr(tokens[:i+2], obj) - } - - if idx >= len(typedObj) { - return nil, opMissingIndexErr{idx, typedObj} - } - - if isLast { - var newAry []interface{} - newAry = append(newAry, typedObj[:idx]...) - newAry = append(newAry, typedObj[idx+1:]...) - prevUpdate(newAry) - } else { - obj = typedObj[idx] - prevUpdate = func(newObj interface{}) { typedObj[idx] = newObj } - } - - case MatchingIndexToken: - typedObj, ok := obj.([]interface{}) - if !ok { - return nil, newOpArrayMismatchTypeErr(tokens[:i+2], obj) - } - - var idxs []int - - for itemIdx, item := range typedObj { - typedItem, ok := item.(map[interface{}]interface{}) - if ok { - if typedItem[typedToken.Key] == typedToken.Value { - idxs = append(idxs, itemIdx) - } - } - } - - if typedToken.Optional && len(idxs) == 0 { - return doc, nil - } - - if len(idxs) != 1 { - return nil, opMultipleMatchingIndexErr{NewPointer(tokens[:i+2]), idxs} - } - - idx := idxs[0] - - if isLast { - var newAry []interface{} - newAry = append(newAry, typedObj[:idx]...) - newAry = append(newAry, typedObj[idx+1:]...) - prevUpdate(newAry) - } else { - obj = typedObj[idx] - // no need to change prevUpdate since matching item can only be a map - } - - case KeyToken: - typedObj, ok := obj.(map[interface{}]interface{}) - if !ok { - return nil, newOpMapMismatchTypeErr(tokens[:i+2], obj) - } - - var found bool - - obj, found = typedObj[typedToken.Key] - if !found { - if typedToken.Optional { - return doc, nil - } - - return nil, opMissingMapKeyErr{typedToken.Key, NewPointer(tokens[:i+2]), typedObj} - } - - if isLast { - delete(typedObj, typedToken.Key) - } else { - prevUpdate = func(newObj interface{}) { typedObj[typedToken.Key] = newObj } - } - - default: - return nil, opUnexpectedTokenErr{token, NewPointer(tokens[:i+2])} - } + _, err := (&tokenContext{ + Tokens: tokens, + TokenIndex: 0, + Node: doc, + Setter: func(newObj interface{}) { doc = newObj }, + Method: methodRemove, + }).Descend() + if err != nil { + return nil, err } return doc, nil diff --git a/patch/remove_op_test.go b/patch/remove_op_test.go index 60f6b87..593e95e 100644 --- a/patch/remove_op_test.go +++ b/patch/remove_op_test.go @@ -64,12 +64,12 @@ var _ = Describe("RemoveOp.Apply", func() { _, err := RemoveOp{Path: MustNewPointerFromString("/1")}.Apply([]interface{}{}) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(Equal( - "Expected to find array index '1' but found array of length '0'")) + "Expected to find array index '1' but found array of length '0' for path '/1'")) _, err = RemoveOp{Path: MustNewPointerFromString("/1/1")}.Apply([]interface{}{}) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(Equal( - "Expected to find array index '1' but found array of length '0'")) + "Expected to find array index '1' but found array of length '0' for path '/1'")) }) }) diff --git a/patch/replace_op.go b/patch/replace_op.go index 48f68c3..beabefa 100644 --- a/patch/replace_op.go +++ b/patch/replace_op.go @@ -19,128 +19,20 @@ func (op ReplaceOp) Apply(doc interface{}) (interface{}, error) { } tokens := op.Path.Tokens() - if len(tokens) == 1 { return clonedValue, nil } - obj := doc - prevUpdate := func(newObj interface{}) { doc = newObj } - - for i, token := range tokens[1:] { - isLast := i == len(tokens)-2 - - switch typedToken := token.(type) { - case IndexToken: - idx := typedToken.Index - - typedObj, ok := obj.([]interface{}) - if !ok { - return nil, newOpArrayMismatchTypeErr(tokens[:i+2], obj) - } - - if idx >= len(typedObj) { - return nil, opMissingIndexErr{idx, typedObj} - } - - if isLast { - typedObj[idx] = clonedValue - } else { - obj = typedObj[idx] - prevUpdate = func(newObj interface{}) { typedObj[idx] = newObj } - } - - case AfterLastIndexToken: - typedObj, ok := obj.([]interface{}) - if !ok { - return nil, newOpArrayMismatchTypeErr(tokens[:i+2], obj) - } - - if isLast { - prevUpdate(append(typedObj, clonedValue)) - } else { - return nil, fmt.Errorf("Expected after last index token to be last in path '%s'", op.Path) - } - - case MatchingIndexToken: - typedObj, ok := obj.([]interface{}) - if !ok { - return nil, newOpArrayMismatchTypeErr(tokens[:i+2], obj) - } - - var idxs []int - - for itemIdx, item := range typedObj { - typedItem, ok := item.(map[interface{}]interface{}) - if ok { - if typedItem[typedToken.Key] == typedToken.Value { - idxs = append(idxs, itemIdx) - } - } - } - - if typedToken.Optional && len(idxs) == 0 { - if isLast { - prevUpdate(append(typedObj, clonedValue)) - } else { - obj = map[interface{}]interface{}{typedToken.Key: typedToken.Value} - prevUpdate(append(typedObj, obj)) - // no need to change prevUpdate since matching item can only be a map - } - } else { - if len(idxs) != 1 { - return nil, opMultipleMatchingIndexErr{NewPointer(tokens[:i+2]), idxs} - } - - idx := idxs[0] - - if isLast { - typedObj[idx] = clonedValue - } else { - obj = typedObj[idx] - // no need to change prevUpdate since matching item can only be a map - } - } - - case KeyToken: - typedObj, ok := obj.(map[interface{}]interface{}) - if !ok { - return nil, newOpMapMismatchTypeErr(tokens[:i+2], obj) - } - - var found bool - - obj, found = typedObj[typedToken.Key] - if !found && !typedToken.Optional { - return nil, opMissingMapKeyErr{typedToken.Key, NewPointer(tokens[:i+2]), typedObj} - } - - if isLast { - typedObj[typedToken.Key] = clonedValue - } else { - prevUpdate = func(newObj interface{}) { typedObj[typedToken.Key] = newObj } - - if !found { - // Determine what type of value to create based on next token - switch tokens[i+2].(type) { - case AfterLastIndexToken: - obj = []interface{}{} - case MatchingIndexToken: - obj = []interface{}{} - case KeyToken: - obj = map[interface{}]interface{}{} - default: - errMsg := "Expected to find key, matching index or after last index token at path '%s'" - return nil, fmt.Errorf(errMsg, NewPointer(tokens[:i+3])) - } - - typedObj[typedToken.Key] = obj - } - } - - default: - return nil, opUnexpectedTokenErr{token, NewPointer(tokens[:i+2])} - } + _, err = (&tokenContext{ + Tokens: tokens, + TokenIndex: 0, + Node: doc, + Setter: func(newObj interface{}) { doc = newObj }, + Value: func() (interface{}, error) { return op.cloneValue(clonedValue) }, + Method: methodReplace, + }).Descend() + if err != nil { + return nil, err } return doc, nil diff --git a/patch/replace_op_test.go b/patch/replace_op_test.go index f50d1b6..6e046f8 100644 --- a/patch/replace_op_test.go +++ b/patch/replace_op_test.go @@ -8,6 +8,35 @@ import ( ) var _ = Describe("ReplaceOp.Apply", func() { + Describe("multiple replace", func() { + It("replaces many items", func() { + res, err := ReplaceOp{Path: MustNewPointerFromString("/instance_groups/*/vm_extensions?/-"), Value: "ex2"}.Apply(map[interface{}]interface{}{ + "instance_groups": []interface{}{ + map[interface{}]interface{}{ + "name": "foo", + "vm_extensions": []interface{}{"ex1"}, + }, + map[interface{}]interface{}{ + "name": "bar", + }, + }, + }) + Expect(err).ToNot(HaveOccurred()) + Expect(res).To(Equal(map[interface{}]interface{}{ + "instance_groups": []interface{}{ + map[interface{}]interface{}{ + "name": "foo", + "vm_extensions": []interface{}{"ex1", "ex2"}, + }, + map[interface{}]interface{}{ + "name": "bar", + "vm_extensions": []interface{}{"ex2"}, + }, + }, + })) + }) + }) + It("returns error if replacement value cloning fails", func() { _, err := ReplaceOp{Path: MustNewPointerFromString(""), Value: func() {}}.Apply("a") Expect(err).To(HaveOccurred()) @@ -83,12 +112,12 @@ var _ = Describe("ReplaceOp.Apply", func() { _, err := ReplaceOp{Path: MustNewPointerFromString("/1")}.Apply([]interface{}{}) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(Equal( - "Expected to find array index '1' but found array of length '0'")) + "Expected to find array index '1' but found array of length '0' for path '/1'")) _, err = ReplaceOp{Path: MustNewPointerFromString("/1/1")}.Apply([]interface{}{}) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(Equal( - "Expected to find array index '1' but found array of length '0'")) + "Expected to find array index '1' but found array of length '0' for path '/1'")) }) }) @@ -103,6 +132,16 @@ var _ = Describe("ReplaceOp.Apply", func() { Expect(res).To(Equal([]interface{}{1, 2, 3, 10})) }) + It("prepends new item", func() { + res, err := ReplaceOp{Path: MustNewPointerFromString("/+"), Value: 10}.Apply([]interface{}{}) + Expect(err).ToNot(HaveOccurred()) + Expect(res).To(Equal([]interface{}{10})) + + res, err = ReplaceOp{Path: MustNewPointerFromString("/+"), Value: 10}.Apply([]interface{}{1, 2, 3}) + Expect(err).ToNot(HaveOccurred()) + Expect(res).To(Equal([]interface{}{10, 1, 2, 3})) + }) + It("appends nested array item", func() { doc := []interface{}{[]interface{}{10, 11, 12}, 2, 3} @@ -111,6 +150,14 @@ var _ = Describe("ReplaceOp.Apply", func() { Expect(res).To(Equal([]interface{}{[]interface{}{10, 11, 12, 100}, 2, 3})) }) + It("prepends nested array item", func() { + doc := []interface{}{[]interface{}{10, 11, 12}, 2, 3} + + res, err := ReplaceOp{Path: MustNewPointerFromString("/0/+"), Value: 100}.Apply(doc) + Expect(err).ToNot(HaveOccurred()) + Expect(res).To(Equal([]interface{}{[]interface{}{100, 10, 11, 12}, 2, 3})) + }) + It("appends array item from an array that is inside a map", func() { doc := map[interface{}]interface{}{ "abc": []interface{}{1, 2, 3}, @@ -124,6 +171,19 @@ var _ = Describe("ReplaceOp.Apply", func() { })) }) + It("prepends array item from an array that is inside a map", func() { + doc := map[interface{}]interface{}{ + "abc": []interface{}{1, 2, 3}, + } + + res, err := ReplaceOp{Path: MustNewPointerFromString("/abc/+"), Value: 10}.Apply(doc) + Expect(err).ToNot(HaveOccurred()) + + Expect(res).To(Equal(map[interface{}]interface{}{ + "abc": []interface{}{10, 1, 2, 3}, + })) + }) + It("returns an error if after last index token is not last", func() { ptr := NewPointer([]Token{RootToken{}, AfterLastIndexToken{}, KeyToken{}}) @@ -133,6 +193,15 @@ var _ = Describe("ReplaceOp.Apply", func() { "Expected after last index token to be last in path '/-/'")) }) + It("returns an error if before first index token is not last", func() { + ptr := NewPointer([]Token{RootToken{}, BeforeFirstIndexToken{}, KeyToken{}}) + + _, err := ReplaceOp{Path: ptr}.Apply([]interface{}{}) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(Equal( + "Expected before first index token to be last in path '/+/'")) + }) + It("returns an error if it's not an array being accessed", func() { _, err := ReplaceOp{Path: MustNewPointerFromString("/-")}.Apply(map[interface{}]interface{}{}) Expect(err).To(HaveOccurred()) @@ -146,6 +215,20 @@ var _ = Describe("ReplaceOp.Apply", func() { Expect(err.Error()).To(Equal( "Expected to find an array at path '/key/-' but found 'map[interface {}]interface {}'")) }) + + It("returns an error if it's not an array being accessed", func() { + _, err := ReplaceOp{Path: MustNewPointerFromString("/+")}.Apply(map[interface{}]interface{}{}) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(Equal( + "Expected to find an array at path '/+' but found 'map[interface {}]interface {}'")) + + doc := map[interface{}]interface{}{"key": map[interface{}]interface{}{}} + + _, err = ReplaceOp{Path: MustNewPointerFromString("/key/+")}.Apply(doc) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(Equal( + "Expected to find an array at path '/key/+' but found 'map[interface {}]interface {}'")) + }) }) Describe("array item with matching key and value", func() { @@ -400,13 +483,25 @@ var _ = Describe("ReplaceOp.Apply", func() { })) }) + It("creates missing key with array value for index access if key is not expected to exist", func() { + doc := map[interface{}]interface{}{"xyz": "xyz"} + + res, err := ReplaceOp{Path: MustNewPointerFromString("/abc?/+"), Value: 1}.Apply(doc) + Expect(err).ToNot(HaveOccurred()) + + Expect(res).To(Equal(map[interface{}]interface{}{ + "abc": []interface{}{1}, + "xyz": "xyz", + })) + }) + It("returns an error if missing key needs to be created but next access does not make sense", func() { doc := map[interface{}]interface{}{"xyz": "xyz"} _, err := ReplaceOp{Path: MustNewPointerFromString("/abc?/0"), Value: 1}.Apply(doc) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(Equal( - "Expected to find key, matching index or after last index token at path '/abc?/0'")) + "Expected to find array index '0' but found array of length '0' for path '/abc?/0'")) }) It("returns an error if it's not a map when key is being accessed", func() { diff --git a/patch/token_afterlastindex.go b/patch/token_afterlastindex.go new file mode 100644 index 0000000..264c4f8 --- /dev/null +++ b/patch/token_afterlastindex.go @@ -0,0 +1,43 @@ +package patch + +import "fmt" + +type AfterLastIndexToken struct{} + +func (t AfterLastIndexToken) String() string { + return "-" +} + +func (t AfterLastIndexToken) processDescent(ctx *tokenContext) (interface{}, error) { + if ctx.Node == nil { + ctx.Node = make([]interface{}, 0) + if ctx.Method == methodReplace { + ctx.Setter(ctx.Node) + } + } + + if ctx.Method != methodReplace { + if ctx.Method == methodFind { + errMsg := "Expected not to find after last index token in path '%s' (not supported in find operations)" + return nil, fmt.Errorf(errMsg, NewPointer(ctx.Tokens)) + } + return nil, opUnexpectedTokenErr{t, NewPointer(ctx.Tokens[:ctx.TokenIndex+1])} + } + + typedObj, ok := ctx.Node.([]interface{}) + if !ok { + return nil, newOpArrayMismatchTypeErr(ctx.Tokens[:ctx.TokenIndex+1], ctx.Node) + } + + if !ctx.IsLast() { + return nil, fmt.Errorf("Expected after last index token to be last in path '%s'", Pointer{tokens: ctx.Tokens}) + } + + v, err := ctx.Value() + if err != nil { + return nil, err + } + + ctx.Setter(append(append(make([]interface{}, 0, len(typedObj)+1), typedObj...), v)) + return nil, nil +} diff --git a/patch/token_index.go b/patch/token_index.go new file mode 100644 index 0000000..f3677d8 --- /dev/null +++ b/patch/token_index.go @@ -0,0 +1,58 @@ +package patch + +import ( + "errors" + "fmt" +) + +type IndexToken struct { + Index int +} + +func (t IndexToken) String() string { + return fmt.Sprintf("%d", t.Index) +} + +func (t IndexToken) processDescent(ctx *tokenContext) (interface{}, error) { + if ctx.Node == nil { + ctx.Node = make([]interface{}, 0) + if ctx.Method == methodReplace { + ctx.Setter(ctx.Node) + } + } + + typedObj, ok := ctx.Node.([]interface{}) + if !ok { + return nil, newOpArrayMismatchTypeErr(ctx.Tokens[:ctx.TokenIndex+1], ctx.Node) + } + + if t.Index >= len(typedObj) { + return nil, opMissingIndexErr{NewPointer(ctx.Tokens[:ctx.TokenIndex+1]), t.Index, typedObj} + } + + if !ctx.IsLast() { + ctx.Node = typedObj[t.Index] + ctx.Setter = func(newObj interface{}) { typedObj[t.Index] = newObj } + return ctx.Descend() + } + + switch ctx.Method { + case methodFind: + return typedObj[t.Index], nil + + case methodReplace: + v, err := ctx.Value() + if err != nil { + return nil, err + } + typedObj[t.Index] = v + return nil, nil + + case methodRemove: + ctx.Setter(append(append(make([]interface{}, 0, len(typedObj)-1), typedObj[:t.Index]...), typedObj[t.Index+1:]...)) + return nil, nil + + default: + return nil, errors.New("unsupported") + } +} diff --git a/patch/token_key.go b/patch/token_key.go new file mode 100644 index 0000000..ff35bb0 --- /dev/null +++ b/patch/token_key.go @@ -0,0 +1,67 @@ +package patch + +import "errors" + +type KeyToken struct { + Key string + + Optional bool +} + +func (t KeyToken) String() string { + str := rfc6901Encoder.Replace(t.Key) + + if t.Optional { // /key?/key2/key3 + str += "?" + } + + return str +} + +func (t KeyToken) processDescent(ctx *tokenContext) (interface{}, error) { + if ctx.Node == nil { + ctx.Node = make(map[interface{}]interface{}) + if ctx.Method == methodReplace { + ctx.Setter(ctx.Node) + } + } + + typedObj, ok := ctx.Node.(map[interface{}]interface{}) + if !ok { + return nil, newOpMapMismatchTypeErr(ctx.Tokens[:ctx.TokenIndex+1], ctx.Node) + } + + var found bool + ctx.Node, found = typedObj[t.Key] + if !found { + if !t.Optional { + return nil, opMissingMapKeyErr{t.Key, NewPointer(ctx.Tokens[:ctx.TokenIndex+1]), typedObj} + } + ctx.Node = nil // up to next to create thyself + } + + if !ctx.IsLast() { + ctx.Setter = func(newObj interface{}) { typedObj[t.Key] = newObj } + return ctx.Descend() + } + + switch ctx.Method { + case methodFind: + return typedObj[t.Key], nil + + case methodReplace: + v, err := ctx.Value() + if err != nil { + return nil, err + } + typedObj[t.Key] = v + return nil, nil + + case methodRemove: + delete(typedObj, t.Key) + return nil, nil + + default: + return nil, errors.New("unsupported") + } +} diff --git a/patch/token_matchingindex.go b/patch/token_matchingindex.go new file mode 100644 index 0000000..b81fd59 --- /dev/null +++ b/patch/token_matchingindex.go @@ -0,0 +1,93 @@ +package patch + +import ( + "errors" + "fmt" +) + +type MatchingIndexToken struct { + Key string + Value string + + Optional bool +} + +func (t MatchingIndexToken) String() string { + key := rfc6901Encoder.Replace(t.Key) + val := rfc6901Encoder.Replace(t.Value) + + if t.Optional { + val += "?" + } + + return fmt.Sprintf("%s=%s", key, val) +} + +func (t MatchingIndexToken) processDescent(ctx *tokenContext) (interface{}, error) { + if ctx.Node == nil { + ctx.Node = make([]interface{}, 0) + if ctx.Method == methodReplace { + ctx.Setter(ctx.Node) + } + } + + typedObj, ok := ctx.Node.([]interface{}) + if !ok { + return nil, newOpArrayMismatchTypeErr(ctx.Tokens[:ctx.TokenIndex+1], ctx.Node) + } + + var idxs []int + for itemIdx, item := range typedObj { + typedItem, ok := item.(map[interface{}]interface{}) + if ok { + if typedItem[t.Key] == t.Value { + idxs = append(idxs, itemIdx) + } + } + } + + switch len(idxs) { + case 0: + if !t.Optional { + return nil, fmt.Errorf("Expected to find exactly one matching array item for path '%s' but found 0", NewPointer(ctx.Tokens[:ctx.TokenIndex+1])) + } + // We know the type here - it must be a map (else we couldn't key match it) + idxs = []int{len(typedObj)} + typedObj = append(append(make([]interface{}, 0, len(typedObj)+1), typedObj...), map[interface{}]interface{}{ + t.Key: t.Value, + }) + if ctx.Method == methodReplace { + ctx.Setter(typedObj) + } + case 1: + // good, proceed as normal + default: + return nil, opMultipleMatchingIndexErr{NewPointer(ctx.Tokens[:ctx.TokenIndex+1]), idxs} + } + + if !ctx.IsLast() { + ctx.Node = typedObj[idxs[0]] + ctx.Setter = func(newObj interface{}) { typedObj[idxs[0]] = newObj } + return ctx.Descend() + } + + switch ctx.Method { + case methodFind: + return typedObj[idxs[0]], nil + + case methodReplace: + v, err := ctx.Value() + if err != nil { + return nil, err + } + typedObj[idxs[0]] = v + return nil, nil + + case methodRemove: + ctx.Setter(append(append(make([]interface{}, 0, len(typedObj)-1), typedObj[:idxs[0]]...), typedObj[idxs[0]+1:]...)) + return nil, nil + + default: + return nil, errors.New("unsupported") + } +} diff --git a/patch/token_prepend.go b/patch/token_prepend.go new file mode 100644 index 0000000..0c5d4aa --- /dev/null +++ b/patch/token_prepend.go @@ -0,0 +1,42 @@ +package patch + +import "fmt" + +type BeforeFirstIndexToken struct{} + +func (t BeforeFirstIndexToken) String() string { + return "+" +} + +func (t BeforeFirstIndexToken) processDescent(ctx *tokenContext) (interface{}, error) { + if ctx.Node == nil { + ctx.Node = make([]interface{}, 0) + if ctx.Method == methodReplace { + ctx.Setter(ctx.Node) + } + } + + if ctx.Method != methodReplace { + if ctx.Method == methodFind { + errMsg := "Expected after last index token to be last in path '%s' (not supported in find operations)" + return nil, fmt.Errorf(errMsg, NewPointer(ctx.Tokens)) + } + return nil, opUnexpectedTokenErr{t, NewPointer(ctx.Tokens[:ctx.TokenIndex+1])} + } + + typedObj, ok := ctx.Node.([]interface{}) + if !ok { + return nil, newOpArrayMismatchTypeErr(ctx.Tokens[:ctx.TokenIndex+1], ctx.Node) + } + + if !ctx.IsLast() { + return nil, fmt.Errorf("Expected before first index token to be last in path '%s'", Pointer{tokens: ctx.Tokens}) + } + + v, err := ctx.Value() + if err != nil { + return nil, err + } + ctx.Setter(append([]interface{}{v}, typedObj...)) + return nil, nil +} diff --git a/patch/token_root.go b/patch/token_root.go new file mode 100644 index 0000000..abd29ea --- /dev/null +++ b/patch/token_root.go @@ -0,0 +1,13 @@ +package patch + +import "errors" + +type RootToken struct{} + +func (t RootToken) String() string { + return "" +} + +func (t RootToken) processDescent(ctx *tokenContext) (interface{}, error) { + return nil, errors.New("not supported") +} diff --git a/patch/token_wildcard.go b/patch/token_wildcard.go new file mode 100644 index 0000000..a492360 --- /dev/null +++ b/patch/token_wildcard.go @@ -0,0 +1,32 @@ +package patch + +import "errors" + +type WildcardToken struct{} + +func (t WildcardToken) String() string { + return "*" +} + +func (t WildcardToken) processDescent(ctx *tokenContext) (interface{}, error) { + if ctx.IsLast() { + return nil, errors.New("wildcard can't be used for last element. Use - instead") + } + if ctx.Method != methodReplace && ctx.Method != methodRemove { + return nil, errors.New("operation using wildcard") + } + + typedArray, ok := ctx.Node.([]interface{}) + if !ok { + return nil, newOpArrayMismatchTypeErr(ctx.Tokens[:ctx.TokenIndex+1], ctx.Node) + } + for idx, e := range typedArray { + ctx.Node = e + ctx.Setter = func(newObj interface{}) { typedArray[idx] = newObj } + _, err := ctx.Descend() + if err != nil { + return nil, err + } + } + return nil, nil +} diff --git a/patch/tokens.go b/patch/tokens.go index 33db37d..ad98bd5 100644 --- a/patch/tokens.go +++ b/patch/tokens.go @@ -1,24 +1,36 @@ package patch -type Token interface{} +const ( + methodFind = 0 + methodReplace = 1 + methodRemove = 2 +) -type RootToken struct{} +type tokenContext struct { + Tokens []Token + TokenIndex int -type IndexToken struct { - Index int -} + Node interface{} -type AfterLastIndexToken struct{} + Setter func(newObj interface{}) + Value func() (interface{}, error) -type MatchingIndexToken struct { - Key string - Value string + Method int +} - Optional bool +func (rc *tokenContext) IsLast() bool { + return (rc.TokenIndex + 1) == len(rc.Tokens) } -type KeyToken struct { - Key string +func (rc *tokenContext) Descend() (interface{}, error) { + // Clone our context so values can be safely overridden + nc := *rc + nc.TokenIndex++ + + return nc.Tokens[nc.TokenIndex].processDescent(&nc) +} - Optional bool +type Token interface { + processDescent(ctx *tokenContext) (interface{}, error) + String() string }