summaryrefslogtreecommitdiff
path: root/ext/typeexpr
diff options
context:
space:
mode:
Diffstat (limited to 'ext/typeexpr')
-rw-r--r--ext/typeexpr/README.md68
-rw-r--r--ext/typeexpr/type_type.go118
-rw-r--r--ext/typeexpr/type_type_test.go118
3 files changed, 304 insertions, 0 deletions
diff --git a/ext/typeexpr/README.md b/ext/typeexpr/README.md
index ec70947..058f1e3 100644
--- a/ext/typeexpr/README.md
+++ b/ext/typeexpr/README.md
@@ -65,3 +65,71 @@ type checking it will be one that has identifiers as its attributes; object
types with weird attributes generally show up only from arbitrary object
constructors in configuration files, which are usually treated either as maps
or as the dynamic pseudo-type.
+
+## Type Constraints as Values
+
+Along with defining a convention for writing down types using HCL expression
+constructs, this package also includes a mechanism for representing types as
+values that can be used as data within an HCL-based language.
+
+`typeexpr.TypeConstraintType` is a
+[`cty` capsule type](https://github.com/zclconf/go-cty/blob/master/docs/types.md#capsule-types)
+that encapsulates `cty.Type` values. You can construct such a value directly
+using the `TypeConstraintVal` function:
+
+```go
+tyVal := typeexpr.TypeConstraintVal(cty.String)
+
+// We can unpack the type from a value using TypeConstraintFromVal
+ty := typeExpr.TypeConstraintFromVal(tyVal)
+```
+
+However, the primary purpose of `typeexpr.TypeConstraintType` is to be
+specified as the type constraint for an argument, in which case it serves
+as a signal for HCL to treat the argument expression as a type constraint
+expression as defined above, rather than as a normal value expression.
+
+"An argument" in the above in practice means the following two locations:
+
+* As the type constraint for a parameter of a cty function that will be
+ used in an `hcl.EvalContext`. In that case, function calls in the HCL
+ native expression syntax will require the argument to be valid type constraint
+ expression syntax and the function implementation will receive a
+ `TypeConstraintType` value as the argument value for that parameter.
+
+* As the type constraint for a `hcldec.AttrSpec` or `hcldec.BlockAttrsSpec`
+ when decoding an HCL body using `hcldec`. In that case, the attributes
+ with that type constraint will be required to be valid type constraint
+ expression syntax and the result will be a `TypeConstraintType` value.
+
+Note that the special handling of these arguments means that an argument
+marked in this way must use the type constraint syntax directly. It is not
+valid to pass in a value of `TypeConstraintType` that has been obtained
+dynamically via some other expression result.
+
+`TypeConstraintType` is provided with the intent of using it internally within
+application code when incorporating type constraint expression syntax into
+an HCL-based language, not to be used for dynamic "programming with types". A
+calling application could support programming with types by defining its _own_
+capsule type, but that is not the purpose of `TypeConstraintType`.
+
+## The "convert" `cty` Function
+
+Building on the `TypeConstraintType` described in the previous section, this
+package also provides `typeexpr.ConvertFunc` which is a cty function that
+can be placed into a `cty.EvalContext` (conventionally named "convert") in
+order to provide a general type conversion function in an HCL-based language:
+
+```hcl
+ foo = convert("true", bool)
+```
+
+The second parameter uses the mechanism described in the previous section to
+require its argument to be a type constraint expression rather than a value
+expression. In doing so, it allows converting with any type constraint that
+can be expressed in this package's type constraint syntax. In the above example,
+the `foo` argument would receive a boolean true, or `cty.True` in `cty` terms.
+
+The target type constraint must always be provided statically using inline
+type constraint syntax. There is no way to _dynamically_ select a type
+constraint using this function.
diff --git a/ext/typeexpr/type_type.go b/ext/typeexpr/type_type.go
new file mode 100644
index 0000000..5462d82
--- /dev/null
+++ b/ext/typeexpr/type_type.go
@@ -0,0 +1,118 @@
+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
+ },
+ })
+}
diff --git a/ext/typeexpr/type_type_test.go b/ext/typeexpr/type_type_test.go
new file mode 100644
index 0000000..2286a2e
--- /dev/null
+++ b/ext/typeexpr/type_type_test.go
@@ -0,0 +1,118 @@
+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)
+ }
+ })
+ }
+}