├── .github ├── dependabot.yml └── workflows │ └── test.yaml ├── .gitignore ├── LICENSE ├── README.md ├── examples ├── basic_example │ └── main.go ├── filter_patches_using_predicates │ └── main.go ├── ignore_slice_order │ └── main.go └── partial_patches │ └── main.go ├── go.mod ├── go.sum ├── handler.go ├── jsonpatch_suite_test.go ├── options.go ├── patch.go ├── patch_test.go ├── pointer.go ├── pointer_test.go ├── predicate.go └── walker.go /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "gomod" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | - package-ecosystem: "github-actions" 13 | directory: "/" 14 | schedule: 15 | interval: "weekly" -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | lint: 14 | name: lint 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v4 18 | - uses: actions/setup-go@v5 19 | with: 20 | go-version: ^1.24 21 | cache: false 22 | - name: golangci-lint 23 | uses: golangci/golangci-lint-action@v8 24 | test: 25 | name: go test 26 | runs-on: ubuntu-latest 27 | steps: 28 | - uses: actions/checkout@v4 29 | - uses: actions/setup-go@v5 30 | with: 31 | go-version: ^1.24 32 | cache: false 33 | - name: go test 34 | run: go test -coverprofile cover.out -timeout 30m 35 | env: 36 | CGO_ENABLED: 0 37 | GO111MODULE: on 38 | GOOS: linux 39 | GOARCH: amd64 40 | - name: send coverage 41 | uses: shogo82148/actions-goveralls@v1 42 | with: 43 | path-to-profile: cover.out -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Binaries for programs and plugins 3 | *.exe 4 | *.exe~ 5 | *.dll 6 | *.so 7 | *.dylib 8 | bin 9 | testbin/* 10 | 11 | # Test binary, build with `go test -c` 12 | *.test 13 | 14 | # Mock (gomock) artifacts 15 | gomock_reflect_* 16 | 17 | # Output of the go coverage tool, specifically when used with LiteIDE 18 | *.out 19 | 20 | # editor and IDE paraphernalia 21 | .idea 22 | *.swp 23 | *.swo 24 | *~ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Norwin Schnyder 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # jsonpatch 2 | [![GitHub Action](https://img.shields.io/badge/GitHub-Action-blue)](https://github.com/features/actions) 3 | [![Documentation](https://img.shields.io/badge/godoc-reference-5272B4.svg)](https://pkg.go.dev/github.com/snorwin/jsonpatch) 4 | [![Test](https://img.shields.io/github/actions/workflow/status/snorwin/jsonpatch/test.yaml?label=tests&logo=github)](https://github.com/snorwin/sonpatch/actions) 5 | [![Go Report Card](https://goreportcard.com/badge/github.com/snorwin/jsonpatch)](https://goreportcard.com/report/github.com/snorwin/jsonpatch) 6 | [![Coverage Status](https://coveralls.io/repos/github/snorwin/jsonpatch/badge.svg?branch=main)](https://coveralls.io/github/snorwin/jsonpatch?branch=main) 7 | [![Releases](https://img.shields.io/github/v/release/snorwin/jsonpatch)](https://github.com/snorwin/jsonpatch/releases) 8 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 9 | 10 | `jsonpatch` is a Go library to create JSON patches ([RFC6902](http://tools.ietf.org/html/rfc6902)) directly from arbitrary Go objects and facilitates the implementation of sophisticated custom (e.g. filtered, validated) patch creation. 11 | 12 | ## Basic Example 13 | 14 | ```go 15 | package main 16 | 17 | import ( 18 | "fmt" 19 | 20 | "github.com/snorwin/jsonpatch" 21 | ) 22 | 23 | type Person struct { 24 | Name string `json:"name"` 25 | Age int `json:"age"` 26 | } 27 | 28 | func main() { 29 | original := &Person{ 30 | Name: "John Doe", 31 | Age: 42, 32 | } 33 | updated := &Person{ 34 | Name: "Jane Doe", 35 | Age: 21, 36 | } 37 | 38 | patch, _ := jsonpatch.CreateJSONPatch(updated, original) 39 | fmt.Println(patch.String()) 40 | } 41 | ``` 42 | ```json 43 | [{"op":"replace","path":"/name","value":"Jane Doe"},{"op":"replace","path":"/age","value":21}] 44 | ``` 45 | 46 | ## Options 47 | ### Filter patches using Predicates 48 | The option `WithPredicate` sets a patch `Predicate` which can be used to filter or validate the patch creation. 49 | For each kind of patch (`add`, `remove` and `replace`) a dedicated filter function can be configured. The 50 | predicate will be checked before a patch is created, or the JSON object is processed further. 51 | 52 | #### Example 53 | ```go 54 | package main 55 | 56 | import ( 57 | "fmt" 58 | 59 | "github.com/snorwin/jsonpatch" 60 | ) 61 | 62 | type Job struct { 63 | Position string `json:"position"` 64 | Company string `json:"company"` 65 | Volunteer bool `json:"volunteer"` 66 | } 67 | 68 | func main() { 69 | original := []Job{ 70 | {Position: "IT Trainer", Company: "Powercoders", Volunteer: true}, 71 | {Position: "Software Engineer", Company: "Github"}, 72 | } 73 | updated := []Job{ 74 | {Position: "Senior IT Trainer", Company: "Powercoders", Volunteer: true}, 75 | {Position: "Senior Software Engineer", Company: "Github"}, 76 | } 77 | 78 | patch, _ := jsonpatch.CreateJSONPatch(updated, original, jsonpatch.WithPredicate(jsonpatch.Funcs{ 79 | ReplaceFunc: func(pointer jsonpatch.JSONPointer, value, _ interface{}) bool { 80 | // only update volunteering jobs 81 | if job, ok := value.(Job); ok { 82 | return job.Volunteer 83 | } 84 | return true 85 | }, 86 | })) 87 | fmt.Println(patch.String()) 88 | } 89 | ``` 90 | 91 | ```json 92 | [{"op":"replace","path":"/0/position","value":"Senior IT Trainer"}] 93 | ``` 94 | 95 | 96 | ### Create partial patches 97 | The option `WithPrefix` is used to specify a JSON pointer prefix if only a sub part of JSON structure needs to be patched, 98 | but the patch still need to be applied on the entire JSON object. 99 | 100 | #### Example 101 | ```go 102 | package main 103 | 104 | import ( 105 | "fmt" 106 | 107 | "github.com/snorwin/jsonpatch" 108 | ) 109 | 110 | type Person struct { 111 | Name string `json:"name"` 112 | Age int `json:"age"` 113 | Jobs []Job `json:"jobs"` 114 | } 115 | 116 | type Job struct { 117 | Position string `json:"position"` 118 | Company string `json:"company"` 119 | Volunteer bool `json:"volunteer"` 120 | } 121 | 122 | func main() { 123 | original := &Person{ 124 | Name: "John Doe", 125 | Age: 42, 126 | Jobs: []Job{{Position: "IT Trainer", Company: "Powercoders"}}, 127 | } 128 | updated := []Job{ 129 | {Position: "Senior IT Trainer", Company: "Powercoders", Volunteer: true}, 130 | {Position: "Software Engineer", Company: "Github"}, 131 | } 132 | 133 | patch, _ := jsonpatch.CreateJSONPatch(updated, original.Jobs, jsonpatch.WithPrefix(jsonpatch.ParseJSONPointer("/jobs"))) 134 | fmt.Println(patch.String()) 135 | } 136 | ``` 137 | ```json 138 | [{"op":"replace","path":"/jobs/0/position","value":"Senior IT Trainer"},{"op":"replace","path":"/jobs/0/volunteer","value":true},{"op":"add","path":"/jobs/1","value":{"position":"Software Engineer","company":"Github","volunteer":false}}] 139 | ``` 140 | 141 | ### Ignore slice order 142 | There are two options to ignore the slice order: 143 | - `IgnoreSliceOrder` will ignore the order of all slices of built-in types (e.g. `int`, `string`) during the patch creation 144 | and will instead use the value itself in order to match and compare the current and modified JSON. 145 | - `IgnoreSliceOrderWithPattern` allows to specify for which slices the order should be ignored using JSONPointer patterns (e.g. `/jobs`, `/jobs/*`). 146 | Furthermore, the slice order of structs (and pointer of structs) slices can be ignored by specifying a JSON field which should be used 147 | to match the struct values. 148 | 149 | > NOTE: Ignoring the slice order only works if the elements (or the values used to match structs) are unique 150 | 151 | #### Example 152 | ```go 153 | package main 154 | 155 | import ( 156 | "fmt" 157 | 158 | "github.com/snorwin/jsonpatch" 159 | ) 160 | 161 | type Person struct { 162 | Name string `json:"name"` 163 | Pseudonyms []string `json:"pseudonyms"` 164 | Jobs []Job `json:"jobs"` 165 | } 166 | 167 | type Job struct { 168 | Position string `json:"position"` 169 | Company string `json:"company"` 170 | Volunteer bool `json:"volunteer"` 171 | } 172 | 173 | func main() { 174 | original := Person{ 175 | Name: "John Doe", 176 | Pseudonyms: []string{"Jo", "JayD"}, 177 | Jobs: []Job{ 178 | {Position: "Software Engineer", Company: "Github"}, 179 | {Position: "IT Trainer", Company: "Powercoders"}, 180 | }, 181 | } 182 | updated := Person{ 183 | Name: "John Doe", 184 | Pseudonyms: []string{"Jonny", "Jo"}, 185 | Jobs: []Job{ 186 | {Position: "IT Trainer", Company: "Powercoders", Volunteer: true}, 187 | {Position: "Senior Software Engineer", Company: "Github"}, 188 | }, 189 | } 190 | 191 | patch, _ := jsonpatch.CreateJSONPatch(updated, original, 192 | jsonpatch.IgnoreSliceOrderWithPattern([]jsonpatch.IgnorePattern{{Pattern: "/*", JSONField: "company"}}), 193 | jsonpatch.IgnoreSliceOrder(), 194 | ) 195 | fmt.Println(patch.String()) 196 | } 197 | ``` 198 | ```json 199 | [{"op":"add","path":"/pseudonyms/2","value":"Jonny"},{"op":"remove","path":"/pseudonyms/1"},{"op":"replace","path":"/jobs/1/volunteer","value":true},{"op":"replace","path":"/jobs/0/position","value":"Senior Software Engineer"}] 200 | ``` -------------------------------------------------------------------------------- /examples/basic_example/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/snorwin/jsonpatch" 7 | ) 8 | 9 | type Person struct { 10 | Name string `json:"name"` 11 | Age int `json:"age"` 12 | } 13 | 14 | func main() { 15 | original := &Person{ 16 | Name: "John Doe", 17 | Age: 42, 18 | } 19 | updated := &Person{ 20 | Name: "Jane Doe", 21 | Age: 21, 22 | } 23 | 24 | patch, _ := jsonpatch.CreateJSONPatch(updated, original) 25 | fmt.Println(patch.String()) 26 | } 27 | -------------------------------------------------------------------------------- /examples/filter_patches_using_predicates/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/snorwin/jsonpatch" 7 | ) 8 | 9 | type Job struct { 10 | Position string `json:"position"` 11 | Company string `json:"company"` 12 | Volunteer bool `json:"volunteer"` 13 | } 14 | 15 | func main() { 16 | original := []Job{ 17 | {Position: "IT Trainer", Company: "Powercoders", Volunteer: true}, 18 | {Position: "Software Engineer", Company: "Github"}, 19 | } 20 | updated := []Job{ 21 | {Position: "Senior IT Trainer", Company: "Powercoders", Volunteer: true}, 22 | {Position: "Senior Software Engineer", Company: "Github"}, 23 | } 24 | 25 | patch, _ := jsonpatch.CreateJSONPatch(updated, original, jsonpatch.WithPredicate(jsonpatch.Funcs{ 26 | ReplaceFunc: func(pointer jsonpatch.JSONPointer, value, _ interface{}) bool { 27 | // only update volunteering jobs 28 | if job, ok := value.(Job); ok { 29 | return job.Volunteer 30 | } 31 | return true 32 | }, 33 | })) 34 | fmt.Println(patch.String()) 35 | } 36 | -------------------------------------------------------------------------------- /examples/ignore_slice_order/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/snorwin/jsonpatch" 7 | ) 8 | 9 | type Person struct { 10 | Name string `json:"name"` 11 | Pseudonyms []string `json:"pseudonyms"` 12 | Jobs []Job `json:"jobs"` 13 | } 14 | 15 | type Job struct { 16 | Position string `json:"position"` 17 | Company string `json:"company"` 18 | Volunteer bool `json:"volunteer"` 19 | } 20 | 21 | func main() { 22 | original := Person{ 23 | Name: "John Doe", 24 | Pseudonyms: []string{"Jo", "JayD"}, 25 | Jobs: []Job{ 26 | {Position: "Software Engineer", Company: "Github"}, 27 | {Position: "IT Trainer", Company: "Powercoders"}, 28 | }, 29 | } 30 | updated := Person{ 31 | Name: "John Doe", 32 | Pseudonyms: []string{"Jonny", "Jo"}, 33 | Jobs: []Job{ 34 | {Position: "IT Trainer", Company: "Powercoders", Volunteer: true}, 35 | {Position: "Senior Software Engineer", Company: "Github"}, 36 | }, 37 | } 38 | 39 | patch, _ := jsonpatch.CreateJSONPatch(updated, original, 40 | jsonpatch.IgnoreSliceOrderWithPattern([]jsonpatch.IgnorePattern{{Pattern: "/*", JSONField: "company"}}), 41 | jsonpatch.IgnoreSliceOrder(), 42 | ) 43 | fmt.Println(patch.String()) 44 | } 45 | -------------------------------------------------------------------------------- /examples/partial_patches/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/snorwin/jsonpatch" 7 | ) 8 | 9 | type Person struct { 10 | Name string `json:"name"` 11 | Age int `json:"age"` 12 | Jobs []Job `json:"jobs"` 13 | } 14 | 15 | type Job struct { 16 | Position string `json:"position"` 17 | Company string `json:"company"` 18 | Volunteer bool `json:"volunteer"` 19 | } 20 | 21 | func main() { 22 | original := &Person{ 23 | Name: "John Doe", 24 | Age: 42, 25 | Jobs: []Job{{Position: "IT Trainer", Company: "Powercoders"}}, 26 | } 27 | updated := []Job{ 28 | {Position: "Senior IT Trainer", Company: "Powercoders", Volunteer: true}, 29 | {Position: "Software Engineer", Company: "Github"}, 30 | } 31 | 32 | patch, _ := jsonpatch.CreateJSONPatch(updated, original.Jobs, jsonpatch.WithPrefix(jsonpatch.ParseJSONPointer("/jobs"))) 33 | fmt.Println(patch.String()) 34 | } 35 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/snorwin/jsonpatch 2 | 3 | go 1.24 4 | 5 | require ( 6 | github.com/evanphx/json-patch/v5 v5.9.11 7 | github.com/go-faker/faker/v4 v4.6.1 8 | github.com/onsi/ginkgo/v2 v2.23.4 9 | github.com/onsi/gomega v1.37.0 10 | ) 11 | 12 | require ( 13 | github.com/go-logr/logr v1.4.2 // indirect 14 | github.com/go-task/slim-sprig/v3 v3.0.0 // indirect 15 | github.com/google/go-cmp v0.7.0 // indirect 16 | github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect 17 | go.uber.org/automaxprocs v1.6.0 // indirect 18 | golang.org/x/net v0.38.0 // indirect 19 | golang.org/x/sys v0.32.0 // indirect 20 | golang.org/x/text v0.24.0 // indirect 21 | golang.org/x/tools v0.31.0 // indirect 22 | gopkg.in/yaml.v3 v3.0.1 // indirect 23 | ) 24 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU= 4 | github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM= 5 | github.com/go-faker/faker/v4 v4.6.1 h1:xUyVpAjEtB04l6XFY0V/29oR332rOSPWV4lU8RwDt4k= 6 | github.com/go-faker/faker/v4 v4.6.1/go.mod h1:arSdxNCSt7mOhdk8tEolvHeIJ7eX4OX80wXjKKvkKBY= 7 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 8 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 9 | github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= 10 | github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= 11 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 12 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 13 | github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8= 14 | github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= 15 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 16 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 17 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 18 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 19 | github.com/onsi/ginkgo/v2 v2.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus= 20 | github.com/onsi/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8= 21 | github.com/onsi/gomega v1.37.0 h1:CdEG8g0S133B4OswTDC/5XPSzE1OeP29QOioj2PID2Y= 22 | github.com/onsi/gomega v1.37.0/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0= 23 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 24 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 25 | github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= 26 | github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U= 27 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 28 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 29 | go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= 30 | go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= 31 | golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= 32 | golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= 33 | golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= 34 | golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 35 | golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= 36 | golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= 37 | golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU= 38 | golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ= 39 | google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= 40 | google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= 41 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 42 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 43 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 44 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 45 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 46 | -------------------------------------------------------------------------------- /handler.go: -------------------------------------------------------------------------------- 1 | package jsonpatch 2 | 3 | // Handler is the interfaces used by the walker to create patches 4 | type Handler interface { 5 | // Add creates a JSONPatch with an 'add' operation and appends it to the patch list 6 | Add(pointer JSONPointer, modified interface{}) []JSONPatch 7 | 8 | // Remove creates a JSONPatch with an 'remove' operation and appends it to the patch list 9 | Remove(pointer JSONPointer, current interface{}) []JSONPatch 10 | 11 | // Replace creates a JSONPatch with an 'replace' operation and appends it to the patch list 12 | Replace(pointer JSONPointer, modified, current interface{}) []JSONPatch 13 | } 14 | 15 | // DefaultHandler implements the Handler 16 | type DefaultHandler struct{} 17 | 18 | // Add implements Handler 19 | func (h *DefaultHandler) Add(pointer JSONPointer, value interface{}) []JSONPatch { 20 | // The 'add' operation either inserts a value into the array at the specified index or adds a new member to the object 21 | // NOTE: If the target location specifies an object member that does exist, that member's value is replaced 22 | return []JSONPatch{ 23 | { 24 | Operation: "add", 25 | Path: pointer.String(), 26 | Value: value, 27 | }, 28 | } 29 | } 30 | 31 | // Remove implements Handler 32 | func (h *DefaultHandler) Remove(pointer JSONPointer, _ interface{}) []JSONPatch { 33 | // The 'remove' operation removes the value at the target location (specified by the pointer) 34 | return []JSONPatch{ 35 | { 36 | Operation: "remove", 37 | Path: pointer.String(), 38 | }, 39 | } 40 | } 41 | 42 | // Replace implements Handler 43 | func (h *DefaultHandler) Replace(pointer JSONPointer, value, _ interface{}) []JSONPatch { 44 | // The 'replace' operation replaces the value at the target location with a new value 45 | return []JSONPatch{ 46 | { 47 | Operation: "replace", 48 | Path: pointer.String(), 49 | Value: value, 50 | }, 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /jsonpatch_suite_test.go: -------------------------------------------------------------------------------- 1 | package jsonpatch_test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/ginkgo/v2" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestPatch(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "JSONPatch Suite") 13 | } 14 | -------------------------------------------------------------------------------- /options.go: -------------------------------------------------------------------------------- 1 | package jsonpatch 2 | 3 | // Option allow to configure the walker instance 4 | type Option func(r *walker) 5 | 6 | // WithPredicate set a patch Predicate for the walker. This can be used to filter or validate the patch creation 7 | func WithPredicate(predicate Predicate) Option { 8 | return func(w *walker) { 9 | w.predicate = predicate 10 | } 11 | } 12 | 13 | // WithHandler set a patch Handler for the walker. This can be used to customize the patch creation 14 | func WithHandler(handler Handler) Option { 15 | return func(w *walker) { 16 | w.handler = handler 17 | } 18 | } 19 | 20 | // WithPrefix is used to specify a prefix if only a sub part of JSON structure needs to be patched 21 | func WithPrefix(prefix []string) Option { 22 | return func(w *walker) { 23 | if len(prefix) > 0 && prefix[0] == "" { 24 | w.prefix = append(w.prefix, prefix[1:]...) 25 | } else { 26 | w.prefix = append(w.prefix, prefix...) 27 | } 28 | } 29 | } 30 | 31 | // IgnoreSliceOrder will ignore the order of all slices of built-in types during the walk and will use instead the value 32 | // itself in order to compare the current and modified JSON. 33 | // NOTE: ignoring order only works if the elements in each slice are unique 34 | func IgnoreSliceOrder() Option { 35 | return func(w *walker) { 36 | w.ignoredSlices = append(w.ignoredSlices, IgnorePattern{Pattern: "*"}) 37 | } 38 | } 39 | 40 | // IgnoreSliceOrderWithPattern will ignore the order of slices which paths match the pattern during the walk 41 | // and will use instead the value in order to compare the current and modified JSON. 42 | // (For structs (and pointers of structs) the value of the JSON field specified in IgnorePattern is used for comparison.) 43 | // NOTE: ignoring order only works if the elements in each slice are unique 44 | func IgnoreSliceOrderWithPattern(slices []IgnorePattern) Option { 45 | return func(w *walker) { 46 | w.ignoredSlices = append(slices, w.ignoredSlices...) 47 | } 48 | } 49 | 50 | // IgnorePattern specifies a JSONPointer Pattern and a an optional JSONField 51 | type IgnorePattern struct { 52 | Pattern string 53 | JSONField string 54 | } 55 | -------------------------------------------------------------------------------- /patch.go: -------------------------------------------------------------------------------- 1 | package jsonpatch 2 | 3 | import ( 4 | "encoding/json" 5 | "reflect" 6 | "slices" 7 | ) 8 | 9 | // JSONPatch format is specified in RFC 6902 10 | type JSONPatch struct { 11 | Operation string `json:"op"` 12 | Path string `json:"path"` 13 | Value interface{} `json:"value,omitempty"` 14 | } 15 | 16 | // JSONPatchList is a list of JSONPatch 17 | type JSONPatchList struct { 18 | list []JSONPatch 19 | raw []byte 20 | } 21 | 22 | // Empty returns true if the JSONPatchList is empty 23 | func (l JSONPatchList) Empty() bool { 24 | return l.Len() == 0 25 | } 26 | 27 | // Len returns the length of the JSONPatchList 28 | func (l JSONPatchList) Len() int { 29 | return len(l.list) 30 | } 31 | 32 | // String returns the encoded JSON string of the JSONPatchList 33 | func (l JSONPatchList) String() string { 34 | return string(l.raw) 35 | } 36 | 37 | // Raw returns the raw encoded JSON of the JSONPatchList 38 | func (l JSONPatchList) Raw() []byte { 39 | return l.raw 40 | } 41 | 42 | // List returns a copy of the underlying JSONPatch slice 43 | func (l JSONPatchList) List() []JSONPatch { 44 | return slices.Clone(l.list) 45 | } 46 | 47 | // CreateJSONPatch compares two JSON data structures and creates a JSONPatch according to RFC 6902 48 | func CreateJSONPatch(modified, current interface{}, options ...Option) (JSONPatchList, error) { 49 | // create a new walker 50 | w := &walker{ 51 | handler: &DefaultHandler{}, 52 | predicate: Funcs{}, 53 | prefix: []string{""}, 54 | } 55 | 56 | // apply options to the walker 57 | for _, apply := range options { 58 | apply(w) 59 | } 60 | 61 | if err := w.walk(reflect.ValueOf(modified), reflect.ValueOf(current), w.prefix); err != nil { 62 | return JSONPatchList{}, err 63 | } 64 | 65 | list := w.patchList 66 | if len(list) == 0 { 67 | return JSONPatchList{}, nil 68 | } 69 | raw, err := json.Marshal(list) 70 | 71 | return JSONPatchList{list: list, raw: raw}, err 72 | } 73 | 74 | // CreateThreeWayJSONPatch compares three JSON data structures and creates a three-way JSONPatch according to RFC 6902 75 | func CreateThreeWayJSONPatch(modified, current, original interface{}, options ...Option) (JSONPatchList, error) { 76 | var list []JSONPatch 77 | 78 | // create a new walker 79 | w := &walker{ 80 | handler: &DefaultHandler{}, 81 | predicate: Funcs{}, 82 | prefix: []string{""}, 83 | } 84 | 85 | // apply options to the walker 86 | for _, apply := range options { 87 | apply(w) 88 | } 89 | 90 | // compare modified with current and only keep addition and changes 91 | if err := w.walk(reflect.ValueOf(modified), reflect.ValueOf(current), w.prefix); err != nil { 92 | return JSONPatchList{}, err 93 | } 94 | for _, patch := range w.patchList { 95 | if patch.Operation != "remove" { 96 | list = append(list, patch) 97 | } 98 | } 99 | 100 | // reset walker 101 | w.patchList = []JSONPatch{} 102 | 103 | // compare modified with original and only keep deletions 104 | if err := w.walk(reflect.ValueOf(modified), reflect.ValueOf(original), w.prefix); err != nil { 105 | return JSONPatchList{}, err 106 | } 107 | for _, patch := range w.patchList { 108 | if patch.Operation == "remove" { 109 | list = append(list, patch) 110 | } 111 | } 112 | 113 | if len(list) == 0 { 114 | return JSONPatchList{}, nil 115 | } 116 | raw, err := json.Marshal(list) 117 | 118 | return JSONPatchList{list: list, raw: raw}, err 119 | } 120 | -------------------------------------------------------------------------------- /patch_test.go: -------------------------------------------------------------------------------- 1 | package jsonpatch_test 2 | 3 | import ( 4 | "encoding/json" 5 | "strconv" 6 | "strings" 7 | "time" 8 | 9 | . "github.com/onsi/ginkgo/v2" 10 | . "github.com/onsi/gomega" 11 | 12 | jsonpatch2 "github.com/evanphx/json-patch/v5" 13 | "github.com/go-faker/faker/v4" 14 | 15 | "github.com/snorwin/jsonpatch" 16 | ) 17 | 18 | type A struct { 19 | B *B `json:"ptr,omitempty"` 20 | C C `json:"struct"` 21 | } 22 | 23 | type B struct { 24 | Str string `json:"str,omitempty"` 25 | Bool bool `json:"bool"` 26 | Int int `json:"int"` 27 | Int8 int8 `json:"int8"` 28 | Int16 int16 `json:"int16"` 29 | Int32 int32 `json:"int32"` 30 | Int64 int64 `json:"int64"` 31 | Uint uint `json:"uint"` 32 | Uint8 uint8 `json:"uint8"` 33 | Uint16 uint16 `json:"uint16"` 34 | Uint32 uint32 `json:"uint32"` 35 | Uint64 uint64 `json:"uint64"` 36 | UintPtr uintptr `json:"ptr" faker:"-"` 37 | Float32 float32 `json:"float32"` 38 | Float64 float64 `json:"float64"` 39 | Time time.Time `json:"time"` 40 | } 41 | 42 | type C struct { 43 | Str string `json:"str,omitempty"` 44 | StrMap map[string]string `json:"strmap"` 45 | IntMap map[string]int `json:"intmap"` 46 | BoolMap map[string]bool `json:"boolmap"` 47 | StructMap map[string]B `json:"structmap"` 48 | PtrMap map[string]*B `json:"ptrmap"` 49 | } 50 | 51 | type D struct { 52 | PtrSlice []*B `json:"ptr"` 53 | StructSlice []C `json:"structs"` 54 | StringSlice []string `json:"strs"` 55 | IntSlice []int `json:"ints"` 56 | FloatSlice []float64 `json:"floats"` 57 | StructSliceWithKey []C `json:"structsWithKey"` 58 | PtrSliceWithKey []*B `json:"ptrWithKey"` 59 | } 60 | 61 | type E struct { 62 | unexported int 63 | Exported int `json:"exported"` 64 | } 65 | 66 | type F struct { 67 | Str string `json:"a~bc//2a"` 68 | Int int `json:"a/b"` 69 | Bool bool `json:"x~~e"` 70 | } 71 | 72 | type G struct { 73 | A *A `json:"a"` 74 | B *B `json:"b,omitempty"` 75 | C C `json:"c"` 76 | D D `json:"d"` 77 | E E `json:"e"` 78 | F F `json:"f"` 79 | } 80 | 81 | type H struct { 82 | Ignored string `json:"_"` 83 | NotIgnored string `json:"notIgnored"` 84 | } 85 | 86 | type I struct { 87 | I interface{} `json:"i"` 88 | } 89 | 90 | var _ = Describe("JSONPatch", func() { 91 | Context("CreateJsonPatch_pointer_values", func() { 92 | It("pointer", func() { 93 | // add 94 | testPatch(A{B: &B{Str: "test"}}, A{}) 95 | // remove 96 | testPatch(A{}, A{B: &B{Str: "test"}}) 97 | // replace 98 | testPatch(A{B: &B{Str: "test1"}}, A{B: &B{Str: "test2"}}) 99 | testPatch(&B{Str: "test1"}, &B{Str: "test2"}) 100 | // no change 101 | testPatch(A{B: &B{Str: "test2"}}, A{B: &B{Str: "test2"}}) 102 | testPatch(A{}, A{}) 103 | }) 104 | }) 105 | Context("CreateJsonPatch_struct", func() { 106 | It("pointer", func() { 107 | // add 108 | testPatch(A{C: C{}}, A{}) 109 | // remove 110 | testPatch(A{}, A{C: C{}}) 111 | // replace 112 | testPatch(A{C: C{Str: "test1"}}, A{C: C{Str: "test2"}}) 113 | // no change 114 | testPatch(A{C: C{Str: "test2"}}, A{C: C{Str: "test2"}}) 115 | }) 116 | }) 117 | Context("CreateJsonPatch_data_type_values", func() { 118 | It("string", func() { 119 | // add 120 | testPatch(B{Str: "test"}, B{}) 121 | // remove 122 | testPatch(B{}, B{Str: "test"}) 123 | // replace 124 | testPatch(B{Str: "test1"}, B{Str: "test2"}) 125 | // no change 126 | testPatch(B{Str: "test1"}, B{Str: "test1"}) 127 | }) 128 | It("bool", func() { 129 | // add 130 | testPatch(B{Bool: true}, B{}) 131 | // remove 132 | testPatch(B{}, B{Bool: false}) 133 | // replace 134 | testPatch(B{Bool: false}, B{Bool: true}) 135 | // no change 136 | testPatch(B{Bool: false}, B{Bool: false}) 137 | }) 138 | It("int", func() { 139 | // add 140 | testPatch(B{Int: -1, Int8: 2, Int16: 5, Int32: -1, Int64: 12}, B{}) 141 | // remove 142 | testPatch(B{}, B{Int: 1, Int8: 2, Int16: 5, Int32: 1, Int64: 12}) 143 | // replace 144 | testPatch(B{Int: -1, Int8: 2, Int16: 5, Int32: 1, Int64: 12}, B{Int: 1, Int8: 1, Int16: 1, Int32: 1, Int64: 1}) 145 | // mixed 146 | testPatch(B{Int: -1, Int16: 5, Int32: 1, Int64: 3}, B{Int: -1, Int8: 1, Int32: 1, Int64: 1}) 147 | testPatch(B{Int32: 22, Int64: 22}, B{Int: 1, Int8: 1, Int32: 1, Int64: 1}) 148 | // no change 149 | testPatch(B{Int: -1, Int8: 1, Int16: 1, Int32: 1, Int64: 1}, B{Int: -1, Int8: 1, Int16: 1, Int32: 1, Int64: 1}) 150 | }) 151 | It("uint", func() { 152 | // add 153 | testPatch(B{Uint: 1, Uint8: 2, Uint16: 5, Uint32: 1, Uint64: 12, UintPtr: 3}, B{}) 154 | // remove 155 | testPatch(B{}, B{Uint: 1, Uint8: 2, Uint16: 5, Uint32: 1, Uint64: 12, UintPtr: 3}) 156 | // replace 157 | testPatch(B{Uint: 1, Uint8: 2, Uint16: 5, Uint32: 1, Uint64: 12}, B{Uint: 1, Uint8: 1, Uint16: 1, Uint32: 1, Uint64: 1}) 158 | // mixed 159 | testPatch(B{Uint: 1, Uint16: 5, Uint32: 1, Uint64: 3}, B{Uint: 1, Uint8: 1, Uint32: 1, Uint64: 1}) 160 | testPatch(B{Uint32: 22, Uint64: 22}, B{Uint: 1, Uint8: 1, Uint32: 1, Uint64: 1}) 161 | // no change 162 | testPatch(B{Uint: 1, Uint8: 1, Uint16: 1, Uint32: 1, Uint64: 1}, B{Uint: 1, Uint8: 1, Uint16: 1, Uint32: 1, Uint64: 1}) 163 | }) 164 | It("float", func() { 165 | // add 166 | testPatch(B{Float32: 1.1, Float64: 2.2}, B{}) 167 | // remove 168 | testPatch(B{}, B{Float32: 1.1, Float64: 2.2}) 169 | // replace 170 | testPatch(B{Float32: 1.1, Float64: 2.2}, B{Float32: 1.12, Float64: 2.22}) 171 | // mixed 172 | testPatch(B{Float32: 1.1}, B{Float64: 2.2}) 173 | testPatch(B{Float32: 1.0, Float64: 2.0}, B{Float64: 2.2}) 174 | // no change 175 | testPatch(B{Float32: 1.1, Float64: 2.2}, B{Float32: 1.1, Float64: 2.2}) 176 | }) 177 | It("time", func() { 178 | now := time.Now() 179 | // add 180 | testPatch(B{Time: now}, B{}) 181 | // remove 182 | testPatch(B{}, B{Time: now}) 183 | // replace 184 | testPatch(B{Time: now}, B{Time: now.Add(1 * time.Hour)}) 185 | // no change 186 | testPatch(B{Time: now}, B{Time: now}) 187 | }) 188 | }) 189 | Context("CreateJsonPatch_map", func() { 190 | It("string map", func() { 191 | // add 192 | testPatch(C{StrMap: map[string]string{"key1": "value1"}}, C{}) 193 | // remove 194 | testPatch(C{StrMap: map[string]string{}}, C{StrMap: map[string]string{"key1": "value1"}}) 195 | // replace 196 | testPatch(C{StrMap: map[string]string{"key1": "value1", "key2": "value2", "key3": "value3"}}, C{StrMap: map[string]string{}}) 197 | testPatch(C{StrMap: map[string]string{"key1": "value1", "key2": "value2", "key3": "value3"}}, C{StrMap: map[string]string{"key1": "value1"}}) 198 | testPatch(C{StrMap: map[string]string{"key1": "value1"}}, C{StrMap: map[string]string{"key1": "value2"}}) 199 | // no change 200 | testPatch(C{StrMap: map[string]string{"key1": "value1", "key2": "value2"}}, C{StrMap: map[string]string{"key1": "value1", "key2": "value2"}}) 201 | }) 202 | It("struct map", func() { 203 | // add 204 | testPatch(C{StructMap: map[string]B{"key1": {Str: "value1"}}}, C{}) 205 | testPatch(C{StructMap: map[string]B{"key1": {Str: "value1"}, "key2": {Str: "value2"}}}, C{StructMap: map[string]B{"key1": {Str: "value1"}}}) 206 | // remove 207 | testPatch(C{StructMap: map[string]B{"key1": {Str: "value1"}}}, C{StructMap: map[string]B{"key1": {Str: "value1"}, "key2": {Str: "value2"}}}) 208 | // replace 209 | testPatch(C{StructMap: map[string]B{"key1": {Str: "value1", Bool: true}}}, C{StructMap: map[string]B{"key1": {Str: "old"}}}) 210 | // no change 211 | testPatch(C{StructMap: map[string]B{"key1": {Str: "value1", Bool: true}, "key2": {Str: "value2"}}}, C{StructMap: map[string]B{"key1": {Str: "value1", Bool: true}, "key2": {Str: "value2"}}}) 212 | }) 213 | }) 214 | Context("CreateJsonPatch_slice", func() { 215 | It("int slice", func() { 216 | // add 217 | testPatch(D{IntSlice: []int{1, 2, 3}}, D{}) 218 | testPatch(D{IntSlice: []int{1, 2, 3}}, D{IntSlice: []int{}}) 219 | testPatch(D{IntSlice: []int{1, 2, 3}}, D{IntSlice: []int{1, 2}}) 220 | testPatch(D{IntSlice: []int{1, 2, 3}}, D{IntSlice: []int{1, 3}}) 221 | testPatch(D{IntSlice: []int{1, 2, 3}}, D{IntSlice: []int{2, 3}}) 222 | // remove 223 | testPatch(D{IntSlice: []int{1, 2, 3}}, D{IntSlice: []int{1, 2, 3, 4}}) 224 | testPatch(D{IntSlice: []int{1, 2}}, D{IntSlice: []int{1, 2, 3, 4}}) 225 | testPatch(D{IntSlice: []int{1}}, D{IntSlice: []int{1, 2, 3, 4}}) 226 | testPatch(D{IntSlice: []int{}}, D{IntSlice: []int{1, 2, 3, 4}}) 227 | // replace 228 | testPatch(D{IntSlice: []int{3, 2, 1}}, D{IntSlice: []int{1, 2, 3}}) 229 | // mixed 230 | testPatch(D{IntSlice: []int{2}}, D{IntSlice: []int{1, 2, 3, 4}}) 231 | testPatch(D{IntSlice: []int{4, 3, 2}}, D{IntSlice: []int{1, 2, 3, 4}}) 232 | // no change 233 | testPatch(D{IntSlice: []int{1, 2, 3}}, D{IntSlice: []int{1, 2, 3}}) 234 | }) 235 | It("int slice ignore order", func() { 236 | // add 237 | testPatchWithExpected([]int{1, 2, 3}, []int{1, 3}, []int{1, 3, 2}, jsonpatch.IgnoreSliceOrder()) 238 | testPatchWithExpected([]int{1, 2, 3}, []int{1, 2}, []int{1, 2, 3}, jsonpatch.IgnoreSliceOrder()) 239 | // no change 240 | testPatchWithExpected([]int{3, 2, 1}, []int{1, 2, 3}, []int{1, 2, 3}, jsonpatch.IgnoreSliceOrder()) 241 | testPatchWithExpected([]int{1, 2, 3}, []int{3, 2, 1}, []int{3, 2, 1}, jsonpatch.IgnoreSliceOrder()) 242 | // remove 243 | testPatchWithExpected([]int{3, 1}, []int{1, 2, 3}, []int{1, 3}, jsonpatch.IgnoreSliceOrder()) 244 | testPatchWithExpected([]int{3, 2}, []int{1, 2, 3}, []int{2, 3}, jsonpatch.IgnoreSliceOrder()) 245 | }) 246 | It("float slice ignore order", func() { 247 | // add 248 | testPatchWithExpected([]float32{1.1, 2.1, 3.1}, []float32{1.1, 3.1}, []float32{1.1, 3.1, 2.1}, jsonpatch.IgnoreSliceOrder()) 249 | testPatchWithExpected([]float64{1.1, 2.1, 3.1}, []float64{1.1, 2.1}, []float64{1.1, 2.1, 3.1}, jsonpatch.IgnoreSliceOrder()) 250 | // no change 251 | testPatchWithExpected([]float32{3.1, 2.1, 1.1}, []float32{1.1, 2.1, 3.1}, []float32{1.1, 2.1, 3.1}, jsonpatch.IgnoreSliceOrder()) 252 | testPatchWithExpected([]float64{1.1, 2.1, 3.1}, []float64{3.1, 2.1, 1.1}, []float64{3.1, 2.1, 1.1}, jsonpatch.IgnoreSliceOrder()) 253 | // remove 254 | testPatchWithExpected([]float32{3.1, 1.1}, []float32{1.1, 2.1, 3.1}, []float32{1.1, 3.1}, jsonpatch.IgnoreSliceOrder()) 255 | testPatchWithExpected([]float64{3.1, 2.1}, []float64{1.1, 2.1, 3.1}, []float64{2.1, 3.1}, jsonpatch.IgnoreSliceOrder()) 256 | }) 257 | It("uint slice ignore order", func() { 258 | // add 259 | testPatchWithExpected([]uint{1, 2, 3}, []uint{1, 3}, []uint{1, 3, 2}, jsonpatch.IgnoreSliceOrder()) 260 | testPatchWithExpected([]uint16{1, 2, 3}, []uint16{1, 2}, []uint16{1, 2, 3}, jsonpatch.IgnoreSliceOrder()) 261 | // remove 262 | testPatchWithExpected([]uint32{3, 1}, []uint32{1, 2, 3}, []uint32{1, 3}, jsonpatch.IgnoreSliceOrder()) 263 | testPatchWithExpected([]uint64{3, 2}, []uint64{1, 2, 3}, []uint64{2, 3}, jsonpatch.IgnoreSliceOrder()) 264 | }) 265 | It("bool slice ignore order", func() { 266 | // add 267 | testPatchWithExpected([]bool{true, false}, []bool{false}, []bool{false, true}, jsonpatch.IgnoreSliceOrder()) 268 | testPatchWithExpected([]bool{true, false}, []bool{true}, []bool{true, false}, jsonpatch.IgnoreSliceOrder()) 269 | // remove 270 | testPatchWithExpected([]bool{true}, []bool{false, true}, []bool{true}, jsonpatch.IgnoreSliceOrder()) 271 | testPatchWithExpected([]bool{true}, []bool{true, false}, []bool{true}, jsonpatch.IgnoreSliceOrder()) 272 | }) 273 | It("ptr slice ignore order", func() { 274 | // add 275 | testPatchWithExpected(D{PtrSliceWithKey: []*B{{Str: "key1"}}}, D{}, D{PtrSliceWithKey: []*B{{Str: "key1"}}}, jsonpatch.IgnoreSliceOrderWithPattern([]jsonpatch.IgnorePattern{{"/ptrWithKey", "str"}})) 276 | testPatchWithExpected(D{PtrSliceWithKey: []*B{{Str: "key1"}}}, D{PtrSliceWithKey: []*B{}}, D{PtrSliceWithKey: []*B{{Str: "key1"}}}, jsonpatch.IgnoreSliceOrderWithPattern([]jsonpatch.IgnorePattern{{"/ptrWithKey", "str"}})) 277 | testPatchWithExpected(D{PtrSliceWithKey: []*B{{Str: "key1"}, {Str: "new"}, {Str: "key3"}}}, D{PtrSliceWithKey: []*B{{Str: "key1"}, {Str: "key3"}}}, D{PtrSliceWithKey: []*B{{Str: "key1"}, {Str: "key3"}, {Str: "new"}}}, jsonpatch.IgnoreSliceOrderWithPattern([]jsonpatch.IgnorePattern{{"/ptrWithKey", "str"}})) 278 | }) 279 | It("struct slice ignore order", func() { 280 | // add 281 | testPatchWithExpected(D{StructSliceWithKey: []C{{Str: "key1"}}}, D{}, D{StructSliceWithKey: []C{{Str: "key1"}}}, jsonpatch.IgnoreSliceOrderWithPattern([]jsonpatch.IgnorePattern{{"/structsWithKey", "str"}})) 282 | testPatchWithExpected(D{StructSliceWithKey: []C{{Str: "key1"}}}, D{StructSliceWithKey: []C{}}, D{StructSliceWithKey: []C{{Str: "key1"}}}, jsonpatch.IgnoreSliceOrderWithPattern([]jsonpatch.IgnorePattern{{"/structsWithKey", "str"}})) 283 | testPatchWithExpected(D{StructSliceWithKey: []C{{Str: "key1"}, {Str: "new"}, {Str: "key3"}}}, D{StructSliceWithKey: []C{{Str: "key1"}, {Str: "key3"}}}, D{StructSliceWithKey: []C{{Str: "key1"}, {Str: "key3"}, {Str: "new"}}}, jsonpatch.IgnoreSliceOrderWithPattern([]jsonpatch.IgnorePattern{{"/structsWithKey", "str"}})) 284 | // remove 285 | testPatchWithExpected(D{StructSliceWithKey: []C{}}, D{StructSliceWithKey: []C{{Str: "key1"}}}, D{StructSliceWithKey: []C{}}, jsonpatch.IgnoreSliceOrderWithPattern([]jsonpatch.IgnorePattern{{"/structsWithKey", "str"}})) 286 | testPatchWithExpected(D{StructSliceWithKey: []C{{Str: "key1"}, {Str: "key3"}}}, D{StructSliceWithKey: []C{{Str: "key1"}, {Str: "key2"}, {Str: "key3"}}}, D{StructSliceWithKey: []C{{Str: "key1"}, {Str: "key3"}}}, jsonpatch.IgnoreSliceOrderWithPattern([]jsonpatch.IgnorePattern{{"/structsWithKey", "str"}})) 287 | testPatchWithExpected(D{StructSliceWithKey: []C{{Str: "key2"}, {Str: "key3"}}}, D{StructSliceWithKey: []C{{Str: "key1"}, {Str: "key2"}, {Str: "key3"}}}, D{StructSliceWithKey: []C{{Str: "key2"}, {Str: "key3"}}}, jsonpatch.IgnoreSliceOrderWithPattern([]jsonpatch.IgnorePattern{{"/structsWithKey", "str"}})) 288 | testPatchWithExpected(D{StructSliceWithKey: []C{{Str: "key1"}, {Str: "key2"}}}, D{StructSliceWithKey: []C{{Str: "key1"}, {Str: "key2"}, {Str: "key3"}}}, D{StructSliceWithKey: []C{{Str: "key1"}, {Str: "key2"}}}, jsonpatch.IgnoreSliceOrderWithPattern([]jsonpatch.IgnorePattern{{"/structsWithKey", "str"}})) 289 | testPatchWithExpected(D{StructSliceWithKey: []C{{Str: "key1"}, {Str: "key3"}}}, D{StructSliceWithKey: []C{{Str: "key1"}, {Str: "key2"}, {Str: "key3"}, {Str: "key4"}}}, D{StructSliceWithKey: []C{{Str: "key1"}, {Str: "key3"}}}, jsonpatch.IgnoreSliceOrderWithPattern([]jsonpatch.IgnorePattern{{"/structsWithKey", "str"}})) 290 | testPatchWithExpected(D{StructSliceWithKey: []C{{Str: "key3"}, {Str: "key2"}}}, D{StructSliceWithKey: []C{{Str: "key1"}, {Str: "key2"}, {Str: "key3"}, {Str: "key4"}}}, D{StructSliceWithKey: []C{{Str: "key2"}, {Str: "key3"}}}, jsonpatch.IgnoreSliceOrderWithPattern([]jsonpatch.IgnorePattern{{"/structsWithKey", "str"}})) 291 | // replace 292 | testPatchWithExpected(D{StructSliceWithKey: []C{{Str: "key", StrMap: map[string]string{"key": "value1"}}}}, D{StructSliceWithKey: []C{{Str: "key", StrMap: map[string]string{"key": "value2"}}}}, D{StructSliceWithKey: []C{{Str: "key", StrMap: map[string]string{"key": "value1"}}}}, jsonpatch.IgnoreSliceOrderWithPattern([]jsonpatch.IgnorePattern{{"/structsWithKey", "str"}})) 293 | testPatchWithExpected(D{StructSliceWithKey: []C{{Str: "key", StrMap: map[string]string{"key1": "value"}}}}, D{StructSliceWithKey: []C{{Str: "key", StrMap: map[string]string{"key1": "value"}}}}, D{StructSliceWithKey: []C{{Str: "key", StrMap: map[string]string{"key1": "value"}}}}, jsonpatch.IgnoreSliceOrderWithPattern([]jsonpatch.IgnorePattern{{"/structsWithKey", "str"}})) 294 | // mixed 295 | testPatchWithExpected(D{StructSliceWithKey: []C{{Str: "key1"}, {Str: "new"}, {Str: "key3"}}}, D{StructSliceWithKey: []C{{Str: "key1"}, {Str: "key2"}, {Str: "key3"}, {Str: "key4"}}}, D{StructSliceWithKey: []C{{Str: "key1"}, {Str: "key3"}, {Str: "new"}}}, jsonpatch.IgnoreSliceOrderWithPattern([]jsonpatch.IgnorePattern{{"/structsWithKey", "str"}})) 296 | testPatchWithExpected(D{StructSliceWithKey: []C{{Str: "key3"}, {Str: "key2"}, {Str: "new"}}}, D{StructSliceWithKey: []C{{Str: "key1"}, {Str: "key2"}, {Str: "key3"}, {Str: "key4"}}}, D{StructSliceWithKey: []C{{Str: "key2"}, {Str: "key3"}, {Str: "new"}}}, jsonpatch.IgnoreSliceOrderWithPattern([]jsonpatch.IgnorePattern{{"/structsWithKey", "str"}})) 297 | // no change 298 | testPatchWithExpected(D{StructSliceWithKey: []C{{Str: "key3"}, {Str: "key2", StrMap: map[string]string{"key": "value"}}}}, D{StructSliceWithKey: []C{{Str: "key2", StrMap: map[string]string{"key": "value"}}, {Str: "key3"}}}, D{StructSliceWithKey: []C{{Str: "key2", StrMap: map[string]string{"key": "value"}}, {Str: "key3"}}}, jsonpatch.IgnoreSliceOrderWithPattern([]jsonpatch.IgnorePattern{{"/structsWithKey", "str"}})) 299 | testPatchWithExpected(D{StructSliceWithKey: []C{{Str: "key2", StrMap: map[string]string{"key": "value"}}, {Str: "key3"}}}, D{StructSliceWithKey: []C{{Str: "key2", StrMap: map[string]string{"key": "value"}}, {Str: "key3"}}}, D{StructSliceWithKey: []C{{Str: "key2", StrMap: map[string]string{"key": "value"}}, {Str: "key3"}}}, jsonpatch.IgnoreSliceOrderWithPattern([]jsonpatch.IgnorePattern{{"/structsWithKey", "str"}})) 300 | }) 301 | }) 302 | Context("CreateJsonPatch_escape_pointer", func() { 303 | It("separator", func() { 304 | // add 305 | testPatch(F{"value2", 2, false}, F{}) 306 | // replace 307 | testPatch(F{"value1", 1, true}, F{"value2", 2, false}) 308 | // no change 309 | testPatch(F{"value1", 1, true}, F{"value1", 1, true}) 310 | }) 311 | }) 312 | Context("CreateJsonPatch_interface", func() { 313 | It("int", func() { 314 | // replace 315 | testPatch(I{2}, I{3}) 316 | // no change 317 | testPatch(I{2}, I{2}) 318 | }) 319 | It("string", func() { 320 | // replace 321 | testPatch(I{"value1"}, I{"value2"}) 322 | // no change 323 | testPatch(I{"value1"}, I{"value1"}) 324 | }) 325 | }) 326 | Context("CreateJsonPatch_ignore", func() { 327 | It("unexported", func() { 328 | // add 329 | testPatch(E{unexported: 1, Exported: 2}, E{unexported: 2}) 330 | // replace 331 | testPatch(E{unexported: 2, Exported: 2}, E{unexported: 1, Exported: 1}) 332 | // remove 333 | testPatch(E{unexported: 1}, E{unexported: 1, Exported: 2}) 334 | testPatch(E{unexported: 1}, E{Exported: 2}) 335 | // no change 336 | testPatch(E{unexported: 2}, E{}) 337 | testPatch(E{unexported: 1, Exported: 2}, E{unexported: 2, Exported: 2}) 338 | }) 339 | It("ignored", func() { 340 | // no change 341 | testPatchWithExpected(H{Ignored: "new", NotIgnored: "new"}, H{Ignored: "old", NotIgnored: "old"}, H{Ignored: "old", NotIgnored: "new"}) 342 | }) 343 | }) 344 | Context("CreateJsonPatch_with_predicates", func() { 345 | var predicate jsonpatch.Predicate 346 | BeforeEach(func() { 347 | predicate = jsonpatch.Funcs{ 348 | AddFunc: func(_ jsonpatch.JSONPointer, modified interface{}) bool { 349 | if b, ok := modified.(B); ok { 350 | return b.Bool || b.Int > 2 351 | } 352 | 353 | return true 354 | }, 355 | ReplaceFunc: func(_ jsonpatch.JSONPointer, modified, current interface{}) bool { 356 | if modifiedC, ok := modified.(C); ok { 357 | if currentC, ok := current.(C); ok { 358 | return len(modifiedC.StrMap) > len(currentC.StrMap) 359 | } 360 | } 361 | 362 | return true 363 | }, 364 | RemoveFunc: func(_ jsonpatch.JSONPointer, current interface{}) bool { 365 | if b, ok := current.(B); ok { 366 | return b.Str != "don't remove me" 367 | } 368 | 369 | return true 370 | }, 371 | } 372 | }) 373 | It("predicate_add", func() { 374 | // add 375 | testPatchWithExpected(G{B: &B{Bool: true, Str: "str"}}, G{}, G{B: &B{Bool: true, Str: "str"}}, jsonpatch.WithPredicate(predicate)) 376 | testPatchWithExpected(G{B: &B{Int: 7, Str: "str"}}, G{}, G{B: &B{Int: 7, Str: "str"}}, jsonpatch.WithPredicate(predicate)) 377 | // don't add 378 | testPatchWithExpected(G{B: &B{Bool: false, Str: "str"}, C: C{StrMap: map[string]string{"key": "value"}}}, G{}, G{C: C{StrMap: map[string]string{"key": "value"}}}, jsonpatch.WithPredicate(predicate)) 379 | testPatchWithExpected(G{B: &B{Int: 0, Str: "str"}, C: C{StrMap: map[string]string{"key": "value"}}}, G{}, G{C: C{StrMap: map[string]string{"key": "value"}}}, jsonpatch.WithPredicate(predicate)) 380 | }) 381 | It("predicate_replace", func() { 382 | // replace 383 | testPatchWithExpected(G{C: C{Str: "new", StrMap: map[string]string{"key": "value"}}}, G{C: C{Str: "old"}}, G{C: C{Str: "new", StrMap: map[string]string{"key": "value"}}}, jsonpatch.WithPredicate(predicate)) 384 | // don't replace 385 | testPatchWithExpected(G{C: C{Str: "new"}}, G{C: C{Str: "old", StrMap: map[string]string{"key": "value"}}}, G{C: C{Str: "old", StrMap: map[string]string{"key": "value"}}}, jsonpatch.WithPredicate(predicate)) 386 | }) 387 | It("predicate_remove", func() { 388 | // remove 389 | testPatchWithExpected(G{}, G{B: &B{Str: "remove me"}}, G{B: nil}, jsonpatch.WithPredicate(predicate)) 390 | // don't remove 391 | testPatchWithExpected(G{}, G{B: &B{Str: "don't remove me"}}, G{B: &B{Str: "don't remove me"}}, jsonpatch.WithPredicate(predicate)) 392 | }) 393 | }) 394 | Context("CreateJsonPatch_with_prefix", func() { 395 | It("empty prefix", func() { 396 | testPatchWithExpected(G{B: &B{Bool: true, Str: "str"}}, G{}, G{B: &B{Bool: true, Str: "str"}}, jsonpatch.WithPrefix([]string{""})) 397 | }) 398 | It("pointer prefix", func() { 399 | prefix := "/a/ptr" 400 | modified := G{A: &A{B: &B{Bool: true, Str: "str"}}} 401 | current := G{A: &A{}} 402 | expected := G{A: &A{B: &B{Bool: true, Str: "str"}}} 403 | 404 | currentJSON, err := json.Marshal(current) 405 | Ω(err).ShouldNot(HaveOccurred()) 406 | _, err = json.Marshal(modified) 407 | Ω(err).ShouldNot(HaveOccurred()) 408 | expectedJSON, err := json.Marshal(expected) 409 | Ω(err).ShouldNot(HaveOccurred()) 410 | 411 | list, err := jsonpatch.CreateJSONPatch(modified.A.B, current.A.B, jsonpatch.WithPrefix(jsonpatch.ParseJSONPointer(prefix))) 412 | Ω(err).ShouldNot(HaveOccurred()) 413 | Ω(list.String()).ShouldNot(Equal("")) 414 | Ω(list.List()).Should(ContainElement(WithTransform(func(p jsonpatch.JSONPatch) string { return p.Path }, HavePrefix(prefix)))) 415 | jsonPatch, err := jsonpatch2.DecodePatch(list.Raw()) 416 | Ω(err).ShouldNot(HaveOccurred()) 417 | patchedJSON, err := jsonPatch.Apply(currentJSON) 418 | Ω(err).ShouldNot(HaveOccurred()) 419 | Ω(patchedJSON).Should(MatchJSON(expectedJSON)) 420 | }) 421 | It("string prefix", func() { 422 | prefix := []string{"b"} 423 | modified := G{B: &B{Bool: true, Str: "str"}} 424 | current := G{} 425 | expected := G{B: &B{Bool: true, Str: "str"}} 426 | 427 | currentJSON, err := json.Marshal(current) 428 | Ω(err).ShouldNot(HaveOccurred()) 429 | _, err = json.Marshal(modified) 430 | Ω(err).ShouldNot(HaveOccurred()) 431 | expectedJSON, err := json.Marshal(expected) 432 | Ω(err).ShouldNot(HaveOccurred()) 433 | 434 | list, err := jsonpatch.CreateJSONPatch(modified.B, current.B, jsonpatch.WithPrefix(prefix)) 435 | Ω(err).ShouldNot(HaveOccurred()) 436 | Ω(list.String()).ShouldNot(Equal("")) 437 | Ω(list.List()).Should(ContainElement(WithTransform(func(p jsonpatch.JSONPatch) string { return p.Path }, HavePrefix("/"+strings.Join(prefix, "/"))))) 438 | jsonPatch, err := jsonpatch2.DecodePatch(list.Raw()) 439 | Ω(err).ShouldNot(HaveOccurred()) 440 | patchedJSON, err := jsonPatch.Apply(currentJSON) 441 | Ω(err).ShouldNot(HaveOccurred()) 442 | Ω(patchedJSON).Should(MatchJSON(expectedJSON)) 443 | }) 444 | }) 445 | Context("CreateJsonPatch_errors", func() { 446 | It("not matching types", func() { 447 | _, err := jsonpatch.CreateJSONPatch(A{}, B{}) 448 | Ω(err).Should(HaveOccurred()) 449 | }) 450 | It("not matching interface types", func() { 451 | _, err := jsonpatch.CreateJSONPatch(I{1}, I{"str"}) 452 | Ω(err).Should(HaveOccurred()) 453 | }) 454 | It("invalid map (map[int]string)", func() { 455 | _, err := jsonpatch.CreateJSONPatch(I{map[int]string{1: "value"}}, I{map[int]string{2: "value"}}) 456 | Ω(err).Should(HaveOccurred()) 457 | }) 458 | It("ignore slice order failed (duplicated key)", func() { 459 | _, err := jsonpatch.CreateJSONPatch([]int{1, 1, 1, 1}, []int{1, 2, 3}, jsonpatch.IgnoreSliceOrder()) 460 | Ω(err).Should(HaveOccurred()) 461 | _, err = jsonpatch.CreateJSONPatch([]string{"1", "2", "3"}, []string{"1", "1"}, jsonpatch.IgnoreSliceOrder()) 462 | Ω(err).Should(HaveOccurred()) 463 | }) 464 | }) 465 | Context("CreateJsonPatch_fuzzy", func() { 466 | var ( 467 | current G 468 | modified G 469 | ) 470 | BeforeEach(func() { 471 | current = G{} 472 | err := faker.FakeData(¤t) 473 | Ω(err).ShouldNot(HaveOccurred()) 474 | 475 | modified = G{} 476 | err = faker.FakeData(&modified) 477 | Ω(err).ShouldNot(HaveOccurred()) 478 | }) 479 | 480 | for i := 0; i < 100; i++ { 481 | It("fuzzy "+strconv.Itoa(i), func() { 482 | testPatch(modified, current) 483 | }) 484 | } 485 | 486 | Measure("fuzzy benchmark", func(b Benchmarker) { 487 | currentJSON, err := json.Marshal(current) 488 | Ω(err).ShouldNot(HaveOccurred()) 489 | modifiedJSON, err := json.Marshal(modified) 490 | Ω(err).ShouldNot(HaveOccurred()) 491 | 492 | var list jsonpatch.JSONPatchList 493 | _ = b.Time("runtime", func() { 494 | list, err = jsonpatch.CreateJSONPatch(modified, current) 495 | }) 496 | Ω(err).ShouldNot(HaveOccurred()) 497 | if list.Empty() { 498 | Ω(currentJSON).Should(MatchJSON(modifiedJSON)) 499 | Ω(list.Len()).Should(Equal(0)) 500 | 501 | return 502 | } 503 | 504 | jsonPatch, err := jsonpatch2.DecodePatch(list.Raw()) 505 | Ω(err).ShouldNot(HaveOccurred()) 506 | patchedJSON, err := jsonPatch.Apply(currentJSON) 507 | Ω(err).ShouldNot(HaveOccurred()) 508 | Ω(patchedJSON).Should(MatchJSON(modifiedJSON)) 509 | }, 100) 510 | }) 511 | Context("CreateThreeWayJSONPatch_simple", func() { 512 | It("should replace and add", func() { 513 | // add 514 | testThreeWayPatchWithExpected(D{IntSlice: []int{1, 2, 3, 4}}, D{IntSlice: []int{1, 2, 3}}, D{IntSlice: []int{1, 2, 3}}, D{IntSlice: []int{1, 2, 3, 4}}) 515 | // replace 516 | testThreeWayPatchWithExpected(B{Str: "new"}, B{Str: "old"}, B{Str: "old"}, B{Str: "new"}) 517 | }) 518 | It("should remove only if exists in original", func() { 519 | // not remove 520 | testThreeWayPatchWithExpected(D{IntSlice: []int{1, 2, 3}}, D{IntSlice: []int{1, 2, 3}, StringSlice: []string{"str1"}}, D{IntSlice: []int{1, 2, 3}}, D{IntSlice: []int{1, 2, 3}, StringSlice: []string{"str1"}}) 521 | // remove 522 | testThreeWayPatchWithExpected(D{IntSlice: []int{1, 2, 3}, StringSlice: []string{}}, D{IntSlice: []int{1, 2, 3}, StringSlice: []string{"str1"}}, D{IntSlice: []int{1, 2, 3}, StringSlice: []string{"str1"}}, D{IntSlice: []int{1, 2, 3}, StringSlice: []string{}}) 523 | }) 524 | }) 525 | Context("CreateThreeWayJSONPatch_fuzzy", func() { 526 | var ( 527 | current G 528 | modified G 529 | ) 530 | BeforeEach(func() { 531 | current = G{} 532 | err := faker.FakeData(¤t) 533 | Ω(err).ShouldNot(HaveOccurred()) 534 | 535 | modified = G{} 536 | err = faker.FakeData(&modified) 537 | Ω(err).ShouldNot(HaveOccurred()) 538 | }) 539 | 540 | for i := 0; i < 100; i++ { 541 | It("fuzzy "+strconv.Itoa(i), func() { 542 | testThreeWayPatch(modified, current) 543 | }) 544 | } 545 | }) 546 | }) 547 | 548 | func testPatch(modified, current interface{}) { 549 | currentJSON, err := json.Marshal(current) 550 | Ω(err).ShouldNot(HaveOccurred()) 551 | modifiedJSON, err := json.Marshal(modified) 552 | Ω(err).ShouldNot(HaveOccurred()) 553 | 554 | list, err := jsonpatch.CreateJSONPatch(modified, current) 555 | Ω(err).ShouldNot(HaveOccurred()) 556 | if list.Empty() { 557 | Ω(currentJSON).Should(MatchJSON(modifiedJSON)) 558 | Ω(list.Len()).Should(Equal(0)) 559 | Ω(list.String()).Should(Equal("")) 560 | 561 | return 562 | } 563 | 564 | Ω(list.String()).ShouldNot(Equal("")) 565 | jsonPatch, err := jsonpatch2.DecodePatch(list.Raw()) 566 | Ω(err).ShouldNot(HaveOccurred()) 567 | patchedJSON, err := jsonPatch.Apply(currentJSON) 568 | Ω(err).ShouldNot(HaveOccurred()) 569 | Ω(patchedJSON).Should(MatchJSON(modifiedJSON)) 570 | } 571 | 572 | func testThreeWayPatch(modified, current interface{}) { 573 | currentJSON, err := json.Marshal(current) 574 | Ω(err).ShouldNot(HaveOccurred()) 575 | modifiedJSON, err := json.Marshal(modified) 576 | Ω(err).ShouldNot(HaveOccurred()) 577 | 578 | list, err := jsonpatch.CreateThreeWayJSONPatch(modified, current, current) 579 | Ω(err).ShouldNot(HaveOccurred()) 580 | if list.Empty() { 581 | Ω(currentJSON).Should(MatchJSON(modifiedJSON)) 582 | Ω(list.Len()).Should(Equal(0)) 583 | Ω(list.String()).Should(Equal("")) 584 | 585 | return 586 | } 587 | 588 | Ω(list.String()).ShouldNot(Equal("")) 589 | jsonPatch, err := jsonpatch2.DecodePatch(list.Raw()) 590 | Ω(err).ShouldNot(HaveOccurred()) 591 | patchedJSON, err := jsonPatch.Apply(currentJSON) 592 | Ω(err).ShouldNot(HaveOccurred()) 593 | Ω(patchedJSON).Should(MatchJSON(modifiedJSON)) 594 | } 595 | 596 | func testPatchWithExpected(modified, current, expected interface{}, options ...jsonpatch.Option) { 597 | currentJSON, err := json.Marshal(current) 598 | Ω(err).ShouldNot(HaveOccurred()) 599 | _, err = json.Marshal(modified) 600 | Ω(err).ShouldNot(HaveOccurred()) 601 | expectedJSON, err := json.Marshal(expected) 602 | Ω(err).ShouldNot(HaveOccurred()) 603 | 604 | list, err := jsonpatch.CreateJSONPatch(modified, current, options...) 605 | Ω(err).ShouldNot(HaveOccurred()) 606 | if list.Empty() { 607 | Ω(currentJSON).Should(MatchJSON(expectedJSON)) 608 | Ω(list.Len()).Should(Equal(0)) 609 | Ω(list.String()).Should(Equal("")) 610 | 611 | return 612 | } 613 | 614 | Ω(list.String()).ShouldNot(Equal("")) 615 | jsonPatch, err := jsonpatch2.DecodePatch(list.Raw()) 616 | Ω(err).ShouldNot(HaveOccurred()) 617 | patchedJSON, err := jsonPatch.Apply(currentJSON) 618 | Ω(err).ShouldNot(HaveOccurred()) 619 | Ω(patchedJSON).Should(MatchJSON(expectedJSON)) 620 | } 621 | 622 | func testThreeWayPatchWithExpected(modified, current, original, expected interface{}) { 623 | currentJSON, err := json.Marshal(current) 624 | Ω(err).ShouldNot(HaveOccurred()) 625 | _, err = json.Marshal(modified) 626 | Ω(err).ShouldNot(HaveOccurred()) 627 | expectedJSON, err := json.Marshal(expected) 628 | Ω(err).ShouldNot(HaveOccurred()) 629 | 630 | list, err := jsonpatch.CreateThreeWayJSONPatch(modified, current, original) 631 | Ω(err).ShouldNot(HaveOccurred()) 632 | if list.Empty() { 633 | Ω(currentJSON).Should(MatchJSON(expectedJSON)) 634 | Ω(list.Len()).Should(Equal(0)) 635 | Ω(list.String()).Should(Equal("")) 636 | 637 | return 638 | } 639 | 640 | Ω(list.String()).ShouldNot(Equal("")) 641 | jsonPatch, err := jsonpatch2.DecodePatch(list.Raw()) 642 | Ω(err).ShouldNot(HaveOccurred()) 643 | patchedJSON, err := jsonPatch.Apply(currentJSON) 644 | Ω(err).ShouldNot(HaveOccurred()) 645 | Ω(patchedJSON).Should(MatchJSON(expectedJSON)) 646 | } 647 | -------------------------------------------------------------------------------- /pointer.go: -------------------------------------------------------------------------------- 1 | package jsonpatch 2 | 3 | import ( 4 | "strings" 5 | ) 6 | 7 | const ( 8 | separator = "/" 9 | wildcard = "*" 10 | tilde = "~" 11 | ) 12 | 13 | // JSONPointer identifies a specific value within a JSON object specified in RFC 6901 14 | type JSONPointer []string 15 | 16 | // ParseJSONPointer converts a string into a JSONPointer 17 | func ParseJSONPointer(str string) JSONPointer { 18 | return strings.Split(str, separator) 19 | } 20 | 21 | // String returns a string representation of a JSONPointer 22 | func (p JSONPointer) String() string { 23 | return strings.Join(p, separator) 24 | } 25 | 26 | // Add adds an element to the JSONPointer 27 | func (p JSONPointer) Add(elem string) JSONPointer { 28 | elem = strings.ReplaceAll(elem, tilde, "~0") 29 | elem = strings.ReplaceAll(elem, separator, "~1") 30 | return append(p, elem) 31 | } 32 | 33 | // Match matches a pattern which is a string JSONPointer which might also contains wildcards 34 | func (p JSONPointer) Match(pattern string) bool { 35 | elements := strings.Split(pattern, separator) 36 | for i, element := range elements { 37 | if element == wildcard { 38 | continue 39 | } else if i >= len(p) || element != p[i] { 40 | return false 41 | } 42 | } 43 | 44 | return strings.HasSuffix(pattern, wildcard) || len(p) == len(elements) 45 | } 46 | -------------------------------------------------------------------------------- /pointer_test.go: -------------------------------------------------------------------------------- 1 | package jsonpatch_test 2 | 3 | import ( 4 | . "github.com/onsi/ginkgo/v2" 5 | . "github.com/onsi/gomega" 6 | 7 | "github.com/snorwin/jsonpatch" 8 | ) 9 | 10 | var _ = Describe("JSONPointer", func() { 11 | Context("ParseJSONPointer_String", func() { 12 | It("should parse and unparse", func() { 13 | Ω(jsonpatch.ParseJSONPointer("/a/b/c").String()).Should(Equal("/a/b/c")) 14 | Ω(jsonpatch.ParseJSONPointer("/a/b/c/").String()).Should(Equal("/a/b/c/")) 15 | Ω(jsonpatch.ParseJSONPointer("a/b/c").String()).Should(Equal("a/b/c")) 16 | }) 17 | }) 18 | Context("Add", func() { 19 | It("should add element", func() { 20 | Ω(jsonpatch.ParseJSONPointer("/a/b/c").Add("d").String()).Should(Equal("/a/b/c/d")) 21 | Ω(jsonpatch.ParseJSONPointer("/a/b/c").Add("d").Add("e").String()).Should(Equal("/a/b/c/d/e")) 22 | }) 23 | It("should not modify original Pointer", func() { 24 | original := jsonpatch.ParseJSONPointer("/1/2/3") 25 | original.Add("4") 26 | original.Add("5") 27 | Ω(original.String()).Should(Equal("/1/2/3")) 28 | }) 29 | }) 30 | Context("Match", func() { 31 | It("should match", func() { 32 | Ω(jsonpatch.ParseJSONPointer("/a/b/c").Match("*")).Should(BeTrue()) 33 | Ω(jsonpatch.ParseJSONPointer("/a/b/c").Match("/a/b/c")).Should(BeTrue()) 34 | Ω(jsonpatch.ParseJSONPointer("/a/b/c").Match("/a/*/c")).Should(BeTrue()) 35 | Ω(jsonpatch.ParseJSONPointer("/a/b/c").Match("/a/*")).Should(BeTrue()) 36 | Ω(jsonpatch.ParseJSONPointer("/a/b/c").Match("/*/*")).Should(BeTrue()) 37 | }) 38 | It("should not match", func() { 39 | Ω(jsonpatch.ParseJSONPointer("/a/b/c").Match("*/d")).Should(BeFalse()) 40 | Ω(jsonpatch.ParseJSONPointer("/a/b/c").Match("/a/c/b")).Should(BeFalse()) 41 | Ω(jsonpatch.ParseJSONPointer("/a/b/c").Match("/a")).Should(BeFalse()) 42 | Ω(jsonpatch.ParseJSONPointer("/a/b/c").Match("/a/b")).Should(BeFalse()) 43 | Ω(jsonpatch.ParseJSONPointer("/a/b/c").Match("/a/b/c/d/e")).Should(BeFalse()) 44 | Ω(jsonpatch.ParseJSONPointer("a/b/c").Match("/a/b/c")).Should(BeFalse()) 45 | Ω(jsonpatch.ParseJSONPointer("/a/b/c").Match("a/b/c")).Should(BeFalse()) 46 | }) 47 | }) 48 | }) 49 | -------------------------------------------------------------------------------- /predicate.go: -------------------------------------------------------------------------------- 1 | package jsonpatch 2 | 3 | // Predicate filters patches 4 | type Predicate interface { 5 | // Add returns true if the object should not be added in the patch 6 | Add(pointer JSONPointer, modified interface{}) bool 7 | 8 | // Remove returns true if the object should not be deleted in the patch 9 | Remove(pointer JSONPointer, current interface{}) bool 10 | 11 | // Replace returns true if the objects should not be updated in the patch - this will stop the recursive processing of those objects 12 | Replace(pointer JSONPointer, modified, current interface{}) bool 13 | } 14 | 15 | // Funcs is a function that implements Predicate 16 | type Funcs struct { 17 | // Add returns true if the object should not be added in the patch 18 | AddFunc func(pointer JSONPointer, modified interface{}) bool 19 | 20 | // Remove returns true if the object should not be deleted in the patch 21 | RemoveFunc func(pointer JSONPointer, current interface{}) bool 22 | 23 | // Replace returns true if the objects should not be updated in the patch - this will stop the recursive processing of those objects 24 | ReplaceFunc func(pointer JSONPointer, modified, current interface{}) bool 25 | } 26 | 27 | // Add implements Predicate 28 | func (p Funcs) Add(pointer JSONPointer, modified interface{}) bool { 29 | if p.AddFunc != nil { 30 | return p.AddFunc(pointer, modified) 31 | } 32 | 33 | return true 34 | } 35 | 36 | // Remove implements Predicate 37 | func (p Funcs) Remove(pointer JSONPointer, current interface{}) bool { 38 | if p.RemoveFunc != nil { 39 | return p.RemoveFunc(pointer, current) 40 | } 41 | 42 | return true 43 | } 44 | 45 | // Replace implements Predicate 46 | func (p Funcs) Replace(pointer JSONPointer, modified, current interface{}) bool { 47 | if p.ReplaceFunc != nil { 48 | return p.ReplaceFunc(pointer, modified, current) 49 | } 50 | 51 | return true 52 | } 53 | -------------------------------------------------------------------------------- /walker.go: -------------------------------------------------------------------------------- 1 | package jsonpatch 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "sort" 7 | "strconv" 8 | "strings" 9 | "time" 10 | ) 11 | 12 | const ( 13 | jsonTag = "json" 14 | ) 15 | 16 | type walker struct { 17 | predicate Predicate 18 | handler Handler 19 | prefix []string 20 | patchList []JSONPatch 21 | ignoredSlices []IgnorePattern 22 | } 23 | 24 | // walk recursively processes the modified and current JSON data structures simultaneously and in every step it compares 25 | // the value of them with each other 26 | func (w *walker) walk(modified, current reflect.Value, pointer JSONPointer) error { 27 | // the data structures of both JSON objects must be identical 28 | if modified.Kind() != current.Kind() { 29 | return fmt.Errorf("kind does not match at: %s modified: %s current: %s", pointer, modified.Kind(), current.Kind()) 30 | } 31 | switch modified.Kind() { 32 | case reflect.Struct: 33 | return w.processStruct(modified, current, pointer) 34 | case reflect.Ptr: 35 | return w.processPtr(modified, current, pointer) 36 | case reflect.Slice: 37 | return w.processSlice(modified, current, pointer) 38 | case reflect.Map: 39 | return w.processMap(modified, current, pointer) 40 | case reflect.Interface: 41 | return w.processInterface(modified, current, pointer) 42 | case reflect.String: 43 | return w.processString(modified, current, pointer) 44 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 45 | if modified.Int() != current.Int() { 46 | w.replace(pointer, modified.Int(), current.Int()) 47 | } 48 | case reflect.Uint, reflect.Uintptr, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: 49 | if modified.Uint() != current.Uint() { 50 | w.replace(pointer, modified.Uint(), current.Uint()) 51 | } 52 | case reflect.Float32: 53 | if modified.Float() != current.Float() { 54 | w.replace(pointer, float32(modified.Float()), float32(current.Float())) 55 | } 56 | case reflect.Float64: 57 | if modified.Float() != current.Float() { 58 | w.replace(pointer, modified.Float(), current.Float()) 59 | } 60 | case reflect.Bool: 61 | if modified.Bool() != current.Bool() { 62 | w.replace(pointer, modified.Bool(), current.Bool()) 63 | } 64 | case reflect.Invalid: 65 | // undefined interfaces are ignored for now 66 | return nil 67 | default: 68 | return fmt.Errorf("unsupported kind: %s at: %s", modified.Kind(), pointer) 69 | } 70 | 71 | return nil 72 | } 73 | 74 | // processInterface processes reflect.Interface values 75 | func (w *walker) processInterface(modified reflect.Value, current reflect.Value, pointer JSONPointer) error { 76 | // extract the value form the interface and try to process it further 77 | if err := w.walk(reflect.ValueOf(modified.Interface()), reflect.ValueOf(current.Interface()), pointer); err != nil { 78 | return err 79 | } 80 | 81 | return nil 82 | } 83 | 84 | // processString processes reflect.String values 85 | func (w *walker) processString(modified reflect.Value, current reflect.Value, pointer JSONPointer) error { 86 | if modified.String() != current.String() { 87 | if modified.String() == "" { 88 | w.remove(pointer, current.String()) 89 | } else if current.String() == "" { 90 | w.add(pointer, modified.String()) 91 | } else { 92 | w.replace(pointer, modified.String(), current.String()) 93 | } 94 | } 95 | 96 | return nil 97 | } 98 | 99 | // processMap processes reflect.Map values 100 | func (w *walker) processMap(modified reflect.Value, current reflect.Value, pointer JSONPointer) error { 101 | // NOTE: currently only map[string]interface{} are supported 102 | if len(modified.MapKeys()) > 0 && len(current.MapKeys()) == 0 { 103 | w.add(pointer, modified.Interface()) 104 | } else { 105 | it := modified.MapRange() 106 | for it.Next() { 107 | key := it.Key() 108 | if key.Kind() != reflect.String { 109 | return fmt.Errorf("only strings are supported as map keys but was: %s at: %s", key.Kind(), pointer) 110 | } 111 | 112 | val1 := it.Value() 113 | val2 := current.MapIndex(key) 114 | if val2.Kind() == reflect.Invalid { 115 | w.add(pointer.Add(key.String()), val1.Interface()) 116 | } else { 117 | if err := w.walk(val1, val2, pointer.Add(key.String())); err != nil { 118 | return err 119 | } 120 | } 121 | } 122 | it = current.MapRange() 123 | for it.Next() { 124 | key := it.Key() 125 | if key.Kind() != reflect.String { 126 | return fmt.Errorf("only strings are supported as map keys but was: %s at: %s", key.Kind(), pointer) 127 | } 128 | 129 | val1 := modified.MapIndex(key) 130 | val2 := it.Value() 131 | if val1.Kind() == reflect.Invalid { 132 | w.remove(pointer.Add(key.String()), val2.Interface()) 133 | } 134 | } 135 | } 136 | 137 | return nil 138 | } 139 | 140 | // processSlice processes reflect.Slice values 141 | func (w *walker) processSlice(modified reflect.Value, current reflect.Value, pointer JSONPointer) error { 142 | if !w.predicate.Replace(pointer, modified.Interface(), current.Interface()) { 143 | return nil 144 | } 145 | 146 | if modified.Len() > 0 && current.Len() == 0 { 147 | w.add(pointer, modified.Interface()) 148 | } else { 149 | var ignoreSliceOrder bool 150 | var patchSliceJSONField string 151 | 152 | // look for a slice tag which pattern matches the pointer 153 | for _, ignore := range w.ignoredSlices { 154 | if pointer.Match(ignore.Pattern) { 155 | ignoreSliceOrder = true 156 | patchSliceJSONField = ignore.JSONField 157 | break 158 | } 159 | } 160 | 161 | if ignoreSliceOrder { 162 | fieldName := jsonFieldNameToFieldName(modified.Type().Elem(), patchSliceJSONField) 163 | 164 | // maps the modified slice elements with the patchSliceKey to their index 165 | idxMap1 := map[string]int{} 166 | for j := 0; j < modified.Len(); j++ { 167 | fieldValue := extractIgnoreSliceOrderMatchValue(modified.Index(j), fieldName) 168 | if _, ok := idxMap1[fieldValue]; ok { 169 | return fmt.Errorf("ignore slice order failed at %s due to unique match field constraint, duplicated value: %s", pointer, fieldValue) 170 | } 171 | idxMap1[fieldValue] = j 172 | } 173 | 174 | // maps the current slice elements with the patchSliceKey to their index 175 | idxMap2 := map[string]int{} 176 | for j := 0; j < current.Len(); j++ { 177 | fieldValue := extractIgnoreSliceOrderMatchValue(current.Index(j), fieldName) 178 | if _, ok := idxMap2[fieldValue]; ok { 179 | return fmt.Errorf("ignore slice order failed at %s due to unique match field constraint, duplicated value: %s", pointer, fieldValue) 180 | } 181 | idxMap2[fieldValue] = j 182 | } 183 | 184 | // IMPORTANT: the order of the patches matters, because and add or delete will change the index of your 185 | // elements. Therefore, elements are only added at the end of the slice and all elements are updated 186 | // before any element is deleted. 187 | 188 | // iterate through the list of modified slice elements in order to identify updated or added elements 189 | idxMax := current.Len() 190 | for k, idx1 := range idxMap1 { 191 | idx2, ok := idxMap2[k] 192 | if ok { 193 | if err := w.walk(modified.Index(idx1), current.Index(idx2), pointer.Add(strconv.Itoa(idx2))); err != nil { 194 | return err 195 | } 196 | } else { 197 | if ok := w.add(pointer.Add(strconv.Itoa(idxMax)), modified.Index(idx1).Interface()); ok { 198 | idxMax++ 199 | } 200 | } 201 | } 202 | 203 | // IMPORTANT: deleting must be done in reverse order 204 | 205 | // iterate through the list of current slice elements in order to identify deleted elements 206 | var deleted []int 207 | for k, idx2 := range idxMap2 { 208 | if _, ok := idxMap1[k]; !ok { 209 | deleted = append(deleted, idx2) 210 | } 211 | } 212 | sort.Ints(deleted) 213 | for j := len(deleted) - 1; j >= 0; j-- { 214 | idx2 := deleted[j] 215 | w.remove(pointer.Add(strconv.Itoa(idx2)), current.Index(idx2).Interface()) 216 | } 217 | } else { 218 | // iterate through both slices and update their elements until on of them is completely processed 219 | for j := 0; j < modified.Len() && j < current.Len(); j++ { 220 | if err := w.walk(modified.Index(j), current.Index(j), pointer.Add(strconv.Itoa(j))); err != nil { 221 | return err 222 | } 223 | } 224 | if modified.Len() > current.Len() { 225 | // add the remaining elements of the modified slice 226 | idx := current.Len() 227 | for j := current.Len(); j < modified.Len(); j++ { 228 | if ok := w.add(pointer.Add(strconv.Itoa(idx)), modified.Index(j).Interface()); ok { 229 | idx++ 230 | } 231 | } 232 | } else if modified.Len() < current.Len() { 233 | // delete the remaining elements of the current slice 234 | // IMPORTANT: deleting must be done in reverse order 235 | for j := current.Len() - 1; j >= modified.Len(); j-- { 236 | w.remove(pointer.Add(strconv.Itoa(j)), current.Index(j).Interface()) 237 | } 238 | } 239 | } 240 | } 241 | 242 | return nil 243 | } 244 | 245 | // processPtr processes reflect.Ptr values 246 | func (w *walker) processPtr(modified reflect.Value, current reflect.Value, pointer JSONPointer) error { 247 | if !modified.IsNil() && !current.IsNil() { 248 | // the values of the pointers will be processed in a next step 249 | if err := w.walk(modified.Elem(), current.Elem(), pointer); err != nil { 250 | return err 251 | } 252 | } else if !modified.IsNil() { 253 | w.add(pointer, modified.Elem().Interface()) 254 | } else if !current.IsNil() { 255 | w.remove(pointer, current.Elem().Interface()) 256 | } 257 | 258 | return nil 259 | } 260 | 261 | // processStruct processes reflect.Struct values 262 | func (w *walker) processStruct(modified, current reflect.Value, pointer JSONPointer) error { 263 | if !w.predicate.Replace(pointer, modified.Interface(), current.Interface()) { 264 | return nil 265 | } 266 | 267 | if modified.Type().PkgPath() == "time" && modified.Type().Name() == "Time" { 268 | m, err := toTimeStrValue(modified) 269 | if err != nil { 270 | return err 271 | } 272 | 273 | c, err := toTimeStrValue(current) 274 | if err != nil { 275 | return err 276 | } 277 | return w.processString(m, c, pointer) 278 | } 279 | 280 | // process all struct fields, the order of the fields of the modified and current JSON object is identical because their types match 281 | for j := 0; j < modified.NumField(); j++ { 282 | tag := strings.Split(modified.Type().Field(j).Tag.Get(jsonTag), ",")[0] 283 | if tag == "" || tag == "_" || !modified.Field(j).CanInterface() { 284 | // struct fields without a JSON tag set or unexported fields are ignored 285 | continue 286 | } 287 | // process the child's value of the modified and current JSON in a next step 288 | if err := w.walk(modified.Field(j), current.Field(j), pointer.Add(tag)); err != nil { 289 | return err 290 | } 291 | } 292 | 293 | return nil 294 | } 295 | 296 | func toTimeStrValue(v reflect.Value) (reflect.Value, error) { 297 | t, err := v.Interface().(time.Time).MarshalText() 298 | if err != nil { 299 | return reflect.Value{}, err 300 | } 301 | return reflect.ValueOf(string(t)), nil 302 | } 303 | 304 | // extractIgnoreSliceOrderMatchValue extracts the value which is used to match the modified and current values to ignore the slice order 305 | func extractIgnoreSliceOrderMatchValue(value reflect.Value, fieldName string) string { 306 | switch value.Kind() { 307 | case reflect.Struct: 308 | return extractIgnoreSliceOrderMatchValue(value.FieldByName(fieldName), "") 309 | case reflect.Ptr: 310 | if !value.IsNil() { 311 | return extractIgnoreSliceOrderMatchValue(value.Elem(), fieldName) 312 | } 313 | return "" 314 | case reflect.String: 315 | return value.String() 316 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 317 | return strconv.FormatInt(value.Int(), 10) 318 | case reflect.Uint, reflect.Uintptr, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: 319 | return strconv.FormatUint(value.Uint(), 10) 320 | case reflect.Float32: 321 | return strconv.FormatFloat(value.Float(), 'f', -1, 32) 322 | case reflect.Float64: 323 | return strconv.FormatFloat(value.Float(), 'f', -1, 64) 324 | case reflect.Bool: 325 | return strconv.FormatBool(value.Bool()) 326 | } 327 | return "" 328 | } 329 | 330 | // jsonFieldNameToFieldName retrieves the actual Go field name for the JSON field name 331 | func jsonFieldNameToFieldName(t reflect.Type, jsonFieldName string) string { 332 | switch t.Kind() { 333 | case reflect.Struct: 334 | for i := 0; i < t.NumField(); i++ { 335 | if strings.Split(t.Field(i).Tag.Get(jsonTag), ",")[0] == jsonFieldName { 336 | return t.Field(i).Name 337 | } 338 | } 339 | case reflect.Ptr: 340 | return jsonFieldNameToFieldName(t.Elem(), jsonFieldName) 341 | } 342 | 343 | return "" 344 | } 345 | 346 | // add adds an add JSON patch by checking the Predicate first and using the Handler to generate it 347 | func (w *walker) add(pointer JSONPointer, modified interface{}) bool { 348 | if w.predicate != nil && !w.predicate.Add(pointer, modified) { 349 | return false 350 | } 351 | w.patchList = append(w.patchList, w.handler.Add(pointer, modified)...) 352 | 353 | return true 354 | } 355 | 356 | // replace adds a replace JSON patch by checking the Predicate first and using the Handler to generate it 357 | func (w *walker) replace(pointer JSONPointer, modified, current interface{}) bool { 358 | if w.predicate != nil && !w.predicate.Replace(pointer, modified, current) { 359 | return false 360 | } 361 | w.patchList = append(w.patchList, w.handler.Replace(pointer, modified, current)...) 362 | 363 | return true 364 | } 365 | 366 | // remove adds a remove JSON patch by checking the Predicate first and using the Handler to generate it 367 | func (w *walker) remove(pointer JSONPointer, current interface{}) bool { 368 | if w.predicate != nil && !w.predicate.Remove(pointer, current) { 369 | return false 370 | } 371 | w.patchList = append(w.patchList, w.handler.Remove(pointer, current)...) 372 | 373 | return true 374 | } 375 | --------------------------------------------------------------------------------