| # HCL Custom Static Decoding Extension |
| |
| This HCL extension provides a mechanism for defining arguments in an HCL-based |
| language whose values are derived using custom decoding rules against the |
| HCL expression syntax, overriding the usual behavior of normal expression |
| evaluation. |
| |
| "Arguments", for the purpose of this extension, currently includes the |
| following two contexts: |
| |
| * For applications using `hcldec` for dynamic decoding, a `hcldec.AttrSpec` |
| or `hcldec.BlockAttrsSpec` can be given a special type constraint that |
| opts in to custom decoding behavior for the attribute(s) that are selected |
| by that specification. |
| |
| * When working with the HCL native expression syntax, a function given in |
| the `hcl.EvalContext` during evaluation can have parameters with special |
| type constraints that opt in to custom decoding behavior for the argument |
| expression associated with that parameter in any call. |
| |
| The above use-cases are rather abstract, so we'll consider a motivating |
| real-world example: sometimes we (language designers) need to allow users |
| to specify type constraints directly in the language itself, such as in |
| [Terraform's Input Variables](https://www.terraform.io/docs/configuration/variables.html). |
| Terraform's `variable` blocks include an argument called `type` which takes |
| a type constraint given using HCL expression building-blocks as defined by |
| [the HCL `typeexpr` extension](../typeexpr/README.md). |
| |
| A "type constraint expression" of that sort is not an expression intended to |
| be evaluated in the usual way. Instead, the physical expression is |
| deconstructed using [the static analysis operations](../../spec.md#static-analysis) |
| to produce a `cty.Type` as the result, rather than a `cty.Value`. |
| |
| The purpose of this Custom Static Decoding Extension, then, is to provide a |
| bridge to allow that sort of custom decoding to be used via mechanisms that |
| normally deal in `cty.Value`, such as `hcldec` and native syntax function |
| calls as listed above. |
| |
| (Note: [`gohcl`](https://pkg.go.dev/github.com/hashicorp/hcl/v2/gohcl) has |
| its own mechanism to support this use case, exploiting the fact that it is |
| working directly with "normal" Go types. Decoding into a struct field of |
| type `hcl.Expression` obtains the expression directly without evaluating it |
| first. The Custom Static Decoding Extension is not necessary for that `gohcl` |
| technique. You can also implement custom decoding by working directly with |
| the lowest-level HCL API, which separates extraction of and evaluation of |
| expressions into two steps.) |
| |
| ## Custom Decoding Types |
| |
| This extension relies on a convention implemented in terms of |
| [_Capsule Types_ in the underlying `cty` type system](https://github.com/zclconf/go-cty/blob/master/docs/types.md#capsule-types). `cty` allows a capsule type to carry arbitrary |
| extension metadata values as an aid to creating higher-level abstractions like |
| this extension. |
| |
| A custom argument decoding mode, then, is implemented by creating a new `cty` |
| capsule type that implements the `ExtensionData` custom operation to return |
| a decoding function when requested. For example: |
| |
| ```go |
| var keywordType cty.Type |
| keywordType = cty.CapsuleWithOps("keyword", reflect.TypeOf(""), &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) { |
| var diags hcl.Diagnostics |
| kw := hcl.ExprAsKeyword(expr) |
| if kw == "" { |
| diags = append(diags, &hcl.Diagnostic{ |
| Severity: hcl.DiagError, |
| Summary: "Invalid keyword", |
| Detail: "A keyword is required", |
| Subject: expr.Range().Ptr(), |
| }) |
| return cty.UnkownVal(keywordType), diags |
| } |
| return cty.CapsuleVal(keywordType, &kw) |
| }, |
| ) |
| default: |
| return nil |
| } |
| }, |
| }) |
| ``` |
| |
| The boilerplate here is a bit fussy, but the important part for our purposes |
| is the `case customdecode.CustomExpressionDecoder:` clause, which uses |
| a custom extension key type defined in this package to recognize when a |
| component implementing this extension is checking to see if a target type |
| has a custom decode implementation. |
| |
| In the above case we've defined a type that decodes expressions as static |
| keywords, so a keyword like `foo` would decode as an encapsulated `"foo"` |
| string, while any other sort of expression like `"baz"` or `1 + 1` would |
| return an error. |
| |
| We could then use `keywordType` as a type constraint either for a function |
| parameter or a `hcldec` attribute specification, which would require the |
| argument for that function parameter or the expression for the matching |
| attributes to be a static keyword, rather than an arbitrary expression. |
| For example, in a `hcldec.AttrSpec`: |
| |
| ```go |
| keywordSpec := &hcldec.AttrSpec{ |
| Name: "keyword", |
| Type: keywordType, |
| } |
| ``` |
| |
| The above would accept input like the following and would set its result to |
| a `cty.Value` of `keywordType`, after decoding: |
| |
| ```hcl |
| keyword = foo |
| ``` |
| |
| ## The Expression and Expression Closure `cty` types |
| |
| Building on the above, this package also includes two capsule types that use |
| the above mechanism to allow calling applications to capture expressions |
| directly and thus defer analysis to a later step, after initial decoding. |
| |
| The `customdecode.ExpressionType` type encapsulates an `hcl.Expression` alone, |
| for situations like our type constraint expression example above where it's |
| the static structure of the expression we want to inspect, and thus any |
| variables and functions defined in the evaluation context are irrelevant. |
| |
| The `customdecode.ExpressionClosureType` type encapsulates a |
| `*customdecode.ExpressionClosure` value, which binds the given expression to |
| the `hcl.EvalContext` it was asked to evaluate against and thus allows the |
| receiver of that result to later perform normal evaluation of the expression |
| with all the same variables and functions that would've been available to it |
| naturally. |
| |
| Both of these types can be used as type constraints either for `hcldec` |
| attribute specifications or for function arguments. Here's an example of |
| `ExpressionClosureType` to implement a function that can evaluate |
| an expression with some additional variables defined locally, which we'll |
| call the `with(...)` function: |
| |
| ```go |
| var WithFunc = function.New(&function.Spec{ |
| Params: []function.Parameter{ |
| { |
| Name: "variables", |
| Type: cty.DynamicPseudoType, |
| }, |
| { |
| Name: "expression", |
| Type: customdecode.ExpressionClosureType, |
| }, |
| }, |
| Type: func(args []cty.Value) (cty.Type, error) { |
| varsVal := args[0] |
| exprVal := args[1] |
| if !varsVal.Type().IsObjectType() { |
| return cty.NilVal, function.NewArgErrorf(0, "must be an object defining local variables") |
| } |
| if !varsVal.IsKnown() { |
| // We can't predict our result type until the variables object |
| // is known. |
| return cty.DynamicPseudoType, nil |
| } |
| vars := varsVal.AsValueMap() |
| closure := customdecode.ExpressionClosureFromVal(exprVal) |
| result, err := evalWithLocals(vars, closure) |
| if err != nil { |
| return cty.NilVal, err |
| } |
| return result.Type(), nil |
| }, |
| Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { |
| varsVal := args[0] |
| exprVal := args[1] |
| vars := varsVal.AsValueMap() |
| closure := customdecode.ExpressionClosureFromVal(exprVal) |
| return evalWithLocals(vars, closure) |
| }, |
| }) |
| |
| func evalWithLocals(locals map[string]cty.Value, closure *customdecode.ExpressionClosure) (cty.Value, error) { |
| childCtx := closure.EvalContext.NewChild() |
| childCtx.Variables = locals |
| val, diags := closure.Expression.Value(childCtx) |
| if diags.HasErrors() { |
| return cty.NilVal, function.NewArgErrorf(1, "couldn't evaluate expression: %s", diags.Error()) |
| } |
| return val, nil |
| } |
| ``` |
| |
| If the above function were placed into an `hcl.EvalContext` as `with`, it |
| could be used in a native syntax call to that function as follows: |
| |
| ```hcl |
| foo = with({name = "Cory"}, "${greeting}, ${name}!") |
| ``` |
| |
| The above assumes a variable in the main context called `greeting`, to which |
| the `with` function adds `name` before evaluating the expression given in |
| its second argument. This makes that second argument context-sensitive -- it |
| would behave differently if the user wrote the same thing somewhere else -- so |
| this capability should be used with care to make sure it doesn't cause confusion |
| for the end-users of your language. |
| |
| There are some other examples of this capability to evaluate expressions in |
| unusual ways in the `tryfunc` directory that is a sibling of this one. |