├── LICENSE ├── README.md ├── bin └── test ├── docs ├── examples.md └── go-yaml.md └── patch ├── array_index.go ├── array_index_test.go ├── array_insertion.go ├── array_insertion_test.go ├── descriptive_op.go ├── diff.go ├── diff_test.go ├── err_op.go ├── errs.go ├── find_op.go ├── find_op_test.go ├── integration_test.go ├── op_definition.go ├── op_definition_test.go ├── ops.go ├── ops_test.go ├── pointer.go ├── pointer_test.go ├── remove_op.go ├── remove_op_test.go ├── replace_op.go ├── replace_op_test.go ├── suite_test.go ├── test_op.go ├── test_op_test.go ├── tokens.go ├── tokens_impl.go └── yaml_compat_test.go /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 Dmitriy Kalinin 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## go-patch 2 | 3 | Roughly based on JSON patch: https://tools.ietf.org/html/rfc6902. 4 | 5 | - [Usage examples](docs/examples.md) 6 | - [Go YAML gotchas](docs/go-yaml.md) 7 | 8 | Used by [BOSH CLI v2](http://bosh.io/docs/cli-ops-files.html). 9 | -------------------------------------------------------------------------------- /bin/test: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | go fmt github.com/cppforlife/go-patch/... 6 | 7 | ginkgo -trace -r patch/ 8 | -------------------------------------------------------------------------------- /docs/examples.md: -------------------------------------------------------------------------------- 1 | ## Pointer syntax 2 | 3 | - pointers always start at the root of the document 4 | 5 | - strings typically refer to hash keys (ex: `/key1`) 6 | - strings ending with `?` refer to hash keys that may or may not exist 7 | - "optionality" carries over to the items to the right 8 | 9 | - integers refer to array indices (ex: `/0`, `/-1`) 10 | 11 | - `-` refers to an imaginary index after last array index (ex: `/-`) 12 | 13 | - `key=val` notation matches hashes within an array (ex: `/key=val`) 14 | - values ending with `?` refer to array items that may or may not exist 15 | 16 | - array index selection could be affected via `:prev` and `:next` 17 | 18 | - array insertion could be affected via `:before` and `:after` 19 | 20 | See pointer test examples in [patch/pointer_test.go](../patch/pointer_test.go). 21 | 22 | ## Operations 23 | 24 | Following example is used to demonstrate operations below: 25 | 26 | ```yaml 27 | key: 1 28 | 29 | key2: 30 | nested: 31 | super_nested: 2 32 | other: 3 33 | 34 | array: [4,5,6] 35 | 36 | items: 37 | - name: item7 38 | - name: item8 39 | - name: item8 40 | ``` 41 | 42 | There are two available operations: `replace` and `remove`. 43 | 44 | ### Hash 45 | 46 | ```yaml 47 | - type: replace 48 | path: /key 49 | value: 10 50 | ``` 51 | 52 | - sets `key` to `10` 53 | 54 | ```yaml 55 | - type: replace 56 | path: /key_not_there 57 | value: 10 58 | ``` 59 | 60 | - errors because `key_not_there` is expected (does not have `?`) 61 | 62 | ```yaml 63 | - type: replace 64 | path: /new_key? 65 | value: 10 66 | ``` 67 | 68 | - creates `new_key` because it ends with `?` and sets it to `10` 69 | 70 | ```yaml 71 | - type: replace 72 | path: /key2/nested/super_nested 73 | value: 10 74 | ``` 75 | 76 | - requires that `key2` and `nested` hashes exist 77 | - sets `super_nested` to `10` 78 | 79 | ```yaml 80 | - type: replace 81 | path: /key2/nested?/another_nested/super_nested 82 | value: 10 83 | ``` 84 | 85 | - requires that `key2` hash exists 86 | - allows `nested`, `another_nested` and `super_nested` not to exist because `?` carries over to nested keys 87 | - creates `another_nested` and `super_nested` before setting `super_nested` to `10`, resulting in: 88 | 89 | ```yaml 90 | ... 91 | key2: 92 | nested: 93 | another_nested: 94 | super_nested: 10 95 | super_nested: 2 96 | other: 3 97 | ``` 98 | 99 | ### Array 100 | 101 | ```yaml 102 | - type: replace 103 | path: /array/0 104 | value: 10 105 | ``` 106 | 107 | - requires `array` to exist and be an array 108 | - replaces 0th item in `array` array with `10` 109 | 110 | ```yaml 111 | - type: replace 112 | path: /array/- 113 | value: 10 114 | ``` 115 | 116 | - requires `array` to exist and be an array 117 | - appends `10` to the end of `array` 118 | 119 | ```yaml 120 | - type: replace 121 | path: /array2?/- 122 | value: 10 123 | ``` 124 | 125 | - creates `array2` array since it does not exist 126 | - appends `10` to the end of `array2` 127 | 128 | ```yaml 129 | - type: replace 130 | path: /array/1:prev 131 | value: 10 132 | ``` 133 | 134 | - requires `array` to exist and be an array 135 | - replaces 0th item in `array` array with `10` 136 | 137 | ```yaml 138 | - type: replace 139 | path: /array/0:next 140 | value: 10 141 | ``` 142 | 143 | - requires `array` to exist and be an array 144 | - replaces 1st item (starting at 0) in `array` array with `10` 145 | 146 | ```yaml 147 | - type: replace 148 | path: /array/0:after 149 | value: 10 150 | ``` 151 | 152 | - requires `array` to exist and be an array 153 | - inserts `10` after 0th item in `array` array 154 | 155 | ```yaml 156 | - type: replace 157 | path: /array/0:before 158 | value: 10 159 | ``` 160 | 161 | - requires `array` to exist and be an array 162 | - inserts `10` before 0th item at the beginning of `array` array 163 | 164 | ### Arrays of hashes 165 | 166 | ```yaml 167 | - type: replace 168 | path: /items/name=item7/count 169 | value: 10 170 | ``` 171 | 172 | - finds array item with matching key `name` with value `item7` 173 | - adds `count` key as a sibling of name, resulting in: 174 | 175 | ```yaml 176 | ... 177 | items: 178 | - name: item7 179 | count: 10 180 | - name: item8 181 | ``` 182 | 183 | ```yaml 184 | - type: replace 185 | path: /items/name=item8/count 186 | value: 10 187 | ``` 188 | 189 | - errors because there are two values that have `item8` as their `name` 190 | 191 | ```yaml 192 | - type: replace 193 | path: /items/name=item9?/count 194 | value: 10 195 | ``` 196 | 197 | - appends array item with matching key `name` with value `item9` because values ends with `?` and item does not exist 198 | - creates `count` and sets it to `10` within created array item, resulting in: 199 | 200 | ```yaml 201 | ... 202 | items: 203 | - name: item7 204 | - name: item8 205 | - name: item8 206 | - name: item9 207 | count: 10 208 | ``` 209 | 210 | See full example in [patch/integration_test.go](../patch/integration_test.go). 211 | -------------------------------------------------------------------------------- /docs/go-yaml.md: -------------------------------------------------------------------------------- 1 | ## Go YAML 2 | 3 | Tests: [patch/yaml_compat_test.go](../patch/yaml_compat_test.go) 4 | 5 | - [fixed] Use `!!str ""` instead of `""` 6 | -------------------------------------------------------------------------------- /patch/array_index.go: -------------------------------------------------------------------------------- 1 | package patch 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | type ArrayIndex struct { 8 | Index int 9 | Modifiers []Modifier 10 | Array []interface{} 11 | Path Pointer 12 | } 13 | 14 | func (i ArrayIndex) Concrete() (int, error) { 15 | result := i.Index 16 | 17 | for _, modifier := range i.Modifiers { 18 | switch modifier.(type) { 19 | case PrevModifier: 20 | result -= 1 21 | case NextModifier: 22 | result += 1 23 | default: 24 | return 0, fmt.Errorf("Expected to find one of the following modifiers: 'prev', 'next', but found modifier '%T'", modifier) 25 | } 26 | } 27 | 28 | if result >= len(i.Array) || (-result)-1 >= len(i.Array) { 29 | return 0, OpMissingIndexErr{result, i.Array, i.Path} 30 | } 31 | 32 | if result < 0 { 33 | result = len(i.Array) + result 34 | } 35 | 36 | return result, nil 37 | } 38 | -------------------------------------------------------------------------------- /patch/array_index_test.go: -------------------------------------------------------------------------------- 1 | package patch_test 2 | 3 | import ( 4 | . "github.com/onsi/ginkgo" 5 | . "github.com/onsi/gomega" 6 | 7 | . "github.com/cppforlife/go-patch/patch" 8 | ) 9 | 10 | var _ = Describe("ArrayIndex", func() { 11 | dummyPath := MustNewPointerFromString("") 12 | 13 | Describe("Concrete", func() { 14 | It("returns positive index", func() { 15 | idx := ArrayIndex{Index: 0, Modifiers: nil, Array: []interface{}{1, 2, 3}, Path: dummyPath} 16 | Expect(idx.Concrete()).To(Equal(0)) 17 | 18 | idx = ArrayIndex{Index: 1, Modifiers: nil, Array: []interface{}{1, 2, 3}, Path: dummyPath} 19 | Expect(idx.Concrete()).To(Equal(1)) 20 | 21 | idx = ArrayIndex{Index: 2, Modifiers: nil, Array: []interface{}{1, 2, 3}, Path: dummyPath} 22 | Expect(idx.Concrete()).To(Equal(2)) 23 | }) 24 | 25 | It("wraps around negative index one time", func() { 26 | idx := ArrayIndex{Index: -0, Modifiers: nil, Array: []interface{}{1, 2, 3}, Path: dummyPath} 27 | Expect(idx.Concrete()).To(Equal(0)) 28 | 29 | idx = ArrayIndex{Index: -1, Modifiers: nil, Array: []interface{}{1, 2, 3}, Path: dummyPath} 30 | Expect(idx.Concrete()).To(Equal(2)) 31 | 32 | idx = ArrayIndex{Index: -2, Modifiers: nil, Array: []interface{}{1, 2, 3}, Path: dummyPath} 33 | Expect(idx.Concrete()).To(Equal(1)) 34 | 35 | idx = ArrayIndex{Index: -3, Modifiers: nil, Array: []interface{}{1, 2, 3}, Path: dummyPath} 36 | Expect(idx.Concrete()).To(Equal(0)) 37 | }) 38 | 39 | It("does not work with empty arrays", func() { 40 | idx := ArrayIndex{Index: 0, Modifiers: nil, Array: []interface{}{}, Path: dummyPath} 41 | _, err := idx.Concrete() 42 | Expect(err).To(Equal(OpMissingIndexErr{0, []interface{}{}, dummyPath})) 43 | 44 | p := PrevModifier{} 45 | n := NextModifier{} 46 | 47 | idx = ArrayIndex{Index: 0, Modifiers: []Modifier{p, n}, Array: []interface{}{}, Path: dummyPath} 48 | _, err = idx.Concrete() 49 | Expect(err).To(Equal(OpMissingIndexErr{0, []interface{}{}, dummyPath})) 50 | }) 51 | 52 | It("does not work with index out of bounds", func() { 53 | idx := ArrayIndex{Index: 3, Modifiers: nil, Array: []interface{}{1, 2, 3}, Path: dummyPath} 54 | _, err := idx.Concrete() 55 | Expect(err).To(Equal(OpMissingIndexErr{3, []interface{}{1, 2, 3}, dummyPath})) 56 | 57 | idx = ArrayIndex{Index: -4, Modifiers: nil, Array: []interface{}{1, 2, 3}, Path: dummyPath} 58 | _, err = idx.Concrete() 59 | Expect(err).To(Equal(OpMissingIndexErr{-4, []interface{}{1, 2, 3}, dummyPath})) 60 | }) 61 | 62 | It("returns previous item when previous modifier is used", func() { 63 | p := PrevModifier{} 64 | 65 | idx := ArrayIndex{Index: 0, Modifiers: []Modifier{p}, Array: []interface{}{1, 2, 3}, Path: dummyPath} 66 | Expect(idx.Concrete()).To(Equal(2)) 67 | 68 | idx = ArrayIndex{Index: 0, Modifiers: []Modifier{p, p}, Array: []interface{}{1, 2, 3}, Path: dummyPath} 69 | Expect(idx.Concrete()).To(Equal(1)) 70 | 71 | idx = ArrayIndex{Index: 0, Modifiers: []Modifier{p, p, p}, Array: []interface{}{1, 2, 3}, Path: dummyPath} 72 | Expect(idx.Concrete()).To(Equal(0)) 73 | 74 | idx = ArrayIndex{Index: 0, Modifiers: []Modifier{p, p, p, p}, Array: []interface{}{1, 2, 3}, Path: dummyPath} 75 | _, err := idx.Concrete() 76 | Expect(err).To(Equal(OpMissingIndexErr{-4, []interface{}{1, 2, 3}, dummyPath})) 77 | 78 | idx = ArrayIndex{Index: 0, Modifiers: []Modifier{p, p, p, p, p}, Array: []interface{}{1, 2, 3}, Path: dummyPath} 79 | _, err = idx.Concrete() 80 | Expect(err).To(Equal(OpMissingIndexErr{-5, []interface{}{1, 2, 3}, dummyPath})) 81 | 82 | idx = ArrayIndex{Index: 2, Modifiers: []Modifier{p, p}, Array: []interface{}{1, 2, 3}, Path: dummyPath} 83 | Expect(idx.Concrete()).To(Equal(0)) 84 | }) 85 | 86 | It("returns next item when next modifier is used", func() { 87 | n := NextModifier{} 88 | 89 | idx := ArrayIndex{Index: 0, Modifiers: []Modifier{n}, Array: []interface{}{1, 2, 3}, Path: dummyPath} 90 | Expect(idx.Concrete()).To(Equal(1)) 91 | 92 | idx = ArrayIndex{Index: 0, Modifiers: []Modifier{n, n}, Array: []interface{}{1, 2, 3}, Path: dummyPath} 93 | Expect(idx.Concrete()).To(Equal(2)) 94 | 95 | idx = ArrayIndex{Index: 0, Modifiers: []Modifier{n, n, n}, Array: []interface{}{1, 2, 3}, Path: dummyPath} 96 | _, err := idx.Concrete() 97 | Expect(err).To(Equal(OpMissingIndexErr{3, []interface{}{1, 2, 3}, dummyPath})) 98 | 99 | idx = ArrayIndex{Index: 0, Modifiers: []Modifier{n, n, n, n}, Array: []interface{}{1, 2, 3}, Path: dummyPath} 100 | _, err = idx.Concrete() 101 | Expect(err).To(Equal(OpMissingIndexErr{4, []interface{}{1, 2, 3}, dummyPath})) 102 | }) 103 | 104 | It("works with multiple previous and next modifiers", func() { 105 | p := PrevModifier{} 106 | n := NextModifier{} 107 | 108 | idx := ArrayIndex{Index: 0, Modifiers: []Modifier{p, n}, Array: []interface{}{1, 2, 3}, Path: dummyPath} 109 | Expect(idx.Concrete()).To(Equal(0)) 110 | 111 | idx = ArrayIndex{Index: 0, Modifiers: []Modifier{n, p}, Array: []interface{}{1, 2, 3}, Path: dummyPath} 112 | Expect(idx.Concrete()).To(Equal(0)) 113 | 114 | idx = ArrayIndex{Index: 0, Modifiers: []Modifier{n, n, p}, Array: []interface{}{1, 2, 3}, Path: dummyPath} 115 | Expect(idx.Concrete()).To(Equal(1)) 116 | 117 | idx = ArrayIndex{Index: 0, Modifiers: []Modifier{n, n, n, p}, Array: []interface{}{1, 2, 3}, Path: dummyPath} 118 | Expect(idx.Concrete()).To(Equal(2)) 119 | }) 120 | 121 | It("does not support any other modifier except previous and next", func() { 122 | b := BeforeModifier{} 123 | 124 | idx := ArrayIndex{Index: 0, Modifiers: []Modifier{b}, Array: []interface{}{1, 2, 3}, Path: dummyPath} 125 | _, err := idx.Concrete() 126 | Expect(err.Error()).To(Equal("Expected to find one of the following modifiers: 'prev', 'next', but found modifier 'patch.BeforeModifier'")) 127 | }) 128 | }) 129 | }) 130 | -------------------------------------------------------------------------------- /patch/array_insertion.go: -------------------------------------------------------------------------------- 1 | package patch 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | type ArrayInsertion struct { 8 | Index int 9 | Modifiers []Modifier 10 | Array []interface{} 11 | Path Pointer 12 | } 13 | 14 | type ArrayInsertionIndex struct { 15 | number int 16 | insert bool 17 | } 18 | 19 | func (i ArrayInsertion) Concrete() (ArrayInsertionIndex, error) { 20 | var mods []Modifier 21 | 22 | before := false 23 | after := false 24 | 25 | for _, modifier := range i.Modifiers { 26 | if before { 27 | return ArrayInsertionIndex{}, fmt.Errorf( 28 | "Expected to not find any modifiers after 'before' modifier, but found modifier '%T'", modifier) 29 | } 30 | if after { 31 | return ArrayInsertionIndex{}, fmt.Errorf( 32 | "Expected to not find any modifiers after 'after' modifier, but found modifier '%T'", modifier) 33 | } 34 | 35 | switch modifier.(type) { 36 | case BeforeModifier: 37 | before = true 38 | case AfterModifier: 39 | after = true 40 | default: 41 | mods = append(mods, modifier) 42 | } 43 | } 44 | 45 | idx := ArrayIndex{Index: i.Index, Modifiers: mods, Array: i.Array, Path: i.Path} 46 | 47 | num, err := idx.Concrete() 48 | if err != nil { 49 | return ArrayInsertionIndex{}, err 50 | } 51 | 52 | if after && num != len(i.Array) { 53 | num += 1 54 | } 55 | 56 | return ArrayInsertionIndex{num, before || after}, nil 57 | } 58 | 59 | func (i ArrayInsertionIndex) Update(array []interface{}, obj interface{}) []interface{} { 60 | if i.insert { 61 | newAry := []interface{}{} 62 | newAry = append(newAry, array[:i.number]...) // not inclusive 63 | newAry = append(newAry, obj) 64 | newAry = append(newAry, array[i.number:]...) // inclusive 65 | return newAry 66 | } 67 | 68 | array[i.number] = obj 69 | return array 70 | } 71 | -------------------------------------------------------------------------------- /patch/array_insertion_test.go: -------------------------------------------------------------------------------- 1 | package patch_test 2 | 3 | import ( 4 | . "github.com/onsi/ginkgo" 5 | . "github.com/onsi/gomega" 6 | 7 | . "github.com/cppforlife/go-patch/patch" 8 | ) 9 | 10 | var _ = Describe("ArrayInsertion", func() { 11 | Describe("Concrete", func() { 12 | act := func(insertion ArrayInsertion, array []interface{}, obj interface{}) ([]interface{}, error) { 13 | insertion.Array = array 14 | 15 | idx, err := insertion.Concrete() 16 | if err != nil { 17 | return nil, err 18 | } 19 | 20 | return idx.Update(array, obj), nil 21 | } 22 | 23 | It("returns specified index when not using any modifiers", func() { 24 | result, err := act(ArrayInsertion{Index: 1, Modifiers: []Modifier{}}, []interface{}{1, 2, 3}, 10) 25 | Expect(err).ToNot(HaveOccurred()) 26 | Expect(result).To(Equal([]interface{}{1, 10, 3})) 27 | }) 28 | 29 | It("returns index adjusted for previous and next modifiers", func() { 30 | p := PrevModifier{} 31 | n := NextModifier{} 32 | 33 | result, err := act(ArrayInsertion{Index: 1, Modifiers: []Modifier{p, n, n}}, []interface{}{1, 2, 3}, 10) 34 | Expect(err).ToNot(HaveOccurred()) 35 | Expect(result).To(Equal([]interface{}{1, 2, 10})) 36 | }) 37 | 38 | It("returns error if both after and before are used", func() { 39 | _, err := act(ArrayInsertion{Index: 0, Modifiers: []Modifier{BeforeModifier{}, AfterModifier{}}}, []interface{}{}, 10) 40 | Expect(err.Error()).To(Equal("Expected to not find any modifiers after 'before' modifier, but found modifier 'patch.AfterModifier'")) 41 | 42 | _, err = act(ArrayInsertion{Index: 0, Modifiers: []Modifier{AfterModifier{}, BeforeModifier{}}}, []interface{}{}, 10) 43 | Expect(err.Error()).To(Equal("Expected to not find any modifiers after 'after' modifier, but found modifier 'patch.BeforeModifier'")) 44 | 45 | _, err = act(ArrayInsertion{Index: 0, Modifiers: []Modifier{AfterModifier{}, PrevModifier{}}}, []interface{}{}, 10) 46 | Expect(err.Error()).To(Equal("Expected to not find any modifiers after 'after' modifier, but found modifier 'patch.PrevModifier'")) 47 | }) 48 | 49 | It("returns (0, true) when inserting in the beginning", func() { 50 | result, err := act(ArrayInsertion{Index: 0, Modifiers: []Modifier{BeforeModifier{}}}, []interface{}{1, 2, 3}, 10) 51 | Expect(err).ToNot(HaveOccurred()) 52 | Expect(result).To(Equal([]interface{}{10, 1, 2, 3})) 53 | 54 | result, err = act(ArrayInsertion{Index: 0, Modifiers: []Modifier{AfterModifier{}}}, []interface{}{1, 2, 3}, 10) 55 | Expect(err).ToNot(HaveOccurred()) 56 | Expect(result).To(Equal([]interface{}{1, 10, 2, 3})) 57 | }) 58 | 59 | It("returns (last, true) when inserting in the end", func() { 60 | result, err := act(ArrayInsertion{Index: 2, Modifiers: []Modifier{AfterModifier{}}}, []interface{}{1, 2, 3}, 10) 61 | Expect(err).ToNot(HaveOccurred()) 62 | Expect(result).To(Equal([]interface{}{1, 2, 3, 10})) 63 | 64 | result, err = act(ArrayInsertion{Index: -1, Modifiers: []Modifier{AfterModifier{}}}, []interface{}{1, 2, 3}, 10) 65 | Expect(err).ToNot(HaveOccurred()) 66 | Expect(result).To(Equal([]interface{}{1, 2, 3, 10})) 67 | }) 68 | 69 | It("returns (mid, true) when inserting in the middle", func() { 70 | result, err := act(ArrayInsertion{Index: 1, Modifiers: []Modifier{AfterModifier{}}}, []interface{}{1, 2, 3}, 10) 71 | Expect(err).ToNot(HaveOccurred()) 72 | Expect(result).To(Equal([]interface{}{1, 2, 10, 3})) 73 | 74 | result, err = act(ArrayInsertion{Index: 1, Modifiers: []Modifier{BeforeModifier{}}}, []interface{}{1, 2, 3}, 10) 75 | Expect(err).ToNot(HaveOccurred()) 76 | Expect(result).To(Equal([]interface{}{1, 10, 2, 3})) 77 | 78 | result, err = act(ArrayInsertion{Index: 2, Modifiers: []Modifier{BeforeModifier{}}}, []interface{}{1, 2, 3}, 10) 79 | Expect(err).ToNot(HaveOccurred()) 80 | Expect(result).To(Equal([]interface{}{1, 2, 10, 3})) 81 | }) 82 | 83 | It("returns index adjusted for previous, next modifiers and before modifier", func() { 84 | p := PrevModifier{} 85 | n := NextModifier{} 86 | b := BeforeModifier{} 87 | 88 | result, err := act(ArrayInsertion{Index: 1, Modifiers: []Modifier{p, n, n, b}}, []interface{}{1, 2, 3}, 10) 89 | Expect(err).ToNot(HaveOccurred()) 90 | Expect(result).To(Equal([]interface{}{1, 2, 10, 3})) 91 | }) 92 | 93 | It("returns index adjusted for previous, next modifiers and after modifier", func() { 94 | p := PrevModifier{} 95 | n := NextModifier{} 96 | a := AfterModifier{} 97 | 98 | result, err := act(ArrayInsertion{Index: 1, Modifiers: []Modifier{p, n, n, a}}, []interface{}{1, 2, 3}, 10) 99 | Expect(err).ToNot(HaveOccurred()) 100 | Expect(result).To(Equal([]interface{}{1, 2, 3, 10})) 101 | }) 102 | }) 103 | }) 104 | -------------------------------------------------------------------------------- /patch/descriptive_op.go: -------------------------------------------------------------------------------- 1 | package patch 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | type DescriptiveOp struct { 8 | Op Op 9 | ErrorMsg string 10 | } 11 | 12 | func (op DescriptiveOp) Apply(doc interface{}) (interface{}, error) { 13 | doc, err := op.Op.Apply(doc) 14 | if err != nil { 15 | return nil, fmt.Errorf("Error '%s': %s", op.ErrorMsg, err.Error()) 16 | } 17 | return doc, nil 18 | } 19 | -------------------------------------------------------------------------------- /patch/diff.go: -------------------------------------------------------------------------------- 1 | package patch 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "sort" 7 | 8 | "gopkg.in/yaml.v2" 9 | ) 10 | 11 | type Diff struct { 12 | Left interface{} 13 | Right interface{} 14 | Unchecked bool 15 | } 16 | 17 | func (d Diff) Calculate() Ops { 18 | ops := d.calculate(d.Left, d.Right, []Token{RootToken{}}) 19 | if !d.Unchecked { 20 | return ops 21 | } 22 | 23 | newOps := []Op{} 24 | for _, op := range ops { 25 | if _, ok := op.(TestOp); !ok { 26 | newOps = append(newOps, op) 27 | } 28 | } 29 | return newOps 30 | } 31 | 32 | func (d Diff) calculate(left, right interface{}, tokens []Token) []Op { 33 | switch typedLeft := left.(type) { 34 | case map[string]interface{}: 35 | if typedRight, ok := right.(map[string]interface{}); ok { 36 | ops := []Op{} 37 | var allKeys []string 38 | for k := range typedLeft { 39 | allKeys = append(allKeys, k) 40 | } 41 | for k := range typedRight { 42 | if _, found := typedLeft[k]; !found { 43 | allKeys = append(allKeys, k) 44 | } 45 | } 46 | sort.SliceStable(allKeys, func(i, j int) bool { 47 | return allKeys[i] < allKeys[j] 48 | }) 49 | for _, k := range allKeys { 50 | newTokens := append([]Token{}, tokens...) 51 | if leftVal, found := typedLeft[k]; found { 52 | newTokens = append(newTokens, KeyToken{Key: k}) 53 | if rightVal, found := typedRight[k]; found { 54 | ops = append(ops, d.calculate(leftVal, rightVal, newTokens)...) 55 | } else { // remove existing 56 | ops = append(ops, 57 | TestOp{Path: NewPointer(newTokens), Value: leftVal}, 58 | RemoveOp{Path: NewPointer(newTokens)}, 59 | ) 60 | } 61 | } else { // add new 62 | testOpTokens := append([]Token{}, newTokens...) 63 | testOpTokens = append(testOpTokens, KeyToken{Key: k}) 64 | newTokens = append(newTokens, KeyToken{Key: k, Optional: true}) 65 | ops = append(ops, 66 | TestOp{Path: NewPointer(testOpTokens), Absent: true}, 67 | ReplaceOp{Path: NewPointer(newTokens), Value: typedRight[k]}, 68 | ) 69 | } 70 | } 71 | return ops 72 | } 73 | return []Op{ 74 | TestOp{Path: NewPointer(tokens), Value: left}, 75 | ReplaceOp{Path: NewPointer(tokens), Value: right}, 76 | } 77 | 78 | case map[interface{}]interface{}: 79 | if typedRight, ok := right.(map[interface{}]interface{}); ok { 80 | ops := []Op{} 81 | var allKeys []interface{} 82 | for k, _ := range typedLeft { 83 | allKeys = append(allKeys, k) 84 | } 85 | for k, _ := range typedRight { 86 | if _, found := typedLeft[k]; !found { 87 | allKeys = append(allKeys, k) 88 | } 89 | } 90 | sort.SliceStable(allKeys, func(i, j int) bool { 91 | iBs, _ := yaml.Marshal(allKeys[i]) 92 | jBs, _ := yaml.Marshal(allKeys[j]) 93 | return string(iBs) < string(jBs) 94 | }) 95 | for _, k := range allKeys { 96 | newTokens := append([]Token{}, tokens...) 97 | if leftVal, found := typedLeft[k]; found { 98 | newTokens = append(newTokens, KeyToken{Key: fmt.Sprintf("%s", k)}) 99 | if rightVal, found := typedRight[k]; found { 100 | ops = append(ops, d.calculate(leftVal, rightVal, newTokens)...) 101 | } else { // remove existing 102 | ops = append(ops, 103 | TestOp{Path: NewPointer(newTokens), Value: leftVal}, 104 | RemoveOp{Path: NewPointer(newTokens)}, 105 | ) 106 | } 107 | } else { // add new 108 | testOpTokens := append([]Token{}, newTokens...) 109 | testOpTokens = append(testOpTokens, KeyToken{Key: fmt.Sprintf("%s", k)}) 110 | newTokens = append(newTokens, KeyToken{Key: fmt.Sprintf("%s", k), Optional: true}) 111 | ops = append(ops, 112 | TestOp{Path: NewPointer(testOpTokens), Absent: true}, 113 | ReplaceOp{Path: NewPointer(newTokens), Value: typedRight[k]}, 114 | ) 115 | } 116 | } 117 | return ops 118 | } 119 | return []Op{ 120 | TestOp{Path: NewPointer(tokens), Value: left}, 121 | ReplaceOp{Path: NewPointer(tokens), Value: right}, 122 | } 123 | 124 | case []interface{}: 125 | if typedRight, ok := right.([]interface{}); ok { 126 | ops := []Op{} 127 | actualIndex := 0 128 | for i := 0; i < max(len(typedLeft), len(typedRight)); i++ { 129 | newTokens := append([]Token{}, tokens...) 130 | switch { 131 | case i >= len(typedRight): // remove existing 132 | newTokens = append(newTokens, IndexToken{Index: actualIndex}) 133 | ops = append(ops, 134 | TestOp{Path: NewPointer(newTokens), Value: typedLeft[i]}, // capture actual value at index 135 | RemoveOp{Path: NewPointer(newTokens)}, 136 | ) 137 | // keep actualIndex the same 138 | case i >= len(typedLeft): // add new 139 | testOpTokens := append([]Token{}, newTokens...) 140 | testOpTokens = append(testOpTokens, IndexToken{Index: i}) // use actual index 141 | newTokens = append(newTokens, AfterLastIndexToken{}) 142 | ops = append(ops, 143 | TestOp{Path: NewPointer(testOpTokens), Absent: true}, 144 | ReplaceOp{Path: NewPointer(newTokens), Value: typedRight[i]}, 145 | ) 146 | actualIndex++ 147 | default: 148 | newTokens = append(newTokens, IndexToken{Index: actualIndex}) 149 | ops = append(ops, d.calculate(typedLeft[i], typedRight[i], newTokens)...) 150 | actualIndex++ 151 | } 152 | } 153 | return ops 154 | } 155 | return []Op{ 156 | TestOp{Path: NewPointer(tokens), Value: left}, 157 | ReplaceOp{Path: NewPointer(tokens), Value: right}, 158 | } 159 | 160 | default: 161 | if !reflect.DeepEqual(jsonToYAMLValue(left), jsonToYAMLValue(right)) { 162 | return []Op{ 163 | TestOp{Path: NewPointer(tokens), Value: left}, 164 | ReplaceOp{Path: NewPointer(tokens), Value: right}, 165 | } 166 | } 167 | } 168 | 169 | return []Op{} 170 | } 171 | 172 | // The Go JSON library doesn't try to pick the right number type (int, float, 173 | // etc.) when unmarshalling to interface{}, it just picks float64 174 | // universally 175 | func jsonToYAMLValue(j interface{}) interface{} { 176 | switch j := j.(type) { 177 | case float64: 178 | // replicate the logic in https://github.com/go-yaml/yaml/blob/51d6538a90f86fe93ac480b35f37b2be17fef232/resolve.go#L151 179 | if i64 := int64(j); j == float64(i64) { 180 | if i := int(i64); i64 == int64(i) { 181 | return i 182 | } 183 | return i64 184 | } 185 | if ui64 := uint64(j); j == float64(ui64) { 186 | return ui64 187 | } 188 | return j 189 | case int64: 190 | if i := int(j); j == int64(i) { 191 | return i 192 | } 193 | return j 194 | } 195 | return j 196 | } 197 | 198 | func max(a, b int) int { 199 | if a > b { 200 | return a 201 | } 202 | return b 203 | } 204 | -------------------------------------------------------------------------------- /patch/diff_test.go: -------------------------------------------------------------------------------- 1 | package patch_test 2 | 3 | import ( 4 | . "github.com/onsi/ginkgo" 5 | . "github.com/onsi/gomega" 6 | 7 | . "github.com/cppforlife/go-patch/patch" 8 | ) 9 | 10 | var _ = Describe("Diff.Calculate", func() { 11 | testDiff := func(left, right interface{}, expectedOps []Op) { 12 | diffOps := Diff{Left: left, Right: right}.Calculate() 13 | Expect(diffOps).To(Equal(Ops(expectedOps))) 14 | 15 | result, err := Ops(diffOps).Apply(left) 16 | Expect(err).ToNot(HaveOccurred()) 17 | 18 | if right == nil { // gomega does not allow nil==nil comparison 19 | Expect(result).To(BeNil()) 20 | } else { 21 | Expect(result).To(Equal(right)) 22 | } 23 | } 24 | 25 | It("returns no ops if both docs are same", func() { 26 | testDiff(nil, nil, []Op{}) 27 | 28 | testDiff( 29 | map[interface{}]interface{}{"a": 124}, 30 | map[interface{}]interface{}{"a": 124}, 31 | []Op{}, 32 | ) 33 | 34 | testDiff( 35 | []interface{}{"a", 124}, 36 | []interface{}{"a", 124}, 37 | []Op{}, 38 | ) 39 | }) 40 | 41 | It("can skip test operations", func() { 42 | expectedOps := []Op{ 43 | ReplaceOp{Path: MustNewPointerFromString(""), Value: "a"}, 44 | } 45 | 46 | var left interface{} 47 | right := "a" 48 | 49 | diffOps := Diff{Left: left, Right: right, Unchecked: true}.Calculate() 50 | Expect(diffOps).To(Equal(Ops(expectedOps))) 51 | 52 | result, err := Ops(diffOps).Apply(left) 53 | Expect(err).ToNot(HaveOccurred()) 54 | 55 | Expect(result).To(Equal(right)) 56 | }) 57 | 58 | It("can replace doc root with nil", func() { 59 | testDiff("a", nil, []Op{ 60 | TestOp{Path: MustNewPointerFromString(""), Value: "a"}, 61 | ReplaceOp{Path: MustNewPointerFromString(""), Value: nil}, 62 | }) 63 | }) 64 | 65 | It("can replace doc root", func() { 66 | testDiff(nil, "a", []Op{ 67 | TestOp{Path: MustNewPointerFromString(""), Value: nil}, 68 | ReplaceOp{Path: MustNewPointerFromString(""), Value: "a"}, 69 | }) 70 | }) 71 | 72 | It("can diff maps", func() { 73 | testDiff( 74 | map[interface{}]interface{}{"a": 123}, 75 | map[interface{}]interface{}{"a": 124}, 76 | []Op{ 77 | TestOp{Path: MustNewPointerFromString("/a"), Value: 123}, 78 | ReplaceOp{Path: MustNewPointerFromString("/a"), Value: 124}, 79 | }, 80 | ) 81 | 82 | testDiff( 83 | map[interface{}]interface{}{"a": 123, "b": 456}, 84 | map[interface{}]interface{}{"a": 124, "c": 456}, 85 | []Op{ 86 | TestOp{Path: MustNewPointerFromString("/a"), Value: 123}, 87 | ReplaceOp{Path: MustNewPointerFromString("/a"), Value: 124}, 88 | TestOp{Path: MustNewPointerFromString("/b"), Value: 456}, 89 | RemoveOp{Path: MustNewPointerFromString("/b")}, 90 | TestOp{Path: MustNewPointerFromString("/c"), Absent: true}, 91 | ReplaceOp{Path: MustNewPointerFromString("/c?"), Value: 456}, 92 | }, 93 | ) 94 | 95 | testDiff( 96 | map[interface{}]interface{}{"a": 123, "b": 456}, 97 | map[interface{}]interface{}{"a": 124}, 98 | []Op{ 99 | TestOp{Path: MustNewPointerFromString("/a"), Value: 123}, 100 | ReplaceOp{Path: MustNewPointerFromString("/a"), Value: 124}, 101 | TestOp{Path: MustNewPointerFromString("/b"), Value: 456}, 102 | RemoveOp{Path: MustNewPointerFromString("/b")}, 103 | }, 104 | ) 105 | 106 | testDiff( 107 | map[interface{}]interface{}{"a": 123, "b": 456}, 108 | map[interface{}]interface{}{}, 109 | []Op{ 110 | TestOp{Path: MustNewPointerFromString("/a"), Value: 123}, 111 | RemoveOp{Path: MustNewPointerFromString("/a")}, 112 | TestOp{Path: MustNewPointerFromString("/b"), Value: 456}, 113 | RemoveOp{Path: MustNewPointerFromString("/b")}, 114 | }, 115 | ) 116 | 117 | testDiff( 118 | map[interface{}]interface{}{"a": 123}, 119 | map[interface{}]interface{}{"a": nil}, 120 | []Op{ 121 | TestOp{Path: MustNewPointerFromString("/a"), Value: 123}, 122 | ReplaceOp{Path: MustNewPointerFromString("/a"), Value: nil}, 123 | }, 124 | ) 125 | 126 | testDiff( 127 | map[interface{}]interface{}{"a": 123, "b": map[interface{}]interface{}{"a": 1024, "b": 4056}}, 128 | map[interface{}]interface{}{"a": 124, "b": map[interface{}]interface{}{"a": 1024, "c": 4056}}, 129 | []Op{ 130 | TestOp{Path: MustNewPointerFromString("/a"), Value: 123}, 131 | ReplaceOp{Path: MustNewPointerFromString("/a"), Value: 124}, 132 | TestOp{Path: MustNewPointerFromString("/b/b"), Value: 4056}, 133 | RemoveOp{Path: MustNewPointerFromString("/b/b")}, 134 | TestOp{Path: MustNewPointerFromString("/b/c"), Absent: true}, 135 | ReplaceOp{Path: MustNewPointerFromString("/b/c?"), Value: 4056}, 136 | }, 137 | ) 138 | 139 | testDiff( 140 | map[interface{}]interface{}{"a": 123}, 141 | "a", 142 | []Op{ 143 | TestOp{Path: MustNewPointerFromString(""), Value: map[interface{}]interface{}{"a": 123}}, 144 | ReplaceOp{Path: MustNewPointerFromString(""), Value: "a"}, 145 | }, 146 | ) 147 | 148 | testDiff( 149 | "a", 150 | map[interface{}]interface{}{"a": 123}, 151 | []Op{ 152 | TestOp{Path: MustNewPointerFromString(""), Value: "a"}, 153 | ReplaceOp{Path: MustNewPointerFromString(""), Value: map[interface{}]interface{}{"a": 123}}, 154 | }, 155 | ) 156 | }) 157 | 158 | It("can diff arrays", func() { 159 | testDiff( 160 | []interface{}{"a", 123}, 161 | []interface{}{"b", 123}, 162 | []Op{ 163 | TestOp{Path: MustNewPointerFromString("/0"), Value: "a"}, 164 | ReplaceOp{Path: MustNewPointerFromString("/0"), Value: "b"}, 165 | }, 166 | ) 167 | 168 | testDiff( 169 | []interface{}{"a"}, 170 | []interface{}{"b", 123, 456}, 171 | []Op{ 172 | TestOp{Path: MustNewPointerFromString("/0"), Value: "a"}, 173 | ReplaceOp{Path: MustNewPointerFromString("/0"), Value: "b"}, 174 | TestOp{Path: MustNewPointerFromString("/1"), Absent: true}, 175 | ReplaceOp{Path: MustNewPointerFromString("/-"), Value: 123}, 176 | TestOp{Path: MustNewPointerFromString("/2"), Absent: true}, 177 | ReplaceOp{Path: MustNewPointerFromString("/-"), Value: 456}, 178 | }, 179 | ) 180 | 181 | testDiff( 182 | []interface{}{"a", 123, 456}, 183 | []interface{}{"b"}, 184 | []Op{ 185 | TestOp{Path: MustNewPointerFromString("/0"), Value: "a"}, 186 | ReplaceOp{Path: MustNewPointerFromString("/0"), Value: "b"}, 187 | TestOp{Path: MustNewPointerFromString("/1"), Value: 123}, 188 | RemoveOp{Path: MustNewPointerFromString("/1")}, 189 | TestOp{Path: MustNewPointerFromString("/1"), Value: 456}, 190 | RemoveOp{Path: MustNewPointerFromString("/1")}, 191 | }, 192 | ) 193 | 194 | testDiff( 195 | []interface{}{123, 456}, 196 | []interface{}{}, 197 | []Op{ 198 | TestOp{Path: MustNewPointerFromString("/0"), Value: 123}, 199 | RemoveOp{Path: MustNewPointerFromString("/0")}, 200 | TestOp{Path: MustNewPointerFromString("/0"), Value: 456}, 201 | RemoveOp{Path: MustNewPointerFromString("/0")}, 202 | }, 203 | ) 204 | 205 | testDiff( 206 | []interface{}{123, 456}, 207 | []interface{}{123, "a", 456}, // TODO unoptimized insertion 208 | []Op{ 209 | TestOp{Path: MustNewPointerFromString("/1"), Value: 456}, 210 | ReplaceOp{Path: MustNewPointerFromString("/1"), Value: "a"}, 211 | TestOp{Path: MustNewPointerFromString("/2"), Absent: true}, 212 | ReplaceOp{Path: MustNewPointerFromString("/-"), Value: 456}, 213 | }, 214 | ) 215 | 216 | testDiff( 217 | []interface{}{[]interface{}{456, 789}}, 218 | []interface{}{[]interface{}{789}}, // TODO unoptimized deletion 219 | []Op{ 220 | TestOp{Path: MustNewPointerFromString("/0/0"), Value: 456}, 221 | ReplaceOp{Path: MustNewPointerFromString("/0/0"), Value: 789}, 222 | TestOp{Path: MustNewPointerFromString("/0/1"), Value: 789}, 223 | RemoveOp{Path: MustNewPointerFromString("/0/1")}, 224 | }, 225 | ) 226 | 227 | testDiff( 228 | []interface{}{"a", 123}, 229 | "a", 230 | []Op{ 231 | TestOp{Path: MustNewPointerFromString(""), Value: []interface{}{"a", 123}}, 232 | ReplaceOp{Path: MustNewPointerFromString(""), Value: "a"}, 233 | }, 234 | ) 235 | 236 | testDiff( 237 | "a", 238 | []interface{}{"a", 123}, 239 | []Op{ 240 | TestOp{Path: MustNewPointerFromString(""), Value: "a"}, 241 | ReplaceOp{Path: MustNewPointerFromString(""), Value: []interface{}{"a", 123}}, 242 | }, 243 | ) 244 | }) 245 | }) 246 | -------------------------------------------------------------------------------- /patch/err_op.go: -------------------------------------------------------------------------------- 1 | package patch 2 | 3 | type ErrOp struct { 4 | Err error 5 | } 6 | 7 | func (op ErrOp) Apply(_ interface{}) (interface{}, error) { 8 | return nil, op.Err 9 | } 10 | -------------------------------------------------------------------------------- /patch/errs.go: -------------------------------------------------------------------------------- 1 | package patch 2 | 3 | import ( 4 | "fmt" 5 | "sort" 6 | "strings" 7 | ) 8 | 9 | type OpMismatchTypeErr struct { 10 | Type_ string 11 | Path Pointer 12 | Obj interface{} 13 | } 14 | 15 | func NewOpArrayMismatchTypeErr(path Pointer, obj interface{}) OpMismatchTypeErr { 16 | return OpMismatchTypeErr{"an array", path, obj} 17 | } 18 | 19 | func NewOpMapMismatchTypeErr(path Pointer, obj interface{}) OpMismatchTypeErr { 20 | return OpMismatchTypeErr{"a map", path, obj} 21 | } 22 | 23 | func (e OpMismatchTypeErr) Error() string { 24 | errMsg := "Expected to find %s at path '%s' but found '%T'" 25 | return fmt.Sprintf(errMsg, e.Type_, e.Path, e.Obj) 26 | } 27 | 28 | type OpMissingMapKeyErr struct { 29 | Key string 30 | Path Pointer 31 | Obj map[interface{}]interface{} 32 | } 33 | 34 | func (e OpMissingMapKeyErr) Error() string { 35 | errMsg := "Expected to find a map key '%s' for path '%s' (%s)" 36 | return fmt.Sprintf(errMsg, e.Key, e.Path, e.siblingKeysErrStr()) 37 | } 38 | 39 | func (e OpMissingMapKeyErr) siblingKeysErrStr() string { 40 | if len(e.Obj) == 0 { 41 | return "found no other map keys" 42 | } 43 | var keys []string 44 | for key, _ := range e.Obj { 45 | if keyStr, ok := key.(string); ok { 46 | keys = append(keys, keyStr) 47 | } 48 | } 49 | sort.Sort(sort.StringSlice(keys)) 50 | return "found map keys: '" + strings.Join(keys, "', '") + "'" 51 | } 52 | 53 | type OpMissingIndexErr struct { 54 | Idx int 55 | Obj []interface{} 56 | Path Pointer 57 | } 58 | 59 | func (e OpMissingIndexErr) Error() string { 60 | 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) 61 | } 62 | 63 | type OpMultipleMatchingIndexErr struct { 64 | Path Pointer 65 | Idxs []int 66 | } 67 | 68 | func (e OpMultipleMatchingIndexErr) Error() string { 69 | return fmt.Sprintf("Expected to find exactly one matching array item for path '%s' but found %d", e.Path, len(e.Idxs)) 70 | } 71 | 72 | type OpUnexpectedTokenErr struct { 73 | Token Token 74 | Path Pointer 75 | } 76 | 77 | func (e OpUnexpectedTokenErr) Error() string { 78 | return fmt.Sprintf("Expected to not find token '%T' at path '%s'", e.Token, e.Path) 79 | } 80 | -------------------------------------------------------------------------------- /patch/find_op.go: -------------------------------------------------------------------------------- 1 | package patch 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | type FindOp struct { 8 | Path Pointer 9 | } 10 | 11 | func (op FindOp) Apply(doc interface{}) (interface{}, error) { 12 | tokens := op.Path.Tokens() 13 | 14 | if len(tokens) == 1 { 15 | return doc, nil 16 | } 17 | 18 | obj := doc 19 | 20 | for i, token := range tokens[1:] { 21 | isLast := i == len(tokens)-2 22 | currPath := NewPointer(tokens[:i+2]) 23 | 24 | switch typedToken := token.(type) { 25 | case IndexToken: 26 | typedObj, ok := obj.([]interface{}) 27 | if !ok { 28 | return nil, NewOpArrayMismatchTypeErr(currPath, obj) 29 | } 30 | 31 | idx, err := ArrayIndex{Index: typedToken.Index, Modifiers: typedToken.Modifiers, Array: typedObj, Path: currPath}.Concrete() 32 | if err != nil { 33 | return nil, err 34 | } 35 | 36 | if isLast { 37 | return typedObj[idx], nil 38 | } else { 39 | obj = typedObj[idx] 40 | } 41 | 42 | case AfterLastIndexToken: 43 | errMsg := "Expected not to find after last index token in path '%s' (not supported in find operations)" 44 | return nil, fmt.Errorf(errMsg, op.Path) 45 | 46 | case MatchingIndexToken: 47 | typedObj, ok := obj.([]interface{}) 48 | if !ok { 49 | return nil, NewOpArrayMismatchTypeErr(currPath, obj) 50 | } 51 | 52 | var idxs []int 53 | 54 | for itemIdx, item := range typedObj { 55 | typedItem, ok := item.(map[interface{}]interface{}) 56 | if ok { 57 | if typedItem[typedToken.Key] == typedToken.Value { 58 | idxs = append(idxs, itemIdx) 59 | } 60 | } 61 | } 62 | 63 | if typedToken.Optional && len(idxs) == 0 { 64 | // todo /blah=foo?:after, modifiers 65 | obj = map[interface{}]interface{}{typedToken.Key: typedToken.Value} 66 | 67 | if isLast { 68 | return obj, nil 69 | } 70 | } else { 71 | if len(idxs) != 1 { 72 | return nil, OpMultipleMatchingIndexErr{currPath, idxs} 73 | } 74 | 75 | idx, err := ArrayIndex{Index: idxs[0], Modifiers: typedToken.Modifiers, Array: typedObj, Path: currPath}.Concrete() 76 | if err != nil { 77 | return nil, err 78 | } 79 | 80 | if isLast { 81 | return typedObj[idx], nil 82 | } else { 83 | obj = typedObj[idx] 84 | } 85 | } 86 | 87 | case KeyToken: 88 | typedObj, ok := obj.(map[interface{}]interface{}) 89 | if !ok { 90 | return nil, NewOpMapMismatchTypeErr(currPath, obj) 91 | } 92 | 93 | var found bool 94 | 95 | obj, found = typedObj[typedToken.Key] 96 | if !found && !typedToken.Optional { 97 | return nil, OpMissingMapKeyErr{typedToken.Key, currPath, typedObj} 98 | } 99 | 100 | if isLast { 101 | return typedObj[typedToken.Key], nil 102 | } else { 103 | if !found { 104 | // Determine what type of value to create based on next token 105 | switch tokens[i+2].(type) { 106 | case MatchingIndexToken: 107 | obj = []interface{}{} 108 | case KeyToken: 109 | obj = map[interface{}]interface{}{} 110 | default: 111 | errMsg := "Expected to find key or matching index token at path '%s'" 112 | return nil, fmt.Errorf(errMsg, NewPointer(tokens[:i+3])) 113 | } 114 | } 115 | } 116 | 117 | default: 118 | return nil, OpUnexpectedTokenErr{token, currPath} 119 | } 120 | } 121 | 122 | return doc, nil 123 | } 124 | -------------------------------------------------------------------------------- /patch/find_op_test.go: -------------------------------------------------------------------------------- 1 | package patch_test 2 | 3 | import ( 4 | . "github.com/onsi/ginkgo" 5 | . "github.com/onsi/gomega" 6 | 7 | . "github.com/cppforlife/go-patch/patch" 8 | ) 9 | 10 | var _ = Describe("FindOp.Apply", func() { 11 | It("returns document if path is for the entire document", func() { 12 | res, err := FindOp{Path: MustNewPointerFromString("")}.Apply("a") 13 | Expect(err).ToNot(HaveOccurred()) 14 | Expect(res).To(Equal("a")) 15 | }) 16 | 17 | Describe("array item", func() { 18 | It("finds array item", func() { 19 | res, err := FindOp{Path: MustNewPointerFromString("/0")}.Apply([]interface{}{1, 2, 3}) 20 | Expect(err).ToNot(HaveOccurred()) 21 | Expect(res).To(Equal(1)) 22 | 23 | res, err = FindOp{Path: MustNewPointerFromString("/1")}.Apply([]interface{}{1, 2, 3}) 24 | Expect(err).ToNot(HaveOccurred()) 25 | Expect(res).To(Equal(2)) 26 | 27 | res, err = FindOp{Path: MustNewPointerFromString("/2")}.Apply([]interface{}{1, 2, 3}) 28 | Expect(err).ToNot(HaveOccurred()) 29 | Expect(res).To(Equal(3)) 30 | 31 | res, err = FindOp{Path: MustNewPointerFromString("/-1")}.Apply([]interface{}{1, 2, 3}) 32 | Expect(err).ToNot(HaveOccurred()) 33 | Expect(res).To(Equal(3)) 34 | }) 35 | 36 | It("finds relative array item", func() { 37 | res, err := FindOp{Path: MustNewPointerFromString("/3:prev")}.Apply([]interface{}{1, 2, 3}) 38 | Expect(err).ToNot(HaveOccurred()) 39 | Expect(res).To(Equal(3)) 40 | }) 41 | 42 | It("finds nested array item", func() { 43 | doc := []interface{}{[]interface{}{10, 11, 12}, 2, 3} 44 | 45 | res, err := FindOp{Path: MustNewPointerFromString("/0/1")}.Apply(doc) 46 | Expect(err).ToNot(HaveOccurred()) 47 | Expect(res).To(Equal(11)) 48 | }) 49 | 50 | It("finds relative nested array item", func() { 51 | doc := []interface{}{1, []interface{}{10, 11, 12}, 3} 52 | 53 | res, err := FindOp{Path: MustNewPointerFromString("/0:next/1")}.Apply(doc) 54 | Expect(err).ToNot(HaveOccurred()) 55 | Expect(res).To(Equal(11)) 56 | }) 57 | 58 | It("finds array item from an array that is inside a map", func() { 59 | doc := map[interface{}]interface{}{ 60 | "abc": []interface{}{1, 2, 3}, 61 | } 62 | 63 | res, err := FindOp{Path: MustNewPointerFromString("/abc/1")}.Apply(doc) 64 | Expect(err).ToNot(HaveOccurred()) 65 | Expect(res).To(Equal(2)) 66 | }) 67 | 68 | It("returns an error if it's not an array when index is being accessed", func() { 69 | _, err := FindOp{Path: MustNewPointerFromString("/0")}.Apply(map[interface{}]interface{}{}) 70 | Expect(err).To(HaveOccurred()) 71 | Expect(err.Error()).To(Equal( 72 | "Expected to find an array at path '/0' but found 'map[interface {}]interface {}'")) 73 | 74 | _, err = FindOp{Path: MustNewPointerFromString("/0/1")}.Apply(map[interface{}]interface{}{}) 75 | Expect(err).To(HaveOccurred()) 76 | Expect(err.Error()).To(Equal( 77 | "Expected to find an array at path '/0' but found 'map[interface {}]interface {}'")) 78 | }) 79 | 80 | It("returns an error if the index is out of bounds", func() { 81 | _, err := FindOp{Path: MustNewPointerFromString("/1")}.Apply([]interface{}{}) 82 | Expect(err).To(HaveOccurred()) 83 | Expect(err.Error()).To(Equal( 84 | "Expected to find array index '1' but found array of length '0' for path '/1'")) 85 | 86 | _, err = FindOp{Path: MustNewPointerFromString("/1/1")}.Apply([]interface{}{}) 87 | Expect(err).To(HaveOccurred()) 88 | Expect(err.Error()).To(Equal( 89 | "Expected to find array index '1' but found array of length '0' for path '/1'")) 90 | }) 91 | }) 92 | 93 | Describe("array with after last item", func() { 94 | It("returns an error as after-last-index tokens are not supported", func() { 95 | _, err := FindOp{Path: MustNewPointerFromString("/-")}.Apply([]interface{}{}) 96 | Expect(err).To(HaveOccurred()) 97 | Expect(err.Error()).To(Equal( 98 | "Expected not to find after last index token in path '/-' (not supported in find operations)")) 99 | }) 100 | 101 | It("returns an error for nested array item", func() { 102 | doc := []interface{}{[]interface{}{10, 11, 12}, 2, 3} 103 | 104 | _, err := FindOp{Path: MustNewPointerFromString("/0/-")}.Apply(doc) 105 | Expect(err).To(HaveOccurred()) 106 | Expect(err.Error()).To(Equal( 107 | "Expected not to find after last index token in path '/0/-' (not supported in find operations)")) 108 | }) 109 | 110 | It("returns an error array item from an array that is inside a map", func() { 111 | doc := map[interface{}]interface{}{ 112 | "abc": []interface{}{1, 2, 3}, 113 | } 114 | 115 | _, err := FindOp{Path: MustNewPointerFromString("/abc/-")}.Apply(doc) 116 | Expect(err).To(HaveOccurred()) 117 | Expect(err.Error()).To(Equal( 118 | "Expected not to find after last index token in path '/abc/-' (not supported in find operations)")) 119 | }) 120 | 121 | It("returns an error if after last index token is not last", func() { 122 | ptr := NewPointer([]Token{RootToken{}, AfterLastIndexToken{}, KeyToken{}}) 123 | 124 | _, err := FindOp{Path: ptr}.Apply([]interface{}{}) 125 | Expect(err).To(HaveOccurred()) 126 | Expect(err.Error()).To(Equal( 127 | "Expected not to find after last index token in path '/-/' (not supported in find operations)")) 128 | }) 129 | 130 | It("returns an error if it's not an array being accessed", func() { 131 | _, err := FindOp{Path: MustNewPointerFromString("/-")}.Apply(map[interface{}]interface{}{}) 132 | Expect(err).To(HaveOccurred()) 133 | Expect(err.Error()).To(Equal( 134 | "Expected not to find after last index token in path '/-' (not supported in find operations)")) 135 | 136 | doc := map[interface{}]interface{}{"key": map[interface{}]interface{}{}} 137 | 138 | _, err = FindOp{Path: MustNewPointerFromString("/key/-")}.Apply(doc) 139 | Expect(err).To(HaveOccurred()) 140 | Expect(err.Error()).To(Equal( 141 | "Expected not to find after last index token in path '/key/-' (not supported in find operations)")) 142 | }) 143 | }) 144 | 145 | Describe("array item with matching key and value", func() { 146 | It("finds array item if found", func() { 147 | doc := []interface{}{ 148 | map[interface{}]interface{}{"key": "val"}, 149 | map[interface{}]interface{}{"key": "val2"}, 150 | } 151 | 152 | res, err := FindOp{Path: MustNewPointerFromString("/key=val")}.Apply(doc) 153 | Expect(err).ToNot(HaveOccurred()) 154 | Expect(res).To(Equal(map[interface{}]interface{}{"key": "val"})) 155 | }) 156 | 157 | It("finds relative array item", func() { 158 | doc := []interface{}{ 159 | map[interface{}]interface{}{"key": "val"}, 160 | map[interface{}]interface{}{"key": "val2"}, 161 | } 162 | 163 | res, err := FindOp{Path: MustNewPointerFromString("/key=val2:prev")}.Apply(doc) 164 | Expect(err).ToNot(HaveOccurred()) 165 | Expect(res).To(Equal(map[interface{}]interface{}{"key": "val"})) 166 | }) 167 | 168 | It("returns an error if no items found and matching is not optional", func() { 169 | doc := []interface{}{ 170 | map[interface{}]interface{}{"key": "val2"}, 171 | map[interface{}]interface{}{"key2": "val"}, 172 | } 173 | 174 | _, err := FindOp{Path: MustNewPointerFromString("/key=val")}.Apply(doc) 175 | Expect(err).To(HaveOccurred()) 176 | Expect(err.Error()).To(Equal( 177 | "Expected to find exactly one matching array item for path '/key=val' but found 0")) 178 | }) 179 | 180 | It("returns an error if multiple items found", func() { 181 | doc := []interface{}{ 182 | map[interface{}]interface{}{"key": "val"}, 183 | map[interface{}]interface{}{"key": "val"}, 184 | } 185 | 186 | _, err := FindOp{Path: MustNewPointerFromString("/key=val")}.Apply(doc) 187 | Expect(err).To(HaveOccurred()) 188 | Expect(err.Error()).To(Equal( 189 | "Expected to find exactly one matching array item for path '/key=val' but found 2")) 190 | }) 191 | 192 | It("finds array item even if not all items are maps", func() { 193 | doc := []interface{}{ 194 | 3, 195 | map[interface{}]interface{}{"key": "val"}, 196 | } 197 | 198 | res, err := FindOp{Path: MustNewPointerFromString("/key=val")}.Apply(doc) 199 | Expect(err).ToNot(HaveOccurred()) 200 | Expect(res).To(Equal(map[interface{}]interface{}{"key": "val"})) 201 | }) 202 | 203 | It("finds nested matching item", func() { 204 | doc := []interface{}{ 205 | map[interface{}]interface{}{ 206 | "key": "val", 207 | "items": []interface{}{ 208 | map[interface{}]interface{}{"nested-key": "val"}, 209 | map[interface{}]interface{}{"nested-key": "val2"}, 210 | }, 211 | }, 212 | map[interface{}]interface{}{"key": "val2"}, 213 | } 214 | 215 | res, err := FindOp{Path: MustNewPointerFromString("/key=val/items/nested-key=val")}.Apply(doc) 216 | Expect(err).ToNot(HaveOccurred()) 217 | Expect(res).To(Equal(map[interface{}]interface{}{"nested-key": "val"})) 218 | }) 219 | 220 | It("finds relative nested matching item", func() { 221 | doc := []interface{}{ 222 | map[interface{}]interface{}{ 223 | "key": "val", 224 | "items": []interface{}{ 225 | map[interface{}]interface{}{"nested-key": "val"}, 226 | map[interface{}]interface{}{"nested-key": "val2"}, 227 | }, 228 | }, 229 | map[interface{}]interface{}{"key": "val2"}, 230 | } 231 | 232 | res, err := FindOp{Path: MustNewPointerFromString("/key=val2:prev/items/nested-key=val")}.Apply(doc) 233 | Expect(err).ToNot(HaveOccurred()) 234 | Expect(res).To(Equal(map[interface{}]interface{}{"nested-key": "val"})) 235 | }) 236 | 237 | It("finds missing matching item if it does not exist", func() { 238 | doc := []interface{}{map[interface{}]interface{}{"xyz": "xyz"}} 239 | 240 | res, err := FindOp{Path: MustNewPointerFromString("/name=val?/efg")}.Apply(doc) 241 | Expect(err).ToNot(HaveOccurred()) 242 | Expect(res).To(BeNil()) 243 | }) 244 | 245 | It("finds nested missing matching item if it does not exist", func() { 246 | doc := []interface{}{map[interface{}]interface{}{"xyz": "xyz"}} 247 | 248 | res, err := FindOp{Path: MustNewPointerFromString("/name=val?/efg/name=val")}.Apply(doc) 249 | Expect(err).ToNot(HaveOccurred()) 250 | Expect(res).To(Equal(map[interface{}]interface{}{"name": "val"})) 251 | }) 252 | 253 | It("returns an error if it's not an array is being accessed", func() { 254 | _, err := FindOp{Path: MustNewPointerFromString("/key=val")}.Apply(map[interface{}]interface{}{}) 255 | Expect(err).To(HaveOccurred()) 256 | Expect(err.Error()).To(Equal( 257 | "Expected to find an array at path '/key=val' but found 'map[interface {}]interface {}'")) 258 | 259 | _, err = FindOp{Path: MustNewPointerFromString("/key=val/items/key=val")}.Apply(map[interface{}]interface{}{}) 260 | Expect(err).To(HaveOccurred()) 261 | Expect(err.Error()).To(Equal( 262 | "Expected to find an array at path '/key=val' but found 'map[interface {}]interface {}'")) 263 | }) 264 | }) 265 | 266 | Describe("map key", func() { 267 | It("finds map key", func() { 268 | doc := map[interface{}]interface{}{ 269 | "abc": "abc", 270 | "xyz": "xyz", 271 | } 272 | 273 | res, err := FindOp{Path: MustNewPointerFromString("/abc")}.Apply(doc) 274 | Expect(err).ToNot(HaveOccurred()) 275 | Expect(res).To(Equal("abc")) 276 | }) 277 | 278 | It("finds nested map key", func() { 279 | doc := map[interface{}]interface{}{ 280 | "abc": map[interface{}]interface{}{ 281 | "efg": "efg", 282 | "opr": "opr", 283 | }, 284 | "xyz": "xyz", 285 | } 286 | 287 | res, err := FindOp{Path: MustNewPointerFromString("/abc/efg")}.Apply(doc) 288 | Expect(err).ToNot(HaveOccurred()) 289 | Expect(res).To(Equal("efg")) 290 | }) 291 | 292 | It("finds nested map key that does not exist", func() { 293 | doc := map[interface{}]interface{}{ 294 | "abc": map[interface{}]interface{}{"opr": "opr"}, 295 | "xyz": "xyz", 296 | } 297 | 298 | res, err := FindOp{Path: MustNewPointerFromString("/abc/efg?")}.Apply(doc) 299 | Expect(err).ToNot(HaveOccurred()) 300 | Expect(res).To(BeNil()) 301 | }) 302 | 303 | It("finds super nested map key that does not exist", func() { 304 | doc := map[interface{}]interface{}{ 305 | "abc": map[interface{}]interface{}{ 306 | "efg": map[interface{}]interface{}{}, // wrong level 307 | }, 308 | } 309 | 310 | res, err := FindOp{Path: MustNewPointerFromString("/abc/opr?/efg")}.Apply(doc) 311 | Expect(err).ToNot(HaveOccurred()) 312 | Expect(res).To(BeNil()) 313 | }) 314 | 315 | It("returns an error if parent key does not exist", func() { 316 | doc := map[interface{}]interface{}{"xyz": "xyz"} 317 | 318 | _, err := FindOp{Path: MustNewPointerFromString("/abc/efg")}.Apply(doc) 319 | Expect(err).To(HaveOccurred()) 320 | Expect(err.Error()).To(Equal( 321 | "Expected to find a map key 'abc' for path '/abc' (found map keys: 'xyz')")) 322 | }) 323 | 324 | It("returns an error if key does not exist", func() { 325 | doc := map[interface{}]interface{}{"xyz": "xyz", 123: "xyz", "other-xyz": "xyz"} 326 | 327 | _, err := FindOp{Path: MustNewPointerFromString("/abc")}.Apply(doc) 328 | Expect(err).To(HaveOccurred()) 329 | Expect(err.Error()).To(Equal( 330 | "Expected to find a map key 'abc' for path '/abc' (found map keys: 'other-xyz', 'xyz')")) 331 | }) 332 | 333 | It("returns an error without other found keys when there are no keys and key does not exist", func() { 334 | doc := map[interface{}]interface{}{} 335 | 336 | _, err := FindOp{Path: MustNewPointerFromString("/abc")}.Apply(doc) 337 | Expect(err).To(HaveOccurred()) 338 | Expect(err.Error()).To(Equal( 339 | "Expected to find a map key 'abc' for path '/abc' (found no other map keys)")) 340 | }) 341 | 342 | It("returns nil for missing key if key is not expected to exist", func() { 343 | doc := map[interface{}]interface{}{"xyz": "xyz"} 344 | 345 | res, err := FindOp{Path: MustNewPointerFromString("/abc?/efg")}.Apply(doc) 346 | Expect(err).ToNot(HaveOccurred()) 347 | Expect(res).To(BeNil()) 348 | }) 349 | 350 | It("returns nil for nested missing keys if key is not expected to exist", func() { 351 | doc := map[interface{}]interface{}{"xyz": "xyz"} 352 | 353 | res, err := FindOp{Path: MustNewPointerFromString("/abc?/other/efg")}.Apply(doc) 354 | Expect(err).ToNot(HaveOccurred()) 355 | Expect(res).To(BeNil()) 356 | }) 357 | 358 | It("returns an error if missing key needs to be created but next access does not make sense", func() { 359 | doc := map[interface{}]interface{}{"xyz": "xyz"} 360 | 361 | _, err := FindOp{Path: MustNewPointerFromString("/abc?/0")}.Apply(doc) 362 | Expect(err).To(HaveOccurred()) 363 | Expect(err.Error()).To(Equal( 364 | "Expected to find key or matching index token at path '/abc?/0'")) 365 | }) 366 | 367 | It("returns an error if it's not a map when key is being accessed", func() { 368 | _, err := FindOp{Path: MustNewPointerFromString("/abc")}.Apply([]interface{}{1, 2, 3}) 369 | Expect(err).To(HaveOccurred()) 370 | Expect(err.Error()).To(Equal( 371 | "Expected to find a map at path '/abc' but found '[]interface {}'")) 372 | 373 | _, err = FindOp{Path: MustNewPointerFromString("/abc/efg")}.Apply([]interface{}{1, 2, 3}) 374 | Expect(err).To(HaveOccurred()) 375 | Expect(err.Error()).To(Equal( 376 | "Expected to find a map at path '/abc' but found '[]interface {}'")) 377 | }) 378 | }) 379 | }) 380 | -------------------------------------------------------------------------------- /patch/integration_test.go: -------------------------------------------------------------------------------- 1 | package patch_test 2 | 3 | import ( 4 | . "github.com/onsi/ginkgo" 5 | . "github.com/onsi/gomega" 6 | "gopkg.in/yaml.v2" 7 | 8 | . "github.com/cppforlife/go-patch/patch" 9 | ) 10 | 11 | var _ = Describe("Integration", func() { 12 | It("works in a basic way", func() { 13 | inStr := ` 14 | releases: 15 | - name: capi 16 | version: 0.1 17 | 18 | instance_groups: 19 | - name: foo 20 | 21 | - name: cloud_controller 22 | instances: 0 23 | jobs: 24 | - name: cloud_controller 25 | release: capi 26 | 27 | - name: uaa 28 | instances: 0 29 | ` 30 | 31 | var in interface{} 32 | 33 | err := yaml.Unmarshal([]byte(inStr), &in) 34 | Expect(err).ToNot(HaveOccurred()) 35 | 36 | ops1Str := ` 37 | - type: replace 38 | path: /instance_groups/name=cloud_controller/instances 39 | value: 1 40 | 41 | - type: replace 42 | path: /instance_groups/name=cloud_controller/jobs/name=cloud_controller/consumes?/db 43 | value: 44 | instances: 45 | - address: some-db.local 46 | properties: 47 | username: user 48 | password: pass 49 | 50 | - type: test 51 | path: /instance_groups/name=uaa/instances-not 52 | absent: true 53 | 54 | # check current value 55 | - type: test 56 | path: /instance_groups/name=uaa/instances 57 | value: 0 58 | 59 | - type: replace 60 | path: /instance_groups/name=uaa/instances 61 | value: 1 62 | 63 | - type: replace 64 | path: /instance_groups/- 65 | value: 66 | name: uaadb 67 | instances: 2 68 | 69 | # check what was appended above 70 | - type: test 71 | path: /instance_groups/3 72 | value: 73 | name: uaadb 74 | instances: 2 75 | 76 | - type: replace 77 | path: /instance_groups/name=cloud_controller:before 78 | value: 79 | name: cc-before 80 | instances: 2 81 | 82 | - type: replace 83 | path: /instance_groups/name=cloud_controller:after 84 | value: 85 | name: cc-after 86 | instances: 1 87 | 88 | - type: replace 89 | path: /instance_groups/name=uaadb:after 90 | value: 91 | name: uaadb-after 92 | instances: 1 93 | 94 | - type: replace 95 | path: /instance_groups/name=uaadb:next/instances 96 | value: 4 97 | ` 98 | 99 | var opDefs1 []OpDefinition 100 | 101 | err = yaml.Unmarshal([]byte(ops1Str), &opDefs1) 102 | Expect(err).ToNot(HaveOccurred()) 103 | 104 | ops1, err := NewOpsFromDefinitions(opDefs1) 105 | Expect(err).ToNot(HaveOccurred()) 106 | 107 | ops2Str := ` 108 | - type: replace 109 | path: /releases/name=capi/version 110 | value: latest 111 | ` 112 | 113 | var opDefs2 []OpDefinition 114 | 115 | err = yaml.Unmarshal([]byte(ops2Str), &opDefs2) 116 | Expect(err).ToNot(HaveOccurred()) 117 | 118 | ops2, err := NewOpsFromDefinitions(opDefs2) 119 | Expect(err).ToNot(HaveOccurred()) 120 | 121 | ops := append(ops1, ops2...) 122 | 123 | res, err := ops.Apply(in) 124 | Expect(err).ToNot(HaveOccurred()) 125 | 126 | outStr := ` 127 | releases: 128 | - name: capi 129 | version: latest 130 | 131 | instance_groups: 132 | - name: foo 133 | 134 | - name: cc-before 135 | instances: 2 136 | 137 | - name: cloud_controller 138 | instances: 1 139 | jobs: 140 | - name: cloud_controller 141 | release: capi 142 | consumes: 143 | db: 144 | instances: 145 | - address: some-db.local 146 | properties: 147 | username: user 148 | password: pass 149 | 150 | - name: cc-after 151 | instances: 1 152 | 153 | - name: uaa 154 | instances: 1 155 | 156 | - name: uaadb 157 | instances: 2 158 | 159 | - name: uaadb-after 160 | instances: 4 161 | ` 162 | 163 | var out interface{} 164 | 165 | err = yaml.Unmarshal([]byte(outStr), &out) 166 | Expect(err).ToNot(HaveOccurred()) 167 | 168 | Expect(res).To(Equal(out)) 169 | }) 170 | 171 | It("works with find op", func() { 172 | inStr := ` 173 | releases: 174 | - name: capi 175 | version: 0.1 176 | 177 | instance_groups: 178 | - name: cloud_controller 179 | instances: 0 180 | jobs: 181 | - name: cloud_controller 182 | release: capi 183 | 184 | - name: uaa 185 | instances: 0 186 | ` 187 | 188 | var in interface{} 189 | 190 | err := yaml.Unmarshal([]byte(inStr), &in) 191 | Expect(err).ToNot(HaveOccurred()) 192 | 193 | path := MustNewPointerFromString("/instance_groups/name=cloud_controller") 194 | 195 | res, err := FindOp{Path: path}.Apply(in) 196 | Expect(err).ToNot(HaveOccurred()) 197 | 198 | outStr := ` 199 | name: cloud_controller 200 | instances: 0 201 | jobs: 202 | - name: cloud_controller 203 | release: capi 204 | ` 205 | 206 | var out interface{} 207 | 208 | err = yaml.Unmarshal([]byte(outStr), &out) 209 | Expect(err).ToNot(HaveOccurred()) 210 | 211 | Expect(res).To(Equal(out)) 212 | }) 213 | 214 | It("shows custom error messages", func() { 215 | inStr := ` 216 | releases: 217 | - name: capi 218 | version: 0.1 219 | ` 220 | 221 | var in interface{} 222 | 223 | err := yaml.Unmarshal([]byte(inStr), &in) 224 | Expect(err).ToNot(HaveOccurred()) 225 | 226 | opsStr := ` 227 | - type: remove 228 | path: /releases/0/not-there 229 | error: "Custom error message" 230 | ` 231 | 232 | var opDefs []OpDefinition 233 | 234 | err = yaml.Unmarshal([]byte(opsStr), &opDefs) 235 | Expect(err).ToNot(HaveOccurred()) 236 | 237 | ops, err := NewOpsFromDefinitions(opDefs) 238 | Expect(err).ToNot(HaveOccurred()) 239 | 240 | _, err = ops.Apply(in) 241 | Expect(err).To(HaveOccurred()) 242 | Expect(err.Error()).To(Equal( 243 | "Error 'Custom error message': Expected to find a map key 'not-there' for path '/releases/0/not-there' (found map keys: 'name', 'version')")) 244 | }) 245 | 246 | It("shows test error messages", func() { 247 | inStr := ` 248 | releases: 249 | - name: capi 250 | version: 0.1 251 | ` 252 | 253 | var in interface{} 254 | 255 | err := yaml.Unmarshal([]byte(inStr), &in) 256 | Expect(err).ToNot(HaveOccurred()) 257 | 258 | opsStr := ` 259 | - type: test 260 | path: /releases/0 261 | absent: true 262 | ` 263 | 264 | var opDefs []OpDefinition 265 | 266 | err = yaml.Unmarshal([]byte(opsStr), &opDefs) 267 | Expect(err).ToNot(HaveOccurred()) 268 | 269 | ops, err := NewOpsFromDefinitions(opDefs) 270 | Expect(err).ToNot(HaveOccurred()) 271 | 272 | _, err = ops.Apply(in) 273 | Expect(err).To(HaveOccurred()) 274 | Expect(err.Error()).To(Equal("Expected to not find '/releases/0'")) 275 | }) 276 | }) 277 | -------------------------------------------------------------------------------- /patch/op_definition.go: -------------------------------------------------------------------------------- 1 | package patch 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "strings" 7 | ) 8 | 9 | // OpDefinition struct is useful for JSON and YAML unmarshaling 10 | type OpDefinition struct { 11 | Type string `json:",omitempty" yaml:",omitempty"` 12 | Path *string `json:",omitempty" yaml:",omitempty"` 13 | Value *interface{} `json:",omitempty" yaml:",omitempty"` 14 | Absent *bool `json:",omitempty" yaml:",omitempty"` 15 | Error *string `json:",omitempty" yaml:",omitempty"` 16 | } 17 | 18 | type parser struct{} 19 | 20 | func NewOpsFromDefinitions(opDefs []OpDefinition) (Ops, error) { 21 | var ops []Op 22 | var p parser 23 | 24 | for i, opDef := range opDefs { 25 | var op Op 26 | var err error 27 | 28 | opFmt := p.fmtOpDef(opDef) 29 | 30 | switch opDef.Type { 31 | case "replace": 32 | op, err = p.newReplaceOp(opDef) 33 | if err != nil { 34 | return nil, fmt.Errorf("Replace operation [%d]: %s within\n%s", i, err, opFmt) 35 | } 36 | 37 | case "remove": 38 | op, err = p.newRemoveOp(opDef) 39 | if err != nil { 40 | return nil, fmt.Errorf("Remove operation [%d]: %s within\n%s", i, err, opFmt) 41 | } 42 | 43 | case "test": 44 | op, err = p.newTestOp(opDef) 45 | if err != nil { 46 | return nil, fmt.Errorf("Test operation [%d]: %s within\n%s", i, err, opFmt) 47 | } 48 | 49 | default: 50 | return nil, fmt.Errorf("Unknown operation [%d] with type '%s' within\n%s", i, opDef.Type, opFmt) 51 | } 52 | 53 | if opDef.Error != nil { 54 | op = DescriptiveOp{Op: op, ErrorMsg: *opDef.Error} 55 | } 56 | 57 | ops = append(ops, op) 58 | } 59 | 60 | return Ops(ops), nil 61 | } 62 | 63 | func (parser) newReplaceOp(opDef OpDefinition) (ReplaceOp, error) { 64 | if opDef.Path == nil { 65 | return ReplaceOp{}, fmt.Errorf("Missing path") 66 | } 67 | 68 | if opDef.Value == nil { 69 | return ReplaceOp{}, fmt.Errorf("Missing value") 70 | } 71 | 72 | ptr, err := NewPointerFromString(*opDef.Path) 73 | if err != nil { 74 | return ReplaceOp{}, fmt.Errorf("Invalid path: %s", err) 75 | } 76 | 77 | return ReplaceOp{Path: ptr, Value: *opDef.Value}, nil 78 | } 79 | 80 | func (parser) newRemoveOp(opDef OpDefinition) (RemoveOp, error) { 81 | if opDef.Path == nil { 82 | return RemoveOp{}, fmt.Errorf("Missing path") 83 | } 84 | 85 | if opDef.Value != nil { 86 | return RemoveOp{}, fmt.Errorf("Cannot specify value") 87 | } 88 | 89 | ptr, err := NewPointerFromString(*opDef.Path) 90 | if err != nil { 91 | return RemoveOp{}, fmt.Errorf("Invalid path: %s", err) 92 | } 93 | 94 | return RemoveOp{Path: ptr}, nil 95 | } 96 | 97 | func (parser) newTestOp(opDef OpDefinition) (TestOp, error) { 98 | if opDef.Path == nil { 99 | return TestOp{}, fmt.Errorf("Missing path") 100 | } 101 | 102 | if opDef.Value == nil && opDef.Absent == nil { 103 | return TestOp{}, fmt.Errorf("Missing value or absent") 104 | } 105 | 106 | ptr, err := NewPointerFromString(*opDef.Path) 107 | if err != nil { 108 | return TestOp{}, fmt.Errorf("Invalid path: %s", err) 109 | } 110 | 111 | op := TestOp{Path: ptr} 112 | 113 | if opDef.Value != nil { 114 | op.Value = *opDef.Value 115 | } 116 | 117 | if opDef.Absent != nil { 118 | op.Absent = *opDef.Absent 119 | } 120 | 121 | return op, nil 122 | } 123 | 124 | func (parser) fmtOpDef(opDef OpDefinition) string { 125 | var ( 126 | redactedVal interface{} = "" 127 | htmlDecoder = strings.NewReplacer("\\u003c", "<", "\\u003e", ">") 128 | ) 129 | 130 | if opDef.Value != nil { 131 | // can't JSON serialize generic interface{} anyway 132 | opDef.Value = &redactedVal 133 | } 134 | 135 | bytes, err := json.MarshalIndent(opDef, "", " ") 136 | if err != nil { 137 | return "" 138 | } 139 | 140 | return htmlDecoder.Replace(string(bytes)) 141 | } 142 | 143 | func NewOpDefinitionsFromOps(ops Ops) ([]OpDefinition, error) { 144 | opDefs := []OpDefinition{} 145 | 146 | for i, op := range ops { 147 | switch typedOp := op.(type) { 148 | case ReplaceOp: 149 | path := typedOp.Path.String() 150 | val := typedOp.Value 151 | 152 | opDefs = append(opDefs, OpDefinition{ 153 | Type: "replace", 154 | Path: &path, 155 | Value: &val, 156 | }) 157 | 158 | case RemoveOp: 159 | path := typedOp.Path.String() 160 | 161 | opDefs = append(opDefs, OpDefinition{ 162 | Type: "remove", 163 | Path: &path, 164 | }) 165 | 166 | case TestOp: 167 | path := typedOp.Path.String() 168 | val := typedOp.Value 169 | 170 | opDef := OpDefinition{ 171 | Type: "test", 172 | Path: &path, 173 | } 174 | 175 | if typedOp.Absent { 176 | opDef.Absent = &typedOp.Absent 177 | } else { 178 | opDef.Value = &val 179 | } 180 | 181 | opDefs = append(opDefs, opDef) 182 | 183 | default: 184 | return nil, fmt.Errorf("Unknown operation [%d] with type '%t'", i, op) 185 | } 186 | } 187 | 188 | return opDefs, nil 189 | } 190 | -------------------------------------------------------------------------------- /patch/op_definition_test.go: -------------------------------------------------------------------------------- 1 | package patch_test 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | . "github.com/onsi/ginkgo" 7 | . "github.com/onsi/gomega" 8 | "gopkg.in/yaml.v2" 9 | 10 | . "github.com/cppforlife/go-patch/patch" 11 | ) 12 | 13 | var _ = Describe("NewOpsFromDefinitions", func() { 14 | var ( 15 | path = "/abc" 16 | invalidPath = "abc" 17 | errorMsg = "error" 18 | val interface{} = 123 19 | complexVal interface{} = map[interface{}]interface{}{123: 123} 20 | trueBool = true 21 | ) 22 | 23 | It("supports 'replace', 'remove', 'test' operations", func() { 24 | opDefs := []OpDefinition{ 25 | {Type: "replace", Path: &path, Value: &val}, 26 | {Type: "remove", Path: &path}, 27 | {Type: "test", Path: &path, Value: &val}, 28 | {Type: "test", Path: &path, Absent: &trueBool}, 29 | } 30 | 31 | ops, err := NewOpsFromDefinitions(opDefs) 32 | Expect(err).ToNot(HaveOccurred()) 33 | 34 | Expect(ops).To(Equal(Ops([]Op{ 35 | ReplaceOp{Path: MustNewPointerFromString("/abc"), Value: 123}, 36 | RemoveOp{Path: MustNewPointerFromString("/abc")}, 37 | TestOp{Path: MustNewPointerFromString("/abc"), Value: 123}, 38 | TestOp{Path: MustNewPointerFromString("/abc"), Absent: true}, 39 | }))) 40 | }) 41 | 42 | It("returns error if operation type is unknown", func() { 43 | _, err := NewOpsFromDefinitions([]OpDefinition{{Type: "op"}}) 44 | Expect(err).To(HaveOccurred()) 45 | Expect(err.Error()).To(Equal(`Unknown operation [0] with type 'op' within 46 | { 47 | "Type": "op" 48 | }`)) 49 | }) 50 | 51 | It("returns error if operation type is find since it's not useful in list of operations", func() { 52 | _, err := NewOpsFromDefinitions([]OpDefinition{{Type: "find"}}) 53 | Expect(err).To(HaveOccurred()) 54 | Expect(err.Error()).To(ContainSubstring("Unknown operation [0] with type 'find'")) 55 | }) 56 | 57 | It("allows values to be complex in error messages", func() { 58 | _, err := NewOpsFromDefinitions([]OpDefinition{{Type: "op", Path: &invalidPath, Value: &complexVal}}) 59 | Expect(err).To(HaveOccurred()) 60 | Expect(err.Error()).To(Equal(`Unknown operation [0] with type 'op' within 61 | { 62 | "Type": "op", 63 | "Path": "abc", 64 | "Value": "" 65 | }`)) 66 | }) 67 | 68 | Describe("replace", func() { 69 | It("allows error description", func() { 70 | opDefs := []OpDefinition{{Type: "replace", Path: &path, Value: &val, Error: &errorMsg}} 71 | 72 | ops, err := NewOpsFromDefinitions(opDefs) 73 | Expect(err).ToNot(HaveOccurred()) 74 | 75 | Expect(ops).To(Equal(Ops([]Op{ 76 | DescriptiveOp{ 77 | Op: ReplaceOp{Path: MustNewPointerFromString("/abc"), Value: 123}, 78 | ErrorMsg: errorMsg, 79 | }, 80 | }))) 81 | }) 82 | 83 | It("requires path", func() { 84 | _, err := NewOpsFromDefinitions([]OpDefinition{{Type: "replace"}}) 85 | Expect(err).To(HaveOccurred()) 86 | Expect(err.Error()).To(Equal(`Replace operation [0]: Missing path within 87 | { 88 | "Type": "replace" 89 | }`)) 90 | }) 91 | 92 | It("requires value", func() { 93 | _, err := NewOpsFromDefinitions([]OpDefinition{{Type: "replace", Path: &path}}) 94 | Expect(err).To(HaveOccurred()) 95 | Expect(err.Error()).To(Equal(`Replace operation [0]: Missing value within 96 | { 97 | "Type": "replace", 98 | "Path": "/abc" 99 | }`)) 100 | }) 101 | 102 | It("requires valid path", func() { 103 | _, err := NewOpsFromDefinitions([]OpDefinition{{Type: "replace", Path: &invalidPath, Value: &val}}) 104 | Expect(err).To(HaveOccurred()) 105 | Expect(err.Error()).To(Equal(`Replace operation [0]: Invalid path: Expected to start with '/' within 106 | { 107 | "Type": "replace", 108 | "Path": "abc", 109 | "Value": "" 110 | }`)) 111 | }) 112 | }) 113 | 114 | Describe("remove", func() { 115 | It("allows error description", func() { 116 | opDefs := []OpDefinition{{Type: "remove", Path: &path, Error: &errorMsg}} 117 | 118 | ops, err := NewOpsFromDefinitions(opDefs) 119 | Expect(err).ToNot(HaveOccurred()) 120 | 121 | Expect(ops).To(Equal(Ops([]Op{ 122 | DescriptiveOp{ 123 | Op: RemoveOp{Path: MustNewPointerFromString("/abc")}, 124 | ErrorMsg: errorMsg, 125 | }, 126 | }))) 127 | }) 128 | 129 | It("requires path", func() { 130 | _, err := NewOpsFromDefinitions([]OpDefinition{{Type: "remove"}}) 131 | Expect(err).To(HaveOccurred()) 132 | Expect(err.Error()).To(Equal(`Remove operation [0]: Missing path within 133 | { 134 | "Type": "remove" 135 | }`)) 136 | }) 137 | 138 | It("does not allow value", func() { 139 | _, err := NewOpsFromDefinitions([]OpDefinition{{Type: "remove", Path: &path, Value: &val}}) 140 | Expect(err).To(HaveOccurred()) 141 | Expect(err.Error()).To(Equal(`Remove operation [0]: Cannot specify value within 142 | { 143 | "Type": "remove", 144 | "Path": "/abc", 145 | "Value": "" 146 | }`)) 147 | }) 148 | 149 | It("requires valid path", func() { 150 | _, err := NewOpsFromDefinitions([]OpDefinition{{Type: "remove", Path: &invalidPath}}) 151 | Expect(err).To(HaveOccurred()) 152 | Expect(err.Error()).To(Equal(`Remove operation [0]: Invalid path: Expected to start with '/' within 153 | { 154 | "Type": "remove", 155 | "Path": "abc" 156 | }`)) 157 | }) 158 | }) 159 | 160 | Describe("test", func() { 161 | It("allows error description", func() { 162 | opDefs := []OpDefinition{{Type: "test", Path: &path, Value: &val, Error: &errorMsg}} 163 | 164 | ops, err := NewOpsFromDefinitions(opDefs) 165 | Expect(err).ToNot(HaveOccurred()) 166 | 167 | Expect(ops).To(Equal(Ops([]Op{ 168 | DescriptiveOp{ 169 | Op: TestOp{Path: MustNewPointerFromString("/abc"), Value: 123}, 170 | ErrorMsg: errorMsg, 171 | }, 172 | }))) 173 | }) 174 | 175 | It("requires path", func() { 176 | _, err := NewOpsFromDefinitions([]OpDefinition{{Type: "test"}}) 177 | Expect(err).To(HaveOccurred()) 178 | Expect(err.Error()).To(Equal(`Test operation [0]: Missing path within 179 | { 180 | "Type": "test" 181 | }`)) 182 | }) 183 | 184 | It("requires value or absent flag", func() { 185 | _, err := NewOpsFromDefinitions([]OpDefinition{{Type: "test", Path: &path}}) 186 | Expect(err).To(HaveOccurred()) 187 | Expect(err.Error()).To(Equal(`Test operation [0]: Missing value or absent within 188 | { 189 | "Type": "test", 190 | "Path": "/abc" 191 | }`)) 192 | }) 193 | 194 | It("requires valid path", func() { 195 | _, err := NewOpsFromDefinitions([]OpDefinition{{Type: "test", Path: &invalidPath, Value: &val}}) 196 | Expect(err).To(HaveOccurred()) 197 | Expect(err.Error()).To(Equal(`Test operation [0]: Invalid path: Expected to start with '/' within 198 | { 199 | "Type": "test", 200 | "Path": "abc", 201 | "Value": "" 202 | }`)) 203 | }) 204 | }) 205 | }) 206 | 207 | var _ = Describe("NewOpDefinitionsFromOps", func() { 208 | It("supports 'replace', 'remove', 'test' operations serialized", func() { 209 | ops := Ops([]Op{ 210 | ReplaceOp{Path: MustNewPointerFromString("/abc"), Value: 123}, 211 | RemoveOp{Path: MustNewPointerFromString("/abc")}, 212 | TestOp{Path: MustNewPointerFromString("/abc"), Value: 123}, 213 | TestOp{Path: MustNewPointerFromString("/abc"), Absent: true}, 214 | }) 215 | 216 | opDefs, err := NewOpDefinitionsFromOps(ops) 217 | Expect(err).ToNot(HaveOccurred()) 218 | 219 | bs, err := yaml.Marshal(opDefs) 220 | Expect(err).ToNot(HaveOccurred()) 221 | 222 | Expect("\n" + string(bs)).To(Equal(` 223 | - type: replace 224 | path: /abc 225 | value: 123 226 | - type: remove 227 | path: /abc 228 | - type: test 229 | path: /abc 230 | value: 123 231 | - type: test 232 | path: /abc 233 | absent: true 234 | `)) 235 | 236 | bs, err = json.MarshalIndent(opDefs, "", " ") 237 | Expect(string(bs)).To(Equal(`[ 238 | { 239 | "Type": "replace", 240 | "Path": "/abc", 241 | "Value": 123 242 | }, 243 | { 244 | "Type": "remove", 245 | "Path": "/abc" 246 | }, 247 | { 248 | "Type": "test", 249 | "Path": "/abc", 250 | "Value": 123 251 | }, 252 | { 253 | "Type": "test", 254 | "Path": "/abc", 255 | "Absent": true 256 | } 257 | ]`)) 258 | }) 259 | }) 260 | -------------------------------------------------------------------------------- /patch/ops.go: -------------------------------------------------------------------------------- 1 | package patch 2 | 3 | type Ops []Op 4 | 5 | type Op interface { 6 | Apply(interface{}) (interface{}, error) 7 | } 8 | 9 | // Ensure basic operations implement Op 10 | var _ Op = Ops{} 11 | var _ Op = ReplaceOp{} 12 | var _ Op = RemoveOp{} 13 | var _ Op = FindOp{} 14 | var _ Op = DescriptiveOp{} 15 | var _ Op = ErrOp{} 16 | 17 | func (ops Ops) Apply(doc interface{}) (interface{}, error) { 18 | var err error 19 | 20 | for _, op := range ops { 21 | doc, err = op.Apply(doc) 22 | if err != nil { 23 | return nil, err 24 | } 25 | } 26 | 27 | return doc, nil 28 | } 29 | -------------------------------------------------------------------------------- /patch/ops_test.go: -------------------------------------------------------------------------------- 1 | package patch_test 2 | 3 | import ( 4 | "errors" 5 | 6 | . "github.com/onsi/ginkgo" 7 | . "github.com/onsi/gomega" 8 | 9 | . "github.com/cppforlife/go-patch/patch" 10 | ) 11 | 12 | var _ = Describe("Ops.Apply", func() { 13 | It("runs through all operations", func() { 14 | ops := Ops([]Op{ 15 | RemoveOp{Path: MustNewPointerFromString("/0")}, 16 | RemoveOp{Path: MustNewPointerFromString("/0")}, 17 | }) 18 | 19 | res, err := ops.Apply([]interface{}{1, 2, 3}) 20 | Expect(err).ToNot(HaveOccurred()) 21 | Expect(res).To(Equal([]interface{}{3})) 22 | }) 23 | 24 | It("returns original input if there are no operations", func() { 25 | res, err := Ops([]Op{}).Apply([]interface{}{1, 2, 3}) 26 | Expect(err).ToNot(HaveOccurred()) 27 | Expect(res).To(Equal([]interface{}{1, 2, 3})) 28 | }) 29 | 30 | It("returns error if any operation errors", func() { 31 | ops := Ops([]Op{ 32 | RemoveOp{Path: MustNewPointerFromString("/0")}, 33 | ErrOp{errors.New("fake-err")}, 34 | }) 35 | 36 | _, err := ops.Apply([]interface{}{1, 2, 3}) 37 | Expect(err).To(HaveOccurred()) 38 | Expect(err.Error()).To(ContainSubstring("fake-err")) 39 | }) 40 | }) 41 | -------------------------------------------------------------------------------- /patch/pointer.go: -------------------------------------------------------------------------------- 1 | package patch 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "strings" 7 | ) 8 | 9 | var ( 10 | rfc6901Decoder = strings.NewReplacer("~0", "~", "~1", "/", "~7", ":") 11 | rfc6901Encoder = strings.NewReplacer("~", "~0", "/", "~1", ":", "~7") 12 | ) 13 | 14 | // More or less based on https://tools.ietf.org/html/rfc6901 15 | type Pointer struct { 16 | tokens []Token 17 | } 18 | 19 | func MustNewPointerFromString(str string) Pointer { 20 | ptr, err := NewPointerFromString(str) 21 | if err != nil { 22 | panic(err.Error()) 23 | } 24 | 25 | return ptr 26 | } 27 | 28 | func NewPointerFromString(str string) (Pointer, error) { 29 | tokens := []Token{RootToken{}} 30 | 31 | if len(str) == 0 { 32 | return Pointer{tokens}, nil 33 | } 34 | 35 | if !strings.HasPrefix(str, "/") { 36 | return Pointer{}, fmt.Errorf("Expected to start with '/'") 37 | } 38 | 39 | tokenStrs := strings.Split(str, "/") 40 | tokenStrs = tokenStrs[1:] 41 | 42 | optional := false 43 | 44 | for i, tok := range tokenStrs { 45 | isLast := i == len(tokenStrs)-1 46 | 47 | var modifiers []Modifier 48 | tokPieces := strings.Split(tok, ":") 49 | 50 | if len(tokPieces) > 1 { 51 | tok = tokPieces[0] 52 | for _, p := range tokPieces[1:] { 53 | switch p { 54 | case "prev": 55 | modifiers = append(modifiers, PrevModifier{}) 56 | case "next": 57 | modifiers = append(modifiers, NextModifier{}) 58 | case "before": 59 | modifiers = append(modifiers, BeforeModifier{}) 60 | case "after": 61 | modifiers = append(modifiers, AfterModifier{}) 62 | default: 63 | return Pointer{}, fmt.Errorf("Expected to find one of the following modifiers: 'prev', 'next', 'before', or 'after' but found '%s'", tokPieces[1]) 64 | } 65 | } 66 | } 67 | 68 | tok = rfc6901Decoder.Replace(tok) 69 | 70 | // parse as after last index 71 | if isLast && tok == "-" { 72 | if len(modifiers) > 0 { 73 | return Pointer{}, fmt.Errorf("Expected not to find any modifiers with after last index token") 74 | } 75 | tokens = append(tokens, AfterLastIndexToken{}) 76 | continue 77 | } 78 | 79 | // parse as index 80 | idx, err := strconv.Atoi(tok) 81 | if err == nil { 82 | tokens = append(tokens, IndexToken{Index: idx, Modifiers: modifiers}) 83 | continue 84 | } 85 | 86 | if strings.HasSuffix(tok, "?") { 87 | optional = true 88 | } 89 | 90 | // parse name=val 91 | kv := strings.SplitN(tok, "=", 2) 92 | if len(kv) == 2 { 93 | token := MatchingIndexToken{ 94 | Key: kv[0], 95 | Value: strings.TrimSuffix(kv[1], "?"), 96 | Optional: optional, 97 | Modifiers: modifiers, 98 | } 99 | 100 | tokens = append(tokens, token) 101 | continue 102 | } 103 | 104 | if len(modifiers) > 0 { 105 | return Pointer{}, fmt.Errorf("Expected not to find any modifiers with key token") 106 | } 107 | 108 | // it's a map key 109 | token := KeyToken{ 110 | Key: strings.TrimSuffix(tok, "?"), 111 | Optional: optional, 112 | } 113 | 114 | tokens = append(tokens, token) 115 | } 116 | 117 | return Pointer{tokens}, nil 118 | } 119 | 120 | func NewPointer(tokens []Token) Pointer { 121 | if len(tokens) == 0 { 122 | panic("Expected at least one token") 123 | } 124 | 125 | _, ok := tokens[0].(RootToken) 126 | if !ok { 127 | panic("Expected first token to be root") 128 | } 129 | 130 | return Pointer{tokens} 131 | } 132 | 133 | func (p Pointer) Tokens() []Token { return p.tokens } 134 | 135 | func (p Pointer) IsSet() bool { return len(p.tokens) > 0 } 136 | 137 | func (p Pointer) String() string { 138 | var strs []string 139 | 140 | optional := false 141 | 142 | for _, token := range p.tokens { 143 | switch typedToken := token.(type) { 144 | case RootToken: 145 | strs = append(strs, "") 146 | 147 | case IndexToken: 148 | strs = append(strs, fmt.Sprintf("%d%s", typedToken.Index, p.modifiersString(typedToken.Modifiers))) 149 | 150 | case AfterLastIndexToken: 151 | strs = append(strs, "-") 152 | 153 | case MatchingIndexToken: 154 | key := rfc6901Encoder.Replace(typedToken.Key) 155 | val := rfc6901Encoder.Replace(typedToken.Value) 156 | 157 | if typedToken.Optional { 158 | if !optional { 159 | val += "?" 160 | optional = true 161 | } 162 | } 163 | 164 | strs = append(strs, fmt.Sprintf("%s=%s%s", key, val, p.modifiersString(typedToken.Modifiers))) 165 | 166 | case KeyToken: 167 | str := rfc6901Encoder.Replace(typedToken.Key) 168 | 169 | if typedToken.Optional { // /key?/key2/key3 170 | if !optional { 171 | str += "?" 172 | optional = true 173 | } 174 | } 175 | 176 | strs = append(strs, str) 177 | 178 | default: 179 | panic(fmt.Sprintf("Unknown token type '%T'", typedToken)) 180 | } 181 | } 182 | 183 | return strings.Join(strs, "/") 184 | } 185 | 186 | func (Pointer) modifiersString(modifiers []Modifier) string { 187 | var str string 188 | for _, modifier := range modifiers { 189 | str += ":" 190 | switch modifier.(type) { 191 | case PrevModifier: 192 | str += "prev" 193 | case NextModifier: 194 | str += "next" 195 | case BeforeModifier: 196 | str += "before" 197 | case AfterModifier: 198 | str += "after" 199 | } 200 | } 201 | return str 202 | } 203 | 204 | // UnmarshalFlag satisfies go-flags flag interface 205 | func (p *Pointer) UnmarshalFlag(data string) error { 206 | ptr, err := NewPointerFromString(data) 207 | if err != nil { 208 | return err 209 | } 210 | 211 | *p = ptr 212 | 213 | return nil 214 | } 215 | -------------------------------------------------------------------------------- /patch/pointer_test.go: -------------------------------------------------------------------------------- 1 | package patch_test 2 | 3 | import ( 4 | "fmt" 5 | 6 | . "github.com/onsi/ginkgo" 7 | . "github.com/onsi/gomega" 8 | 9 | . "github.com/cppforlife/go-patch/patch" 10 | ) 11 | 12 | type PointerTestCase struct { 13 | String string 14 | Tokens []Token 15 | } 16 | 17 | var testCases = []PointerTestCase{ 18 | {"", []Token{RootToken{}}}, 19 | 20 | // Root level 21 | {"/", []Token{RootToken{}, KeyToken{Key: ""}}}, 22 | {"//", []Token{RootToken{}, KeyToken{Key: ""}, KeyToken{Key: ""}}}, 23 | {"/ ", []Token{RootToken{}, KeyToken{Key: " "}}}, 24 | 25 | // Maps 26 | {"/key", []Token{RootToken{}, KeyToken{Key: "key"}}}, 27 | {"/key/", []Token{RootToken{}, KeyToken{Key: "key"}, KeyToken{Key: ""}}}, 28 | {"/key/key2", []Token{RootToken{}, KeyToken{Key: "key"}, KeyToken{Key: "key2"}}}, 29 | {"/key?/key2/key3", []Token{ 30 | RootToken{}, 31 | KeyToken{Key: "key", Optional: true}, 32 | KeyToken{Key: "key2", Optional: true}, 33 | KeyToken{Key: "key3", Optional: true}, 34 | }}, 35 | 36 | // Array indices 37 | {"/0", []Token{RootToken{}, IndexToken{Index: 0}}}, 38 | {"/1000001", []Token{RootToken{}, IndexToken{Index: 1000001}}}, 39 | {"/-2", []Token{RootToken{}, IndexToken{Index: -2}}}, 40 | 41 | {"/-", []Token{RootToken{}, AfterLastIndexToken{}}}, 42 | {"/ary/-", []Token{RootToken{}, KeyToken{Key: "ary"}, AfterLastIndexToken{}}}, 43 | {"/-/key", []Token{RootToken{}, KeyToken{Key: "-"}, KeyToken{Key: "key"}}}, 44 | 45 | {"/0:before", []Token{ 46 | RootToken{}, 47 | IndexToken{Index: 0, Modifiers: []Modifier{BeforeModifier{}}}, 48 | }}, 49 | {"/0:after", []Token{ 50 | RootToken{}, 51 | IndexToken{Index: 0, Modifiers: []Modifier{AfterModifier{}}}, 52 | }}, 53 | {"/-1:before", []Token{ 54 | RootToken{}, 55 | IndexToken{Index: -1, Modifiers: []Modifier{BeforeModifier{}}}, 56 | }}, 57 | {"/-1:prev:before", []Token{ 58 | RootToken{}, 59 | IndexToken{Index: -1, Modifiers: []Modifier{PrevModifier{}, BeforeModifier{}}}, 60 | }}, 61 | 62 | // Matching index token 63 | {"/name=val", []Token{RootToken{}, MatchingIndexToken{Key: "name", Value: "val"}}}, 64 | {"/name=val?", []Token{RootToken{}, MatchingIndexToken{Key: "name", Value: "val", Optional: true}}}, 65 | {"/name=val?/name2=val", []Token{ 66 | RootToken{}, 67 | MatchingIndexToken{Key: "name", Value: "val", Optional: true}, 68 | MatchingIndexToken{Key: "name2", Value: "val", Optional: true}, 69 | }}, 70 | {"/=", []Token{RootToken{}, MatchingIndexToken{Key: "", Value: ""}}}, 71 | {"/=?", []Token{RootToken{}, MatchingIndexToken{Key: "", Value: "", Optional: true}}}, 72 | {"/name=", []Token{RootToken{}, MatchingIndexToken{Key: "name", Value: ""}}}, 73 | {"/=val", []Token{RootToken{}, MatchingIndexToken{Key: "", Value: "val"}}}, 74 | {"/==", []Token{RootToken{}, MatchingIndexToken{Key: "", Value: "="}}}, 75 | 76 | {"/name=val:before", []Token{ 77 | RootToken{}, 78 | MatchingIndexToken{Key: "name", Value: "val", Modifiers: []Modifier{BeforeModifier{}}}, 79 | }}, 80 | {"/name=val:after", []Token{ 81 | RootToken{}, 82 | MatchingIndexToken{Key: "name", Value: "val", Modifiers: []Modifier{AfterModifier{}}}, 83 | }}, 84 | 85 | // Optionality 86 | {"/key?/name=val", []Token{ 87 | RootToken{}, 88 | KeyToken{Key: "key", Optional: true}, 89 | MatchingIndexToken{Key: "name", Value: "val", Optional: true}, 90 | }}, 91 | {"/name=val?/key", []Token{ 92 | RootToken{}, 93 | MatchingIndexToken{Key: "name", Value: "val", Optional: true}, 94 | KeyToken{Key: "key", Optional: true}, 95 | }}, 96 | 97 | // Escaping (todo support ~2 for '?'; ~3 for '=') 98 | {"/m~0n", []Token{RootToken{}, KeyToken{Key: "m~n"}}}, 99 | {"/a~01b", []Token{RootToken{}, KeyToken{Key: "a~1b"}}}, 100 | {"/a~1b", []Token{RootToken{}, KeyToken{Key: "a/b"}}}, 101 | {"/name~0n=val~0n", []Token{RootToken{}, MatchingIndexToken{Key: "name~n", Value: "val~n"}}}, 102 | {"/m~7n", []Token{RootToken{}, KeyToken{Key: "m:n"}}}, 103 | 104 | // Special chars 105 | {"/c%d", []Token{RootToken{}, KeyToken{Key: "c%d"}}}, 106 | {"/e^f", []Token{RootToken{}, KeyToken{Key: "e^f"}}}, 107 | {"/g|h", []Token{RootToken{}, KeyToken{Key: "g|h"}}}, 108 | {"/i\\j", []Token{RootToken{}, KeyToken{Key: "i\\j"}}}, 109 | {"/k\"l", []Token{RootToken{}, KeyToken{Key: "k\"l"}}}, 110 | } 111 | 112 | var _ = Describe("NewPointer", func() { 113 | It("panics if no tokens are given", func() { 114 | Expect(func() { NewPointer([]Token{}) }).To(Panic()) 115 | }) 116 | 117 | It("panics if first token is not root token", func() { 118 | Expect(func() { NewPointer([]Token{IndexToken{}}) }).To(Panic()) 119 | }) 120 | 121 | It("succeeds for basic case", func() { 122 | Expect(NewPointer([]Token{RootToken{}}).Tokens()).To(Equal([]Token{RootToken{}})) 123 | }) 124 | }) 125 | 126 | var _ = Describe("NewPointerFromString", func() { 127 | It("returns error if string doesn't start with /", func() { 128 | _, err := NewPointerFromString("abc") 129 | Expect(err).To(HaveOccurred()) 130 | Expect(err.Error()).To(Equal("Expected to start with '/'")) 131 | }) 132 | 133 | It("returns error if string includes unknown modifiers", func() { 134 | _, err := NewPointerFromString("/abc:unknown") 135 | Expect(err).To(HaveOccurred()) 136 | Expect(err.Error()).To(Equal("Expected to find one of the following modifiers: 'prev', 'next', 'before', or 'after' but found 'unknown'")) 137 | }) 138 | 139 | It("returns error if string has modifiers in after-last-index-token", func() { 140 | _, err := NewPointerFromString("/-:prev") 141 | Expect(err).To(HaveOccurred()) 142 | Expect(err.Error()).To(Equal("Expected not to find any modifiers with after last index token")) 143 | }) 144 | 145 | It("returns error if string has modifiers in key-token", func() { 146 | _, err := NewPointerFromString("/key:prev") 147 | Expect(err).To(HaveOccurred()) 148 | Expect(err.Error()).To(Equal("Expected not to find any modifiers with key token")) 149 | }) 150 | }) 151 | 152 | var _ = Describe("Pointer.String", func() { 153 | for _, tc := range testCases { 154 | tc := tc // copy 155 | It(fmt.Sprintf("'%#v' results in '%s'", tc.Tokens, tc.String), func() { 156 | Expect(NewPointer(tc.Tokens).String()).To(Equal(tc.String)) 157 | }) 158 | } 159 | }) 160 | 161 | var _ = Describe("Pointer.Tokens", func() { 162 | parsingTestCases := []PointerTestCase{ 163 | {"/key/key2?", []Token{ 164 | RootToken{}, 165 | KeyToken{Key: "key"}, 166 | KeyToken{Key: "key2", Optional: true}, 167 | }}, 168 | } 169 | 170 | parsingTestCases = append(parsingTestCases, testCases...) 171 | 172 | for _, tc := range parsingTestCases { 173 | tc := tc // copy 174 | It(fmt.Sprintf("'%s' results in '%#v'", tc.String, tc.Tokens), func() { 175 | Expect(MustNewPointerFromString(tc.String).Tokens()).To(Equal(tc.Tokens)) 176 | }) 177 | } 178 | }) 179 | 180 | var _ = Describe("Pointer.IsSet", func() { 181 | It("returns true if there is at least one token", func() { 182 | Expect(MustNewPointerFromString("").IsSet()).To(BeTrue()) 183 | }) 184 | 185 | It("returns false if it's not a valid pointer", func() { 186 | Expect(Pointer{}.IsSet()).To(BeFalse()) 187 | }) 188 | }) 189 | 190 | var _ = Describe("Pointer.UnmarshalFlag", func() { 191 | It("parses pointer if it's valid", func() { 192 | ptr := &Pointer{} 193 | 194 | err := ptr.UnmarshalFlag("/abc") 195 | Expect(err).ToNot(HaveOccurred()) 196 | Expect(*ptr).To(Equal(MustNewPointerFromString("/abc"))) 197 | }) 198 | 199 | It("returns error if string doesn't start with /", func() { 200 | ptr := &Pointer{} 201 | 202 | err := ptr.UnmarshalFlag("abc") 203 | Expect(err).To(HaveOccurred()) 204 | Expect(err.Error()).To(Equal("Expected to start with '/'")) 205 | }) 206 | }) 207 | -------------------------------------------------------------------------------- /patch/remove_op.go: -------------------------------------------------------------------------------- 1 | package patch 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | type RemoveOp struct { 8 | Path Pointer 9 | } 10 | 11 | func (op RemoveOp) Apply(doc interface{}) (interface{}, error) { 12 | tokens := op.Path.Tokens() 13 | 14 | if len(tokens) == 1 { 15 | return nil, fmt.Errorf("Cannot remove entire document") 16 | } 17 | 18 | obj := doc 19 | prevUpdate := func(newObj interface{}) { doc = newObj } 20 | 21 | for i, token := range tokens[1:] { 22 | isLast := i == len(tokens)-2 23 | currPath := NewPointer(tokens[:i+2]) 24 | 25 | switch typedToken := token.(type) { 26 | case IndexToken: 27 | typedObj, ok := obj.([]interface{}) 28 | if !ok { 29 | return nil, NewOpArrayMismatchTypeErr(currPath, obj) 30 | } 31 | 32 | idx, err := ArrayIndex{Index: typedToken.Index, Modifiers: typedToken.Modifiers, Array: typedObj, Path: currPath}.Concrete() 33 | if err != nil { 34 | return nil, err 35 | } 36 | 37 | if isLast { 38 | newAry := []interface{}{} 39 | newAry = append(newAry, typedObj[:idx]...) 40 | newAry = append(newAry, typedObj[idx+1:]...) 41 | prevUpdate(newAry) 42 | } else { 43 | obj = typedObj[idx] 44 | prevUpdate = func(newObj interface{}) { typedObj[idx] = newObj } 45 | } 46 | 47 | case MatchingIndexToken: 48 | typedObj, ok := obj.([]interface{}) 49 | if !ok { 50 | return nil, NewOpArrayMismatchTypeErr(currPath, obj) 51 | } 52 | 53 | var idxs []int 54 | 55 | for itemIdx, item := range typedObj { 56 | typedItem, ok := item.(map[interface{}]interface{}) 57 | if ok { 58 | if typedItem[typedToken.Key] == typedToken.Value { 59 | idxs = append(idxs, itemIdx) 60 | } 61 | } 62 | } 63 | 64 | if typedToken.Optional && len(idxs) == 0 { 65 | return doc, nil 66 | } 67 | 68 | if len(idxs) != 1 { 69 | return nil, OpMultipleMatchingIndexErr{currPath, idxs} 70 | } 71 | 72 | idx, err := ArrayIndex{Index: idxs[0], Modifiers: typedToken.Modifiers, Array: typedObj, Path: currPath}.Concrete() 73 | if err != nil { 74 | return nil, err 75 | } 76 | 77 | if isLast { 78 | newAry := []interface{}{} 79 | newAry = append(newAry, typedObj[:idx]...) 80 | newAry = append(newAry, typedObj[idx+1:]...) 81 | prevUpdate(newAry) 82 | } else { 83 | obj = typedObj[idx] 84 | // no need to change prevUpdate since matching item can only be a map 85 | } 86 | 87 | case KeyToken: 88 | typedObj, ok := obj.(map[interface{}]interface{}) 89 | if !ok { 90 | return nil, NewOpMapMismatchTypeErr(currPath, obj) 91 | } 92 | 93 | var found bool 94 | 95 | obj, found = typedObj[typedToken.Key] 96 | if !found { 97 | if typedToken.Optional { 98 | return doc, nil 99 | } 100 | 101 | return nil, OpMissingMapKeyErr{typedToken.Key, currPath, typedObj} 102 | } 103 | 104 | if isLast { 105 | delete(typedObj, typedToken.Key) 106 | } else { 107 | prevUpdate = func(newObj interface{}) { typedObj[typedToken.Key] = newObj } 108 | } 109 | 110 | default: 111 | return nil, OpUnexpectedTokenErr{token, currPath} 112 | } 113 | } 114 | 115 | return doc, nil 116 | } 117 | -------------------------------------------------------------------------------- /patch/remove_op_test.go: -------------------------------------------------------------------------------- 1 | package patch_test 2 | 3 | import ( 4 | . "github.com/onsi/ginkgo" 5 | . "github.com/onsi/gomega" 6 | 7 | . "github.com/cppforlife/go-patch/patch" 8 | ) 9 | 10 | var _ = Describe("RemoveOp.Apply", func() { 11 | It("returns an error if path is for the entire document", func() { 12 | _, err := RemoveOp{Path: MustNewPointerFromString("")}.Apply("a") 13 | Expect(err).To(HaveOccurred()) 14 | Expect(err.Error()).To(Equal("Cannot remove entire document")) 15 | }) 16 | 17 | Describe("array item", func() { 18 | It("removes array item", func() { 19 | res, err := RemoveOp{Path: MustNewPointerFromString("/0")}.Apply([]interface{}{1, 2, 3}) 20 | Expect(err).ToNot(HaveOccurred()) 21 | Expect(res).To(Equal([]interface{}{2, 3})) 22 | 23 | res, err = RemoveOp{Path: MustNewPointerFromString("/1")}.Apply([]interface{}{1, 2, 3}) 24 | Expect(err).ToNot(HaveOccurred()) 25 | Expect(res).To(Equal([]interface{}{1, 3})) 26 | 27 | res, err = RemoveOp{Path: MustNewPointerFromString("/2")}.Apply([]interface{}{1, 2, 3}) 28 | Expect(err).ToNot(HaveOccurred()) 29 | Expect(res).To(Equal([]interface{}{1, 2})) 30 | 31 | res, err = RemoveOp{Path: MustNewPointerFromString("/-1")}.Apply([]interface{}{1, 2, 3}) 32 | Expect(err).ToNot(HaveOccurred()) 33 | Expect(res).To(Equal([]interface{}{1, 2})) 34 | 35 | res, err = RemoveOp{Path: MustNewPointerFromString("/0")}.Apply([]interface{}{1}) 36 | Expect(err).ToNot(HaveOccurred()) 37 | Expect(res).To(Equal([]interface{}{})) 38 | }) 39 | 40 | It("removes relative array item", func() { 41 | res, err := RemoveOp{Path: MustNewPointerFromString("/3:prev")}.Apply([]interface{}{1, 2, 3}) 42 | Expect(err).ToNot(HaveOccurred()) 43 | Expect(res).To(Equal([]interface{}{1, 2})) 44 | }) 45 | 46 | It("removes nested array item", func() { 47 | res, err := RemoveOp{Path: MustNewPointerFromString("/0/1")}.Apply([]interface{}{[]interface{}{10, 11, 12}, 2, 3}) 48 | Expect(err).ToNot(HaveOccurred()) 49 | Expect(res).To(Equal([]interface{}{[]interface{}{10, 12}, 2, 3})) 50 | }) 51 | 52 | It("removes relative nested array item", func() { 53 | res, err := RemoveOp{Path: MustNewPointerFromString("/1:prev/1")}.Apply([]interface{}{[]interface{}{10, 11, 12}, 2, 3}) 54 | Expect(err).ToNot(HaveOccurred()) 55 | Expect(res).To(Equal([]interface{}{[]interface{}{10, 12}, 2, 3})) 56 | }) 57 | 58 | It("removes array item from an array that is inside a map", func() { 59 | doc := map[interface{}]interface{}{ 60 | "abc": []interface{}{1, 2, 3}, 61 | } 62 | 63 | res, err := RemoveOp{Path: MustNewPointerFromString("/abc/1")}.Apply(doc) 64 | Expect(err).ToNot(HaveOccurred()) 65 | 66 | Expect(res).To(Equal(map[interface{}]interface{}{ 67 | "abc": []interface{}{1, 3}, 68 | })) 69 | }) 70 | 71 | It("returns an error if it's not an array when index is being accessed", func() { 72 | _, err := RemoveOp{Path: MustNewPointerFromString("/0")}.Apply(map[interface{}]interface{}{}) 73 | Expect(err).To(HaveOccurred()) 74 | Expect(err.Error()).To(Equal( 75 | "Expected to find an array at path '/0' but found 'map[interface {}]interface {}'")) 76 | 77 | _, err = RemoveOp{Path: MustNewPointerFromString("/0/1")}.Apply(map[interface{}]interface{}{}) 78 | Expect(err).To(HaveOccurred()) 79 | Expect(err.Error()).To(Equal( 80 | "Expected to find an array at path '/0' but found 'map[interface {}]interface {}'")) 81 | }) 82 | 83 | It("returns an error if the index is out of bounds", func() { 84 | _, err := RemoveOp{Path: MustNewPointerFromString("/1")}.Apply([]interface{}{}) 85 | Expect(err).To(HaveOccurred()) 86 | Expect(err.Error()).To(Equal( 87 | "Expected to find array index '1' but found array of length '0' for path '/1'")) 88 | 89 | _, err = RemoveOp{Path: MustNewPointerFromString("/1/1")}.Apply([]interface{}{}) 90 | Expect(err).To(HaveOccurred()) 91 | Expect(err.Error()).To(Equal( 92 | "Expected to find array index '1' but found array of length '0' for path '/1'")) 93 | }) 94 | }) 95 | 96 | It("returns an error if after last token is found", func() { 97 | _, err := RemoveOp{Path: MustNewPointerFromString("/-")}.Apply([]interface{}{}) 98 | Expect(err).To(HaveOccurred()) 99 | Expect(err.Error()).To(Equal( 100 | "Expected to not find token 'patch.AfterLastIndexToken' at path '/-'")) 101 | }) 102 | 103 | Describe("array item with matching key and value", func() { 104 | It("removes array item if found", func() { 105 | doc := []interface{}{ 106 | map[interface{}]interface{}{"key": "val"}, 107 | map[interface{}]interface{}{"key": "val2"}, 108 | } 109 | 110 | res, err := RemoveOp{Path: MustNewPointerFromString("/key=val")}.Apply(doc) 111 | Expect(err).ToNot(HaveOccurred()) 112 | Expect(res).To(Equal([]interface{}{ 113 | map[interface{}]interface{}{"key": "val2"}, 114 | })) 115 | }) 116 | 117 | It("removes array item if found, leaving empty array", func() { 118 | doc := []interface{}{ 119 | map[interface{}]interface{}{"key": "val"}, 120 | } 121 | 122 | res, err := RemoveOp{Path: MustNewPointerFromString("/key=val")}.Apply(doc) 123 | Expect(err).ToNot(HaveOccurred()) 124 | Expect(res).To(Equal([]interface{}{})) 125 | }) 126 | 127 | It("removes relative array item", func() { 128 | doc := []interface{}{ 129 | map[interface{}]interface{}{"key": "val"}, 130 | map[interface{}]interface{}{"key": "val2"}, 131 | } 132 | 133 | res, err := RemoveOp{Path: MustNewPointerFromString("/key=val:next")}.Apply(doc) 134 | Expect(err).ToNot(HaveOccurred()) 135 | Expect(res).To(Equal([]interface{}{ 136 | map[interface{}]interface{}{"key": "val"}, 137 | })) 138 | }) 139 | 140 | It("returns an error if no items found", func() { 141 | doc := []interface{}{ 142 | map[interface{}]interface{}{"key": "val2"}, 143 | map[interface{}]interface{}{"key2": "val"}, 144 | } 145 | 146 | _, err := RemoveOp{Path: MustNewPointerFromString("/key=val")}.Apply(doc) 147 | Expect(err).To(HaveOccurred()) 148 | Expect(err.Error()).To(Equal( 149 | "Expected to find exactly one matching array item for path '/key=val' but found 0")) 150 | }) 151 | 152 | It("returns an error if multiple items found", func() { 153 | doc := []interface{}{ 154 | map[interface{}]interface{}{"key": "val"}, 155 | map[interface{}]interface{}{"key": "val"}, 156 | } 157 | 158 | _, err := RemoveOp{Path: MustNewPointerFromString("/key=val")}.Apply(doc) 159 | Expect(err).To(HaveOccurred()) 160 | Expect(err.Error()).To(Equal( 161 | "Expected to find exactly one matching array item for path '/key=val' but found 2")) 162 | }) 163 | 164 | It("removes array item even if not all items are maps", func() { 165 | doc := []interface{}{ 166 | 3, 167 | map[interface{}]interface{}{"key": "val"}, 168 | } 169 | 170 | res, err := RemoveOp{Path: MustNewPointerFromString("/key=val")}.Apply(doc) 171 | Expect(err).ToNot(HaveOccurred()) 172 | Expect(res).To(Equal([]interface{}{3})) 173 | }) 174 | 175 | It("removes nested matching item", func() { 176 | doc := []interface{}{ 177 | map[interface{}]interface{}{ 178 | "key": "val", 179 | "items": []interface{}{ 180 | map[interface{}]interface{}{"nested-key": "val"}, 181 | map[interface{}]interface{}{"nested-key": "val2"}, 182 | }, 183 | }, 184 | map[interface{}]interface{}{"key": "val2"}, 185 | } 186 | 187 | res, err := RemoveOp{Path: MustNewPointerFromString("/key=val/items/nested-key=val")}.Apply(doc) 188 | Expect(err).ToNot(HaveOccurred()) 189 | 190 | Expect(res).To(Equal([]interface{}{ 191 | map[interface{}]interface{}{ 192 | "key": "val", 193 | "items": []interface{}{ 194 | map[interface{}]interface{}{"nested-key": "val2"}, 195 | }, 196 | }, 197 | map[interface{}]interface{}{"key": "val2"}, 198 | })) 199 | }) 200 | 201 | It("removes relative nested matching item", func() { 202 | doc := []interface{}{ 203 | map[interface{}]interface{}{ 204 | "key": "val", 205 | "items": []interface{}{ 206 | map[interface{}]interface{}{"nested-key": "val"}, 207 | map[interface{}]interface{}{"nested-key": "val2"}, 208 | }, 209 | }, 210 | map[interface{}]interface{}{"key": "val2"}, 211 | } 212 | 213 | res, err := RemoveOp{Path: MustNewPointerFromString("/key=val2:prev/items/nested-key=val")}.Apply(doc) 214 | Expect(err).ToNot(HaveOccurred()) 215 | 216 | Expect(res).To(Equal([]interface{}{ 217 | map[interface{}]interface{}{ 218 | "key": "val", 219 | "items": []interface{}{ 220 | map[interface{}]interface{}{"nested-key": "val2"}, 221 | }, 222 | }, 223 | map[interface{}]interface{}{"key": "val2"}, 224 | })) 225 | }) 226 | 227 | It("removes nested matching item that does not exist", func() { 228 | doc := map[interface{}]interface{}{ 229 | "abc": []interface{}{ 230 | map[interface{}]interface{}{"opr": "opr"}, 231 | }, 232 | "xyz": "xyz", 233 | } 234 | 235 | res, err := RemoveOp{Path: MustNewPointerFromString("/abc/opr=not-opr?")}.Apply(doc) 236 | Expect(err).ToNot(HaveOccurred()) 237 | 238 | Expect(res).To(Equal(map[interface{}]interface{}{ 239 | "abc": []interface{}{ 240 | map[interface{}]interface{}{"opr": "opr"}, 241 | }, 242 | "xyz": "xyz", 243 | })) 244 | }) 245 | 246 | It("returns an error if it's not an array is being accessed", func() { 247 | _, err := RemoveOp{Path: MustNewPointerFromString("/key=val")}.Apply(map[interface{}]interface{}{}) 248 | Expect(err).To(HaveOccurred()) 249 | Expect(err.Error()).To(Equal( 250 | "Expected to find an array at path '/key=val' but found 'map[interface {}]interface {}'")) 251 | 252 | _, err = RemoveOp{Path: MustNewPointerFromString("/key=val/items/key=val")}.Apply(map[interface{}]interface{}{}) 253 | Expect(err).To(HaveOccurred()) 254 | Expect(err.Error()).To(Equal( 255 | "Expected to find an array at path '/key=val' but found 'map[interface {}]interface {}'")) 256 | }) 257 | }) 258 | 259 | Describe("map key", func() { 260 | It("removes map key", func() { 261 | doc := map[interface{}]interface{}{ 262 | "abc": "abc", 263 | "xyz": "xyz", 264 | } 265 | 266 | res, err := RemoveOp{Path: MustNewPointerFromString("/abc")}.Apply(doc) 267 | Expect(err).ToNot(HaveOccurred()) 268 | Expect(res).To(Equal(map[interface{}]interface{}{"xyz": "xyz"})) 269 | }) 270 | 271 | It("removes nested map key", func() { 272 | doc := map[interface{}]interface{}{ 273 | "abc": map[interface{}]interface{}{ 274 | "efg": "efg", 275 | "opr": "opr", 276 | }, 277 | "xyz": "xyz", 278 | } 279 | 280 | res, err := RemoveOp{Path: MustNewPointerFromString("/abc/efg")}.Apply(doc) 281 | Expect(err).ToNot(HaveOccurred()) 282 | 283 | Expect(res).To(Equal(map[interface{}]interface{}{ 284 | "abc": map[interface{}]interface{}{"opr": "opr"}, 285 | "xyz": "xyz", 286 | })) 287 | }) 288 | 289 | It("removes nested map key that does not exist", func() { 290 | doc := map[interface{}]interface{}{ 291 | "abc": map[interface{}]interface{}{"opr": "opr"}, 292 | "xyz": "xyz", 293 | } 294 | 295 | res, err := RemoveOp{Path: MustNewPointerFromString("/abc/efg?")}.Apply(doc) 296 | Expect(err).ToNot(HaveOccurred()) 297 | 298 | Expect(res).To(Equal(map[interface{}]interface{}{ 299 | "abc": map[interface{}]interface{}{"opr": "opr"}, 300 | "xyz": "xyz", 301 | })) 302 | }) 303 | 304 | It("removes super nested map key that does not exist", func() { 305 | doc := map[interface{}]interface{}{ 306 | "abc": map[interface{}]interface{}{ 307 | "efg": map[interface{}]interface{}{}, // wrong level 308 | }, 309 | } 310 | 311 | res, err := RemoveOp{Path: MustNewPointerFromString("/abc/opr?/efg")}.Apply(doc) 312 | Expect(err).ToNot(HaveOccurred()) 313 | 314 | Expect(res).To(Equal(map[interface{}]interface{}{ 315 | "abc": map[interface{}]interface{}{ 316 | "efg": map[interface{}]interface{}{}, // wrong level 317 | }, 318 | })) 319 | }) 320 | 321 | It("returns an error if parent key does not exist", func() { 322 | doc := map[interface{}]interface{}{"xyz": "xyz"} 323 | 324 | _, err := RemoveOp{Path: MustNewPointerFromString("/abc/efg")}.Apply(doc) 325 | Expect(err).To(HaveOccurred()) 326 | Expect(err.Error()).To(Equal( 327 | "Expected to find a map key 'abc' for path '/abc' (found map keys: 'xyz')")) 328 | }) 329 | 330 | It("returns an error if key does not exist", func() { 331 | doc := map[interface{}]interface{}{"xyz": "xyz", 123: "xyz", "other-xyz": "xyz"} 332 | 333 | _, err := RemoveOp{Path: MustNewPointerFromString("/abc")}.Apply(doc) 334 | Expect(err).To(HaveOccurred()) 335 | Expect(err.Error()).To(Equal( 336 | "Expected to find a map key 'abc' for path '/abc' (found map keys: 'other-xyz', 'xyz')")) 337 | }) 338 | 339 | It("returns an error without other found keys when there are no keys and key does not exist", func() { 340 | doc := map[interface{}]interface{}{} 341 | 342 | _, err := RemoveOp{Path: MustNewPointerFromString("/abc")}.Apply(doc) 343 | Expect(err).To(HaveOccurred()) 344 | Expect(err.Error()).To(Equal( 345 | "Expected to find a map key 'abc' for path '/abc' (found no other map keys)")) 346 | }) 347 | 348 | It("returns an error if it's not a map when key is being accessed", func() { 349 | _, err := RemoveOp{Path: MustNewPointerFromString("/abc")}.Apply([]interface{}{1, 2, 3}) 350 | Expect(err).To(HaveOccurred()) 351 | Expect(err.Error()).To(Equal( 352 | "Expected to find a map at path '/abc' but found '[]interface {}'")) 353 | 354 | _, err = RemoveOp{Path: MustNewPointerFromString("/abc/efg")}.Apply([]interface{}{1, 2, 3}) 355 | Expect(err).To(HaveOccurred()) 356 | Expect(err.Error()).To(Equal( 357 | "Expected to find a map at path '/abc' but found '[]interface {}'")) 358 | }) 359 | }) 360 | }) 361 | -------------------------------------------------------------------------------- /patch/replace_op.go: -------------------------------------------------------------------------------- 1 | package patch 2 | 3 | import ( 4 | "fmt" 5 | 6 | "gopkg.in/yaml.v2" 7 | ) 8 | 9 | type ReplaceOp struct { 10 | Path Pointer 11 | Value interface{} // will be cloned using yaml library 12 | } 13 | 14 | func (op ReplaceOp) Apply(doc interface{}) (interface{}, error) { 15 | // Ensure that value is not modified by future operations 16 | clonedValue, err := op.cloneValue(op.Value) 17 | if err != nil { 18 | return nil, fmt.Errorf("ReplaceOp cloning value: %s", err) 19 | } 20 | 21 | tokens := op.Path.Tokens() 22 | 23 | if len(tokens) == 1 { 24 | return clonedValue, nil 25 | } 26 | 27 | obj := doc 28 | prevUpdate := func(newObj interface{}) { doc = newObj } 29 | 30 | for i, token := range tokens[1:] { 31 | isLast := i == len(tokens)-2 32 | currPath := NewPointer(tokens[:i+2]) 33 | 34 | switch typedToken := token.(type) { 35 | case IndexToken: 36 | typedObj, ok := obj.([]interface{}) 37 | if !ok { 38 | return nil, NewOpArrayMismatchTypeErr(currPath, obj) 39 | } 40 | 41 | if isLast { 42 | idx, err := ArrayInsertion{Index: typedToken.Index, Modifiers: typedToken.Modifiers, Array: typedObj, Path: currPath}.Concrete() 43 | if err != nil { 44 | return nil, err 45 | } 46 | 47 | prevUpdate(idx.Update(typedObj, clonedValue)) 48 | } else { 49 | idx, err := ArrayIndex{Index: typedToken.Index, Modifiers: typedToken.Modifiers, Array: typedObj, Path: currPath}.Concrete() 50 | if err != nil { 51 | return nil, err 52 | } 53 | 54 | obj = typedObj[idx] 55 | prevUpdate = func(newObj interface{}) { typedObj[idx] = newObj } 56 | } 57 | 58 | case AfterLastIndexToken: 59 | typedObj, ok := obj.([]interface{}) 60 | if !ok { 61 | return nil, NewOpArrayMismatchTypeErr(currPath, obj) 62 | } 63 | 64 | if isLast { 65 | prevUpdate(append(typedObj, clonedValue)) 66 | } else { 67 | return nil, fmt.Errorf("Expected after last index token to be last in path '%s'", op.Path) 68 | } 69 | 70 | case MatchingIndexToken: 71 | typedObj, ok := obj.([]interface{}) 72 | if !ok { 73 | return nil, NewOpArrayMismatchTypeErr(currPath, obj) 74 | } 75 | 76 | var idxs []int 77 | 78 | for itemIdx, item := range typedObj { 79 | typedItem, ok := item.(map[interface{}]interface{}) 80 | if ok { 81 | if typedItem[typedToken.Key] == typedToken.Value { 82 | idxs = append(idxs, itemIdx) 83 | } 84 | } 85 | } 86 | 87 | if typedToken.Optional && len(idxs) == 0 { 88 | if isLast { 89 | prevUpdate(append(typedObj, clonedValue)) 90 | } else { 91 | obj = map[interface{}]interface{}{typedToken.Key: typedToken.Value} 92 | prevUpdate(append(typedObj, obj)) 93 | // no need to change prevUpdate since matching item can only be a map 94 | } 95 | } else { 96 | if len(idxs) != 1 { 97 | return nil, OpMultipleMatchingIndexErr{currPath, idxs} 98 | } 99 | 100 | if isLast { 101 | idx, err := ArrayInsertion{Index: idxs[0], Modifiers: typedToken.Modifiers, Array: typedObj, Path: currPath}.Concrete() 102 | if err != nil { 103 | return nil, err 104 | } 105 | 106 | prevUpdate(idx.Update(typedObj, clonedValue)) 107 | } else { 108 | idx, err := ArrayIndex{Index: idxs[0], Modifiers: typedToken.Modifiers, Array: typedObj, Path: currPath}.Concrete() 109 | if err != nil { 110 | return nil, err 111 | } 112 | 113 | obj = typedObj[idx] 114 | // no need to change prevUpdate since matching item can only be a map 115 | } 116 | } 117 | 118 | case KeyToken: 119 | typedObj, ok := obj.(map[interface{}]interface{}) 120 | if !ok { 121 | return nil, NewOpMapMismatchTypeErr(currPath, obj) 122 | } 123 | 124 | var found bool 125 | 126 | obj, found = typedObj[typedToken.Key] 127 | if !found && !typedToken.Optional { 128 | return nil, OpMissingMapKeyErr{typedToken.Key, currPath, typedObj} 129 | } 130 | 131 | if isLast { 132 | typedObj[typedToken.Key] = clonedValue 133 | } else { 134 | prevUpdate = func(newObj interface{}) { typedObj[typedToken.Key] = newObj } 135 | 136 | if !found { 137 | // Determine what type of value to create based on next token 138 | switch tokens[i+2].(type) { 139 | case AfterLastIndexToken: 140 | obj = []interface{}{} 141 | case MatchingIndexToken: 142 | obj = []interface{}{} 143 | case KeyToken: 144 | obj = map[interface{}]interface{}{} 145 | default: 146 | errMsg := "Expected to find key, matching index or after last index token at path '%s'" 147 | return nil, fmt.Errorf(errMsg, NewPointer(tokens[:i+3])) 148 | } 149 | 150 | typedObj[typedToken.Key] = obj 151 | } 152 | } 153 | 154 | default: 155 | return nil, OpUnexpectedTokenErr{token, currPath} 156 | } 157 | } 158 | 159 | return doc, nil 160 | } 161 | 162 | func (ReplaceOp) cloneValue(in interface{}) (out interface{}, err error) { 163 | defer func() { 164 | if recoverVal := recover(); recoverVal != nil { 165 | err = fmt.Errorf("Recovered: %s", recoverVal) 166 | } 167 | }() 168 | 169 | bytes, err := yaml.Marshal(in) 170 | if err != nil { 171 | return nil, err 172 | } 173 | 174 | err = yaml.Unmarshal(bytes, &out) 175 | if err != nil { 176 | return nil, err 177 | } 178 | 179 | return out, nil 180 | } 181 | -------------------------------------------------------------------------------- /patch/replace_op_test.go: -------------------------------------------------------------------------------- 1 | package patch_test 2 | 3 | import ( 4 | . "github.com/onsi/ginkgo" 5 | . "github.com/onsi/gomega" 6 | 7 | . "github.com/cppforlife/go-patch/patch" 8 | ) 9 | 10 | var _ = Describe("ReplaceOp.Apply", func() { 11 | It("returns error if replacement value cloning fails", func() { 12 | _, err := ReplaceOp{Path: MustNewPointerFromString(""), Value: func() {}}.Apply("a") 13 | Expect(err).To(HaveOccurred()) 14 | Expect(err.Error()).To(ContainSubstring("ReplaceOp cloning value")) 15 | }) 16 | 17 | It("uses cloned value for replacement", func() { 18 | repVal := map[interface{}]interface{}{"a": "b"} 19 | 20 | res, err := ReplaceOp{Path: MustNewPointerFromString(""), Value: repVal}.Apply("a") 21 | Expect(err).ToNot(HaveOccurred()) 22 | Expect(res).To(Equal(repVal)) 23 | 24 | res.(map[interface{}]interface{})["c"] = "d" 25 | Expect(res).ToNot(Equal(repVal)) 26 | }) 27 | 28 | It("replaces document if path is for the entire document", func() { 29 | res, err := ReplaceOp{Path: MustNewPointerFromString(""), Value: "b"}.Apply("a") 30 | Expect(err).ToNot(HaveOccurred()) 31 | Expect(res).To(Equal("b")) 32 | }) 33 | 34 | Describe("array item", func() { 35 | It("replaces array item", func() { 36 | res, err := ReplaceOp{Path: MustNewPointerFromString("/0"), Value: 10}.Apply([]interface{}{1, 2, 3}) 37 | Expect(err).ToNot(HaveOccurred()) 38 | Expect(res).To(Equal([]interface{}{10, 2, 3})) 39 | 40 | res, err = ReplaceOp{Path: MustNewPointerFromString("/1"), Value: 10}.Apply([]interface{}{1, 2, 3}) 41 | Expect(err).ToNot(HaveOccurred()) 42 | Expect(res).To(Equal([]interface{}{1, 10, 3})) 43 | 44 | res, err = ReplaceOp{Path: MustNewPointerFromString("/2"), Value: 10}.Apply([]interface{}{1, 2, 3}) 45 | Expect(err).ToNot(HaveOccurred()) 46 | Expect(res).To(Equal([]interface{}{1, 2, 10})) 47 | }) 48 | 49 | It("replaces relative array item", func() { 50 | res, err := ReplaceOp{Path: MustNewPointerFromString("/3:prev"), Value: 10}.Apply([]interface{}{1, 2, 3}) 51 | Expect(err).ToNot(HaveOccurred()) 52 | Expect(res).To(Equal([]interface{}{1, 2, 10})) 53 | 54 | res, err = ReplaceOp{Path: MustNewPointerFromString("/0:before"), Value: 10}.Apply([]interface{}{1, 2, 3}) 55 | Expect(err).ToNot(HaveOccurred()) 56 | Expect(res).To(Equal([]interface{}{10, 1, 2, 3})) 57 | 58 | res, err = ReplaceOp{Path: MustNewPointerFromString("/1:before"), Value: 10}.Apply([]interface{}{1, 2, 3}) 59 | Expect(err).ToNot(HaveOccurred()) 60 | Expect(res).To(Equal([]interface{}{1, 10, 2, 3})) 61 | 62 | res, err = ReplaceOp{Path: MustNewPointerFromString("/3:prev:after"), Value: 10}.Apply([]interface{}{1, 2, 3}) 63 | Expect(err).ToNot(HaveOccurred()) 64 | Expect(res).To(Equal([]interface{}{1, 2, 3, 10})) 65 | }) 66 | 67 | It("replaces nested array item", func() { 68 | doc := []interface{}{[]interface{}{10, 11, 12}, 2, 3} 69 | 70 | res, err := ReplaceOp{Path: MustNewPointerFromString("/0/1"), Value: 100}.Apply(doc) 71 | Expect(err).ToNot(HaveOccurred()) 72 | Expect(res).To(Equal([]interface{}{[]interface{}{10, 100, 12}, 2, 3})) 73 | }) 74 | 75 | It("replaces relative nested array item", func() { 76 | doc := []interface{}{1, []interface{}{10, 11, 12}, 3} 77 | 78 | res, err := ReplaceOp{Path: MustNewPointerFromString("/0:next/1"), Value: 100}.Apply(doc) 79 | Expect(err).ToNot(HaveOccurred()) 80 | Expect(res).To(Equal([]interface{}{1, []interface{}{10, 100, 12}, 3})) 81 | }) 82 | 83 | It("replaces array item from an array that is inside a map", func() { 84 | doc := map[interface{}]interface{}{ 85 | "abc": []interface{}{1, 2, 3}, 86 | } 87 | 88 | res, err := ReplaceOp{Path: MustNewPointerFromString("/abc/1"), Value: 10}.Apply(doc) 89 | Expect(err).ToNot(HaveOccurred()) 90 | 91 | Expect(res).To(Equal(map[interface{}]interface{}{ 92 | "abc": []interface{}{1, 10, 3}, 93 | })) 94 | }) 95 | 96 | It("returns an error if it's not an array when index is being accessed", func() { 97 | _, err := ReplaceOp{Path: MustNewPointerFromString("/0")}.Apply(map[interface{}]interface{}{}) 98 | Expect(err).To(HaveOccurred()) 99 | Expect(err.Error()).To(Equal( 100 | "Expected to find an array at path '/0' but found 'map[interface {}]interface {}'")) 101 | 102 | _, err = ReplaceOp{Path: MustNewPointerFromString("/0/1")}.Apply(map[interface{}]interface{}{}) 103 | Expect(err).To(HaveOccurred()) 104 | Expect(err.Error()).To(Equal( 105 | "Expected to find an array at path '/0' but found 'map[interface {}]interface {}'")) 106 | }) 107 | 108 | It("returns an error if the index is out of bounds", func() { 109 | _, err := ReplaceOp{Path: MustNewPointerFromString("/1")}.Apply([]interface{}{}) 110 | Expect(err).To(HaveOccurred()) 111 | Expect(err.Error()).To(Equal( 112 | "Expected to find array index '1' but found array of length '0' for path '/1'")) 113 | 114 | _, err = ReplaceOp{Path: MustNewPointerFromString("/1/1")}.Apply([]interface{}{}) 115 | Expect(err).To(HaveOccurred()) 116 | Expect(err.Error()).To(Equal( 117 | "Expected to find array index '1' but found array of length '0' for path '/1'")) 118 | }) 119 | }) 120 | 121 | Describe("array with after last item", func() { 122 | It("appends new item", func() { 123 | res, err := ReplaceOp{Path: MustNewPointerFromString("/-"), Value: 10}.Apply([]interface{}{}) 124 | Expect(err).ToNot(HaveOccurred()) 125 | Expect(res).To(Equal([]interface{}{10})) 126 | 127 | res, err = ReplaceOp{Path: MustNewPointerFromString("/-"), Value: 10}.Apply([]interface{}{1, 2, 3}) 128 | Expect(err).ToNot(HaveOccurred()) 129 | Expect(res).To(Equal([]interface{}{1, 2, 3, 10})) 130 | }) 131 | 132 | It("appends nested array item", func() { 133 | doc := []interface{}{[]interface{}{10, 11, 12}, 2, 3} 134 | 135 | res, err := ReplaceOp{Path: MustNewPointerFromString("/0/-"), Value: 100}.Apply(doc) 136 | Expect(err).ToNot(HaveOccurred()) 137 | Expect(res).To(Equal([]interface{}{[]interface{}{10, 11, 12, 100}, 2, 3})) 138 | }) 139 | 140 | It("appends array item from an array that is inside a map", func() { 141 | doc := map[interface{}]interface{}{ 142 | "abc": []interface{}{1, 2, 3}, 143 | } 144 | 145 | res, err := ReplaceOp{Path: MustNewPointerFromString("/abc/-"), Value: 10}.Apply(doc) 146 | Expect(err).ToNot(HaveOccurred()) 147 | 148 | Expect(res).To(Equal(map[interface{}]interface{}{ 149 | "abc": []interface{}{1, 2, 3, 10}, 150 | })) 151 | }) 152 | 153 | It("returns an error if after last index token is not last", func() { 154 | ptr := NewPointer([]Token{RootToken{}, AfterLastIndexToken{}, KeyToken{}}) 155 | 156 | _, err := ReplaceOp{Path: ptr}.Apply([]interface{}{}) 157 | Expect(err).To(HaveOccurred()) 158 | Expect(err.Error()).To(Equal( 159 | "Expected after last index token to be last in path '/-/'")) 160 | }) 161 | 162 | It("returns an error if it's not an array being accessed", func() { 163 | _, err := ReplaceOp{Path: MustNewPointerFromString("/-")}.Apply(map[interface{}]interface{}{}) 164 | Expect(err).To(HaveOccurred()) 165 | Expect(err.Error()).To(Equal( 166 | "Expected to find an array at path '/-' but found 'map[interface {}]interface {}'")) 167 | 168 | doc := map[interface{}]interface{}{"key": map[interface{}]interface{}{}} 169 | 170 | _, err = ReplaceOp{Path: MustNewPointerFromString("/key/-")}.Apply(doc) 171 | Expect(err).To(HaveOccurred()) 172 | Expect(err.Error()).To(Equal( 173 | "Expected to find an array at path '/key/-' but found 'map[interface {}]interface {}'")) 174 | }) 175 | }) 176 | 177 | Describe("array item with matching key and value", func() { 178 | It("replaces array item if found", func() { 179 | doc := []interface{}{ 180 | map[interface{}]interface{}{"key": "val"}, 181 | map[interface{}]interface{}{"key": "val2"}, 182 | } 183 | 184 | res, err := ReplaceOp{Path: MustNewPointerFromString("/key=val"), Value: 100}.Apply(doc) 185 | Expect(err).ToNot(HaveOccurred()) 186 | Expect(res).To(Equal([]interface{}{ 187 | 100, 188 | map[interface{}]interface{}{"key": "val2"}, 189 | })) 190 | }) 191 | 192 | It("replaces relative array item if found", func() { 193 | doc := []interface{}{ 194 | map[interface{}]interface{}{"key": "val"}, 195 | map[interface{}]interface{}{"key": "val2"}, 196 | } 197 | 198 | res, err := ReplaceOp{Path: MustNewPointerFromString("/key=val2:prev"), Value: 100}.Apply(doc) 199 | Expect(err).ToNot(HaveOccurred()) 200 | Expect(res).To(Equal([]interface{}{ 201 | 100, 202 | map[interface{}]interface{}{"key": "val2"}, 203 | })) 204 | 205 | doc = []interface{}{ 206 | map[interface{}]interface{}{"key": "val"}, 207 | map[interface{}]interface{}{"key": "val2"}, 208 | } 209 | 210 | res, err = ReplaceOp{Path: MustNewPointerFromString("/key=val2:before"), Value: 100}.Apply(doc) 211 | Expect(err).ToNot(HaveOccurred()) 212 | Expect(res).To(Equal([]interface{}{ 213 | map[interface{}]interface{}{"key": "val"}, 214 | 100, 215 | map[interface{}]interface{}{"key": "val2"}, 216 | })) 217 | 218 | doc = []interface{}{ 219 | map[interface{}]interface{}{"key": "val"}, 220 | map[interface{}]interface{}{"key": "val2"}, 221 | } 222 | 223 | res, err = ReplaceOp{Path: MustNewPointerFromString("/key=val:next:after"), Value: 100}.Apply(doc) 224 | Expect(err).ToNot(HaveOccurred()) 225 | Expect(res).To(Equal([]interface{}{ 226 | map[interface{}]interface{}{"key": "val"}, 227 | map[interface{}]interface{}{"key": "val2"}, 228 | 100, 229 | })) 230 | }) 231 | 232 | It("returns an error if no items found and matching is not optional", func() { 233 | doc := []interface{}{ 234 | map[interface{}]interface{}{"key": "val2"}, 235 | map[interface{}]interface{}{"key2": "val"}, 236 | } 237 | 238 | _, err := ReplaceOp{Path: MustNewPointerFromString("/key=val")}.Apply(doc) 239 | Expect(err).To(HaveOccurred()) 240 | Expect(err.Error()).To(Equal( 241 | "Expected to find exactly one matching array item for path '/key=val' but found 0")) 242 | }) 243 | 244 | It("returns an error if multiple items found", func() { 245 | doc := []interface{}{ 246 | map[interface{}]interface{}{"key": "val"}, 247 | map[interface{}]interface{}{"key": "val"}, 248 | } 249 | 250 | _, err := ReplaceOp{Path: MustNewPointerFromString("/key=val")}.Apply(doc) 251 | Expect(err).To(HaveOccurred()) 252 | Expect(err.Error()).To(Equal( 253 | "Expected to find exactly one matching array item for path '/key=val' but found 2")) 254 | }) 255 | 256 | It("replaces array item even if not all items are maps", func() { 257 | doc := []interface{}{ 258 | 3, 259 | map[interface{}]interface{}{"key": "val"}, 260 | } 261 | 262 | res, err := ReplaceOp{Path: MustNewPointerFromString("/key=val"), Value: 100}.Apply(doc) 263 | Expect(err).ToNot(HaveOccurred()) 264 | Expect(res).To(Equal([]interface{}{3, 100})) 265 | }) 266 | 267 | It("replaces nested matching item", func() { 268 | doc := []interface{}{ 269 | map[interface{}]interface{}{ 270 | "key": "val", 271 | "items": []interface{}{ 272 | map[interface{}]interface{}{"nested-key": "val"}, 273 | map[interface{}]interface{}{"nested-key": "val2"}, 274 | }, 275 | }, 276 | map[interface{}]interface{}{"key": "val2"}, 277 | } 278 | 279 | res, err := ReplaceOp{Path: MustNewPointerFromString("/key=val/items/nested-key=val"), Value: 100}.Apply(doc) 280 | Expect(err).ToNot(HaveOccurred()) 281 | 282 | Expect(res).To(Equal([]interface{}{ 283 | map[interface{}]interface{}{ 284 | "key": "val", 285 | "items": []interface{}{ 286 | 100, 287 | map[interface{}]interface{}{"nested-key": "val2"}, 288 | }, 289 | }, 290 | map[interface{}]interface{}{"key": "val2"}, 291 | })) 292 | }) 293 | 294 | It("appends missing matching item if it does not exist", func() { 295 | doc := []interface{}{map[interface{}]interface{}{"xyz": "xyz"}} 296 | 297 | res, err := ReplaceOp{Path: MustNewPointerFromString("/name=val?/efg"), Value: 1}.Apply(doc) 298 | Expect(err).ToNot(HaveOccurred()) 299 | 300 | Expect(res).To(Equal([]interface{}{ 301 | map[interface{}]interface{}{"xyz": "xyz"}, 302 | map[interface{}]interface{}{ 303 | "name": "val", 304 | "efg": 1, 305 | }, 306 | })) 307 | }) 308 | 309 | It("appends nested missing matching item if it does not exist", func() { 310 | doc := []interface{}{map[interface{}]interface{}{"xyz": "xyz"}} 311 | 312 | res, err := ReplaceOp{Path: MustNewPointerFromString("/name=val?/efg/name=val"), Value: 1}.Apply(doc) 313 | Expect(err).ToNot(HaveOccurred()) 314 | 315 | Expect(res).To(Equal([]interface{}{ 316 | map[interface{}]interface{}{"xyz": "xyz"}, 317 | map[interface{}]interface{}{ 318 | "name": "val", 319 | "efg": []interface{}{1}, 320 | }, 321 | })) 322 | }) 323 | 324 | It("returns an error if it's not an array is being accessed", func() { 325 | _, err := ReplaceOp{Path: MustNewPointerFromString("/key=val")}.Apply(map[interface{}]interface{}{}) 326 | Expect(err).To(HaveOccurred()) 327 | Expect(err.Error()).To(Equal( 328 | "Expected to find an array at path '/key=val' but found 'map[interface {}]interface {}'")) 329 | 330 | _, err = ReplaceOp{Path: MustNewPointerFromString("/key=val/items/key=val")}.Apply(map[interface{}]interface{}{}) 331 | Expect(err).To(HaveOccurred()) 332 | Expect(err.Error()).To(Equal( 333 | "Expected to find an array at path '/key=val' but found 'map[interface {}]interface {}'")) 334 | }) 335 | }) 336 | 337 | Describe("map key", func() { 338 | It("replaces map key", func() { 339 | doc := map[interface{}]interface{}{ 340 | "abc": "abc", 341 | "xyz": "xyz", 342 | } 343 | 344 | res, err := ReplaceOp{Path: MustNewPointerFromString("/abc"), Value: 1}.Apply(doc) 345 | Expect(err).ToNot(HaveOccurred()) 346 | Expect(res).To(Equal(map[interface{}]interface{}{"abc": 1, "xyz": "xyz"})) 347 | 348 | res, err = ReplaceOp{Path: MustNewPointerFromString("/abc")}.Apply(doc) 349 | Expect(err).ToNot(HaveOccurred()) 350 | Expect(res).To(Equal(map[interface{}]interface{}{"abc": nil, "xyz": "xyz"})) 351 | }) 352 | 353 | It("replaces nested map key", func() { 354 | doc := map[interface{}]interface{}{ 355 | "abc": map[interface{}]interface{}{ 356 | "efg": "efg", 357 | "opr": "opr", 358 | }, 359 | "xyz": "xyz", 360 | } 361 | 362 | res, err := ReplaceOp{Path: MustNewPointerFromString("/abc/efg"), Value: 1}.Apply(doc) 363 | Expect(err).ToNot(HaveOccurred()) 364 | 365 | Expect(res).To(Equal(map[interface{}]interface{}{ 366 | "abc": map[interface{}]interface{}{"efg": 1, "opr": "opr"}, 367 | "xyz": "xyz", 368 | })) 369 | }) 370 | 371 | It("replaces nested map key that does not exist", func() { 372 | doc := map[interface{}]interface{}{ 373 | "abc": map[interface{}]interface{}{"opr": "opr"}, 374 | "xyz": "xyz", 375 | } 376 | 377 | res, err := ReplaceOp{Path: MustNewPointerFromString("/abc/efg?"), Value: 1}.Apply(doc) 378 | Expect(err).ToNot(HaveOccurred()) 379 | 380 | Expect(res).To(Equal(map[interface{}]interface{}{ 381 | "abc": map[interface{}]interface{}{"efg": 1, "opr": "opr"}, 382 | "xyz": "xyz", 383 | })) 384 | }) 385 | 386 | It("replaces super nested map key that does not exist", func() { 387 | doc := map[interface{}]interface{}{ 388 | "abc": map[interface{}]interface{}{ 389 | "efg": map[interface{}]interface{}{}, // wrong level 390 | }, 391 | } 392 | 393 | res, err := ReplaceOp{Path: MustNewPointerFromString("/abc/opr?/efg"), Value: 1}.Apply(doc) 394 | Expect(err).ToNot(HaveOccurred()) 395 | 396 | Expect(res).To(Equal(map[interface{}]interface{}{ 397 | "abc": map[interface{}]interface{}{ 398 | "efg": map[interface{}]interface{}{}, // wrong level 399 | "opr": map[interface{}]interface{}{"efg": 1}, 400 | }, 401 | })) 402 | }) 403 | 404 | It("returns an error if parent key does not exist", func() { 405 | doc := map[interface{}]interface{}{"xyz": "xyz"} 406 | 407 | _, err := ReplaceOp{Path: MustNewPointerFromString("/abc/efg")}.Apply(doc) 408 | Expect(err).To(HaveOccurred()) 409 | Expect(err.Error()).To(Equal( 410 | "Expected to find a map key 'abc' for path '/abc' (found map keys: 'xyz')")) 411 | }) 412 | 413 | It("returns an error if key does not exist", func() { 414 | doc := map[interface{}]interface{}{"xyz": "xyz", 123: "xyz", "other-xyz": "xyz"} 415 | 416 | _, err := ReplaceOp{Path: MustNewPointerFromString("/abc")}.Apply(doc) 417 | Expect(err).To(HaveOccurred()) 418 | Expect(err.Error()).To(Equal( 419 | "Expected to find a map key 'abc' for path '/abc' (found map keys: 'other-xyz', 'xyz')")) 420 | }) 421 | 422 | It("returns an error without other found keys when there are no keys and key does not exist", func() { 423 | doc := map[interface{}]interface{}{} 424 | 425 | _, err := ReplaceOp{Path: MustNewPointerFromString("/abc")}.Apply(doc) 426 | Expect(err).To(HaveOccurred()) 427 | Expect(err.Error()).To(Equal( 428 | "Expected to find a map key 'abc' for path '/abc' (found no other map keys)")) 429 | }) 430 | 431 | It("creates missing key if key is not expected to exist", func() { 432 | doc := map[interface{}]interface{}{"xyz": "xyz"} 433 | 434 | res, err := ReplaceOp{Path: MustNewPointerFromString("/abc?/efg"), Value: 1}.Apply(doc) 435 | Expect(err).ToNot(HaveOccurred()) 436 | 437 | Expect(res).To(Equal(map[interface{}]interface{}{ 438 | "abc": map[interface{}]interface{}{"efg": 1}, 439 | "xyz": "xyz", 440 | })) 441 | }) 442 | 443 | It("creates nested missing keys if key is not expected to exist", func() { 444 | doc := map[interface{}]interface{}{"xyz": "xyz"} 445 | 446 | res, err := ReplaceOp{Path: MustNewPointerFromString("/abc?/other/efg"), Value: 1}.Apply(doc) 447 | Expect(err).ToNot(HaveOccurred()) 448 | 449 | Expect(res).To(Equal(map[interface{}]interface{}{ 450 | "abc": map[interface{}]interface{}{ 451 | "other": map[interface{}]interface{}{"efg": 1}, 452 | }, 453 | "xyz": "xyz", 454 | })) 455 | }) 456 | 457 | It("creates missing key with array value for index access if key is not expected to exist", func() { 458 | doc := map[interface{}]interface{}{"xyz": "xyz"} 459 | 460 | res, err := ReplaceOp{Path: MustNewPointerFromString("/abc?/-"), Value: 1}.Apply(doc) 461 | Expect(err).ToNot(HaveOccurred()) 462 | 463 | Expect(res).To(Equal(map[interface{}]interface{}{ 464 | "abc": []interface{}{1}, 465 | "xyz": "xyz", 466 | })) 467 | }) 468 | 469 | It("returns an error if missing key needs to be created but next access does not make sense", func() { 470 | doc := map[interface{}]interface{}{"xyz": "xyz"} 471 | 472 | _, err := ReplaceOp{Path: MustNewPointerFromString("/abc?/0"), Value: 1}.Apply(doc) 473 | Expect(err).To(HaveOccurred()) 474 | Expect(err.Error()).To(Equal( 475 | "Expected to find key, matching index or after last index token at path '/abc?/0'")) 476 | }) 477 | 478 | It("returns an error if it's not a map when key is being accessed", func() { 479 | _, err := ReplaceOp{Path: MustNewPointerFromString("/abc")}.Apply([]interface{}{1, 2, 3}) 480 | Expect(err).To(HaveOccurred()) 481 | Expect(err.Error()).To(Equal( 482 | "Expected to find a map at path '/abc' but found '[]interface {}'")) 483 | 484 | _, err = ReplaceOp{Path: MustNewPointerFromString("/abc/efg")}.Apply([]interface{}{1, 2, 3}) 485 | Expect(err).To(HaveOccurred()) 486 | Expect(err.Error()).To(Equal( 487 | "Expected to find a map at path '/abc' but found '[]interface {}'")) 488 | }) 489 | }) 490 | }) 491 | -------------------------------------------------------------------------------- /patch/suite_test.go: -------------------------------------------------------------------------------- 1 | package patch_test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/ginkgo" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestPatch(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "/") 13 | } 14 | -------------------------------------------------------------------------------- /patch/test_op.go: -------------------------------------------------------------------------------- 1 | package patch 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | ) 7 | 8 | type TestOp struct { 9 | Path Pointer 10 | Value interface{} 11 | Absent bool 12 | } 13 | 14 | func (op TestOp) Apply(doc interface{}) (interface{}, error) { 15 | if op.Absent { 16 | return op.checkAbsence(doc) 17 | } 18 | return op.checkValue(doc) 19 | } 20 | 21 | func (op TestOp) checkAbsence(doc interface{}) (interface{}, error) { 22 | _, err := FindOp{Path: op.Path}.Apply(doc) 23 | if err != nil { 24 | if typedErr, ok := err.(OpMissingIndexErr); ok { 25 | if typedErr.Path.String() == op.Path.String() { 26 | return doc, nil 27 | } 28 | } 29 | if typedErr, ok := err.(OpMissingMapKeyErr); ok { 30 | if typedErr.Path.String() == op.Path.String() { 31 | return doc, nil 32 | } 33 | } 34 | return nil, err 35 | } 36 | 37 | return nil, fmt.Errorf("Expected to not find '%s'", op.Path) 38 | } 39 | 40 | func (op TestOp) checkValue(doc interface{}) (interface{}, error) { 41 | foundVal, err := FindOp{Path: op.Path}.Apply(doc) 42 | if err != nil { 43 | return nil, err 44 | } 45 | 46 | if !reflect.DeepEqual(foundVal, op.Value) { 47 | return nil, fmt.Errorf("Found value does not match expected value") 48 | } 49 | 50 | // Return same input document 51 | return doc, nil 52 | } 53 | -------------------------------------------------------------------------------- /patch/test_op_test.go: -------------------------------------------------------------------------------- 1 | package patch_test 2 | 3 | import ( 4 | . "github.com/onsi/ginkgo" 5 | . "github.com/onsi/gomega" 6 | 7 | . "github.com/cppforlife/go-patch/patch" 8 | ) 9 | 10 | var _ = Describe("TestOp.Apply", func() { 11 | Describe("value check", func() { 12 | It("does not error and returns original document if value matches", func() { 13 | res, err := TestOp{ 14 | Path: MustNewPointerFromString("/0"), 15 | Value: 1, 16 | }.Apply([]interface{}{1, 2, 3}) 17 | 18 | Expect(err).ToNot(HaveOccurred()) 19 | Expect(res).To(Equal([]interface{}{1, 2, 3})) 20 | 21 | res, err = TestOp{ 22 | Path: MustNewPointerFromString("/0"), 23 | Value: nil, 24 | }.Apply([]interface{}{nil, 2, 3}) 25 | 26 | Expect(err).ToNot(HaveOccurred()) 27 | Expect(res).To(Equal([]interface{}{nil, 2, 3})) 28 | }) 29 | 30 | It("returns an error if value does not match", func() { 31 | _, err := TestOp{ 32 | Path: MustNewPointerFromString("/0"), 33 | Value: 2, 34 | }.Apply([]interface{}{1, 2, 3}) 35 | 36 | Expect(err).To(HaveOccurred()) 37 | Expect(err.Error()).To(Equal("Found value does not match expected value")) 38 | 39 | _, err = TestOp{ 40 | Path: MustNewPointerFromString("/0"), 41 | Value: 2, 42 | }.Apply([]interface{}{nil}) 43 | 44 | Expect(err).To(HaveOccurred()) 45 | Expect(err.Error()).To(Equal("Found value does not match expected value")) 46 | }) 47 | }) 48 | 49 | Describe("absence check", func() { 50 | It("does not error and returns original document if key is absent", func() { 51 | res, err := TestOp{ 52 | Path: MustNewPointerFromString("/0"), 53 | Absent: true, 54 | }.Apply([]interface{}{}) 55 | 56 | Expect(err).ToNot(HaveOccurred()) 57 | Expect(res).To(Equal([]interface{}{})) 58 | 59 | res, err = TestOp{ 60 | Path: MustNewPointerFromString("/a"), 61 | Absent: true, 62 | }.Apply(map[interface{}]interface{}{"b": 123}) 63 | 64 | Expect(err).ToNot(HaveOccurred()) 65 | Expect(res).To(Equal(map[interface{}]interface{}{"b": 123})) 66 | }) 67 | 68 | It("returns an error if parent key is absent", func() { 69 | _, err := TestOp{ 70 | Path: MustNewPointerFromString("/0/0"), 71 | Absent: true, 72 | }.Apply([]interface{}{}) 73 | 74 | Expect(err).To(HaveOccurred()) 75 | Expect(err.Error()).To(Equal("Expected to find array index '0' but found array of length '0' for path '/0'")) 76 | 77 | _, err = TestOp{ 78 | Path: MustNewPointerFromString("/a/b"), 79 | Absent: true, 80 | }.Apply(map[interface{}]interface{}{}) 81 | 82 | Expect(err).To(HaveOccurred()) 83 | Expect(err.Error()).To(Equal("Expected to find a map key 'a' for path '/a' (found no other map keys)")) 84 | }) 85 | 86 | It("returns an error if key is present", func() { 87 | _, err := TestOp{ 88 | Path: MustNewPointerFromString("/0"), 89 | Absent: true, 90 | }.Apply([]interface{}{1}) 91 | 92 | Expect(err).To(HaveOccurred()) 93 | Expect(err.Error()).To(Equal("Expected to not find '/0'")) 94 | 95 | _, err = TestOp{ 96 | Path: MustNewPointerFromString("/a"), 97 | Absent: true, 98 | }.Apply(map[interface{}]interface{}{"a": 123}) 99 | 100 | Expect(err).To(HaveOccurred()) 101 | Expect(err.Error()).To(Equal("Expected to not find '/a'")) 102 | }) 103 | }) 104 | }) 105 | -------------------------------------------------------------------------------- /patch/tokens.go: -------------------------------------------------------------------------------- 1 | package patch 2 | 3 | type Token interface { 4 | _token() 5 | } 6 | 7 | type RootToken struct{} 8 | 9 | type IndexToken struct { 10 | Index int 11 | Modifiers []Modifier 12 | } 13 | 14 | type AfterLastIndexToken struct{} 15 | 16 | type MatchingIndexToken struct { 17 | Key string 18 | Value string 19 | Optional bool 20 | Modifiers []Modifier 21 | } 22 | 23 | type KeyToken struct { 24 | Key string 25 | Optional bool 26 | } 27 | 28 | type Modifier interface { 29 | _modifier() 30 | } 31 | 32 | type PrevModifier struct{} 33 | type NextModifier struct{} 34 | 35 | type BeforeModifier struct{} 36 | type AfterModifier struct{} 37 | -------------------------------------------------------------------------------- /patch/tokens_impl.go: -------------------------------------------------------------------------------- 1 | package patch 2 | 3 | var _ Token = RootToken{} 4 | var _ Token = IndexToken{} 5 | var _ Token = AfterLastIndexToken{} 6 | var _ Token = MatchingIndexToken{} 7 | var _ Token = KeyToken{} 8 | 9 | func (RootToken) _token() {} 10 | func (IndexToken) _token() {} 11 | func (AfterLastIndexToken) _token() {} 12 | func (MatchingIndexToken) _token() {} 13 | func (KeyToken) _token() {} 14 | 15 | var _ Modifier = PrevModifier{} 16 | var _ Modifier = NextModifier{} 17 | var _ Modifier = BeforeModifier{} 18 | var _ Modifier = AfterModifier{} 19 | 20 | func (PrevModifier) _modifier() {} 21 | func (NextModifier) _modifier() {} 22 | func (BeforeModifier) _modifier() {} 23 | func (AfterModifier) _modifier() {} 24 | -------------------------------------------------------------------------------- /patch/yaml_compat_test.go: -------------------------------------------------------------------------------- 1 | package patch_test 2 | 3 | import ( 4 | . "github.com/onsi/ginkgo" 5 | . "github.com/onsi/gomega" 6 | "gopkg.in/yaml.v2" 7 | 8 | . "github.com/cppforlife/go-patch/patch" 9 | ) 10 | 11 | var _ = Describe("YAML compatibility", func() { 12 | Describe("empty string", func() { 13 | It("[workaround] works deserializing empty strings", func() { 14 | str := ` 15 | - type: replace 16 | path: /instance_groups/name=cloud_controller/instances 17 | value: !!str "" 18 | ` 19 | 20 | var opDefs []OpDefinition 21 | 22 | err := yaml.Unmarshal([]byte(str), &opDefs) 23 | Expect(err).ToNot(HaveOccurred()) 24 | 25 | val := opDefs[0].Value 26 | Expect((*val).(string)).To(Equal("")) 27 | }) 28 | 29 | It("[fixed] does not work deserializing empty strings", func() { 30 | str := ` 31 | - type: replace 32 | path: /instance_groups/name=cloud_controller/instances 33 | value: "" 34 | ` 35 | 36 | var opDefs []OpDefinition 37 | 38 | err := yaml.Unmarshal([]byte(str), &opDefs) 39 | Expect(err).ToNot(HaveOccurred()) 40 | 41 | val := opDefs[0].Value 42 | Expect((*val).(string)).To(Equal("")) 43 | }) 44 | }) 45 | }) 46 | --------------------------------------------------------------------------------