├── .travis.yml ├── README.md ├── operation.go ├── operation_add.go ├── operation_copy.go ├── operation_move.go ├── operation_remove.go ├── operation_replace.go ├── operation_test.go ├── operation_test_impl.go ├── patch.go ├── patch_examples_test.go └── patch_test.go /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.7 5 | - tip 6 | 7 | script: 8 | - go test 9 | 10 | matrix: 11 | allow_failures: 12 | - go: tip 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # patchstructure [![GoDoc](https://godoc.org/github.com/mitchellh/patchstructure?status.svg)](https://godoc.org/github.com/mitchellh/patchstructure) 2 | 3 | patchstructure is a Go library for applying patches to modify existing 4 | Go structures. 5 | 6 | patchstructure is based on 7 | [JSON Patch (RFC 6902)](https://tools.ietf.org/html/rfc6902), but 8 | applies to Go strucutures instead of JSON objects. 9 | 10 | The goal of patchstructure is to provide a single API and format for 11 | representing and applying changes to Go structures. With this in place, 12 | diffs between structures can be represented, changes to a structure 13 | can be treated as a log, etc. 14 | 15 | ## Features 16 | 17 | * Apply a "patch" to perform a set of operations on a Go structure 18 | 19 | * Operations support add, remove, replace, move, copy 20 | 21 | * Operations work on all Go primitive types and collection types 22 | 23 | * JSON encode/decode Operation structures 24 | 25 | For an exhaustive list of supported features, please view the 26 | [JSON Patch RFC (RFC 6902)](https://tools.ietf.org/html/rfc6902) which 27 | this implements completely, but for Go structures. Exceptions to the RFC 28 | are documented below. 29 | 30 | ## Differences from RFC 6902 31 | 32 | RFC 6902 was created for JSON structures. Due to minor differences in 33 | Go structures, there are some differences. These don't change the 34 | function of the RFC, but are important to know when using this library: 35 | 36 | * The "copy" operation will perform a deep copy by default using 37 | [copystructure](https://github.com/mitchellh/copystructure). You can 38 | set the `Shallow` field to true on the operation to avoid this behavior. 39 | 40 | * The "test" operation is currently implemented with `reflect.DeepEqual` 41 | which is _not_ exactly correct according to the RFC. This will work fine 42 | for most basic primitives but the limitations of that approach should be 43 | known. In the future we should rework this. 44 | 45 | ## Installation 46 | 47 | Standard `go get`: 48 | 49 | ``` 50 | $ go get github.com/mitchellh/patchstructure 51 | ``` 52 | 53 | ## Usage & Example 54 | 55 | For usage and examples see the [Godoc](http://godoc.org/github.com/mitchellh/patchstructure). 56 | 57 | A quick code example is shown below: 58 | 59 | ```go 60 | complex := map[string]interface{}{ 61 | "alice": 42, 62 | "bob": []interface{}{ 63 | map[string]interface{}{ 64 | "name": "Bob", 65 | }, 66 | }, 67 | } 68 | 69 | value, err := Patch(complex, []*Operation{ 70 | &Operation{ 71 | Op: OpCopy, 72 | Path: "/alice", 73 | From: "/bob", 74 | }, 75 | 76 | &Operation{ 77 | Op: OpReplace, 78 | Path: "/alice/0/name", 79 | Value: "Alice", 80 | }, 81 | }) 82 | if err != nil { 83 | panic(err) 84 | } 85 | 86 | fmt.Printf("%s", value) 87 | // Output: 88 | // map[alice:[map[name:Alice]] bob:[map[name:Bob]]] 89 | ``` 90 | -------------------------------------------------------------------------------- /operation.go: -------------------------------------------------------------------------------- 1 | package patchstructure 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | ) 7 | 8 | // Operation represents a single operation to apply to a structure. 9 | // 10 | // Note that Value and From are dependent on the operation. Please see 11 | // the JSON patch documentation for details on this since the semantics for 12 | // the Go patch is identical. 13 | type Operation struct { 14 | Op Op `json:"op"` // Op is the operation type to apply 15 | Path string `json:"path"` // Path is required 16 | Value interface{} `json:"value"` // Optional depending on op 17 | From string `json:"from"` // Optional depending on op 18 | Shallow bool `json:"shallow"` // If true, OpCopy will not deep copy the value 19 | } 20 | 21 | // Op is an enum representing the supported operations for a patch. 22 | // 23 | // The values should obviously match the JSON patch operations and their 24 | // semantics are meant to be identical for Go structures. 25 | type Op int 26 | 27 | const ( 28 | OpInvalid Op = iota // Set zero to invalid to prevent accidental adds 29 | OpAdd 30 | OpRemove 31 | OpReplace 32 | OpMove 33 | OpCopy 34 | OpTest 35 | ) 36 | 37 | // String format of an operation matching what it should be if JSON encoded. 38 | func (o Op) String() string { 39 | return opString[o] 40 | } 41 | 42 | // json.Marshaler 43 | func (o Op) MarshalJSON() ([]byte, error) { 44 | return json.Marshal(o.String()) 45 | } 46 | 47 | // json.Unmarshaler 48 | func (o *Op) UnmarshalJSON(raw []byte) error { 49 | var expected string 50 | if err := json.Unmarshal(raw, &expected); err != nil { 51 | return err 52 | } 53 | 54 | for k, v := range opString { 55 | if v == expected { 56 | *o = k 57 | return nil 58 | } 59 | } 60 | 61 | return fmt.Errorf("unsupported op type: %s", string(raw)) 62 | } 63 | 64 | // Apply performs the operation on the value v. The value v will be modified. 65 | // In the case of an error, v may still be modified. If you wish to protect 66 | // against partial failure, please deep copy the object prior to changes. 67 | func (o *Operation) Apply(v interface{}) (interface{}, error) { 68 | f, ok := opApplyMap[o.Op] 69 | if !ok { 70 | return v, fmt.Errorf("unknown operation: %s", o.Op) 71 | } 72 | 73 | result, err := f(o, v) 74 | if err != nil { 75 | return result, fmt.Errorf("error applying operation %s: %s", o.Op, err) 76 | } 77 | 78 | return result, nil 79 | } 80 | 81 | var opString = map[Op]string{ 82 | OpInvalid: "invalid", 83 | OpAdd: "add", 84 | OpRemove: "remove", 85 | OpReplace: "replace", 86 | OpMove: "move", 87 | OpCopy: "copy", 88 | OpTest: "test", 89 | } 90 | 91 | // onApplyFunc is the type used internally for applying operations. 92 | type opApplyFunc func(*Operation, interface{}) (interface{}, error) 93 | 94 | // onApplyMap is the map used for lookup for the action to perform 95 | // when applying an operation. 96 | var opApplyMap map[Op]opApplyFunc 97 | 98 | func init() { 99 | // We can't initialize this inline above since it causes an 100 | // "initialization loop" error on the compiler. 101 | opApplyMap = map[Op]opApplyFunc{ 102 | OpAdd: opAdd, 103 | OpRemove: opRemove, 104 | OpReplace: opReplace, 105 | OpMove: opMove, 106 | OpCopy: opCopy, 107 | OpTest: opTest, 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /operation_add.go: -------------------------------------------------------------------------------- 1 | package patchstructure 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "strconv" 7 | 8 | "github.com/mitchellh/pointerstructure" 9 | ) 10 | 11 | // RFC6902 4.1 12 | func opAdd(op *Operation, v interface{}) (interface{}, error) { 13 | // Parse the path 14 | pointer, err := pointerstructure.Parse(op.Path) 15 | if err != nil { 16 | return v, err 17 | } 18 | 19 | // If the pointer is root, then we apply directly to it since it'll 20 | // replace the entire doc. RFC quote below. 21 | if pointer.IsRoot() { 22 | // "The root of the target document - whereupon the specified value 23 | // becomes the entire content of the target document." 24 | return pointer.Set(v, op.Value) 25 | } 26 | 27 | // Get the path that we want to add to (the parent) 28 | parent, err := pointer.Parent().Get(v) 29 | if err != nil { 30 | return v, err 31 | } 32 | 33 | // The type will determine how we handle this 34 | parentVal := reflect.ValueOf(parent) 35 | switch parentVal.Kind() { 36 | case reflect.Map: 37 | // "If the target location specifies an object member that does not 38 | // already exist, a new member is added to the object." 39 | // 40 | // "If the target location specifies an object member that does exist, 41 | // that member's value is replaced." 42 | return pointer.Set(v, op.Value) 43 | 44 | case reflect.Slice: 45 | return opAddSlice(pointer, parentVal, op, v) 46 | 47 | default: 48 | return v, fmt.Errorf( 49 | "can only add to maps, slices, arrays, or structs, got %q", 50 | parentVal.Kind()) 51 | } 52 | } 53 | 54 | func opAddSlice( 55 | p *pointerstructure.Pointer, 56 | parentVal reflect.Value, 57 | op *Operation, 58 | v interface{}) (interface{}, error) { 59 | // Get the final part. If the part is "-" we can directly set since 60 | // pointerstructure will handle the append. 61 | endPart := p.Parts[len(p.Parts)-1] 62 | if endPart == "-" { 63 | return p.Set(v, op.Value) 64 | } 65 | 66 | // "An element to add to an existing array - whereupon the supplied 67 | // value is added to the array at the indicated location. Any 68 | // elements at or above the specified index are shifted one position 69 | // to the right." 70 | // 71 | // The above isn't a natural or built-in JSON pointer operation so 72 | // we're going to have to do some custom reflection here to mimic this: 73 | // 74 | // s = append(s, 0) 75 | // copy(s[i+1:], s[i:]) 76 | // s[i] = x 77 | 78 | // First step: convert the part to an int so we can determine what index 79 | idxRaw, err := strconv.ParseInt(endPart, 10, 0) 80 | if err != nil { 81 | return v, fmt.Errorf("error parsing index %q: %s", endPart, err) 82 | } 83 | idx := int(idxRaw) 84 | 85 | // "The specified index MUST NOT be greater than the 86 | // number of elements in the array" 87 | if idx >= parentVal.Len() { 88 | return v, fmt.Errorf( 89 | "index %d is greater than the length %d", 90 | idx, parentVal.Len()) 91 | } 92 | 93 | // Create a zero value to append for: s = append(s, 0) 94 | sliceType := parentVal.Type() 95 | slice := reflect.Append(parentVal, reflect.Indirect(reflect.New(sliceType.Elem()))) 96 | 97 | // Perform the copy: copy(s[i+1:], s[i:]) 98 | reflect.Copy( 99 | slice.Slice(idx+1, slice.Len()), 100 | slice.Slice(idx, slice.Len())) 101 | 102 | // Set the parent so that the slice is overwritten 103 | v, err = p.Parent().Set(v, slice.Interface()) 104 | if err != nil { 105 | return v, err 106 | } 107 | 108 | // Write: s[i] = x 109 | return p.Set(v, op.Value) 110 | } 111 | -------------------------------------------------------------------------------- /operation_copy.go: -------------------------------------------------------------------------------- 1 | package patchstructure 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/mitchellh/copystructure" 7 | "github.com/mitchellh/pointerstructure" 8 | ) 9 | 10 | // RFC6902 4.5 11 | func opCopy(op *Operation, v interface{}) (interface{}, error) { 12 | // Parse the path. We do this even though we don't use it to 13 | // avoid syntax errors causing partial applies. 14 | _, err := pointerstructure.Parse(op.Path) 15 | if err != nil { 16 | return v, err 17 | } 18 | 19 | // Parse the from path, which must exist 20 | from, err := pointerstructure.Parse(op.From) 21 | if err != nil { 22 | return v, err 23 | } 24 | 25 | // Get the from value, which must exist 26 | fromValue, err := from.Get(v) 27 | if err != nil { 28 | return v, err 29 | } 30 | 31 | // Perform a deep copy if requested. This is unique to Go to avoid 32 | // references matching. We make it opt-out since it feels like the obvious 33 | // behavior when requesting a "copy". 34 | if !op.Shallow { 35 | copy, err := copystructure.Copy(fromValue) 36 | if err != nil { 37 | return v, fmt.Errorf("error copying from value: %s", err) 38 | } 39 | 40 | fromValue = copy 41 | } 42 | 43 | // "This operation is functionally identical to an "add" operation at the 44 | // target location using the value specified in the "from" member." 45 | 46 | addOp := &Operation{ 47 | Op: OpAdd, 48 | Path: op.Path, 49 | Value: fromValue, 50 | } 51 | 52 | // Add 53 | return addOp.Apply(v) 54 | } 55 | -------------------------------------------------------------------------------- /operation_move.go: -------------------------------------------------------------------------------- 1 | package patchstructure 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | 7 | "github.com/mitchellh/pointerstructure" 8 | ) 9 | 10 | // RFC6902 4.4 11 | func opMove(op *Operation, v interface{}) (interface{}, error) { 12 | // Parse the path. We do this even though we don't use it to 13 | // avoid syntax errors causing partial applies. 14 | to, err := pointerstructure.Parse(op.Path) 15 | if err != nil { 16 | return v, err 17 | } 18 | 19 | // Parse the from path, which must exist 20 | from, err := pointerstructure.Parse(op.From) 21 | if err != nil { 22 | return v, err 23 | } 24 | 25 | // "The "from" location MUST NOT be a proper prefix of the "path" 26 | // location; i.e., a location cannot be moved into one of its children." 27 | if len(from.Parts) < len(to.Parts) { 28 | if reflect.DeepEqual(from.Parts, to.Parts[:len(from.Parts)]) { 29 | return v, fmt.Errorf( 30 | "move cannot move into a child path of the from path") 31 | } 32 | } 33 | 34 | // Get the from value, which must exist 35 | fromValue, err := from.Get(v) 36 | if err != nil { 37 | return v, err 38 | } 39 | 40 | // "This operation is functionally identical to a "remove" operation on 41 | // the "from" location, followed immediately by an "add" operation at 42 | // the target location with the value that was just removed." 43 | removeOp := &Operation{ 44 | Op: OpRemove, 45 | Path: op.From, 46 | } 47 | 48 | addOp := &Operation{ 49 | Op: OpAdd, 50 | Path: op.Path, 51 | Value: fromValue, 52 | } 53 | 54 | // Remove first 55 | v, err = removeOp.Apply(v) 56 | if err != nil { 57 | return v, err 58 | } 59 | 60 | // Add 61 | return addOp.Apply(v) 62 | } 63 | -------------------------------------------------------------------------------- /operation_remove.go: -------------------------------------------------------------------------------- 1 | package patchstructure 2 | 3 | import ( 4 | "github.com/mitchellh/pointerstructure" 5 | ) 6 | 7 | // RFC6902 4.2 8 | func opRemove(op *Operation, v interface{}) (interface{}, error) { 9 | // Parse the path 10 | pointer, err := pointerstructure.Parse(op.Path) 11 | if err != nil { 12 | return v, err 13 | } 14 | 15 | // The only thing we need to check is that the pointer path actually 16 | // exists. If it doesn't, it is an error. To quote the RFC: 17 | // 18 | // "The target location MUST exist for the operation to be successful." 19 | if _, err := pointer.Get(v); err != nil { 20 | return v, err 21 | } 22 | 23 | // Delete always does the right thing 24 | return pointer.Delete(v) 25 | } 26 | -------------------------------------------------------------------------------- /operation_replace.go: -------------------------------------------------------------------------------- 1 | package patchstructure 2 | 3 | import ( 4 | "github.com/mitchellh/pointerstructure" 5 | ) 6 | 7 | // RFC6902 4.3 8 | func opReplace(op *Operation, v interface{}) (interface{}, error) { 9 | // Parse the path 10 | pointer, err := pointerstructure.Parse(op.Path) 11 | if err != nil { 12 | return v, err 13 | } 14 | 15 | // The only thing we need to check is that the pointer path actually 16 | // exists. If it doesn't, it is an error. To quote the RFC: 17 | // 18 | // "The target location MUST exist for the operation to be successful." 19 | if _, err := pointer.Get(v); err != nil { 20 | return v, err 21 | } 22 | 23 | // Set always does the right thing 24 | return pointer.Set(v, op.Value) 25 | } 26 | -------------------------------------------------------------------------------- /operation_test.go: -------------------------------------------------------------------------------- 1 | package patchstructure 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "reflect" 7 | "testing" 8 | ) 9 | 10 | func TestOperationApply(t *testing.T) { 11 | cases := []struct { 12 | Name string 13 | Operation Operation 14 | Input interface{} 15 | Expected interface{} 16 | Err bool 17 | }{ 18 | //----------------------------------------------------------- 19 | // add 20 | //----------------------------------------------------------- 21 | 22 | // "The root of the target document - whereupon the specified value 23 | // becomes the entire content of the target document." 24 | { 25 | "add: root", 26 | Operation{ 27 | Op: OpAdd, 28 | Path: "", 29 | Value: "bar", 30 | }, 31 | nil, 32 | "bar", 33 | false, 34 | }, 35 | 36 | // "A member to add to an existing object - whereupon the supplied 37 | // value is added to that object at the indicated location. If the 38 | // member already exists, it is replaced by the specified value." 39 | { 40 | "add: new member", 41 | Operation{ 42 | Op: OpAdd, 43 | Path: "/a", 44 | Value: "bar", 45 | }, 46 | map[string]interface{}{}, 47 | map[string]interface{}{"a": "bar"}, 48 | false, 49 | }, 50 | 51 | { 52 | "add: existing member", 53 | Operation{ 54 | Op: OpAdd, 55 | Path: "/a", 56 | Value: "bar", 57 | }, 58 | map[string]interface{}{"a": "foo"}, 59 | map[string]interface{}{"a": "bar"}, 60 | false, 61 | }, 62 | 63 | // "If the target location specifies an array index, a new value is 64 | // inserted into the array at the specified index." 65 | { 66 | "add: slice append", 67 | Operation{ 68 | Op: OpAdd, 69 | Path: "/-", 70 | Value: "bar", 71 | }, 72 | []interface{}{1, 2}, 73 | []interface{}{1, 2, "bar"}, 74 | false, 75 | }, 76 | 77 | { 78 | "add: slice index", 79 | Operation{ 80 | Op: OpAdd, 81 | Path: "/1", 82 | Value: "bar", 83 | }, 84 | []interface{}{1, 2}, 85 | []interface{}{1, "bar", 2}, 86 | false, 87 | }, 88 | 89 | { 90 | "add: slice index at 0", 91 | Operation{ 92 | Op: OpAdd, 93 | Path: "/0", 94 | Value: "bar", 95 | }, 96 | []interface{}{1, 2}, 97 | []interface{}{"bar", 1, 2}, 98 | false, 99 | }, 100 | 101 | // "The specified index MUST NOT be greater than the 102 | // number of elements in the array" 103 | { 104 | "add: slice index out of bounds", 105 | Operation{ 106 | Op: OpAdd, 107 | Path: "/4", 108 | Value: "bar", 109 | }, 110 | []interface{}{1, 2}, 111 | nil, 112 | true, 113 | }, 114 | 115 | // "However, the object itself or an array containing it does need to 116 | // exist, and it remains an error for that not to be the case." 117 | { 118 | "add: non-existent container", 119 | Operation{ 120 | Op: OpAdd, 121 | Path: "/b/a", 122 | Value: "bar", 123 | }, 124 | map[string]interface{}{"a": "foo"}, 125 | nil, 126 | true, 127 | }, 128 | 129 | //----------------------------------------------------------- 130 | // remove 131 | //----------------------------------------------------------- 132 | 133 | { 134 | "remove: map element", 135 | Operation{ 136 | Op: OpRemove, 137 | Path: "/foo/bar", 138 | }, 139 | map[string]interface{}{ 140 | "foo": map[string]interface{}{ 141 | "bar": 42, 142 | }, 143 | }, 144 | map[string]interface{}{ 145 | "foo": map[string]interface{}{}, 146 | }, 147 | false, 148 | }, 149 | 150 | { 151 | "remove: map element that doesn't exist", 152 | Operation{ 153 | Op: OpRemove, 154 | Path: "/foo/baz", 155 | }, 156 | map[string]interface{}{ 157 | "foo": map[string]interface{}{ 158 | "bar": 42, 159 | }, 160 | }, 161 | nil, 162 | true, 163 | }, 164 | 165 | { 166 | "remove: slice index at 0", 167 | Operation{ 168 | Op: OpRemove, 169 | Path: "/0", 170 | }, 171 | []interface{}{1, 2}, 172 | []interface{}{2}, 173 | false, 174 | }, 175 | 176 | { 177 | "remove: slice index that doesn't exist", 178 | Operation{ 179 | Op: OpRemove, 180 | Path: "/4", 181 | }, 182 | []interface{}{1, 2}, 183 | nil, 184 | true, 185 | }, 186 | 187 | //----------------------------------------------------------- 188 | // replace 189 | //----------------------------------------------------------- 190 | 191 | { 192 | "replace: root", 193 | Operation{ 194 | Op: OpReplace, 195 | Path: "", 196 | Value: "bar", 197 | }, 198 | nil, 199 | "bar", 200 | false, 201 | }, 202 | 203 | { 204 | "replace: new member", 205 | Operation{ 206 | Op: OpReplace, 207 | Path: "/a", 208 | Value: "bar", 209 | }, 210 | map[string]interface{}{}, 211 | nil, 212 | true, 213 | }, 214 | 215 | { 216 | "replace: existing member", 217 | Operation{ 218 | Op: OpReplace, 219 | Path: "/a", 220 | Value: "bar", 221 | }, 222 | map[string]interface{}{"a": "foo"}, 223 | map[string]interface{}{"a": "bar"}, 224 | false, 225 | }, 226 | 227 | // NOTE(mitchellh): It is unclear what the RFC expects for this 228 | // behavior. It says that the target path must exist, and yet 229 | // I'm unsure if a "-" addr exists... it isn't clear. 230 | { 231 | "replace: slice append", 232 | Operation{ 233 | Op: OpReplace, 234 | Path: "/-", 235 | Value: "bar", 236 | }, 237 | []interface{}{1, 2}, 238 | nil, 239 | true, 240 | }, 241 | 242 | { 243 | "replace: slice index", 244 | Operation{ 245 | Op: OpReplace, 246 | Path: "/1", 247 | Value: "bar", 248 | }, 249 | []interface{}{1, 2}, 250 | []interface{}{1, "bar"}, 251 | false, 252 | }, 253 | 254 | { 255 | "replace: slice index at 0", 256 | Operation{ 257 | Op: OpReplace, 258 | Path: "/0", 259 | Value: "bar", 260 | }, 261 | []interface{}{1, 2}, 262 | []interface{}{"bar", 2}, 263 | false, 264 | }, 265 | 266 | { 267 | "replace: slice index out of bounds", 268 | Operation{ 269 | Op: OpReplace, 270 | Path: "/4", 271 | Value: "bar", 272 | }, 273 | []interface{}{1, 2}, 274 | nil, 275 | true, 276 | }, 277 | 278 | { 279 | "replace: non-existent container", 280 | Operation{ 281 | Op: OpReplace, 282 | Path: "/b/a", 283 | Value: "bar", 284 | }, 285 | map[string]interface{}{"a": "foo"}, 286 | nil, 287 | true, 288 | }, 289 | 290 | //----------------------------------------------------------- 291 | // move 292 | //----------------------------------------------------------- 293 | 294 | { 295 | "move: object member", 296 | Operation{ 297 | Op: OpMove, 298 | Path: "/b", 299 | From: "/a", 300 | }, 301 | map[string]interface{}{"a": "bar"}, 302 | map[string]interface{}{"b": "bar"}, 303 | false, 304 | }, 305 | 306 | { 307 | "move: slice index", 308 | Operation{ 309 | Op: OpMove, 310 | Path: "/2", 311 | From: "/1", 312 | }, 313 | []interface{}{1, 2, 3, 4}, 314 | []interface{}{1, 3, 2, 4}, 315 | false, 316 | }, 317 | 318 | { 319 | "move: into self subpath", 320 | Operation{ 321 | Op: OpMove, 322 | Path: "/b/a", 323 | From: "/b", 324 | }, 325 | map[string]interface{}{ 326 | "b": map[string]interface{}{ 327 | "a": 42, 328 | }, 329 | }, 330 | nil, 331 | true, 332 | }, 333 | 334 | //----------------------------------------------------------- 335 | // copy 336 | //----------------------------------------------------------- 337 | 338 | { 339 | "copy: object member", 340 | Operation{ 341 | Op: OpCopy, 342 | Path: "/b", 343 | From: "/a", 344 | }, 345 | map[string]interface{}{"a": "bar"}, 346 | map[string]interface{}{"a": "bar", "b": "bar"}, 347 | false, 348 | }, 349 | 350 | { 351 | "copy: slice index", 352 | Operation{ 353 | Op: OpCopy, 354 | Path: "/2", 355 | From: "/1", 356 | }, 357 | []interface{}{1, 2, 3, 4}, 358 | []interface{}{1, 2, 2, 3, 4}, 359 | false, 360 | }, 361 | 362 | { 363 | "copy: non-existent member", 364 | Operation{ 365 | Op: OpCopy, 366 | Path: "/b", 367 | From: "/a", 368 | }, 369 | map[string]interface{}{}, 370 | nil, 371 | true, 372 | }, 373 | 374 | //----------------------------------------------------------- 375 | // test 376 | //----------------------------------------------------------- 377 | 378 | { 379 | "test: member", 380 | Operation{ 381 | Op: OpTest, 382 | Path: "/a", 383 | Value: "bar", 384 | }, 385 | map[string]interface{}{"a": "bar"}, 386 | map[string]interface{}{"a": "bar"}, 387 | false, 388 | }, 389 | } 390 | 391 | for i, tc := range cases { 392 | t.Run(fmt.Sprintf("%d-%s", i, tc.Name), func(t *testing.T) { 393 | actual, err := tc.Operation.Apply(tc.Input) 394 | if (err != nil) != tc.Err { 395 | t.Fatalf("err: %s", err) 396 | } 397 | if err != nil { 398 | return 399 | } 400 | 401 | if !reflect.DeepEqual(actual, tc.Expected) { 402 | t.Fatalf("bad: %#v", actual) 403 | } 404 | }) 405 | } 406 | } 407 | 408 | func TestOperationJSON(t *testing.T) { 409 | cases := []struct { 410 | Name string 411 | Input string 412 | Expected *Operation 413 | Err bool 414 | }{ 415 | { 416 | "basic", 417 | `{ "op": "replace", "path": "/a/b/c", "value": 42 }`, 418 | &Operation{ 419 | Op: OpReplace, 420 | Path: "/a/b/c", 421 | Value: float64(42), 422 | }, 423 | false, 424 | }, 425 | 426 | { 427 | "shallow", 428 | `{ "op": "copy", "path": "/a/b/c", "shallow": true }`, 429 | &Operation{ 430 | Op: OpCopy, 431 | Path: "/a/b/c", 432 | Shallow: true, 433 | }, 434 | false, 435 | }, 436 | } 437 | 438 | for i, tc := range cases { 439 | t.Run(fmt.Sprintf("%d-%s", i, tc.Name), func(t *testing.T) { 440 | var actual Operation 441 | err := json.Unmarshal([]byte(tc.Input), &actual) 442 | if (err != nil) != tc.Err { 443 | t.Fatalf("err: %s", err) 444 | } 445 | if err != nil { 446 | return 447 | } 448 | 449 | if !reflect.DeepEqual(&actual, tc.Expected) { 450 | t.Fatalf("bad: %#v", &actual) 451 | } 452 | }) 453 | } 454 | } 455 | -------------------------------------------------------------------------------- /operation_test_impl.go: -------------------------------------------------------------------------------- 1 | package patchstructure 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | 7 | "github.com/mitchellh/pointerstructure" 8 | ) 9 | 10 | // RFC6902 4.6 11 | func opTest(op *Operation, v interface{}) (interface{}, error) { 12 | // Parse the path 13 | pointer, err := pointerstructure.Parse(op.Path) 14 | if err != nil { 15 | return v, err 16 | } 17 | 18 | // Target location must exist 19 | target, err := pointer.Get(v) 20 | if err != nil { 21 | return v, err 22 | } 23 | 24 | // Perform the test with "reflect". This can be improved in the future 25 | // with good reason since there are subtle type issues with reflect 26 | // DeepEqual. 27 | err = nil 28 | if !reflect.DeepEqual(target, op.Value) { 29 | err = fmt.Errorf("values not equal: %#v != %#v", target, op.Value) 30 | } 31 | 32 | return v, err 33 | } 34 | -------------------------------------------------------------------------------- /patch.go: -------------------------------------------------------------------------------- 1 | // Package patchstructure is a Go library for applying patches to modify 2 | // existing Go structures. 3 | // 4 | // patchstructure is based on 5 | // [JSON Patch (RFC 6902)](https://tools.ietf.org/html/rfc6902), but 6 | // applies to Go strucutures instead of JSON objects. 7 | // 8 | // The goal of patchstructure is to provide a single API and format for 9 | // representing and applying changes to Go structures. With this in place, 10 | // diffs between structures can be represented, changes to a structure 11 | // can be treated as a log, etc. 12 | package patchstructure 13 | 14 | // Patch applies the set of operations sequentially to the value v. 15 | // 16 | // Patch will halt at the first error. In this case, the returned value 17 | // may be a partial value. This differs from the JSON Patch RFC which states 18 | // that a patch should be atomic. Due to the complexity and cost in deep 19 | // copying and the ability for the interface to store unsupported types 20 | // such as functions (as long as they're not addressed it is okay), we defer 21 | // this functionality to the end user. 22 | // 23 | // If you wish to deep copy your structures take a look at the "copystruture" 24 | // library and call that prior to this. 25 | func Patch(v interface{}, ops []*Operation) (result interface{}, err error) { 26 | result = v 27 | for _, op := range ops { 28 | result, err = op.Apply(result) 29 | if err != nil { 30 | return 31 | } 32 | } 33 | 34 | return 35 | } 36 | -------------------------------------------------------------------------------- /patch_examples_test.go: -------------------------------------------------------------------------------- 1 | package patchstructure 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | func ExamplePatch() { 8 | complex := map[string]interface{}{ 9 | "alice": 42, 10 | "bob": []interface{}{ 11 | map[string]interface{}{ 12 | "name": "Bob", 13 | }, 14 | }, 15 | } 16 | 17 | value, err := Patch(complex, []*Operation{ 18 | &Operation{ 19 | Op: OpCopy, 20 | Path: "/alice", 21 | From: "/bob", 22 | }, 23 | 24 | &Operation{ 25 | Op: OpReplace, 26 | Path: "/alice/0/name", 27 | Value: "Alice", 28 | }, 29 | }) 30 | if err != nil { 31 | panic(err) 32 | } 33 | 34 | fmt.Printf("%s", value) 35 | // Output: 36 | // map[alice:[map[name:Alice]] bob:[map[name:Bob]]] 37 | } 38 | -------------------------------------------------------------------------------- /patch_test.go: -------------------------------------------------------------------------------- 1 | package patchstructure 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "testing" 7 | ) 8 | 9 | // Note that most operation tests that are more exhaustive are in 10 | // operation_test.go. This test just tests basic sequences to ensure 11 | // Patch behavior. 12 | func TestPatch(t *testing.T) { 13 | cases := []struct { 14 | Name string 15 | Ops []*Operation 16 | Input interface{} 17 | Expected interface{} 18 | Err bool 19 | }{ 20 | { 21 | "basic sequence", 22 | []*Operation{ 23 | &Operation{ 24 | Op: OpAdd, 25 | Path: "/a", 26 | Value: "A", 27 | }, 28 | 29 | &Operation{ 30 | Op: OpRemove, 31 | Path: "/b", 32 | }, 33 | }, 34 | map[string]interface{}{"b": 42}, 35 | map[string]interface{}{"a": "A"}, 36 | false, 37 | }, 38 | 39 | { 40 | "partial failure", 41 | []*Operation{ 42 | &Operation{ 43 | Op: OpAdd, 44 | Path: "/a", 45 | Value: "A", 46 | }, 47 | 48 | &Operation{ 49 | Op: OpRemove, 50 | Path: "/c", 51 | }, 52 | 53 | &Operation{ 54 | Op: OpRemove, 55 | Path: "/b", 56 | }, 57 | }, 58 | map[string]interface{}{"b": 42}, 59 | map[string]interface{}{"a": "A", "b": 42}, 60 | true, 61 | }, 62 | } 63 | 64 | for i, tc := range cases { 65 | t.Run(fmt.Sprintf("%d-%s", i, tc.Name), func(t *testing.T) { 66 | actual, err := Patch(tc.Input, tc.Ops) 67 | if (err != nil) != tc.Err { 68 | t.Fatalf("err: %s", err) 69 | } 70 | 71 | if !reflect.DeepEqual(actual, tc.Expected) { 72 | t.Fatalf("bad: %#v", actual) 73 | } 74 | }) 75 | } 76 | } 77 | --------------------------------------------------------------------------------