Commit 9d864c24 authored by Alisdair McDiarmid's avatar Alisdair McDiarmid
Browse files

Use upstreamed HCL typexpr package

The 2.14.0 release of HCL includes the typeexpr changes we tested here,
so now we can revert to using the HCL package and remove our fork.
parent b8bc1dd7
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