-
Notifications
You must be signed in to change notification settings - Fork 41
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
test: replace evanphx/json-patch dependency with custom patch apply m…
…ethod
- Loading branch information
Showing
9 changed files
with
310 additions
and
39 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,176 @@ | ||
package jsondiff | ||
|
||
import ( | ||
"bytes" | ||
"encoding/json" | ||
"fmt" | ||
"strconv" | ||
"strings" | ||
"unicode" | ||
|
||
"github.com/tidwall/gjson" | ||
"github.com/tidwall/sjson" | ||
) | ||
|
||
// apply applies the patch to the given source document. | ||
// If valid is true, the document is validated prior to | ||
// the application of the patch. | ||
func (p Patch) apply(src []byte, valid bool) ([]byte, error) { | ||
if valid && !json.Valid(src) { | ||
return nil, fmt.Errorf("invalid source document") | ||
} | ||
// Make a copy of the source document which | ||
// will receive the patch mutations. | ||
tgt := bytes.Clone(src) | ||
|
||
for _, op := range p { | ||
dp, err := toDotPath(op.Path, src) | ||
if err != nil { | ||
return nil, err | ||
} | ||
switch op.Type { | ||
case OperationAdd: | ||
tgt, err = add(tgt, dp, op.Value) | ||
case OperationRemove: | ||
tgt, err = sjson.DeleteBytes(tgt, dp) | ||
case OperationReplace: | ||
tgt, err = replace(tgt, dp, op.Value) | ||
case OperationMove, OperationCopy: | ||
// First fetch the value from the source path, | ||
// and then add it to the destination path. | ||
fp, err := toDotPath(op.From, src) | ||
if err != nil { | ||
return nil, err | ||
} | ||
tgt, err = add(tgt, dp, op.Value) | ||
if err != nil { | ||
break // bail out to interpret error | ||
} | ||
// Finally, if the operation was a move, the | ||
// source value must be deleted. | ||
if op.Type == OperationMove { | ||
tgt, err = sjson.DeleteBytes(tgt, fp) | ||
} | ||
case OperationTest: | ||
r := gjson.GetBytes(tgt, dp) | ||
if !r.Exists() { | ||
return nil, fmt.Errorf("invalid patch: %q value is not set", op.Path) | ||
} | ||
} | ||
if err != nil { | ||
return nil, fmt.Errorf("failed to apply op: %w", err) | ||
} | ||
} | ||
return tgt, nil | ||
} | ||
|
||
func replace(tgt []byte, path string, val interface{}) ([]byte, error) { | ||
if path == "@this" { | ||
return json.Marshal(val) | ||
} | ||
return sjson.SetBytesOptions(tgt, path, val, &sjson.Options{ | ||
Optimistic: true, | ||
ReplaceInPlace: true, | ||
}) | ||
} | ||
|
||
func add(tgt []byte, path string, val interface{}) ([]byte, error) { | ||
if path == "@this" { | ||
// Unsupported by the sjson package. | ||
// Since an empty path represent the root | ||
// document, we can simply marshal the value | ||
// and return it as-is. | ||
return json.Marshal(val) | ||
} | ||
// If we're dealing with an array indice, we want to | ||
// "insert" the element instead of replacing it. | ||
// We insert a null value manually where the new element | ||
// is supposed to be (before the current element), and | ||
// finally replace the placeholder with the new value. | ||
if isArrayIndex(path) { | ||
r := gjson.GetBytes(tgt, path) | ||
if r.Index > 0 { | ||
tgt = append(tgt[:r.Index], append([]byte(`null,`), tgt[r.Index:]...)...) | ||
} | ||
} | ||
return sjson.SetBytesOptions(tgt, path, val, &sjson.Options{ReplaceInPlace: true}) | ||
} | ||
|
||
func isArrayIndex(path string) bool { | ||
i := strings.LastIndexByte(path, '.') | ||
if i == -1 { | ||
if path != "" && unicode.IsDigit(rune(path[0])) { | ||
return true | ||
} | ||
return false | ||
} | ||
if i != 0 && path[i-1] == '\\' { | ||
return false | ||
} | ||
if i < len(path) && unicode.IsDigit(rune(path[i+1])) { | ||
return true | ||
} | ||
return false | ||
} | ||
|
||
// dotPath converts the given JSON Pointer string to the | ||
// dot-path notation used by tidwall/sjson package. | ||
// The source document is required in order to distinguish | ||
// // numeric object keys from array indices | ||
func toDotPath(path string, src []byte) (string, error) { | ||
if path == "" { | ||
// @this returns the current element. | ||
// It is used to retrieve the root element. | ||
return "@this", nil | ||
} | ||
fragments, err := parsePointer(path) | ||
if err != nil { | ||
return "", fmt.Errorf("failed to parse path: %w", err) | ||
} | ||
sb := strings.Builder{} | ||
|
||
for i, f := range fragments { | ||
var key string | ||
if len(f) != 0 && unicode.IsDigit(rune(f[0])) { | ||
// The fragment starts with a digit, which | ||
// indicate that it might be a number. | ||
if _, err := strconv.ParseInt(f, 10, 64); err == nil { | ||
// The number is valid, but it could either be an | ||
// array indice or an object key. | ||
// Since the JSON Pointer RFC does not differentiate | ||
// between the two, we have to look up the value to | ||
// know what we're dealing with. | ||
p := sb.String() | ||
if p == "" { | ||
p = "@this" | ||
} | ||
r := gjson.GetBytes(src, p) | ||
switch { | ||
case r.IsArray(): | ||
// Write array indice as-is. | ||
key = f | ||
case r.IsObject(): | ||
// Force the number as an object key, by | ||
// preceding it with a colon character. | ||
key = ":" + f | ||
default: | ||
return "", fmt.Errorf("unexpected value type at path: %s", sb.String()) | ||
} | ||
} | ||
} else if f == "-" && i == len(fragments)-1 { | ||
// If the last fragment is the "-" character, | ||
// it indicates that the value is a nonexistent | ||
// element to append to the array. | ||
key = "-1" | ||
} else { | ||
key = rfc6901Unescaper.Replace(f) | ||
key = strings.Replace(key, ".", `\.`, -1) | ||
} | ||
if i != 0 { | ||
// Add separator character | ||
sb.WriteByte('.') | ||
} | ||
sb.WriteString(key) | ||
} | ||
return sb.String(), nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
package jsondiff | ||
|
||
import "testing" | ||
|
||
func Test_toDotPath(t *testing.T) { | ||
for _, tc := range []struct { | ||
ptr string | ||
json string | ||
path string | ||
}{ | ||
{ | ||
"", | ||
`{}`, | ||
"@this", | ||
}, | ||
{ | ||
"/a/b/c", | ||
`{"a":{"b":{"c":1}}}`, | ||
"a.b.c", | ||
}, | ||
{ | ||
"/a/1/c", | ||
`{"a":[null,{"c":1}]}`, | ||
"a.1.c", | ||
}, | ||
{ | ||
"/a/123/b", | ||
`{"a":{"123":{"b":1"}}}`, | ||
"a.:123.b", | ||
}, | ||
{ | ||
"/1", | ||
`["a","b","c"]`, | ||
"1", | ||
}, | ||
{ | ||
"/0", | ||
`{"0":"a"}`, | ||
":0", | ||
}, | ||
{ | ||
"/a/-", | ||
`{"a":[1,2,3]}`, | ||
"a.-1", | ||
}, | ||
} { | ||
s, err := toDotPath(tc.ptr, []byte(tc.json)) | ||
if err != nil { | ||
t.Error(err) | ||
} | ||
if s != tc.path { | ||
t.Errorf("got %q, want %q", s, tc.path) | ||
} | ||
} | ||
} | ||
|
||
func Test_isArrayIndex(t *testing.T) { | ||
for _, tc := range []struct { | ||
path string | ||
isIndex bool | ||
}{ | ||
{"a.b.c", false}, | ||
{"", false}, | ||
{"a.b.:124", false}, | ||
{"a.-1", false}, | ||
{"0.1.a", false}, | ||
{"0.1.2.:3", false}, | ||
{"a\\.b", false}, | ||
{"0.1\\.2", false}, | ||
{"0", true}, | ||
{"a.b.1", true}, | ||
{"0.1.2", true}, | ||
} { | ||
b := isArrayIndex(tc.path) | ||
if tc.isIndex && !b { | ||
t.Errorf("expected path %q to be an array index", tc.path) | ||
} | ||
if !tc.isIndex && b { | ||
t.Errorf("expected path %q to not be an array index", tc.path) | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,9 @@ | ||
github.com/evanphx/json-patch/v5 v5.7.0 h1:nJqP7uwL84RJInrohHfW0Fx3awjbm8qZeFv0nW9SYGc= | ||
github.com/evanphx/json-patch/v5 v5.7.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ= | ||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= | ||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= | ||
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= | ||
github.com/tidwall/gjson v1.17.0 h1:/Jocvlh98kcTfpN2+JzGQWQcqrPQwDrVEMApx/M5ZwM= | ||
github.com/tidwall/gjson v1.17.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= | ||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= | ||
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= | ||
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= | ||
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= | ||
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= | ||
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= |
Oops, something went wrong.