Files
opentf/internal/typeexpr/defaults.go
Alisdair McDiarmid c85ae29419 typeexpr: Replace null attr values with defaults
Previously, when applying defaults to an input variable's given value
before type conversion, we would permit `null` attribute values to
override a specified default. This behaviour is inconsistent with the
intent of the type system underlying Terraform, and represented a
divergence from the treatment of `null` as equivalent to unset which
exists in resources. The same behaviour exists in top-level variable
definitions with `nullable = false`, and we consider this to be the
preferred behaviour here too.

This commit slightly changes default value application such that an
explicit `null` attribute value is treated as equivalent to the
attribute being missing. Default values for attributes will now replace
explicit nulls.
2022-08-12 10:26:36 -04:00

158 lines
5.1 KiB
Go

package typeexpr
import (
"github.com/zclconf/go-cty/cty"
)
// 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 an input value before converting it to the 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 {
val, err := cty.TransformWithTransformer(val, &defaultsTransformer{defaults: d})
// The transformer should never return an error.
if err != nil {
panic(err)
}
return val
}
// defaultsTransformer implements cty.Transformer, as a pre-order traversal,
// applying defaults as it goes. The pre-order traversal allows us to specify
// defaults more loosely for structural types, as the defaults for the types
// will be applied to the default value later in the walk.
type defaultsTransformer struct {
defaults *Defaults
}
var _ cty.Transformer = (*defaultsTransformer)(nil)
func (t *defaultsTransformer) Enter(p cty.Path, v cty.Value) (cty.Value, error) {
// Cannot apply defaults to an unknown value
if !v.IsKnown() {
return v, nil
}
// Look up the defaults for this path.
defaults := t.defaults.traverse(p)
// If we have no defaults, nothing to do.
if len(defaults) == 0 {
return v, nil
}
// Ensure we are working with an object or map.
vt := v.Type()
if !vt.IsObjectType() && !vt.IsMapType() {
// Cannot apply defaults because the value type is incompatible.
// We'll ignore this and let the later conversion stage display a
// more useful diagnostic.
return v, nil
}
// Unmark the value and reapply the marks later.
v, valMarks := v.Unmark()
// Convert the given value into an attribute map (if it's non-null and
// non-empty).
attrs := make(map[string]cty.Value)
if !v.IsNull() && v.LengthInt() > 0 {
attrs = v.AsValueMap()
}
// Apply defaults where attributes are missing, constructing a new
// value with the same marks.
for attr, defaultValue := range defaults {
if attrValue, ok := attrs[attr]; !ok || attrValue.IsNull() {
attrs[attr] = defaultValue
}
}
// We construct an object even if the input value was a map, as the
// type of an attribute's default value may be incompatible with the
// map element type.
return cty.ObjectVal(attrs).WithMarks(valMarks), nil
}
func (t *defaultsTransformer) Exit(p cty.Path, v cty.Value) (cty.Value, error) {
return v, nil
}
// traverse walks the abstract defaults structure for a given path, returning
// a set of default values (if any are present) or nil (if not). This operation
// differs from applying a path to a value because we need to customize the
// traversal steps for collection types, where a single set of defaults can be
// applied to an arbitrary number of elements.
func (d *Defaults) traverse(path cty.Path) map[string]cty.Value {
if len(path) == 0 {
return d.DefaultValues
}
switch s := path[0].(type) {
case cty.GetAttrStep:
if d.Type.IsObjectType() {
// Attribute path steps are normally applied to objects, where each
// attribute may have different defaults.
return d.traverseChild(s.Name, path)
} else if d.Type.IsMapType() {
// Literal values for maps can result in attribute path steps, in which
// case we need to disregard the attribute name, as maps can have only
// one child.
return d.traverseChild("", path)
}
return nil
case cty.IndexStep:
if d.Type.IsTupleType() {
// Tuples can have different types for each element, so we look
// up the defaults based on the index key.
return d.traverseChild(s.Key.AsBigFloat().String(), path)
} else if d.Type.IsCollectionType() {
// Defaults for collection element types are stored with a blank
// key, so we disregard the index key.
return d.traverseChild("", path)
}
return nil
default:
// At time of writing there are no other path step types.
return nil
}
}
// traverseChild continues the traversal for a given child key, and mutually
// recurses with traverse.
func (d *Defaults) traverseChild(name string, path cty.Path) map[string]cty.Value {
if child, ok := d.Children[name]; ok {
return child.traverse(path[1:])
}
return nil
}