cmd/hcldec: allow spec file to define variables and functions
The spec file can now additionally define default variables and functions for the eval context used to evaluate the input file.
This commit is contained in:
parent
6c3ae68a0e
commit
2ddf8b4b8c
@ -12,6 +12,7 @@ import (
|
||||
"github.com/hashicorp/hcl2/hclparse"
|
||||
flag "github.com/spf13/pflag"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
"github.com/zclconf/go-cty/cty/function"
|
||||
ctyjson "github.com/zclconf/go-cty/cty/json"
|
||||
"golang.org/x/crypto/ssh/terminal"
|
||||
)
|
||||
@ -69,18 +70,26 @@ func realmain(args []string) error {
|
||||
|
||||
var diags hcl.Diagnostics
|
||||
|
||||
spec, specDiags := loadSpecFile(*specFile)
|
||||
specContent, specDiags := loadSpecFile(*specFile)
|
||||
diags = append(diags, specDiags...)
|
||||
if specDiags.HasErrors() {
|
||||
diagWr.WriteDiagnostics(diags)
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
var ctx *hcl.EvalContext
|
||||
if len(*vars) != 0 {
|
||||
ctx = &hcl.EvalContext{
|
||||
spec := specContent.RootSpec
|
||||
|
||||
ctx := &hcl.EvalContext{
|
||||
Variables: map[string]cty.Value{},
|
||||
Functions: map[string]function.Function{},
|
||||
}
|
||||
for name, val := range specContent.Variables {
|
||||
ctx.Variables[name] = val
|
||||
}
|
||||
for name, f := range specContent.Functions {
|
||||
ctx.Functions[name] = f
|
||||
}
|
||||
if len(*vars) != 0 {
|
||||
for i, varsSpec := range *vars {
|
||||
var vals map[string]cty.Value
|
||||
var valsDiags hcl.Diagnostics
|
||||
@ -98,6 +107,19 @@ func realmain(args []string) error {
|
||||
}
|
||||
}
|
||||
|
||||
// If we have empty context elements then we'll nil them out so that
|
||||
// we'll produce e.g. "variables are not allowed" errors instead of
|
||||
// "variable not found" errors.
|
||||
if len(ctx.Variables) == 0 {
|
||||
ctx.Variables = nil
|
||||
}
|
||||
if len(ctx.Functions) == 0 {
|
||||
ctx.Functions = nil
|
||||
}
|
||||
if ctx.Variables == nil && ctx.Functions == nil {
|
||||
ctx = nil
|
||||
}
|
||||
|
||||
var bodies []hcl.Body
|
||||
|
||||
if len(args) == 0 {
|
||||
|
@ -275,7 +275,7 @@ literal {
|
||||
`literal` spec blocks accept the following argument:
|
||||
|
||||
* `value` (required) - The value to return. This attribute may be an expression
|
||||
that uses [functions](#functions).
|
||||
that uses [functions](#spec-definition-functions).
|
||||
|
||||
`literal` is a leaf spec type, so no nested spec blocks are permitted.
|
||||
|
||||
@ -331,9 +331,62 @@ transform {
|
||||
spec. The variable `nested` is defined when evaluating this expression, with
|
||||
the result value of the nested spec.
|
||||
|
||||
The `result` expression may use [functions](#functions).
|
||||
The `result` expression may use [functions](#spec-definition-functions).
|
||||
|
||||
## Functions
|
||||
## Predefined Variables
|
||||
|
||||
`hcldec` accepts values for variables to expose into the input file's
|
||||
expression scope as CLI options, and this is the most common way to pass
|
||||
values since it allows them to be dynamically populated by the calling
|
||||
application.
|
||||
|
||||
However, it's also possible to pre-define variables with constant values
|
||||
within a spec file, using the top-level `variables` block type:
|
||||
|
||||
```hcl
|
||||
variables {
|
||||
name = "Stephen"
|
||||
}
|
||||
```
|
||||
|
||||
Variables of the same name defined via the `hcldec` command line with override
|
||||
predefined variables of the same name, so this mechanism can also be used to
|
||||
provide defaults for variables that are overridden only in certain contexts.
|
||||
|
||||
## Custom Functions
|
||||
|
||||
The spec can make arbitrary HCL functions available in the input file's
|
||||
expression scope, and thus allow simple computation within the input file,
|
||||
in addition to HCL's built-in operators.
|
||||
|
||||
Custom functions are defined in the spec file with the top-level `function`
|
||||
block type:
|
||||
|
||||
```
|
||||
function "add_one" {
|
||||
params = ["n"]
|
||||
result = n + 1
|
||||
}
|
||||
```
|
||||
|
||||
Functions behave in a similar way to the `transform` spec type in that the
|
||||
given `result` attribute expression is evaluated with additional variables
|
||||
defined with the same names as the defined `params`.
|
||||
|
||||
The [spec definition functions](#spec-definition-functions) can be used within
|
||||
custom function expressions, allowing them to be optionally exposed into the
|
||||
input file:
|
||||
|
||||
```
|
||||
function "upper" {
|
||||
params = ["str"]
|
||||
result = upper(str)
|
||||
}
|
||||
```
|
||||
|
||||
Custom functions defined in the spec cannot be called from the spec itself.
|
||||
|
||||
## Spec Definition Functions
|
||||
|
||||
Certain expressions within a specification may use the following functions.
|
||||
The documentation for each spec type above specifies where functions may
|
||||
@ -355,6 +408,11 @@ be used.
|
||||
* `substr(string, offset, length)` returns the requested substring of the given string.
|
||||
* `upper(string)` returns the given string with all lowercase letters converted to uppercase.
|
||||
|
||||
Note that these expressions are valid in the context of the _spec_ file, not
|
||||
the _input_. Functions can be exposed into the input file using
|
||||
[Custom Functions](#custom-functions) within the spec, which may in turn
|
||||
refer to these spec definition functions.
|
||||
|
||||
## Type Expressions
|
||||
|
||||
Type expressions are used to describe the expected type of an attribute, as
|
||||
|
@ -3,23 +3,72 @@ package main
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/hcl2/ext/userfunc"
|
||||
"github.com/hashicorp/hcl2/gohcl"
|
||||
"github.com/hashicorp/hcl2/hcl"
|
||||
"github.com/hashicorp/hcl2/hcldec"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
"github.com/zclconf/go-cty/cty/function"
|
||||
)
|
||||
|
||||
type specFileContent struct {
|
||||
Variables map[string]cty.Value
|
||||
Functions map[string]function.Function
|
||||
RootSpec hcldec.Spec
|
||||
}
|
||||
|
||||
var specCtx = &hcl.EvalContext{
|
||||
Functions: specFuncs,
|
||||
}
|
||||
|
||||
func loadSpecFile(filename string) (hcldec.Spec, hcl.Diagnostics) {
|
||||
func loadSpecFile(filename string) (specFileContent, hcl.Diagnostics) {
|
||||
file, diags := parser.ParseHCLFile(filename)
|
||||
if diags.HasErrors() {
|
||||
return errSpec, diags
|
||||
return specFileContent{RootSpec: errSpec}, diags
|
||||
}
|
||||
|
||||
return decodeSpecRoot(file.Body)
|
||||
vars, funcs, specBody, declDiags := decodeSpecDecls(file.Body)
|
||||
diags = append(diags, declDiags...)
|
||||
|
||||
spec, specDiags := decodeSpecRoot(specBody)
|
||||
diags = append(diags, specDiags...)
|
||||
|
||||
return specFileContent{
|
||||
Variables: vars,
|
||||
Functions: funcs,
|
||||
RootSpec: spec,
|
||||
}, diags
|
||||
}
|
||||
|
||||
func decodeSpecDecls(body hcl.Body) (map[string]cty.Value, map[string]function.Function, hcl.Body, hcl.Diagnostics) {
|
||||
funcs, body, diags := userfunc.DecodeUserFunctions(body, "function", func() *hcl.EvalContext {
|
||||
return specCtx
|
||||
})
|
||||
|
||||
content, body, moreDiags := body.PartialContent(&hcl.BodySchema{
|
||||
Blocks: []hcl.BlockHeaderSchema{
|
||||
{
|
||||
Type: "variables",
|
||||
},
|
||||
},
|
||||
})
|
||||
diags = append(diags, moreDiags...)
|
||||
|
||||
vars := make(map[string]cty.Value)
|
||||
for _, block := range content.Blocks {
|
||||
// We only have one block type in our schema, so we can assume all
|
||||
// blocks are of that type.
|
||||
attrs, moreDiags := block.Body.JustAttributes()
|
||||
diags = append(diags, moreDiags...)
|
||||
|
||||
for name, attr := range attrs {
|
||||
val, moreDiags := attr.Expr.Value(specCtx)
|
||||
diags = append(diags, moreDiags...)
|
||||
vars[name] = val
|
||||
}
|
||||
}
|
||||
|
||||
return vars, funcs, body, diags
|
||||
}
|
||||
|
||||
func decodeSpecRoot(body hcl.Body) (hcldec.Spec, hcl.Diagnostics) {
|
||||
|
Loading…
Reference in New Issue
Block a user