diff options
Diffstat (limited to 'jsonmatch')
| -rw-r--r-- | jsonmatch/diff_test.go | 34 | ||||
| -rw-r--r-- | jsonmatch/interface.go | 11 | ||||
| -rw-r--r-- | jsonmatch/machinery.go | 30 |
3 files changed, 72 insertions, 3 deletions
diff --git a/jsonmatch/diff_test.go b/jsonmatch/diff_test.go index 1a49010..aba2d00 100644 --- a/jsonmatch/diff_test.go +++ b/jsonmatch/diff_test.go @@ -409,3 +409,37 @@ func TestArrayOnArrayAction(t *testing.T) { match = jsonmatch.Array{jsonmatch.Array{1, 2}} AssertEqual(t, match.DiffAgainst(message), expected) } + +func TestDispatchIntoCustomDiffable(t *testing.T) { + message := []byte(`{"name":"data.json","type":"application/json","content":"{\"foo\":1,\"bar\":3}"}`) + match := jsonmatch.Object{ + "name": "data.json", + "type": "application/json", + "content": jsonWithinJSONString{jsonmatch.Object{ + "foo": 1, + "bar": 2, + }}, + } + expected := []jsonmatch.Diff{{ + Kind: "value mismatch", + Pointer: "/content/bar", + ExpectedJSON: "2", + ActualJSON: "3", + }} + AssertEqual(t, match.DiffAgainst(message), expected) +} + +// jsonWithinJSONString appears in TestDispatchIntoCustomDiffable. +type jsonWithinJSONString struct { + inner jsonmatch.Diffable +} + +// DiffAgainst implements the DiffAgainst interface. +func (j jsonWithinJSONString) DiffAgainst(buf []byte) []jsonmatch.Diff { + var s string + err := json.Unmarshal(buf, &s) + if err != nil { + panic(err.Error()) + } + return j.inner.DiffAgainst([]byte(s)) +} diff --git a/jsonmatch/interface.go b/jsonmatch/interface.go index d7b6b5b..f175f59 100644 --- a/jsonmatch/interface.go +++ b/jsonmatch/interface.go @@ -128,6 +128,13 @@ // }, // }.DiffAgainst(resp2.Body.Bytes()) // // ... +// +// # TODO +// +// As a special case, [json.RawMessage] may appear on the "expected" side to match string values that contain JSON themselves. +// For example: +// +// actual := `{"name":"data.json","type":"application/json","content":"{\"foo\":2"}` package jsonmatch // import "go.xyrillian.de/gg/jsonmatch" import ( @@ -160,6 +167,10 @@ import ( // However, the implementation will only recurse into substructures of the following well-known types: jsonmatch.Object, map[string]any, jsonmatch.Array, []any, []jsonmatch.Object, []map[string]any. // Any other map, array, slice, struct or pointer type will be treated as a black box: // If its JSON serialization differs from that of the respective section of the actual payload, a diff will be generated for its entirety only, not for any specific subfields. +// +// As an exception, if a substructure is of a foreign type that implements the Diffable interface, its DiffAgainst() method will be called by reserializing the actual payload. +// This is usually not what you want: Most of the time, it is much easier to have helper functions generate instances of the standard Diffable types. +// This extension interface is intended only for bizarre encodings (e.g. JSON payloads within JSON strings that jsonmatch itself would not be able to inspect). type Diffable interface { DiffAgainst([]byte) []Diff } diff --git a/jsonmatch/machinery.go b/jsonmatch/machinery.go index f3f5348..a10251a 100644 --- a/jsonmatch/machinery.go +++ b/jsonmatch/machinery.go @@ -26,7 +26,7 @@ func marshalExpectedForDiff(value any) string { func marshalActualForDiff(value any) string { // `actual` values are always safe to marshal because they were - // unmarshaled from JSON into any and thus can only contain safe + // unmarshaled from JSON into any and thus can only contain safe types buf, err := json.Marshal(value) if err != nil { // this line is therefore unreachable in tests and only exists as defense in depth @@ -121,8 +121,9 @@ func keyIntoPointerFragment(key string) string { } const ( - kindValueMismatch = "value mismatch" - kindTypeMismatch = "type mismatch" + kindValueMismatch = "value mismatch" + kindTypeMismatch = "type mismatch" + kindDispatchFailed = "dispatch failed" ) // NOTE: getDiffsForValue is the main part of the recursion to generate the diff. @@ -168,6 +169,29 @@ func getDiffsForValue(path []pathElement, expected, actual any) []Diff { } } + // generic handling for custom Diffables + // (if any unexpected error occurs here, we fall back to the default handling) + if diffable, ok := expected.(Diffable); ok { + // `actual` values are always safe to marshal because they were + // unmarshaled from JSON into any and thus can only contain safe types + buf, err := json.Marshal(actual) + if err != nil { + // this branch is therefore unreachable in tests and only exists as defense in depth + return []Diff{{ + Kind: kindDispatchFailed, + Pointer: pathIntoPointer(path), + ExpectedJSON: fmt.Sprintf("<custom diffable: %#v>", diffable), + ActualJSON: fmt.Sprintf("<marshal error: %s>", err.Error()), + }} + } + diffs := diffable.DiffAgainst(buf) + for idx, diff := range diffs { + diff.Pointer = pathIntoPointer(path) + diff.Pointer + diffs[idx] = diff + } + return diffs + } + // generic handling for values or structures that we do not recurse into further: // check that `expected` encodes to JSON in an equivalent way to `actual` actualJSON := marshalActualForDiff(actual) |
