Skip to content

Commit

Permalink
hcldec: BlockAttrsSpec spec type
Browse files Browse the repository at this point in the history
This is the hcldec interface to Body.JustAttributes, producing a map whose
keys are the child attribute names and whose values are the results of
evaluating those expressions.

We can't just expose a JustAttributes-style spec directly here because
it's not really compatible with how hcldec thinks about things, but we
can expose a spec that decodes a specific child block because that can
then compose properly with other specs at the same level without
interfering with their operation.

The primary use for this is to allow the use of the block syntax to define
a map:

    dynamic_stuff {
      foo = "bar"
    }

JustAttributes is normally used in static analysis situations such as
enumerating the contents of a block to decide what to include in the
final EvalContext. That's not really possible with the hcldec model
because both structural decoding and expression evaluation happen
together. Therefore the use of this is pretty limited: it's useful if you
want to be compatible with an existing format based on legacy HCL where a
map was conventionally defined using block syntax, relying on the fact
that HCL did not make a strong distinction between attribute and block
syntax.
  • Loading branch information
apparentlymart committed Aug 9, 2018
1 parent 59bb5c2 commit bb724af
Show file tree
Hide file tree
Showing 6 changed files with 412 additions and 0 deletions.
34 changes: 34 additions & 0 deletions cmd/hcldec/spec-format.md
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,40 @@ of the given type must have.
`block` expects a single nested spec block, which is applied to the body of
each matching block to produce the resulting map items.

## `block_attrs` spec blocks

The `block_attrs` spec type is similar to an `attr` spec block of a map type,
but it produces a map from the attributes of a block rather than from an
attribute's expression.

```hcl
block_attrs {
block_type = "variables"
element_type = string
required = false
}
```

This allows a map with user-defined keys to be produced within block syntax,
but due to the constraints of that syntax it also means that the user will
be unable to dynamically-generate either individual key names using key
expressions or the entire map value using a `for` expression.

`block_attrs` spec blocks accept the following arguments:

* `block_type` (required) - The block type name to expect within the HCL
input file. This may be omitted when a default name selector is created
by a parent `object` spec, if the input block type name should match the
output JSON object property name.

* `element_type` (required) - The value type to require for each of the
attributes within a matched block. The resulting value will be a JSON
object whose property values are of this type.

* `required` (optional) - If `true`, an error will be produced if a block
of the given type is not present. If `false` -- the default -- an absent
block will be indicated by producing `null`.

## `literal` spec blocks

The `literal` spec type returns a given literal value, and creates no
Expand Down
44 changes: 44 additions & 0 deletions cmd/hcldec/spec.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,9 @@ func decodeSpecBlock(block *hcl.Block) (hcldec.Spec, hcl.Diagnostics) {
case "block_map":
return decodeBlockMapSpec(block.Body, impliedName)

case "block_attrs":
return decodeBlockAttrsSpec(block.Body, impliedName)

case "default":
return decodeDefaultSpec(block.Body)

Expand Down Expand Up @@ -429,6 +432,47 @@ func decodeBlockNestedSpec(body hcl.Body) (hcldec.Spec, hcl.Diagnostics) {
return spec, diags
}

func decodeBlockAttrsSpec(body hcl.Body, impliedName string) (hcldec.Spec, hcl.Diagnostics) {
type content struct {
TypeName *string `hcl:"block_type"`
ElementType hcl.Expression `hcl:"element_type"`
Required *bool `hcl:"required"`
}

var args content
diags := gohcl.DecodeBody(body, nil, &args)
if diags.HasErrors() {
return errSpec, diags
}

spec := &hcldec.BlockAttrsSpec{
TypeName: impliedName,
}

if args.Required != nil {
spec.Required = *args.Required
}
if args.TypeName != nil {
spec.TypeName = *args.TypeName
}

var typeDiags hcl.Diagnostics
spec.ElementType, typeDiags = evalTypeExpr(args.ElementType)
diags = append(diags, typeDiags...)

if spec.TypeName == "" {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Missing block_type in block_attrs spec",
Detail: "The block_type attribute is required, to specify the block type name that is expected in an input HCL file.",
Subject: body.MissingItemRange().Ptr(),
})
return errSpec, diags
}

return spec, diags
}

func decodeLiteralSpec(body hcl.Body) (hcldec.Spec, hcl.Diagnostics) {
type content struct {
Value cty.Value `hcl:"value"`
Expand Down
115 changes: 115 additions & 0 deletions hcldec/public_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,121 @@ b {}
},
{
`
b {
}
`,
&BlockAttrsSpec{
TypeName: "b",
ElementType: cty.String,
},
nil,
cty.MapValEmpty(cty.String),
0,
},
{
`
b {
hello = "world"
}
`,
&BlockAttrsSpec{
TypeName: "b",
ElementType: cty.String,
},
nil,
cty.MapVal(map[string]cty.Value{
"hello": cty.StringVal("world"),
}),
0,
},
{
`
b {
hello = true
}
`,
&BlockAttrsSpec{
TypeName: "b",
ElementType: cty.String,
},
nil,
cty.MapVal(map[string]cty.Value{
"hello": cty.StringVal("true"),
}),
0,
},
{
`
b {
hello = true
goodbye = 5
}
`,
&BlockAttrsSpec{
TypeName: "b",
ElementType: cty.String,
},
nil,
cty.MapVal(map[string]cty.Value{
"hello": cty.StringVal("true"),
"goodbye": cty.StringVal("5"),
}),
0,
},
{
``,
&BlockAttrsSpec{
TypeName: "b",
ElementType: cty.String,
},
nil,
cty.NullVal(cty.Map(cty.String)),
0,
},
{
``,
&BlockAttrsSpec{
TypeName: "b",
ElementType: cty.String,
Required: true,
},
nil,
cty.NullVal(cty.Map(cty.String)),
1, // missing b block
},
{
`
b {
}
b {
}
`,
&BlockAttrsSpec{
TypeName: "b",
ElementType: cty.String,
},
nil,
cty.MapValEmpty(cty.String),
1, // duplicate b block
},
{
`
b {
}
b {
}
`,
&BlockAttrsSpec{
TypeName: "b",
ElementType: cty.String,
Required: true,
},
nil,
cty.MapValEmpty(cty.String),
1, // duplicate b block
},
{
`
b {}
b {}
`,
Expand Down
Loading

0 comments on commit bb724af

Please sign in to comment.