-
Notifications
You must be signed in to change notification settings - Fork 18
/
parseable.go
187 lines (152 loc) · 4.92 KB
/
parseable.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
package parseable
import (
"github.com/cirruslabs/cirrus-ci-agent/api"
"github.com/cirruslabs/cirrus-cli/pkg/parser/nameable"
nodepkg "github.com/cirruslabs/cirrus-cli/pkg/parser/node"
"github.com/cirruslabs/cirrus-cli/pkg/parser/parserkit"
"github.com/lestrrat-go/jsschema"
"regexp"
)
type Parseable interface {
Parse(node *nodepkg.Node, parserKit *parserkit.ParserKit) error
Schema() *schema.Schema
CollectibleFields() []CollectibleField
Fields() []Field
Proto() interface{}
}
type nodeFunc func(node *nodepkg.Node) error
type Field struct {
name nameable.Nameable
required bool
repeatable bool
onFound nodeFunc
schema *schema.Schema
}
func (field *Field) Name() nameable.Nameable {
return field.name
}
func (field *Field) Repeatable() bool {
return field.repeatable
}
type CollectibleField struct {
Name string
DefinesInstance bool
onFound nodeFunc
Schema *schema.Schema
}
//nolint:gocognit // yes, it's complicated
func (parser *DefaultParser) Parse(node *nodepkg.Node, parserKit *parserkit.ParserKit) error {
// Detect possible incorrect usage of fields that expect a map
// (e.g. "container: ruby:latest"), yet allow "container:" since
// there's no clear intention to configure this field from the user
if _, ok := node.Value.(*nodepkg.MapValue); !ok && !node.ValueIsEmpty() {
parserKit.IssueRegistry.RegisterIssuef(api.Issue_WARNING, node.Line, node.Column,
"expected a map, found %s", node.ValueTypeAsString())
}
// Evaluate collectible fields
var delayedError error
var atLeastOneInstanceMatched bool
for _, field := range parser.collectibleFields {
matched, err := evaluateCollectible(node, field)
if err != nil {
// Delay an error in the hope that we'll
// find another, more suitable instance
if field.DefinesInstance {
delayedError = err
continue
}
return err
}
// Record that we've successfully assigned
// at least one instance to the task (otherwise
// there would be an error above)
if field.DefinesInstance && matched {
atLeastOneInstanceMatched = true
}
}
// If no instances were assigned, and we've had an error —
// stop processing and output the error to the user
if !atLeastOneInstanceMatched && delayedError != nil {
return delayedError
}
for _, child := range node.Children {
// double check collectible fields
for _, collectibleField := range parser.collectibleFields {
if collectibleField.Name == child.Name {
if _, err := evaluateCollectible(node, collectibleField); err != nil {
return err
}
break
}
}
}
// Calculate redefinitions index to answer the question:
// "Is the field we're processing right now will be redefined later?"
redefinitions := map[string]*nodepkg.Node{}
for _, child := range node.Children {
redefinitions[child.Name] = child
}
// Evaluate ordinary fields
seenFields := map[string]struct{}{}
for _, child := range node.Children {
field := parser.FindFieldByName(child.Name)
if field == nil {
continue
}
redefinitionWillBeEncounteredLater := child != redefinitions[child.Name]
// Skip processing this child if it corresponds to a non-repeatable
// field and there will be more similarly-named children in the next
// iterations
if !field.repeatable && redefinitionWillBeEncounteredLater {
continue
}
seenFields[field.name.MapKey()] = struct{}{}
if err := field.onFound(child); err != nil {
return err
}
}
// Check ordinary fields with the "required" flag set
for _, field := range parser.fields {
_, seen := seenFields[field.name.MapKey()]
if field.required && !seen {
return node.ParserError("required field %q was not set", field.name.String())
}
}
return nil
}
func (parser *DefaultParser) Schema() *schema.Schema {
schema := &schema.Schema{
Properties: make(map[string]*schema.Schema),
PatternProperties: make(map[*regexp.Regexp]*schema.Schema),
AdditionalItems: &schema.AdditionalItems{Schema: nil},
AdditionalProperties: &schema.AdditionalProperties{Schema: nil},
}
for _, field := range parser.fields {
switch nameable := field.name.(type) {
case *nameable.SimpleNameable:
schema.Properties[nameable.Name()] = field.schema
case *nameable.RegexNameable:
schema.PatternProperties[nameable.Regex()] = field.schema
}
if field.required && !parser.Collectible() {
schema.Required = append(schema.Required, field.name.String())
}
}
for _, collectibleField := range parser.collectibleFields {
schema.Properties[collectibleField.Name] = collectibleField.Schema
}
return schema
}
func (parser *DefaultParser) CollectibleFields() []CollectibleField {
return parser.collectibleFields
}
func (parser *DefaultParser) Fields() []Field {
return parser.fields
}
func evaluateCollectible(node *nodepkg.Node, field CollectibleField) (bool, error) {
children := node.DeepFindCollectible(field.Name)
if children == nil {
return false, nil
}
return true, field.onFound(children)
}