Skip to content

Commit 30148a3

Browse files
cppforlifehunan-rostomyan
authored andcommitted
init
Signed-off-by: Hunan Rostomyan <[email protected]>
0 parents  commit 30148a3

16 files changed

+1473
-0
lines changed

README.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
## go-patch
2+
3+
More or less based on https://tools.ietf.org/html/rfc6902.
4+
5+
## Pointer (aka path)
6+
7+
More or less based on https://tools.ietf.org/html/rfc6901.
8+
9+
- Root
10+
- Index (ex: `/0`, `/-1`)
11+
- AfterLastIndex (ex: `/-`)
12+
- MatchingIndex (ex: `/key=val`)
13+
- Key: `/key`
14+
15+
## Operations
16+
17+
- Remove
18+
- Replace

err_op.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package patch
2+
3+
type ErrOp struct {
4+
Err error
5+
}
6+
7+
func (op ErrOp) Apply(_ interface{}) (interface{}, error) {
8+
return nil, op.Err
9+
}

errs.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package patch
2+
3+
import (
4+
"fmt"
5+
)
6+
7+
type opMismatchTypeErr struct {
8+
type_ string
9+
path Pointer
10+
obj interface{}
11+
}
12+
13+
func newOpArrayMismatchTypeErr(tokens []Token, obj interface{}) opMismatchTypeErr {
14+
return opMismatchTypeErr{"an array", NewPointer(tokens), obj}
15+
}
16+
17+
func newOpMapMismatchTypeErr(tokens []Token, obj interface{}) opMismatchTypeErr {
18+
return opMismatchTypeErr{"a map", NewPointer(tokens), obj}
19+
}
20+
21+
func (e opMismatchTypeErr) Error() string {
22+
errMsg := "Expected to find %s at path '%s' but found '%T'"
23+
return fmt.Sprintf(errMsg, e.type_, e.path, e.obj)
24+
}

integration_test.go

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
package patch_test
2+
3+
import (
4+
. "github.com/onsi/ginkgo"
5+
. "github.com/onsi/gomega"
6+
"gopkg.in/yaml.v2"
7+
8+
. "github.com/cppforlife/go-patch"
9+
)
10+
11+
var _ = Describe("Integration", func() {
12+
It("works in a basic way", func() {
13+
inStr := `
14+
releases:
15+
- name: capi
16+
version: 0.1
17+
18+
instance_groups:
19+
- name: cloud_controller
20+
instances: 0
21+
jobs:
22+
- name: cloud_controller
23+
release: capi
24+
25+
- name: uaa
26+
instances: 0
27+
`
28+
29+
var in interface{}
30+
31+
err := yaml.Unmarshal([]byte(inStr), &in)
32+
Expect(err).ToNot(HaveOccurred())
33+
34+
ops1Str := `
35+
- type: replace
36+
path: /instance_groups/name=cloud_controller/instances
37+
value: 1
38+
39+
- type: replace
40+
path: /instance_groups/name=cloud_controller/jobs/name=cloud_controller/consumes?/db
41+
value:
42+
instances:
43+
- address: some-db.local
44+
properties:
45+
username: user
46+
password: pass
47+
48+
- type: replace
49+
path: /instance_groups/name=uaa/instances
50+
value: 1
51+
52+
- type: replace
53+
path: /instance_groups/-
54+
value:
55+
name: uaadb
56+
instances: 2
57+
`
58+
59+
var opDefs1 []OpDefinition
60+
61+
err = yaml.Unmarshal([]byte(ops1Str), &opDefs1)
62+
Expect(err).ToNot(HaveOccurred())
63+
64+
ops1, err := NewOpsFromDefinitions(opDefs1)
65+
Expect(err).ToNot(HaveOccurred())
66+
67+
ops2Str := `
68+
- type: replace
69+
path: /releases/name=capi/version
70+
value: latest
71+
`
72+
73+
var opDefs2 []OpDefinition
74+
75+
err = yaml.Unmarshal([]byte(ops2Str), &opDefs2)
76+
Expect(err).ToNot(HaveOccurred())
77+
78+
ops2, err := NewOpsFromDefinitions(opDefs2)
79+
Expect(err).ToNot(HaveOccurred())
80+
81+
ops := append(ops1, ops2...)
82+
83+
res, err := ops.Apply(in)
84+
Expect(err).ToNot(HaveOccurred())
85+
86+
outStr := `
87+
releases:
88+
- name: capi
89+
version: latest
90+
91+
instance_groups:
92+
- name: cloud_controller
93+
instances: 1
94+
jobs:
95+
- name: cloud_controller
96+
release: capi
97+
consumes:
98+
db:
99+
instances:
100+
- address: some-db.local
101+
properties:
102+
username: user
103+
password: pass
104+
105+
- name: uaa
106+
instances: 1
107+
108+
- name: uaadb
109+
instances: 2
110+
`
111+
112+
var out interface{}
113+
114+
err = yaml.Unmarshal([]byte(outStr), &out)
115+
Expect(err).ToNot(HaveOccurred())
116+
117+
Expect(res).To(Equal(out))
118+
})
119+
})

