-
Notifications
You must be signed in to change notification settings - Fork 18
/
yaml.go
236 lines (199 loc) · 6.63 KB
/
yaml.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
package node
import (
"errors"
"fmt"
"github.com/cirruslabs/cirrus-cli/pkg/parser/nameable"
"github.com/cirruslabs/cirrus-cli/pkg/parser/parsererror"
"github.com/cirruslabs/cirrus-cli/pkg/yamlhelper"
"gopkg.in/yaml.v3"
)
var ErrFailedToMarshal = errors.New("failed to marshal to YAML")
func (node *Node) MarshalYAML() (*yaml.Node, error) {
switch obj := node.Value.(type) {
case *MapValue:
var resultChildren []*yaml.Node
for _, child := range node.Children {
marshalledItem, err := child.MarshalYAML()
if err != nil {
return nil, err
}
resultChildren = append(resultChildren, yamlhelper.NewStringNode(child.Name))
resultChildren = append(resultChildren, marshalledItem)
}
return yamlhelper.NewMapNode(resultChildren), nil
case *ListValue:
var resultChildren []*yaml.Node
for _, child := range node.Children {
marshalledItem, err := child.MarshalYAML()
if err != nil {
return nil, err
}
resultChildren = append(resultChildren, marshalledItem)
}
return yamlhelper.NewSeqNode(resultChildren), nil
case *ScalarValue:
return yamlhelper.NewStringNode(obj.Value), nil
default:
return nil, fmt.Errorf("%w: unknown node type: %T", ErrFailedToMarshal, node.Value)
}
}
func NewFromText(text string) (*Node, error) {
return NewFromTextWithMergeExemptions(text, []nameable.Nameable{})
}
func NewFromTextWithMergeExemptions(text string, mergeExemptions []nameable.Nameable) (*Node, error) {
var yamlNode yaml.Node
// Unmarshal YAML
if err := yaml.Unmarshal([]byte(text), &yamlNode); err != nil {
return nil, err
}
// Empty document
if yamlNode.Kind == 0 || len(yamlNode.Content) == 0 {
return &Node{Name: "root"}, nil
}
if yamlNode.Kind != yaml.DocumentNode {
return nil, parsererror.NewRich(yamlNode.Line, yamlNode.Column, "expected a YAML document, but got %s",
yamlKindToString(yamlNode.Kind))
}
if len(yamlNode.Content) > 1 {
extraneous := yamlNode.Content[1]
return nil, parsererror.NewRich(yamlNode.Line, yamlNode.Column, "YAML document contains extraneous"+
" top-level elements, such as %s", yamlKindToString(extraneous.Kind))
}
if yamlNode.Content[0].Kind != yaml.MappingNode {
return nil, parsererror.NewRich(yamlNode.Line, yamlNode.Column, "YAML document should contain a mapping"+
" as it top-level element, but found %s", yamlKindToString(yamlNode.Kind))
}
return convert(nil, "root", yamlNode.Content[0], yamlNode.Line, yamlNode.Column, mergeExemptions)
}
//nolint:gocognit // splitting this into multiple functions would probably make this even less comprehensible
func convert(
parent *Node,
name string,
yamlNode *yaml.Node,
line int,
column int,
mergeExemptions []nameable.Nameable,
) (*Node, error) {
result := &Node{
Name: name,
Parent: parent,
Line: line,
Column: column,
}
switch yamlNode.Kind {
case yaml.SequenceNode:
result.Value = &ListValue{}
for _, item := range yamlNode.Content {
// Ignore null[1] array elements for backwards compatibility
//
// [1]: https://yaml.org/type/null.html
if item.Tag == "!!null" {
continue
}
listSubtree, err := convert(result, "", item, item.Line, item.Column, mergeExemptions)
if err != nil {
return nil, err
}
result.Children = append(result.Children, listSubtree)
}
case yaml.MappingNode:
result.Value = &MapValue{}
if !isEven(len(yamlNode.Content)) {
return nil, parsererror.NewRich(yamlNode.Line, yamlNode.Column, "unbalanced map")
}
for i := 0; i < len(yamlNode.Content); i += 2 {
key := yamlNode.Content[i]
value := yamlNode.Content[i+1]
// Handle "<<" keys
if key.Tag == "!!merge" {
if err := result.merge(yamlNode, key, value, mergeExemptions); err != nil {
return nil, err
}
continue
}
// Apparently this is possible, so do the sanity check
if key.Tag != "!!str" {
return nil, parsererror.NewRich(yamlNode.Line, yamlNode.Column, "map key is not a string")
}
mapSubtree, err := convert(result, key.Value, value, key.Line, key.Column, mergeExemptions)
if err != nil {
return nil, err
}
result.Children = append(result.Children, mapSubtree)
}
case yaml.ScalarNode:
result.Value = &ScalarValue{Value: yamlNode.Value}
case yaml.AliasNode:
// YAML aliases generally don't need line and column helper values
// since they are merged into some other data structure afterwards
// and this helps to find bugs easier in the future
resolvedAlias, err := convert(parent, name, yamlNode.Alias, 0, 0, mergeExemptions)
if err != nil {
return nil, err
}
return resolvedAlias, nil
default:
return nil, parsererror.NewRich(yamlNode.Line, yamlNode.Column, "unexpected %s",
yamlKindToString(yamlNode.Kind))
}
return result, nil
}
func (node *Node) merge(parent *yaml.Node, _ *yaml.Node, value *yaml.Node, mergeExemptions []nameable.Nameable) error {
// YAML aliases generally don't need line and column helper values
// since they are merged into some other data structure afterwards
// and this helps to find bugs easier in the future
aliasValue, err := convert(node, "", value, 0, 0, mergeExemptions)
if err != nil {
return err
}
if value.Kind == yaml.AliasNode {
// According to spec[1], a merge key "<<" can either be associated with a single mapping node
// or a sequence.
//
// [1]: https://yaml.org/type/merge.html
switch aliasValue.Value.(type) {
case *MapValue:
node.MergeFromMap(aliasValue, mergeExemptions)
case *ListValue:
node.OverwriteWith(aliasValue)
default:
return parsererror.NewRich(parent.Line, parent.Column,
"merge key should either be associated with a mapping or a sequence")
}
} else if value.Kind == yaml.SequenceNode {
// According to spec[1], if the value associated with the merge key is a sequence,
// then this sequence is expected to contain mapping nodes and each of these nodes
// is merged in turn according to its order in the sequence.
//
// [1]: https://yaml.org/type/merge.html
for _, aliasValueChild := range aliasValue.Children {
if !aliasValueChild.IsMap() {
return parsererror.NewRich(parent.Line, parent.Column,
"got a sequence as a merge key's value, but not all of it's entries are mappings"+
" (as required per spec)")
}
node.MergeFromMap(aliasValueChild, mergeExemptions)
}
}
return nil
}
func yamlKindToString(kind yaml.Kind) string {
switch kind {
case yaml.DocumentNode:
return "document"
case yaml.SequenceNode:
return "sequence"
case yaml.MappingNode:
return "mapping"
case yaml.ScalarNode:
return "scalar"
case yaml.AliasNode:
return "alias"
default:
return fmt.Sprintf("element of kind %d", kind)
}
}
func isEven(number int) bool {
const divisor = 2
return (number % divisor) == 0
}