Skip to content

Commit 6f457cb

Browse files
committed
add diff obj to calculate ops file based diffs
Signed-off-by: dmitriy kalinin <[email protected]>
1 parent b580235 commit 6f457cb

File tree

2 files changed

+288
-0
lines changed

2 files changed

+288
-0
lines changed

patch/diff.go

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
package patch
2+
3+
import (
4+
"fmt"
5+
"reflect"
6+
"sort"
7+
8+
"gopkg.in/yaml.v2"
9+
)
10+
11+
type Diff struct {
12+
Left interface{}
13+
Right interface{}
14+
}
15+
16+
func (d Diff) Calculate() Ops {
17+
return d.calculate(d.Left, d.Right, []Token{RootToken{}})
18+
}
19+
20+
func (d Diff) calculate(left, right interface{}, tokens []Token) []Op {
21+
switch typedLeft := left.(type) {
22+
case map[interface{}]interface{}:
23+
if typedRight, ok := right.(map[interface{}]interface{}); ok {
24+
ops := []Op{}
25+
var allKeys []interface{}
26+
for k, _ := range typedLeft {
27+
allKeys = append(allKeys, k)
28+
}
29+
for k, _ := range typedRight {
30+
if _, found := typedLeft[k]; !found {
31+
allKeys = append(allKeys, k)
32+
}
33+
}
34+
sort.SliceStable(allKeys, func(i, j int) bool {
35+
iBs, _ := yaml.Marshal(allKeys[i])
36+
jBs, _ := yaml.Marshal(allKeys[j])
37+
return string(iBs) < string(jBs)
38+
})
39+
for _, k := range allKeys {
40+
newTokens := append([]Token{}, tokens...)
41+
if leftVal, found := typedLeft[k]; found {
42+
newTokens = append(newTokens, KeyToken{Key: fmt.Sprintf("%s", k)})
43+
if rightVal, found := typedRight[k]; found {
44+
ops = append(ops, d.calculate(leftVal, rightVal, newTokens)...)
45+
} else {
46+
ops = append(ops, RemoveOp{Path: NewPointer(newTokens)})
47+
}
48+
} else {
49+
newTokens = append(newTokens, KeyToken{Key: fmt.Sprintf("%s", k), Optional: true})
50+
ops = append(ops, ReplaceOp{Path: NewPointer(newTokens), Value: typedRight[k]})
51+
}
52+
}
53+
return ops
54+
}
55+
return []Op{ReplaceOp{Path: NewPointer(tokens), Value: right}}
56+
57+
case []interface{}:
58+
if typedRight, ok := right.([]interface{}); ok {
59+
ops := []Op{}
60+
actualIndex := 0
61+
for i := 0; i < max(len(typedLeft), len(typedRight)); i++ {
62+
newTokens := append([]Token{}, tokens...)
63+
switch {
64+
case i >= len(typedRight):
65+
newTokens = append(newTokens, IndexToken{Index: actualIndex})
66+
ops = append(ops, RemoveOp{Path: NewPointer(newTokens)})
67+
// keep actualIndex the same
68+
case i >= len(typedLeft):
69+
newTokens = append(newTokens, AfterLastIndexToken{})
70+
ops = append(ops, ReplaceOp{Path: NewPointer(newTokens), Value: typedRight[i]})
71+
actualIndex++
72+
default:
73+
newTokens = append(newTokens, IndexToken{Index: actualIndex})
74+
ops = append(ops, d.calculate(typedLeft[i], typedRight[i], newTokens)...)
75+
actualIndex++
76+
}
77+
}
78+
return ops
79+
}
80+
return []Op{ReplaceOp{Path: NewPointer(tokens), Value: right}}
81+
82+
default:
83+
if !reflect.DeepEqual(left, right) {
84+
return []Op{ReplaceOp{Path: NewPointer(tokens), Value: right}}
85+
}
86+
}
87+
88+
return []Op{}
89+
}
90+
91+
func max(a, b int) int {
92+
if a > b {
93+
return a
94+
}
95+
return b
96+
}

