Skip to content

Commit 1c0b3ba

Browse files
switch last key token to be required
1 parent feb38e9 commit 1c0b3ba

File tree

8 files changed

+131
-67
lines changed

8 files changed

+131
-67
lines changed

patch/integration_test.go

Lines changed: 30 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
package patch_test
22

33
import (
4-
. "github.com/onsi/ginkgo"
5-
. "github.com/onsi/gomega"
6-
"gopkg.in/yaml.v2"
4+
. "github.com/onsi/ginkgo"
5+
. "github.com/onsi/gomega"
6+
"gopkg.in/yaml.v2"
77

8-
. "github.com/cppforlife/go-patch/patch"
8+
. "github.com/cppforlife/go-patch/patch"
99
)
1010

1111
var _ = Describe("Integration", func() {
12-
It("works in a basic way", func() {
13-
inStr := `
12+
It("works in a basic way", func() {
13+
inStr := `
1414
releases:
1515
- name: capi
1616
version: 0.1
@@ -26,12 +26,12 @@ instance_groups:
2626
instances: 0
2727
`
2828

29-
var in interface{}
29+
var in interface{}
3030

31-
err := yaml.Unmarshal([]byte(inStr), &in)
32-
Expect(err).ToNot(HaveOccurred())
31+
err := yaml.Unmarshal([]byte(inStr), &in)
32+
Expect(err).ToNot(HaveOccurred())
3333

34-
ops1Str := `
34+
ops1Str := `
3535
- type: replace
3636
path: /instance_groups/name=cloud_controller/instances
3737
value: 1
@@ -56,34 +56,34 @@ instance_groups:
5656
instances: 2
5757
`
5858

59-
var opDefs1 []OpDefinition
59+
var opDefs1 []OpDefinition
6060

61-
err = yaml.Unmarshal([]byte(ops1Str), &opDefs1)
62-
Expect(err).ToNot(HaveOccurred())
61+
err = yaml.Unmarshal([]byte(ops1Str), &opDefs1)
62+
Expect(err).ToNot(HaveOccurred())
6363

64-
ops1, err := NewOpsFromDefinitions(opDefs1)
65-
Expect(err).ToNot(HaveOccurred())
64+
ops1, err := NewOpsFromDefinitions(opDefs1)
65+
Expect(err).ToNot(HaveOccurred())
6666

67-
ops2Str := `
67+
ops2Str := `
6868
- type: replace
6969
path: /releases/name=capi/version
7070
value: latest
7171
`
7272

73-
var opDefs2 []OpDefinition
73+
var opDefs2 []OpDefinition
7474

75-
err = yaml.Unmarshal([]byte(ops2Str), &opDefs2)
76-
Expect(err).ToNot(HaveOccurred())
75+
err = yaml.Unmarshal([]byte(ops2Str), &opDefs2)
76+
Expect(err).ToNot(HaveOccurred())
7777

78-
ops2, err := NewOpsFromDefinitions(opDefs2)
79-
Expect(err).ToNot(HaveOccurred())
78+
ops2, err := NewOpsFromDefinitions(opDefs2)
79+
Expect(err).ToNot(HaveOccurred())
8080

81-
ops := append(ops1, ops2...)
81+
ops := append(ops1, ops2...)
8282

83-
res, err := ops.Apply(in)
84-
Expect(err).ToNot(HaveOccurred())
83+
res, err := ops.Apply(in)
84+
Expect(err).ToNot(HaveOccurred())
8585

86-
outStr := `
86+
outStr := `
8787
releases:
8888
- name: capi
8989
version: latest
@@ -109,11 +109,11 @@ instance_groups:
109109
instances: 2
110110
`
111111

112-
var out interface{}
112+
var out interface{}
113113

114-
err = yaml.Unmarshal([]byte(outStr), &out)
115-
Expect(err).ToNot(HaveOccurred())
114+
err = yaml.Unmarshal([]byte(outStr), &out)
115+
Expect(err).ToNot(HaveOccurred())
116116

117-
Expect(res).To(Equal(out))
118-
})
117+
Expect(res).To(Equal(out))
118+
})
119119
})

