summaryrefslogtreecommitdiff
path: root/ext/typeexpr
diff options
context:
space:
mode:
authorAlisdair McDiarmid <alisdair@users.noreply.github.com>2022-08-23 09:46:00 -0400
committerAlisdair McDiarmid <alisdair@users.noreply.github.com>2022-08-23 09:46:00 -0400
commit47464b236929afd2315f9c2db1dacf4514b9898e (patch)
treef437df057556032c366dc8f893772885dedd99ad /ext/typeexpr
parent3186414a5404fe3bcb0d03d36c5e8a5290d75353 (diff)
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.
Diffstat (limited to 'ext/typeexpr')
-rw-r--r--ext/typeexpr/defaults.go157
-rw-r--r--ext/typeexpr/defaults_test.go504
-rw-r--r--ext/typeexpr/get_type.go209
-rw-r--r--ext/typeexpr/get_type_test.go327
-rw-r--r--ext/typeexpr/public.go18
5 files changed, 1176 insertions, 39 deletions
diff --git a/ext/typeexpr/defaults.go b/ext/typeexpr/defaults.go
new file mode 100644
index 0000000..851c72f
--- /dev/null
+++ b/ext/typeexpr/defaults.go
@@ -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
+}
diff --git a/ext/typeexpr/defaults_test.go b/ext/typeexpr/defaults_test.go
new file mode 100644
index 0000000..a4da6bb
--- /dev/null
+++ b/ext/typeexpr/defaults_test.go
@@ -0,0 +1,504 @@
+package typeexpr
+
+import (
+ "testing"
+
+ "github.com/google/go-cmp/cmp"
+ "github.com/zclconf/go-cty/cty"
+)
+
+var (
+ valueComparer = cmp.Comparer(cty.Value.RawEquals)
+)
+
+func TestDefaults_Apply(t *testing.T) {
+ simpleObject := cty.ObjectWithOptionalAttrs(map[string]cty.Type{
+ "a": cty.String,
+ "b": cty.Bool,
+ }, []string{"b"})
+ nestedObject := cty.ObjectWithOptionalAttrs(map[string]cty.Type{
+ "c": simpleObject,
+ "d": cty.Number,
+ }, []string{"c"})
+
+ testCases := map[string]struct {
+ defaults *Defaults
+ value cty.Value
+ want cty.Value
+ }{
+ // Nothing happens when there are no default values and no children.
+ "no defaults": {
+ defaults: &Defaults{
+ Type: cty.Map(cty.String),
+ },
+ value: cty.MapVal(map[string]cty.Value{
+ "a": cty.StringVal("foo"),
+ "b": cty.StringVal("bar"),
+ }),
+ want: cty.MapVal(map[string]cty.Value{
+ "a": cty.StringVal("foo"),
+ "b": cty.StringVal("bar"),
+ }),
+ },
+ // Passing a map which does not include one of the attributes with a
+ // default results in the default being applied to the output. Output
+ // is always an object.
+ "simple object with defaults applied": {
+ defaults: &Defaults{
+ Type: simpleObject,
+ DefaultValues: map[string]cty.Value{
+ "b": cty.True,
+ },
+ },
+ value: cty.MapVal(map[string]cty.Value{
+ "a": cty.StringVal("foo"),
+ }),
+ want: cty.ObjectVal(map[string]cty.Value{
+ "a": cty.StringVal("foo"),
+ "b": cty.True,
+ }),
+ },
+ // Unknown values may be assigned to root modules during validation,
+ // and we cannot apply defaults at that time.
+ "simple object with defaults but unknown value": {
+ defaults: &Defaults{
+ Type: simpleObject,
+ DefaultValues: map[string]cty.Value{
+ "b": cty.True,
+ },
+ },
+ value: cty.UnknownVal(cty.Map(cty.String)),
+ want: cty.UnknownVal(cty.Map(cty.String)),
+ },
+ // Defaults do not override attributes which are present in the given
+ // value.
+ "simple object with optional attributes specified": {
+ defaults: &Defaults{
+ Type: simpleObject,
+ DefaultValues: map[string]cty.Value{
+ "b": cty.True,
+ },
+ },
+ value: cty.MapVal(map[string]cty.Value{
+ "a": cty.StringVal("foo"),
+ "b": cty.StringVal("false"),
+ }),
+ want: cty.ObjectVal(map[string]cty.Value{
+ "a": cty.StringVal("foo"),
+ "b": cty.StringVal("false"),
+ }),
+ },
+ // Defaults will replace explicit nulls.
+ "object with explicit null for attribute with default": {
+ defaults: &Defaults{
+ Type: simpleObject,
+ DefaultValues: map[string]cty.Value{
+ "b": cty.True,
+ },
+ },
+ value: cty.MapVal(map[string]cty.Value{
+ "a": cty.StringVal("foo"),
+ "b": cty.NullVal(cty.String),
+ }),
+ want: cty.ObjectVal(map[string]cty.Value{
+ "a": cty.StringVal("foo"),
+ "b": cty.True,
+ }),
+ },
+ // Defaults can be specified at any level of depth and will be applied
+ // so long as there is a parent value to populate.
+ "nested object with defaults applied": {
+ defaults: &Defaults{
+ Type: nestedObject,
+ Children: map[string]*Defaults{
+ "c": {
+ Type: simpleObject,
+ DefaultValues: map[string]cty.Value{
+ "b": cty.False,
+ },
+ },
+ },
+ },
+ value: cty.ObjectVal(map[string]cty.Value{
+ "c": cty.ObjectVal(map[string]cty.Value{
+ "a": cty.StringVal("foo"),
+ }),
+ "d": cty.NumberIntVal(5),
+ }),
+ want: cty.ObjectVal(map[string]cty.Value{
+ "c": cty.ObjectVal(map[string]cty.Value{
+ "a": cty.StringVal("foo"),
+ "b": cty.False,
+ }),
+ "d": cty.NumberIntVal(5),
+ }),
+ },
+ // Testing traversal of collections.
+ "map of objects with defaults applied": {
+ defaults: &Defaults{
+ Type: cty.Map(simpleObject),
+ Children: map[string]*Defaults{
+ "": {
+ Type: simpleObject,
+ DefaultValues: map[string]cty.Value{
+ "b": cty.True,
+ },
+ },
+ },
+ },
+ value: cty.MapVal(map[string]cty.Value{
+ "f": cty.ObjectVal(map[string]cty.Value{
+ "a": cty.StringVal("foo"),
+ }),
+ "b": cty.ObjectVal(map[string]cty.Value{
+ "a": cty.StringVal("bar"),
+ }),
+ }),
+ want: cty.MapVal(map[string]cty.Value{
+ "f": cty.ObjectVal(map[string]cty.Value{
+ "a": cty.StringVal("foo"),
+ "b": cty.True,
+ }),
+ "b": cty.ObjectVal(map[string]cty.Value{
+ "a": cty.StringVal("bar"),
+ "b": cty.True,
+ }),
+ }),
+ },
+ // A map variable value specified in a tfvars file will be an object,
+ // in which case we must still traverse the defaults structure
+ // correctly.
+ "map of objects with defaults applied, given object instead of map": {
+ defaults: &Defaults{
+ Type: cty.Map(simpleObject),
+ Children: map[string]*Defaults{
+ "": {
+ Type: simpleObject,
+ DefaultValues: map[string]cty.Value{
+ "b": cty.True,
+ },
+ },
+ },
+ },
+ value: cty.ObjectVal(map[string]cty.Value{
+ "f": cty.ObjectVal(map[string]cty.Value{
+ "a": cty.StringVal("foo"),
+ }),
+ "b": cty.ObjectVal(map[string]cty.Value{
+ "a": cty.StringVal("bar"),
+ }),
+ }),
+ want: cty.ObjectVal(map[string]cty.Value{
+ "f": cty.ObjectVal(map[string]cty.Value{
+ "a": cty.StringVal("foo"),
+ "b": cty.True,
+ }),
+ "b": cty.ObjectVal(map[string]cty.Value{
+ "a": cty.StringVal("bar"),
+ "b": cty.True,
+ }),
+ }),
+ },
+ // Another example of a collection type, this time exercising the code
+ // processing a tuple input.
+ "list of objects with defaults applied": {
+ defaults: &Defaults{
+ Type: cty.List(simpleObject),
+ Children: map[string]*Defaults{
+ "": {
+ Type: simpleObject,
+ DefaultValues: map[string]cty.Value{
+ "b": cty.True,
+ },
+ },
+ },
+ },
+ value: cty.TupleVal([]cty.Value{
+ cty.ObjectVal(map[string]cty.Value{
+ "a": cty.StringVal("foo"),
+ }),
+ cty.ObjectVal(map[string]cty.Value{
+ "a": cty.StringVal("bar"),
+ }),
+ }),
+ want: cty.TupleVal([]cty.Value{
+ cty.ObjectVal(map[string]cty.Value{
+ "a": cty.StringVal("foo"),
+ "b": cty.True,
+ }),
+ cty.ObjectVal(map[string]cty.Value{
+ "a": cty.StringVal("bar"),
+ "b": cty.True,
+ }),
+ }),
+ },
+ // Unlike collections, tuple variable types can have defaults for
+ // multiple element types.
+ "tuple of objects with defaults applied": {
+ defaults: &Defaults{
+ Type: cty.Tuple([]cty.Type{simpleObject, nestedObject}),
+ Children: map[string]*Defaults{
+ "0": {
+ Type: simpleObject,
+ DefaultValues: map[string]cty.Value{
+ "b": cty.False,
+ },
+ },
+ "1": {
+ Type: nestedObject,
+ DefaultValues: map[string]cty.Value{
+ "c": cty.ObjectVal(map[string]cty.Value{
+ "a": cty.StringVal("default"),
+ "b": cty.True,
+ }),
+ },
+ },
+ },
+ },
+ value: cty.TupleVal([]cty.Value{
+ cty.ObjectVal(map[string]cty.Value{
+ "a": cty.StringVal("foo"),
+ }),
+ cty.ObjectVal(map[string]cty.Value{
+ "d": cty.NumberIntVal(5),
+ }),
+ }),
+ want: cty.TupleVal([]cty.Value{
+ cty.ObjectVal(map[string]cty.Value{
+ "a": cty.StringVal("foo"),
+ "b": cty.False,
+ }),
+ cty.ObjectVal(map[string]cty.Value{
+ "c": cty.ObjectVal(map[string]cty.Value{
+ "a": cty.StringVal("default"),
+ "b": cty.True,
+ }),
+ "d": cty.NumberIntVal(5),
+ }),
+ }),
+ },
+ // More complex cases with deeply nested defaults, testing the "default
+ // within a default" edges.
+ "set of nested objects, no default sub-object": {
+ defaults: &Defaults{
+ Type: cty.Set(nestedObject),
+ Children: map[string]*Defaults{
+ "": {
+ Type: nestedObject,
+ Children: map[string]*Defaults{
+ "c": {
+ Type: simpleObject,
+ DefaultValues: map[string]cty.Value{
+ "b": cty.True,
+ },
+ },
+ },
+ },
+ },
+ },
+ value: cty.TupleVal([]cty.Value{
+ cty.ObjectVal(map[string]cty.Value{
+ "c": cty.ObjectVal(map[string]cty.Value{
+ "a": cty.StringVal("foo"),
+ }),
+ "d": cty.NumberIntVal(5),
+ }),
+ cty.ObjectVal(map[string]cty.Value{
+ "d": cty.NumberIntVal(7),
+ }),
+ }),
+ want: cty.TupleVal([]cty.Value{
+ cty.ObjectVal(map[string]cty.Value{
+ "c": cty.ObjectVal(map[string]cty.Value{
+ "a": cty.StringVal("foo"),
+ "b": cty.True,
+ }),
+ "d": cty.NumberIntVal(5),
+ }),
+ cty.ObjectVal(map[string]cty.Value{
+ // No default value for "c" specified, so none applied. The
+ // convert stage will fill in a null.
+ "d": cty.NumberIntVal(7),
+ }),
+ }),
+ },
+ "set of nested objects, empty default sub-object": {
+ defaults: &Defaults{
+ Type: cty.Set(nestedObject),
+ Children: map[string]*Defaults{
+ "": {
+ Type: nestedObject,
+ DefaultValues: map[string]cty.Value{
+ // This is a convenient shorthand which causes a
+ // missing sub-object to be filled with an object
+ // with all of the default values specified in the
+ // sub-object's type.
+ "c": cty.EmptyObjectVal,
+ },
+ Children: map[string]*Defaults{
+ "c": {
+ Type: simpleObject,
+ DefaultValues: map[string]cty.Value{
+ "b": cty.True,
+ },
+ },
+ },
+ },
+ },
+ },
+ value: cty.TupleVal([]cty.Value{
+ cty.ObjectVal(map[string]cty.Value{
+ "c": cty.ObjectVal(map[string]cty.Value{
+ "a": cty.StringVal("foo"),
+ }),
+ "d": cty.NumberIntVal(5),
+ }),
+ cty.ObjectVal(map[string]cty.Value{
+ "d": cty.NumberIntVal(7),
+ }),
+ }),
+ want: cty.TupleVal([]cty.Value{
+ cty.ObjectVal(map[string]cty.Value{
+ "c": cty.ObjectVal(map[string]cty.Value{
+ "a": cty.StringVal("foo"),
+ "b": cty.True,
+ }),
+ "d": cty.NumberIntVal(5),
+ }),
+ cty.ObjectVal(map[string]cty.Value{
+ "c": cty.ObjectVal(map[string]cty.Value{
+ // Default value for "b" is applied to the empty object
+ // specified as the default for "c"
+ "b": cty.True,
+ }),
+ "d": cty.NumberIntVal(7),
+ }),
+ }),
+ },
+ "set of nested objects, overriding default sub-object": {
+ defaults: &Defaults{
+ Type: cty.Set(nestedObject),
+ Children: map[string]*Defaults{
+ "": {
+ Type: nestedObject,
+ DefaultValues: map[string]cty.Value{
+ // If no value is given for "c", we use this object
+ // of non-default values instead. These take
+ // precedence over the default values specified in
+ // the child type.
+ "c": cty.ObjectVal(map[string]cty.Value{
+ "a": cty.StringVal("fallback"),
+ "b": cty.False,
+ }),
+ },
+ Children: map[string]*Defaults{
+ "c": {
+ Type: simpleObject,
+ DefaultValues: map[string]cty.Value{
+ "b": cty.True,
+ },
+ },
+ },
+ },
+ },
+ },
+ value: cty.TupleVal([]cty.Value{
+ cty.ObjectVal(map[string]cty.Value{
+ "c": cty.ObjectVal(map[string]cty.Value{
+ "a": cty.StringVal("foo"),
+ }),
+ "d": cty.NumberIntVal(5),
+ }),
+ cty.ObjectVal(map[string]cty.Value{
+ "d": cty.NumberIntVal(7),
+ }),
+ }),
+ want: cty.TupleVal([]cty.Value{
+ cty.ObjectVal(map[string]cty.Value{
+ "c": cty.ObjectVal(map[string]cty.Value{
+ "a": cty.StringVal("foo"),
+ "b": cty.True,
+ }),
+ "d": cty.NumberIntVal(5),
+ }),
+ cty.ObjectVal(map[string]cty.Value{
+ "c": cty.ObjectVal(map[string]cty.Value{
+ // The default value for "b" is not applied, as the
+ // default value for "c" includes a non-default value
+ // already.
+ "a": cty.StringVal("fallback"),
+ "b": cty.False,
+ }),
+ "d": cty.NumberIntVal(7),
+ }),
+ }),
+ },
+ "set of nested objects, nulls in default sub-object overridden": {
+ defaults: &Defaults{
+ Type: cty.Set(nestedObject),
+ Children: map[string]*Defaults{
+ "": {
+ Type: nestedObject,
+ DefaultValues: map[string]cty.Value{
+ // The default value for "c" is used to prepopulate
+ // the nested object's value if not specified, but
+ // the null default for its "b" attribute will be
+ // overridden by the default specified in the child
+ // type.
+ "c": cty.ObjectVal(map[string]cty.Value{
+ "a": cty.StringVal("fallback"),
+ "b": cty.NullVal(cty.Bool),
+ }),
+ },
+ Children: map[string]*Defaults{
+ "c": {
+ Type: simpleObject,
+ DefaultValues: map[string]cty.Value{
+ "b": cty.True,
+ },
+ },
+ },
+ },
+ },
+ },
+ value: cty.TupleVal([]cty.Value{
+ cty.ObjectVal(map[string]cty.Value{
+ "c": cty.ObjectVal(map[string]cty.Value{
+ "a": cty.StringVal("foo"),
+ }),
+ "d": cty.NumberIntVal(5),
+ }),
+ cty.ObjectVal(map[string]cty.Value{
+ "d": cty.NumberIntVal(7),
+ }),
+ }),
+ want: cty.TupleVal([]cty.Value{
+ cty.ObjectVal(map[string]cty.Value{
+ "c": cty.ObjectVal(map[string]cty.Value{
+ "a": cty.StringVal("foo"),
+ "b": cty.True,
+ }),
+ "d": cty.NumberIntVal(5),
+ }),
+ cty.ObjectVal(map[string]cty.Value{
+ "c": cty.ObjectVal(map[string]cty.Value{
+ // The default value for "b" overrides the explicit
+ // null in the default value for "c".
+ "a": cty.StringVal("fallback"),
+ "b": cty.True,
+ }),
+ "d": cty.NumberIntVal(7),
+ }),
+ }),
+ },
+ }
+
+ for name, tc := range testCases {
+ t.Run(name, func(t *testing.T) {
+ got := tc.defaults.Apply(tc.value)
+ if !cmp.Equal(tc.want, got, valueComparer) {
+ t.Errorf("wrong result\n%s", cmp.Diff(tc.want, got, valueComparer))
+ }
+ })
+ }
+}
diff --git a/ext/typeexpr/get_type.go b/ext/typeexpr/get_type.go
index 11b0689..98a861e 100644
--- a/ext/typeexpr/get_type.go
+++ b/ext/typeexpr/get_type.go
@@ -5,49 +5,52 @@ import (
"github.com/hashicorp/hcl/v2"
"github.com/zclconf/go-cty/cty"
+ "github.com/zclconf/go-cty/cty/convert"
)
const invalidTypeSummary = "Invalid type specification"
-// getType is the internal implementation of both Type and TypeConstraint,
-// using the passed flag to distinguish. When constraint is false, the "any"
-// keyword will produce an error.
-func getType(expr hcl.Expression, constraint bool) (cty.Type, hcl.Diagnostics) {
+// getType is the internal implementation of Type, TypeConstraint, and
+// TypeConstraintWithDefaults, using the passed flags to distinguish. When
+// `constraint` is true, the "any" keyword can be used in place of a concrete
+// type. When `withDefaults` is true, the "optional" call expression supports
+// an additional argument describing a default value.
+func getType(expr hcl.Expression, constraint, withDefaults bool) (cty.Type, *Defaults, hcl.Diagnostics) {
// First we'll try for one of our keywords
kw := hcl.ExprAsKeyword(expr)
switch kw {
case "bool":
- return cty.Bool, nil
+ return cty.Bool, nil, nil
case "string":
- return cty.String, nil
+ return cty.String, nil, nil
case "number":
- return cty.Number, nil
+ return cty.Number, nil, nil
case "any":
if constraint {
- return cty.DynamicPseudoType, nil
+ return cty.DynamicPseudoType, nil, nil
}
- return cty.DynamicPseudoType, hcl.Diagnostics{{
+ return cty.DynamicPseudoType, nil, hcl.Diagnostics{{
Severity: hcl.DiagError,
Summary: invalidTypeSummary,
Detail: fmt.Sprintf("The keyword %q cannot be used in this type specification: an exact type is required.", kw),
Subject: expr.Range().Ptr(),
}}
case "list", "map", "set":
- return cty.DynamicPseudoType, hcl.Diagnostics{{
+ return cty.DynamicPseudoType, nil, hcl.Diagnostics{{
Severity: hcl.DiagError,
Summary: invalidTypeSummary,
Detail: fmt.Sprintf("The %s type constructor requires one argument specifying the element type.", kw),
Subject: expr.Range().Ptr(),
}}
case "object":
- return cty.DynamicPseudoType, hcl.Diagnostics{{
+ return cty.DynamicPseudoType, nil, hcl.Diagnostics{{
Severity: hcl.DiagError,
Summary: invalidTypeSummary,
Detail: "The object type constructor requires one argument specifying the attribute types and values as a map.",
Subject: expr.Range().Ptr(),
}}
case "tuple":
- return cty.DynamicPseudoType, hcl.Diagnostics{{
+ return cty.DynamicPseudoType, nil, hcl.Diagnostics{{
Severity: hcl.DiagError,
Summary: invalidTypeSummary,
Detail: "The tuple type constructor requires one argument specifying the element types as a list.",
@@ -56,7 +59,7 @@ func getType(expr hcl.Expression, constraint bool) (cty.Type, hcl.Diagnostics) {
case "":
// okay! we'll fall through and try processing as a call, then.
default:
- return cty.DynamicPseudoType, hcl.Diagnostics{{
+ return cty.DynamicPseudoType, nil, hcl.Diagnostics{{
Severity: hcl.DiagError,
Summary: invalidTypeSummary,
Detail: fmt.Sprintf("The keyword %q is not a valid type specification.", kw),
@@ -68,7 +71,7 @@ func getType(expr hcl.Expression, constraint bool) (cty.Type, hcl.Diagnostics) {
// try to process it as a call instead.
call, diags := hcl.ExprCall(expr)
if diags.HasErrors() {
- return cty.DynamicPseudoType, hcl.Diagnostics{{
+ return cty.DynamicPseudoType, nil, hcl.Diagnostics{{
Severity: hcl.DiagError,
Summary: invalidTypeSummary,
Detail: "A type specification is either a primitive type keyword (bool, number, string) or a complex type constructor call, like list(string).",
@@ -77,13 +80,20 @@ func getType(expr hcl.Expression, constraint bool) (cty.Type, hcl.Diagnostics) {
}
switch call.Name {
- case "bool", "string", "number", "any":
- return cty.DynamicPseudoType, hcl.Diagnostics{{
+ case "bool", "string", "number":
+ return cty.DynamicPseudoType, nil, hcl.Diagnostics{{
Severity: hcl.DiagError,
Summary: invalidTypeSummary,
Detail: fmt.Sprintf("Primitive type keyword %q does not expect arguments.", call.Name),
Subject: &call.ArgsRange,
}}
+ case "any":
+ return cty.DynamicPseudoType, nil, hcl.Diagnostics{{
+ Severity: hcl.DiagError,
+ Summary: invalidTypeSummary,
+ Detail: fmt.Sprintf("Type constraint keyword %q does not expect arguments.", call.Name),
+ Subject: &call.ArgsRange,
+ }}
}
if len(call.Arguments) != 1 {
@@ -98,7 +108,7 @@ func getType(expr hcl.Expression, constraint bool) (cty.Type, hcl.Diagnostics) {
switch call.Name {
case "list", "set", "map":
- return cty.DynamicPseudoType, hcl.Diagnostics{{
+ return cty.DynamicPseudoType, nil, hcl.Diagnostics{{
Severity: hcl.DiagError,
Summary: invalidTypeSummary,
Detail: fmt.Sprintf("The %s type constructor requires one argument specifying the element type.", call.Name),
@@ -106,7 +116,7 @@ func getType(expr hcl.Expression, constraint bool) (cty.Type, hcl.Diagnostics) {
Context: &contextRange,
}}
case "object":
- return cty.DynamicPseudoType, hcl.Diagnostics{{
+ return cty.DynamicPseudoType, nil, hcl.Diagnostics{{
Severity: hcl.DiagError,
Summary: invalidTypeSummary,
Detail: "The object type constructor requires one argument specifying the attribute types and values as a map.",
@@ -114,7 +124,7 @@ func getType(expr hcl.Expression, constraint bool) (cty.Type, hcl.Diagnostics) {
Context: &contextRange,
}}
case "tuple":
- return cty.DynamicPseudoType, hcl.Diagnostics{{
+ return cty.DynamicPseudoType, nil, hcl.Diagnostics{{
Severity: hcl.DiagError,
Summary: invalidTypeSummary,
Detail: "The tuple type constructor requires one argument specifying the element types as a list.",
@@ -127,18 +137,21 @@ func getType(expr hcl.Expression, constraint bool) (cty.Type, hcl.Diagnostics) {
switch call.Name {
case "list":
- ety, diags := getType(call.Arguments[0], constraint)
- return cty.List(ety), diags
+ ety, defaults, diags := getType(call.Arguments[0], constraint, withDefaults)
+ ty := cty.List(ety)
+ return ty, collectionDefaults(ty, defaults), diags
case "set":
- ety, diags := getType(call.Arguments[0], constraint)
- return cty.Set(ety), diags
+ ety, defaults, diags := getType(call.Arguments[0], constraint, withDefaults)
+ ty := cty.Set(ety)
+ return ty, collectionDefaults(ty, defaults), diags
case "map":
- ety, diags := getType(call.Arguments[0], constraint)
- return cty.Map(ety), diags
+ ety, defaults, diags := getType(call.Arguments[0], constraint, withDefaults)
+ ty := cty.Map(ety)
+ return ty, collectionDefaults(ty, defaults), diags
case "object":
attrDefs, diags := hcl.ExprMap(call.Arguments[0])
if diags.HasErrors() {
- return cty.DynamicPseudoType, hcl.Diagnostics{{
+ return cty.DynamicPseudoType, nil, hcl.Diagnostics{{
Severity: hcl.DiagError,
Summary: invalidTypeSummary,
Detail: "Object type constructor requires a map whose keys are attribute names and whose values are the corresponding attribute types.",
@@ -148,6 +161,9 @@ func getType(expr hcl.Expression, constraint bool) (cty.Type, hcl.Diagnostics) {
}
atys := make(map[string]cty.Type)
+ defaultValues := make(map[string]cty.Value)
+ children := make(map[string]*Defaults)
+ var optAttrs []string
for _, attrDef := range attrDefs {
attrName := hcl.ExprAsKeyword(attrDef.Key)
if attrName == "" {
@@ -160,15 +176,102 @@ func getType(expr hcl.Expression, constraint bool) (cty.Type, hcl.Diagnostics) {
})
continue
}
- aty, attrDiags := getType(attrDef.Value, constraint)
+ atyExpr := attrDef.Value
+
+ // the attribute type expression might be wrapped in the special
+ // modifier optional(...) to indicate an optional attribute. If
+ // so, we'll unwrap that first and make a note about it being
+ // optional for when we construct the type below.
+ var defaultExpr hcl.Expression
+ if call, callDiags := hcl.ExprCall(atyExpr); !callDiags.HasErrors() {
+ if call.Name == "optional" {
+ if len(call.Arguments) < 1 {
+ diags = append(diags, &hcl.Diagnostic{
+ Severity: hcl.DiagError,
+ Summary: invalidTypeSummary,
+ Detail: "Optional attribute modifier requires the attribute type as its argument.",
+ Subject: call.ArgsRange.Ptr(),
+ Context: atyExpr.Range().Ptr(),
+ })
+ continue
+ }
+ if constraint {
+ if withDefaults {
+ switch len(call.Arguments) {
+ case 2:
+ defaultExpr = call.Arguments[1]
+ defaultVal, defaultDiags := defaultExpr.Value(nil)
+ diags = append(diags, defaultDiags...)
+ if !defaultDiags.HasErrors() {
+ optAttrs = append(optAttrs, attrName)
+ defaultValues[attrName] = defaultVal
+ }
+ case 1:
+ optAttrs = append(optAttrs, attrName)
+ default:
+ diags = append(diags, &hcl.Diagnostic{
+ Severity: hcl.DiagError,
+ Summary: invalidTypeSummary,
+ Detail: "Optional attribute modifier expects at most two arguments: the attribute type, and a default value.",
+ Subject: call.ArgsRange.Ptr(),
+ Context: atyExpr.Range().Ptr(),
+ })
+ }
+ } else {
+ if len(call.Arguments) == 1 {
+ optAttrs = append(optAttrs, attrName)
+ } else {
+ diags = append(diags, &hcl.Diagnostic{
+ Severity: hcl.DiagError,
+ Summary: invalidTypeSummary,
+ Detail: "Optional attribute modifier expects only one argument: the attribute type.",
+ Subject: call.ArgsRange.Ptr(),
+ Context: atyExpr.Range().Ptr(),
+ })
+ }
+ }
+ } else {
+ diags = append(diags, &hcl.Diagnostic{
+ Severity: hcl.DiagError,
+ Summary: invalidTypeSummary,
+ Detail: "Optional attribute modifier is only for type constraints, not for exact types.",
+ Subject: call.NameRange.Ptr(),
+ Context: atyExpr.Range().Ptr(),
+ })
+ }
+ atyExpr = call.Arguments[0]
+ }
+ }
+
+ aty, aDefaults, attrDiags := getType(atyExpr, constraint, withDefaults)
diags = append(diags, attrDiags...)
+
+ // If a default is set for an optional attribute, verify that it is
+ // convertible to the attribute type.
+ if defaultVal, ok := defaultValues[attrName]; ok {
+ _, err := convert.Convert(defaultVal, aty)
+ if err != nil {
+ diags = append(diags, &hcl.Diagnostic{
+ Severity: hcl.DiagError,
+ Summary: "Invalid default value for optional attribute",
+ Detail: fmt.Sprintf("This default value is not compatible with the attribute's type constraint: %s.", err),
+ Subject: defaultExpr.Range().Ptr(),
+ })
+ delete(defaultValues, attrName)
+ }
+ }
+
atys[attrName] = aty
+ if aDefaults != nil {
+ children[attrName] = aDefaults
+ }
}
- return cty.Object(atys), diags
+ ty := cty.ObjectWithOptionalAttrs(atys, optAttrs)
+ return ty, structuredDefaults(ty, defaultValues, children), diags
case "tuple":
elemDefs, diags := hcl.ExprList(call.Arguments[0])
if diags.HasErrors() {
- return cty.DynamicPseudoType, hcl.Diagnostics{{
+ return cty.DynamicPseudoType, nil, hcl.Diagnostics{{
Severity: hcl.DiagError,
Summary: invalidTypeSummary,
Detail: "Tuple type constructor requires a list of element types.",
@@ -177,16 +280,28 @@ func getType(expr hcl.Expression, constraint bool) (cty.Type, hcl.Diagnostics) {
}}
}
etys := make([]cty.Type, len(elemDefs))
+ children := make(map[string]*Defaults, len(elemDefs))
for i, defExpr := range elemDefs {
- ety, elemDiags := getType(defExpr, constraint)
+ ety, elemDefaults, elemDiags := getType(defExpr, constraint, withDefaults)
diags = append(diags, elemDiags...)
etys[i] = ety
+ if elemDefaults != nil {
+ children[fmt.Sprintf("%d", i)] = elemDefaults
+ }
}
- return cty.Tuple(etys), diags
+ ty := cty.Tuple(etys)
+ return ty, structuredDefaults(ty, nil, children), diags
+ case "optional":
+ return cty.DynamicPseudoType, nil, hcl.Diagnostics{{
+ Severity: hcl.DiagError,
+ Summary: invalidTypeSummary,
+ Detail: fmt.Sprintf("Keyword %q is valid only as a modifier for object type attributes.", call.Name),
+ Subject: call.NameRange.Ptr(),
+ }}
default:
// Can't access call.Arguments in this path because we've not validated
// that it contains exactly one expression here.
- return cty.DynamicPseudoType, hcl.Diagnostics{{
+ return cty.DynamicPseudoType, nil, hcl.Diagnostics{{
Severity: hcl.DiagError,
Summary: invalidTypeSummary,
Detail: fmt.Sprintf("Keyword %q is not a valid type constructor.", call.Name),
@@ -194,3 +309,33 @@ func getType(expr hcl.Expression, constraint bool) (cty.Type, hcl.Diagnostics) {
}}
}
}
+
+func collectionDefaults(ty cty.Type, defaults *Defaults) *Defaults {
+ if defaults == nil {
+ return nil
+ }
+ return &Defaults{
+ Type: ty,
+ Children: map[string]*Defaults{
+ "": defaults,
+ },
+ }
+}
+
+func structuredDefaults(ty cty.Type, defaultValues map[string]cty.Value, children map[string]*Defaults) *Defaults {
+ if len(defaultValues) == 0 && len(children) == 0 {
+ return nil
+ }
+
+ defaults := &Defaults{
+ Type: ty,
+ }
+ if len(defaultValues) > 0 {
+ defaults.DefaultValues = defaultValues
+ }
+ if len(children) > 0 {
+ defaults.Children = children
+ }
+
+ return defaults
+}
diff --git a/ext/typeexpr/get_type_test.go b/ext/typeexpr/get_type_test.go
index 391bf4f..2dca23d 100644
--- a/ext/typeexpr/get_type_test.go
+++ b/ext/typeexpr/get_type_test.go
@@ -1,16 +1,22 @@
package typeexpr
import (
+ "fmt"
"testing"
"github.com/hashicorp/hcl/v2/gohcl"
+ "github.com/google/go-cmp/cmp"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
"github.com/hashicorp/hcl/v2/json"
"github.com/zclconf/go-cty/cty"
)
+var (
+ typeComparer = cmp.Comparer(cty.Type.Equals)
+)
+
func TestGetType(t *testing.T) {
tests := []struct {
Source string
@@ -103,13 +109,13 @@ func TestGetType(t *testing.T) {
`any()`,
false,
cty.DynamicPseudoType,
- `Primitive type keyword "any" does not expect arguments.`,
+ `Type constraint keyword "any" does not expect arguments.`,
},
{
`any()`,
true,
cty.DynamicPseudoType,
- `Primitive type keyword "any" does not expect arguments.`,
+ `Type constraint keyword "any" does not expect arguments.`,
},
{
`list(string)`,
@@ -245,16 +251,83 @@ func TestGetType(t *testing.T) {
cty.List(cty.Map(cty.EmptyTuple)),
``,
},
+
+ // Optional modifier
+ {
+ `object({name=string,age=optional(number)})`,
+ true,
+ cty.ObjectWithOptionalAttrs(map[string]cty.Type{
+ "name": cty.String,
+ "age": cty.Number,
+ }, []string{"age"}),
+ ``,
+ },
+ {
+ `object({name=string,meta=optional(any)})`,
+ true,
+ cty.ObjectWithOptionalAttrs(map[string]cty.Type{
+ "name": cty.String,
+ "meta": cty.DynamicPseudoType,
+ }, []string{"meta"}),
+ ``,
+ },
+ {
+ `object({name=string,age=optional(number)})`,
+ false,
+ cty.Object(map[string]cty.Type{
+ "name": cty.String,
+ "age": cty.Number,
+ }),
+ `Optional attribute modifier is only for type constraints, not for exact types.`,
+ },
+ {
+ `object({name=string,meta=optional(any)})`,
+ false,
+ cty.Object(map[string]cty.Type{
+ "name": cty.String,
+ "meta": cty.DynamicPseudoType,
+ }),
+ `Optional attribute modifier is only for type constraints, not for exact types.`,
+ },
+ {
+ `object({name=string,meta=optional()})`,
+ true,
+ cty.Object(map[string]cty.Type{
+ "name": cty.String,
+ }),
+ `Optional attribute modifier requires the attribute type as its argument.`,
+ },
+ {
+ `object({name=string,meta=optional(string, "hello")})`,
+ true,
+ cty.Object(map[string]cty.Type{
+ "name": cty.String,
+ "meta": cty.String,
+ }),
+ `Optional attribute modifier expects only one argument: the attribute type.`,
+ },
+ {
+ `optional(string)`,
+ false,
+ cty.DynamicPseudoType,
+ `Keyword "optional" is valid only as a modifier for object type attributes.`,
+ },
+ {
+ `optional`,
+ false,
+ cty.DynamicPseudoType,
+ `The keyword "optional" is not a valid type specification.`,
+ },
}
for _, test := range tests {
- t.Run(test.Source, func(t *testing.T) {
+ t.Run(fmt.Sprintf("%s (constraint=%v)", test.Source, test.Constraint), func(t *testing.T) {
expr, diags := hclsyntax.ParseExpression([]byte(test.Source), "", hcl.Pos{Line: 1, Column: 1})
if diags.HasErrors() {
t.Fatalf("failed to parse: %s", diags)
}
- got, diags := getType(expr, test.Constraint)
+ got, _, diags := getType(expr, test.Constraint, false)
if test.WantError == "" {
for _, diag := range diags {
t.Error(diag)
@@ -326,7 +399,7 @@ func TestGetTypeJSON(t *testing.T) {
t.Fatalf("failed to decode: %s", diags)
}
- got, diags := getType(content.Expr, test.Constraint)
+ got, _, diags := getType(content.Expr, test.Constraint, false)
if test.WantError == "" {
for _, diag := range diags {
t.Error(diag)
@@ -350,3 +423,247 @@ func TestGetTypeJSON(t *testing.T) {
})
}
}
+
+func TestGetTypeDefaults(t *testing.T) {
+ tests := []struct {
+ Source string
+ Want *Defaults
+ WantError string
+ }{
+ // primitive types have nil defaults
+ {
+ `bool`,
+ nil,
+ "",
+ },
+ {
+ `number`,
+ nil,
+ "",
+ },
+ {
+ `string`,
+ nil,
+ "",
+ },
+ {
+ `any`,
+ nil,
+ "",
+ },
+
+ // complex structures with no defaults have nil defaults
+ {
+ `map(string)`,
+ nil,
+ "",
+ },
+ {
+ `set(number)`,
+ nil,
+ "",
+ },
+ {
+ `tuple([number, string])`,
+ nil,
+ "",
+ },
+ {
+ `object({ a = string, b = number })`,
+ nil,
+ "",
+ },
+ {
+ `map(list(object({ a = string, b = optional(number) })))`,
+ nil,
+ "",
+ },
+
+ // object optional attribute with defaults
+ {
+ `object({ a = string, b = optional(number, 5) })`,
+ &Defaults{
+ Type: cty.ObjectWithOptionalAttrs(map[string]cty.Type{
+ "a": cty.String,
+ "b": cty.Number,
+ }, []string{"b"}),
+ DefaultValues: map[string]cty.Value{
+ "b": cty.NumberIntVal(5),
+ },
+ },
+ "",
+ },
+
+ // nested defaults
+ {
+ `object({ a = optional(object({ b = optional(number, 5) }), {}) })`,
+ &Defaults{
+ Type: cty.ObjectWithOptionalAttrs(map[string]cty.Type{
+ "a": cty.ObjectWithOptionalAttrs(map[string]cty.Type{
+ "b": cty.Number,
+ }, []string{"b"}),
+ }, []string{"a"}),
+ DefaultValues: map[string]cty.Value{
+ "a": cty.EmptyObjectVal,
+ },
+ Children: map[string]*Defaults{
+ "a": {
+ Type: cty.ObjectWithOptionalAttrs(map[string]cty.Type{
+ "b": cty.Number,
+ }, []string{"b"}),
+ DefaultValues: map[string]cty.Value{
+ "b": cty.NumberIntVal(5),
+ },
+ },
+ },
+ },
+ "",
+ },
+
+ // collections of objects with defaults
+ {
+ `map(object({ a = string, b = optional(number, 5) }))`,
+ &Defaults{
+ Type: cty.Map(cty.ObjectWithOptionalAttrs(map[string]cty.Type{
+ "a": cty.String,
+ "b": cty.Number,
+ }, []string{"b"})),
+ Children: map[string]*Defaults{
+ "": {
+ Type: cty.ObjectWithOptionalAttrs(map[string]cty.Type{
+ "a": cty.String,
+ "b": cty.Number,
+ }, []string{"b"}),
+ DefaultValues: map[string]cty.Value{
+ "b": cty.NumberIntVal(5),
+ },
+ },
+ },
+ },
+ "",
+ },
+ {
+ `list(object({ a = string, b = optional(number, 5) }))`,
+ &Defaults{
+ Type: cty.List(cty.ObjectWithOptionalAttrs(map[string]cty.Type{
+ "a": cty.String,
+ "b": cty.Number,
+ }, []string{"b"})),
+ Children: map[string]*Defaults{
+ "": {
+ Type: cty.ObjectWithOptionalAttrs(map[string]cty.Type{
+ "a": cty.String,
+ "b": cty.Number,
+ }, []string{"b"}),
+ DefaultValues: map[string]cty.Value{
+ "b": cty.NumberIntVal(5),
+ },
+ },
+ },
+ },
+ "",
+ },
+ {
+ `set(object({ a = string, b = optional(number, 5) }))`,
+ &Defaults{
+ Type: cty.Set(cty.ObjectWithOptionalAttrs(map[string]cty.Type{
+ "a": cty.String,
+ "b": cty.Number,
+ }, []string{"b"})),
+ Children: map[string]*Defaults{
+ "": {
+ Type: cty.ObjectWithOptionalAttrs(map[string]cty.Type{
+ "a": cty.String,
+ "b": cty.Number,
+ }, []string{"b"}),
+ DefaultValues: map[string]cty.Value{
+ "b": cty.NumberIntVal(5),
+ },
+ },
+ },
+ },
+ "",
+ },
+
+ // tuples containing objects with defaults work differently from
+ // collections
+ {
+ `tuple([string, bool, object({ a = string, b = optional(number, 5) })])`,
+ &Defaults{
+ Type: cty.Tuple([]cty.Type{
+ cty.String,
+ cty.Bool,
+ cty.ObjectWithOptionalAttrs(map[string]cty.Type{
+ "a": cty.String,
+ "b": cty.Number,
+ }, []string{"b"}),
+ }),
+ Children: map[string]*Defaults{
+ "2": {
+ Type: cty.ObjectWithOptionalAttrs(map[string]cty.Type{
+ "a": cty.String,
+ "b": cty.Number,
+ }, []string{"b"}),
+ DefaultValues: map[string]cty.Value{
+ "b": cty.NumberIntVal(5),
+ },
+ },
+ },
+ },
+ "",
+ },
+
+ // incompatible default value causes an error
+ {
+ `object({ a = optional(string, "hello"), b = optional(number, true) })`,
+ &Defaults{
+ Type: cty.ObjectWithOptionalAttrs(map[string]cty.Type{
+ "a": cty.String,
+ "b": cty.Number,
+ }, []string{"a", "b"}),
+ DefaultValues: map[string]cty.Value{
+ "a": cty.StringVal("hello"),
+ },
+ },
+ "This default value is not compatible with the attribute's type constraint: number required.",
+ },
+
+ // Too many arguments
+ {
+ `object({name=string,meta=optional(string, "hello", "world")})`,
+ nil,
+ `Optional attribute modifier expects at most two arguments: the attribute type, and a default value.`,
+ },
+ }
+
+ for _, test := range tests {
+ t.Run(test.Source, func(t *testing.T) {
+ expr, diags := hclsyntax.ParseExpression([]byte(test.Source), "", hcl.Pos{Line: 1, Column: 1})
+ if diags.HasErrors() {
+ t.Fatalf("failed to parse: %s", diags)
+ }
+
+ _, got, diags := getType(expr, true, true)
+ if test.WantError == "" {
+ for _, diag := range diags {
+ t.Error(diag)
+ }
+ } else {
+ found := false
+ for _, diag := range diags {
+ t.Log(diag)
+ if diag.Severity == hcl.DiagError && diag.Detail == test.WantError {
+ found = true
+ }
+ }
+ if !found {
+ t.Errorf("missing expected error detail message: %s", test.WantError)
+ }
+ }
+
+ if !cmp.Equal(test.Want, got, valueComparer, typeComparer) {
+ t.Errorf("wrong result\n%s", cmp.Diff(test.Want, got, valueComparer, typeComparer))
+ }
+ })
+ }
+}
diff --git a/ext/typeexpr/public.go b/ext/typeexpr/public.go
index 3b8f618..82f215c 100644
--- a/ext/typeexpr/public.go
+++ b/ext/typeexpr/public.go
@@ -15,7 +15,8 @@ import (
// successful, returns the resulting type. If unsuccessful, error diagnostics
// are returned.
func Type(expr hcl.Expression) (cty.Type, hcl.Diagnostics) {
- return getType(expr, false)
+ ty, _, diags := getType(expr, false, false)
+ return ty, diags
}
// TypeConstraint attempts to parse the given expression as a type constraint
@@ -26,7 +27,20 @@ func Type(expr hcl.Expression) (cty.Type, hcl.Diagnostics) {
// allows the keyword "any" to represent cty.DynamicPseudoType, which is often
// used as a wildcard in type checking and type conversion operations.
func TypeConstraint(expr hcl.Expression) (cty.Type, hcl.Diagnostics) {
- return getType(expr, true)
+ ty, _, diags := getType(expr, true, false)
+ return ty, diags
+}
+
+// TypeConstraintWithDefaults attempts to parse the given expression as a type
+// constraint which may include default values for object attributes. If
+// successful both the resulting type and corresponding defaults are returned.
+// If unsuccessful, error diagnostics are returned.
+//
+// When using this function, defaults should be applied to the input value
+// before type conversion, to ensure that objects with missing attributes have
+// default values populated.
+func TypeConstraintWithDefaults(expr hcl.Expression) (cty.Type, *Defaults, hcl.Diagnostics) {
+ return getType(expr, true, true)
}
// TypeString returns a string rendering of the given type as it would be