-
Notifications
You must be signed in to change notification settings - Fork 602
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
typeexpr: Optional object attributes with defaults
This commit extends the type expression package to add two new features: - In constraint mode, the `optional(...)` modifier can be used on object attributes to allow them to be omitted from input values to a type conversion process. Any such missing attributes will be replaced with a `null` value of the appropriate type upon conversion. - In the new defaults mode, the `optional(...)` modifier takes a second argument, which accepts a default value of an appropriate type. These defaults are returned alongside the type constraint, and may be applied prior to type conversion through the new `Defaults.Apply()` method. This change is upstreamed from Terraform, where optional object attributes have been available for some time. The defaults functionality is new and due to be released with Terraform 1.3.
- Loading branch information
Showing
5 changed files
with
1,176 additions
and
39 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,157 @@ | ||
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 | ||
} |
Oops, something went wrong.