diff options
| author | Martin Atkins <mart@degeneration.co.uk> | 2018-02-04 11:05:23 -0800 |
|---|---|---|
| committer | Martin Atkins <mart@degeneration.co.uk> | 2018-02-04 11:05:23 -0800 |
| commit | 2ddf8b4b8c9659fb62262037e41d0293306234e9 (patch) | |
| tree | a85e3842ae083630701a2a7bc0ec5036c4f37a57 /cmd | |
| parent | 6c3ae68a0e00743020f02aa21a76f3d37ca215c9 (diff) | |
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.
Diffstat (limited to 'cmd')
| -rw-r--r-- | cmd/hcldec/main.go | 32 | ||||
| -rw-r--r-- | cmd/hcldec/spec-format.md | 64 | ||||
| -rw-r--r-- | cmd/hcldec/spec.go | 55 |
3 files changed, 140 insertions, 11 deletions
diff --git a/cmd/hcldec/main.go b/cmd/hcldec/main.go index 9ace52d..6bb3bf7 100644 --- a/cmd/hcldec/main.go +++ b/cmd/hcldec/main.go @@ -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 { diff --git a/cmd/hcldec/spec-format.md b/cmd/hcldec/spec-format.md index 69b21b8..d80beda 100644 --- a/cmd/hcldec/spec-format.md +++ b/cmd/hcldec/spec-format.md @@ -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 diff --git a/cmd/hcldec/spec.go b/cmd/hcldec/spec.go index 8010919..31effd3 100644 --- a/cmd/hcldec/spec.go +++ b/cmd/hcldec/spec.go @@ -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 + } + + 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 decodeSpecRoot(file.Body) + return vars, funcs, body, diags } func decodeSpecRoot(body hcl.Body) (hcldec.Spec, hcl.Diagnostics) { |
