diff --git a/README.md b/README.md index 890c2a0..2c1c15f 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![Build Status](https://travis-ci.com/davidae/jm.svg "Travis CI status")](https://travis-ci.com/davidae/jm) A small package to match JSONs with the addition of using placeholders for possible unknown values. -It should work with any type of valid JSON - however nested and tangled it may be. This package uses only +It should work with any type of valid JSON - however nested, unordered, and tangled it may be. This package uses only golang's standard library, no dependencies. # Installation diff --git a/match.go b/match.go index f8a8eb3..ae6a5aa 100644 --- a/match.go +++ b/match.go @@ -39,18 +39,10 @@ func Match(expected, actual []byte, placeholders ...Placeholder) error { } func isEqual(expected, actual interface{}, key string, ph ...Placeholder) error { - if ea, aa, ok := isArray(expected, actual); ok { - l, err := areLenEqual(ea, aa) - if err != nil { + if ea, aa, ok := isArray(expected, actual, ph); ok { + if err := matchArray(ea, aa, ph); err != nil { return errUnderKey(err, key) } - - for i := 0; i < l; i++ { - if err := isEqual(ea[i], aa[i], key, ph...); err != nil { - return err - } - } - return nil } @@ -97,7 +89,7 @@ func isEqualValue(expected, actual interface{}, ph []Placeholder) error { return nil } -func isArray(i, j interface{}) ([]interface{}, []interface{}, bool) { +func isArray(i, j interface{}, ph []Placeholder) ([]interface{}, []interface{}, bool) { ia, ok := i.([]interface{}) if !ok { return []interface{}{}, []interface{}{}, false @@ -111,6 +103,49 @@ func isArray(i, j interface{}) ([]interface{}, []interface{}, bool) { return ia, ja, true } +func matchArray(listA, listB interface{}, ph []Placeholder) error { + if isEmpty(listA) && isEmpty(listB) { + return nil + } + + var ( + aValue = reflect.ValueOf(listA) + bValue = reflect.ValueOf(listB) + + aLen = aValue.Len() + bLen = bValue.Len() + ) + + if aLen != bLen { + return fmt.Errorf("mismatch array length %d and %d: %w", aLen, bLen, ErrArrayLengths) + } + + visited := make([]bool, bLen) + for i := 0; i < bLen; i++ { + var err error + element := bValue.Index(i).Interface() + found := false + for j := 0; j < aLen; j++ { + if visited[j] { + continue + } + if err := isEqual(aValue.Index(j).Interface(), element, "", ph...); err == nil { + visited[j] = true + found = true + break + } + } + if !found { + return fmt.Errorf( + "element %s appears more times in %s than in %s: %w", + marshalToJSON(element), + marshalToJSON(aValue.Interface()), + marshalToJSON(bValue.Interface()), err) + } + } + return nil +} + func isObject(i, j interface{}) (map[string]interface{}, map[string]interface{}, bool) { io, ok := i.(map[string]interface{}) if !ok { @@ -124,15 +159,6 @@ func isObject(i, j interface{}) (map[string]interface{}, map[string]interface{}, return io, jo, true } -func areLenEqual(i, j []interface{}) (int, error) { - il, jl := len(i), len(j) - if il != jl { - return 0, fmt.Errorf("mismatch array length %d and %d: %w", il, jl, ErrArrayLengths) - } - - return il, nil -} - type keyMatcher struct { expected, actual bool } @@ -169,3 +195,8 @@ func errUnderKey(err error, key string) error { return err } + +func marshalToJSON(i interface{}) string { + d, _ := json.Marshal(i) + return string(d) +} diff --git a/match_test.go b/match_test.go index 1083cd0..4325a55 100644 --- a/match_test.go +++ b/match_test.go @@ -24,7 +24,7 @@ func TestNotEqualJSONWithAdditionalArrayItem(t *testing.T) { if err := Match(expected, actual); err == nil { t.Error("expected an error, but nil was returned") } else if err.Error() != expectedErrMsg { - t.Errorf("expected error message %s to be, but got %s", expectedErrMsg, err) + t.Errorf("expected error message to be %q, but got %q", expectedErrMsg, err) } } @@ -32,13 +32,13 @@ func TestNotEqualJSONWithMismatchValue(t *testing.T) { var ( expected = mustReadFile(t, "test/stubs/match/expected.json") actual = mustReadFile(t, "test/stubs/match/mismatch_value.json") - expectedErrMsg = `value 2 and "hex": values are not equal` + expectedErrMsg = "mismatch under key arr_2: element [{},[2]] appears more times in [\"A\",1,[{},[2]]] than in [\"A\",1,[{},[\"hex\"]]]" ) if err := Match(expected, actual); err == nil { t.Error("expected an error, but nil was returned") } else if err.Error() != expectedErrMsg { - t.Errorf("expected error message %s to be, but got %s", expectedErrMsg, err) + t.Errorf("expected error message to be %q, but got %q", expectedErrMsg, err) } } @@ -102,13 +102,13 @@ func TestEqualWithArrayIntegerMismatch(t *testing.T) { var ( expected = `{"id": "1","count":[1,2,3,4,5,6]}` actual = `{"id": "1","count":[1,2,3,4,"s",6]}` - expectedErrMsg = `value 5 and "s": values are not equal` + expectedErrMsg = "mismatch under key count: element 5 appears more times in [1,2,3,4,5,6] than in [1,2,3,4,\"s\",6]" ) if err := Match([]byte(expected), []byte(actual), WithTimeLayout("$TIME_RFC3339", time.RFC3339)); err == nil { t.Error("expected a mismatch on 'count'") } else if err.Error() != expectedErrMsg { - t.Errorf("expected error message %s to be, but got %s", expectedErrMsg, err) + t.Errorf("expected error message to be %q, but got %q", expectedErrMsg, err) } } @@ -130,3 +130,39 @@ func mustReadFile(t *testing.T, filename string) []byte { return out } + +func TestEqualWithUnorderedArrayWhenIsEqualWithPrimitiveDataTypes(t *testing.T) { + var ( + expected = `{"id": "1","count":[1,2,["a","c","b"]]}` + actual = `{"id": "1","count":[2,1,["a","b","c"]]}` + ) + + if err := Match([]byte(expected), []byte(actual)); err != nil { + t.Errorf("unexpected mismatch: %s", err) + } +} + +func TestEqualWithUnorderedArrayWhenIsEqualWithStruct(t *testing.T) { + var ( + expected = `{"id": "1","count":[1,2,["a","c",{"flag":true}]]}` + actual = `{"id": "1","count":[2,1,["a",{"flag":true},"c"]]}` + ) + + if err := Match([]byte(expected), []byte(actual)); err != nil { + t.Errorf("unexpected mismatch: %s", err) + } +} + +func TestEqualWithUnorderedArrayWhenIsNotEqual(t *testing.T) { + var ( + expected = `{"id": "1","count":[1,2,["a","c","e"]]}` + actual = `{"id": "1","count":[2,1,["a","b","c"]]}` + expectedErrMsg = "mismatch under key count: element [\"a\",\"c\",\"e\"] appears more times in [1,2,[\"a\",\"c\",\"e\"]] than in [2,1,[\"a\",\"b\",\"c\"]]" + ) + + if err := Match([]byte(expected), []byte(actual)); err == nil { + t.Error("expected an error, but nil was returned") + } else if err.Error() != expectedErrMsg { + t.Errorf("expected error message to be %q, but got %q", expectedErrMsg, err) + } +}