Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions ext/dynblock/expand_body.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ type expandBody struct {
forEachCtx *hcl.EvalContext
iteration *iteration // non-nil if we're nested inside another "dynamic" block

checkForEach []func(cty.Value, hcl.Expression, *hcl.EvalContext) hcl.Diagnostics

// These are used with PartialContent to produce a "remaining items"
// body to return. They are nil on all bodies fresh out of the transformer.
//
Expand Down Expand Up @@ -66,6 +68,7 @@ func (b *expandBody) PartialContent(schema *hcl.BodySchema) (*hcl.BodyContent, h
original: b.original,
forEachCtx: b.forEachCtx,
iteration: b.iteration,
checkForEach: b.checkForEach,
hiddenAttrs: make(map[string]struct{}),
hiddenBlocks: make(map[string]hcl.BlockHeaderSchema),
}
Expand Down Expand Up @@ -236,6 +239,7 @@ func (b *expandBody) expandChild(child hcl.Body, i *iteration) hcl.Body {
chiCtx := i.EvalContext(b.forEachCtx)
ret := Expand(child, chiCtx)
ret.(*expandBody).iteration = i
ret.(*expandBody).checkForEach = b.checkForEach
return ret
}

Expand Down
85 changes: 85 additions & 0 deletions ext/dynblock/expand_body_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@ import (
"strings"
"testing"

"github.com/davecgh/go-spew/spew"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hcldec"
"github.com/hashicorp/hcl/v2/hcltest"
"github.com/zclconf/go-cty-debug/ctydebug"
"github.com/zclconf/go-cty/cty"
)

Expand Down Expand Up @@ -336,6 +338,89 @@ func TestExpand(t *testing.T) {

}

func TestExpandWithForEachCheck(t *testing.T) {
forEachExpr := hcltest.MockExprLiteral(cty.MapValEmpty(cty.String).Mark("boop"))
evalCtx := &hcl.EvalContext{}
srcContent := &hcl.BodyContent{
Blocks: hcl.Blocks{
{
Type: "dynamic",
Labels: []string{"foo"},
LabelRanges: []hcl.Range{{}},
Body: hcltest.MockBody(&hcl.BodyContent{
Attributes: hcltest.MockAttrs(map[string]hcl.Expression{
"for_each": forEachExpr,
}),
Blocks: hcl.Blocks{
{
Type: "content",
Body: hcltest.MockBody(&hcl.BodyContent{}),
},
},
}),
},
},
}
srcBody := hcltest.MockBody(srcContent)

hookCalled := false
var gotV cty.Value
var gotEvalCtx *hcl.EvalContext

expBody := Expand(
srcBody, evalCtx,
OptCheckForEach(func(v cty.Value, e hcl.Expression, ec *hcl.EvalContext) hcl.Diagnostics {
hookCalled = true
gotV = v
gotEvalCtx = ec
return hcl.Diagnostics{
&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Bad for_each",
Detail: "I don't like it.",
Expression: e,
EvalContext: ec,
Extra: "diagnostic extra",
},
}
}),
)

_, diags := expBody.Content(&hcl.BodySchema{
Blocks: []hcl.BlockHeaderSchema{
{
Type: "foo",
},
},
})
if !diags.HasErrors() {
t.Fatal("succeeded; want an error")
}
if len(diags) != 1 {
t.Fatalf("wrong number of diagnostics; want only one\n%s", spew.Sdump(diags))
}
if got, want := diags[0].Summary, "Bad for_each"; got != want {
t.Fatalf("wrong error\ngot: %s\nwant: %s\n\n%s", got, want, spew.Sdump(diags[0]))
}
if got, want := diags[0].Extra, "diagnostic extra"; got != want {
// This is important to allow the application which provided the
// hook to pass application-specific extra values through this
// API in case the hook's diagnostics need some sort of special
// treatment.
t.Fatalf("diagnostic didn't preserve 'extra' field\ngot: %s\nwant: %s\n\n%s", got, want, spew.Sdump(diags[0]))
}

if !hookCalled {
t.Fatal("check hook wasn't called")
}
if !gotV.HasMark("boop") {
t.Errorf("wrong value passed to check hook; want the value marked \"boop\"\n%s", ctydebug.ValueString(gotV))
}
if gotEvalCtx != evalCtx {
t.Error("wrong EvalContext passed to check hook; want the one passed to Expand")
}
}

func TestExpandUnknownBodies(t *testing.T) {
srcContent := &hcl.BodyContent{
Blocks: hcl.Blocks{
Expand Down
10 changes: 10 additions & 0 deletions ext/dynblock/expand_spec.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,16 @@ func (b *expandBody) decodeSpec(blockS *hcl.BlockHeaderSchema, rawSpec *hcl.Bloc
eachAttr := specContent.Attributes["for_each"]
eachVal, eachDiags := eachAttr.Expr.Value(b.forEachCtx)
diags = append(diags, eachDiags...)
if diags.HasErrors() {
return nil, diags
}
for _, check := range b.checkForEach {
moreDiags := check(eachVal, eachAttr.Expr, b.forEachCtx)
diags = append(diags, moreDiags...)
if moreDiags.HasErrors() {
return nil, diags
}
}

if !eachVal.CanIterateElements() && eachVal.Type() != cty.DynamicPseudoType {
// We skip this error for DynamicPseudoType because that means we either
Expand Down
23 changes: 23 additions & 0 deletions ext/dynblock/options.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package dynblock

import (
"github.com/hashicorp/hcl/v2"
"github.com/zclconf/go-cty/cty"
)

type ExpandOption interface {
applyExpandOption(*expandBody)
}

type optCheckForEach struct {
check func(cty.Value, hcl.Expression, *hcl.EvalContext) hcl.Diagnostics
}

func OptCheckForEach(check func(cty.Value, hcl.Expression, *hcl.EvalContext) hcl.Diagnostics) ExpandOption {
return optCheckForEach{check}
}

// applyExpandOption implements ExpandOption.
func (o optCheckForEach) applyExpandOption(body *expandBody) {
body.checkForEach = append(body.checkForEach, o.check)
}
38 changes: 21 additions & 17 deletions ext/dynblock/public.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,24 +27,28 @@ import (
// multi-dimensional iteration. However, it is not possible to
// dynamically-generate the "dynamic" blocks themselves except through nesting.
//
// parent {
// dynamic "child" {
// for_each = child_objs
// content {
// dynamic "grandchild" {
// for_each = child.value.children
// labels = [grandchild.key]
// content {
// parent_key = child.key
// value = grandchild.value
// }
// }
// }
// }
// }
func Expand(body hcl.Body, ctx *hcl.EvalContext) hcl.Body {
return &expandBody{
// parent {
// dynamic "child" {
// for_each = child_objs
// content {
// dynamic "grandchild" {
// for_each = child.value.children
// labels = [grandchild.key]
// content {
// parent_key = child.key
// value = grandchild.value
// }
// }
// }
// }
// }
func Expand(body hcl.Body, ctx *hcl.EvalContext, opts ...ExpandOption) hcl.Body {
ret := &expandBody{
original: body,
forEachCtx: ctx,
}
for _, opt := range opts {
opt.applyExpandOption(ret)
}
return ret
}