diff --git a/diff/builder.go b/diff/builder.go new file mode 100644 index 0000000..bd34c51 --- /dev/null +++ b/diff/builder.go @@ -0,0 +1,223 @@ +package diff + +import ( + "fmt" + + "github.com/yazgazan/jaydiff/jpath" +) + +type Builder struct { + diff DiffBuilder + err error +} + +func (b *Builder) Add(path string, v interface{}) *Builder { + if b.err != nil { + return b + } + if b.diff == nil { + b.diff = &value{} + } + pp, err := jpath.Parse(path) + if err != nil { + b.err = err + return b + } + + b.err = b.diff.Add(pp, v) + + return b +} + +func (b *Builder) Delete(path string, v interface{}) *Builder { + if b.err != nil { + return b + } + if b.diff == nil { + b.diff = &value{} + } + pp, err := jpath.Parse(path) + if err != nil { + b.err = err + return b + } + + b.err = b.diff.Delete(pp, v) + + return b +} + +func (b *Builder) Build() (Differ, error) { + if b.err != nil { + return nil, b.err + } + if b.diff == nil { + return Ignore() + } + + return b.diff, nil +} + +type value struct { + Differ +} + +func (v *value) Add(path jpath.Path, i interface{}) error { + if len(path) == 0 && v.Differ == nil { + v.Differ = valueExcess{i} + return nil + } + if len(path) > 0 && v.Differ == nil { + v.Differ = emptyContainer(path[0]) + } + if len(path) > 0 { + diffBuilder, ok := v.Differ.(DiffBuilder) + if !ok { + return fmt.Errorf("cannot add value to %T(%+v)", v.Differ, v.Differ) + } + return diffBuilder.Add(path, i) + } + + if t, ok := v.Differ.(valueMissing); ok { + v.Differ = valueDiffers{ + lhs: t.value, + rhs: i, + } + + return nil + } + + return fmt.Errorf("cannot add value to %T(%+v)", v.Differ, v.Differ) +} + +func (v *value) Delete(path jpath.Path, i interface{}) error { + if len(path) == 0 && v.Differ == nil { + v.Differ = valueMissing{i} + return nil + } + if len(path) > 0 && v.Differ == nil { + v.Differ = emptyContainer(path[0]) + } + if len(path) > 0 { + diffBuilder, ok := v.Differ.(DiffBuilder) + if !ok { + return fmt.Errorf("cannot delete value from %T(%+v)", v.Differ, v.Differ) + } + return diffBuilder.Delete(path, i) + } + + return fmt.Errorf("cannot delete value to %T(%+v)", v.Differ, v.Differ) +} + +func (v *value) Walk(path string, fn WalkFn) error { + d, err := walk(v, v.Differ, path, fn) + if err != nil { + return err + } + if d != nil { + v.Differ = d + } + + return nil +} + +func emptyContainer(p jpath.PathPart) DiffBuilder { + switch p.Kind() { + default: + panic(fmt.Errorf("unknown path part %v", p.Kind())) + case jpath.PathKindIndex: + return emptySliceDiff() + case jpath.PathKindKey: + return emptyMapDiff() + } +} + +func emptyMapDiff() *mapDiff { + return &mapDiff{ + diffs: map[interface{}]Differ{}, + lhs: map[string]interface{}{}, + rhs: map[string]interface{}{}, + } +} + +func emptySliceDiff() *slice { + return &slice{ + diffs: []Differ{}, + indices: []int{}, + lhs: []interface{}{}, + rhs: []interface{}{}, + } +} + +type valueDiffers struct { + lhs interface{} + rhs interface{} +} + +func (v valueDiffers) Diff() Type { + return ContentDiffer +} + +func (v valueDiffers) Strings() []string { + return []string{ + fmt.Sprintf("- %T %v", v.lhs, v.lhs), + fmt.Sprintf("+ %T %v", v.rhs, v.rhs), + } +} + +func (v valueDiffers) StringIndent(key, prefix string, conf Output) string { + return "-" + prefix + key + conf.red(v.lhs) + newLineSeparatorString(conf) + + "+" + prefix + key + conf.green(v.rhs) +} + +func (v valueDiffers) LHS() interface{} { + return v.lhs +} + +func (v valueDiffers) RHS() interface{} { + return v.rhs +} + +type valueMissing struct { + value interface{} +} + +func (v valueMissing) Diff() Type { + return ContentDiffer +} + +func (v valueMissing) Strings() []string { + return []string{ + fmt.Sprintf("- %T %v", v.value, v.value), + } +} + +func (v valueMissing) StringIndent(key, prefix string, conf Output) string { + return "-" + prefix + key + conf.red(v.value) +} + +func (v valueMissing) LHS() interface{} { + return v.value +} + +type valueExcess struct { + value interface{} +} + +func (v valueExcess) Diff() Type { + return ContentDiffer +} + +func (v valueExcess) Strings() []string { + return []string{ + fmt.Sprintf("+ %T %v", v.value, v.value), + } +} + +func (v valueExcess) StringIndent(key, prefix string, conf Output) string { + return "+" + prefix + key + conf.green(v.value) +} + +func (v valueExcess) RHS() interface{} { + return v.value +} diff --git a/diff/builder_test.go b/diff/builder_test.go new file mode 100644 index 0000000..fc7befb --- /dev/null +++ b/diff/builder_test.go @@ -0,0 +1,647 @@ +package diff + +import ( + "reflect" + "strings" + "testing" + + "github.com/yazgazan/jaydiff/jpath" +) + +func TestBuilder(t *testing.T) { + t.Run("add value", func(t *testing.T) { + diff, err := (&Builder{}).Add("", 42).Build() + if err != nil { + t.Errorf(`Buidler{}.Add("", 42): unexpected error: %v`, err) + return + } + + testbuilderReport(t, diff, [][]string{ + {"+", "42"}, + }) + }) + t.Run("delete value", func(t *testing.T) { + diff, err := (&Builder{}).Delete("", 23).Build() + if err != nil { + t.Errorf(`Buidler{}.Delete("", 23): unexpected error: %v`, err) + return + } + + testbuilderReport(t, diff, [][]string{ + {"-", "23"}, + }) + }) + t.Run("replace value", func(t *testing.T) { + diff, err := (&Builder{}).Delete("", 23).Add("", 42).Build() + if err != nil { + t.Errorf(`Buidler{}.Delete("", 23).Add("", 42): unexpected error: %v`, err) + return + } + + testbuilderReport(t, diff, [][]string{ + {"-", "23", "+", "42"}, + }) + }) + + t.Run("add to map", func(t *testing.T) { + diff, err := (&Builder{}).Add(".foo", 42).Build() + if err != nil { + t.Errorf(`Builder{}.Add(".foo", 42): unexpected error: %v`, err) + return + } + + testbuilderReport(t, diff, [][]string{ + {"+", ".foo", "42"}, + }) + }) + + t.Run("multiple add to map", func(t *testing.T) { + diff, err := (&Builder{}).Add(".foo", 42).Add(".bar", 23).Build() + if err != nil { + t.Errorf(`Builder{}.Add(".foo", 42).Add(".bar", 23): unexpected error: %v`, err) + return + } + + testbuilderReport(t, diff, [][]string{ + {"+", ".bar", "23"}, + {"+", ".foo", "42"}, + }) + }) + + t.Run("delete from map", func(t *testing.T) { + diff, err := (&Builder{}).Delete(".foo", 23).Build() + if err != nil { + t.Errorf(`Builder{}.Delete(".foo", 23): unexpected error: %v`, err) + return + } + + testbuilderReport(t, diff, [][]string{ + {"-", ".foo", "23"}, + }) + }) + + t.Run("multiple delete from map", func(t *testing.T) { + diff, err := (&Builder{}).Delete(".foo", 23).Delete(".bar", 42).Build() + if err != nil { + t.Errorf(`Builder{}.Delete(".foo", 23).Delete(".bar", 42): unexpected error: %v`, err) + return + } + + testbuilderReport(t, diff, [][]string{ + {"-", ".bar", "42"}, + {"-", ".foo", "23"}, + }) + }) + + t.Run("replace in map", func(t *testing.T) { + diff, err := (&Builder{}).Delete(".foo", 23).Add(".foo", 42).Build() + if err != nil { + t.Errorf(`Builder{}.Delete(".foo", 23).Add(".foo", 42): unexpected error: %v`, err) + return + } + + testbuilderReport(t, diff, [][]string{ + {"-", "foo", "23", "+", ".foo", "42"}, + }) + }) + + t.Run("deep add to map", func(t *testing.T) { + diff, err := (&Builder{}).Add(".foo.bar", 42).Build() + if err != nil { + t.Errorf(`Builder{}.Add(".foo.bar", 42): unexpected error: %v`, err) + return + } + + testbuilderReport(t, diff, [][]string{ + {"+", ".foo.bar", "42"}, + }) + }) + + t.Run("add to slice", func(t *testing.T) { + diff, err := (&Builder{}).Add("[2]", 42).Build() + if err != nil { + t.Errorf(`Builder{}.Add("[2]", 42): unexpected error: %v`, err) + return + } + + testbuilderReport(t, diff, [][]string{ + {"+", "[2]", "42"}, + }) + }) + + t.Run("multiple add to slice", func(t *testing.T) { + diff, err := (&Builder{}).Add("[2]", 42).Add("[3]", 23).Build() + if err != nil { + t.Errorf(`Builder{}.Add("[2]", 42).Add("[3]", 23): unexpected error: %v`, err) + return + } + + testbuilderReport(t, diff, [][]string{ + {"+", "[2]", "42"}, + {"+", "[3]", "23"}, + }) + }) + + t.Run("delete from slice", func(t *testing.T) { + diff, err := (&Builder{}).Delete("[2]", 23).Build() + if err != nil { + t.Errorf(`Builder{}.Delete("[2]", 23): unexpected error: %v`, err) + return + } + + testbuilderReport(t, diff, [][]string{ + {"-", "[2]", "23"}, + }) + }) + + t.Run("complex diff", func(t *testing.T) { + b := (&Builder{}).Delete(".b[1]", 3).Add(".b[1]", 5).Add(".b[2]", 4) + b.Delete(".c.a", "toto").Add(".c.a", "titi") + b.Delete(".c.b", 23).Add(".c.b", "23") + b.Delete(".e", []interface{}{}) + b.Delete(".f", 42) + b.Add(".h", 42) + + diff, err := b.Build() + if err != nil { + t.Errorf(`Builder{}...: unexpected error: %v`, err) + return + } + + testbuilderReport(t, diff, [][]string{ + {"-", ".b[1]", "3", "+", ".b[1]", "5"}, + {"+", ".b[2]", "4"}, + {"-", ".c.a", `"toto"`, "+", ".c.a", `"titi"`}, + {"-", ".c.b", "23", "+", ".c.b", `"23"`}, + {"-", ".e", "[]"}, + {"-", ".f", "42"}, + {"+", ".h", "42"}, + }) + }) + + t.Run("delete same twice from map", func(t *testing.T) { + _, err := (&Builder{}).Delete(".a", 42).Delete(".a", 23).Add(".b", 0).Delete(".c", 1).Build() + + if err == nil { + t.Error(`Builder{}.Delete(".a", 42).Delete(".a", 23): expected error, got nil`) + } + }) + + t.Run("delete same twice from slice", func(t *testing.T) { + _, err := (&Builder{}).Delete("[1]", 42).Delete("[1]", 23).Add("[2]", 0).Delete("[3]", 1).Build() + + if err == nil { + t.Error(`Builder{}.Delete("[1]", 42).Delete("[1]", 23): expected error, got nil`) + } + }) + + t.Run("add invalid path", func(t *testing.T) { + _, err := (&Builder{}).Add(`."foo`, 42).Build() + + if err == nil { + t.Error("Builder{}.Add(`.\"foo`, 42): expected error, got nil") + } + }) + + t.Run("delete invalid path", func(t *testing.T) { + _, err := (&Builder{}).Delete(`."foo`, 42).Build() + + if err == nil { + t.Error("Builder{}.Delete(`.\"foo`, 42): expected error, got nil") + } + }) + + t.Run("nothing", func(t *testing.T) { + diff, err := (&Builder{}).Build() + if err != nil { + t.Errorf(`Builder{}.Build(): unexpected error: %v`, err) + return + } + + if !IsIgnore(diff) { + t.Errorf("Builder{}.Build() = %+v, expected Ignore{}", diff) + } + }) + + t.Run("path cannot be added (value)", func(t *testing.T) { + _, err := (&Builder{}).Add("", 4).Add(".bar", 5).Build() + + if err == nil { + t.Error(`Builder{}.Add("", 4).Add(".bar", 5): expected error, got nil`) + } + }) + + t.Run("path cannot be deleted (value)", func(t *testing.T) { + _, err := (&Builder{}).Add("", 4).Delete(".bar", 5).Build() + + if err == nil { + t.Error(`Builder{}.Add("", 4).Delete(".bar", 5): expected error, got nil`) + } + }) + + t.Run("path cannot be added (map)", func(t *testing.T) { + _, err := (&Builder{}).Add(".foo", 4).Add(".foo.bar", 5).Build() + + if err == nil { + t.Error(`Builder{}.Add(".foo", 4).Add(".foo.bar", 5): expected error, got nil`) + } + }) + + t.Run("path cannot be deleted (map)", func(t *testing.T) { + _, err := (&Builder{}).Add(".foo", 4).Delete(".foo.bar", 5).Build() + + if err == nil { + t.Error(`Builder{}.Add(".foo", 4).Delete(".foo.bar", 5): expected error, got nil`) + } + }) + + t.Run("add value twice", func(t *testing.T) { + _, err := (&Builder{}).Add("", 4).Add("", 5).Build() + + if err == nil { + t.Error(`Builder{}.Add("", 4).Add("", 5): expected error, got nil`) + } + }) + + t.Run("delete value twice", func(t *testing.T) { + _, err := (&Builder{}).Delete("", 4).Delete("", 5).Build() + + if err == nil { + t.Error(`Builder{}.Delete("", 4).Delete("", 5): expected error, got nil`) + } + }) + + t.Run("add to map twice", func(t *testing.T) { + _, err := (&Builder{}).Add(".foo", 4).Add(".foo", 5).Build() + + if err == nil { + t.Error(`Builder{}.Add(".foo", 4).Add(".foo", 5): expected error, got nil`) + } + }) + + t.Run("del from map twice", func(t *testing.T) { + _, err := (&Builder{}).Delete(".foo", 4).Delete(".foo", 5).Build() + + if err == nil { + t.Error(`Builder{}.Delete(".foo", 4).Delete(".foo", 5): expected error, got nil`) + } + }) + + t.Run("del after add in map", func(t *testing.T) { + _, err := (&Builder{}).Add(".foo", 4).Delete(".foo", 4).Build() + + if err == nil { + t.Error(`Builder{}.Add(".foo", 4).Delete(".foo", 4): expected error, got nil`) + } + }) + + t.Run("add to slice twice", func(t *testing.T) { + _, err := (&Builder{}).Add("[5]", 4).Add("[5]", 5).Build() + + if err == nil { + t.Error(`Builder{}.Add("[5]", 4).Add("[5]", 5): expected error, got nil`) + } + }) + + t.Run("del from slice twice", func(t *testing.T) { + _, err := (&Builder{}).Delete("[5]", 4).Delete("[5]", 5).Build() + + if err == nil { + t.Error(`Builder{}.Delete("[5]", 4).Delete("[5]", 5): expected error, got nil`) + } + }) + + t.Run("del after add in slice", func(t *testing.T) { + _, err := (&Builder{}).Add("[5]", 4).Delete("[5]", 4).Build() + + if err == nil { + t.Error(`Builder{}.Add("[5]", 4).Delete("[5]", 4): expected error, got nil`) + } + }) + + t.Run("add to slice reverse order", func(t *testing.T) { + diff, err := (&Builder{}).Add("[5]", 4).Add("[2].foo", 1).Build() + + if err != nil { + t.Errorf(`Builder{}.Add("[5]", 4).Add("[2].foo", 1): unexpected error :%v`, err) + return + } + + testbuilderReport(t, diff, [][]string{ + {"+", "[2].foo", "1"}, + {"+", "[5]", "4"}, + }) + }) + + t.Run("add to slice after delete", func(t *testing.T) { + _, err := (&Builder{}).Delete("[2]", 4).Add("[2].foo", 1).Build() + + if err == nil { + t.Error(`Builder{}.Delete("[2]", 4).Add("[2].foo", 1): expected error, got nil`) + return + } + }) + + t.Run("delete from slice reverse order", func(t *testing.T) { + diff, err := (&Builder{}).Delete("[5]", 4).Delete("[2].foo", 1).Build() + + if err != nil { + t.Errorf(`Builder{}.Delete("[5]", 4).Delete("[2].foo", 1): unexpected error :%v`, err) + return + } + + testbuilderReport(t, diff, [][]string{ + {"-", "[2].foo", "1"}, + {"-", "[5]", "4"}, + }) + }) + + t.Run("delete from slice after add", func(t *testing.T) { + _, err := (&Builder{}).Add("[2]", 4).Delete("[2].foo", 1).Build() + + if err == nil { + t.Error(`Builder{}.Add("[2]", 4).Delete("[2].foo", 1): expected error, got nil`) + return + } + }) +} + +func testbuilderReport(t *testing.T, diff Differ, wants [][]string) { + t.Helper() + + ss, err := Report(diff, Output{ + JSONValues: true, + }) + if err != nil { + t.Errorf("failed to generate report: %v", err) + t.Fail() + } + + for i, want := range wants { + s := ss[i] + + for i, needle := range want { + if !strings.Contains(s, needle) { + t.Errorf("Builder{}.Report[%d] = %q, expected it to contain %q", i, s, needle) + } + } + } +} + +func testbuilderStrings(t *testing.T, diff Differ, wants [][]string) { + t.Helper() + + ss := diff.Strings() + for i, want := range wants { + s := ss[i] + + for i, needle := range want { + if !strings.Contains(s, needle) { + t.Errorf("Builder{}.Strings()[%d] = %q, expected it to contain %q", i, s, needle) + } + } + } +} + +func testbuilderStringIndent(t *testing.T, diff Differ, wants []string) { + t.Helper() + + const ( + key = "(key)" + prefix = "(prefix)" + ) + + s := diff.StringIndent(key, prefix, Output{}) + if !strings.Contains(s, key) { + t.Errorf(".StringIndent() = %q, expected it to contain %q", s, key) + } + if !strings.Contains(s, prefix) { + t.Errorf(".StringIndent() = %q, expected it to contain %q", s, prefix) + } + for i, needle := range wants { + if !strings.Contains(s, needle) { + t.Errorf("Builder{}.Strings()[%d] = %q, expected it to contain %q", i, s, needle) + } + } +} + +type invalidPathPart struct{} + +func (p invalidPathPart) Kind() jpath.PathKind { + return -1 +} + +func (p invalidPathPart) String() string { + return "" +} + +func TestEmptyContainer(t *testing.T) { + defer func() { + err := recover() + if err == nil { + t.Errorf("emptyContainer(invalidPathPart{}): expected panic") + } + }() + + _ = emptyContainer(invalidPathPart{}) +} + +func TestMapDiffAdd(t *testing.T) { + t.Run("empty path", func(t *testing.T) { + err := emptyMapDiff().Add(jpath.Path{}, 42) + + if err == nil { + t.Error("mapDiff{}.Add([], ...): expected error, got nil") + } + }) + t.Run("wrong path", func(t *testing.T) { + err := emptyMapDiff().Add(jpath.Path{jpath.PathIndex(2)}, 42) + + if err == nil { + t.Error("mapDiff{}.Add(jpath.Path{jpath.PathIndex(2)}, ...): expected error, got nil") + } + }) +} + +func TestMapDiffDelete(t *testing.T) { + t.Run("empty path", func(t *testing.T) { + err := emptyMapDiff().Delete(jpath.Path{}, 42) + + if err == nil { + t.Error("mapDiff{}.Delete([], ...): expected error, got nil") + } + }) + t.Run("wrong path", func(t *testing.T) { + err := emptyMapDiff().Delete(jpath.Path{jpath.PathIndex(2)}, 42) + + if err == nil { + t.Error("mapDiff{}.Delete(jpath.Path{jpath.PathIndex(2)}, ...): expected error, got nil") + } + }) +} + +func TestSliceDiffAdd(t *testing.T) { + t.Run("empty path", func(t *testing.T) { + err := emptySliceDiff().Add(jpath.Path{}, 42) + + if err == nil { + t.Error("slice{}.Add([], ...): expected error, got nil") + } + }) + t.Run("wrong path", func(t *testing.T) { + err := emptySliceDiff().Add(jpath.Path{jpath.PathKey(`foo`)}, 42) + + if err == nil { + t.Error("slice{}.Add(jpath.Path{jpath.PathKey(`foo`)}, ...): expected error, got nil") + } + }) +} + +func TestSliceDiffDelete(t *testing.T) { + t.Run("empty path", func(t *testing.T) { + err := emptySliceDiff().Delete(jpath.Path{}, 42) + + if err == nil { + t.Error("slice{}.Delete([], ...): expected error, got nil") + } + }) + t.Run("wrong path", func(t *testing.T) { + err := emptySliceDiff().Delete(jpath.Path{jpath.PathKey(`foo`)}, 42) + + if err == nil { + t.Error("slice{}.Delete(jpath.Path{jpath.PathKey(`foo`)}, ...): expected error, got nil") + } + }) +} + +func TestValueDiffers(t *testing.T) { + t.Run("Diff()", func(t *testing.T) { + dt := valueDiffers{42, 23}.Diff() + if dt != ContentDiffer { + t.Errorf("valueDiffers{42, 23}.Diff() = %v, expected %v", dt, ContentDiffer) + } + }) + + t.Run("Strings()", func(t *testing.T) { + d := valueDiffers{ + lhs: 42, + rhs: 23, + } + + testbuilderStrings(t, d, [][]string{ + {"-", "42"}, + {"+", "23"}, + }) + }) + + t.Run("StringIndent()", func(t *testing.T) { + d := valueDiffers{ + lhs: 42, + rhs: 23, + } + + testbuilderStringIndent(t, d, []string{ + "-", "42", "+", "23", + }) + }) + + t.Run("LHS()", func(t *testing.T) { + d := valueDiffers{ + lhs: 42, + rhs: 23, + } + + if !reflect.DeepEqual(d.LHS(), 42) { + t.Errorf("valueDiffers{42, 23}.LHS() = %v, expected 42", d.LHS()) + } + }) + + t.Run("RHS()", func(t *testing.T) { + d := valueDiffers{ + lhs: 42, + rhs: 23, + } + + if !reflect.DeepEqual(d.RHS(), 23) { + t.Errorf("valueDiffers{42, 23}.RHS() = %v, expected 23", d.RHS()) + } + }) +} + +func TestValueMissing(t *testing.T) { + t.Run("Diff()", func(t *testing.T) { + dt := valueMissing{42}.Diff() + if dt != ContentDiffer { + t.Errorf("valueMissing{42}.Diff() = %v, expected %v", dt, ContentDiffer) + } + }) + + t.Run("Strings()", func(t *testing.T) { + d := valueMissing{ + value: 42, + } + + testbuilderStrings(t, d, [][]string{ + {"-", "42"}, + }) + }) + + t.Run("StringIndent()", func(t *testing.T) { + d := valueMissing{ + value: 42, + } + + testbuilderStringIndent(t, d, []string{ + "-", "42", + }) + }) + + t.Run("LHS()", func(t *testing.T) { + d := valueMissing{ + value: 42, + } + + if !reflect.DeepEqual(d.LHS(), 42) { + t.Errorf("valueMissing{42}.LHS() = %v, expected 42", d.LHS()) + } + }) +} + +func TestValueExcess(t *testing.T) { + t.Run("Diff()", func(t *testing.T) { + dt := valueExcess{42}.Diff() + if dt != ContentDiffer { + t.Errorf("valueExcess{42}.Diff() = %v, expected %v", dt, ContentDiffer) + } + }) + + t.Run("Strings()", func(t *testing.T) { + d := valueExcess{ + value: 42, + } + + testbuilderStrings(t, d, [][]string{ + {"+", "42"}, + }) + }) + + t.Run("StringIndent()", func(t *testing.T) { + d := valueExcess{ + value: 42, + } + + testbuilderStringIndent(t, d, []string{ + "+", "42", + }) + }) + + t.Run("RHS()", func(t *testing.T) { + d := valueExcess{ + value: 42, + } + + if !reflect.DeepEqual(d.RHS(), 42) { + t.Errorf("valueExcess{42}.RHS() = %v, expected 42", d.RHS()) + } + }) +} diff --git a/diff/diff.go b/diff/diff.go index 4bf74b2..d9c9445 100644 --- a/diff/diff.go +++ b/diff/diff.go @@ -3,6 +3,8 @@ package diff import ( "reflect" + + "github.com/yazgazan/jaydiff/jpath" ) // Type is used to specify the nature of the difference @@ -27,6 +29,12 @@ type Differ interface { StringIndent(key, prefix string, conf Output) string } +type DiffBuilder interface { + Differ + Add(path jpath.Path, i interface{}) error + Delete(path jpath.Path, i interface{}) error +} + type diffFn func(c config, lhs, rhs interface{}, visited *visited) (Differ, error) // Diff generates a tree representing differences and similarities between two objects. diff --git a/diff/diff_test.go b/diff/diff_test.go index e6b68c1..d27cce5 100644 --- a/diff/diff_test.go +++ b/diff/diff_test.go @@ -1084,6 +1084,8 @@ func TestReport(t *testing.T) { } func testStrings(context string, t *testing.T, wants [][]string, ss []string, indented string) { + t.Helper() + for i, want := range wants { s := ss[i] diff --git a/diff/map.go b/diff/map.go index 7f11aea..771ce44 100644 --- a/diff/map.go +++ b/diff/map.go @@ -1,6 +1,7 @@ package diff import ( + "errors" "fmt" "reflect" "sort" @@ -177,6 +178,81 @@ func (m mapDiff) StringIndent(keyprefix, prefix string, conf Output) string { return "" } +func (m *mapDiff) Add(path jpath.Path, i interface{}) error { + if len(path) == 0 { + return errors.New("cannot add value to empty path") + } + key, ok := path[0].(jpath.PathKey) + if !ok { + return fmt.Errorf("cannot add value to %T path", path[0]) + } + + path = path[1:] + d, ok := m.diffs[string(key)] + if len(path) > 0 && !ok { + d = emptyContainer(path[0]) + m.diffs[string(key)] = d + } + if len(path) > 0 { + diffBuilder, ok := d.(DiffBuilder) + if !ok { + return fmt.Errorf("cannot add value to %T(%+v)", d, d) + } + return diffBuilder.Add(path, i) + } + + if len(path) == 0 && !ok { + m.diffs[string(key)] = mapExcess{ + value: i, + } + return nil + } + + if t, ok := d.(mapMissing); ok { + m.diffs[string(key)] = valueDiffers{ + lhs: t.value, + rhs: i, + } + + return nil + } + + return fmt.Errorf("cannot add value to %T(%+v)", d, d) +} + +func (m *mapDiff) Delete(path jpath.Path, i interface{}) error { + if len(path) == 0 { + return errors.New("cannot delete value from empty path") + } + key, ok := path[0].(jpath.PathKey) + if !ok { + return fmt.Errorf("cannot delete value from %T path", path[0]) + } + + path = path[1:] + d, ok := m.diffs[string(key)] + if len(path) > 0 && !ok { + d = emptyContainer(path[0]) + m.diffs[string(key)] = d + } + if len(path) > 0 { + diffBuilder, ok := d.(DiffBuilder) + if !ok { + return fmt.Errorf("cannot delete value from %T(%+v)", d, d) + } + return diffBuilder.Delete(path, i) + } + + if len(path) == 0 && !ok { + m.diffs[string(key)] = mapMissing{ + value: i, + } + return nil + } + + return fmt.Errorf("cannot delete value from %T(%+v)", d, d) +} + func (m mapDiff) openString(keyprefix, prefix string, conf Output) string { if conf.JSON { return " " + prefix + keyprefix + "{" diff --git a/diff/report.go b/diff/report.go index 3ca1246..8a1003d 100644 --- a/diff/report.go +++ b/diff/report.go @@ -6,7 +6,7 @@ package diff func Report(d Differ, outConf Output) ([]string, error) { var errs []string - _, err := Walk(d, func(parent, diff Differ, path string) (Differ, error) { + _, err := Walk(d, func(_, diff Differ, path string) (Differ, error) { switch diff.Diff() { case Identical: return nil, nil diff --git a/diff/slice.go b/diff/slice.go index 6757c6b..1cbd5f9 100644 --- a/diff/slice.go +++ b/diff/slice.go @@ -1,12 +1,14 @@ package diff import ( + "errors" "fmt" "reflect" "strconv" "strings" myersdiff "github.com/mb0/diff" + "github.com/yazgazan/jaydiff/jpath" ) type slice struct { @@ -256,6 +258,102 @@ func (s slice) StringIndent(key, prefix string, conf Output) string { return "" } +func (s *slice) Add(path jpath.Path, i interface{}) error { + if len(path) == 0 { + return errors.New("cannot add value to empty path") + } + idx, ok := path[0].(jpath.PathIndex) + if !ok { + return fmt.Errorf("cannot add value to %T path", path[0]) + } + + return s.add(int(idx), path[1:], i) +} + +func (s *slice) add(idx int, path jpath.Path, i interface{}) error { + if len(s.diffs) <= idx { + s.addIgnoreUntil(idx) + } + d := s.diffs[idx] + if len(path) > 0 && IsIgnore(d) { + d = emptyContainer(path[0]) + s.diffs[idx] = d + } + if len(path) > 0 { + diffBuilder, ok := d.(DiffBuilder) + if !ok { + return fmt.Errorf("cannot add value to %T(%+v)", d, d) + } + return diffBuilder.Add(path, i) + } + + if len(path) == 0 && IsIgnore(d) { + s.diffs[idx] = sliceExcess{ + value: i, + } + + return nil + } + + if t, ok := d.(sliceMissing); ok { + s.diffs[idx] = valueDiffers{ + lhs: t.value, + rhs: i, + } + + return nil + } + + return fmt.Errorf("cannot add value to %T(%+v)", d, d) +} + +func (s *slice) Delete(path jpath.Path, i interface{}) error { + if len(path) == 0 { + return errors.New("cannot delete value from empty path") + } + idx, ok := path[0].(jpath.PathIndex) + if !ok { + return fmt.Errorf("cannot delete value from %T path", path[0]) + } + + return s.delete(int(idx), path[1:], i) +} + +func (s *slice) delete(idx int, path jpath.Path, i interface{}) error { + if len(s.diffs) <= idx { + s.addIgnoreUntil(idx) + } + d := s.diffs[idx] + if len(path) > 0 && IsIgnore(d) { + d = emptyContainer(path[0]) + s.diffs[idx] = d + } + if len(path) > 0 { + diffBuilder, ok := d.(DiffBuilder) + if !ok { + return fmt.Errorf("cannot delete value from %T(%+v)", d, d) + } + return diffBuilder.Delete(path, i) + } + + if len(path) == 0 && IsIgnore(d) { + s.diffs[idx] = sliceMissing{ + value: i, + } + + return nil + } + + return fmt.Errorf("cannot delete value to %T(%+v)", d, d) +} + +func (s *slice) addIgnoreUntil(idx int) { + for len(s.diffs) <= idx { + s.diffs = append(s.diffs, ignore{}) + s.indices = append(s.indices, len(s.diffs)-1) + } +} + func (s slice) openString(key, prefix string, conf Output) string { if conf.JSON { return " " + prefix + key + "[" diff --git a/diff/walk_test.go b/diff/walk_test.go index af30e93..61c3983 100644 --- a/diff/walk_test.go +++ b/diff/walk_test.go @@ -157,3 +157,27 @@ func TestIsMissing(t *testing.T) { } } } + +func TestValueWalk(t *testing.T) { + t.Run("returning error", func(t *testing.T) { + walkError := errors.New("fake error") + + diff, err := (&Builder{}).Add("", 5).Build() + if err != nil { + t.Errorf(`Builder{}.Add("", 5): unexpected error: %s`, err) + return + } + + _, err = Walk(diff, func(parent Differ, diff Differ, path string) (Differ, error) { + if _, ok := diff.(valueExcess); ok { + return nil, walkError + } + + return nil, nil + }) + + if err != walkError { + t.Errorf("Walk(...): expected fake error, got %v", err) + } + }) +} diff --git a/jpath/jpath.go b/jpath/jpath.go index 4874d3d..14a4423 100644 --- a/jpath/jpath.go +++ b/jpath/jpath.go @@ -55,6 +55,8 @@ func isUnescapedQuote(s string) bool { return s[0] != '\\' && s[1] == '"' } +const keySpecials = "[].\":" + // HasSuffix tests whether the string s ends with suffix, ignoring indices in brackets. func HasSuffix(s, suffix string) bool { stripped := StripIndices(s) @@ -67,12 +69,12 @@ func HasSuffix(s, suffix string) bool { func EscapeKey(v interface{}) string { s, ok := v.(string) if !ok { - return fmt.Sprintf("%v", v) + s = fmt.Sprintf("%v", v) } - if s != "" && !strings.ContainsAny(s, "[].\"") { + if s != "" && !strings.ContainsAny(s, keySpecials) { return s } - return fmt.Sprintf("%q", s) + return strconv.Quote(s) } func Split(path string) (head, tail string) { @@ -114,66 +116,211 @@ func getKey(s string, kind reflect.Kind) (reflect.Value, error) { } func ExecutePath(path string, i interface{}) (interface{}, error) { - // TODO(yazgazan): better errors - head, tail := Split(path) - if head == "" { + pp, _, err := parsePath(path) + if err != nil { + return nil, err + } + + return executePath(pp, i) +} + +func executePath(path []PathPart, i interface{}) (interface{}, error) { + if len(path) == 0 { return i, nil } + head, tail := path[0], path[1:] v := reflect.ValueOf(i) - switch head[0] { + switch head.Kind() { default: return nil, ErrInvalidPath - case '[': - return executeSlice(head, tail, v) - case '.': - return executeMap(head, tail, v) + case PathKindIndex: + return executeSlice(head.(PathIndex), tail, v) + case PathKindKey: + return executeMap(head.(PathKey), tail, v) } } -func executeSlice(head, tail string, v reflect.Value) (interface{}, error) { +func executeSlice(idx PathIndex, tail []PathPart, v reflect.Value) (interface{}, error) { if v.Kind() != reflect.Slice { return nil, ErrNotSlice } if v.IsNil() { return nil, ErrNil } - if head[len(head)-1] != ']' { - return nil, ErrInvalidPath - } - index, err := strconv.Atoi(head[1 : len(head)-1]) - if err != nil { - return nil, err - } - if index >= v.Len() { + + if int(idx) >= v.Len() { return nil, ErrOutOfBounds } - val := v.Index(index) + val := v.Index(int(idx)) if !val.CanInterface() { return nil, ErrInvalidInterface } - return ExecutePath(tail, val.Interface()) + return executePath(tail, val.Interface()) } -func executeMap(head, tail string, v reflect.Value) (interface{}, error) { +func executeMap(keyStr PathKey, tail []PathPart, v reflect.Value) (interface{}, error) { if v.Kind() != reflect.Map { return nil, ErrNotMap } - keyStr := head[1:] - if keyStr == "" { - return nil, ErrInvalidPath + + if v.IsNil() { + return nil, ErrNil } - key, err := getKey(keyStr, v.Type().Key().Kind()) + key, err := getKey(string(keyStr), v.Type().Key().Kind()) if err != nil { return nil, err } - if v.IsNil() { - return nil, ErrNil - } val := v.MapIndex(key) if !val.CanInterface() { return nil, ErrInvalidInterface } - return ExecutePath(tail, val.Interface()) + return executePath(tail, val.Interface()) +} + +//go:generate stringer -type PathKind +type PathKind int + +const ( + PathKindIndex PathKind = iota + PathKindKey +) + +type PathPart interface { + Kind() PathKind + String() string +} + +type PathIndex int + +func (i PathIndex) Kind() PathKind { + return PathKindIndex +} + +func (i PathIndex) String() string { + return "[" + strconv.Itoa(int(i)) + "]" +} + +type PathKey string + +func (k PathKey) Kind() PathKind { + return PathKindKey +} + +func (k PathKey) String() string { + return "." + EscapeKey(string(k)) +} + +type Path []PathPart + +func Parse(path string) (Path, error) { + pp, i, err := parsePath(path) + if err != nil { + return nil, err + } + if i != len(path) { + return nil, fmt.Errorf("parsing path: unexpected %q", path[i:]) + } + + return pp, nil +} + +func parsePath(path string) (Path, int, error) { + var ( + part PathPart + i int + err error + ) + + if path == "" { + return Path{}, 0, nil + } + + switch path[0] { + default: + return Path{}, 0, nil + case '.': + part, i, err = parseKey(path) + case '[': + part, i, err = parseIndex(path) + } + if err != nil { + return nil, 0, err + } + path = path[i:] + + parts, j, err := parsePath(path) + if err != nil { + return Path{part}, i + j, err + } + + return append(Path{part}, parts...), i + j, nil +} + +func parseKey(path string) (PathKey, int, error) { + i := 1 + if len(path) <= 1 { + return "", i, errors.New("expected key after '.'") + } + + if path[i] == '"' { + return parseQuotedKey(path) + } + + for ; i < len(path) && !strings.ContainsAny(keySpecials, path[i:i+1]); i++ { + } + + return PathKey(path[1:i]), i, nil +} + +func parseQuotedKey(path string) (PathKey, int, error) { + i := 1 + + i++ + escaping := false + for ; i < len(path); i++ { + if escaping { + escaping = false + continue + } + if path[i] == '\\' { + escaping = true + continue + } + if path[i] == '"' { + break + } + } + if escaping || i >= len(path) { + return "", i, errors.New("malformed key") + } + s, err := strconv.Unquote(path[1 : i+1]) + if err != nil { + fmt.Println(path[1 : i+1]) + return "", i, err + } + + return PathKey(s), i + 1, nil +} + +func parseIndex(path string) (PathIndex, int, error) { + i := 1 + if len(path) < 3 { + return 0, i, errors.New("expected index to be of the form [number]") + } + + for ; i < len(path) && path[i] != ']'; i++ { + } + + if i == len(path) { + return 0, i, errors.New("expected index to be of the form [number]") + } + + n, err := strconv.ParseInt(path[1:i], 10, strconv.IntSize) + if err != nil { + return 0, i, err + } + + return PathIndex(n), i + 1, nil } diff --git a/jpath/jpath_test.go b/jpath/jpath_test.go index e55569f..dfd2026 100644 --- a/jpath/jpath_test.go +++ b/jpath/jpath_test.go @@ -21,6 +21,7 @@ func TestStripIndices(t *testing.T) { {`."f[oo]"[22]`, `."f[oo]"[]`}, {`."f[00]"[22]`, `."f[00]"[]`}, {`."f[0\"]"[22]`, `."f[0\"]"[]`}, + {`."foo[42]`, `."foo[42]`}, } { got := StripIndices(test.In) if got != test.Expected { @@ -30,6 +31,8 @@ func TestStripIndices(t *testing.T) { } func TestEscapeKey(t *testing.T) { + type CustomString string + for _, test := range []struct { In interface{} Expected string @@ -39,6 +42,8 @@ func TestEscapeKey(t *testing.T) { {"42", `42`}, {`"foo`, `"\"foo"`}, {"[foo]", `"[foo]"`}, + {"foo:bar", `"foo:bar"`}, + {CustomString("foo:bar"), `"foo:bar"`}, {42, "42"}, } { got := EscapeKey(test.In) @@ -92,27 +97,74 @@ func TestSplit(t *testing.T) { } } +func TestGetKey(t *testing.T) { + for _, test := range []struct { + S string + Kind reflect.Kind + Expected interface{} + ExpectError bool + }{ + { + S: "foo", + Kind: reflect.String, + Expected: "foo", + }, + { + S: "42", + Kind: reflect.Int, + Expected: 42, + }, + { + S: "foo", + Kind: reflect.Struct, + ExpectError: true, + }, + { + S: "foo", + Kind: reflect.Int, + ExpectError: true, + }, + } { + got, err := getKey(test.S, test.Kind) + if test.ExpectError && err == nil { + t.Errorf("getKey(%q, %v): expected error, got nil", test.S, test.Kind) + } + if !test.ExpectError && err != nil { + t.Errorf("getKey(%q, %v): unexpected error: %v", test.S, test.Kind, err) + } + if err != nil || test.ExpectError { + continue + } + + i := got.Interface() + if !reflect.DeepEqual(i, test.Expected) { + t.Errorf("getKey(%q, %v) = %v, expected %v", test.S, test.Kind, got, test.Expected) + } + } +} + func TestExecutePath(t *testing.T) { for _, test := range []struct { - I interface{} - Path string - Expected interface{} + I interface{} + Path string + Expected interface{} + ExpectError bool }{ { - map[string]int{"foo": 42}, - ".foo", - 42, + I: map[string]int{"foo": 42}, + Path: ".foo", + Expected: 42, }, { - map[string][]int{ + I: map[string][]int{ "foo": []int{1, 2, 3}, "bar": []int{4, 5, 6}, }, - ".foo[1]", - 2, + Path: ".foo[1]", + Expected: 2, }, { - map[string][]map[int]string{ + I: map[string][]map[int]string{ "foo": []map[int]string{ map[int]string{ 23: "ha", @@ -120,17 +172,535 @@ func TestExecutePath(t *testing.T) { }, }, }, - ".foo[0].23", - "ha", + Path: ".foo[0].23", + Expected: "ha", + }, + { + I: map[string]interface{}{ + "foo": []interface{}{ + 42, + 23, + }, + }, + Path: `."foo"[1]`, + Expected: 23, + }, + { + I: map[string]interface{}{ + "foo[0]": []interface{}{ + 42, + 23, + }, + }, + Path: `."foo[0]"[1]`, + Expected: 23, + }, + { + I: []interface{}{ + map[string]interface{}{ + "foo": 23, + "bar": 42, + }, + }, + Path: "[0].foo", + Expected: 23, + }, + { + I: []interface{}{ + 42, + }, + Path: ".foo", + ExpectError: true, + }, + { + I: map[string]interface{}{ + "foo": 42, + }, + Path: "[2]", + ExpectError: true, + }, + { + I: []interface{}(nil), + Path: "[2]", + ExpectError: true, + }, + { + I: map[string]interface{}(nil), + Path: ".foo", + ExpectError: true, + }, + { + I: map[struct{}]interface{}{}, + Path: ".foo", + ExpectError: true, + }, + { + I: []interface{}{}, + Path: "[2]", + ExpectError: true, }, } { got, err := ExecutePath(test.Path, test.I) - if err != nil { - t.Errorf("ExecutePath(%q, %+v): unexpected error: %s", test.Path, test.I, err) + if !test.ExpectError && err != nil { + t.Errorf("ExecutePath(%q, %+v): unexpected error: %v", test.Path, test.I, err) + } + if test.ExpectError && err == nil { + t.Errorf("ExecutePath(%q, %+v): expected error: got nil", test.Path, test.I) + } + if test.ExpectError || err != nil { continue } + if !reflect.DeepEqual(got, test.Expected) { t.Errorf("ExecutePath(%q, %+v) = %+v, expected %+v", test.Path, test.I, got, test.Expected) } } + + malformedPath := `."foo[4]` + _, err := ExecutePath(malformedPath, map[string][]string{ + `"foo`: []string{"a", "b", "c", "d"}, + }) + if err == nil { + t.Errorf("ExecutePath(%q): expected error, got nil", malformedPath) + } +} + +type invalidPathPart struct{} + +func (p invalidPathPart) Kind() PathKind { + return -1 +} + +func (p invalidPathPart) String() string { + return "" +} + +func TestExecutePath2(t *testing.T) { + t.Run("invalid path", func(t *testing.T) { + _, err := executePath(Path{invalidPathPart{}}, 42) + + if err == nil { + t.Error("executePath(Path{invalidPathPart{}}): expected error, got nil") + } + }) +} + +func TestExecuteSlice(t *testing.T) { + t.Run("accessing private field", func(t *testing.T) { + type Foo struct { + private []int + } + + f := Foo{ + private: []int{1, 2, 3}, + } + v := reflect.ValueOf(f).FieldByName("private") + + _, err := executeSlice(PathIndex(0), Path{}, v) + if err == nil { + t.Error("executeSlice(0, [], fromPrivateField): expected error, got nil") + } + }) +} + +func TestExecuteMap(t *testing.T) { + t.Run("accessing private field", func(t *testing.T) { + type Foo struct { + private map[string]int + } + + f := Foo{ + private: map[string]int{ + "foo": 42, + }, + } + v := reflect.ValueOf(f).FieldByName("private") + + _, err := executeMap(PathKey("foo"), Path{}, v) + if err == nil { + t.Error(`executeMap("foo", [], fromPrivateField): expected error, got nil`) + } + }) +} + +func TestParseKey(t *testing.T) { + for _, test := range []struct { + In string + Expected PathKey + I int + ExpectError bool + }{ + { + In: ".foo[42]", + Expected: "foo", + I: 4, + }, + { + In: ".foo", + Expected: "foo", + I: 4, + }, + { + In: ".foo: ", + Expected: "foo", + I: 4, + }, + { + In: `."foo".bar`, + Expected: "foo", + I: 6, + }, + { + In: `."foo"`, + Expected: "foo", + I: 6, + }, + { + In: `."foo": `, + Expected: "foo", + I: 6, + }, + { + In: `."foo\"bar"`, + Expected: `foo"bar`, + I: 11, + }, + { + In: ".", + ExpectError: true, + }, + { + In: `."foo`, + ExpectError: true, + }, + { + In: `."foo\"bar`, + ExpectError: true, + }, + { + In: `."foo\`, + ExpectError: true, + }, + } { + got, i, err := parseKey(test.In) + if test.ExpectError && err == nil { + t.Errorf("parseKey(%q): expected error, got nil", test.In) + continue + } + if !test.ExpectError && err != nil { + t.Errorf("parseKey(%q): unexpected error: %v", test.In, err) + } + if err != nil { + continue + } + + if got != test.Expected { + t.Errorf("parseKey(%q) = %q, expected %q", test.In, got, test.Expected) + } + if i != test.I { + t.Errorf("parseKey(%q) = [%d], expected [%d]", test.In, i, test.I) + } + } +} + +func TestParseIndex(t *testing.T) { + for _, test := range []struct { + In string + Expected PathIndex + I int + ExpectError bool + }{ + { + In: "[42].foo", + Expected: 42, + I: 4, + }, + { + In: "[3].foo", + Expected: 3, + I: 3, + }, + { + In: "[3]", + Expected: 3, + I: 3, + }, + { + In: "[3]:", + Expected: 3, + I: 3, + }, + { + In: "[a]", + ExpectError: true, + }, + { + In: "[]", + ExpectError: true, + }, + { + In: "[", + ExpectError: true, + }, + { + In: "[42", + ExpectError: true, + }, + } { + got, i, err := parseIndex(test.In) + if test.ExpectError && err == nil { + t.Errorf("parseIndex(%q): expected error, got nil", test.In) + continue + } + if !test.ExpectError && err != nil { + t.Errorf("parseIndex(%q): unexpected error: %v", test.In, err) + } + if err != nil { + continue + } + + if got != test.Expected { + t.Errorf("parseIndex(%q) = %d, expected %d", test.In, got, test.Expected) + } + if i != test.I { + t.Errorf("parseIndex(%q) = [%d], expected [%d]", test.In, i, test.I) + } + } +} + +func TestParsePath(t *testing.T) { + for _, test := range []struct { + In string + Expected Path + I int + ExpectError bool + }{ + { + In: ".foo.bar", + Expected: Path{ + PathKey("foo"), + PathKey("bar"), + }, + I: 8, + }, + { + In: "[2]", + Expected: Path{ + PathIndex(2), + }, + I: 3, + }, + { + In: "[2].foo: bar", + Expected: Path{ + PathIndex(2), + PathKey("foo"), + }, + I: 7, + }, + { + In: `[2]."": bar`, + Expected: Path{ + PathIndex(2), + PathKey(""), + }, + I: 6, + }, + { + In: ".foo.bar: ", + Expected: Path{ + PathKey("foo"), + PathKey("bar"), + }, + I: 8, + }, + { + In: `.foo.bar[42]."hello world!": `, + Expected: Path{ + PathKey("foo"), + PathKey("bar"), + PathIndex(42), + PathKey("hello world!"), + }, + I: 27, + }, + { + In: ": ", + Expected: Path{}, + I: 0, + }, + { + In: `.foo.bar[42]."hello world!: `, + ExpectError: true, + }, + } { + got, i, err := parsePath(test.In) + if test.ExpectError && err == nil { + t.Errorf("parsePath(%q): expected error, got nil", test.In) + continue + } + if !test.ExpectError && err != nil { + t.Errorf("parsePath(%q): unexpected error: %v", test.In, err) + } + if err != nil { + continue + } + + if !reflect.DeepEqual(got, test.Expected) { + t.Errorf("parsePath(%q) = %v, expected %v", test.In, got, test.Expected) + } + if i != test.I { + t.Errorf("parsePath(%q) = [%d], expected [%d]", test.In, i, test.I) + } + } +} + +func TestParse(t *testing.T) { + for _, test := range []struct { + In string + Expected Path + ExpectError bool + }{ + { + In: ".foo.bar", + Expected: Path{ + PathKey("foo"), + PathKey("bar"), + }, + }, + { + In: "[2]", + Expected: Path{ + PathIndex(2), + }, + }, + { + In: "[2].foo: bar", + ExpectError: true, + }, + { + In: `[2].""`, + Expected: Path{ + PathIndex(2), + PathKey(""), + }, + }, + { + In: ".foo.bar", + Expected: Path{ + PathKey("foo"), + PathKey("bar"), + }, + }, + { + In: `.foo.bar[42]."hello world!"`, + Expected: Path{ + PathKey("foo"), + PathKey("bar"), + PathIndex(42), + PathKey("hello world!"), + }, + }, + { + In: "", + Expected: Path{}, + }, + { + In: `.foo."ba\xzar"`, + ExpectError: true, + }, + { + In: `.foo.bar[42]."hello world!: `, + ExpectError: true, + }, + } { + got, err := Parse(test.In) + if test.ExpectError && err == nil { + t.Errorf("parsePath(%q): expected error, got nil", test.In) + continue + } + if !test.ExpectError && err != nil { + t.Errorf("parsePath(%q): unexpected error: %v", test.In, err) + } + if err != nil { + continue + } + + if !reflect.DeepEqual(got, test.Expected) { + t.Errorf("parsePath(%q) = %v, expected %v", test.In, got, test.Expected) + } + } +} + +func TestPathIndex(t *testing.T) { + t.Run("Kind()", func(t *testing.T) { + k := PathIndex(2).Kind() + + if k != PathKindIndex { + t.Errorf("PathIndex().Kind() = %v, expected %v", k, PathKindIndex) + } + }) + + t.Run("String()", func(t *testing.T) { + s := PathIndex(2).String() + + if s != "[2]" { + t.Errorf("PathIndex(2).String() = %q, expected %q", s, "[2]") + } + }) +} + +func TestPathKey(t *testing.T) { + t.Run("Kind()", func(t *testing.T) { + k := PathKey("foo").Kind() + + if k != PathKindKey { + t.Errorf("PathKey().Kind() = %v, expected %v", k, PathKindKey) + } + }) + + t.Run("String()", func(t *testing.T) { + s := PathKey("foo").String() + + if s != ".foo" { + t.Errorf(`PathKey("foo").String() = %q, expected %q`, s, ".foo") + } + }) + + t.Run("String() escaped", func(t *testing.T) { + s := PathKey(`foo"`).String() + + if s != `."foo\""` { + t.Errorf(`PathKey("foo").String() = %q, expected %q`, s, `."foo\""`) + } + }) +} + +func TestPathKind(t *testing.T) { + t.Run("String()", func(t *testing.T) { + + for _, test := range []struct { + In PathKind + Expected string + }{ + { + In: PathKindIndex, + Expected: "PathKindIndex", + }, + { + In: PathKindKey, + Expected: "PathKindKey", + }, + { + In: PathKind(-1), + Expected: "PathKind(-1)", + }, + } { + got := test.In.String() + + if got != test.Expected { + t.Errorf("PathKind(%d).String() = %q, expected %q", int(test.In), got, test.Expected) + } + } + }) } diff --git a/jpath/pathkind_string.go b/jpath/pathkind_string.go new file mode 100644 index 0000000..8311a62 --- /dev/null +++ b/jpath/pathkind_string.go @@ -0,0 +1,24 @@ +// Code generated by "stringer -type PathKind"; DO NOT EDIT. + +package jpath + +import "strconv" + +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[PathKindIndex-0] + _ = x[PathKindKey-1] +} + +const _PathKind_name = "PathKindIndexPathKindKey" + +var _PathKind_index = [...]uint8{0, 13, 24} + +func (i PathKind) String() string { + if i < 0 || i >= PathKind(len(_PathKind_index)-1) { + return "PathKind(" + strconv.FormatInt(int64(i), 10) + ")" + } + return _PathKind_name[_PathKind_index[i]:_PathKind_index[i+1]] +} diff --git a/test_files/lhs_array.json b/test_files/lhs_array.json new file mode 100644 index 0000000..a3608a2 --- /dev/null +++ b/test_files/lhs_array.json @@ -0,0 +1,3 @@ +[ + 1, 3, 5, 7 +] diff --git a/test_files/lhs_scalar.json b/test_files/lhs_scalar.json new file mode 100644 index 0000000..4099407 --- /dev/null +++ b/test_files/lhs_scalar.json @@ -0,0 +1 @@ +23 diff --git a/test_files/rhs_array.json b/test_files/rhs_array.json new file mode 100644 index 0000000..6275da2 --- /dev/null +++ b/test_files/rhs_array.json @@ -0,0 +1,3 @@ +[ + 1, 2, 3, 5 +] diff --git a/test_files/rhs_scalar.json b/test_files/rhs_scalar.json new file mode 100644 index 0000000..d81cc07 --- /dev/null +++ b/test_files/rhs_scalar.json @@ -0,0 +1 @@ +42