From 1a2b7e90bebaabfdefb9523f1a920542c335b0a8 Mon Sep 17 00:00:00 2001 From: "Matheus F. Prado" Date: Fri, 14 Feb 2025 23:41:55 -0300 Subject: [PATCH 1/6] Consider omitempty --- constants.go | 7 +++++ examples/partial_patches/main.go | 2 +- main.go | 33 +++++++++++++++++++++ options.go | 14 ++++++--- patch.go | 6 ++-- patch_test.go | 9 +++--- pointer.go | 49 +++++++++++++++++++++++-------- walker.go | 50 +++++++++++++++++++++++++++----- 8 files changed, 137 insertions(+), 33 deletions(-) create mode 100644 constants.go create mode 100644 main.go diff --git a/constants.go b/constants.go new file mode 100644 index 0000000..3744f22 --- /dev/null +++ b/constants.go @@ -0,0 +1,7 @@ +package jsonpatch + +const ( + separator = "/" + wildcard = "*" + tilde = "~" +) diff --git a/examples/partial_patches/main.go b/examples/partial_patches/main.go index e721eb7..d5f958f 100644 --- a/examples/partial_patches/main.go +++ b/examples/partial_patches/main.go @@ -29,6 +29,6 @@ func main() { {Position: "Software Engineer", Company: "Github"}, } - patch, _ := jsonpatch.CreateJSONPatch(updated, original.Jobs, jsonpatch.WithPrefix(jsonpatch.ParseJSONPointer("/jobs"))) + patch, _ := jsonpatch.CreateJSONPatch(updated, original.Jobs, jsonpatch.WithPrefix("/jobs")) fmt.Println(patch.String()) } diff --git a/main.go b/main.go new file mode 100644 index 0000000..13f7224 --- /dev/null +++ b/main.go @@ -0,0 +1,33 @@ +package jsonpatch + +type Complex struct { + String string `json:"string,omitempty"` + Bolean bool `json:"boolean"` + Float float64 `json:"float"` + Uint uint `json:"uint"` + Int int `json:"int"` + Slice []string `json:"slice"` + Map map[string]string `json:"map"` +} + +type Basic struct { + Name string `json:"name"` + Age int `json:"age"` + Complex Complex `json:"complex"` +} + +func main() { + // base := Basic{Name: "Matheus", Complex: Complex{ + // String: "a", + // Bolean: true, + // Float: float64(1), + // Uint: uint(1), + // Int: int(1), + // Slice: []string{"a"}, + // Map: map[string]string{"a": "a"}, + // }} + // modified := Basic{Name: "Matheus", Complex: Complex{}} + + // p, err := jsonpatch.CreateJSONPatch(modified, base) + // fmt.Println(p, err) +} diff --git a/options.go b/options.go index 2786fc4..1ebb5dd 100644 --- a/options.go +++ b/options.go @@ -1,5 +1,9 @@ package jsonpatch +import ( + "strings" +) + // Option allow to configure the walker instance type Option func(r *walker) @@ -18,12 +22,14 @@ func WithHandler(handler Handler) Option { } // WithPrefix is used to specify a prefix if only a sub part of JSON structure needs to be patched -func WithPrefix(prefix []string) Option { +func WithPrefix(prefix string) Option { + sPrefix := strings.Split(prefix, separator) + return func(w *walker) { - if len(prefix) > 0 && prefix[0] == "" { - w.prefix = append(w.prefix, prefix[1:]...) + if len(sPrefix) > 0 && sPrefix[0] == "" { + w.prefix = append(w.prefix, sPrefix[1:]...) } else { - w.prefix = append(w.prefix, prefix...) + w.prefix = append(w.prefix, sPrefix...) } } } diff --git a/patch.go b/patch.go index 67ed0e3..59a801a 100644 --- a/patch.go +++ b/patch.go @@ -58,7 +58,7 @@ func CreateJSONPatch(modified, current interface{}, options ...Option) (JSONPatc apply(w) } - if err := w.walk(reflect.ValueOf(modified), reflect.ValueOf(current), w.prefix); err != nil { + if err := w.walk(reflect.ValueOf(modified), reflect.ValueOf(current), NewWithPrefix(w.prefix)); err != nil { return JSONPatchList{}, err } @@ -88,7 +88,7 @@ func CreateThreeWayJSONPatch(modified, current, original interface{}, options .. } // compare modified with current and only keep addition and changes - if err := w.walk(reflect.ValueOf(modified), reflect.ValueOf(current), w.prefix); err != nil { + if err := w.walk(reflect.ValueOf(modified), reflect.ValueOf(current), NewWithPrefix(w.prefix)); err != nil { return JSONPatchList{}, err } for _, patch := range w.patchList { @@ -101,7 +101,7 @@ func CreateThreeWayJSONPatch(modified, current, original interface{}, options .. w.patchList = []JSONPatch{} // compare modified with original and only keep deletions - if err := w.walk(reflect.ValueOf(modified), reflect.ValueOf(original), w.prefix); err != nil { + if err := w.walk(reflect.ValueOf(modified), reflect.ValueOf(original), NewWithPrefix(w.prefix)); err != nil { return JSONPatchList{}, err } for _, patch := range w.patchList { diff --git a/patch_test.go b/patch_test.go index 8dd7b3e..54539c6 100644 --- a/patch_test.go +++ b/patch_test.go @@ -3,7 +3,6 @@ package jsonpatch_test import ( "encoding/json" "strconv" - "strings" "time" . "github.com/onsi/ginkgo/v2" @@ -393,7 +392,7 @@ var _ = Describe("JSONPatch", func() { }) Context("CreateJsonPatch_with_prefix", func() { It("empty prefix", func() { - testPatchWithExpected(G{B: &B{Bool: true, Str: "str"}}, G{}, G{B: &B{Bool: true, Str: "str"}}, jsonpatch.WithPrefix([]string{""})) + testPatchWithExpected(G{B: &B{Bool: true, Str: "str"}}, G{}, G{B: &B{Bool: true, Str: "str"}}, jsonpatch.WithPrefix("")) }) It("pointer prefix", func() { prefix := "/a/ptr" @@ -408,7 +407,7 @@ var _ = Describe("JSONPatch", func() { expectedJSON, err := json.Marshal(expected) Ω(err).ShouldNot(HaveOccurred()) - list, err := jsonpatch.CreateJSONPatch(modified.A.B, current.A.B, jsonpatch.WithPrefix(jsonpatch.ParseJSONPointer(prefix))) + list, err := jsonpatch.CreateJSONPatch(modified.A.B, current.A.B, jsonpatch.WithPrefix(prefix)) Ω(err).ShouldNot(HaveOccurred()) Ω(list.String()).ShouldNot(Equal("")) Ω(list.List()).Should(ContainElement(WithTransform(func(p jsonpatch.JSONPatch) string { return p.Path }, HavePrefix(prefix)))) @@ -419,7 +418,7 @@ var _ = Describe("JSONPatch", func() { Ω(patchedJSON).Should(MatchJSON(expectedJSON)) }) It("string prefix", func() { - prefix := []string{"b"} + prefix := "b" modified := G{B: &B{Bool: true, Str: "str"}} current := G{} expected := G{B: &B{Bool: true, Str: "str"}} @@ -434,7 +433,7 @@ var _ = Describe("JSONPatch", func() { list, err := jsonpatch.CreateJSONPatch(modified.B, current.B, jsonpatch.WithPrefix(prefix)) Ω(err).ShouldNot(HaveOccurred()) Ω(list.String()).ShouldNot(Equal("")) - Ω(list.List()).Should(ContainElement(WithTransform(func(p jsonpatch.JSONPatch) string { return p.Path }, HavePrefix("/"+strings.Join(prefix, "/"))))) + Ω(list.List()).Should(ContainElement(WithTransform(func(p jsonpatch.JSONPatch) string { return p.Path }, HavePrefix("/"+prefix+"/")))) jsonPatch, err := jsonpatch2.DecodePatch(list.Raw()) Ω(err).ShouldNot(HaveOccurred()) patchedJSON, err := jsonPatch.Apply(currentJSON) diff --git a/pointer.go b/pointer.go index 7027ce6..cb51652 100644 --- a/pointer.go +++ b/pointer.go @@ -1,33 +1,42 @@ package jsonpatch import ( + "slices" "strings" ) -const ( - separator = "/" - wildcard = "*" - tilde = "~" -) - // JSONPointer identifies a specific value within a JSON object specified in RFC 6901 -type JSONPointer []string +type JSONPointer struct { + path []string + tags []string +} + +func NewWithPrefix(prefix []string) JSONPointer { + return JSONPointer{ + path: prefix, + tags: []string{}, + } +} // ParseJSONPointer converts a string into a JSONPointer func ParseJSONPointer(str string) JSONPointer { - return strings.Split(str, separator) + return JSONPointer{ + path: strings.Split(str, separator), + tags: []string{}, + } } // String returns a string representation of a JSONPointer func (p JSONPointer) String() string { - return strings.Join(p, separator) + return strings.Join(p.path, separator) } // Add adds an element to the JSONPointer func (p JSONPointer) Add(elem string) JSONPointer { elem = strings.ReplaceAll(elem, tilde, "~0") elem = strings.ReplaceAll(elem, separator, "~1") - return append(p, elem) + p.path = append(p.path, elem) + return p } // Match matches a pattern which is a string JSONPointer which might also contains wildcards @@ -36,10 +45,26 @@ func (p JSONPointer) Match(pattern string) bool { for i, element := range elements { if element == wildcard { continue - } else if i >= len(p) || element != p[i] { + } else if i >= len(p.path) || element != p.path[i] { return false } } - return strings.HasSuffix(pattern, wildcard) || len(p) == len(elements) + return strings.HasSuffix(pattern, wildcard) || len(p.path) == len(elements) +} + +// AddTags override tags referencing the JSONPointer +func (p JSONPointer) AddTags(tags []string) JSONPointer { + p.tags = tags + return p +} + +// AddTags override tags referencing the JSONPointer +func (p JSONPointer) ShouldOmite() bool { + return slices.Contains(p.tags, "omitempty") +} + +// Prefix return a path as list to be used as prefix +func (p JSONPointer) Prefix() []string { + return p.path } diff --git a/walker.go b/walker.go index 6da556a..be382c8 100644 --- a/walker.go +++ b/walker.go @@ -1,6 +1,7 @@ package jsonpatch import ( + "errors" "fmt" "reflect" "sort" @@ -13,6 +14,8 @@ const ( jsonTag = "json" ) +var ErrShouldDelete = errors.New("must delete") + type walker struct { predicate Predicate handler Handler @@ -43,23 +46,43 @@ func (w *walker) walk(modified, current reflect.Value, pointer JSONPointer) erro return w.processString(modified, current, pointer) case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: if modified.Int() != current.Int() { - w.replace(pointer, modified.Int(), current.Int()) + if pointer.ShouldOmite() && modified.IsZero() { + w.remove(pointer, current.Int()) + } else { + w.replace(pointer, modified.Int(), current.Int()) + } } case reflect.Uint, reflect.Uintptr, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: if modified.Uint() != current.Uint() { - w.replace(pointer, modified.Uint(), current.Uint()) + if pointer.ShouldOmite() && modified.IsZero() { + w.remove(pointer, current.Uint()) + } else { + w.replace(pointer, modified.Uint(), current.Uint()) + } } case reflect.Float32: if modified.Float() != current.Float() { - w.replace(pointer, float32(modified.Float()), float32(current.Float())) + if pointer.ShouldOmite() && modified.IsZero() { + w.remove(pointer, float32(current.Float())) + } else { + w.replace(pointer, float32(modified.Float()), float32(current.Float())) + } } case reflect.Float64: if modified.Float() != current.Float() { - w.replace(pointer, modified.Float(), current.Float()) + if pointer.ShouldOmite() && modified.IsZero() { + w.remove(pointer, current.Float()) + } else { + w.replace(pointer, modified.Float(), current.Float()) + } } case reflect.Bool: if modified.Bool() != current.Bool() { - w.replace(pointer, modified.Bool(), current.Bool()) + if pointer.ShouldOmite() && modified.IsZero() { + w.remove(pointer, current.Bool()) + } else { + w.replace(pointer, modified.Bool(), current.Bool()) + } } case reflect.Invalid: // undefined interfaces are ignored for now @@ -84,7 +107,7 @@ func (w *walker) processInterface(modified reflect.Value, current reflect.Value, // processString processes reflect.String values func (w *walker) processString(modified reflect.Value, current reflect.Value, pointer JSONPointer) error { if modified.String() != current.String() { - if modified.String() == "" { + if pointer.ShouldOmite() && modified.String() == "" { w.remove(pointer, current.String()) } else if current.String() == "" { w.add(pointer, modified.String()) @@ -260,10 +283,19 @@ func (w *walker) processPtr(modified reflect.Value, current reflect.Value, point // processStruct processes reflect.Struct values func (w *walker) processStruct(modified, current reflect.Value, pointer JSONPointer) error { + if !w.predicate.Remove(pointer, current) { + return nil + } + if !w.predicate.Replace(pointer, modified.Interface(), current.Interface()) { return nil } + if pointer.ShouldOmite() && modified.IsZero() { + w.remove(pointer, current.Interface()) + return nil + } + if modified.Type().PkgPath() == "time" && modified.Type().Name() == "Time" { m, err := toTimeStrValue(modified) if err != nil { @@ -279,13 +311,15 @@ func (w *walker) processStruct(modified, current reflect.Value, pointer JSONPoin // process all struct fields, the order of the fields of the modified and current JSON object is identical because their types match for j := 0; j < modified.NumField(); j++ { - tag := strings.Split(modified.Type().Field(j).Tag.Get(jsonTag), ",")[0] + tags := strings.Split(modified.Type().Field(j).Tag.Get(jsonTag), ",") + tag := tags[0] if tag == "" || tag == "_" || !modified.Field(j).CanInterface() { // struct fields without a JSON tag set or unexported fields are ignored continue } + // process the child's value of the modified and current JSON in a next step - if err := w.walk(modified.Field(j), current.Field(j), pointer.Add(tag)); err != nil { + if err := w.walk(modified.Field(j), current.Field(j), pointer.Add(tag).AddTags(tags)); err != nil { return err } } From 7f6bf578b88d92b310851eea7d1a9cd1db339deb Mon Sep 17 00:00:00 2001 From: "Matheus F. Prado" Date: Fri, 14 Feb 2025 23:52:47 -0300 Subject: [PATCH 2/6] Removed main --- main.go | 33 --------------------------------- 1 file changed, 33 deletions(-) delete mode 100644 main.go diff --git a/main.go b/main.go deleted file mode 100644 index 13f7224..0000000 --- a/main.go +++ /dev/null @@ -1,33 +0,0 @@ -package jsonpatch - -type Complex struct { - String string `json:"string,omitempty"` - Bolean bool `json:"boolean"` - Float float64 `json:"float"` - Uint uint `json:"uint"` - Int int `json:"int"` - Slice []string `json:"slice"` - Map map[string]string `json:"map"` -} - -type Basic struct { - Name string `json:"name"` - Age int `json:"age"` - Complex Complex `json:"complex"` -} - -func main() { - // base := Basic{Name: "Matheus", Complex: Complex{ - // String: "a", - // Bolean: true, - // Float: float64(1), - // Uint: uint(1), - // Int: int(1), - // Slice: []string{"a"}, - // Map: map[string]string{"a": "a"}, - // }} - // modified := Basic{Name: "Matheus", Complex: Complex{}} - - // p, err := jsonpatch.CreateJSONPatch(modified, base) - // fmt.Println(p, err) -} From a75364786a81edc904675389df0f2a4a4de7567e Mon Sep 17 00:00:00 2001 From: "Matheus F. Prado" Date: Fri, 14 Feb 2025 23:54:16 -0300 Subject: [PATCH 3/6] Left over --- walker.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/walker.go b/walker.go index be382c8..61545d0 100644 --- a/walker.go +++ b/walker.go @@ -1,7 +1,6 @@ package jsonpatch import ( - "errors" "fmt" "reflect" "sort" @@ -14,8 +13,6 @@ const ( jsonTag = "json" ) -var ErrShouldDelete = errors.New("must delete") - type walker struct { predicate Predicate handler Handler From 86e79aceacc15c494c5cbab2f7736496e17518b8 Mon Sep 17 00:00:00 2001 From: "Matheus F. Prado" Date: Fri, 14 Feb 2025 23:54:23 -0300 Subject: [PATCH 4/6] Renamed method --- patch.go | 6 +++--- pointer.go | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/patch.go b/patch.go index 59a801a..4739647 100644 --- a/patch.go +++ b/patch.go @@ -58,7 +58,7 @@ func CreateJSONPatch(modified, current interface{}, options ...Option) (JSONPatc apply(w) } - if err := w.walk(reflect.ValueOf(modified), reflect.ValueOf(current), NewWithPrefix(w.prefix)); err != nil { + if err := w.walk(reflect.ValueOf(modified), reflect.ValueOf(current), NewJSONPointerWithPrefix(w.prefix)); err != nil { return JSONPatchList{}, err } @@ -88,7 +88,7 @@ func CreateThreeWayJSONPatch(modified, current, original interface{}, options .. } // compare modified with current and only keep addition and changes - if err := w.walk(reflect.ValueOf(modified), reflect.ValueOf(current), NewWithPrefix(w.prefix)); err != nil { + if err := w.walk(reflect.ValueOf(modified), reflect.ValueOf(current), NewJSONPointerWithPrefix(w.prefix)); err != nil { return JSONPatchList{}, err } for _, patch := range w.patchList { @@ -101,7 +101,7 @@ func CreateThreeWayJSONPatch(modified, current, original interface{}, options .. w.patchList = []JSONPatch{} // compare modified with original and only keep deletions - if err := w.walk(reflect.ValueOf(modified), reflect.ValueOf(original), NewWithPrefix(w.prefix)); err != nil { + if err := w.walk(reflect.ValueOf(modified), reflect.ValueOf(original), NewJSONPointerWithPrefix(w.prefix)); err != nil { return JSONPatchList{}, err } for _, patch := range w.patchList { diff --git a/pointer.go b/pointer.go index cb51652..b491d0f 100644 --- a/pointer.go +++ b/pointer.go @@ -11,7 +11,7 @@ type JSONPointer struct { tags []string } -func NewWithPrefix(prefix []string) JSONPointer { +func NewJSONPointerWithPrefix(prefix []string) JSONPointer { return JSONPointer{ path: prefix, tags: []string{}, From 6b153576cef55b591fdedb70e2d979a1b6b3d4b8 Mon Sep 17 00:00:00 2001 From: "Matheus F. Prado" Date: Fri, 14 Feb 2025 23:56:03 -0300 Subject: [PATCH 5/6] Unused method --- pointer.go | 5 ----- 1 file changed, 5 deletions(-) diff --git a/pointer.go b/pointer.go index b491d0f..7528489 100644 --- a/pointer.go +++ b/pointer.go @@ -63,8 +63,3 @@ func (p JSONPointer) AddTags(tags []string) JSONPointer { func (p JSONPointer) ShouldOmite() bool { return slices.Contains(p.tags, "omitempty") } - -// Prefix return a path as list to be used as prefix -func (p JSONPointer) Prefix() []string { - return p.path -} From 2e557026522f3f34eabb3561ce0b5176d03b9e34 Mon Sep 17 00:00:00 2001 From: "Matheus F. Prado" Date: Sat, 15 Feb 2025 01:07:32 -0300 Subject: [PATCH 6/6] Fix prefix --- patch.go | 10 +++++----- pointer.go | 4 ++-- walker.go | 17 ++++++++++++++++- 3 files changed, 23 insertions(+), 8 deletions(-) diff --git a/patch.go b/patch.go index 4739647..9be582c 100644 --- a/patch.go +++ b/patch.go @@ -50,7 +50,7 @@ func CreateJSONPatch(modified, current interface{}, options ...Option) (JSONPatc w := &walker{ handler: &DefaultHandler{}, predicate: Funcs{}, - prefix: []string{""}, + prefix: []string{}, } // apply options to the walker @@ -58,7 +58,7 @@ func CreateJSONPatch(modified, current interface{}, options ...Option) (JSONPatc apply(w) } - if err := w.walk(reflect.ValueOf(modified), reflect.ValueOf(current), NewJSONPointerWithPrefix(w.prefix)); err != nil { + if err := w.walk(reflect.ValueOf(modified), reflect.ValueOf(current), NewJSONPointerWithPrefix()); err != nil { return JSONPatchList{}, err } @@ -79,7 +79,7 @@ func CreateThreeWayJSONPatch(modified, current, original interface{}, options .. w := &walker{ handler: &DefaultHandler{}, predicate: Funcs{}, - prefix: []string{""}, + prefix: []string{}, } // apply options to the walker @@ -88,7 +88,7 @@ func CreateThreeWayJSONPatch(modified, current, original interface{}, options .. } // compare modified with current and only keep addition and changes - if err := w.walk(reflect.ValueOf(modified), reflect.ValueOf(current), NewJSONPointerWithPrefix(w.prefix)); err != nil { + if err := w.walk(reflect.ValueOf(modified), reflect.ValueOf(current), NewJSONPointerWithPrefix()); err != nil { return JSONPatchList{}, err } for _, patch := range w.patchList { @@ -101,7 +101,7 @@ func CreateThreeWayJSONPatch(modified, current, original interface{}, options .. w.patchList = []JSONPatch{} // compare modified with original and only keep deletions - if err := w.walk(reflect.ValueOf(modified), reflect.ValueOf(original), NewJSONPointerWithPrefix(w.prefix)); err != nil { + if err := w.walk(reflect.ValueOf(modified), reflect.ValueOf(original), NewJSONPointerWithPrefix()); err != nil { return JSONPatchList{}, err } for _, patch := range w.patchList { diff --git a/pointer.go b/pointer.go index 7528489..a7d6480 100644 --- a/pointer.go +++ b/pointer.go @@ -11,9 +11,9 @@ type JSONPointer struct { tags []string } -func NewJSONPointerWithPrefix(prefix []string) JSONPointer { +func NewJSONPointerWithPrefix() JSONPointer { return JSONPointer{ - path: prefix, + path: []string{}, tags: []string{}, } } diff --git a/walker.go b/walker.go index 61545d0..e67f3ec 100644 --- a/walker.go +++ b/walker.go @@ -310,7 +310,8 @@ func (w *walker) processStruct(modified, current reflect.Value, pointer JSONPoin for j := 0; j < modified.NumField(); j++ { tags := strings.Split(modified.Type().Field(j).Tag.Get(jsonTag), ",") tag := tags[0] - if tag == "" || tag == "_" || !modified.Field(j).CanInterface() { + + if tag == "" || tag == "_" || !modified.Field(j).CanInterface() || !w.checkPrefix(pointer, tag) { // struct fields without a JSON tag set or unexported fields are ignored continue } @@ -403,3 +404,17 @@ func (w *walker) remove(pointer JSONPointer, current interface{}) bool { return true } + +func (w *walker) checkPrefix(pointer JSONPointer, tag string) bool { + if len(w.prefix) > len(pointer.Add(tag).path) { + return w.prefix[0] == tag + } + + for i := range w.prefix { + if w.prefix[i] != pointer.Add(tag).path[i] { + return false + } + } + + return true +}