forked from hashicorp/hcl
-
Notifications
You must be signed in to change notification settings - Fork 0
/
defaults.go
254 lines (226 loc) · 7.75 KB
/
defaults.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
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package typeexpr
import (
"sort"
"strconv"
"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/convert"
)
// Defaults represents a type tree which may contain default values for
// optional object attributes at any level. This is used to apply nested
// defaults to a given cty.Value before converting it to a concrete type.
type Defaults struct {
// Type of the node for which these defaults apply. This is necessary in
// order to determine how to inspect the Defaults and Children collections.
Type cty.Type
// DefaultValues contains the default values for each object attribute,
// indexed by attribute name.
DefaultValues map[string]cty.Value
// Children is a map of Defaults for elements contained in this type. This
// only applies to structural and collection types.
//
// The map is indexed by string instead of cty.Value because cty.Number
// instances are non-comparable, due to embedding a *big.Float.
//
// Collections have a single element type, which is stored at key "".
Children map[string]*Defaults
}
// Apply walks the given value, applying specified defaults wherever optional
// attributes are missing. The input and output values may have different
// types, and the result may still require type conversion to the final desired
// type.
//
// This function is permissive and does not report errors, assuming that the
// caller will have better context to report useful type conversion failure
// diagnostics.
func (d *Defaults) Apply(val cty.Value) cty.Value {
return d.apply(val)
}
func (d *Defaults) apply(v cty.Value) cty.Value {
// We don't apply defaults to null values or unknown values. To be clear,
// we will overwrite children values with defaults if they are null but not
// if the actual value is null.
if !v.IsKnown() || v.IsNull() {
return v
}
// Also, do nothing if we have no defaults to apply.
if len(d.DefaultValues) == 0 && len(d.Children) == 0 {
return v
}
v, marks := v.Unmark()
switch {
case v.Type().IsSetType(), v.Type().IsListType(), v.Type().IsTupleType():
values := d.applyAsSlice(v)
if v.Type().IsSetType() {
if len(values) == 0 {
v = cty.SetValEmpty(v.Type().ElementType())
break
}
if converts := d.unifyAsSlice(values); len(converts) > 0 {
v = cty.SetVal(converts).WithMarks(marks)
break
}
} else if v.Type().IsListType() {
if len(values) == 0 {
v = cty.ListValEmpty(v.Type().ElementType())
break
}
if converts := d.unifyAsSlice(values); len(converts) > 0 {
v = cty.ListVal(converts)
break
}
}
v = cty.TupleVal(values)
case v.Type().IsObjectType(), v.Type().IsMapType():
values := d.applyAsMap(v)
for key, defaultValue := range d.DefaultValues {
if value, ok := values[key]; !ok || value.IsNull() {
if defaults, ok := d.Children[key]; ok {
values[key] = defaults.apply(defaultValue)
continue
}
values[key] = defaultValue
}
if defaultRng := defaultValue.Range(); defaultRng.DefinitelyNotNull() {
values[key] = values[key].RefineNotNull()
}
}
if v.Type().IsMapType() {
if len(values) == 0 {
v = cty.MapValEmpty(v.Type().ElementType())
break
}
if converts := d.unifyAsMap(values); len(converts) > 0 {
v = cty.MapVal(converts)
break
}
}
v = cty.ObjectVal(values)
}
return v.WithMarks(marks)
}
func (d *Defaults) applyAsSlice(value cty.Value) []cty.Value {
var elements []cty.Value
for ix, element := range value.AsValueSlice() {
if childDefaults := d.getChild(ix); childDefaults != nil {
element = childDefaults.apply(element)
elements = append(elements, element)
continue
}
elements = append(elements, element)
}
return elements
}
func (d *Defaults) applyAsMap(value cty.Value) map[string]cty.Value {
elements := make(map[string]cty.Value)
for key, element := range value.AsValueMap() {
if childDefaults := d.getChild(key); childDefaults != nil {
elements[key] = childDefaults.apply(element)
continue
}
elements[key] = element
}
return elements
}
func (d *Defaults) getChild(key interface{}) *Defaults {
// Children for tuples are keyed by an int.
// Children for objects are keyed by a string.
// Children for maps, lists, and sets are always keyed by the empty string.
//
// For maps and objects the supplied key type is a string type.
// For lists, sets, and tuples the supplied key type is an int type.
//
// The callers of the defaults package could, in theory, pass a value in
// where the types expected based on the defaults do not match the actual
// type in the value. In this case, we get a mismatch between what the
// defaults package expects the key to be, and which type it actually is.
//
// In the event of such a mismatch, we just won't apply defaults. Instead,
// relying on the user later calling go-cty.Convert to detect this same
// error (as documented). In this case we'd just want to return nil to
// indicate either there are no defaults or we can't work out how to apply
// them. Both of these outcomes are treated the same by the rest of the
// package.
//
// For the above reasons it isn't necessarily safe to just rely on a single
// metric for working out how we should retrieve the key. If the defaults
// type is a tuple we can't just assume the supplied key will be an int (as
// the concrete value actually supplied by the user could be an object or a
// map). Similarly, if the supplied key is an int we can't just assume we
// should treat the type as a tuple (as a list would also specify an int,
// but we should return the children keyed by the empty string rather than
// the index).
switch concrete := key.(type) {
case int:
if d.Type.IsTupleType() {
// If the type is an int, and our defaults are expecting a tuple
// then we return the children for the tuple at the index.
return d.Children[strconv.Itoa(concrete)]
}
case string:
if d.Type.IsObjectType() {
// If the type is a string, and our defaults are expecting an object
// then we return the children for the object at the key.
return d.Children[concrete]
}
}
// Otherwise, either our defaults are expecting this to be a map, list, or
// set or the type our defaults expecting didn't line up with something we
// can convert between. So, either we want to return the child keyed by
// the empty string (in the first case) or nil (in the second case).
// Luckily, Golang maps return nil when referencing something that doesn't
// exist. So, we can just try and retrieve the child at the empty key and
// if it doesn't exist then that's fine since we'd just return nil anyway.
return d.Children[""]
}
func (d *Defaults) unifyAsSlice(values []cty.Value) []cty.Value {
var types []cty.Type
for _, value := range values {
types = append(types, value.Type())
}
unify, conversions := convert.UnifyUnsafe(types)
if unify == cty.NilType {
return nil
}
var converts []cty.Value
for ix := 0; ix < len(conversions); ix++ {
if conversions[ix] == nil {
converts = append(converts, values[ix])
continue
}
converted, err := conversions[ix](values[ix])
if err != nil {
return nil
}
converts = append(converts, converted)
}
return converts
}
func (d *Defaults) unifyAsMap(values map[string]cty.Value) map[string]cty.Value {
var keys []string
for key := range values {
keys = append(keys, key)
}
sort.Strings(keys)
var types []cty.Type
for _, key := range keys {
types = append(types, values[key].Type())
}
unify, conversions := convert.UnifyUnsafe(types)
if unify == cty.NilType {
return nil
}
converts := make(map[string]cty.Value)
for ix, key := range keys {
if conversions[ix] == nil {
converts[key] = values[key]
continue
}
var err error
if converts[key], err = conversions[ix](values[key]); err != nil {
return nil
}
}
return converts
}