op_definition.go

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package patch
2+
3+
import (
4+
"fmt"
5+
)
6+
7+
// OpDefinition struct is useful for JSON and YAML unmarshaling
8+
type OpDefinition struct {
9+
Type string
10+
Path *string
11+
Value *interface{}
12+
}
13+
14+
func NewOpsFromDefinitions(opDefs []OpDefinition) (Ops, error) {
15+
var ops []Op
16+
var op Op
17+
var err error
18+
19+
for i, opDef := range opDefs {
20+
switch opDef.Type {
21+
case "replace":
22+
op, err = newReplaceOp(opDef)
23+
if err != nil {
24+
return nil, fmt.Errorf("Replace operation [%d]: %s", i, err)
25+
}
26+
27+
case "remove":
28+
op, err = newRemoveOp(opDef)
29+
if err != nil {
30+
return nil, fmt.Errorf("Remove operation [%d]: %s", i, err)
31+
}
32+
33+
default:
34+
return nil, fmt.Errorf("Unknown operation [%d] with type '%s'", i, opDef.Type)
35+
}
36+
37+
ops = append(ops, op)
38+
}
39+
40+
return Ops(ops), nil
41+
}
42+
43+
func newReplaceOp(opDef OpDefinition) (ReplaceOp, error) {
44+
if opDef.Path == nil {
45+
return ReplaceOp{}, fmt.Errorf("Missing path")
46+
}
47+
48+
if opDef.Value == nil {
49+
return ReplaceOp{}, fmt.Errorf("Missing value")
50+
}
51+
52+
ptr, err := NewPointerFromString(*opDef.Path)
53+
if err != nil {
54+
return ReplaceOp{}, fmt.Errorf("Invalid path: %s", err)
55+
}
56+
57+
return ReplaceOp{Path: ptr, Value: *opDef.Value}, nil
58+
}
59+
60+
func newRemoveOp(opDef OpDefinition) (RemoveOp, error) {
61+
if opDef.Path == nil {
62+
return RemoveOp{}, fmt.Errorf("Missing path")
63+
}
64+
65+
if opDef.Value != nil {
66+
return RemoveOp{}, fmt.Errorf("Cannot specify value")
67+
}
68+
69+
ptr, err := NewPointerFromString(*opDef.Path)
70+
if err != nil {
71+
return RemoveOp{}, fmt.Errorf("Invalid path: %s", err)
72+
}
73+
74+
return RemoveOp{Path: ptr}, nil
75+
}

