Skip to content

Commit a9941e6

Browse files
author
dmitriy kalinin
committed
support modifiers (:next, :prev, :before, :after)
1 parent d9e51be commit a9941e6

18 files changed

+733
-44
lines changed

bin/test

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
#!/bin/bash
2+
3+
set -e
4+
5+
go fmt github.com/cppforlife/go-patch/...
6+
7+
ginkgo -r patch/

docs/examples.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@
1313
- `key=val` notation matches hashes within an array (ex: `/key=val`)
1414
- values ending with `?` refer to array items that may or may not exist
1515

16+
- array index selection could be affected via `:prev` and `:next`
17+
18+
- array insertion could be affected via `:before` and `:after`
19+
1620
See pointer test examples in [patch/pointer_test.go](../patch/pointer_test.go).
1721

1822
## Operations
@@ -121,6 +125,42 @@ There are two available operations: `replace` and `remove`.
121125
- creates `array2` array since it does not exist
122126
- appends `10` to the end of `array2`
123127

128+
```yaml
129+
- type: replace
130+
path: /array/1:prev
131+
value: 10
132+
```
133+
134+
- requires `array` to exist and be an array
135+
- replaces 0th item in `array` array with `10`
136+
137+
```yaml
138+
- type: replace
139+
path: /array/0:next
140+
value: 10
141+
```
142+
143+
- requires `array` to exist and be an array
144+
- replaces 1st item (starting at 0) in `array` array with `10`
145+
146+
```yaml
147+
- type: replace
148+
path: /array/0:after
149+
value: 10
150+
```
151+
152+
- requires `array` to exist and be an array
153+
- inserts `10` after 0th item in `array` array
154+
155+
```yaml
156+
- type: replace
157+
path: /array/0:before
158+
value: 10
159+
```
160+
161+
- requires `array` to exist and be an array
162+
- inserts `10` before 0th item at the beginning of `array` array
163+
124164
### Arrays of hashes
125165

