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.