Skip to content

Commit b4bb677

Browse files
authored
Add method to insert a value at a given JSONPointer location (#3)
Add method to insert a value at a given JSONPointer location
1 parent 260bbdf commit b4bb677

File tree

2 files changed

+215
-0
lines changed

2 files changed

+215
-0
lines changed

yptr.go

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44
package yptr
55

66
import (
7+
"errors"
78
"fmt"
9+
"slices"
810
"strconv"
911
"strings"
1012

@@ -78,6 +80,120 @@ func find(root *yaml.Node, toks []string) ([]*yaml.Node, error) {
7880
return res, nil
7981
}
8082

83+
// Insert inserts a value at the location pointed by the JSONPointer ptr in the yaml tree rooted at root.
84+
// If any nodes along the way do not exist, they are created such that a subsequent call to Find would find
85+
// the value at that location.
86+
//
87+
// Note that Insert does not replace existing values. If the location already exists in the yaml tree, Insert will
88+
// attempt to append the value to the existing node there. If this isn't possible, an error is returned.
89+
//
90+
// Also note that '-' is only treated as a special character if the currently referenced value is an existing array.
91+
// It cannot be used to create a new empty array at the current location.
92+
func Insert(root *yaml.Node, ptr string, value yaml.Node) error {
93+
toks, err := jsonPointerToTokens(ptr)
94+
if err != nil {
95+
return err
96+
}
97+
98+
// skip document nodes
99+
if value.Kind == yaml.DocumentNode {
100+
value = *value.Content[0]
101+
}
102+
if root.Kind == yaml.DocumentNode {
103+
root = root.Content[0]
104+
}
105+
106+
return insert(root, toks, value)
107+
}
108+
109+
func insert(root *yaml.Node, toks []string, value yaml.Node) error {
110+
if len(toks) == 0 {
111+
if root.Kind == yaml.MappingNode {
112+
if value.Kind == yaml.MappingNode {
113+
root.Content = append(root.Content, value.Content...)
114+
return nil
115+
}
116+
if len(root.Content) == 0 {
117+
*root = value
118+
return nil
119+
}
120+
}
121+
return fmt.Errorf("cannot insert node type %v (%v) in node type %v (%v)", value.Kind, value.Tag, root.Kind, root.Tag)
122+
}
123+
124+
switch root.Kind {
125+
case yaml.SequenceNode:
126+
return sequenceInsert(root, toks, value)
127+
case yaml.MappingNode:
128+
return mapInsert(root, toks, value)
129+
default:
130+
return fmt.Errorf("unhandled node type: %v (%v)", root.Kind, root.Tag)
131+
}
132+
}
133+
134+
func sequenceInsert(root *yaml.Node, toks []string, value yaml.Node) error {
135+
// try to find the token in the node
136+
next, err := match(root, toks[0])
137+
if err != nil {
138+
return err
139+
}
140+
if len(toks) == 1 {
141+
return sequenceInsertAt(root, toks[0], value)
142+
}
143+
if next[0].Kind != yaml.ScalarNode {
144+
return insert(next[0], toks[1:], value)
145+
}
146+
// insert an empty map and try again
147+
n := yaml.Node{Kind: yaml.MappingNode, Tag: "!!map"}
148+
k := yaml.Node{Kind: yaml.ScalarNode, Value: toks[1], Tag: "!!str"}
149+
v := yaml.Node{Kind: yaml.MappingNode, Tag: "!!map"}
150+
n.Content = append(n.Content, &k, &v)
151+
152+
err = sequenceInsertAt(root, toks[0], n)
153+
if err != nil {
154+
return err
155+
}
156+
return insert(&n, toks[1:], value)
157+
}
158+
159+
// helper function for inserting a value at a specific index in an array
160+
func sequenceInsertAt(root *yaml.Node, tok string, n yaml.Node) error {
161+
if tok == "-" {
162+
root.Content = append(root.Content, &n)
163+
} else {
164+
i, err := strconv.Atoi(tok)
165+
if err != nil {
166+
return err
167+
}
168+
if i < 0 || i >= len(root.Content) {
169+
return fmt.Errorf("out of bounds")
170+
}
171+
root.Content = slices.Insert(root.Content, i, &n)
172+
}
173+
return nil
174+
}
175+
176+
func mapInsert(root *yaml.Node, toks []string, value yaml.Node) error {
177+
// try to find the token in the node
178+
next, err := match(root, toks[0])
179+
if err != nil && !errors.Is(err, ErrNotFound) {
180+
return err
181+
}
182+
183+
if errors.Is(err, ErrNotFound) {
184+
k := yaml.Node{Kind: yaml.ScalarNode, Value: toks[0]}
185+
if len(toks) == 1 {
186+
root.Content = append(root.Content, &k, &value)
187+
return nil
188+
}
189+
// insert an empty map and try again
190+
v := yaml.Node{Kind: yaml.MappingNode, Tag: "!!map"}
191+
root.Content = append(root.Content, &k, &v)
192+
return insert(&v, toks[1:], value)
193+
}
194+
return insert(next[0], toks[1:], value)
195+
}
196+
81197
// match matches a JSONPointer token against a yaml Node.
82198
//
83199
// If root is a map, it performs a field lookup using tok as field name,
@@ -111,6 +227,10 @@ func match(root *yaml.Node, tok string) ([]*yaml.Node, error) {
111227
}
112228
return filter(c, treeSubsetPred(&mtree))
113229
default:
230+
if tok == "-" {
231+
// dummy leaf node
232+
return []*yaml.Node{{Kind: yaml.ScalarNode}}, nil
233+
}
114234
i, err := strconv.Atoi(tok)
115235
if err != nil {
116236
return nil, err

yptr_test.go

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,107 @@ package yptr_test
66
import (
77
"errors"
88
"fmt"
9+
"strings"
910
"testing"
1011

1112
yptr "github.com/zillow/go-yaml-jsonpointer"
1213
"github.com/zillow/go-yaml/v3"
1314
)
1415

16+
func ExampleInsert() {
17+
src := `
18+
d:
19+
- e
20+
- f:
21+
g: x
22+
- h: y
23+
- - i
24+
- j
25+
`
26+
arr1 := `[1, 2, 3]`
27+
map1 := `q: xyz`
28+
s1 := `x`
29+
30+
var n, a, m, x yaml.Node
31+
yaml.Unmarshal([]byte(src), &n)
32+
yaml.Unmarshal([]byte(arr1), &a)
33+
yaml.Unmarshal([]byte(map1), &m)
34+
yaml.Unmarshal([]byte(s1), &x)
35+
36+
_ = yptr.Insert(&n, `/f/d`, a)
37+
_ = yptr.Insert(&n, ``, m)
38+
39+
_ = yptr.Insert(&n, `/d/1`, m)
40+
_ = yptr.Insert(&n, `/d/-/c`, x)
41+
_ = yptr.Insert(&n, `/d/2/f`, m)
42+
_ = yptr.Insert(&n, `/d/3/f`, a)
43+
_ = yptr.Insert(&n, `/d/4/-`, x)
44+
45+
out, err := yaml.Marshal(n.Content[0])
46+
if err != nil {
47+
panic(err)
48+
}
49+
50+
fmt.Println(string(out))
51+
/* Output:
52+
d:
53+
- e
54+
- q: xyz
55+
- f:
56+
g: x
57+
q: xyz
58+
- h: y
59+
f: [1, 2, 3]
60+
- - i
61+
- j
62+
- x
63+
- c: x
64+
f:
65+
d: [1, 2, 3]
66+
q: xyz
67+
*/
68+
}
69+
70+
func TestInsertErrors(t *testing.T) {
71+
src := `
72+
a:
73+
b:
74+
c: 42
75+
d:
76+
- e
77+
- f
78+
`
79+
s1 := `x`
80+
var n, x yaml.Node
81+
yaml.Unmarshal([]byte(src), &n)
82+
yaml.Unmarshal([]byte(s1), &x)
83+
84+
tests := []struct {
85+
ptr string
86+
value yaml.Node
87+
err string
88+
}{
89+
{``, x, "cannot insert node type"},
90+
{`/a/b/c`, x, "cannot insert node type"},
91+
{`/d`, x, "cannot insert node type"},
92+
{`/a/b/c/f`, x, "unhandled node type"},
93+
{`/d/f`, x, "strconv.Atoi"},
94+
{`/d/5`, x, "out of bounds"},
95+
}
96+
97+
for i, tc := range tests {
98+
t.Run(fmt.Sprint(i), func(t *testing.T) {
99+
err := yptr.Insert(&n, tc.ptr, tc.value)
100+
if err == nil {
101+
t.Fatal("expecting error")
102+
}
103+
if !strings.HasPrefix(err.Error(), tc.err) {
104+
t.Fatalf("expecting error %q, got %q", tc.err, err)
105+
}
106+
})
107+
}
108+
}
109+
15110
func ExampleFind() {
16111
src := `
17112
a:

0 commit comments

Comments
 (0)