diff options
Diffstat (limited to 'ext/typeexpr')
| -rw-r--r-- | ext/typeexpr/README.md | 68 | ||||
| -rw-r--r-- | ext/typeexpr/type_type.go | 118 | ||||
| -rw-r--r-- | ext/typeexpr/type_type_test.go | 118 |
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) + } + }) + } +} |
