github.com/hashicorp/hcl/v2@v2.20.0/ext/customdecode/README.md (about) 1 # HCL Custom Static Decoding Extension 2 3 This HCL extension provides a mechanism for defining arguments in an HCL-based 4 language whose values are derived using custom decoding rules against the 5 HCL expression syntax, overriding the usual behavior of normal expression 6 evaluation. 7 8 "Arguments", for the purpose of this extension, currently includes the 9 following two contexts: 10 11 * For applications using `hcldec` for dynamic decoding, a `hcldec.AttrSpec` 12 or `hcldec.BlockAttrsSpec` can be given a special type constraint that 13 opts in to custom decoding behavior for the attribute(s) that are selected 14 by that specification. 15 16 * When working with the HCL native expression syntax, a function given in 17 the `hcl.EvalContext` during evaluation can have parameters with special 18 type constraints that opt in to custom decoding behavior for the argument 19 expression associated with that parameter in any call. 20 21 The above use-cases are rather abstract, so we'll consider a motivating 22 real-world example: sometimes we (language designers) need to allow users 23 to specify type constraints directly in the language itself, such as in 24 [Terraform's Input Variables](https://www.terraform.io/docs/configuration/variables.html). 25 Terraform's `variable` blocks include an argument called `type` which takes 26 a type constraint given using HCL expression building-blocks as defined by 27 [the HCL `typeexpr` extension](../typeexpr/README.md). 28 29 A "type constraint expression" of that sort is not an expression intended to 30 be evaluated in the usual way. Instead, the physical expression is 31 deconstructed using [the static analysis operations](../../spec.md#static-analysis) 32 to produce a `cty.Type` as the result, rather than a `cty.Value`. 33 34 The purpose of this Custom Static Decoding Extension, then, is to provide a 35 bridge to allow that sort of custom decoding to be used via mechanisms that 36 normally deal in `cty.Value`, such as `hcldec` and native syntax function 37 calls as listed above. 38 39 (Note: [`gohcl`](https://pkg.go.dev/github.com/hashicorp/hcl/v2/gohcl) has 40 its own mechanism to support this use case, exploiting the fact that it is 41 working directly with "normal" Go types. Decoding into a struct field of 42 type `hcl.Expression` obtains the expression directly without evaluating it 43 first. The Custom Static Decoding Extension is not necessary for that `gohcl` 44 technique. You can also implement custom decoding by working directly with 45 the lowest-level HCL API, which separates extraction of and evaluation of 46 expressions into two steps.) 47 48 ## Custom Decoding Types 49 50 This extension relies on a convention implemented in terms of 51 [_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 52 extension metadata values as an aid to creating higher-level abstractions like 53 this extension. 54 55 A custom argument decoding mode, then, is implemented by creating a new `cty` 56 capsule type that implements the `ExtensionData` custom operation to return 57 a decoding function when requested. For example: 58 59 ```go 60 var keywordType cty.Type 61 keywordType = cty.CapsuleWithOps("keyword", reflect.TypeOf(""), &cty.CapsuleOps{ 62 ExtensionData: func(key interface{}) interface{} { 63 switch key { 64 case customdecode.CustomExpressionDecoder: 65 return customdecode.CustomExpressionDecoderFunc( 66 func(expr hcl.Expression, ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) { 67 var diags hcl.Diagnostics 68 kw := hcl.ExprAsKeyword(expr) 69 if kw == "" { 70 diags = append(diags, &hcl.Diagnostic{ 71 Severity: hcl.DiagError, 72 Summary: "Invalid keyword", 73 Detail: "A keyword is required", 74 Subject: expr.Range().Ptr(), 75 }) 76 return cty.UnkownVal(keywordType), diags 77 } 78 return cty.CapsuleVal(keywordType, &kw) 79 }, 80 ) 81 default: 82 return nil 83 } 84 }, 85 }) 86 ``` 87 88 The boilerplate here is a bit fussy, but the important part for our purposes 89 is the `case customdecode.CustomExpressionDecoder:` clause, which uses 90 a custom extension key type defined in this package to recognize when a 91 component implementing this extension is checking to see if a target type 92 has a custom decode implementation. 93 94 In the above case we've defined a type that decodes expressions as static 95 keywords, so a keyword like `foo` would decode as an encapsulated `"foo"` 96 string, while any other sort of expression like `"baz"` or `1 + 1` would 97 return an error. 98 99 We could then use `keywordType` as a type constraint either for a function 100 parameter or a `hcldec` attribute specification, which would require the 101 argument for that function parameter or the expression for the matching 102 attributes to be a static keyword, rather than an arbitrary expression. 103 For example, in a `hcldec.AttrSpec`: 104 105 ```go 106 keywordSpec := &hcldec.AttrSpec{ 107 Name: "keyword", 108 Type: keywordType, 109 } 110 ``` 111 112 The above would accept input like the following and would set its result to 113 a `cty.Value` of `keywordType`, after decoding: 114 115 ```hcl 116 keyword = foo 117 ``` 118 119 ## The Expression and Expression Closure `cty` types 120 121 Building on the above, this package also includes two capsule types that use 122 the above mechanism to allow calling applications to capture expressions 123 directly and thus defer analysis to a later step, after initial decoding. 124 125 The `customdecode.ExpressionType` type encapsulates an `hcl.Expression` alone, 126 for situations like our type constraint expression example above where it's 127 the static structure of the expression we want to inspect, and thus any 128 variables and functions defined in the evaluation context are irrelevant. 129 130 The `customdecode.ExpressionClosureType` type encapsulates a 131 `*customdecode.ExpressionClosure` value, which binds the given expression to 132 the `hcl.EvalContext` it was asked to evaluate against and thus allows the 133 receiver of that result to later perform normal evaluation of the expression 134 with all the same variables and functions that would've been available to it 135 naturally. 136 137 Both of these types can be used as type constraints either for `hcldec` 138 attribute specifications or for function arguments. Here's an example of 139 `ExpressionClosureType` to implement a function that can evaluate 140 an expression with some additional variables defined locally, which we'll 141 call the `with(...)` function: 142 143 ```go 144 var WithFunc = function.New(&function.Spec{ 145 Params: []function.Parameter{ 146 { 147 Name: "variables", 148 Type: cty.DynamicPseudoType, 149 }, 150 { 151 Name: "expression", 152 Type: customdecode.ExpressionClosureType, 153 }, 154 }, 155 Type: func(args []cty.Value) (cty.Type, error) { 156 varsVal := args[0] 157 exprVal := args[1] 158 if !varsVal.Type().IsObjectType() { 159 return cty.NilVal, function.NewArgErrorf(0, "must be an object defining local variables") 160 } 161 if !varsVal.IsKnown() { 162 // We can't predict our result type until the variables object 163 // is known. 164 return cty.DynamicPseudoType, nil 165 } 166 vars := varsVal.AsValueMap() 167 closure := customdecode.ExpressionClosureFromVal(exprVal) 168 result, err := evalWithLocals(vars, closure) 169 if err != nil { 170 return cty.NilVal, err 171 } 172 return result.Type(), nil 173 }, 174 Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { 175 varsVal := args[0] 176 exprVal := args[1] 177 vars := varsVal.AsValueMap() 178 closure := customdecode.ExpressionClosureFromVal(exprVal) 179 return evalWithLocals(vars, closure) 180 }, 181 }) 182 183 func evalWithLocals(locals map[string]cty.Value, closure *customdecode.ExpressionClosure) (cty.Value, error) { 184 childCtx := closure.EvalContext.NewChild() 185 childCtx.Variables = locals 186 val, diags := closure.Expression.Value(childCtx) 187 if diags.HasErrors() { 188 return cty.NilVal, function.NewArgErrorf(1, "couldn't evaluate expression: %s", diags.Error()) 189 } 190 return val, nil 191 } 192 ``` 193 194 If the above function were placed into an `hcl.EvalContext` as `with`, it 195 could be used in a native syntax call to that function as follows: 196 197 ```hcl 198 foo = with({name = "Cory"}, "${greeting}, ${name}!") 199 ``` 200 201 The above assumes a variable in the main context called `greeting`, to which 202 the `with` function adds `name` before evaluating the expression given in 203 its second argument. This makes that second argument context-sensitive -- it 204 would behave differently if the user wrote the same thing somewhere else -- so 205 this capability should be used with care to make sure it doesn't cause confusion 206 for the end-users of your language. 207 208 There are some other examples of this capability to evaluate expressions in 209 unusual ways in the `tryfunc` directory that is a sibling of this one.