Commit cce23644 authored by Alisdair McDiarmid's avatar Alisdair McDiarmid
Browse files

backport of commit 9d864c24

parent 6cb417e1
Showing with 4 additions and 2169 deletions
+4 -2169
......@@ -42,7 +42,7 @@ require (
github.com/hashicorp/go-uuid v1.0.3
github.com/hashicorp/go-version v1.6.0
github.com/hashicorp/hcl v0.0.0-20170504190234-a4b07c25de5f
github.com/hashicorp/hcl/v2 v2.13.0
github.com/hashicorp/hcl/v2 v2.14.0
github.com/hashicorp/terraform-config-inspect v0.0.0-20210209133302-4fd17a0faac2
github.com/hashicorp/terraform-registry-address v0.0.0-20220623143253-7d51757b572c
github.com/hashicorp/terraform-svchost v0.0.0-20200729002733-f050f53b9734
......
......@@ -4,13 +4,13 @@ import (
"fmt"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/ext/typeexpr"
"github.com/hashicorp/hcl/v2/gohcl"
"github.com/hashicorp/hcl/v2/hclsyntax"
"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/convert"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/typeexpr"
)
// A consistent detail message for all "not a valid identifier" diagnostics.
......
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
}
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))
}
})
}
}
// Package typeexpr is a fork of github.com/hashicorp/hcl/v2/ext/typeexpr
// which has additional experimental support for optional attributes.
//
// This is here as part of the module_variable_optional_attrs experiment.
// If that experiment is successful, the changes here may be upstreamed into
// HCL itself or, if we deem it to be Terraform-specific, we should at least
// update this documentation to reflect that this is now the primary
// Terraform-specific type expression implementation, separate from the
// upstream HCL one.
package typeexpr
package typeexpr
import (
"fmt"
"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 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, nil
case "string":
return cty.String, nil, nil
case "number":
return cty.Number, nil, nil
case "any":
if constraint {
return cty.DynamicPseudoType, nil, nil
}
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, 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, 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, nil, hcl.Diagnostics{{
Severity: hcl.DiagError,
Summary: invalidTypeSummary,
Detail: "The tuple type constructor requires one argument specifying the element types as a list.",
Subject: expr.Range().Ptr(),
}}
case "":
// okay! we'll fall through and try processing as a call, then.
default:
return cty.DynamicPseudoType, nil, hcl.Diagnostics{{
Severity: hcl.DiagError,
Summary: invalidTypeSummary,
Detail: fmt.Sprintf("The keyword %q is not a valid type specification.", kw),
Subject: expr.Range().Ptr(),
}}
}
// If we get down here then our expression isn't just a keyword, so we'll
// try to process it as a call instead.
call, diags := hcl.ExprCall(expr)
if diags.HasErrors() {
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).",
Subject: expr.Range().Ptr(),
}}
}
switch call.Name {
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 {
contextRange := call.ArgsRange
subjectRange := call.ArgsRange
if len(call.Arguments) > 1 {
// If we have too many arguments (as opposed to too _few_) then
// we'll highlight the extraneous arguments as the diagnostic
// subject.
subjectRange = hcl.RangeBetween(call.Arguments[1].Range(), call.Arguments[len(call.Arguments)-1].Range())
}
switch call.Name {
case "list", "set", "map":
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),
Subject: &subjectRange,
Context: &contextRange,
}}
case "object":
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: &subjectRange,
Context: &contextRange,
}}
case "tuple":
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.",
Subject: &subjectRange,
Context: &contextRange,
}}
}
}
switch call.Name {
case "list":
ety, defaults, diags := getType(call.Arguments[0], constraint, withDefaults)
ty := cty.List(ety)
return ty, collectionDefaults(ty, defaults), diags
case "set":
ety, defaults, diags := getType(call.Arguments[0], constraint, withDefaults)
ty := cty.Set(ety)
return ty, collectionDefaults(ty, defaults), diags
case "map":
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, 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.",
Subject: call.Arguments[0].Range().Ptr(),
Context: expr.Range().Ptr(),
}}
}
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 == "" {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: invalidTypeSummary,
Detail: "Object constructor map keys must be attribute names.",
Subject: attrDef.Key.Range().Ptr(),
Context: expr.Range().Ptr(),
})
continue
}
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
}
}
// NOTE: ObjectWithOptionalAttrs is experimental in cty at the
// time of writing, so this interface might change even in future
// minor versions of cty. We're accepting that because Terraform
// itself is considering optional attributes as experimental right now.
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, nil, hcl.Diagnostics{{
Severity: hcl.DiagError,
Summary: invalidTypeSummary,
Detail: "Tuple type constructor requires a list of element types.",
Subject: call.Arguments[0].Range().Ptr(),
Context: expr.Range().Ptr(),
}}
}
etys := make([]cty.Type, len(elemDefs))
children := make(map[string]*Defaults, len(elemDefs))
for i, defExpr := range elemDefs {
ety, elemDefaults, elemDiags := getType(defExpr, constraint, withDefaults)
diags = append(diags, elemDiags...)
etys[i] = ety
if elemDefaults != nil {
children[fmt.Sprintf("%d", i)] = elemDefaults
}
}
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, nil, hcl.Diagnostics{{
Severity: hcl.DiagError,
Summary: invalidTypeSummary,
Detail: fmt.Sprintf("Keyword %q is not a valid type constructor.", call.Name),
Subject: expr.Range().Ptr(),
}}
}
}
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
}
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
Constraint bool
Want cty.Type
WantError string
}{
// keywords
{
`bool`,
false,
cty.Bool,
"",
},
{
`number`,
false,
cty.Number,
"",
},
{
`string`,
false,
cty.String,
"",
},
{
`any`,
false,
cty.DynamicPseudoType,
`The keyword "any" cannot be used in this type specification: an exact type is required.`,
},
{
`any`,
true,
cty.DynamicPseudoType,
"",
},
{
`list`,
false,
cty.DynamicPseudoType,
"The list type constructor requires one argument specifying the element type.",
},
{
`map`,
false,
cty.DynamicPseudoType,
"The map type constructor requires one argument specifying the element type.",
},
{
`set`,
false,
cty.DynamicPseudoType,
"The set type constructor requires one argument specifying the element type.",
},
{
`object`,
false,
cty.DynamicPseudoType,
"The object type constructor requires one argument specifying the attribute types and values as a map.",
},
{
`tuple`,
false,
cty.DynamicPseudoType,
"The tuple type constructor requires one argument specifying the element types as a list.",
},
// constructors
{
`bool()`,
false,
cty.DynamicPseudoType,
`Primitive type keyword "bool" does not expect arguments.`,
},
{
`number()`,
false,
cty.DynamicPseudoType,
`Primitive type keyword "number" does not expect arguments.`,
},
{
`string()`,
false,
cty.DynamicPseudoType,
`Primitive type keyword "string" does not expect arguments.`,
},
{
`any()`,
false,
cty.DynamicPseudoType,
`Type constraint keyword "any" does not expect arguments.`,
},
{
`any()`,
true,
cty.DynamicPseudoType,
`Type constraint keyword "any" does not expect arguments.`,
},
{
`list(string)`,
false,
cty.List(cty.String),
``,
},
{
`set(string)`,
false,
cty.Set(cty.String),
``,
},
{
`map(string)`,
false,
cty.Map(cty.String),
``,
},
{
`list()`,
false,
cty.DynamicPseudoType,
`The list type constructor requires one argument specifying the element type.`,
},
{
`list(string, string)`,
false,
cty.DynamicPseudoType,
`The list type constructor requires one argument specifying the element type.`,
},
{
`list(any)`,
false,
cty.List(cty.DynamicPseudoType),
`The keyword "any" cannot be used in this type specification: an exact type is required.`,
},
{
`list(any)`,
true,
cty.List(cty.DynamicPseudoType),
``,
},
{
`object({})`,
false,
cty.EmptyObject,
``,
},
{
`object({name=string})`,
false,
cty.Object(map[string]cty.Type{"name": cty.String}),
``,
},
{
`object({"name"=string})`,
false,
cty.EmptyObject,
`Object constructor map keys must be attribute names.`,
},
{
`object({name=nope})`,
false,
cty.Object(map[string]cty.Type{"name": cty.DynamicPseudoType}),
`The keyword "nope" is not a valid type specification.`,
},
{
`object()`,
false,
cty.DynamicPseudoType,
`The object type constructor requires one argument specifying the attribute types and values as a map.`,
},
{
`object(string)`,
false,
cty.DynamicPseudoType,
`Object type constructor requires a map whose keys are attribute names and whose values are the corresponding attribute types.`,
},
{
`tuple([])`,
false,
cty.EmptyTuple,
``,
},
{
`tuple([string, bool])`,
false,
cty.Tuple([]cty.Type{cty.String, cty.Bool}),
``,
},
{
`tuple([nope])`,
false,
cty.Tuple([]cty.Type{cty.DynamicPseudoType}),
`The keyword "nope" is not a valid type specification.`,
},
{
`tuple()`,
false,
cty.DynamicPseudoType,
`The tuple type constructor requires one argument specifying the element types as a list.`,
},
{
`tuple(string)`,
false,
cty.DynamicPseudoType,
`Tuple type constructor requires a list of element types.`,
},
{
`shwoop(string)`,
false,
cty.DynamicPseudoType,
`Keyword "shwoop" is not a valid type constructor.`,
},
{
`list("string")`,
false,
cty.List(cty.DynamicPseudoType),
`A type specification is either a primitive type keyword (bool, number, string) or a complex type constructor call, like list(string).`,
},
// More interesting combinations
{
`list(object({}))`,
false,
cty.List(cty.EmptyObject),
``,
},
{
`list(map(tuple([])))`,
false,
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(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, false)
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 !got.Equals(test.Want) {
t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.Want)
}
})
}
}
func TestGetTypeJSON(t *testing.T) {
// We have fewer test cases here because we're mainly exercising the
// extra indirection in the JSON syntax package, which ultimately calls
// into the native syntax parser (which we tested extensively in
// TestGetType).
tests := []struct {
Source string
Constraint bool
Want cty.Type
WantError string
}{
{
`{"expr":"bool"}`,
false,
cty.Bool,
"",
},
{
`{"expr":"list(bool)"}`,
false,
cty.List(cty.Bool),
"",
},
{
`{"expr":"list"}`,
false,
cty.DynamicPseudoType,
"The list type constructor requires one argument specifying the element type.",
},
}
for _, test := range tests {
t.Run(test.Source, func(t *testing.T) {
file, diags := json.Parse([]byte(test.Source), "")
if diags.HasErrors() {
t.Fatalf("failed to parse: %s", diags)
}
type TestContent struct {
Expr hcl.Expression `hcl:"expr"`
}
var content TestContent
diags = gohcl.DecodeBody(file.Body, nil, &content)
if diags.HasErrors() {
t.Fatalf("failed to decode: %s", diags)
}
got, _, diags := getType(content.Expr, test.Constraint, false)
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 !got.Equals(test.Want) {
t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.Want)
}
})
}
}
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))
}
})
}
}
package typeexpr
import (
"bytes"
"fmt"
"sort"
"github.com/hashicorp/hcl/v2/hclsyntax"
"github.com/hashicorp/hcl/v2"
"github.com/zclconf/go-cty/cty"
)
// Type attempts to process the given expression as a type expression and, if
// successful, returns the resulting type. If unsuccessful, error diagnostics
// are returned.
func Type(expr hcl.Expression) (cty.Type, hcl.Diagnostics) {
ty, _, diags := getType(expr, false, false)
return ty, diags
}
// TypeConstraint attempts to parse the given expression as a type constraint
// and, if successful, returns the resulting type. If unsuccessful, error
// diagnostics are returned.
//
// A type constraint has the same structure as a type, but it additionally
// 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) {
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
// expected to appear in the HCL native syntax.
//
// This is primarily intended for showing types to the user in an application
// that uses typexpr, where the user can be assumed to be familiar with the
// type expression syntax. In applications that do not use typeexpr these
// results may be confusing to the user and so type.FriendlyName may be
// preferable, even though it's less precise.
//
// TypeString produces reasonable results only for types like what would be
// produced by the Type and TypeConstraint functions. In particular, it cannot
// support capsule types.
func TypeString(ty cty.Type) string {
// Easy cases first
switch ty {
case cty.String:
return "string"
case cty.Bool:
return "bool"
case cty.Number:
return "number"
case cty.DynamicPseudoType:
return "any"
}
if ty.IsCapsuleType() {
panic("TypeString does not support capsule types")
}
if ty.IsCollectionType() {
ety := ty.ElementType()
etyString := TypeString(ety)
switch {
case ty.IsListType():
return fmt.Sprintf("list(%s)", etyString)
case ty.IsSetType():
return fmt.Sprintf("set(%s)", etyString)
case ty.IsMapType():
return fmt.Sprintf("map(%s)", etyString)
default:
// Should never happen because the above is exhaustive
panic("unsupported collection type")
}
}
if ty.IsObjectType() {
var buf bytes.Buffer
buf.WriteString("object({")
atys := ty.AttributeTypes()
names := make([]string, 0, len(atys))
for name := range atys {
names = append(names, name)
}
sort.Strings(names)
first := true
for _, name := range names {
aty := atys[name]
if !first {
buf.WriteByte(',')
}
if !hclsyntax.ValidIdentifier(name) {
// Should never happen for any type produced by this package,
// but we'll do something reasonable here just so we don't
// produce garbage if someone gives us a hand-assembled object
// type that has weird attribute names.
// Using Go-style quoting here isn't perfect, since it doesn't
// exactly match HCL syntax, but it's fine for an edge-case.
buf.WriteString(fmt.Sprintf("%q", name))
} else {
buf.WriteString(name)
}
buf.WriteByte('=')
buf.WriteString(TypeString(aty))
first = false
}
buf.WriteString("})")
return buf.String()
}
if ty.IsTupleType() {
var buf bytes.Buffer
buf.WriteString("tuple([")
etys := ty.TupleElementTypes()
first := true
for _, ety := range etys {
if !first {
buf.WriteByte(',')
}
buf.WriteString(TypeString(ety))
first = false
}
buf.WriteString("])")
return buf.String()
}
// Should never happen because we covered all cases above.
panic(fmt.Errorf("unsupported type %#v", ty))
}
package typeexpr
import (
"testing"
"github.com/zclconf/go-cty/cty"
)
func TestTypeString(t *testing.T) {
tests := []struct {
Type cty.Type
Want string
}{
{
cty.DynamicPseudoType,
"any",
},
{
cty.String,
"string",
},
{
cty.Number,
"number",
},
{
cty.Bool,
"bool",
},
{
cty.List(cty.Number),
"list(number)",
},
{
cty.Set(cty.Bool),
"set(bool)",
},
{
cty.Map(cty.String),
"map(string)",
},
{
cty.EmptyObject,
"object({})",
},
{
cty.Object(map[string]cty.Type{"foo": cty.Bool}),
"object({foo=bool})",
},
{
cty.Object(map[string]cty.Type{"foo": cty.Bool, "bar": cty.String}),
"object({bar=string,foo=bool})",
},
{
cty.EmptyTuple,
"tuple([])",
},
{
cty.Tuple([]cty.Type{cty.Bool}),
"tuple([bool])",
},
{
cty.Tuple([]cty.Type{cty.Bool, cty.String}),
"tuple([bool,string])",
},
{
cty.List(cty.DynamicPseudoType),
"list(any)",
},
{
cty.Tuple([]cty.Type{cty.DynamicPseudoType}),
"tuple([any])",
},
{
cty.Object(map[string]cty.Type{"foo": cty.DynamicPseudoType}),
"object({foo=any})",
},
{
// We don't expect to find attributes that aren't valid identifiers
// because we only promise to support types that this package
// would've created, but we allow this situation during rendering
// just because it's convenient for applications trying to produce
// error messages about mismatched types. Note that the quoted
// attribute name is not actually accepted by our Type and
// TypeConstraint functions, so this is one situation where the
// TypeString result cannot be re-parsed by those functions.
cty.Object(map[string]cty.Type{"foo bar baz": cty.String}),
`object({"foo bar baz"=string})`,
},
}
for _, test := range tests {
t.Run(test.Type.GoString(), func(t *testing.T) {
got := TypeString(test.Type)
if got != test.Want {
t.Errorf("wrong result\ntype: %#v\ngot: %s\nwant: %s", test.Type, got, test.Want)
}
})
}
}
package typeexpr
import (
"fmt"
"reflect"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/ext/customdecode"
"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/convert"
"github.com/zclconf/go-cty/cty/function"
)
// TypeConstraintType is a cty capsule type that allows cty type constraints to
// be used as values.
//
// If TypeConstraintType is used in a context supporting the
// customdecode.CustomExpressionDecoder extension then it will implement
// expression decoding using the TypeConstraint function, thus allowing
// type expressions to be used in contexts where value expressions might
// normally be expected, such as in arguments to function calls.
var TypeConstraintType cty.Type
// TypeConstraintVal constructs a cty.Value whose type is
// TypeConstraintType.
func TypeConstraintVal(ty cty.Type) cty.Value {
return cty.CapsuleVal(TypeConstraintType, &ty)
}
// TypeConstraintFromVal extracts the type from a cty.Value of
// TypeConstraintType that was previously constructed using TypeConstraintVal.
//
// If the given value isn't a known, non-null value of TypeConstraintType
// then this function will panic.
func TypeConstraintFromVal(v cty.Value) cty.Type {
if !v.Type().Equals(TypeConstraintType) {
panic("value is not of TypeConstraintType")
}
ptr := v.EncapsulatedValue().(*cty.Type)
return *ptr
}
// ConvertFunc is a cty function that implements type conversions.
//
// Its signature is as follows:
//
// convert(value, type_constraint)
//
// ...where type_constraint is a type constraint expression as defined by
// typeexpr.TypeConstraint.
//
// It relies on HCL's customdecode extension and so it's not suitable for use
// in non-HCL contexts or if you are using a HCL syntax implementation that
// does not support customdecode for function arguments. However, it _is_
// supported for function calls in the HCL native expression syntax.
var ConvertFunc function.Function
func init() {
TypeConstraintType = cty.CapsuleWithOps("type constraint", reflect.TypeOf(cty.Type{}), &cty.CapsuleOps{
ExtensionData: func(key interface{}) interface{} {
switch key {
case customdecode.CustomExpressionDecoder:
return customdecode.CustomExpressionDecoderFunc(
func(expr hcl.Expression, ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) {
ty, diags := TypeConstraint(expr)
if diags.HasErrors() {
return cty.NilVal, diags
}
return TypeConstraintVal(ty), nil
},
)
default:
return nil
}
},
TypeGoString: func(_ reflect.Type) string {
return "typeexpr.TypeConstraintType"
},
GoString: func(raw interface{}) string {
tyPtr := raw.(*cty.Type)
return fmt.Sprintf("typeexpr.TypeConstraintVal(%#v)", *tyPtr)
},
RawEquals: func(a, b interface{}) bool {
aPtr := a.(*cty.Type)
bPtr := b.(*cty.Type)
return (*aPtr).Equals(*bPtr)
},
})
ConvertFunc = function.New(&function.Spec{
Params: []function.Parameter{
{
Name: "value",
Type: cty.DynamicPseudoType,
AllowNull: true,
AllowDynamicType: true,
},
{
Name: "type",
Type: TypeConstraintType,
},
},
Type: func(args []cty.Value) (cty.Type, error) {
wantTypePtr := args[1].EncapsulatedValue().(*cty.Type)
got, err := convert.Convert(args[0], *wantTypePtr)
if err != nil {
return cty.NilType, function.NewArgError(0, err)
}
return got.Type(), nil
},
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
v, err := convert.Convert(args[0], retType)
if err != nil {
return cty.NilVal, function.NewArgError(0, err)
}
return v, nil
},
})
}
package typeexpr
import (
"fmt"
"testing"
"github.com/zclconf/go-cty/cty"
)
func TestTypeConstraintType(t *testing.T) {
tyVal1 := TypeConstraintVal(cty.String)
tyVal2 := TypeConstraintVal(cty.String)
tyVal3 := TypeConstraintVal(cty.Number)
if !tyVal1.RawEquals(tyVal2) {
t.Errorf("tyVal1 not equal to tyVal2\ntyVal1: %#v\ntyVal2: %#v", tyVal1, tyVal2)
}
if tyVal1.RawEquals(tyVal3) {
t.Errorf("tyVal1 equal to tyVal2, but should not be\ntyVal1: %#v\ntyVal3: %#v", tyVal1, tyVal3)
}
if got, want := TypeConstraintFromVal(tyVal1), cty.String; !got.Equals(want) {
t.Errorf("wrong type extracted from tyVal1\ngot: %#v\nwant: %#v", got, want)
}
if got, want := TypeConstraintFromVal(tyVal3), cty.Number; !got.Equals(want) {
t.Errorf("wrong type extracted from tyVal3\ngot: %#v\nwant: %#v", got, want)
}
}
func TestConvertFunc(t *testing.T) {
// This is testing the convert function directly, skipping over the HCL
// parsing and evaluation steps that would normally lead there. There is
// another test in the "integrationtest" package called TestTypeConvertFunc
// that exercises the full path to this function via the hclsyntax parser.
tests := []struct {
val, ty cty.Value
want cty.Value
wantErr string
}{
// The goal here is not an exhaustive set of conversions, since that's
// already covered in cty/convert, but rather exercising different
// permutations of success and failure to make sure the function
// handles all of the results in a reasonable way.
{
cty.StringVal("hello"),
TypeConstraintVal(cty.String),
cty.StringVal("hello"),
``,
},
{
cty.True,
TypeConstraintVal(cty.String),
cty.StringVal("true"),
``,
},
{
cty.StringVal("hello"),
TypeConstraintVal(cty.Bool),
cty.NilVal,
`a bool is required`,
},
{
cty.UnknownVal(cty.Bool),
TypeConstraintVal(cty.Bool),
cty.UnknownVal(cty.Bool),
``,
},
{
cty.DynamicVal,
TypeConstraintVal(cty.Bool),
cty.UnknownVal(cty.Bool),
``,
},
{
cty.NullVal(cty.Bool),
TypeConstraintVal(cty.Bool),
cty.NullVal(cty.Bool),
``,
},
{
cty.NullVal(cty.DynamicPseudoType),
TypeConstraintVal(cty.Bool),
cty.NullVal(cty.Bool),
``,
},
{
cty.StringVal("hello").Mark(1),
TypeConstraintVal(cty.String),
cty.StringVal("hello").Mark(1),
``,
},
}
for _, test := range tests {
t.Run(fmt.Sprintf("%#v to %#v", test.val, test.ty), func(t *testing.T) {
got, err := ConvertFunc.Call([]cty.Value{test.val, test.ty})
if err != nil {
if test.wantErr != "" {
if got, want := err.Error(), test.wantErr; got != want {
t.Errorf("wrong error\ngot: %s\nwant: %s", got, want)
}
} else {
t.Errorf("unexpected error\ngot: %s\nwant: <nil>", err)
}
return
}
if test.wantErr != "" {
t.Errorf("wrong error\ngot: <nil>\nwant: %s", test.wantErr)
}
if !test.want.RawEquals(got) {
t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.want)
}
})
}
}
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment