-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathstep_command_matrix.go
375 lines (329 loc) · 10.3 KB
/
step_command_matrix.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
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
package pipeline
import (
"encoding/json"
"errors"
"fmt"
"github.com/buildkite/go-pipeline/ordered"
"gopkg.in/yaml.v3"
)
var (
_ interface {
json.Marshaler
ordered.Unmarshaler
yaml.Marshaler
selfInterpolater
} = (*Matrix)(nil)
_ interface {
json.Marshaler
selfInterpolater
} = (*MatrixAdjustment)(nil)
_ = []interface {
json.Marshaler
ordered.Unmarshaler
yaml.Marshaler
}{
(*MatrixSetup)(nil),
(*MatrixAdjustmentWith)(nil),
}
)
var (
errNilMatrix = errors.New("non-empty permutation but matrix is nil")
errPermutationLengthMismatch = errors.New("permutation has wrong length")
errPermutationUnknownDimension = errors.New("permutation has unknown dimension")
errAdjustmentLengthMismatch = errors.New("adjustment has wrong length")
errAdjustmentUnknownDimension = errors.New("adjustment has unknown dimension")
errPermutationSkipped = errors.New("permutation is skipped by adjustment")
errPermutationNoMatch = errors.New("permutation is neither a valid matrix combination nor an adjustment")
)
// Matrix models the matrix specification for command steps.
type Matrix struct {
Setup MatrixSetup `yaml:"setup"`
Adjustments MatrixAdjustments `yaml:"adjustments,omitempty"`
RemainingFields map[string]any `yaml:",inline"`
}
// IsEmpty reports whether the matrix is empty (is nil, or has no setup,
// no adjustments, and no other data within it).
func (m *Matrix) IsEmpty() bool {
return m == nil || (len(m.Setup) == 0 && len(m.Adjustments) == 0 && len(m.RemainingFields) == 0)
}
// UnmarshalOrdererd unmarshals from either []any or *ordered.MapSA.
func (m *Matrix) UnmarshalOrdered(o any) error {
switch src := o.(type) {
case []any:
// Single anonymous dimension matrix, no adjustments.
//
// matrix:
// - apple
// - 47
s := make([]string, 0, len(src))
if err := ordered.Unmarshal(src, &s); err != nil {
return err
}
m.Setup = MatrixSetup{"": s}
case *ordered.MapSA:
// Single anonymous dimension, or multiple named dimensions, with or
// without adjustments.
// Unmarshal into this secret wrapper type to avoid infinite recursion.
type wrappedMatrix Matrix
if err := ordered.Unmarshal(o, (*wrappedMatrix)(m)); err != nil {
return err
}
default:
return fmt.Errorf("unsupported src type for Matrix: %T", o)
}
return nil
}
// Reports if the matrix is a single anonymous dimension matrix with no
// adjustments or any other fields. (It's a list of items.)
func (m *Matrix) isSimple() bool {
return len(m.Setup) == 1 && len(m.Setup[""]) != 0 && len(m.Adjustments) == 0 && len(m.RemainingFields) == 0
}
// MarshalJSON is needed to use inlineFriendlyMarshalJSON, and reduces the
// representation to a single list if the matrix is simple.
func (m *Matrix) MarshalJSON() ([]byte, error) {
if m.isSimple() {
return json.Marshal(m.Setup[""])
}
return inlineFriendlyMarshalJSON(m)
}
// MarshalYAML is needed to reduce the representation to a single slice if
// the matrix is simple.
func (m *Matrix) MarshalYAML() (any, error) {
if m.isSimple() {
return m.Setup[""], nil
}
// Just in case the YAML marshaler tries to call MarshalYAML on the output,
// wrap m in a type without a MarshalYAML method.
type wrappedMatrix Matrix
return (*wrappedMatrix)(m), nil
}
func (m *Matrix) interpolate(tf stringTransformer) error {
if m == nil {
return nil
}
if _, is := tf.(matrixInterpolator); is {
// Don't interpolate matrixes into matrixes.
return nil
}
if err := interpolateMap(tf, m.Setup); err != nil {
return err
}
if err := interpolateSlice(tf, m.Adjustments); err != nil {
return err
}
return interpolateMap(tf, m.RemainingFields)
}
// validatePermutation checks that the permutation is a valid selection of
// dimension values, including any non-skipped adjustments.
func (m *Matrix) validatePermutation(p MatrixPermutation) error {
if m == nil {
if len(p) > 0 {
return errNilMatrix
}
// An empty permutation from a nil matrix...seems fine to me?
return nil
}
if len(p) != len(m.Setup) {
return fmt.Errorf("%w: %d != %d", errPermutationLengthMismatch, len(p), len(m.Setup))
}
// Check that the dimensions in the permutation are unique and defined in
// the matrix setup.
for dim := range p {
// An empty but non-nil setup dimension is valid (all values may be
// given by adjustment tuples).
if m.Setup[dim] == nil {
return fmt.Errorf("%w: %q", errPermutationUnknownDimension, dim)
}
}
// Check that the permutation values are in the matrix setup (a basic
// permutation). Whether they are or are not, we still check adjustments.
valid := true
for dim, val := range p {
match := false
for _, v := range m.Setup[dim] {
if val == v {
match = true
break
}
}
if !match {
// Not a basic permutation. It could still be an adjustment though.
valid = false
break
}
}
// Check if the permutation matches any adjustment.
for _, adj := range m.Adjustments {
// Ensure adj.With has the same size and dimension names as m.Setup.
// adj.With is a map so no need to check for repetition.
// Because adjustments can introduce new dimension values, only the
// names of dimensions are checked.
if len(adj.With) != len(m.Setup) {
return fmt.Errorf("%w: %d != %d", errAdjustmentLengthMismatch, len(adj.With), len(m.Setup))
}
for dim := range adj.With {
// An empty but non-nil setup dimension is valid (all values may be
// given by adjustment tuples).
if m.Setup[dim] == nil {
return fmt.Errorf("%w: %q", errAdjustmentUnknownDimension, dim)
}
}
// Now we can test whether p == adj.With.
match := true
for dim, val := range p {
if val != adj.With[dim] {
match = false
break
}
}
if !match {
continue
}
if adj.ShouldSkip() {
return errPermutationSkipped
}
// Not skipped, but is an adjustment, so it's valid.
// If multiple adjustments have the same permutation, and any of them
// have "skip: true", then that applies, so we can't bail early.
valid = true
}
if !valid {
return errPermutationNoMatch
}
return nil
}
// MatrixPermutation represents a possible permutation of a matrix.
type MatrixPermutation map[string]string
// MatrixSetup is the main setup of a matrix - one or more dimensions. The cross
// product of the dimensions in the setup produces the base combinations of
// matrix values.
type MatrixSetup map[string][]string
// MarshalJSON returns either a list (if the setup is a single anonymous
// dimension) or an object (if it contains one or more (named) dimensions).
func (ms MatrixSetup) MarshalJSON() ([]byte, error) {
// Note that MarshalYAML (below) always returns nil error.
o, _ := ms.MarshalYAML()
return json.Marshal(o)
}
// MarshalYAML returns either a Scalars (if the setup is a single anonymous
// dimension) or a map (if it contains one or more (named) dimensions).
func (ms MatrixSetup) MarshalYAML() (any, error) {
if len(ms) == 1 && len(ms[""]) > 0 {
return ms[""], nil
}
return map[string][]string(ms), nil
}
// UnmarshalOrdered unmarshals from either []any or *ordered.MapSA.
func (ms *MatrixSetup) UnmarshalOrdered(o any) error {
if *ms == nil {
*ms = make(MatrixSetup)
}
switch src := o.(type) {
case []any:
// Single anonymous dimension, but we only get here if its under a setup
// key. (Maybe the user wants adjustments for their single dimension.)
//
// matrix:
// setup:
// - apple
// - 47
s := make([]string, 0, len(src))
if err := ordered.Unmarshal(src, &s); err != nil {
return err
}
(*ms)[""] = s
case *ordered.MapSA:
// One or more (named) dimensions.
// Unmarshal into the underlying type to avoid infinite recursion.
if err := ordered.Unmarshal(src, (*map[string][]string)(ms)); err != nil {
return err
}
default:
return fmt.Errorf("unsupported src type for MatrixSetup: %T", o)
}
return nil
}
// MatrixAdjustments is a set of adjustments.
type MatrixAdjustments []*MatrixAdjustment
// MatrixAdjustment models an adjustment - a combination of (possibly new)
// matrix values, and skip/soft fail configuration.
type MatrixAdjustment struct {
With MatrixAdjustmentWith `yaml:"with"`
Skip any `yaml:"skip,omitempty"`
RemainingFields map[string]any `yaml:",inline"` // NB: soft_fail is in the remaining fields
}
func (ma *MatrixAdjustment) ShouldSkip() bool {
switch s := ma.Skip.(type) {
case bool:
return s
case nil:
return false
default:
return true
}
}
// MarshalJSON is needed to use inlineFriendlyMarshalJSON.
func (ma *MatrixAdjustment) MarshalJSON() ([]byte, error) {
return inlineFriendlyMarshalJSON(ma)
}
func (ma *MatrixAdjustment) interpolate(tf stringTransformer) error {
if ma == nil {
return nil
}
if err := interpolateMap(tf, ma.With); err != nil {
return err
}
return interpolateMap(tf, ma.RemainingFields)
}
// MatrixAdjustmentWith is either a map of dimension key -> dimension value,
// or a single value (for single anonymous dimension matrices).
type MatrixAdjustmentWith map[string]string
// MarshalJSON returns either a single scalar or an object.
func (maw MatrixAdjustmentWith) MarshalJSON() ([]byte, error) {
// Note that MarshalYAML (below) always returns nil error.
o, _ := maw.MarshalYAML()
return json.Marshal(o)
}
// MarshalYAML returns either a single scalar or a map.
func (maw MatrixAdjustmentWith) MarshalYAML() (any, error) {
if _, has := maw[""]; has && len(maw) == 1 {
return maw[""], nil
}
return map[string]string(maw), nil
}
// UnmarshalOrdered unmarshals from either a scalar value (string, bool, or int)
// or *ordered.MapSA.
func (maw *MatrixAdjustmentWith) UnmarshalOrdered(o any) error {
if *maw == nil {
*maw = make(MatrixAdjustmentWith)
}
switch src := o.(type) {
case bool, int, string:
// A single scalar.
// (This is how you can do adjustments on a single anonymous dimension.)
//
// matrix:
// setup:
// - apple
// - 47
// adjustments:
// - with: banana
// soft_fail: true
(*maw)[""] = fmt.Sprint(src)
case *ordered.MapSA:
// A map of dimension key -> dimension value. (Tuple of dimension value
// selections.)
return src.Range(func(k string, v any) error {
switch vt := v.(type) {
case bool, int, string:
(*maw)[k] = fmt.Sprint(vt)
default:
return fmt.Errorf("unsupported value type %T in key %q for MatrixAdjustmentsWith", v, k)
}
return nil
})
default:
return fmt.Errorf("unsupported src type for MatrixAdjustmentsWith: %T", o)
}
return nil
}