patch/pointer.go

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ func NewPointerFromString(str string) (Pointer, error) {
3939
tokenStrs := strings.Split(str, "/")
4040
tokenStrs = tokenStrs[1:]
4141

42-
expecting := true
42+
optional := false
4343

4444
for i, tok := range tokenStrs {
4545
isLast := i == len(tokenStrs)-1
@@ -64,12 +64,12 @@ func NewPointerFromString(str string) (Pointer, error) {
6464
}
6565

6666
if strings.HasSuffix(tok, "?") {
67-
expecting = false
67+
optional = true
6868
}
6969

7070
token := KeyToken{
7171
Key: strings.TrimSuffix(tok, "?"),
72-
Expected: expecting && !isLast,
72+
Optional: optional,
7373
}
7474

7575
tokens = append(tokens, token)
@@ -96,11 +96,9 @@ func (p Pointer) Tokens() []Token { return p.tokens }
9696
func (p Pointer) String() string {
9797
var strs []string
9898

99-
expecting := true
100-
101-
for i, token := range p.tokens {
102-
isLast := i == len(p.tokens)-1
99+
optional := false
103100

101+
for _, token := range p.tokens {
104102
switch typedToken := token.(type) {
105103
case RootToken:
106104
strs = append(strs, "")
@@ -116,10 +114,10 @@ func (p Pointer) String() string {
116114

117115
case KeyToken:
118116
str := rfc6901Encoder.Replace(typedToken.Key)
119-
if !isLast && !typedToken.Expected { // /key?/key2/key3
120-
if expecting {
117+
if typedToken.Optional { // /key?/key2/key3
118+
if !optional {
121119
str += "?"
122-
expecting = false
120+
optional = true
123121
}
124122
}
125123
strs = append(strs, str)

patch/pointer_test.go

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ var testCases = []PointerTestCase{
2626
{"/a~01b", []Token{RootToken{}, KeyToken{Key: "a~1b"}}},
2727
{"/a~1b", []Token{RootToken{}, KeyToken{Key: "a/b"}}},
2828

29-
// Speacial chars
29+
// Special chars
3030
{"/c%d", []Token{RootToken{}, KeyToken{Key: "c%d"}}},
3131
{"/e^f", []Token{RootToken{}, KeyToken{Key: "e^f"}}},
3232
{"/g|h", []Token{RootToken{}, KeyToken{Key: "g|h"}}},
@@ -35,18 +35,23 @@ var testCases = []PointerTestCase{
3535

3636
// Maps
3737
{"/key", []Token{RootToken{}, KeyToken{Key: "key"}}},
38-
{"/key/", []Token{RootToken{}, KeyToken{Key: "key", Expected: true}, KeyToken{Key: ""}}},
39-
{"/key/key2", []Token{RootToken{}, KeyToken{Key: "key", Expected: true}, KeyToken{Key: "key2"}}},
40-
{"/key?/key2/key3", []Token{RootToken{}, KeyToken{Key: "key"}, KeyToken{Key: "key2"}, KeyToken{Key: "key3"}}},
38+
{"/key/", []Token{RootToken{}, KeyToken{Key: "key"}, KeyToken{Key: ""}}},
39+
{"/key/key2", []Token{RootToken{}, KeyToken{Key: "key"}, KeyToken{Key: "key2"}}},
40+
{"/key?/key2/key3", []Token{
41+
RootToken{},
42+
KeyToken{Key: "key", Optional: true},
43+
KeyToken{Key: "key2", Optional: true},
44+
KeyToken{Key: "key3", Optional: true},
45+
}},
4146

4247
// Array indices
4348
{"/0", []Token{RootToken{}, IndexToken{0}}},
4449
{"/1000001", []Token{RootToken{}, IndexToken{1000001}}},
4550
{"/-2", []Token{RootToken{}, IndexToken{-2}}},
4651

4752
{"/-", []Token{RootToken{}, AfterLastIndexToken{}}},
48-
{"/ary/-", []Token{RootToken{}, KeyToken{Key: "ary", Expected: true}, AfterLastIndexToken{}}},
49-
{"/-/key", []Token{RootToken{}, KeyToken{Key: "-", Expected: true}, KeyToken{Key: "key"}}},
53+
{"/ary/-", []Token{RootToken{}, KeyToken{Key: "ary"}, AfterLastIndexToken{}}},
54+
{"/-/key", []Token{RootToken{}, KeyToken{Key: "-"}, KeyToken{Key: "key"}}},
5055

5156
// Matching index token
5257
{"/name=val", []Token{RootToken{}, MatchingIndexToken{Key: "name", Value: "val"}}},
@@ -89,7 +94,11 @@ var _ = Describe("Pointer.String", func() {
8994

9095
var _ = Describe("Pointer.Tokens", func() {
9196
parsingTestCases := []PointerTestCase{
92-
{"/key/key2?", []Token{RootToken{}, KeyToken{Key: "key", Expected: true}, KeyToken{Key: "key2"}}},
97+
{"/key/key2?", []Token{
98+
RootToken{},
99+
KeyToken{Key: "key"},
100+
KeyToken{Key: "key2", Optional: true},
101+
}},
93102
}
94103

95104
parsingTestCases = append(parsingTestCases, testCases...)

patch/remove_op.go

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -85,17 +85,21 @@ func (op RemoveOp) Apply(doc interface{}) (interface{}, error) {
8585
return nil, newOpMapMismatchTypeErr(tokens[:i+2], obj)
8686
}
8787

88-
if isLast {
89-
delete(typedObj, typedToken.Key)
90-
} else {
91-
var found bool
88+
var found bool
9289

93-
obj, found = typedObj[typedToken.Key]
94-
if !found {
95-
errMsg := "Expected to find a map key '%s' for path '%s'"
96-
return nil, fmt.Errorf(errMsg, typedToken.Key, NewPointer(tokens[:i+2]))
90+
obj, found = typedObj[typedToken.Key]
91+
if !found {
92+
if typedToken.Optional {
93+
return doc, nil
9794
}
9895

96+
errMsg := "Expected to find a map key '%s' for path '%s'"
97+
return nil, fmt.Errorf(errMsg, typedToken.Key, NewPointer(tokens[:i+2]))
98+
}
99+
100+
if isLast {
101+
delete(typedObj, typedToken.Key)
102+
} else {
99103
prevUpdate = func(newObj interface{}) { typedObj[typedToken.Key] = newObj }
100104
}
101105

patch/remove_op_test.go

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,7 @@ var _ = Describe("RemoveOp.Apply", func() {
204204
"xyz": "xyz",
205205
}
206206

207-
res, err := RemoveOp{Path: MustNewPointerFromString("/abc/efg")}.Apply(doc)
207+
res, err := RemoveOp{Path: MustNewPointerFromString("/abc/efg?")}.Apply(doc)
208208
Expect(err).ToNot(HaveOccurred())
209209

210210
Expect(res).To(Equal(map[interface{}]interface{}{
@@ -213,6 +213,23 @@ var _ = Describe("RemoveOp.Apply", func() {
213213
}))
214214
})
215215

216+
It("removes super nested map key that does not exist", func() {
217+
doc := map[interface{}]interface{}{
218+
"abc": map[interface{}]interface{}{
219+
"efg": map[interface{}]interface{}{}, // wrong level
220+
},
221+
}
222+
223+
res, err := RemoveOp{Path: MustNewPointerFromString("/abc/opr?/efg")}.Apply(doc)
224+
Expect(err).ToNot(HaveOccurred())
225+
226+
Expect(res).To(Equal(map[interface{}]interface{}{
227+
"abc": map[interface{}]interface{}{
228+
"efg": map[interface{}]interface{}{}, // wrong level
229+
},
230+
}))
231+
})
232+
216233
It("returns an error if parent key does not exist", func() {
217234
doc := map[interface{}]interface{}{"xyz": "xyz"}
218235

@@ -222,6 +239,15 @@ var _ = Describe("RemoveOp.Apply", func() {
222239
"Expected to find a map key 'abc' for path '/abc'"))
223240
})
224241

242+
It("returns an error if key does not exist", func() {
243+
doc := map[interface{}]interface{}{"xyz": "xyz"}
244+
245+
_, err := RemoveOp{Path: MustNewPointerFromString("/abc")}.Apply(doc)
246+
Expect(err).To(HaveOccurred())
247+
Expect(err.Error()).To(Equal(
248+
"Expected to find a map key 'abc' for path '/abc'"))
249+
})
250+
225251
It("returns an error if it's not a map when key is being accessed", func() {
226252
_, err := RemoveOp{Path: MustNewPointerFromString("/abc")}.Apply([]interface{}{1, 2, 3})
227253
Expect(err).To(HaveOccurred())

patch/replace_op.go

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -92,18 +92,20 @@ func (op ReplaceOp) Apply(doc interface{}) (interface{}, error) {
9292
return nil, newOpMapMismatchTypeErr(tokens[:i+2], obj)
9393
}
9494

95+
var found bool
96+
97+
obj, found = typedObj[typedToken.Key]
98+
if !found && !typedToken.Optional {
99+
errMsg := "Expected to find a map key '%s' for path '%s'"
100+
return nil, fmt.Errorf(errMsg, typedToken.Key, NewPointer(tokens[:i+2]))
101+
}
102+
95103
if isLast {
96104
typedObj[typedToken.Key] = op.Value
97105
} else {
98-
var found bool
106+
prevUpdate = func(newObj interface{}) { typedObj[typedToken.Key] = newObj }
99107

100-
obj, found = typedObj[typedToken.Key]
101108
if !found {
102-
if typedToken.Expected {
103-
errMsg := "Expected to find a map key '%s' for path '%s'"
104-
return nil, fmt.Errorf(errMsg, typedToken.Key, NewPointer(tokens[:i+2]))
105-
}
106-
107109
// Determine what type of value to create based on next token
108110
switch tokens[i+2].(type) {
109111
case AfterLastIndexToken:
@@ -117,8 +119,6 @@ func (op ReplaceOp) Apply(doc interface{}) (interface{}, error) {
117119

118120
typedObj[typedToken.Key] = obj
119121
}
120-
121-
prevUpdate = func(newObj interface{}) { typedObj[typedToken.Key] = newObj }
122122
}
123123

124124
default:

patch/replace_op_test.go

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -261,7 +261,7 @@ var _ = Describe("ReplaceOp.Apply", func() {
261261
"xyz": "xyz",
262262
}
263263

264-
res, err := ReplaceOp{Path: MustNewPointerFromString("/abc/efg"), Value: 1}.Apply(doc)
264+
res, err := ReplaceOp{Path: MustNewPointerFromString("/abc/efg?"), Value: 1}.Apply(doc)
265265
Expect(err).ToNot(HaveOccurred())
266266

267267
Expect(res).To(Equal(map[interface{}]interface{}{
@@ -270,6 +270,24 @@ var _ = Describe("ReplaceOp.Apply", func() {
270270
}))
271271
})
272272

273+
It("replaces super nested map key that does not exist", func() {
274+
doc := map[interface{}]interface{}{
275+
"abc": map[interface{}]interface{}{
276+
"efg": map[interface{}]interface{}{}, // wrong level
277+
},
278+
}
279+
280+
res, err := ReplaceOp{Path: MustNewPointerFromString("/abc/opr?/efg"), Value: 1}.Apply(doc)
281+
Expect(err).ToNot(HaveOccurred())
282+
283+
Expect(res).To(Equal(map[interface{}]interface{}{
284+
"abc": map[interface{}]interface{}{
285+
"efg": map[interface{}]interface{}{}, // wrong level
286+
"opr": map[interface{}]interface{}{"efg": 1},
287+
},
288+
}))
289+
})
290+
273291
It("returns an error if parent key does not exist", func() {
274292
doc := map[interface{}]interface{}{"xyz": "xyz"}
275293

@@ -279,6 +297,15 @@ var _ = Describe("ReplaceOp.Apply", func() {
279297
"Expected to find a map key 'abc' for path '/abc'"))
280298
})
281299

300+
It("returns an error if key does not exist", func() {
301+
doc := map[interface{}]interface{}{"xyz": "xyz"}
302+
303+
_, err := ReplaceOp{Path: MustNewPointerFromString("/abc")}.Apply(doc)
304+
Expect(err).To(HaveOccurred())
305+
Expect(err.Error()).To(Equal(
306+
"Expected to find a map key 'abc' for path '/abc'"))
307+
})
308+
282309
It("creates missing key if key is not expected to exist", func() {
283310
doc := map[interface{}]interface{}{"xyz": "xyz"}
284311

patch/tokens.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,5 @@ type MatchingIndexToken struct {
1717

1818
type KeyToken struct {
1919
Key string
20-
Expected bool
20+
Optional bool
2121
}

0 commit comments

Comments
 (0)