Skip to content

Commit 75d5692

Browse files
authored
Merge pull request #549 from hashicorp/typeexpr-optional-defaults
typeexpr: Optional object attributes with defaults
2 parents 3186414 + 47464b2 commit 75d5692

File tree

5 files changed

+1176
-39
lines changed

5 files changed

+1176
-39
lines changed

ext/typeexpr/defaults.go

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
package typeexpr
2+
3+
import (
4+
"github.com/zclconf/go-cty/cty"
5+
)
6+
7+
// Defaults represents a type tree which may contain default values for
8+
// optional object attributes at any level. This is used to apply nested
9+
// defaults to an input value before converting it to the concrete type.
10+
type Defaults struct {
11+
// Type of the node for which these defaults apply. This is necessary in
12+
// order to determine how to inspect the Defaults and Children collections.
13+
Type cty.Type
14+
15+
// DefaultValues contains the default values for each object attribute,
16+
// indexed by attribute name.
17+
DefaultValues map[string]cty.Value
18+
19+
// Children is a map of Defaults for elements contained in this type. This
20+
// only applies to structural and collection types.
21+
//
22+
// The map is indexed by string instead of cty.Value because cty.Number
23+
// instances are non-comparable, due to embedding a *big.Float.
24+
//
25+
// Collections have a single element type, which is stored at key "".
26+
Children map[string]*Defaults
27+
}
28+
29+
// Apply walks the given value, applying specified defaults wherever optional
30+
// attributes are missing. The input and output values may have different
31+
// types, and the result may still require type conversion to the final desired
32+
// type.
33+
//
34+
// This function is permissive and does not report errors, assuming that the
35+
// caller will have better context to report useful type conversion failure
36+
// diagnostics.
37+
func (d *Defaults) Apply(val cty.Value) cty.Value {
38+
val, err := cty.TransformWithTransformer(val, &defaultsTransformer{defaults: d})
39+
40+
// The transformer should never return an error.
41+
if err != nil {
42+
panic(err)
43+
}
44+
45+
return val
46+
}
47+
48+
// defaultsTransformer implements cty.Transformer, as a pre-order traversal,
49+
// applying defaults as it goes. The pre-order traversal allows us to specify
50+
// defaults more loosely for structural types, as the defaults for the types
51+
// will be applied to the default value later in the walk.
52+
type defaultsTransformer struct {
53+
defaults *Defaults
54+
}
55+
56+
var _ cty.Transformer = (*defaultsTransformer)(nil)
57+
58+
func (t *defaultsTransformer) Enter(p cty.Path, v cty.Value) (cty.Value, error) {
59+
// Cannot apply defaults to an unknown value
60+
if !v.IsKnown() {
61+
return v, nil
62+
}
63+
64+
// Look up the defaults for this path.
65+
defaults := t.defaults.traverse(p)
66+
67+
// If we have no defaults, nothing to do.
68+
if len(defaults) == 0 {
69+
return v, nil
70+
}
71+
72+
// Ensure we are working with an object or map.
73+
vt := v.Type()
74+
if !vt.IsObjectType() && !vt.IsMapType() {
75+
// Cannot apply defaults because the value type is incompatible.
76+
// We'll ignore this and let the later conversion stage display a
77+
// more useful diagnostic.
78+
return v, nil
79+
}
80+
81+
// Unmark the value and reapply the marks later.
82+
v, valMarks := v.Unmark()
83+
84+
// Convert the given value into an attribute map (if it's non-null and
85+
// non-empty).
86+
attrs := make(map[string]cty.Value)
87+
if !v.IsNull() && v.LengthInt() > 0 {
88+
attrs = v.AsValueMap()
89+
}
90+
91+
// Apply defaults where attributes are missing, constructing a new
92+
// value with the same marks.
93+
for attr, defaultValue := range defaults {
94+
if attrValue, ok := attrs[attr]; !ok || attrValue.IsNull() {
95+
attrs[attr] = defaultValue
96+
}
97+
}
98+
99+
// We construct an object even if the input value was a map, as the
100+
// type of an attribute's default value may be incompatible with the
101+
// map element type.
102+
return cty.ObjectVal(attrs).WithMarks(valMarks), nil
103+
}
104+
105+
func (t *defaultsTransformer) Exit(p cty.Path, v cty.Value) (cty.Value, error) {
106+
return v, nil
107+
}
108+
109+
// traverse walks the abstract defaults structure for a given path, returning
110+
// a set of default values (if any are present) or nil (if not). This operation
111+
// differs from applying a path to a value because we need to customize the
112+
// traversal steps for collection types, where a single set of defaults can be
113+
// applied to an arbitrary number of elements.
114+
func (d *Defaults) traverse(path cty.Path) map[string]cty.Value {
115+
if len(path) == 0 {
116+
return d.DefaultValues
117+
}
118+
119+
switch s := path[0].(type) {
120+
case cty.GetAttrStep:
121+
if d.Type.IsObjectType() {
122+
// Attribute path steps are normally applied to objects, where each
123+
// attribute may have different defaults.
124+
return d.traverseChild(s.Name, path)
125+
} else if d.Type.IsMapType() {
126+
// Literal values for maps can result in attribute path steps, in which
127+
// case we need to disregard the attribute name, as maps can have only
128+
// one child.
129+
return d.traverseChild("", path)
130+
}
131+
132+
return nil
133+
case cty.IndexStep:
134+
if d.Type.IsTupleType() {
135+
// Tuples can have different types for each element, so we look
136+
// up the defaults based on the index key.
137+
return d.traverseChild(s.Key.AsBigFloat().String(), path)
138+
} else if d.Type.IsCollectionType() {
139+
// Defaults for collection element types are stored with a blank
140+
// key, so we disregard the index key.
141+
return d.traverseChild("", path)
142+
}
143+
return nil
144+
default:
145+
// At time of writing there are no other path step types.
146+
return nil
147+
}
148+
}
149+
150+
// traverseChild continues the traversal for a given child key, and mutually
151+
// recurses with traverse.
152+
func (d *Defaults) traverseChild(name string, path cty.Path) map[string]cty.Value {
153+
if child, ok := d.Children[name]; ok {
154+
return child.traverse(path[1:])
155+
}
156+
return nil
157+
}

0 commit comments

Comments
 (0)