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/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..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), 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), 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), 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/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..a7d6480 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 NewJSONPointerWithPrefix() JSONPointer { + return JSONPointer{ + path: []string{}, + 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,21 @@ 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") } diff --git a/walker.go b/walker.go index 6da556a..e67f3ec 100644 --- a/walker.go +++ b/walker.go @@ -43,23 +43,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 +104,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 +280,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 +308,16 @@ 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] - if tag == "" || tag == "_" || !modified.Field(j).CanInterface() { + tags := strings.Split(modified.Type().Field(j).Tag.Get(jsonTag), ",") + tag := tags[0] + + if tag == "" || tag == "_" || !modified.Field(j).CanInterface() || !w.checkPrefix(pointer, tag) { // 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 } } @@ -372,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 +}