diff --git a/cmd/config/edit.go b/cmd/config/edit.go index 61a72716b..747c2eacf 100644 --- a/cmd/config/edit.go +++ b/cmd/config/edit.go @@ -10,11 +10,11 @@ import ( "path/filepath" "sort" - jsonpatch "github.com/mattbaird/jsonpatch" "github.com/nhost/be/services/mimir/model" "github.com/nhost/cli/clienv" "github.com/pelletier/go-toml/v2" "github.com/urfave/cli/v2" + "github.com/wI2L/jsondiff" ) const ( @@ -81,7 +81,7 @@ func copyConfig(ce *clienv.CliEnv, dst, overlay string) error { return nil } -func toJSON(filepath string) ([]byte, error) { +func readFile(filepath string) (any, error) { f, err := os.Open(filepath) if err != nil { return nil, fmt.Errorf("failed to open file: %w", err) @@ -98,26 +98,21 @@ func toJSON(filepath string) ([]byte, error) { return nil, fmt.Errorf("failed to unmarshal toml: %w", err) } - b, err = json.Marshal(v) - if err != nil { - return nil, fmt.Errorf("failed to marshal json: %w", err) - } - - return b, nil + return v, nil } func generateJSONPatch(origfilepath, newfilepath, dst string) error { - origb, err := toJSON(origfilepath) + origo, err := readFile(origfilepath) if err != nil { return fmt.Errorf("failed to convert original toml to json: %w", err) } - newb, err := toJSON(newfilepath) + newo, err := readFile(newfilepath) if err != nil { return fmt.Errorf("failed to convert new toml to json: %w", err) } - patches, err := jsonpatch.CreatePatch(origb, newb) + patches, err := jsondiff.Compare(origo, newo) if err != nil { return fmt.Errorf("failed to generate json patch: %w", err) } diff --git a/examples/myproject/nhost/nhost.toml b/examples/myproject/nhost/nhost.toml index 377c23a53..98b7c3da3 100644 --- a/examples/myproject/nhost/nhost.toml +++ b/examples/myproject/nhost/nhost.toml @@ -18,6 +18,7 @@ key = '{{ secrets.HASURA_GRAPHQL_JWT_SECRET }}' [hasura.settings] enableRemoteSchemaPermissions = false +corsDomain = ['https://zero.app.io', 'https://one.app.io', 'https://two.app.io'] [functions] [functions.node] diff --git a/go.mod b/go.mod index c7b8460a8..ff925d3ed 100644 --- a/go.mod +++ b/go.mod @@ -9,10 +9,10 @@ require ( github.com/go-git/go-git/v5 v5.6.1 github.com/google/go-cmp v0.5.9 github.com/hashicorp/go-getter v1.7.1 - github.com/mattbaird/jsonpatch v0.0.0-20230413205102-771768614e91 github.com/nhost/be v0.0.0-20230716092554-a6815011be18 github.com/pelletier/go-toml/v2 v2.0.8 github.com/urfave/cli/v2 v2.25.5 + github.com/wI2L/jsondiff v0.4.0 golang.org/x/mod v0.10.0 golang.org/x/term v0.8.0 gopkg.in/evanphx/json-patch.v5 v5.6.0 @@ -37,7 +37,6 @@ require ( github.com/cockroachdb/apd/v2 v2.0.2 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect github.com/emirpasic/gods v1.18.1 // indirect - github.com/evanphx/json-patch v0.5.2 // indirect github.com/go-git/gcfg v1.5.0 // indirect github.com/go-git/go-billy/v5 v5.4.1 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect diff --git a/go.sum b/go.sum index 8cf5c52b2..2df800b2c 100644 --- a/go.sum +++ b/go.sum @@ -268,8 +268,6 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.m github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/evanphx/json-patch v0.5.2 h1:xVCHIVMUu1wtM/VkR9jVZ45N3FhZfYMMYGorLCR8P3k= -github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/gliderlabs/ssh v0.3.5 h1:OcaySEmAQJgyYcArR+gGGTHCyE7nvhEMTlYY+Dp8CpY= @@ -398,7 +396,6 @@ github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= -github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4= github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= @@ -425,8 +422,6 @@ github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69 github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/matryer/is v1.2.0 h1:92UTHpy8CDwaJ08GqLDzhhuixiBUUD1p3AU6PHddz4A= github.com/matryer/is v1.2.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlWXA= -github.com/mattbaird/jsonpatch v0.0.0-20230413205102-771768614e91 h1:JnZSkFP1/GLwKCEuuWVhsacvbDQIVa5BRwAwd+9k2Vw= -github.com/mattbaird/jsonpatch v0.0.0-20230413205102-771768614e91/go.mod h1:M1qoD/MqPgTZIk0EWKB38wE28ACRfVcn+cU08jyArI0= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= @@ -499,6 +494,8 @@ github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQ github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= github.com/vektah/gqlparser/v2 v2.5.3 h1:goUwv4+blhtwR3GwefadPVI4ubYc/WZSypljWMQa6IE= github.com/vektah/gqlparser/v2 v2.5.3/go.mod h1:z8xXUff237NntSuH8mLFijZ+1tjV1swDbpDqjJmk6ME= +github.com/wI2L/jsondiff v0.4.0 h1:iP56F9tK83eiLttg3YdmEENtZnwlYd3ezEpNNnfZVyM= +github.com/wI2L/jsondiff v0.4.0/go.mod h1:nR/vyy1efuDeAtMwc3AF6nZf/2LD1ID8GTyyJ+K8YB0= github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= diff --git a/vendor/github.com/mattbaird/jsonpatch/.gitignore b/vendor/github.com/mattbaird/jsonpatch/.gitignore deleted file mode 100644 index 540a8d20b..000000000 --- a/vendor/github.com/mattbaird/jsonpatch/.gitignore +++ /dev/null @@ -1,24 +0,0 @@ -# Compiled Object files, Static and Dynamic libs (Shared Objects) -*.o -*.a -*.so -.idea -# Folders -_obj -_test - -# Architecture specific extensions/prefixes -*.[568vq] -[568vq].out - -*.cgo1.go -*.cgo2.c -_cgo_defun.c -_cgo_gotypes.go -_cgo_export.* - -_testmain.go - -*.exe -*.test -*.prof diff --git a/vendor/github.com/mattbaird/jsonpatch/LICENSE b/vendor/github.com/mattbaird/jsonpatch/LICENSE deleted file mode 100644 index 8f71f43fe..000000000 --- a/vendor/github.com/mattbaird/jsonpatch/LICENSE +++ /dev/null @@ -1,202 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "{}" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright {yyyy} {name of copyright owner} - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - diff --git a/vendor/github.com/mattbaird/jsonpatch/README.md b/vendor/github.com/mattbaird/jsonpatch/README.md deleted file mode 100644 index 54f34a2bc..000000000 --- a/vendor/github.com/mattbaird/jsonpatch/README.md +++ /dev/null @@ -1,46 +0,0 @@ -# jsonpatch -As per http://jsonpatch.com/ JSON Patch is specified in RFC 6902 from the IETF. - -JSON Patch allows you to generate JSON that describes changes you want to make to a document, so you don't have to send the whole doc. JSON Patch format is supported by HTTP PATCH method, allowing for standards based partial updates via REST APIs. - -```bash -go get github.com/mattbaird/jsonpatch -``` - -I tried some of the other "jsonpatch" go implementations, but none of them could diff two json documents and -generate format like jsonpatch.com specifies. Here's an example of the patch format: - -```json -[ - { "op": "replace", "path": "/baz", "value": "boo" }, - { "op": "add", "path": "/hello", "value": ["world"] }, - { "op": "remove", "path": "/foo"} -] - -``` -The API is super simple -#example -```go -package main - -import ( - "fmt" - "github.com/mattbaird/jsonpatch" -) - -var simpleA = `{"a":100, "b":200, "c":"hello"}` -var simpleB = `{"a":100, "b":200, "c":"goodbye"}` - -func main() { - patch, e := jsonpatch.CreatePatch([]byte(simpleA), []byte(simpleB)) - if e != nil { - fmt.Printf("Error creating JSON patch:%v", e) - return - } - for _, operation := range patch { - fmt.Printf("%s\n", operation.Json()) - } -} -``` - -This code needs more tests, as it's a highly recursive, type-fiddly monster. It's not a lot of code, but it has to deal with a lot of complexity. diff --git a/vendor/github.com/mattbaird/jsonpatch/jsonpatch.go b/vendor/github.com/mattbaird/jsonpatch/jsonpatch.go deleted file mode 100644 index 5b46d9282..000000000 --- a/vendor/github.com/mattbaird/jsonpatch/jsonpatch.go +++ /dev/null @@ -1,273 +0,0 @@ -package jsonpatch - -import ( - "bytes" - "encoding/json" - "fmt" - "reflect" - "strings" -) - -var errBadJSONDoc = fmt.Errorf("Invalid JSON Document") - -type JsonPatchOperation struct { - Operation string `json:"op"` - Path string `json:"path"` - Value interface{} `json:"value,omitempty"` -} - -func (j *JsonPatchOperation) Json() string { - b, _ := json.Marshal(j) - return string(b) -} - -func (j *JsonPatchOperation) MarshalJSON() ([]byte, error) { - var b bytes.Buffer - b.WriteString("{") - b.WriteString(fmt.Sprintf(`"op":"%s"`, j.Operation)) - b.WriteString(fmt.Sprintf(`,"path":"%s"`, j.Path)) - // Consider omitting Value for non-nullable operations. - if j.Value != nil || j.Operation == "replace" || j.Operation == "add" { - v, err := json.Marshal(j.Value) - if err != nil { - return nil, err - } - b.WriteString(`,"value":`) - b.Write(v) - } - b.WriteString("}") - return b.Bytes(), nil -} - -type ByPath []JsonPatchOperation - -func (a ByPath) Len() int { return len(a) } -func (a ByPath) Swap(i, j int) { a[i], a[j] = a[j], a[i] } -func (a ByPath) Less(i, j int) bool { return a[i].Path < a[j].Path } - -func NewPatch(operation, path string, value interface{}) JsonPatchOperation { - return JsonPatchOperation{Operation: operation, Path: path, Value: value} -} - -// CreatePatch creates a patch as specified in http://jsonpatch.com/ -// -// 'a' is original, 'b' is the modified document. Both are to be given as json encoded content. -// The function will return an array of JsonPatchOperations -// -// An error will be returned if any of the two documents are invalid. -func CreatePatch(a, b []byte) ([]JsonPatchOperation, error) { - var aI interface{} - var bI interface{} - - err := json.Unmarshal(a, &aI) - if err != nil { - return nil, errBadJSONDoc - } - err = json.Unmarshal(b, &bI) - if err != nil { - return nil, errBadJSONDoc - } - - return handleValues(aI, bI, "", []JsonPatchOperation{}) -} - -// Returns true if the values matches (must be json types) -// The types of the values must match, otherwise it will always return false -// If two map[string]interface{} are given, all elements must match. -func matchesValue(av, bv interface{}) bool { - if reflect.TypeOf(av) != reflect.TypeOf(bv) { - return false - } - switch at := av.(type) { - case string: - bt := bv.(string) - if bt == at { - return true - } - case float64: - bt := bv.(float64) - if bt == at { - return true - } - case bool: - bt := bv.(bool) - if bt == at { - return true - } - case map[string]interface{}: - bt := bv.(map[string]interface{}) - for key := range at { - if !matchesValue(at[key], bt[key]) { - return false - } - } - for key := range bt { - if !matchesValue(at[key], bt[key]) { - return false - } - } - return true - case []interface{}: - bt := bv.([]interface{}) - if len(bt) != len(at) { - return false - } - for key := range at { - if !matchesValue(at[key], bt[key]) { - return false - } - } - for key := range bt { - if !matchesValue(at[key], bt[key]) { - return false - } - } - return true - } - return false -} - -// From http://tools.ietf.org/html/rfc6901#section-4 : -// -// Evaluation of each reference token begins by decoding any escaped -// character sequence. This is performed by first transforming any -// occurrence of the sequence '~1' to '/', and then transforming any -// occurrence of the sequence '~0' to '~'. -// TODO decode support: -// var rfc6901Decoder = strings.NewReplacer("~1", "/", "~0", "~") - -var rfc6901Encoder = strings.NewReplacer("~", "~0", "/", "~1") - -func makePath(path string, newPart interface{}) string { - key := rfc6901Encoder.Replace(fmt.Sprintf("%v", newPart)) - if path == "" { - return "/" + key - } - if strings.HasSuffix(path, "/") { - return path + key - } - return path + "/" + key -} - -// diff returns the (recursive) difference between a and b as an array of JsonPatchOperations. -func diff(a, b map[string]interface{}, path string, patch []JsonPatchOperation) ([]JsonPatchOperation, error) { - for key, bv := range b { - p := makePath(path, key) - av, ok := a[key] - // value was added - if !ok { - patch = append(patch, NewPatch("add", p, bv)) - continue - } - // If types have changed, replace completely - if reflect.TypeOf(av) != reflect.TypeOf(bv) { - patch = append(patch, NewPatch("replace", p, bv)) - continue - } - // Types are the same, compare values - var err error - patch, err = handleValues(av, bv, p, patch) - if err != nil { - return nil, err - } - } - // Now add all deleted values as nil - for key := range a { - _, found := b[key] - if !found { - p := makePath(path, key) - - patch = append(patch, NewPatch("remove", p, nil)) - } - } - return patch, nil -} - -func handleValues(av, bv interface{}, p string, patch []JsonPatchOperation) ([]JsonPatchOperation, error) { - var err error - switch at := av.(type) { - case map[string]interface{}: - bt := bv.(map[string]interface{}) - patch, err = diff(at, bt, p, patch) - if err != nil { - return nil, err - } - case string, float64, bool: - if !matchesValue(av, bv) { - patch = append(patch, NewPatch("replace", p, bv)) - } - case []interface{}: - bt, ok := bv.([]interface{}) - if !ok { - // array replaced by non-array - patch = append(patch, NewPatch("replace", p, bv)) - } else if len(at) != len(bt) { - // arrays are not the same length - patch = append(patch, compareArray(at, bt, p)...) - - } else { - for i := range bt { - patch, err = handleValues(at[i], bt[i], makePath(p, i), patch) - if err != nil { - return nil, err - } - } - } - case nil: - switch bv.(type) { - case nil: - // Both nil, fine. - default: - patch = append(patch, NewPatch("add", p, bv)) - } - default: - panic(fmt.Sprintf("Unknown type:%T ", av)) - } - return patch, nil -} - -// compareArray generates remove and add operations for `av` and `bv`. -func compareArray(av, bv []interface{}, p string) []JsonPatchOperation { - retval := []JsonPatchOperation{} - - // Find elements that need to be removed - processArray(av, bv, func(i int, value interface{}) { - retval = append(retval, NewPatch("remove", makePath(p, i), nil)) - }) - reversed := make([]JsonPatchOperation, len(retval)) - for i := 0; i < len(retval); i++ { - reversed[len(retval)-1-i] = retval[i] - } - retval = reversed - // Find elements that need to be added. - // NOTE we pass in `bv` then `av` so that processArray can find the missing elements. - processArray(bv, av, func(i int, value interface{}) { - retval = append(retval, NewPatch("add", makePath(p, i), value)) - }) - - return retval -} - -// processArray processes `av` and `bv` calling `applyOp` whenever a value is absent. -// It keeps track of which indexes have already had `applyOp` called for and automatically skips them so you can process duplicate objects correctly. -func processArray(av, bv []interface{}, applyOp func(i int, value interface{})) { - foundIndexes := make(map[int]struct{}, len(av)) - reverseFoundIndexes := make(map[int]struct{}, len(av)) - for i, v := range av { - for i2, v2 := range bv { - if _, ok := reverseFoundIndexes[i2]; ok { - // We already found this index. - continue - } - if reflect.DeepEqual(v, v2) { - // Mark this index as found since it matches exactly. - foundIndexes[i] = struct{}{} - reverseFoundIndexes[i2] = struct{}{} - break - } - } - if _, ok := foundIndexes[i]; !ok { - applyOp(i, v) - } - } -} diff --git a/vendor/github.com/wI2L/jsondiff/.codecov.yml b/vendor/github.com/wI2L/jsondiff/.codecov.yml new file mode 100644 index 000000000..d01e6a487 --- /dev/null +++ b/vendor/github.com/wI2L/jsondiff/.codecov.yml @@ -0,0 +1,8 @@ +coverage: + range: 80..90 + status: + # Prevent small variations of coverage from failing CI. + project: + default: + threshold: 1% + patch: off diff --git a/vendor/github.com/wI2L/jsondiff/.gitignore b/vendor/github.com/wI2L/jsondiff/.gitignore new file mode 100644 index 000000000..e30e05973 --- /dev/null +++ b/vendor/github.com/wI2L/jsondiff/.gitignore @@ -0,0 +1,56 @@ +# Linux +*~ +.Trash-* + +# Mac OSX +.DS_Store +.AppleDouble +.LSOverride +._* + +# Folders +_obj +_test + +# Compiled Object files +# Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so +*.dll +*.dylib + +# Executables +*.exe +*.exe~ + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +# Cgo +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +# Go Tools +*.test +*.prof +*.out +_testmain.go + +# Dependency directories +vendor/ +Godeps/ + +# markdown-spellcheck +.spelling + +# CI/CD +benchstats +.benchruns +coverage.txt +dist/ +*.txt diff --git a/vendor/github.com/wI2L/jsondiff/.golangci.yaml b/vendor/github.com/wI2L/jsondiff/.golangci.yaml new file mode 100644 index 000000000..4c9976039 --- /dev/null +++ b/vendor/github.com/wI2L/jsondiff/.golangci.yaml @@ -0,0 +1,46 @@ +run: + timeout: 10m +linters: + disable-all: true + enable: + - asciicheck + - bodyclose + - deadcode + - depguard + - dogsled + - dupl + - errcheck + - exportloopref + - funlen + - gocognit + - goconst + - gocritic + - gocyclo + - gofmt + - goimports + - goprintffuncname + - gosec + - gosimple + - govet + - ineffassign + - misspell + - nakedret + - nolintlint + - revive + - rowserrcheck + - staticcheck + - structcheck + - stylecheck + - typecheck + - unconvert + - unparam + - unused + - varcheck + - whitespace +linters-settings: + gofmt: + simplify: true + dupl: + threshold: 400 + funlen: + lines: 120 \ No newline at end of file diff --git a/vendor/github.com/wI2L/jsondiff/.goreleaser.yml b/vendor/github.com/wI2L/jsondiff/.goreleaser.yml new file mode 100644 index 000000000..ba3e33b79 --- /dev/null +++ b/vendor/github.com/wI2L/jsondiff/.goreleaser.yml @@ -0,0 +1,11 @@ +project_name: jsondiff +builds: + - skip: true +release: + github: + owner: wI2L + name: jsondiff + draft: true + prerelease: auto +env_files: + github_token: ~/.goreleaser_github_token \ No newline at end of file diff --git a/vendor/github.com/wI2L/jsondiff/LICENSE b/vendor/github.com/wI2L/jsondiff/LICENSE new file mode 100644 index 000000000..a778a4ffd --- /dev/null +++ b/vendor/github.com/wI2L/jsondiff/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020-2023 William Poussier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/vendor/github.com/wI2L/jsondiff/README.md b/vendor/github.com/wI2L/jsondiff/README.md new file mode 100644 index 000000000..b8d3e4345 --- /dev/null +++ b/vendor/github.com/wI2L/jsondiff/README.md @@ -0,0 +1,552 @@ +

jsondiff

+
+

jsondiff is a Go package for computing the diff between two JSON documents as a series of RFC6902 (JSON Patch) operations, which is particularly suitable to create the patch response of a Kubernetes Mutating Webhook for example.

+

+ + + + + + + + +

+ +--- + +## Usage + +First, get the latest version of the library using the following command: + +```shell +$ go get github.com/wI2L/jsondiff@latest +``` + +:warning: Requires Go1.18+, due to the usage of the package [`hash/maphash`](https://golang.org/pkg/hash/maphash/), and the `any` keyword (predeclared type alias for the empty interface). + +### Example use cases + +#### Kubernetes Dynamic Admission Controller + +The typical use case within an application is to compare two values of the same type that represents the source and desired target of a JSON document. A concrete application of that would be to generate the patch returned by a Kubernetes [dynamic admission controller](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/) to mutate a resource. Thereby, instead of generating the operations, just copy the source in order to apply the required changes and delegate the patch generation to the library. + +For example, given the following `corev1.Pod` value that represents a Kubernetes [demo pod](https://raw.githubusercontent.com/kubernetes/website/master/content/en/examples/application/shell-demo.yaml) containing a single container: + +```go +import corev1 "k8s.io/api/core/v1" + +pod := corev1.Pod{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{{ + Name: "webserver", + Image: "nginx:latest", + VolumeMounts: []corev1.VolumeMount{{ + Name: "shared-data", + MountPath: "/usr/share/nginx/html", + }}, + }}, + Volumes: []corev1.Volume{{ + Name: "shared-data", + VolumeSource: corev1.VolumeSource{ + EmptyDir: &corev1.EmptyDirVolumeSource{ + Medium: corev1.StorageMediumMemory, + }, + }, + }}, + }, +} +``` + +The first step is to copy the original pod value. The `corev1.Pod` type defines a `DeepCopy` method, which is handy, but for other types, a [shallow copy is discouraged](https://medium.com/@alenkacz/shallow-copy-of-a-go-struct-in-golang-is-never-a-good-idea-83be60106af8), instead use a specific library, such as [ulule/deepcopier](https://github.com/ulule/deepcopier). Alternatively, if you don't require to keep the original value, you can marshal it to JSON using `json.Marshal` to store a pre-encoded copy of the document, and mutate the value. + +```go +newPod := pod.DeepCopy() +// or +podBytes, err := json.Marshal(pod) +if err != nil { + // handle error +} +``` + +Secondly, make some changes to the pod spec. Here we modify the image and the *storage medium* used by the pod's volume `shared-data`. + +```go +// Update the image of the webserver container. +newPod.Spec.Containers[0].Image = "nginx:1.19.5-alpine" + +// Switch storage medium from memory to default. +newPod.Spec.Volumes[0].EmptyDir.Medium = corev1.StorageMediumDefault +``` + +Finally, generate the patch that represents the changes relative to the original value. Note that when the `Compare` or `CompareOpts` functions are used, the `source` and `target` parameters are first marshaled using the `encoding/json` package in order to obtain their final JSON representation, prior to comparing them. + +```go +import "github.com/wI2L/jsondiff" + +patch, err := jsondiff.Compare(pod, newPod) +if err != nil { + // handle error +} +b, err := json.MarshalIndent(patch, "", " ") +if err != nil { + // handle error +} +os.Stdout.Write(b) +``` + +The output is similar to the following: + +```json +[{ + "op": "replace", + "path": "/spec/containers/0/image", + "value": "nginx:1.19.5-alpine" +}, { + "op": "remove", + "path": "/spec/volumes/0/emptyDir/medium" +}] +``` + +The JSON patch can then be used in the response payload of you Kubernetes [webhook](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#response). + +##### Optional fields gotcha + +Note that the above example is used for simplicity, but in a real-world admission controller, you should create the diff from the raw bytes of the `AdmissionReview.AdmissionRequest.Object.Raw` field. As pointed out by user [/u/terinjokes](https://www.reddit.com/user/terinjokes/) on Reddit, due to the nature of Go structs, the "hydrated" `corev1.Pod` object may contain "optional fields", resulting in a patch that state added/changed values that the Kubernetes API server doesn't know about. Below is a quote of the original comment: + +> Optional fields being ones that are a struct type, but are not pointers to those structs. These will exist when you unmarshal from JSON, because of how Go structs work, but are not in the original JSON. Comparing between the unmarshaled and copied versions can generate add and change patches below a path not in the original JSON, and the API server will reject your patch. + +A realistic usage would be similar to the following snippet: + +```go +podBytes, err := json.Marshal(pod) +if err != nil { + // handle error +} +// req is a k8s.io/api/admission/v1.AdmissionRequest object +jsondiff.CompareJSON(req.AdmissionRequest.Object.Raw, podBytes) +``` + +Mutating the original pod object or a copy is up to you, as long as you use the raw bytes of the `AdmissionReview` object to generate the patch. + +You can find a detailed description of that problem and its resolution in this GitHub [issue](https://github.com/kubernetes-sigs/kubebuilder/issues/510). + +##### Outdated package version + +There's also one other downside to the above example. If your webhook does not have the latest version of the `client-go` package, or whatever package that contains the types for the resource you're manipulating, all fields not known in that version will be deleted. + +For example, if your webhook mutate `Service` resources, a user could set the field `.spec.allocateLoadBalancerNodePort` in Kubernetes 1.20 to disable allocating a node port for services with `Type=LoadBalancer`. However, if the webhook is still using the v1.19.x version of the `k8s.io/api/core/v1` package that define the `Service` type, instead of simply ignoring this field, a `remove` operation will be generated for it. + +### Diff options + +If more control over the diff behaviour is required, use the `CompareOpts` or `CompareJSONOpts` function instead. The third parameter is variadic and accept a list of functional opt-in options described below. + +Note that any combination of options can be used without issues. + +#### Operations factorization + +By default, when computing the difference between two JSON documents, the package does not produce `move` or `copy` operations. To enable the factorization of value removals and additions as moves and copies, you should use the functional option `Factorize()`. Factorization reduces the number of operations generated, which inevitably reduce the size of the patch once it is marshaled as JSON. + +For instance, given the following document: + +```json +{ + "a": [ 1, 2, 3 ], + "b": { "foo": "bar" } +} +``` + +In order to obtain this updated version: + +```json +{ + "a": [ 1, 2, 3 ], + "c": [ 1, 2, 3 ], + "d": { "foo": "bar" } +} +``` + +The package generates the following patch: + +```json +[ + { "op": "remove", "path": "/b" }, + { "op": "add", "path": "/c", "value": [ 1, 2, 3 ] }, + { "op": "add", "path": "/d", "value": { "foo": "bar" } } +] +``` + +If we take the previous example and generate the patch with factorization enabled, we then get a different patch, containing `copy` and `move` operations instead: + +```json +[ + { "op": "copy", "from": "/a", "path": "/c" }, + { "op": "move", "from": "/b", "path": "/d" } +] +``` + +#### Operations rationalization + +The default method used to compare two JSON documents is a recursive comparison. This produce one or more operations for each difference found. On the other hand, in certain situations, it might be beneficial to replace a set of operations representing several changes inside a JSON node by a single replace operation targeting the parent node, in order to reduce the "size" of the patch (the length in bytes of the JSON representation of the patch). + +For that purpose, you can use the `Rationalize()` option. It uses a simple weight function to decide which patch is best (it marshals both sets of operations to JSON and looks at the length of bytes to keep the smaller footprint). + +Let's illustrate that with the following document: + +```json +{ + "a": { "b": { "c": { "1": 1, "2": 2, "3": 3 } } } +} +``` + +In order to obtain this updated version: + +```json +{ + "a": { "b": { "c": { "x": 1, "y": 2, "z": 3 } } } +} +``` + +The expected output is one remove/add operation combo for each children field of the object located at path `a.b.c`: + +```json +[ + { "op": "remove", "path": "/a/b/c/1" }, + { "op": "remove", "path": "/a/b/c/2" }, + { "op": "remove", "path": "/a/b/c/3" }, + { "op": "add", "path": "/a/b/c/x", "value": 1 }, + { "op": "add", "path": "/a/b/c/y", "value": 2 }, + { "op": "add", "path": "/a/b/c/z", "value": 3 } +] +``` + +If we also enable factorization, as seen above, we can reduce the number of operations by half: + +```json +[ + { "op": "move", "from": "/a/b/c/1", "path": "/a/b/c/x" }, + { "op": "move", "from": "/a/b/c/2", "path": "/a/b/c/y" }, + { "op": "move", "from": "/a/b/c/3", "path": "/a/b/c/z" } +] +``` + +And finally, with rationalization enabled, those operations are replaced with a single `replace` of the parent object: + +```json +[ + { "op": "replace", "path": "/a/b/c", "value": { "x": 1, "y": 2, "z": 3 } } +] +``` + +##### Input compaction + +Reducing the size of a JSON Patch is usually beneficial when it needs to be sent on the wire (HTTP request with the `application/json-patch+json` media type for example). As such, the package assumes that the desired JSON representation of a patch is a compact ("minified") JSON document. + +When the `Rationalize()` option is enabled, the package pre-process the JSON input given to the `CompareJSON*` functions. If your inputs are already compact JSON documents, you **should** also use the `SkipCompact()` option to instruct the package to skip the compaction step, resulting in a *nice and free* performance improvement. + +##### In-place compaction + +By default, the package will not modify the JSON documents given to the `CompareJSON` and `CompareJSONOpts` functions. Instead, a copy of the `target` byte slice argument is created and then compacted to remove insignificant spaces. + +To avoid an extra allocation, you can use the `InPlaceCompaction()` option to allow the package to *take ownership* of the `target` byte slice and modify it directly. **Note that you should not update it concurrently with a call to the `CompareJSON*` functions.** + +#### Invertible patch + +Using the functional option `Invertible()`, it is possible to instruct the diff generator to precede each `remove` and `replace` operation with a `test` operation. Such patches can be inverted to return a patched document to its original form. + +However, note that it comes with one limitation. `copy` operations cannot be inverted, as they are ambiguous (the reverse of a `copy` is a `remove`, which could then become either an `add` or a `copy`). As such, using this option disable the generation of `copy` operations (if option `Factorize()` is used) and replace them with `add` operations, albeit potentially at the cost of increased patch size. + +For example, let's generate the diff between those two JSON documents: + +```json +{ + "a": "1", + "b": "2" +} +``` +```json +{ + "a": "3", + "c": "4" +} +``` + +The patch is similar to the following: + +```json +[ + { "op": "test", "path": "/a", "value": "1" }, + { "op": "replace", "path": "/a", "value": "3" }, + { "op": "test", "path": "/b", "value": "2" }, + { "op": "remove", "path": "/b" }, + { "op": "add", "path": "/c", "value": "4" } +] +``` + +As you can see, the `remove` and `replace` operations are preceded with a `test` operation which assert/verify the `value` of the previous `path`. On the other hand, the `add` operation can be reverted to a remove operation directly and doesn't need to be preceded by a `test`. + +[Run this example](https://pkg.go.dev/github.com/wI2L/jsondiff#example-Invertible). + +Finally, as a side example, if we were to use the `Rationalize()` option in the context of the previous example, the output would be shorter, but the generated patch would still remain invertible: + +```json +[ + { "op": "test", "path": "", "value": { "a": "1", "b": "2" } }, + { "op": "replace", "path": "", "value": { "a": "3", "c": "4" } } +] +``` + +#### Equivalence + +Some data types, such as arrays, can be deeply unequal and equivalent at the same time. + +Take the following JSON documents: +```json +[ + "a", "b", "c", "d" +] +``` +```json +[ + "d", "c", "b", "a" +] +``` + +The root arrays of each document are not equal because the values differ at each index. However, they are equivalent in terms of content: +- they have the same length +- the elements of the first can be found in the second, the same number of times for each + +For such situations, you can use the `Equivalent()` option to instruct the diff generator to skip the generation of operations that would otherwise be added to the patch to represent the differences between the two arrays. + +#### Ignores + +:construction: *This option is experimental and might be revised in the future.* +
+
+ +The `Ignores()` option allows to exclude one or more JSON fields/values from the *generated diff*. The fields must be identified using the JSON Pointer (RFC6901) string syntax. + +The option accepts a variadic list of JSON Pointers, which all individually represent a value in the source document. However, if the value does not exist in the source document, the value will be considered to be in the target document, which allows to *ignore* `add` operations. + +For example, let's generate the diff between those two JSON documents: + +```json +{ + "A": "bar", + "B": "baz", + "C": "foo" +} +``` + +```json +{ + "A": "rab", + "B": "baz", + "D": "foo" +} +``` + +Without the `Ignores()` option, the output patch is the following: + +```json +[ + { "op": "replace", "path": "/A", "value": "rab" }, + { "op": "remove", "path": "/C" }, + { "op": "add", "path": "/D", "value": "foo" } +] +``` + +Using the option with the following pointers list, we can ignore some of the fields that were updated, added or removed: + +```go +jsondiff.Ignores("/A", "/B", "/C") +``` + +The resulting patch is empty, because all changes and ignored. + +[Run this example](https://pkg.go.dev/github.com/wI2L/jsondiff#example-Ignores). + +> See the actual [testcases](testdata/tests/options/ignore.json) for more examples. + +#### MarshalFunc / UnmarshalFunc + +By default, the package uses the `json.Marshal` and `json.Unmarshal` functions from the standard library's `encoding` package, to marshal and unmarshal objects to/from JSON. If you wish to use another package for performance reasons, or simply to customize the encoding/decoding behavior, you can use the `MarshalFunc` and `UnmarshalFunc` options to configure it. + +The prototype of the function argument accepted by these options is the same as the official `json.Marshal` and `json.Unmarshal` functions. + +##### Custom decoder + +In the following example, the `UnmarshalFunc` option is used to set up a custom JSON [`Decoder`](https://pkg.go.dev/encoding/json#Decoder) with the [`UserNumber`](https://pkg.go.dev/encoding/json#Decoder.UseNumber) flag enabled, to decode JSON numbers as [`json.Number`](https://pkg.go.dev/encoding/json#Decoder.UseNumber) instead of `float64`: + +```go +patch, err := jsondiff.CompareJSONOpts( + source, + target, + jsondiff.UnmarshalFunc(func(b []byte, v any) error { + dec := json.NewDecoder(bytes.NewReader(b)) + dec.UseNumber() + return dec.Decode(v) + }), +) +``` + +## Benchmarks + +A couple of benchmarks that compare the performance for different JSON document sizes are provided to give a rough estimate of the cost of each option. You can find the JSON documents used by those benchmarks in the directory [testdata/benchs](testdata/benchs). + +If you'd like to run the benchmarks yourself, use the following command: + +```shell +go get github.com/cespare/prettybench +go test -bench=. | prettybench +``` + +### Results + +The benchmarks were run 10x (statistics computed with [benchstat](https://godoc.org/golang.org/x/perf/cmd/benchstat)) on a MacBook Pro 15", with the following specs: + +``` +OS : macOS Big Sur (11.7.6) +CPU: 2.6 GHz Intel Core i7 +Mem: 16GB 1600 MHz +Go : go version go1.20.4 darwin/amd64 +``` + +
Output
+name                                       time/op
+Small/DifferReset/default-8                2.15µs ± 0%
+Small/Differ/default-8                     2.57µs ± 1%
+Small/DifferReset/default-unordered-8      2.31µs ± 1%
+Small/Differ/default-unordered-8           2.95µs ± 0%
+Small/DifferReset/invertible-8             2.18µs ± 1%
+Small/Differ/invertible-8                  2.82µs ± 1%
+Small/DifferReset/factorize-8              3.53µs ± 0%
+Small/Differ/factorize-8                   4.11µs ± 0%
+Small/DifferReset/rationalize-8            2.29µs ± 0%
+Small/Differ/rationalize-8                 2.73µs ± 1%
+Small/DifferReset/equivalent-8             2.14µs ± 1%
+Small/Differ/equivalent-8                  2.57µs ± 1%
+Small/DifferReset/equivalent-unordered-8   2.32µs ± 1%
+Small/Differ/equivalent-unordered-8        2.76µs ± 1%
+Small/DifferReset/factor+ratio-8           3.67µs ± 1%
+Small/Differ/factor+ratio-8                4.26µs ± 1%
+Small/DifferReset/all-8                    3.77µs ± 0%
+Small/Differ/all-8                         4.59µs ± 0%
+Small/DifferReset/all-unordered-8          3.99µs ± 1%
+Small/Differ/all-unordered-8               4.82µs ± 0%
+Medium/DifferReset/default-8               6.23µs ± 0%
+Medium/Differ/default-8                    7.30µs ± 1%
+Medium/DifferReset/default-unordered-8     6.81µs ± 1%
+Medium/Differ/default-unordered-8          8.52µs ± 1%
+Medium/DifferReset/invertible-8            6.32µs ± 1%
+Medium/Differ/invertible-8                 8.09µs ± 0%
+Medium/DifferReset/factorize-8             11.3µs ± 1%
+Medium/Differ/factorize-8                  13.0µs ± 1%
+Medium/DifferReset/rationalize-8           6.91µs ± 1%
+Medium/Differ/rationalize-8                7.66µs ± 1%
+Medium/DifferReset/equivalent-8            10.0µs ± 1%
+Medium/Differ/equivalent-8                 11.1µs ± 1%
+Medium/DifferReset/equivalent-unordered-8  11.0µs ± 1%
+Medium/Differ/equivalent-unordered-8       12.1µs ± 0%
+Medium/DifferReset/factor+ratio-8          11.8µs ± 0%
+Medium/Differ/factor+ratio-8               13.1µs ± 0%
+Medium/DifferReset/all-8                   16.1µs ± 1%
+Medium/Differ/all-8                        17.9µs ± 1%
+Medium/DifferReset/all-unordered-8         17.7µs ± 1%
+Medium/Differ/all-unordered-8              19.5µs ± 0%
+
name alloc/op +Small/DifferReset/default-8 216B ± 0% +Small/Differ/default-8 1.19kB ± 0% +Small/DifferReset/default-unordered-8 312B ± 0% +Small/Differ/default-unordered-8 1.99kB ± 0% +Small/DifferReset/invertible-8 216B ± 0% +Small/Differ/invertible-8 1.90kB ± 0% +Small/DifferReset/factorize-8 400B ± 0% +Small/Differ/factorize-8 1.78kB ± 0% +Small/DifferReset/rationalize-8 224B ± 0% +Small/Differ/rationalize-8 1.20kB ± 0% +Small/DifferReset/equivalent-8 216B ± 0% +Small/Differ/equivalent-8 1.19kB ± 0% +Small/DifferReset/equivalent-unordered-8 216B ± 0% +Small/Differ/equivalent-unordered-8 1.19kB ± 0% +Small/DifferReset/factor+ratio-8 408B ± 0% +Small/Differ/factor+ratio-8 1.78kB ± 0% +Small/DifferReset/all-8 408B ± 0% +Small/Differ/all-8 2.49kB ± 0% +Small/DifferReset/all-unordered-8 520B ± 0% +Small/Differ/all-unordered-8 2.60kB ± 0% +Medium/DifferReset/default-8 624B ± 0% +Medium/Differ/default-8 3.71kB ± 0% +Medium/DifferReset/default-unordered-8 848B ± 0% +Medium/Differ/default-unordered-8 7.01kB ± 0% +Medium/DifferReset/invertible-8 624B ± 0% +Medium/Differ/invertible-8 6.78kB ± 0% +Medium/DifferReset/factorize-8 1.41kB ± 0% +Medium/Differ/factorize-8 5.60kB ± 0% +Medium/DifferReset/rationalize-8 672B ± 0% +Medium/Differ/rationalize-8 2.35kB ± 0% +Medium/DifferReset/equivalent-8 1.39kB ± 0% +Medium/Differ/equivalent-8 4.48kB ± 0% +Medium/DifferReset/equivalent-unordered-8 1.49kB ± 0% +Medium/Differ/equivalent-unordered-8 4.58kB ± 0% +Medium/DifferReset/factor+ratio-8 1.45kB ± 0% +Medium/Differ/factor+ratio-8 4.24kB ± 0% +Medium/DifferReset/all-8 2.22kB ± 0% +Medium/Differ/all-8 6.41kB ± 0% +Medium/DifferReset/all-unordered-8 2.36kB ± 0% +Medium/Differ/all-unordered-8 6.55kB ± 0% +
name allocs/op +Small/DifferReset/default-8 9.00 ± 0% +Small/Differ/default-8 13.0 ± 0% +Small/DifferReset/default-unordered-8 13.0 ± 0% +Small/Differ/default-unordered-8 18.0 ± 0% +Small/DifferReset/invertible-8 9.00 ± 0% +Small/Differ/invertible-8 14.0 ± 0% +Small/DifferReset/factorize-8 21.0 ± 0% +Small/Differ/factorize-8 27.0 ± 0% +Small/DifferReset/rationalize-8 10.0 ± 0% +Small/Differ/rationalize-8 14.0 ± 0% +Small/DifferReset/equivalent-8 9.00 ± 0% +Small/Differ/equivalent-8 13.0 ± 0% +Small/DifferReset/equivalent-unordered-8 9.00 ± 0% +Small/Differ/equivalent-unordered-8 13.0 ± 0% +Small/DifferReset/factor+ratio-8 22.0 ± 0% +Small/Differ/factor+ratio-8 28.0 ± 0% +Small/DifferReset/all-8 22.0 ± 0% +Small/Differ/all-8 29.0 ± 0% +Small/DifferReset/all-unordered-8 25.0 ± 0% +Small/Differ/all-unordered-8 32.0 ± 0% +Medium/DifferReset/default-8 18.0 ± 0% +Medium/Differ/default-8 24.0 ± 0% +Medium/DifferReset/default-unordered-8 26.0 ± 0% +Medium/Differ/default-unordered-8 33.0 ± 0% +Medium/DifferReset/invertible-8 18.0 ± 0% +Medium/Differ/invertible-8 25.0 ± 0% +Medium/DifferReset/factorize-8 55.0 ± 0% +Medium/Differ/factorize-8 64.0 ± 0% +Medium/DifferReset/rationalize-8 22.0 ± 0% +Medium/Differ/rationalize-8 27.0 ± 0% +Medium/DifferReset/equivalent-8 26.0 ± 0% +Medium/Differ/equivalent-8 32.0 ± 0% +Medium/DifferReset/equivalent-unordered-8 30.0 ± 0% +Medium/Differ/equivalent-unordered-8 36.0 ± 0% +Medium/DifferReset/factor+ratio-8 59.0 ± 0% +Medium/Differ/factor+ratio-8 67.0 ± 0% +Medium/DifferReset/all-8 67.0 ± 0% +Medium/Differ/all-8 76.0 ± 0% +Medium/DifferReset/all-unordered-8 74.0 ± 0% +Medium/Differ/all-unordered-8 83.0 ± 0% +
+ +## Credits + +This package has been inspired by existing implementations of JSON Patch in various languages: + +- [cujojs/jiff](https://github.com/cujojs/jiff) +- [Starcounter-Jack/JSON-Patch](https://github.com/Starcounter-Jack/JSON-Patch) +- [java-json-tools/json-patch](https://github.com/java-json-tools/json-patch) +- [Lattyware/elm-json-diff](https://github.com/Lattyware/elm-json-diff) +- [espadrine/json-diff](https://github.com/espadrine/json-diff) + +## License + +`jsondiff` is licensed under the **MIT** license. See the [LICENSE](LICENSE) file. diff --git a/vendor/github.com/wI2L/jsondiff/compare.go b/vendor/github.com/wI2L/jsondiff/compare.go new file mode 100644 index 000000000..24859b4f1 --- /dev/null +++ b/vendor/github.com/wI2L/jsondiff/compare.go @@ -0,0 +1,77 @@ +package jsondiff + +import "encoding/json" + +// Compare compares the JSON representations of the +// given values and returns the differences relative +// to the former as a list of JSON Patch operations. +func Compare(source, target interface{}, opts ...Option) (Patch, error) { + var d Differ + d.applyOpts(opts...) + + return compare(&d, source, target) +} + +// CompareJSON compares the given JSON documents and +// returns the differences relative to the former as +// a list of JSON Patch operations. +func CompareJSON(source, target []byte, opts ...Option) (Patch, error) { + var d Differ + d.applyOpts(opts...) + + return compareJSON(&d, source, target, d.opts.unmarshal) +} + +func compare(d *Differ, src, tgt interface{}) (Patch, error) { + if d.opts.marshal == nil { + d.opts.marshal = json.Marshal + } + if d.opts.unmarshal == nil { + d.opts.unmarshal = json.Unmarshal + } + si, _, err := marshalUnmarshal(src, d.opts) + if err != nil { + return nil, err + } + ti, tb, err := marshalUnmarshal(tgt, d.opts) + if err != nil { + return nil, err + } + d.targetBytes = tb + d.compactInPlace = true + + d.Compare(si, ti) + return d.patch, nil +} + +func compareJSON(d *Differ, src, tgt []byte, unmarshal unmarshalFunc) (Patch, error) { + if unmarshal == nil { + unmarshal = json.Unmarshal + } + var si, ti interface{} + if err := unmarshal(src, &si); err != nil { + return nil, err + } + if err := unmarshal(tgt, &ti); err != nil { + return nil, err + } + d.targetBytes = tgt + d.compactInPlace = true + + d.Compare(si, ti) + return d.patch, nil +} + +// marshalUnmarshal returns the result of unmarshaling +// the JSON representation of the given interface value. +func marshalUnmarshal(v any, opts options) (interface{}, []byte, error) { + b, err := opts.marshal(v) + if err != nil { + return nil, nil, err + } + var i interface{} + if err := opts.unmarshal(b, &i); err != nil { + return nil, nil, err + } + return i, b, nil +} diff --git a/vendor/github.com/wI2L/jsondiff/differ.go b/vendor/github.com/wI2L/jsondiff/differ.go new file mode 100644 index 000000000..870b6c263 --- /dev/null +++ b/vendor/github.com/wI2L/jsondiff/differ.go @@ -0,0 +1,454 @@ +package jsondiff + +import ( + "sort" + "strings" + "unsafe" +) + +// A Differ generates JSON Patch (RFC 6902). +// The zero value is an empty generator ready to use. +type Differ struct { + hashmap map[uint64]jsonNode + opts options + patch Patch + targetBytes []byte + ptr pointer + hasher hasher + isCompact bool + compactInPlace bool +} + +type ( + marshalFunc func(any) ([]byte, error) + unmarshalFunc func([]byte, any) error +) + +type options struct { + ignores map[string]struct{} + marshal marshalFunc + unmarshal unmarshalFunc + hasIgnore bool + factorize bool + rationalize bool + invertible bool + equivalent bool +} + +type jsonNode struct { + val any + ptr string +} + +// Reset resets the Differ to be empty, but it retains the +// underlying storage for use by future comparisons. +func (d *Differ) Reset() { + d.patch = d.patch[:0] + d.ptr.reset() + + // Optimized map clear. + for k := range d.hashmap { + delete(d.hashmap, k) + } +} + +// WithOpts applies the given options to the Differ +// and returns it to allow chained calls. +func (d *Differ) WithOpts(opts ...Option) *Differ { + for _, o := range opts { + o(d) + } + return d +} + +// Patch returns the list of JSON patch operations +// generated by the Differ. The patch is valid for usage +// until the next comparison or reset. +func (d *Differ) Patch() Patch { + return d.patch +} + +// Compare computes the differences between src and tgt as +// a series of JSON Patch operations. +func (d *Differ) Compare(src, tgt interface{}) { + if d.opts.factorize { + d.prepare(d.ptr, src, tgt) + d.ptr.reset() + } + if d.opts.rationalize { + if !d.isCompact { + if d.compactInPlace { + d.targetBytes = compactInPlace(d.targetBytes) + } else { + d.targetBytes = compact(d.targetBytes) + } + } + } + d.diff(d.ptr, src, tgt, b2s(d.targetBytes)) +} + +func (d *Differ) isIgnored(ptr pointer) bool { + // Fast path, inlined map check. + if !d.opts.hasIgnore { + return false + } + // Slow path. + // Outlined so that the fast path can be inlined. + return d.findIgnored(ptr) +} + +func (d *Differ) findIgnored(ptr pointer) bool { + _, found := d.opts.ignores[ptr.string()] + return found +} + +func (d *Differ) diff(ptr pointer, src, tgt interface{}, doc string) { + if d.isIgnored(ptr) { + return + } + if !areComparable(src, tgt) { + if ptr.isRoot() { + // If incomparable values are located at the root + // of the document, use an add operation to replace + // the entire content of the document. + // https://tools.ietf.org/html/rfc6902#section-4.1 + d.patch = d.patch.append(OperationAdd, emptyPointer, ptr.copy(), src, tgt, 0) + } else { + // Values are incomparable, generate a replacement. + d.replace(ptr.copy(), src, tgt, doc) + } + return + } + if deepEqual(src, tgt) { + return + } + // Save the current size of the patch to detect later + // on if we have new operations to rationalize. + size := len(d.patch) + + // Values are comparable, but are not + // equivalent. + switch val := src.(type) { + case []interface{}: + d.compareArrays(ptr, val, tgt.([]interface{}), doc) + case map[string]interface{}: + d.compareObjects(ptr, val, tgt.(map[string]interface{}), doc) + default: + // Generate a replace operation for + // scalar types. + if !deepEqual(src, tgt) { + d.replace(ptr.copy(), src, tgt, doc) + return + } + } + // Rationalize new operations, if any. + if d.opts.rationalize && len(d.patch) > size { + d.rationalize(ptr, src, tgt, size, doc) + } +} + +func (d *Differ) prepare(ptr pointer, src, tgt interface{}) { + // When both values are deeply equals, save + // the location indexed by the value hash. + if !areComparable(src, tgt) { + return + } else if deepEqual(src, tgt) { + k := d.hasher.digest(tgt) + if d.hashmap == nil { + d.hashmap = make(map[uint64]jsonNode) + } + d.hashmap[k] = jsonNode{ + ptr: ptr.copy(), + val: tgt, + } + return + } + // At this point, the source and target values + // are non-nil and have comparable types. + switch vsrc := src.(type) { + case []interface{}: + oarr := vsrc + narr := tgt.([]interface{}) + + for i := 0; i < min(len(oarr), len(narr)); i++ { + p := ptr.clone() + p.appendIndex(i) + d.prepare(p, oarr[i], narr[i]) + } + case map[string]interface{}: + oobj := vsrc + nobj := tgt.(map[string]interface{}) + + for k, v1 := range oobj { + if v2, ok := nobj[k]; ok { + p := ptr.clone() + p.appendKey(k) + d.prepare(p, v1, v2) + } + } + default: + // Skipped. + } +} + +func (d *Differ) rationalize(ptr pointer, src, tgt interface{}, lastOpIdx int, doc string) { + // replaceOp represents a single operation that + // replace the source document with the target. + replaceOp := Operation{ + Type: OperationReplace, + Path: ptr.string(), // shallow copy + Value: tgt, + valueLen: len(doc), + } + curOps := d.patch[lastOpIdx:] + curLen := curOps.jsonLength() + + // If one operation is cheaper than many small + // operations that represents the changes between + // the two objects, replace the last operations. + if curLen > replaceOp.jsonLength() { + d.patch = d.patch[:lastOpIdx] + + // Allocate a new string for the operation's path. + replaceOp.Path = ptr.copy() + + if d.opts.invertible { + d.patch = d.patch.append(OperationTest, emptyPointer, replaceOp.Path, nil, src, len(doc)) + } + d.patch = append(d.patch, replaceOp) + } +} + +// compareObjects generates the patch operations that +// represents the differences between two JSON objects. +func (d *Differ) compareObjects(ptr pointer, src, tgt map[string]interface{}, doc string) { + cmpSet := make(map[string]uint8, max(len(src), len(tgt))) + + for k := range src { + cmpSet[k] |= 1 << 0 + } + for k := range tgt { + cmpSet[k] |= 1 << 1 + } + keys := make([]string, 0, len(cmpSet)) + + for k := range cmpSet { + keys = append(keys, k) + } + sortStrings(keys) + + ptr.snapshot() + for _, k := range keys { + v := cmpSet[k] + inOld := v&(1<<0) != 0 + inNew := v&(1<<1) != 0 + + ptr.appendKey(k) + + switch { + case inOld && inNew: + if d.opts.rationalize { + d.diff(ptr, src[k], tgt[k], findKey(doc, ptr.base.key)) + } else { + d.diff(ptr, src[k], tgt[k], doc) + } + case inOld && !inNew: + if !d.isIgnored(ptr) { + d.remove(ptr.copy(), src[k]) + } + case !inOld && inNew: + if !d.isIgnored(ptr) { + d.add(ptr.copy(), tgt[k], doc) + } + } + ptr.rewind() + } +} + +// compareArrays generates the patch operations that +// represents the differences between two JSON arrays. +func (d *Differ) compareArrays(ptr pointer, src, tgt []interface{}, doc string) { + ptr.snapshot() + sl, tl := len(src), len(tgt) + min := min(sl, tl) + + // When the source array contains more elements + // than the target, entries are being removed + // from the destination and the removal index + // is always equal to the original array length. + if tl < sl { + np := ptr.clone() + np.appendIndex(min) // "removal" path + p := np.copy() + for i := min; i < sl; i++ { + ptr.appendIndex(i) + + if !d.isIgnored(ptr) { + d.remove(p, src[i]) + } + ptr.rewind() + } + goto comparisons // skip equivalence test since arrays are different + } + if d.opts.equivalent && d.unorderedDeepEqualSlice(src, tgt) { + return + } +comparisons: + // Compare the elements at each index present in + // both the source and destination arrays. + for i := 0; i < min; i++ { + ptr.appendIndex(i) + if d.opts.rationalize { + d.diff(ptr, src[i], tgt[i], findIndex(doc, ptr.base.idx)) + } else { + d.diff(ptr, src[i], tgt[i], doc) + } + ptr.rewind() + } + // When the target array contains more elements + // than the source, entries are appended to the + // destination. + if tl > sl { + np := ptr.clone() + np.appendKey("-") // "append" path + p := np.copy() + for i := min; i < tl; i++ { + ptr.appendIndex(i) + if !d.isIgnored(ptr) { + d.add(p, tgt[i], doc) + } + ptr.rewind() + } + } +} + +func (d *Differ) unorderedDeepEqualSlice(src, tgt []interface{}) bool { + if len(src) != len(tgt) { + return false + } + diff := make(map[uint64]struct{}, len(src)) + count := 0 + + for _, v := range src { + k := d.hasher.digest(v) + diff[k] = struct{}{} + count++ + } + for _, v := range tgt { + k := d.hasher.digest(v) + // If the digest hash is not in the compare, + // return early. + if _, ok := diff[k]; !ok { + return false + } + count-- + } + return count == 0 +} + +func (d *Differ) replace(path string, src, tgt interface{}, doc string) { + vl := len(doc) + + if d.opts.invertible { + d.patch = d.patch.append(OperationTest, emptyPointer, path, nil, src, vl) + } + d.patch = d.patch.append(OperationReplace, emptyPointer, path, src, tgt, vl) +} + +func (d *Differ) add(path string, v interface{}, doc string) { + if !d.opts.factorize { + d.patch = d.patch.append(OperationAdd, emptyPointer, path, nil, v, 0) + return + } + idx := d.findRemoved(v) + if idx != -1 { + op := d.patch[idx] + + // https://tools.ietf.org/html/rfc6902#section-4.4f + // The "from" location MUST NOT be a proper prefix + // of the "path" location; i.e., a location cannot + // be moved into one of its children. + if !strings.HasPrefix(path, op.Path) { + d.patch = d.patch.remove(idx) + d.patch = d.patch.append(OperationMove, op.Path, path, v, v, 0) + } + return + } + uptr := d.findUnchanged(v) + + if len(uptr) != 0 && !d.opts.invertible { + d.patch = d.patch.append(OperationCopy, uptr, path, nil, v, 0) + } else { + d.patch = d.patch.append(OperationAdd, emptyPointer, path, nil, v, len(doc)) + } +} + +func (d *Differ) remove(path string, v interface{}) { + if d.opts.invertible { + d.patch = d.patch.append(OperationTest, emptyPointer, path, nil, v, 0) + } + d.patch = d.patch.append(OperationRemove, emptyPointer, path, v, nil, 0) +} + +func (d *Differ) findUnchanged(v interface{}) string { + if d.hashmap != nil { + k := d.hasher.digest(v) + node, ok := d.hashmap[k] + if ok { + return node.ptr + } + } + return emptyPointer +} + +func (d *Differ) findRemoved(v interface{}) int { + for i := 0; i < len(d.patch); i++ { + op := d.patch[i] + if op.Type == OperationRemove && deepEqual(op.OldValue, v) { + return i + } + } + return -1 +} + +func (d *Differ) applyOpts(opts ...Option) { + for _, opt := range opts { + if opt != nil { + opt(d) + } + } +} + +func sortStrings(v []string) { + if len(v) <= 20 { + insertionSort(v) + } else { + sort.Strings(v) + } +} + +func insertionSort(v []string) { + for i := 0; i < len(v); i++ { + for j := i; j > 0 && v[j-1] > v[j]; j-- { + v[j], v[j-1] = v[j-1], v[j] + } + } +} + +func b2s(b []byte) string { + return *(*string)(unsafe.Pointer(&b)) +} + +func min(i, j int) int { + if i < j { + return i + } + return j +} + +func max(i, j int) int { + if i > j { + return i + } + return j +} diff --git a/vendor/github.com/wI2L/jsondiff/equal.go b/vendor/github.com/wI2L/jsondiff/equal.go new file mode 100644 index 000000000..42aae88d8 --- /dev/null +++ b/vendor/github.com/wI2L/jsondiff/equal.go @@ -0,0 +1,131 @@ +package jsondiff + +import ( + "encoding/json" + "strconv" +) + +// jsonValueType represents the type of JSON value. +// It follows the types of values stored by json.Unmarshal +// in interface values. +type jsonValueType uint + +const ( + jsonInvalid jsonValueType = iota + jsonNull + jsonString + jsonBoolean + jsonNumberFloat + jsonNumberString + jsonArray + jsonObject +) + +// jsonTypeSwitch returns the JSON type of the value +// held by the interface using a type switch statement. +func jsonTypeSwitch(i interface{}) jsonValueType { + switch i.(type) { + case nil: + return jsonNull + case string: + return jsonString + case bool: + return jsonBoolean + case float64: + return jsonNumberFloat + case json.Number: + return jsonNumberString + case []interface{}: + return jsonArray + case map[string]interface{}: + return jsonObject + default: + return jsonInvalid + } +} + +// areComparable returns whether the interface values +// i1 and i2 can be compared. The values are comparable +// only if they are both non-nil and share the same kind. +func areComparable(i1, i2 interface{}) bool { + return jsonTypeSwitch(i1) == jsonTypeSwitch(i2) +} + +func deepEqual(src, tgt interface{}) bool { + if src == nil && tgt == nil { + // Fast path. + return true + } + return deepEqualValue(src, tgt) +} + +func deepEqualValue(src, tgt interface{}) bool { + typ := jsonTypeSwitch(src) + if typ != jsonTypeSwitch(tgt) { + return false + } + switch typ { + case jsonNull: + return true + case jsonString: + return src.(string) == tgt.(string) + case jsonBoolean: + return src.(bool) == tgt.(bool) + case jsonNumberFloat: + return src.(float64) == tgt.(float64) + case jsonNumberString: + return src.(json.Number) == tgt.(json.Number) + case jsonArray: + oarr := src.([]interface{}) + narr := tgt.([]interface{}) + + if len(oarr) != len(narr) { + return false + } + for i := 0; i < len(oarr); i++ { + if !deepEqual(oarr[i], narr[i]) { + return false + } + } + return true + case jsonObject: + oobj := src.(map[string]interface{}) + nobj := tgt.(map[string]interface{}) + + if len(oobj) != len(nobj) { + return false + } + for k, v1 := range oobj { + v2, ok := nobj[k] + if !ok { + // Key not found in target. + return false + } + if !deepEqual(v1, v2) { + return false + } + } + return true + default: + panic("invalid json type") + } +} + +var jsonTypeNames = []string{ + jsonInvalid: "Invalid", + jsonBoolean: "Boolean", + jsonNumberFloat: "Number", + jsonNumberString: "json.Number", + jsonString: "String", + jsonNull: "Null", + jsonObject: "Object", + jsonArray: "Array", +} + +// String implements fmt.Stringer for jsonValueType. +func (t jsonValueType) String() string { + if uint(t) < uint(len(jsonTypeNames)) { + return jsonTypeNames[t] + } + return "type" + strconv.Itoa(int(t)) +} diff --git a/vendor/github.com/wI2L/jsondiff/hash.go b/vendor/github.com/wI2L/jsondiff/hash.go new file mode 100644 index 000000000..14d2c0cca --- /dev/null +++ b/vendor/github.com/wI2L/jsondiff/hash.go @@ -0,0 +1,55 @@ +package jsondiff + +import ( + "encoding/binary" + "hash/maphash" + "math" +) + +type hasher struct { + mh maphash.Hash +} + +func (h *hasher) digest(val interface{}) uint64 { + h.mh.Reset() + h.hash(val) + + return h.mh.Sum64() +} + +func (h *hasher) hash(i interface{}) { + switch v := i.(type) { + case string: + _, _ = h.mh.WriteString(v) + case bool: + if v { + _ = h.mh.WriteByte('1') + } else { + _ = h.mh.WriteByte('0') + } + case float64: + var buf [8]byte + binary.BigEndian.PutUint64(buf[:], math.Float64bits(v)) + _, _ = h.mh.Write(buf[:]) + case nil: + _ = h.mh.WriteByte('0') + case []interface{}: + for _, e := range v { + h.hash(e) + } + case map[string]interface{}: + keys := make([]string, 0, len(v)) + + // Extract keys first, and sort them + // in lexicographical order. + for k := range v { + keys = append(keys, k) + } + sortStrings(keys) + + for _, k := range keys { + _, _ = h.mh.WriteString(k) + h.hash(v[k]) + } + } +} diff --git a/vendor/github.com/wI2L/jsondiff/json.go b/vendor/github.com/wI2L/jsondiff/json.go new file mode 100644 index 000000000..6bf773f12 --- /dev/null +++ b/vendor/github.com/wI2L/jsondiff/json.go @@ -0,0 +1,161 @@ +package jsondiff + +// findKey finds and return the object value that match key. +// It assumes to be on an opening curly bracket. +// The function expects a compact JSON input. +func findKey(json string, key string) string { + for i := 1; i < len(json); { + ki := i + i = squashString(json, i) + k := json[ki+1 : i-1] + i++ // skip semicolon + vi := i + i = squashValue(json, i) + if key == k { + return json[vi:i] + } + i++ // skip comma + } + return "" +} + +// findIndex finds and return the array value at the given index. +// It assumes to be on opening square bracket. +// The function expects a compact JSON input. +func findIndex(json string, idx int) string { + for i, j := 1, 0; i < len(json); { + vi := i + i = squashValue(json, i) + if j == idx { + return json[vi:i] + } + i++ // skip comma + j++ // next elem index + } + return "" +} + +func squashValue(json string, i int) int { + switch json[i] { + case 't', 'n': // true, null + i += 4 + case 'f': // false + i += 5 + case '"': // string + i = squashString(json, i) + case '-', '+', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': // number + i = squashNumber(json, i) + case '[', '{': // array, object + i = squashObjectOrArray(json, i) + } + return i +} + +// squashNumber reads b until it encounter a character +// than can follow a properly formatted number. +func squashNumber(json string, i int) int { + i++ + for ; i < len(json); i++ { + if json[i] <= ' ' || json[i] == ',' || json[i] == ']' || json[i] == '}' { + break + } + } + return i +} + +// squashString reads b until it encounter a non-escaped +// double quote, which indicate the sep of the string. +func squashString(json string, i int) int { + i++ // assume to be on opening quote + for ; i < len(json); i++ { + if json[i] == '"' && json[i-1] != '\\' { + break + } + } + i++ // move to closing quote + return i +} + +// note: taken from https://github.com/tidwall/gjson +func squashObjectOrArray(json string, i int) int { + depth := 1 + i++ +L: + for ; i < len(json); i++ { + if json[i] >= '"' && json[i] <= '}' { + switch json[i] { + case '"': + i++ + s2 := i + for ; i < len(json); i++ { + if json[i] > '\\' { + continue + } + if json[i] == '"' { + if json[i-1] == '\\' { + n := 0 + for j := i - 2; j > s2-1; j-- { + if json[j] != '\\' { + break + } + n++ + } + if n%2 == 0 { + continue + } + } + break + } + } + case '{', '[': + depth++ + case '}', ']': + depth-- + if depth == 0 { + i++ + break L + } + } + } + } + return i +} + +// compact removes insignificant space characters from the +// input JSON byte slice and returns the compacted result. +func compact(json []byte) []byte { + b := make([]byte, 0, len(json)) + return _compact(json, b) +} + +// compactInPlace is similar to compact, but it reuses the input +// JSON buffer to avoid allocations. +func compactInPlace(json []byte) []byte { + return _compact(json, json) +} + +func _compact(src, dst []byte) []byte { + dst = dst[:0] + for i := 0; i < len(src); i++ { + if src[i] > ' ' { + dst = append(dst, src[i]) + if src[i] == '"' { + for i = i + 1; i < len(src); i++ { + dst = append(dst, src[i]) + if src[i] == '"' { + j := i - 1 + for ; ; j-- { + if src[j] != '\\' { + break + } + } + if (j-i)%2 != 0 { + break + } + } + } + } + } + } + return dst +} diff --git a/vendor/github.com/wI2L/jsondiff/operation.go b/vendor/github.com/wI2L/jsondiff/operation.go new file mode 100644 index 000000000..762dabc91 --- /dev/null +++ b/vendor/github.com/wI2L/jsondiff/operation.go @@ -0,0 +1,152 @@ +package jsondiff + +import ( + "encoding/json" + "strings" + "unsafe" +) + +// JSON Patch operation types. +// These are defined in RFC 6902 section 4. +// https://datatracker.ietf.org/doc/html/rfc6902#section-4 +const ( + OperationAdd = "add" + OperationReplace = "replace" + OperationRemove = "remove" + OperationMove = "move" + OperationCopy = "copy" + OperationTest = "test" +) + +const ( + fromFieldLen = len(`,"from":""`) + valueFieldLen = len(`,"value":`) + opBaseLen = len(`{"op":"","path":""}`) +) + +// null represents a JSON null value. +type null struct{} + +// Patch represents a series of JSON Patch operations. +type Patch []Operation + +// Operation represents a single JSON Patch (RFC6902) operation. +type Operation struct { + Value interface{} `json:"value,omitempty"` + OldValue interface{} `json:"-"` + Type string `json:"op"` + From string `json:"from,omitempty"` + Path string `json:"path"` + valueLen int +} + +// MarshalJSON implements the json.Marshaler interface. +func (null) MarshalJSON() ([]byte, error) { + return []byte("null"), nil +} + +// String implements the fmt.Stringer interface. +func (o Operation) String() string { + b, err := json.Marshal(o) + if err != nil { + return "" + } + return string(b) +} + +// MarshalJSON implements the json.Marshaler interface. +func (o Operation) MarshalJSON() ([]byte, error) { + type op Operation + + if !o.marshalWithValue() { + o.Value = nil + } else { + // Generic check that works for nil + // and typed nil interface values. + if (*[2]uintptr)(unsafe.Pointer(&o.Value))[1] == 0 { + o.Value = null{} + } + } + if !o.hasFrom() { + o.From = emptyPointer + } + return json.Marshal(op(o)) +} + +// jsonLength returns the length in bytes that the +// operation would occupy when marshaled to JSON. +func (o Operation) jsonLength() int { + l := opBaseLen + len(o.Type) + len(o.Path) + + if o.marshalWithValue() { + l += valueFieldLen + o.valueLen + } + if o.hasFrom() { + l += fromFieldLen + len(o.From) + } + return l +} + +func (o Operation) hasFrom() bool { + switch o.Type { + case OperationCopy, OperationMove: + return true + default: + return false + } +} + +func (o Operation) marshalWithValue() bool { + switch o.Type { + case OperationAdd, OperationReplace, OperationTest: + return true + default: + return false + } +} + +func (p *Patch) remove(idx int) Patch { + return (*p)[:idx+copy((*p)[idx:], (*p)[idx+1:])] +} + +func (p *Patch) append(typ string, from, path string, src, tgt interface{}, vl int) Patch { + return append(*p, Operation{ + Type: typ, + From: from, + Path: path, + OldValue: src, + Value: tgt, + valueLen: vl, + }) +} + +func (p *Patch) jsonLength() int { + if p == nil { + return 0 + } + var length int + for _, op := range *p { + length += op.jsonLength() + } + // Count comma-separators if the patch + // has more than one operation. + if len(*p) > 1 { + length += len(*p) - 1 + } + return length +} + +// String implements the fmt.Stringer interface. +func (p *Patch) String() string { + if p == nil || len(*p) == 0 { + return "" + } + sb := strings.Builder{} + for i, op := range *p { + if i != 0 { + sb.WriteByte('\n') + } + sb.WriteString(op.String()) + } + return sb.String() +} diff --git a/vendor/github.com/wI2L/jsondiff/option.go b/vendor/github.com/wI2L/jsondiff/option.go new file mode 100644 index 000000000..1ed140f3d --- /dev/null +++ b/vendor/github.com/wI2L/jsondiff/option.go @@ -0,0 +1,85 @@ +package jsondiff + +// An Option changes the default behavior of a Differ. +type Option func(*Differ) + +// Factorize enables factorization of operations. +func Factorize() Option { + return func(o *Differ) { o.opts.factorize = true } +} + +// Rationalize enables rationalization of operations. +func Rationalize() Option { + return func(o *Differ) { o.opts.rationalize = true } +} + +// Equivalent disables the generation of operations for +// arrays of equal length and unordered/equal elements. +func Equivalent() Option { + return func(o *Differ) { o.opts.equivalent = true } +} + +// Invertible enables the generation of an invertible +// patch, by preceding each remove and replace operation +// by a test operation that verifies the value at the +// path that is being removed/replaced. +// Note that copy operations are not invertible, and as +// such, using this option disable the usage of copy +// operation in favor of add operations. +func Invertible() Option { + return func(o *Differ) { o.opts.invertible = true } +} + +// MarshalFunc allows to define the function/package +// used to marshal objects to JSON. +// The prototype of fn must match the one of the +// encoding/json.Marshal function. +func MarshalFunc(fn marshalFunc) Option { + return func(o *Differ) { + o.opts.marshal = fn + } +} + +// UnmarshalFunc allows to define the function/package +// used to unmarshal objects from JSON. +// The prototype of fn must match the one of the +// encoding/json.Unmarshal function. +func UnmarshalFunc(fn unmarshalFunc) Option { + return func(o *Differ) { + o.opts.unmarshal = fn + } +} + +// SkipCompact instructs to skip the compaction of the input +// JSON documents when the Rationalize option is enabled. +func SkipCompact() Option { + return func(o *Differ) { + o.isCompact = true + } +} + +// InPlaceCompaction instructs to compact the input JSON +// documents in place; it does not allocate to create +// a copy, but modify the original byte slice. +// This option has no effect if used alongside SkipCompact. +func InPlaceCompaction() Option { + return func(o *Differ) { + o.compactInPlace = true + } +} + +// Ignores defines the list of values that are ignored +// by the diff generation, represented as a list of JSON +// Pointer strings (RFC 6901). +func Ignores(ptrs ...string) Option { + return func(o *Differ) { + if len(ptrs) == 0 { + return + } + o.opts.ignores = make(map[string]struct{}, len(ptrs)) + for _, ptr := range ptrs { + o.opts.ignores[ptr] = struct{}{} + } + o.opts.hasIgnore = true + } +} diff --git a/vendor/github.com/wI2L/jsondiff/pointer.go b/vendor/github.com/wI2L/jsondiff/pointer.go new file mode 100644 index 000000000..30fc13f30 --- /dev/null +++ b/vendor/github.com/wI2L/jsondiff/pointer.go @@ -0,0 +1,135 @@ +package jsondiff + +import ( + "errors" + "strconv" + "strings" + "unsafe" +) + +const ( + separator = '/' + escapeSlash = "~1" + escapeTilde = "~0" + emptyPointer = "" +) + +// rfc6901Escaper is a replacer that escapes a JSON Pointer string +// in compliance with the JavaScript Object Notation Pointer syntax. +// https://tools.ietf.org/html/rfc6901 +var rfc6901Escaper = strings.NewReplacer("~", escapeTilde, "/", escapeSlash) + +type segment struct { + key string + idx int +} + +// pointer represents an RFC 6901 JSON Pointer. +type pointer struct { + buf []byte + base segment + prev segment + sep int +} + +func (p *pointer) clone() pointer { + return *p +} + +func (p *pointer) copy() string { + return string(p.buf) +} + +func (p *pointer) string() string { + return *(*string)(unsafe.Pointer(&p.buf)) +} + +func (p *pointer) isRoot() bool { + return len(p.buf) == 0 +} + +func (p *pointer) appendKey(key string) { + p.buf = append(p.buf, separator) + p.base = segment{key: key} + p.appendEscapeKey(key) +} + +func (p *pointer) appendIndex(idx int) { + p.buf = append(p.buf, separator) + p.buf = strconv.AppendInt(p.buf, int64(idx), 10) + p.base = segment{idx: idx} +} + +func (p *pointer) snapshot() { + p.sep = len(p.buf) + p.prev = p.base +} + +func (p *pointer) rewind() { + p.buf = p.buf[:p.sep] + p.base = p.prev +} + +func (p *pointer) reset() { + p.buf = p.buf[:0] + p.sep = 0 +} + +func (p *pointer) appendEscapeKey(k string) { + for _, c := range []byte(k) { + switch c { + case '/': + p.buf = append(p.buf, escapeSlash...) + case '~': + p.buf = append(p.buf, escapeTilde...) + default: + p.buf = append(p.buf, c) + } + } +} + +var ( + errLeadingSlash = errors.New("no leading slash") + errIncompleteEscapeSequence = errors.New("incomplete escape sequence") + errInvalidEscapeSequence = errors.New("invalid escape sequence") +) + +func parsePointer(s string) ([]string, error) { + if s == "" { + return nil, nil + } + a := []rune(s) + + if len(a) > 0 && a[0] != '/' { + return nil, errLeadingSlash + } + var tokens []string + + ls := 0 + for i, r := range a { + if r == '/' { + if i != 0 { + tokens = append(tokens, string(a[ls+1:i])) + } + if i == len(a)-1 { + // Last char is a '/', next fragment is an empty string. + tokens = append(tokens, "") + break + } + ls = i + } else if r == '~' { + if i == len(a)-1 { + return nil, errIncompleteEscapeSequence + } + if a[i+1] != '0' && a[i+1] != '1' { + return nil, errInvalidEscapeSequence + } + } else { + if i == len(a)-1 { + // End of string, accumulate from last separator. + tokens = append(tokens, string(a[ls+1:])) + } + } + } + return tokens, nil +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 22bd3fe67..292738919 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -214,8 +214,6 @@ github.com/emirpasic/gods/lists/arraylist github.com/emirpasic/gods/trees github.com/emirpasic/gods/trees/binaryheap github.com/emirpasic/gods/utils -# github.com/evanphx/json-patch v0.5.2 -## explicit; go 1.14 # github.com/go-git/gcfg v1.5.0 ## explicit github.com/go-git/gcfg @@ -347,9 +345,6 @@ github.com/klauspost/compress/zstd/internal/xxhash # github.com/lucasb-eyer/go-colorful v1.2.0 ## explicit; go 1.12 github.com/lucasb-eyer/go-colorful -# github.com/mattbaird/jsonpatch v0.0.0-20230413205102-771768614e91 -## explicit -github.com/mattbaird/jsonpatch # github.com/mattn/go-isatty v0.0.19 ## explicit; go 1.15 github.com/mattn/go-isatty @@ -425,6 +420,9 @@ github.com/valyala/fasttemplate ## explicit; go 1.16 github.com/vektah/gqlparser/v2/ast github.com/vektah/gqlparser/v2/gqlerror +# github.com/wI2L/jsondiff v0.4.0 +## explicit; go 1.18 +github.com/wI2L/jsondiff # github.com/xanzy/ssh-agent v0.3.3 ## explicit; go 1.16 github.com/xanzy/ssh-agent