diff --git a/.travis.yml b/.travis.yml index c53178c..3232d8a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,8 @@ language: go go: - "1.10" - - 1.11 - - 1.12 + - "1.11" + - "1.12" before_install: - go get -u golang.org/x/tools/cmd/cover diff --git a/Readme.md b/Readme.md index 49e17d9..94db70a 100644 --- a/Readme.md +++ b/Readme.md @@ -8,21 +8,21 @@ A JSON diff utility. -# Install +## Install -## Downloading the compiled binary +### Downloading the compiled binary - Download the latest version of the binary: [releases](https://github.com/yazgazan/jaydiff/releases) - extract the archive and place the `jaydiff` binary in your `$PATH` -## From source +### From source - Have go 1.10 or greater installed: [golang.org](https://golang.org/doc/install) - run `go get -u github.com/yazgazan/jaydiff` -# Usage +## Usage -``` +```text Usage: jaydiff [OPTIONS] FILE_1 FILE_2 @@ -41,7 +41,7 @@ Help Options: -h, --help Show this help message ``` -## Examples +### Examples Getting a full diff of two json files: @@ -73,8 +73,8 @@ Ignoring fields: ```diff $ jaydiff --show-types \ - --ignore='.b\[\]' --ignore='.d' --ignore='.c.[ac]' \ - old.json new.json + --ignore='.b\[\]' --ignore='.d' --ignore='.c.[ac]' \ + old.json new.json map[string]interface {} map[ a: float64 42 @@ -159,9 +159,8 @@ $ jaydiff --report --show-types --ignore-excess --ignore-values old.json new.jso - .f: float64 42 ``` -# Ideas +## Ideas - JayPatch -- Have the diff lib support more types (Structs, interfaces (?), Arrays, ...) Sponsored by [Datumprikker.nl](https://datumprikker.nl) diff --git a/diff/diff.go b/diff/diff.go index 24536ca..4bf74b2 100644 --- a/diff/diff.go +++ b/diff/diff.go @@ -43,8 +43,8 @@ func Diff(lhs, rhs interface{}, opts ...ConfigOpt) (Differ, error) { } func diff(c config, lhs, rhs interface{}, visited *visited) (Differ, error) { - lhsVal := reflect.ValueOf(lhs) - rhsVal := reflect.ValueOf(rhs) + lhsVal, lhs := indirectValueOf(lhs) + rhsVal, rhs := indirectValueOf(rhs) if d, ok := nilCheck(lhs, rhs); ok { return d, nil @@ -56,23 +56,43 @@ func diff(c config, lhs, rhs interface{}, visited *visited) (Differ, error) { return types{lhs, rhs}, ErrCyclic } - if lhsVal.Type().Comparable() && rhsVal.Type().Comparable() { + if valueIsScalar(lhsVal) && valueIsScalar(rhsVal) { return scalar{lhs, rhs}, nil } if lhsVal.Kind() != rhsVal.Kind() { return types{lhs, rhs}, nil } - if lhsVal.Kind() == reflect.Slice { + switch lhsVal.Kind() { + case reflect.Slice, reflect.Array: return c.sliceFn(c, lhs, rhs, visited) - } - if lhsVal.Kind() == reflect.Map { + case reflect.Map: return newMap(c, lhs, rhs, visited) + case reflect.Struct: + return newStruct(c, lhs, rhs, visited) } return types{lhs, rhs}, &ErrUnsupported{lhsVal.Type(), rhsVal.Type()} } +func indirectValueOf(i interface{}) (reflect.Value, interface{}) { + v := reflect.Indirect(reflect.ValueOf(i)) + if !v.IsValid() || !v.CanInterface() { + return reflect.ValueOf(i), i + } + + return v, v.Interface() +} + +func valueIsScalar(v reflect.Value) bool { + switch v.Kind() { + default: + return v.Type().Comparable() + case reflect.Struct, reflect.Array, reflect.Ptr, reflect.Chan: + return false + } +} + func nilCheck(lhs, rhs interface{}) (Differ, bool) { if lhs == nil && rhs == nil { return scalar{lhs, rhs}, true diff --git a/diff/diff_test.go b/diff/diff_test.go index 729a39b..e6b68c1 100644 --- a/diff/diff_test.go +++ b/diff/diff_test.go @@ -2,6 +2,7 @@ package diff import ( "fmt" + "reflect" "strings" "testing" ) @@ -57,6 +58,21 @@ func TestDiff(t *testing.T) { {LHS: []interface{}(nil), RHS: []interface{}{1, 2, 3.3}, Want: ContentDiffer}, {LHS: []int(nil), RHS: []int{}, Want: Identical}, {LHS: func() {}, RHS: func() {}, Want: TypesDiffer, Error: true}, + { + LHS: struct{}{}, + RHS: struct{}{}, + Want: Identical, + }, + { + LHS: struct{ Foo int }{Foo: 42}, + RHS: struct{ Foo int }{Foo: 21}, + Want: ContentDiffer, + }, + { + LHS: struct{ Foo int }{Foo: 42}, + RHS: struct{ Bar int }{Bar: 42}, + Want: TypesDiffer, + }, } { diff, err := Diff(test.LHS, test.RHS) @@ -205,6 +221,32 @@ func TestScalar(t *testing.T) { }, Type: TypesDiffer, }, + { + LHS: complex(4, -3), + RHS: complex(4, -3), + Want: [][]string{ + {"complex128", "4", "-3"}, + }, + Type: Identical, + }, + { + LHS: complex(4, -3), + RHS: complex(-7, 32), + Want: [][]string{ + {"complex128", "4", "-3"}, + {"complex128", "-7", "32"}, + }, + Type: ContentDiffer, + }, + { + LHS: 2.1, + RHS: complex(4, -3), + Want: [][]string{ + {"float64", "2.1"}, + {"complex128", "4", "-3"}, + }, + Type: TypesDiffer, + }, } { typ := scalar{test.LHS, test.RHS} @@ -552,7 +594,7 @@ func TestMap(t *testing.T) { m, err := newMap(defaultConfig(), test.LHS, test.RHS, &visited{}) if err != nil { - t.Errorf("NewMap(%+v, %+v): unexpected error: %q", test.LHS, test.RHS, err) + t.Errorf("newMap(%+v, %+v): unexpected error: %q", test.LHS, test.RHS, err) continue } if m.Diff() != test.Type { @@ -570,10 +612,10 @@ func TestMap(t *testing.T) { invalid, err := newMap(defaultConfig(), nil, nil, &visited{}) if invalidErr, ok := err.(errInvalidType); ok { if !strings.Contains(invalidErr.Error(), "nil") { - t.Errorf("NewMap(nil, nil): unexpected format for InvalidType error: got %s", err) + t.Errorf("newMap(nil, nil): unexpected format for InvalidType error: got %s", err) } } else { - t.Errorf("NewMap(nil, nil): expected InvalidType error, got %s", err) + t.Errorf("newMap(nil, nil): expected InvalidType error, got %s", err) } ss := invalid.Strings() if len(ss) != 0 { @@ -604,6 +646,197 @@ func TestMap(t *testing.T) { } } +type emptyStruct struct{} +type subStruct struct { + A int +} +type structA struct { + Foo int + Bar subStruct + baz float64 +} +type structB struct { + Foo int + Bar subStruct + baz float64 +} +type structC struct { + Foo []int +} +type structInvalid struct { + A func() +} + +func TestTypeStruct(t *testing.T) { + for i, test := range []stringTest{ + { + LHS: emptyStruct{}, + RHS: emptyStruct{}, + Want: [][]string{ + {"emptyStruct", "{}"}, + }, + WantJSON: [][]string{ + {"{}"}, + }, + Type: Identical, + }, + { + LHS: structA{ + Foo: 42, + Bar: subStruct{ + A: 2, + }, + baz: 4.2, + }, + RHS: structA{ + Foo: 42, + Bar: subStruct{ + A: 2, + }, + baz: 1.1, + }, + Want: [][]string{ + {"structA", "42", "{2}", "4.2"}, + }, + WantJSON: [][]string{ + {"42", "{2}", "4.2"}, + }, + Type: Identical, + }, + { + LHS: structA{ + Foo: 42, + Bar: subStruct{ + A: 2, + }, + baz: 4.2, + }, + RHS: structB{ + Foo: 42, + Bar: subStruct{ + A: 2, + }, + baz: 1.1, + }, + Want: [][]string{ + {"structA", "42", "{2}", "4.2"}, + }, + WantJSON: [][]string{ + {"42", "{2}", "4.2"}, + }, + Type: Identical, + }, + { + LHS: structA{ + Foo: 42, + Bar: subStruct{ + A: 2, + }, + baz: 4.2, + }, + RHS: structB{ + Foo: 23, + Bar: subStruct{ + A: 2, + }, + baz: 1.1, + }, + Want: [][]string{ + {}, + {"Bar"}, + {"Foo", "-", "int", "42"}, + {"Foo", "+", "int", "23"}, + {}, + }, + WantJSON: [][]string{ + {}, + {"Bar"}, + {"Foo", "-", "42"}, + {"Foo", "+", "23"}, + {}, + }, + Type: ContentDiffer, + }, + { + LHS: structA{ + Foo: 42, + Bar: subStruct{ + A: 2, + }, + baz: 4.2, + }, + RHS: structC{ + Foo: []int{1, 2}, + }, + Want: [][]string{ + {"-", "structA", "42", "{2}", "4.2"}, + {"+", "structC", "[1 2]"}, + }, + WantJSON: [][]string{ + {"-", "42", "{2}", "4.2"}, + {"+", "{[1 2]}"}, + }, + Type: TypesDiffer, + }, + } { + s, err := newStruct(defaultConfig(), test.LHS, test.RHS, &visited{}) + + if err != nil { + t.Errorf("newStruct(%+v, %+v): unexpected error: %q", test.LHS, test.RHS, err) + continue + } + if s.Diff() != test.Type { + t.Errorf("Types.Diff() = %q, expected %q", s.Diff(), test.Type) + } + + ss := s.Strings() + indented := s.StringIndent(testKey, testPrefix, testOutput) + testStrings(fmt.Sprintf("TestMap[%d]", i), t, test.Want, ss, indented) + + indentedJSON := s.StringIndent(testKey, testPrefix, testJSONOutput) + testStrings(fmt.Sprintf("TestMap[%d]", i), t, test.WantJSON, ss, indentedJSON) + } + + invalid, err := newStruct(defaultConfig(), nil, nil, &visited{}) + if invalidErr, ok := err.(errInvalidType); ok { + if !strings.Contains(invalidErr.Error(), "nil") { + t.Errorf("newStruct(nil, nil): unexpected format for InvalidType error: got %s", err) + } + } else { + t.Errorf("newStruct(nil, nil): expected InvalidType error, got %s", err) + } + ss := invalid.Strings() + if len(ss) != 0 { + t.Errorf("len(invalidStruct.Strings()) = %d, expected 0", len(ss)) + } + indented := invalid.StringIndent(testKey, testPrefix, testOutput) + if indented != "" { + t.Errorf("invalidStruct.StringIndent(%q, %q, %+v) = %q, expected %q", testKey, testPrefix, testOutput, indented, "") + } + + invalid, err = newStruct(defaultConfig(), structA{}, nil, &visited{}) + if invalidErr, ok := err.(errInvalidType); ok { + if !strings.Contains(invalidErr.Error(), "nil") { + t.Errorf("newStruct(structA{}, nil): unexpected format for InvalidType error: got %s", err) + } + } else { + t.Errorf("newStruct(structA{}, nil): expected InvalidType error, got %s", err) + } + ss = invalid.Strings() + if len(ss) != 0 { + t.Errorf("len(invalidStruct.Strings()) = %d, expected 0", len(ss)) + } + indented = invalid.StringIndent(testKey, testPrefix, testOutput) + if indented != "" { + t.Errorf("invalidStruct.StringIndent(%q, %q, %+v) = %q, expected %q", testKey, testPrefix, testOutput, indented, "") + } + + invalid, err = newStruct(defaultConfig(), structInvalid{}, structInvalid{}, &visited{}) + if err == nil { + t.Errorf("newStruct(structInvalid{}, structInvalid{}): expected error, got nil") + } +} + func TestIgnore(t *testing.T) { ignoreDiff, _ := Ignore() @@ -643,6 +876,18 @@ func TestLHS(t *testing.T) { t.Errorf("LHS(%+v) = %v, expected %d", validLHSMapGetter, v, 42) } + validLHSStructGetter := Differ(&structDiff{ + lhs: structA{Foo: 42}, + rhs: structB{Foo: 23}, + }) + v, err = LHS(validLHSStructGetter) + if err != nil { + t.Errorf("LHS(%+v): unexpected error: %s", validLHSStructGetter, err) + } + if s, ok := v.(structA); !ok || s.Foo != 42 { + t.Errorf("LHS(%+v).Foo = %v, expected %d", validLHSStructGetter, s.Foo, 42) + } + validLHSSliceGetter := Differ(&slice{ lhs: 42, rhs: "hello", @@ -727,6 +972,18 @@ func TestRHS(t *testing.T) { t.Errorf("RHS(%+v) = %v, expected %q", validRHSMapGetter, v, "hello") } + validRHSStructGetter := Differ(&structDiff{ + lhs: structA{Foo: 42}, + rhs: structB{Foo: 23}, + }) + v, err = RHS(validRHSStructGetter) + if err != nil { + t.Errorf("RHS(%+v): unexpected error: %s", validRHSStructGetter, err) + } + if s, ok := v.(structB); !ok || s.Foo != 23 { + t.Errorf("RHS(%+v).Foo = %v, expected %d", validRHSStructGetter, s.Foo, 23) + } + validRHSSliceGetter := Differ(&slice{ lhs: 42, rhs: "hello", @@ -898,3 +1155,32 @@ func TestIsSlice(t *testing.T) { t.Error("IsSlice(Diff(map{...}, map{...})) = false, expected true") } } + +func TestValueIsScalar(t *testing.T) { + for _, test := range []struct { + In interface{} + Expected bool + }{ + {int(42), true}, + {int8(23), true}, + {"foo", true}, + {true, true}, + {float32(1.2), true}, + {complex(5, -3), true}, + + {[]byte("foo"), false}, + {struct{}{}, false}, + {&struct{}{}, false}, + {[]int{1, 2, 3}, false}, + {[3]int{1, 2, 3}, false}, + {map[string]int{"foo": 22}, false}, + {func() {}, false}, + {make(chan struct{}), false}, + } { + v := reflect.ValueOf(test.In) + got := valueIsScalar(v) + if got != test.Expected { + t.Errorf("valueIsScalar(%T) = %v, expected %v", test.In, got, test.Expected) + } + } +} diff --git a/diff/example_test.go b/diff/example_test.go index e095ecd..d3c53cd 100644 --- a/diff/example_test.go +++ b/diff/example_test.go @@ -2,8 +2,9 @@ package diff_test import ( "fmt" - "github.com/yazgazan/jaydiff/diff" "strings" + + "github.com/yazgazan/jaydiff/diff" ) func ExampleDiff() { @@ -24,8 +25,87 @@ func ExampleDiff() { Indent: " ", ShowTypes: true, })) + + // Output: + // map[string]interface {} map[ + // - a: int 42 + // + a: int 21 + // b: []int [ + // int 1 + // int 2 + // + int 3 + // ] + // c: string abc + // ] } +func ExampleDiff_struct() { + type subStruct struct { + Hello int + World float64 + } + type structA struct { + Foo int + Bar string + Baz subStruct + Ban [2]int + + priv int + } + type structB struct { + Foo int + Bar string + Baz subStruct + Ban [2]int + + priv int + } + + lhs := structA{ + Foo: 42, + Bar: "hello", + Baz: subStruct{ + Hello: 11, + World: 3.5, + }, + Ban: [2]int{3, 5}, + priv: 0, + } + rhs := structB{ + Foo: 21, + Bar: "hello", + Baz: subStruct{ + Hello: 11, + World: 3.5, + }, + Ban: [2]int{3, 7}, + priv: 1, + } + + d, err := diff.Diff(lhs, rhs) + if err != nil { + fmt.Printf("Error: %v\n", err) + return + } + + fmt.Println(d.StringIndent("", "", diff.Output{ + Indent: " ", + ShowTypes: true, + })) + + // Output: + // diff_test.structA map[ + // Ban: [2]int [ + // int 3 + // - int 5 + // + int 7 + // ] + // Bar: string hello + // Baz: diff_test.subStruct {11 3.5} + // - Foo: int 42 + // + Foo: int 21 + // ] +} func ExampleReport() { lhs := map[string]interface{}{ "a": 42, @@ -48,6 +128,11 @@ func ExampleReport() { for _, report := range reports { fmt.Println(report) } + + // Output: + // - .a: int 42 + // + .a: int 21 + // + .b[2]: int 3 } func ExampleWalk() { @@ -85,4 +170,23 @@ func ExampleWalk() { Indent: " ", ShowTypes: true, })) + + // Output: + // Before: + // map[string]interface {} map[ + // - a: int 42 + // + a: int 41 + // b: []int [1 2] + // c: string abc + // + exess_key: string will be ignored + // - will_be_ignored: []int [3 4] + // + will_be_ignored: int 5 + // ] + // After: + // map[string]interface {} map[ + // - a: int 42 + // + a: int 41 + // b: []int [1 2] + // c: string abc + // ] } diff --git a/diff/map.go b/diff/map.go index 242164f..7f11aea 100644 --- a/diff/map.go +++ b/diff/map.go @@ -115,7 +115,7 @@ func (m mapDiff) Strings() []string { } case ContentDiffer: var ss = []string{"{"} - var keys []interface{} + keys := make([]interface{}, 0, len(m.diffs)) for key := range m.diffs { keys = append(keys, key) @@ -147,7 +147,7 @@ func (m mapDiff) StringIndent(keyprefix, prefix string, conf Output) string { "+" + prefix + keyprefix + conf.green(m.rhs) case ContentDiffer: var ss = []string{} - var keys []interface{} + keys := make([]interface{}, 0, len(m.diffs)) for key := range m.diffs { keys = append(keys, key) diff --git a/diff/struct.go b/diff/struct.go new file mode 100644 index 0000000..dbb74f2 --- /dev/null +++ b/diff/struct.go @@ -0,0 +1,201 @@ +package diff + +import ( + "fmt" + "reflect" + "sort" + "strings" + + "github.com/yazgazan/jaydiff/jpath" +) + +type structDiff struct { + diffs map[string]Differ + lhs interface{} + rhs interface{} +} + +func newStruct(c config, lhs, rhs interface{}, visited *visited) (Differ, error) { + var diffs = make(map[string]Differ) + + if typesDiffer, err := structTypesDiffer(lhs, rhs); err != nil { + return structDiff{ + lhs: lhs, + rhs: rhs, + diffs: diffs, + }, err + } else if !typesDiffer { + lhsVal := reflect.ValueOf(lhs) + lhsType := lhsVal.Type() + rhsVal := reflect.ValueOf(rhs) + + for i := 0; i < lhsType.NumField(); i++ { + fType := lhsType.Field(i) + lhsFVal := lhsVal.Field(i) + rhsFVal := rhsVal.Field(i) + if !lhsFVal.CanInterface() { + continue + } + + diff, err := diff(c, lhsFVal.Interface(), rhsFVal.Interface(), visited) + diffs[fType.Name] = diff + + if err != nil { + return structDiff{ + lhs: lhs, + rhs: rhs, + diffs: diffs, + }, err + } + } + } + + return structDiff{ + lhs: lhs, + rhs: rhs, + diffs: diffs, + }, nil +} + +func structTypesDiffer(lhs, rhs interface{}) (bool, error) { + if lhs == nil { + return true, errInvalidType{Value: lhs, For: "struct"} + } + if rhs == nil { + return true, errInvalidType{Value: rhs, For: "struct"} + } + + lhsType := reflect.TypeOf(lhs) + rhsType := reflect.TypeOf(rhs) + + return !lhsType.ConvertibleTo(rhsType), nil +} + +func (s structDiff) Diff() Type { + if ok, err := structTypesDiffer(s.lhs, s.rhs); err != nil { + return Invalid + } else if ok { + return TypesDiffer + } + + for _, d := range s.diffs { + if d.Diff() != Identical { + return ContentDiffer + } + } + + return Identical +} + +func (s structDiff) Strings() []string { + switch s.Diff() { + case Identical: + return []string{fmt.Sprintf(" %T %v", s.lhs, s.lhs)} + case TypesDiffer: + return []string{ + fmt.Sprintf("- %T %v", s.lhs, s.lhs), + fmt.Sprintf("+ %T %v", s.rhs, s.rhs), + } + case ContentDiffer: + var ss = []string{"{"} + keys := make([]string, 0, len(s.diffs)) + + for key := range s.diffs { + keys = append(keys, key) + } + + sort.Strings(keys) + + for _, key := range keys { + d := s.diffs[key] + for _, s := range d.Strings() { + ss = append(ss, fmt.Sprintf("%v: %s", key, s)) + } + } + + return append(ss, "}") + } + + return []string{} +} + +func (s structDiff) StringIndent(keyprefix, prefix string, conf Output) string { + switch s.Diff() { + case Identical: + return " " + prefix + keyprefix + conf.white(s.lhs) + case TypesDiffer: + return "-" + prefix + keyprefix + conf.red(s.lhs) + newLineSeparatorString(conf) + + "+" + prefix + keyprefix + conf.green(s.rhs) + case ContentDiffer: + var ss = []string{} + keys := make([]string, 0, len(s.diffs)) + + for key := range s.diffs { + keys = append(keys, key) + } + + sort.Strings(keys) + + for _, key := range keys { + d := s.diffs[key] + + s := d.StringIndent(key+": ", prefix+conf.Indent, conf) + if s != "" { + ss = append(ss, s) + } + } + + return strings.Join([]string{ + s.openString(keyprefix, prefix, conf), + strings.Join(ss, newLineSeparatorString(conf)), + s.closeString(prefix, conf), + }, "\n") + } + + return "" +} + +func (s structDiff) openString(keyprefix, prefix string, conf Output) string { + if conf.JSON { + return " " + prefix + keyprefix + "{" + } + return " " + prefix + keyprefix + conf.typ(s.lhs) + "map[" +} + +func (s structDiff) closeString(prefix string, conf Output) string { + if conf.JSON { + return " " + prefix + "}" + } + return " " + prefix + "]" +} + +func (s structDiff) Walk(path string, fn WalkFn) error { + keys := make([]string, 0, len(s.diffs)) + + for k := range s.diffs { + keys = append(keys, k) + } + + sort.Strings(keys) + + for _, k := range keys { + diff := s.diffs[k] + d, err := walk(s, diff, path+"."+jpath.EscapeKey(k), fn) + if err != nil { + return err + } + if d != nil { + s.diffs[k] = d + } + } + + return nil +} + +func (s structDiff) LHS() interface{} { + return s.lhs +} + +func (s structDiff) RHS() interface{} { + return s.rhs +} diff --git a/diff/walk_test.go b/diff/walk_test.go index af6751b..af30e93 100644 --- a/diff/walk_test.go +++ b/diff/walk_test.go @@ -23,6 +23,11 @@ func TestWalk(t *testing.T) { }, Want: 5, }, + { + LHS: structA{Foo: 42, Bar: subStruct{A: 2}}, + RHS: structB{Foo: 23, Bar: subStruct{A: 4}}, + Want: 4, + }, } { var nCalls int @@ -59,6 +64,10 @@ func TestWalkError(t *testing.T) { {42, 43}, {[]int{42}, []int{43}}, {map[string]int{"ha": 42}, map[string]int{"ha": 45}}, + { + LHS: structA{Foo: 42, Bar: subStruct{A: 2}}, + RHS: structB{Foo: 23, Bar: subStruct{A: 4}}, + }, } { d, err := Diff(test.LHS, test.RHS) if err != nil {