From d6fc633aa01bb8ae988ce5d013f79c4b97bfdd96 Mon Sep 17 00:00:00 2001 From: Martin Atkins Date: Sun, 21 Jan 2018 18:24:00 -0800 Subject: [PATCH] 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. --- ext/dynblock/README.md | 38 +++++++++++++++++++++- ext/dynblock/variables_hcldec.go | 33 +++++++++++++++++++ ext/dynblock/variables_test.go | 55 +++++++------------------------- 3 files changed, 82 insertions(+), 44 deletions(-) create mode 100644 ext/dynblock/variables_hcldec.go diff --git a/ext/dynblock/README.md b/ext/dynblock/README.md index 91e22e0..2b24fdb 100644 --- a/ext/dynblock/README.md +++ b/ext/dynblock/README.md @@ -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 regardless of context, a walk can be implemented as follows: -``` +```go func walkVariables(node dynblock.WalkVariablesNode, schema *hcl.BodySchema) []hcl.Traversal { 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 This extension is going quite harshly against the grain of the HCL API, and diff --git a/ext/dynblock/variables_hcldec.go b/ext/dynblock/variables_hcldec.go new file mode 100644 index 0000000..480873a --- /dev/null +++ b/ext/dynblock/variables_hcldec.go @@ -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 +} diff --git a/ext/dynblock/variables_test.go b/ext/dynblock/variables_test.go index 372c61a..83748ef 100644 --- a/ext/dynblock/variables_test.go +++ b/ext/dynblock/variables_test.go @@ -1,10 +1,12 @@ package dynblock import ( - "fmt" "reflect" "testing" + "github.com/hashicorp/hcl2/hcldec" + "github.com/zclconf/go-cty/cty" + "github.com/davecgh/go-spew/spew" "github.com/hashicorp/hcl2/hcl" @@ -77,15 +79,18 @@ dynamic "a" { return } - rootNode := WalkForEachVariables(f.Body) - traversals := testWalkAndAccumVars(rootNode, &hcl.BodySchema{ - Blocks: []hcl.BlockHeaderSchema{ - { - Type: "a", + spec := &hcldec.BlockListSpec{ + TypeName: "a", + Nested: &hcldec.BlockListSpec{ + TypeName: "b", + Nested: &hcldec.AttrSpec{ + Name: "val", + Type: cty.String, }, }, - }) + } + traversals := ForEachVariablesHCLDec(f.Body, spec) got := make([]string, len(traversals)) for i, traversal := range traversals { got[i] = traversal.RootName() @@ -112,39 +117,3 @@ dynamic "a" { 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 -}