|
4 | 4 | package yptr
|
5 | 5 |
|
6 | 6 | import (
|
| 7 | + "errors" |
7 | 8 | "fmt"
|
| 9 | + "slices" |
8 | 10 | "strconv"
|
9 | 11 | "strings"
|
10 | 12 |
|
@@ -78,6 +80,120 @@ func find(root *yaml.Node, toks []string) ([]*yaml.Node, error) {
|
78 | 80 | return res, nil
|
79 | 81 | }
|
80 | 82 |
|
| 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 | + |
81 | 197 | // match matches a JSONPointer token against a yaml Node.
|
82 | 198 | //
|
83 | 199 | // 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) {
|
111 | 227 | }
|
112 | 228 | return filter(c, treeSubsetPred(&mtree))
|
113 | 229 | default:
|
| 230 | + if tok == "-" { |
| 231 | + // dummy leaf node |
| 232 | + return []*yaml.Node{{Kind: yaml.ScalarNode}}, nil |
| 233 | + } |
114 | 234 | i, err := strconv.Atoi(tok)
|
115 | 235 | if err != nil {
|
116 | 236 | return nil, err
|
|
0 commit comments