mirror of
https://github.com/opentofu/opentofu.git
synced 2025-02-25 18:45:20 -06:00
Merge pull request #31729 from hashicorp/alisdair/typeexpr-upstreamed
Use upstreamed HCL typexpr package
This commit is contained in:
commit
bce43faf2e
2
go.mod
2
go.mod
@ -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
go.sum
4
go.sum
@ -387,8 +387,8 @@ github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ
|
||||
github.com/hashicorp/hcl v0.0.0-20170504190234-a4b07c25de5f h1:UdxlrJz4JOnY8W+DbLISwf2B8WXEolNRA8BGCwI9jws=
|
||||
github.com/hashicorp/hcl v0.0.0-20170504190234-a4b07c25de5f/go.mod h1:oZtUIOe8dh44I2q6ScRibXws4Ajl+d+nod3AaR9vL5w=
|
||||
github.com/hashicorp/hcl/v2 v2.0.0/go.mod h1:oVVDG71tEinNGYCxinCYadcmKU9bglqW9pV3txagJ90=
|
||||
github.com/hashicorp/hcl/v2 v2.13.0 h1:0Apadu1w6M11dyGFxWnmhhcMjkbAiKCv7G1r/2QgCNc=
|
||||
github.com/hashicorp/hcl/v2 v2.13.0/go.mod h1:e4z5nxYlWNPdDSNYX+ph14EvWYMFm3eP0zIUqPc2jr0=
|
||||
github.com/hashicorp/hcl/v2 v2.14.0 h1:jX6+Q38Ly9zaAJlAjnFVyeNSNCKKW8D0wvyg7vij5Wc=
|
||||
github.com/hashicorp/hcl/v2 v2.14.0/go.mod h1:e4z5nxYlWNPdDSNYX+ph14EvWYMFm3eP0zIUqPc2jr0=
|
||||
github.com/hashicorp/jsonapi v0.0.0-20210826224640-ee7dae0fb22d h1:9ARUJJ1VVynB176G1HCwleORqCaXm/Vx0uUi0dL26I0=
|
||||
github.com/hashicorp/jsonapi v0.0.0-20210826224640-ee7dae0fb22d/go.mod h1:Yog5+CPEM3c99L1CL2CFCYoSzgWm5vTU58idbRUaLik=
|
||||
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
|
||||
|
@ -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.
|
||||
|
@ -1,157 +0,0 @@
|
||||
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
|
||||
}
|
@ -1,504 +0,0 @@
|
||||
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))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -1,10 +0,0 @@
|
||||
// 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
|
@ -1,345 +0,0 @@
|
||||
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
|
||||
}
|
@ -1,669 +0,0 @@
|
||||
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))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -1,143 +0,0 @@
|
||||
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))
|
||||
}
|
@ -1,100 +0,0 @@
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -1,119 +0,0 @@
|
||||
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
|
||||
},
|
||||
})
|
||||
}
|
@ -1,118 +0,0 @@
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user