op_definition_test.go

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
package patch_test
2+
3+
import (
4+
. "github.com/onsi/ginkgo"
5+
. "github.com/onsi/gomega"
6+
7+
. "github.com/cppforlife/go-patch"
8+
)
9+
10+
var _ = Describe("NewOpsFromDefinitions", func() {
11+
var (
12+
path = "/abc"
13+
invalidPath = "abc"
14+
val interface{} = 123
15+
)
16+
17+
It("supports 'replace' and 'remove' operations", func() {
18+
opDefs := []OpDefinition{
19+
{Type: "replace", Path: &path, Value: &val},
20+
{Type: "remove", Path: &path},
21+
}
22+
23+
ops, err := NewOpsFromDefinitions(opDefs)
24+
Expect(err).ToNot(HaveOccurred())
25+
26+
Expect(ops).To(Equal(Ops([]Op{
27+
ReplaceOp{Path: MustNewPointerFromString("/abc"), Value: 123},
28+
RemoveOp{Path: MustNewPointerFromString("/abc")},
29+
})))
30+
})
31+
32+
It("returns error if operation type is unknown", func() {
33+
_, err := NewOpsFromDefinitions([]OpDefinition{{Type: "test"}})
34+
Expect(err).To(HaveOccurred())
35+
Expect(err.Error()).To(Equal("Unknown operation [0] with type 'test'"))
36+
})
37+
38+
Describe("replace", func() {
39+
It("requires path", func() {
40+
_, err := NewOpsFromDefinitions([]OpDefinition{{Type: "replace"}})
41+
Expect(err).To(HaveOccurred())
42+
Expect(err.Error()).To(Equal("Replace operation [0]: Missing path"))
43+
})
44+
45+
It("requires value", func() {
46+
_, err := NewOpsFromDefinitions([]OpDefinition{{Type: "replace", Path: &path}})
47+
Expect(err).To(HaveOccurred())
48+
Expect(err.Error()).To(Equal("Replace operation [0]: Missing value"))
49+
})
50+
51+
It("requires valid path", func() {
52+
_, err := NewOpsFromDefinitions([]OpDefinition{{Type: "replace", Path: &invalidPath, Value: &val}})
53+
Expect(err).To(HaveOccurred())
54+
Expect(err.Error()).To(ContainSubstring("Replace operation [0]: Invalid path: Expected to start with '/'"))
55+
})
56+
})
57+
58+
Describe("remove", func() {
59+
It("requires path", func() {
60+
_, err := NewOpsFromDefinitions([]OpDefinition{{Type: "remove"}})
61+
Expect(err).To(HaveOccurred())
62+
Expect(err.Error()).To(Equal("Remove operation [0]: Missing path"))
63+
})
64+
65+
It("does not allow value", func() {
66+
_, err := NewOpsFromDefinitions([]OpDefinition{{Type: "remove", Path: &path, Value: &val}})
67+
Expect(err).To(HaveOccurred())
68+
Expect(err.Error()).To(Equal("Remove operation [0]: Cannot specify value"))
69+
})
70+
71+
It("requires valid path", func() {
72+
_, err := NewOpsFromDefinitions([]OpDefinition{{Type: "remove", Path: &invalidPath}})
73+
Expect(err).To(HaveOccurred())
74+
Expect(err.Error()).To(ContainSubstring("Remove operation [0]: Invalid path: Expected to start with '/'"))
75+
})
76+
})
77+
})

ops.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package patch
2+
3+
type Ops []Op
4+
5+
type Op interface {
6+
Apply(interface{}) (interface{}, error)
7+
}
8+
9+
// Ensure basic operations implement Op
10+
var _ Op = ReplaceOp{}
11+
var _ Op = RemoveOp{}
12+
var _ Op = ErrOp{}
13+
14+
func (ops Ops) Apply(doc interface{}) (interface{}, error) {
15+
var err error
16+
17+
for _, op := range ops {
18+
doc, err = op.Apply(doc)
19+
if err != nil {
20+
return nil, err
21+
}
22+
}
23+
24+
return doc, nil
25+
}

ops_test.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package patch_test
2+
3+
import (
4+
"errors"
5+
6+
. "github.com/onsi/ginkgo"
7+
. "github.com/onsi/gomega"
8+
9+
. "github.com/cppforlife/go-patch"
10+
)
11+
12+
var _ = Describe("Ops.Apply", func() {
13+
It("runs through all operations", func() {
14+
ops := Ops([]Op{
15+
RemoveOp{Path: MustNewPointerFromString("/0")},
16+
RemoveOp{Path: MustNewPointerFromString("/0")},
17+
})
18+
19+
res, err := ops.Apply([]interface{}{1, 2, 3})
20+
Expect(err).ToNot(HaveOccurred())
21+
Expect(res).To(Equal([]interface{}{3}))
22+
})
23+
24+
It("returns original input if there are no operations", func() {
25+
res, err := Ops([]Op{}).Apply([]interface{}{1, 2, 3})
26+
Expect(err).ToNot(HaveOccurred())
27+
Expect(res).To(Equal([]interface{}{1, 2, 3}))
28+
})
29+
30+
It("returns error if any operation errors", func() {
31+
ops := Ops([]Op{
32+
RemoveOp{Path: MustNewPointerFromString("/0")},
33+
ErrOp{errors.New("fake-err")},
34+
})
35+
36+
_, err := ops.Apply([]interface{}{1, 2, 3})
37+
Expect(err).To(HaveOccurred())
38+
Expect(err.Error()).To(ContainSubstring("fake-err"))
39+
})
40+
})

0 commit comments

Comments
 (0)