ext/dynblock: ForEachVariablesHCLDec helper

For applications already using hcldec, a decoder specification can be used
to automatically drive the recursive variable detection walk that begins
with WalkForEachVariables, allowing all "for_each" and "labels" variables
in a recursive block structure to be detected in a single call.
This commit is contained in:
Martin Atkins 2018-01-21 18:24:00 -08:00
parent 130b3c5105
commit d6fc633aa0
3 changed files with 82 additions and 44 deletions

View File

@ -103,7 +103,7 @@ requires that the caller be able to look up a schema given a nested block type.
For _simple_ formats where a specific block type name always has the same schema For _simple_ formats where a specific block type name always has the same schema
regardless of context, a walk can be implemented as follows: regardless of context, a walk can be implemented as follows:
``` ```go
func walkVariables(node dynblock.WalkVariablesNode, schema *hcl.BodySchema) []hcl.Traversal { func walkVariables(node dynblock.WalkVariablesNode, schema *hcl.BodySchema) []hcl.Traversal {
vars, children := node.Visit(schema) vars, children := node.Visit(schema)
@ -139,6 +139,42 @@ func walkVariables(node dynblock.WalkVariablesNode, schema *hcl.BodySchema) []hc
} }
``` ```
### Detecting Variables with `hcldec` Specifications
For applications that use the higher-level `hcldec` package to decode nested
configuration structures into `cty` values, the same specification can be used
to automatically drive the recursive variable-detection walk described above.
The helper function `ForEachVariablesHCLDec` allows an entire recursive
configuration structure to be analyzed in a single call given a `hcldec.Spec`
that describes the nested block structure. This means a `hcldec`-based
application can support dynamic blocks with only a little additional effort:
```go
func decodeBody(body hcl.Body, spec hcldec.Spec) (cty.Value, hcl.Diagnostics) {
// Determine which variables are needed to expand dynamic blocks
neededForDynamic := dynblock.ForEachVariablesHCLDec(body, spec)
// Build a suitable EvalContext and expand dynamic blocks
dynCtx := buildEvalContext(neededForDynamic)
dynBody := dynblock.Expand(body, dynCtx)
// Determine which variables are needed to fully decode the expanded body
// This will analyze expressions that came both from static blocks in the
// original body and from blocks that were dynamically added by Expand.
neededForDecode := hcldec.Variables(dynBody, spec)
// Build a suitable EvalContext and then fully decode the body as per the
// hcldec specification.
decCtx := buildEvalContext(neededForDecode)
return hcldec.Decode(dynBody, spec, decCtx)
}
func buildEvalContext(needed []hcl.Traversal) *hcl.EvalContext {
// (to be implemented by your application)
}
```
# Performance # Performance
This extension is going quite harshly against the grain of the HCL API, and This extension is going quite harshly against the grain of the HCL API, and

View File

@ -0,0 +1,33 @@
package dynblock
import (
"github.com/hashicorp/hcl2/hcl"
"github.com/hashicorp/hcl2/hcldec"
)
// ForEachVariablesHCLDec is a wrapper around WalkForEachVariables that
// uses the given hcldec specification to automatically drive the recursive
// walk through nested blocks in the given body.
//
// This provides more convenient access to all of the "for_each" and "labels"
// dependencies in a body for applications that are already using hcldec
// as a more convenient way to recursively decode body contents.
func ForEachVariablesHCLDec(body hcl.Body, spec hcldec.Spec) []hcl.Traversal {
rootNode := WalkForEachVariables(body)
return walkVariablesWithHCLDec(rootNode, spec)
}
func walkVariablesWithHCLDec(node WalkVariablesNode, spec hcldec.Spec) []hcl.Traversal {
vars, children := node.Visit(hcldec.ImpliedSchema(spec))
if len(children) > 0 {
childSpecs := hcldec.ChildBlockTypes(spec)
for _, child := range children {
if childSpec, exists := childSpecs[child.BlockTypeName]; exists {
vars = append(vars, walkVariablesWithHCLDec(child.Node, childSpec)...)
}
}
}
return vars
}

View File

@ -1,10 +1,12 @@
package dynblock package dynblock
import ( import (
"fmt"
"reflect" "reflect"
"testing" "testing"
"github.com/hashicorp/hcl2/hcldec"
"github.com/zclconf/go-cty/cty"
"github.com/davecgh/go-spew/spew" "github.com/davecgh/go-spew/spew"
"github.com/hashicorp/hcl2/hcl" "github.com/hashicorp/hcl2/hcl"
@ -77,15 +79,18 @@ dynamic "a" {
return return
} }
rootNode := WalkForEachVariables(f.Body) spec := &hcldec.BlockListSpec{
traversals := testWalkAndAccumVars(rootNode, &hcl.BodySchema{ TypeName: "a",
Blocks: []hcl.BlockHeaderSchema{ Nested: &hcldec.BlockListSpec{
{ TypeName: "b",
Type: "a", Nested: &hcldec.AttrSpec{
Name: "val",
Type: cty.String,
}, },
}, },
}) }
traversals := ForEachVariablesHCLDec(f.Body, spec)
got := make([]string, len(traversals)) got := make([]string, len(traversals))
for i, traversal := range traversals { for i, traversal := range traversals {
got[i] = traversal.RootName() got[i] = traversal.RootName()
@ -112,39 +117,3 @@ dynamic "a" {
t.Errorf("wrong result\ngot: %swant: %s", spew.Sdump(got), spew.Sdump(want)) t.Errorf("wrong result\ngot: %swant: %s", spew.Sdump(got), spew.Sdump(want))
} }
} }
func testWalkAndAccumVars(node WalkVariablesNode, schema *hcl.BodySchema) []hcl.Traversal {
vars, children := node.Visit(schema)
for _, child := range children {
var childSchema *hcl.BodySchema
switch child.BlockTypeName {
case "a":
childSchema = &hcl.BodySchema{
Blocks: []hcl.BlockHeaderSchema{
{
Type: "b",
LabelNames: []string{"key"},
},
},
}
case "b":
childSchema = &hcl.BodySchema{
Attributes: []hcl.AttributeSchema{
{
Name: "val",
Required: true,
},
},
}
default:
// Should never happen, because we have no other block types
// in our test input.
panic(fmt.Errorf("can't find schema for unknown block type %q", child.BlockTypeName))
}
vars = append(vars, testWalkAndAccumVars(child.Node, childSchema)...)
}
return vars
}