patch/diff_test.go

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
package patch_test
2+
3+
import (
4+
. "github.com/onsi/ginkgo"
5+
. "github.com/onsi/gomega"
6+
7+
. "github.com/cppforlife/go-patch/patch"
8+
)
9+
10+
var _ = Describe("Diff.Calculate", func() {
11+
testDiff := func(left, right interface{}, expectedOps []Op) {
12+
Expect(Diff{Left: left, Right: right}.Calculate()).To(Equal(Ops(expectedOps)))
13+
14+
result, err := Ops(expectedOps).Apply(left)
15+
Expect(err).ToNot(HaveOccurred())
16+
17+
if right == nil { // gomega does not allow nil==nil comparison
18+
Expect(result).To(BeNil())
19+
} else {
20+
Expect(result).To(Equal(right))
21+
}
22+
}
23+
24+
It("returns no ops if both docs are same", func() {
25+
testDiff(nil, nil, []Op{})
26+
27+
testDiff(
28+
map[interface{}]interface{}{"a": 124},
29+
map[interface{}]interface{}{"a": 124},
30+
[]Op{},
31+
)
32+
33+
testDiff(
34+
[]interface{}{"a", 124},
35+
[]interface{}{"a", 124},
36+
[]Op{},
37+
)
38+
})
39+
40+
It("can replace doc root", func() {
41+
testDiff(nil, "a", []Op{ReplaceOp{Path: MustNewPointerFromString(""), Value: "a"}})
42+
})
43+
44+
It("can replace doc root with nil", func() {
45+
testDiff("a", nil, []Op{ReplaceOp{Path: MustNewPointerFromString(""), Value: nil}})
46+
})
47+
48+
It("can diff maps", func() {
49+
testDiff(
50+
map[interface{}]interface{}{"a": 123},
51+
map[interface{}]interface{}{"a": 124},
52+
[]Op{
53+
ReplaceOp{Path: MustNewPointerFromString("/a"), Value: 124},
54+
},
55+
)
56+
57+
testDiff(
58+
map[interface{}]interface{}{"a": 123, "b": 456},
59+
map[interface{}]interface{}{"a": 124, "c": 456},
60+
[]Op{
61+
ReplaceOp{Path: MustNewPointerFromString("/a"), Value: 124},
62+
RemoveOp{Path: MustNewPointerFromString("/b")},
63+
ReplaceOp{Path: MustNewPointerFromString("/c?"), Value: 456},
64+
},
65+
)
66+
67+
testDiff(
68+
map[interface{}]interface{}{"a": 123, "b": 456},
69+
map[interface{}]interface{}{"a": 124},
70+
[]Op{
71+
ReplaceOp{Path: MustNewPointerFromString("/a"), Value: 124},
72+
RemoveOp{Path: MustNewPointerFromString("/b")},
73+
},
74+
)
75+
76+
testDiff(
77+
map[interface{}]interface{}{"a": 123, "b": 456},
78+
map[interface{}]interface{}{},
79+
[]Op{
80+
RemoveOp{Path: MustNewPointerFromString("/a")},
81+
RemoveOp{Path: MustNewPointerFromString("/b")},
82+
},
83+
)
84+
85+
testDiff(
86+
map[interface{}]interface{}{"a": 123},
87+
map[interface{}]interface{}{"a": nil},
88+
[]Op{
89+
ReplaceOp{Path: MustNewPointerFromString("/a"), Value: nil},
90+
},
91+
)
92+
93+
testDiff(
94+
map[interface{}]interface{}{"a": 123, "b": map[interface{}]interface{}{"a": 1024, "b": 4056}},
95+
map[interface{}]interface{}{"a": 124, "b": map[interface{}]interface{}{"a": 1024, "c": 4056}},
96+
[]Op{
97+
ReplaceOp{Path: MustNewPointerFromString("/a"), Value: 124},
98+
RemoveOp{Path: MustNewPointerFromString("/b/b")},
99+
ReplaceOp{Path: MustNewPointerFromString("/b/c?"), Value: 4056},
100+
},
101+
)
102+
103+
testDiff(
104+
map[interface{}]interface{}{"a": 123},
105+
"a",
106+
[]Op{
107+
ReplaceOp{Path: MustNewPointerFromString(""), Value: "a"},
108+
},
109+
)
110+
111+
testDiff(
112+
"a",
113+
map[interface{}]interface{}{"a": 123},
114+
[]Op{
115+
ReplaceOp{Path: MustNewPointerFromString(""), Value: map[interface{}]interface{}{"a": 123}},
116+
},
117+
)
118+
})
119+
120+
It("can diff arrays", func() {
121+
testDiff(
122+
[]interface{}{"a", 123},
123+
[]interface{}{"b", 123},
124+
[]Op{
125+
ReplaceOp{Path: MustNewPointerFromString("/0"), Value: "b"},
126+
},
127+
)
128+
129+
testDiff(
130+
[]interface{}{"a"},
131+
[]interface{}{"b", 123, 456},
132+
[]Op{
133+
ReplaceOp{Path: MustNewPointerFromString("/0"), Value: "b"},
134+
ReplaceOp{Path: MustNewPointerFromString("/-"), Value: 123},
135+
ReplaceOp{Path: MustNewPointerFromString("/-"), Value: 456},
136+
},
137+
)
138+
139+
testDiff(
140+
[]interface{}{"a", 123, 456},
141+
[]interface{}{"b"},
142+
[]Op{
143+
ReplaceOp{Path: MustNewPointerFromString("/0"), Value: "b"},
144+
RemoveOp{Path: MustNewPointerFromString("/1")},
145+
RemoveOp{Path: MustNewPointerFromString("/1")},
146+
},
147+
)
148+
149+
testDiff(
150+
[]interface{}{123, 456},
151+
[]interface{}{},
152+
[]Op{
153+
RemoveOp{Path: MustNewPointerFromString("/0")},
154+
RemoveOp{Path: MustNewPointerFromString("/0")},
155+
},
156+
)
157+
158+
testDiff(
159+
[]interface{}{123, 456},
160+
[]interface{}{123, "a", 456}, // TODO unoptimized insertion
161+
[]Op{
162+
ReplaceOp{Path: MustNewPointerFromString("/1"), Value: "a"},
163+
ReplaceOp{Path: MustNewPointerFromString("/-"), Value: 456},
164+
},
165+
)
166+
167+
testDiff(
168+
[]interface{}{[]interface{}{456, 789}},
169+
[]interface{}{[]interface{}{789}},
170+
[]Op{
171+
ReplaceOp{Path: MustNewPointerFromString("/0/0"), Value: 789},
172+
RemoveOp{Path: MustNewPointerFromString("/0/1")},
173+
},
174+
)
175+
176+
testDiff(
177+
[]interface{}{"a", 123},
178+
"a",
179+
[]Op{
180+
ReplaceOp{Path: MustNewPointerFromString(""), Value: "a"},
181+
},
182+
)
183+
184+
testDiff(
185+
"a",
186+
[]interface{}{"a", 123},
187+
[]Op{
188+
ReplaceOp{Path: MustNewPointerFromString(""), Value: []interface{}{"a", 123}},
189+
},
190+
)
191+
})
192+
})

0 commit comments

Comments
 (0)