Skip to content

Consider omitempty #79

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions constants.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package jsonpatch

const (
separator = "/"
wildcard = "*"
tilde = "~"
)
2 changes: 1 addition & 1 deletion examples/partial_patches/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())
}
14 changes: 10 additions & 4 deletions options.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
package jsonpatch

import (
"strings"
)

// Option allow to configure the walker instance
type Option func(r *walker)

Expand All @@ -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...)
}
}
}
Expand Down
10 changes: 5 additions & 5 deletions patch.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,15 +50,15 @@ func CreateJSONPatch(modified, current interface{}, options ...Option) (JSONPatc
w := &walker{
handler: &DefaultHandler{},
predicate: Funcs{},
prefix: []string{""},
prefix: []string{},
}

// apply options to the walker
for _, apply := range options {
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
}

Expand All @@ -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
Expand All @@ -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 {
Expand All @@ -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 {
Expand Down
9 changes: 4 additions & 5 deletions patch_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import (
"encoding/json"
"strconv"
"strings"
"time"

. "github.com/onsi/ginkgo/v2"
Expand Down Expand Up @@ -87,9 +86,9 @@
I interface{} `json:"i"`
}

var _ = Describe("JSONPatch", func() {

Check failure on line 89 in patch_test.go

View workflow job for this annotation

GitHub Actions / lint

undefined: Describe (typecheck)
Context("CreateJsonPatch_pointer_values", func() {

Check failure on line 90 in patch_test.go

View workflow job for this annotation

GitHub Actions / lint

undefined: Context (typecheck)
It("pointer", func() {

Check failure on line 91 in patch_test.go

View workflow job for this annotation

GitHub Actions / lint

undefined: It (typecheck)
// add
testPatch(A{B: &B{Str: "test"}}, A{})
// remove
Expand All @@ -102,8 +101,8 @@
testPatch(A{}, A{})
})
})
Context("CreateJsonPatch_struct", func() {

Check failure on line 104 in patch_test.go

View workflow job for this annotation

GitHub Actions / lint

undefined: Context (typecheck)
It("pointer", func() {

Check failure on line 105 in patch_test.go

View workflow job for this annotation

GitHub Actions / lint

undefined: It (typecheck)
// add
testPatch(A{C: C{}}, A{})
// remove
Expand All @@ -114,8 +113,8 @@
testPatch(A{C: C{Str: "test2"}}, A{C: C{Str: "test2"}})
})
})
Context("CreateJsonPatch_data_type_values", func() {

Check failure on line 116 in patch_test.go

View workflow job for this annotation

GitHub Actions / lint

undefined: Context (typecheck)
It("string", func() {

Check failure on line 117 in patch_test.go

View workflow job for this annotation

GitHub Actions / lint

undefined: It (typecheck)
// add
testPatch(B{Str: "test"}, B{})
// remove
Expand Down Expand Up @@ -393,7 +392,7 @@
})
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"
Expand All @@ -408,7 +407,7 @@
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))))
Expand All @@ -419,7 +418,7 @@
Ω(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"}}
Expand All @@ -434,7 +433,7 @@
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)
Expand Down
44 changes: 32 additions & 12 deletions pointer.go
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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")
}
64 changes: 55 additions & 9 deletions walker.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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())
Expand Down Expand Up @@ -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 {
Expand All @@ -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
}
}
Expand Down Expand Up @@ -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
}
Loading