diff --git a/cmd/dozadiff/main.go b/cmd/dozadiff/main.go index 60216c1..485eefe 100644 --- a/cmd/dozadiff/main.go +++ b/cmd/dozadiff/main.go @@ -8,13 +8,13 @@ import ( "github.com/sanity-io/mendoza" ) -func readJson(jsonPath string) (interface{}, error) { +func readJson(jsonPath string) (any, error) { jsonFile, err := os.Open(jsonPath) if err != nil { return nil, err } decoder := json.NewDecoder(jsonFile) - var doc interface{} + var doc any err = decoder.Decode(&doc) if err != nil { return nil, err diff --git a/cmd/dozapatch/main.go b/cmd/dozapatch/main.go index 70e85f9..6016ebf 100644 --- a/cmd/dozapatch/main.go +++ b/cmd/dozapatch/main.go @@ -8,7 +8,7 @@ import ( "github.com/sanity-io/mendoza" ) -func readJson(jsonPath string, data interface{}) ( error) { +func readJson(jsonPath string, data any) error { jsonFile, err := os.Open(jsonPath) if err != nil { return err @@ -18,7 +18,7 @@ func readJson(jsonPath string, data interface{}) ( error) { } func run(originalPath, patchPath string) error { - var original interface{} + var original any if err := readJson(originalPath, &original); err != nil { return err } @@ -28,7 +28,10 @@ func run(originalPath, patchPath string) error { return err } - result := mendoza.ApplyPatch(original, patch) + result, err := mendoza.ApplyPatch(original, patch) + if err != nil { + return err + } encoder := json.NewEncoder(os.Stdout) if err := encoder.Encode(result); err != nil { diff --git a/convert_test.go b/convert_test.go index afc84a2..eb045ae 100644 --- a/convert_test.go +++ b/convert_test.go @@ -7,11 +7,11 @@ import ( ) type CustomObject struct { - attrs map[string]interface{} + attrs map[string]any } func TestConvertObject(t *testing.T) { - opts := mendoza.DefaultOptions.WithConvertFunc(func(value interface{}) interface{} { + opts := mendoza.DefaultOptions.WithConvertFunc(func(value any) any { if value, ok := value.(CustomObject); ok { return value.attrs } @@ -22,13 +22,13 @@ func TestConvertObject(t *testing.T) { }) customLeft := CustomObject{ - attrs: map[string]interface{}{ + attrs: map[string]any{ "a": "abcdefgh", }, } customRight := CustomObject{ - attrs: map[string]interface{}{ + attrs: map[string]any{ "a": "abcdefgh", "b": 123.0, }, @@ -42,29 +42,31 @@ func TestConvertObject(t *testing.T) { patch, err := opts.CreatePatch(left, right) require.NoError(t, err) - newRight := opts.ApplyPatch(left, patch) + newRight, err := opts.ApplyPatch(left, patch) + require.NoError(t, err) require.EqualValues(t, result, newRight) }) t.Run("Nested", func(t *testing.T) { - left := map[string]interface{}{"a": customLeft} - right := map[string]interface{}{"a": customRight} - result := map[string]interface{}{"a": customRight.attrs} + left := map[string]any{"a": customLeft} + right := map[string]any{"a": customRight} + result := map[string]any{"a": customRight.attrs} patch, err := opts.CreatePatch(left, right) require.NoError(t, err) - newRight := opts.ApplyPatch(left, patch) + newRight, err := opts.ApplyPatch(left, patch) + require.NoError(t, err) require.EqualValues(t, result, newRight) }) } type CustomArray struct { - values []interface{} + values []any } func TestConvertArray(t *testing.T) { - opts := mendoza.DefaultOptions.WithConvertFunc(func(value interface{}) interface{} { + opts := mendoza.DefaultOptions.WithConvertFunc(func(value any) any { if value, ok := value.(CustomArray); ok { return value.values } @@ -72,13 +74,13 @@ func TestConvertArray(t *testing.T) { }) customLeft := CustomArray{ - []interface{}{map[string]interface{}{ + []any{map[string]any{ "a": "abcdefgh", }}, } customRight := CustomArray{ - []interface{}{map[string]interface{}{ + []any{map[string]any{ "a": "abcdefgh", "b": 123.0, }}, @@ -92,19 +94,21 @@ func TestConvertArray(t *testing.T) { patch, err := opts.CreatePatch(left, right) require.NoError(t, err) - newRight := opts.ApplyPatch(left, patch) + newRight, err := opts.ApplyPatch(left, patch) + require.NoError(t, err) require.EqualValues(t, result, newRight) }) t.Run("Nested", func(t *testing.T) { - left := map[string]interface{}{"a": customLeft} - right := map[string]interface{}{"a": customRight} - result := map[string]interface{}{"a": customRight.values} + left := map[string]any{"a": customLeft} + right := map[string]any{"a": customRight} + result := map[string]any{"a": customRight.values} patch, err := opts.CreatePatch(left, right) require.NoError(t, err) - newRight := opts.ApplyPatch(left, patch) + newRight, err := opts.ApplyPatch(left, patch) + require.NoError(t, err) require.EqualValues(t, result, newRight) }) } diff --git a/differ.go b/differ.go index e6e9571..8c08b67 100644 --- a/differ.go +++ b/differ.go @@ -15,7 +15,7 @@ type differ struct { // Creates a patch which can be applied to the left document to produce the right document. // // This function uses the default options. -func CreatePatch(left, right interface{}) (Patch, error) { +func CreatePatch(left, right any) (Patch, error) { return DefaultOptions.CreatePatch(left, right) } @@ -23,12 +23,12 @@ func CreatePatch(left, right interface{}) (Patch, error) { // the second can be applied to the right document to produce the left document. // // This function uses the default options. -func CreateDoublePatch(left, right interface{}) (Patch, Patch, error) { +func CreateDoublePatch(left, right any) (Patch, Patch, error) { return DefaultOptions.CreateDoublePatch(left, right) } // Creates a patch which can be applied to the left document to produce the right document. -func (options *Options) CreatePatch(left, right interface{}) (Patch, error) { +func (options *Options) CreatePatch(left, right any) (Patch, error) { if left == nil { if right == nil { return Patch{}, nil @@ -56,7 +56,7 @@ func (options *Options) CreatePatch(left, right interface{}) (Patch, error) { // Creates two patches: The first can be applied to the left document to produce the right document, // the second can be applied to the right document to produce the left document. -func (options *Options) CreateDoublePatch(left, right interface{}) (Patch, Patch, error) { +func (options *Options) CreateDoublePatch(left, right any) (Patch, Patch, error) { if left == nil && right == nil { return Patch{}, Patch{}, nil } diff --git a/format.go b/format.go index 6be556e..da7f239 100644 --- a/format.go +++ b/format.go @@ -11,7 +11,7 @@ type Writer interface { WriteUint8(v uint8) error WriteUint(v int) error WriteString(v string) error - WriteValue(v interface{}) error + WriteValue(v any) error } // Reader is an interface for reading values. This can be used for supporting a custom serialization format. @@ -19,11 +19,11 @@ type Reader interface { ReadUint8() (uint8, error) ReadUint() (int, error) ReadString() (string, error) - ReadValue() (interface{}, error) + ReadValue() (any, error) } type ValueReader interface { - ReadValue() (interface{}, error) + ReadValue() (any, error) } // Note: This code is intentionally very verbose/repetitive in order to be forward compatible. diff --git a/go.mod b/go.mod index 5d792ae..a9afd24 100644 --- a/go.mod +++ b/go.mod @@ -1,10 +1,18 @@ module github.com/sanity-io/mendoza -go 1.13 +go 1.25 require ( - github.com/stretchr/objx v0.2.0 // indirect github.com/stretchr/testify v1.3.0 github.com/vmihailenco/msgpack/v4 v4.3.5 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a ) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/golang/protobuf v1.3.2 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/vmihailenco/tagparser v0.1.1 // indirect + golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553 // indirect + google.golang.org/appengine v1.6.5 // indirect +) diff --git a/go.sum b/go.sum index 4849401..4a617aa 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,3 @@ -github.com/davecgh/go-spew v0.0.0-20161028175848-04cdfd42973b h1:XxMZvQZtTXpWMNWK82vdjCLCe7uGMFXdTsJH0v3Hkvw= -github.com/davecgh/go-spew v0.0.0-20161028175848-04cdfd42973b/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -8,26 +6,18 @@ github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pty v1.1.1 h1:VkoXIwSboBpnk99O/KFauAEILuNHv5DVFKZMBN/gUgw= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0 h1:GD+A8+e+wFkqje55/2fOVnZPkoDIu1VooBWfNrnY8Uo= -github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48= -github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= -github.com/stretchr/testify v0.0.0-20161117074351-18a02ba4a312 h1:UsFdQ3ZmlzS0BqZYGxvYaXvFGUbCmPGy8DM7qWJJiIQ= -github.com/stretchr/testify v0.0.0-20161117074351-18a02ba4a312/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/vmihailenco/msgpack/v4 v4.3.5 h1:UBGCmLC4h5pe4sMyL3E8fqrpCTtbLesLH5mHb/0xC2M= github.com/vmihailenco/msgpack/v4 v4.3.5/go.mod h1:DuaveEe48abshDmz5UBKyZ+yDugvaeFk5ayfrewUOaw= github.com/vmihailenco/tagparser v0.1.1 h1:quXMXlA39OCbd2wAdTsGDlK9RkOk6Wuw+x37wVyIuWY= github.com/vmihailenco/tagparser v0.1.1/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553 h1:efeOvDhwQ29Dj3SdAV/MJf8oukgn+8D8WgaCaRMchF8= @@ -35,9 +25,7 @@ golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e h1:FDhOuMEY4JVRztM/gsbk+IKUQ8kj74bxZrgw87eMMVc= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= google.golang.org/appengine v1.6.5 h1:tycE03LOZYQNhDpS27tcQdAzLCVMaj7QT2SXxebnpCM= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= diff --git a/internal/fuzz/fuzz.go b/internal/fuzz/fuzz.go index be9f12a..6f8809a 100644 --- a/internal/fuzz/fuzz.go +++ b/internal/fuzz/fuzz.go @@ -65,12 +65,18 @@ func Fuzz(data []byte) int { panic(err) } - constructedRight := mendoza.ApplyPatch(left, patch1) + constructedRight, err := mendoza.ApplyPatch(left, patch1) + if err != nil { + panic(err) + } if !reflect.DeepEqual(right, constructedRight) { panic("up patch is incorrect") } - constructedLeft := mendoza.ApplyPatch(right, patch2) + constructedLeft, err := mendoza.ApplyPatch(right, patch2) + if err != nil { + panic(err) + } if !reflect.DeepEqual(left, constructedLeft) { panic("down patch is incorrect") } diff --git a/json.go b/json.go index 99a12d0..968e97a 100644 --- a/json.go +++ b/json.go @@ -24,7 +24,7 @@ func (w *jsonWriter) WriteString(v string) error { return w.WriteValue(v) } -func (w *jsonWriter) WriteValue(v interface{}) error { +func (w *jsonWriter) WriteValue(v any) error { w.next() b, err := json.Marshal(v) if err != nil { @@ -83,12 +83,12 @@ func (r *jsonReader) ReadString() (string, error) { return ReadStringFromValueReader(r) } -func (r *jsonReader) ReadValue() (interface{}, error) { +func (r *jsonReader) ReadValue() (any, error) { err := r.tryEof() if err != nil { return nil, err } - var val interface{} + var val any err = r.dec.Decode(&val) if err != nil { return nil, err @@ -110,8 +110,8 @@ func (r *jsonReader) expectArray() error { } type jsonValueReader struct { - data []interface{} - idx int + data []any + idx int } func (r *jsonValueReader) ReadUint8() (uint8, error) { @@ -126,7 +126,7 @@ func (r *jsonValueReader) ReadString() (string, error) { return ReadStringFromValueReader(r) } -func (r *jsonValueReader) ReadValue() (interface{}, error) { +func (r *jsonValueReader) ReadValue() (any, error) { if r.idx >= len(r.data) { return nil, io.EOF } @@ -158,7 +158,7 @@ func (patch *Patch) UnmarshalJSON(data []byte) error { } // DecodeJSON decodes a patch from an []interface{} as parsed by encoding/json. -func (patch *Patch) DecodeJSON(data []interface{}) error { +func (patch *Patch) DecodeJSON(data []any) error { r := jsonValueReader{data: data} return patch.ReadFrom(&r) } diff --git a/ops.go b/ops.go index 92ccaf1..1e69c94 100644 --- a/ops.go +++ b/ops.go @@ -6,25 +6,32 @@ // The Patch type is already JSON serializable, but you can implement Reader/Writer // (and use WriteTo/ReadFrom) if you need a custom serialization. // -// Supported types +// # Supported types // // The differ/patcher is only implemented to work on the following types: -// bool -// float64 -// string -// map[string]interface{} -// []interface{} -// nil +// +// bool +// float64 +// string +// map[string]interface{} +// []interface{} +// nil // // If you need to support additional types you can use the option WithConvertFunc which // defines a function that is applied to every value. package mendoza +import "errors" + +// ErrInvalidPatch is returned when a patch cannot be applied to a document, +// typically because the document doesn't match the expected structure. +var ErrInvalidPatch = errors.New("invalid patch: document structure does not match patch expectations") + //go-sumtype:decl Op // Op is the interface for an operation. type Op interface { - applyTo(p *patcher) + applyTo(p *patcher) error readParams(r Reader) error writeParams(w Writer) error } @@ -32,11 +39,10 @@ type Op interface { // A patch is a list of operations. type Patch []Op - // Output stack operators type OpValue struct { - Value interface{} + Value any } type OpCopy struct { @@ -55,7 +61,6 @@ type OpReturnIntoObjectSameKey struct { type OpReturnIntoArray struct { } - // Input stack operators type OpPushField struct { @@ -124,7 +129,6 @@ type OpObjectCopyField struct { OpPop } -// type OpObjectDeleteField struct { Index int } @@ -132,7 +136,7 @@ type OpObjectDeleteField struct { // Array helpers type OpArrayAppendValue struct { - Value interface{} + Value any } type OpArrayAppendSlice struct { diff --git a/options.go b/options.go index c816420..6c14530 100644 --- a/options.go +++ b/options.go @@ -1,7 +1,7 @@ package mendoza type Options struct { - convertFunc func(value interface{}) interface{} + convertFunc func(value any) any } // The default options. @@ -11,7 +11,7 @@ var DefaultOptions = Options{} // // The convert function is applied by CreatePatch and ApplyPatch to every value it looks at. // This can be used to support additional types by converting it into one of the supported types. -func (options Options) WithConvertFunc(convertFunc func(value interface{}) interface{}) Options { +func (options Options) WithConvertFunc(convertFunc func(value any) any) Options { options.convertFunc = convertFunc return options } diff --git a/patcher.go b/patcher.go index ab5f166..354c918 100644 --- a/patcher.go +++ b/patcher.go @@ -1,47 +1,56 @@ package mendoza import ( + "maps" "sort" ) type outputEntry struct { - source interface{} - writableArray []interface{} - writableObject map[string]interface{} + source any + writableArray []any + writableObject map[string]any writableString string } type inputEntry struct { key string - value interface{} + value any fields []fieldEntry } type fieldEntry struct { key string - value interface{} + value any } type patcher struct { - root interface{} + root any inputStack []inputEntry outputStack []outputEntry options *Options } -// Applies a patch to a document. Note that this method can panic if -// the document is not the same that was used to produce the patch. +// Applies a patch to a document. Returns an error if the patch +// cannot be applied (e.g., the document structure doesn't match). // // This function uses the default options. -func ApplyPatch(root interface{}, patch Patch) interface{} { +func ApplyPatch(root any, patch Patch) (any, error) { return DefaultOptions.ApplyPatch(root, patch) } -// Applies a patch to a document. Note that this method can panic if -// the document is not the same that was used to produce the patch. -func (options *Options) ApplyPatch(root interface{}, patch Patch) interface{} { +// MustApplyPatch applies a patch to a document. It panics if the patch +// cannot be applied (e.g., the document structure doesn't match). +// +// This function uses the default options. +func MustApplyPatch(root any, patch Patch) any { + return DefaultOptions.MustApplyPatch(root, patch) +} + +// Applies a patch to a document. Returns an error if the patch +// cannot be applied (e.g., the document structure doesn't match). +func (options *Options) ApplyPatch(root any, patch Patch) (any, error) { if len(patch) == 0 { - return root + return root, nil } if options.convertFunc != nil { @@ -55,10 +64,22 @@ func (options *Options) ApplyPatch(root interface{}, patch Patch) interface{} { } for _, op := range patch { - op.applyTo(&p) + if err := op.applyTo(&p); err != nil { + return nil, err + } } - return p.result() + return p.result(), nil +} + +// MustApplyPatch applies a patch to a document. It panics if the patch +// cannot be applied (e.g., the document structure doesn't match). +func (options *Options) MustApplyPatch(root any, patch Patch) any { + result, err := options.ApplyPatch(root, patch) + if err != nil { + panic(err) + } + return result } func (patcher *patcher) popInput() { @@ -81,7 +102,7 @@ func (patcher *patcher) outputEntry() *outputEntry { return &patcher.outputStack[len(patcher.outputStack)-1] } -func (entry *outputEntry) result() interface{} { +func (entry *outputEntry) result() any { if entry.writableObject != nil { return entry.writableObject } @@ -97,10 +118,13 @@ func (entry *outputEntry) result() interface{} { return entry.source } -func (entry *inputEntry) getField(idx int) fieldEntry { +func (entry *inputEntry) getField(idx int) (fieldEntry, error) { if entry.fields == nil { + obj, ok := entry.value.(map[string]any) + if !ok { + return fieldEntry{}, ErrInvalidPatch + } fields := []fieldEntry{} - obj := entry.value.(map[string]interface{}) keys := []string{} for key := range obj { keys = append(keys, key) @@ -116,114 +140,155 @@ func (entry *inputEntry) getField(idx int) fieldEntry { entry.fields = fields } - return entry.fields[idx] + if idx < 0 || idx >= len(entry.fields) { + return fieldEntry{}, ErrInvalidPatch + } + + return entry.fields[idx], nil } -func (patcher *patcher) inputObject() map[string]interface{} { - return patcher.inputEntry().value.(map[string]interface{}) +func (patcher *patcher) inputObject() (map[string]any, error) { + obj, ok := patcher.inputEntry().value.(map[string]any) + if !ok { + return nil, ErrInvalidPatch + } + return obj, nil } -func (patcher *patcher) inputArray() []interface{} { - return patcher.inputEntry().value.([]interface{}) +func (patcher *patcher) inputArray() ([]any, error) { + arr, ok := patcher.inputEntry().value.([]any) + if !ok { + return nil, ErrInvalidPatch + } + return arr, nil } -func (patcher *patcher) inputString() string { - return patcher.inputEntry().value.(string) +func (patcher *patcher) inputString() (string, error) { + str, ok := patcher.inputEntry().value.(string) + if !ok { + return "", ErrInvalidPatch + } + return str, nil } -func (patcher *patcher) result() interface{} { +func (patcher *patcher) result() any { entry := patcher.outputStack[len(patcher.outputStack)-1] return entry.result() } -func (patcher *patcher) outputObject() map[string]interface{} { +func (patcher *patcher) outputObject() (map[string]any, error) { entry := &patcher.outputStack[len(patcher.outputStack)-1] if entry.writableObject == nil { if entry.source == nil { - entry.writableObject = make(map[string]interface{}) + entry.writableObject = make(map[string]any) } else { - src := entry.source.(map[string]interface{}) - obj := make(map[string]interface{}, len(src)) - - for k, v := range src { - obj[k] = v + src, ok := entry.source.(map[string]any) + if !ok { + return nil, ErrInvalidPatch } + obj := make(map[string]any, len(src)) + + maps.Copy(obj, src) entry.writableObject = obj } } - return entry.writableObject + return entry.writableObject, nil } -func (patcher *patcher) outputArray() *[]interface{} { +func (patcher *patcher) outputArray() (*[]any, error) { entry := &patcher.outputStack[len(patcher.outputStack)-1] if entry.source != nil { - src := entry.source.([]interface{}) - entry.writableArray = make([]interface{}, len(src)) + src, ok := entry.source.([]any) + if !ok { + return nil, ErrInvalidPatch + } + entry.writableArray = make([]any, len(src)) copy(entry.writableArray, src) entry.source = nil } - return &entry.writableArray + return &entry.writableArray, nil } -func (patcher *patcher) outputString() *string { +func (patcher *patcher) outputString() (*string, error) { entry := &patcher.outputStack[len(patcher.outputStack)-1] if entry.source != nil { - src := entry.source.(string) + src, ok := entry.source.(string) + if !ok { + return nil, ErrInvalidPatch + } entry.writableString = src entry.source = nil } - return &entry.writableString + return &entry.writableString, nil } -func (op OpValue) applyTo(p *patcher) { +func (op OpValue) applyTo(p *patcher) error { p.outputStack = append(p.outputStack, outputEntry{ source: op.Value, }) + return nil } -func (op OpCopy) applyTo(p *patcher) { +func (op OpCopy) applyTo(p *patcher) error { input := p.inputEntry() p.outputStack = append(p.outputStack, outputEntry{ source: input.value, }) + return nil } -func (op OpBlank) applyTo(p *patcher) { +func (op OpBlank) applyTo(p *patcher) error { p.outputStack = append(p.outputStack, outputEntry{ source: nil, }) + return nil } -func (op OpReturnIntoObject) applyTo(p *patcher) { +func (op OpReturnIntoObject) applyTo(p *patcher) error { result := p.outputEntry().result() p.popOutput() - obj := p.outputObject() + obj, err := p.outputObject() + if err != nil { + return err + } obj[op.Key] = result + return nil } -func (op OpReturnIntoObjectSameKey) applyTo(p *patcher) { +func (op OpReturnIntoObjectSameKey) applyTo(p *patcher) error { key := p.inputEntry().key result := p.outputEntry().result() p.popOutput() - obj := p.outputObject() + obj, err := p.outputObject() + if err != nil { + return err + } obj[key] = result + return nil } -func (op OpReturnIntoArray) applyTo(p *patcher) { +func (op OpReturnIntoArray) applyTo(p *patcher) error { result := p.outputEntry().result() p.popOutput() - arr := p.outputArray() + arr, err := p.outputArray() + if err != nil { + return err + } *arr = append(*arr, result) + return nil } -func (op OpPushField) applyTo(p *patcher) { - field := p.inputEntry().getField(op.Index) +func (op OpPushField) applyTo(p *patcher) error { + field, err := p.inputEntry().getField(op.Index) + if err != nil { + return err + } value := field.value if p.options.convertFunc != nil { value = p.options.convertFunc(value) @@ -232,99 +297,170 @@ func (op OpPushField) applyTo(p *patcher) { key: field.key, value: value, }) + return nil } -func (op OpPushElement) applyTo(p *patcher) { - value := p.inputArray()[op.Index] +func (op OpPushElement) applyTo(p *patcher) error { + arr, err := p.inputArray() + if err != nil { + return err + } + if op.Index < 0 || op.Index >= len(arr) { + return ErrInvalidPatch + } + value := arr[op.Index] if p.options.convertFunc != nil { value = p.options.convertFunc(value) } p.inputStack = append(p.inputStack, inputEntry{ value: value, }) + return nil } -func (op OpPushParent) applyTo(p *patcher) { +func (op OpPushParent) applyTo(p *patcher) error { idx := len(p.inputStack) - 2 - op.N + if idx < 0 || idx >= len(p.inputStack) { + return ErrInvalidPatch + } entry := p.inputStack[idx] p.inputStack = append(p.inputStack, entry) + return nil } -func (op OpPop) applyTo(p *patcher) { +func (op OpPop) applyTo(p *patcher) error { p.popInput() + return nil } -func (op OpPushFieldCopy) applyTo(p *patcher) { - op.OpPushField.applyTo(p) - op.OpCopy.applyTo(p) +func (op OpPushFieldCopy) applyTo(p *patcher) error { + if err := op.OpPushField.applyTo(p); err != nil { + return err + } + return op.OpCopy.applyTo(p) } -func (op OpPushFieldBlank) applyTo(p *patcher) { - op.OpPushField.applyTo(p) - op.OpBlank.applyTo(p) +func (op OpPushFieldBlank) applyTo(p *patcher) error { + if err := op.OpPushField.applyTo(p); err != nil { + return err + } + return op.OpBlank.applyTo(p) } -func (op OpPushElementCopy) applyTo(p *patcher) { - op.OpPushElement.applyTo(p) - op.OpCopy.applyTo(p) +func (op OpPushElementCopy) applyTo(p *patcher) error { + if err := op.OpPushElement.applyTo(p); err != nil { + return err + } + return op.OpCopy.applyTo(p) } -func (op OpPushElementBlank) applyTo(p *patcher) { - op.OpPushElement.applyTo(p) - op.OpBlank.applyTo(p) +func (op OpPushElementBlank) applyTo(p *patcher) error { + if err := op.OpPushElement.applyTo(p); err != nil { + return err + } + return op.OpBlank.applyTo(p) } -func (op OpReturnIntoObjectPop) applyTo(p *patcher) { - op.OpReturnIntoObject.applyTo(p) - op.OpPop.applyTo(p) +func (op OpReturnIntoObjectPop) applyTo(p *patcher) error { + if err := op.OpReturnIntoObject.applyTo(p); err != nil { + return err + } + return op.OpPop.applyTo(p) } -func (op OpReturnIntoObjectSameKeyPop) applyTo(p *patcher) { - op.OpReturnIntoObjectSameKey.applyTo(p) - op.OpPop.applyTo(p) +func (op OpReturnIntoObjectSameKeyPop) applyTo(p *patcher) error { + if err := op.OpReturnIntoObjectSameKey.applyTo(p); err != nil { + return err + } + return op.OpPop.applyTo(p) } -func (op OpReturnIntoArrayPop) applyTo(p *patcher) { - op.OpReturnIntoArray.applyTo(p) - op.OpPop.applyTo(p) +func (op OpReturnIntoArrayPop) applyTo(p *patcher) error { + if err := op.OpReturnIntoArray.applyTo(p); err != nil { + return err + } + return op.OpPop.applyTo(p) } -func (op OpObjectSetFieldValue) applyTo(p *patcher) { - op.OpValue.applyTo(p) - op.OpReturnIntoObject.applyTo(p) +func (op OpObjectSetFieldValue) applyTo(p *patcher) error { + if err := op.OpValue.applyTo(p); err != nil { + return err + } + return op.OpReturnIntoObject.applyTo(p) } -func (op OpObjectCopyField) applyTo(p *patcher) { - op.OpPushField.applyTo(p) - op.OpCopy.applyTo(p) - op.OpReturnIntoObjectSameKey.applyTo(p) - op.OpPop.applyTo(p) +func (op OpObjectCopyField) applyTo(p *patcher) error { + if err := op.OpPushField.applyTo(p); err != nil { + return err + } + if err := op.OpCopy.applyTo(p); err != nil { + return err + } + if err := op.OpReturnIntoObjectSameKey.applyTo(p); err != nil { + return err + } + return op.OpPop.applyTo(p) } -func (op OpObjectDeleteField) applyTo(p *patcher) { - field := p.inputEntry().getField(op.Index) - obj := p.outputObject() +func (op OpObjectDeleteField) applyTo(p *patcher) error { + field, err := p.inputEntry().getField(op.Index) + if err != nil { + return err + } + obj, err := p.outputObject() + if err != nil { + return err + } delete(obj, field.key) + return nil } -func (op OpArrayAppendValue) applyTo(p *patcher) { - arr := p.outputArray() +func (op OpArrayAppendValue) applyTo(p *patcher) error { + arr, err := p.outputArray() + if err != nil { + return err + } *arr = append(*arr, op.Value) + return nil } -func (op OpArrayAppendSlice) applyTo(p *patcher) { - src := p.inputArray() - arr := p.outputArray() +func (op OpArrayAppendSlice) applyTo(p *patcher) error { + src, err := p.inputArray() + if err != nil { + return err + } + if op.Left < 0 || op.Right > len(src) || op.Left > op.Right { + return ErrInvalidPatch + } + arr, err := p.outputArray() + if err != nil { + return err + } *arr = append(*arr, src[op.Left:op.Right]...) + return nil } -func (op OpStringAppendString) applyTo(p *patcher) { - str := p.outputString() +func (op OpStringAppendString) applyTo(p *patcher) error { + str, err := p.outputString() + if err != nil { + return err + } *str = *str + op.String + return nil } -func (op OpStringAppendSlice) applyTo(p *patcher) { - src := p.inputString() - str := p.outputString() +func (op OpStringAppendSlice) applyTo(p *patcher) error { + src, err := p.inputString() + if err != nil { + return err + } + if op.Left < 0 || op.Right > len(src) || op.Left > op.Right { + return ErrInvalidPatch + } + str, err := p.outputString() + if err != nil { + return err + } *str = *str + src[op.Left:op.Right] + return nil } diff --git a/roundtrip_test.go b/roundtrip_test.go index f8d9aef..1e40324 100644 --- a/roundtrip_test.go +++ b/roundtrip_test.go @@ -103,7 +103,7 @@ var Documents = []struct { } func decodePatch(data []byte, patch *mendoza.Patch) error { - var value []interface{} + var value []any err := json.Unmarshal(data, &value) if err != nil { return err @@ -118,7 +118,7 @@ func decodePatch(data []byte, patch *mendoza.Patch) error { func TestRoundtrip(t *testing.T) { for idx, pair := range Documents { t.Run(fmt.Sprintf("N%d", idx), func(t *testing.T) { - var left, right interface{} + var left, right any err := json.Unmarshal([]byte(pair.Left), &left) require.NoError(t, err) @@ -129,10 +129,12 @@ func TestRoundtrip(t *testing.T) { patch1, patch2, err := mendoza.CreateDoublePatch(left, right) require.NoError(t, err) - result1 := mendoza.ApplyPatch(left, patch1) + result1, err := mendoza.ApplyPatch(left, patch1) + require.NoError(t, err) require.EqualValues(t, right, result1) - result2 := mendoza.ApplyPatch(right, patch2) + result2, err := mendoza.ApplyPatch(right, patch2) + require.NoError(t, err) require.EqualValues(t, left, result2) // Now try to encode and decode the patch