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:
Martin Atkins 2018-02-04 11:05:23 -08:00
parent 6c3ae68a0e
commit 2ddf8b4b8c
3 changed files with 140 additions and 11 deletions

View File

@ -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
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 {
ctx = &hcl.EvalContext{
Variables: map[string]cty.Value{},
}
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 {

View File

@ -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

View File

@ -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) {