126166
```yaml

patch/array_index.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package patch
2+
3+
import (
4+
"fmt"
5+
)
6+
7+
type ArrayIndex struct {
8+
Index int
9+
Modifiers []Modifier
10+
Array []interface{}
11+
}
12+
13+
func (i ArrayIndex) Concrete() (int, error) {
14+
result := i.Index
15+
16+
for _, modifier := range i.Modifiers {
17+
switch modifier.(type) {
18+
case PrevModifier:
19+
result -= 1
20+
case NextModifier:
21+
result += 1
22+
default:
23+
return 0, fmt.Errorf("Expected to find one of the following modifiers: 'prev', 'next', but found modifier '%T'", modifier)
24+
}
25+
}
26+
27+
if result >= len(i.Array) || (-result)-1 >= len(i.Array) {
28+
return 0, OpMissingIndexErr{result, i.Array}
29+
}
30+
31+
if result < 0 {
32+
result = len(i.Array) + result
33+
}
34+
35+
return result, nil
36+
}

patch/array_index_test.go

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
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("ArrayIndex", func() {
11+
Describe("Concrete", func() {
12+
It("returns positive index", func() {
13+
idx := ArrayIndex{Index: 0, Modifiers: nil, Array: []interface{}{1, 2, 3}}
14+
Expect(idx.Concrete()).To(Equal(0))
15+
16+
idx = ArrayIndex{Index: 1, Modifiers: nil, Array: []interface{}{1, 2, 3}}
17+
Expect(idx.Concrete()).To(Equal(1))
18+
19+
idx = ArrayIndex{Index: 2, Modifiers: nil, Array: []interface{}{1, 2, 3}}
20+
Expect(idx.Concrete()).To(Equal(2))
21+
})
22+
23+
It("wraps around negative index one time", func() {
24+
idx := ArrayIndex{Index: -0, Modifiers: nil, Array: []interface{}{1, 2, 3}}
25+
Expect(idx.Concrete()).To(Equal(0))
26+
27+
idx = ArrayIndex{Index: -1, Modifiers: nil, Array: []interface{}{1, 2, 3}}
28+
Expect(idx.Concrete()).To(Equal(2))
29+
30+
idx = ArrayIndex{Index: -2, Modifiers: nil, Array: []interface{}{1, 2, 3}}
31+
Expect(idx.Concrete()).To(Equal(1))
32+
33+
idx = ArrayIndex{Index: -3, Modifiers: nil, Array: []interface{}{1, 2, 3}}
34+
Expect(idx.Concrete()).To(Equal(0))
35+
})
36+
37+
It("does not work with empty arrays", func() {
38+
idx := ArrayIndex{Index: 0, Modifiers: nil, Array: []interface{}{}}
39+
_, err := idx.Concrete()
40+
Expect(err).To(Equal(OpMissingIndexErr{0, []interface{}{}}))
41+
42+
p := PrevModifier{}
43+
n := NextModifier{}
44+
45+
idx = ArrayIndex{Index: 0, Modifiers: []Modifier{p, n}, Array: []interface{}{}}
46+
_, err = idx.Concrete()
47+
Expect(err).To(Equal(OpMissingIndexErr{0, []interface{}{}}))
48+
})
49+
50+
It("does not work with index out of bounds", func() {
51+
idx := ArrayIndex{Index: 3, Modifiers: nil, Array: []interface{}{1, 2, 3}}
52+
_, err := idx.Concrete()
53+
Expect(err).To(Equal(OpMissingIndexErr{3, []interface{}{1, 2, 3}}))
54+
55+
idx = ArrayIndex{Index: -4, Modifiers: nil, Array: []interface{}{1, 2, 3}}
56+
_, err = idx.Concrete()
57+
Expect(err).To(Equal(OpMissingIndexErr{-4, []interface{}{1, 2, 3}}))
58+
})
59+
60+
It("returns previous item when previous modifier is used", func() {
61+
p := PrevModifier{}
62+
63+
idx := ArrayIndex{Index: 0, Modifiers: []Modifier{p}, Array: []interface{}{1, 2, 3}}
64+
Expect(idx.Concrete()).To(Equal(2))
65+
66+
idx = ArrayIndex{Index: 0, Modifiers: []Modifier{p, p}, Array: []interface{}{1, 2, 3}}
67+
Expect(idx.Concrete()).To(Equal(1))
68+
69+
idx = ArrayIndex{Index: 0, Modifiers: []Modifier{p, p, p}, Array: []interface{}{1, 2, 3}}
70+
Expect(idx.Concrete()).To(Equal(0))
71+
72+
idx = ArrayIndex{Index: 0, Modifiers: []Modifier{p, p, p, p}, Array: []interface{}{1, 2, 3}}
73+
_, err := idx.Concrete()
74+
Expect(err).To(Equal(OpMissingIndexErr{-4, []interface{}{1, 2, 3}}))
75+
76+
idx = ArrayIndex{Index: 0, Modifiers: []Modifier{p, p, p, p, p}, Array: []interface{}{1, 2, 3}}
77+
_, err = idx.Concrete()
78+
Expect(err).To(Equal(OpMissingIndexErr{-5, []interface{}{1, 2, 3}}))
79+
80+
idx = ArrayIndex{Index: 2, Modifiers: []Modifier{p, p}, Array: []interface{}{1, 2, 3}}
81+
Expect(idx.Concrete()).To(Equal(0))
82+
})
83+
84+
It("returns next item when next modifier is used", func() {
85+
n := NextModifier{}
86+
87+
idx := ArrayIndex{Index: 0, Modifiers: []Modifier{n}, Array: []interface{}{1, 2, 3}}
88+
Expect(idx.Concrete()).To(Equal(1))
89+
90+
idx = ArrayIndex{Index: 0, Modifiers: []Modifier{n, n}, Array: []interface{}{1, 2, 3}}
91+
Expect(idx.Concrete()).To(Equal(2))
92+
93+
idx = ArrayIndex{Index: 0, Modifiers: []Modifier{n, n, n}, Array: []interface{}{1, 2, 3}}
94+
_, err := idx.Concrete()
95+
Expect(err).To(Equal(OpMissingIndexErr{3, []interface{}{1, 2, 3}}))
96+
97+
idx = ArrayIndex{Index: 0, Modifiers: []Modifier{n, n, n, n}, Array: []interface{}{1, 2, 3}}
98+
_, err = idx.Concrete()
99+
Expect(err).To(Equal(OpMissingIndexErr{4, []interface{}{1, 2, 3}}))
100+
})
101+
102+
It("works with multiple previous and next modifiers", func() {
103+
p := PrevModifier{}
104+
n := NextModifier{}
105+
106+
idx := ArrayIndex{Index: 0, Modifiers: []Modifier{p, n}, Array: []interface{}{1, 2, 3}}
107+
Expect(idx.Concrete()).To(Equal(0))
108+
109+
idx = ArrayIndex{Index: 0, Modifiers: []Modifier{n, p}, Array: []interface{}{1, 2, 3}}
110+
Expect(idx.Concrete()).To(Equal(0))
111+
112+
idx = ArrayIndex{Index: 0, Modifiers: []Modifier{n, n, p}, Array: []interface{}{1, 2, 3}}
113+
Expect(idx.Concrete()).To(Equal(1))
114+
115+
idx = ArrayIndex{Index: 0, Modifiers: []Modifier{n, n, n, p}, Array: []interface{}{1, 2, 3}}
116+
Expect(idx.Concrete()).To(Equal(2))
117+
})
118+
119+
It("does not support any other modifier except previous and next", func() {
120+
b := BeforeModifier{}
121+
122+
idx := ArrayIndex{Index: 0, Modifiers: []Modifier{b}, Array: []interface{}{1, 2, 3}}
123+
_, err := idx.Concrete()
124+
Expect(err.Error()).To(Equal("Expected to find one of the following modifiers: 'prev', 'next', but found modifier 'patch.BeforeModifier'"))
125+
})
126+
})
127+
})

patch/array_insertion.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package patch
2+
3+
import (
4+
"fmt"
5+
)
6+
7+
type ArrayInsertion struct {
8+
Index int
9+
Modifiers []Modifier
10+
Array []interface{}
11+
}
12+
13+
type ArrayInsertionIndex struct {
14+
Number int
15+
Insert bool
16+
}
17+
18+
func (i ArrayInsertion) Concrete() (ArrayInsertionIndex, error) {
19+
var mods []Modifier
20+
21+
before := false
22+
after := false
23+
24+
for _, modifier := range i.Modifiers {
25+
if before {
26+
return ArrayInsertionIndex{}, fmt.Errorf(
27+
"Expected to not find any modifiers after 'before' modifier, but found modifier '%T'", modifier)
28+
}
29+
if after {
30+
return ArrayInsertionIndex{}, fmt.Errorf(
31+
"Expected to not find any modifiers after 'after' modifier, but found modifier '%T'", modifier)
32+
}
33+
34+
switch modifier.(type) {
35+
case BeforeModifier:
36+
before = true
37+
case AfterModifier:
38+
after = true
39+
default:
40+
mods = append(mods, modifier)
41+
}
42+
}
43+
44+
idx := ArrayIndex{Index: i.Index, Modifiers: mods, Array: i.Array}
45+
46+
num, err := idx.Concrete()
47+
if err != nil {
48+
return ArrayInsertionIndex{}, err
49+
}
50+
51+
if before {
52+
num -= 1
53+
if num < 0 {
54+
num = 0
55+
}
56+
}
57+
58+
if after {
59+
num += 1
60+
}
61+
62+
return ArrayInsertionIndex{num, before || after}, nil
63+
}

patch/array_insertion_test.go

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
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("ArrayInsertion", func() {
11+
Describe("Concrete", func() {
12+
It("returns specified index when not using any modifiers", func() {
13+
idx := ArrayInsertion{Index: 1, Modifiers: []Modifier{}, Array: []interface{}{1, 2, 3}}
14+
Expect(idx.Concrete()).To(Equal(ArrayInsertionIndex{1, false}))
15+
})
16+
17+
It("returns index adjusted for previous and next modifiers", func() {
18+
p := PrevModifier{}
19+
n := NextModifier{}
20+
21+
idx := ArrayInsertion{Index: 1, Modifiers: []Modifier{p, n, n}, Array: []interface{}{1, 2, 3}}
22+
Expect(idx.Concrete()).To(Equal(ArrayInsertionIndex{2, false}))
23+
})
24+
25+
It("returns error if both after and before are used", func() {
26+
idx := ArrayInsertion{Index: 0, Modifiers: []Modifier{BeforeModifier{}, AfterModifier{}}, Array: []interface{}{}}
27+
_, err := idx.Concrete()
28+
Expect(err.Error()).To(Equal("Expected to not find any modifiers after 'before' modifier, but found modifier 'patch.AfterModifier'"))
29+
30+
idx = ArrayInsertion{Index: 0, Modifiers: []Modifier{AfterModifier{}, BeforeModifier{}}, Array: []interface{}{}}
31+
_, err = idx.Concrete()
32+
Expect(err.Error()).To(Equal("Expected to not find any modifiers after 'after' modifier, but found modifier 'patch.BeforeModifier'"))
33+
34+
idx = ArrayInsertion{Index: 0, Modifiers: []Modifier{AfterModifier{}, PrevModifier{}}, Array: []interface{}{}}
35+
_, err = idx.Concrete()
36+
Expect(err.Error()).To(Equal("Expected to not find any modifiers after 'after' modifier, but found modifier 'patch.PrevModifier'"))
37+
})
38+
39+
It("returns (0, true) when inserting in the beginning", func() {
40+
idx := ArrayInsertion{Index: 0, Modifiers: []Modifier{BeforeModifier{}}, Array: []interface{}{1, 2, 3}}
41+
Expect(idx.Concrete()).To(Equal(ArrayInsertionIndex{0, true}))
42+
})
43+
44+
It("returns (last+1, true) when inserting in the end", func() {
45+
idx := ArrayInsertion{Index: 2, Modifiers: []Modifier{AfterModifier{}}, Array: []interface{}{1, 2, 3}}
46+
Expect(idx.Concrete()).To(Equal(ArrayInsertionIndex{3, true}))
47+
48+
idx = ArrayInsertion{Index: -1, Modifiers: []Modifier{AfterModifier{}}, Array: []interface{}{1, 2, 3}}
49+
Expect(idx.Concrete()).To(Equal(ArrayInsertionIndex{3, true}))
50+
})
51+
52+
It("returns (mid+1, true) when inserting in the middle", func() {
53+
idx := ArrayInsertion{Index: 1, Modifiers: []Modifier{AfterModifier{}}, Array: []interface{}{1, 2, 3}}
54+
Expect(idx.Concrete()).To(Equal(ArrayInsertionIndex{2, true}))
55+
})
56+
57+
It("returns index adjusted for previous, next modifiers and before modifier", func() {
58+
p := PrevModifier{}
59+
n := NextModifier{}
60+
b := BeforeModifier{}
61+
62+
idx := ArrayInsertion{Index: 1, Modifiers: []Modifier{p, n, n, b}, Array: []interface{}{1, 2, 3}}
63+
Expect(idx.Concrete()).To(Equal(ArrayInsertionIndex{1, true}))
64+
})
65+
66+
It("returns index adjusted for previous, next modifiers and after modifier", func() {
67+
p := PrevModifier{}
68+
n := NextModifier{}
69+
a := AfterModifier{}
70+
71+
idx := ArrayInsertion{Index: 1, Modifiers: []Modifier{p, n, n, a}, Array: []interface{}{1, 2, 3}}
72+
Expect(idx.Concrete()).To(Equal(ArrayInsertionIndex{3, true}))
73+
})
74+
})
75+
})

patch/errs.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -50,13 +50,13 @@ func (e opMissingMapKeyErr) siblingKeysErrStr() string {
5050
return "found map keys: '" + strings.Join(keys, "', '") + "'"
5151
}
5252

53-
type opMissingIndexErr struct {
54-
idx int
55-
obj []interface{}
53+
type OpMissingIndexErr struct {
54+
Idx int
55+
Obj []interface{}
5656
}
5757

58-
func (e opMissingIndexErr) Error() string {
59-
return fmt.Sprintf("Expected to find array index '%d' but found array of length '%d'", e.idx, len(e.obj))
58+
func (e OpMissingIndexErr) Error() string {
59+
return fmt.Sprintf("Expected to find array index '%d' but found array of length '%d'", e.Idx, len(e.Obj))
6060
}
6161

6262
type opMultipleMatchingIndexErr struct {

0 commit comments

Comments
 (0)