bb724af7fd
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.
637 lines
16 KiB
Go
637 lines
16 KiB
Go
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) (specFileContent, hcl.Diagnostics) {
|
|
file, diags := parser.ParseHCLFile(filename)
|
|
if diags.HasErrors() {
|
|
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 vars, funcs, body, diags
|
|
}
|
|
|
|
func decodeSpecRoot(body hcl.Body) (hcldec.Spec, hcl.Diagnostics) {
|
|
content, diags := body.Content(specSchemaUnlabelled)
|
|
|
|
if len(content.Blocks) == 0 {
|
|
if diags.HasErrors() {
|
|
// If we already have errors then they probably explain
|
|
// why we have no blocks, so we'll skip our additional
|
|
// error message added below.
|
|
return errSpec, diags
|
|
}
|
|
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Missing spec block",
|
|
Detail: "A spec file must have exactly one root block specifying how to map to a JSON value.",
|
|
Subject: body.MissingItemRange().Ptr(),
|
|
})
|
|
return errSpec, diags
|
|
}
|
|
|
|
if len(content.Blocks) > 1 {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Extraneous spec block",
|
|
Detail: "A spec file must have exactly one root block specifying how to map to a JSON value.",
|
|
Subject: &content.Blocks[1].DefRange,
|
|
})
|
|
return errSpec, diags
|
|
}
|
|
|
|
spec, specDiags := decodeSpecBlock(content.Blocks[0])
|
|
diags = append(diags, specDiags...)
|
|
return spec, diags
|
|
}
|
|
|
|
func decodeSpecBlock(block *hcl.Block) (hcldec.Spec, hcl.Diagnostics) {
|
|
var impliedName string
|
|
if len(block.Labels) > 0 {
|
|
impliedName = block.Labels[0]
|
|
}
|
|
|
|
switch block.Type {
|
|
|
|
case "object":
|
|
return decodeObjectSpec(block.Body)
|
|
|
|
case "array":
|
|
return decodeArraySpec(block.Body)
|
|
|
|
case "attr":
|
|
return decodeAttrSpec(block.Body, impliedName)
|
|
|
|
case "block":
|
|
return decodeBlockSpec(block.Body, impliedName)
|
|
|
|
case "block_list":
|
|
return decodeBlockListSpec(block.Body, impliedName)
|
|
|
|
case "block_set":
|
|
return decodeBlockSetSpec(block.Body, impliedName)
|
|
|
|
case "block_map":
|
|
return decodeBlockMapSpec(block.Body, impliedName)
|
|
|
|
case "block_attrs":
|
|
return decodeBlockAttrsSpec(block.Body, impliedName)
|
|
|
|
case "default":
|
|
return decodeDefaultSpec(block.Body)
|
|
|
|
case "transform":
|
|
return decodeTransformSpec(block.Body)
|
|
|
|
case "literal":
|
|
return decodeLiteralSpec(block.Body)
|
|
|
|
default:
|
|
// Should never happen, because the above cases should be exhaustive
|
|
// for our schema.
|
|
var diags hcl.Diagnostics
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Invalid spec block",
|
|
Detail: fmt.Sprintf("Blocks of type %q are not expected here.", block.Type),
|
|
Subject: &block.TypeRange,
|
|
})
|
|
return errSpec, diags
|
|
}
|
|
}
|
|
|
|
func decodeObjectSpec(body hcl.Body) (hcldec.Spec, hcl.Diagnostics) {
|
|
content, diags := body.Content(specSchemaLabelled)
|
|
|
|
spec := make(hcldec.ObjectSpec)
|
|
for _, block := range content.Blocks {
|
|
propSpec, propDiags := decodeSpecBlock(block)
|
|
diags = append(diags, propDiags...)
|
|
spec[block.Labels[0]] = propSpec
|
|
}
|
|
|
|
return spec, diags
|
|
}
|
|
|
|
func decodeArraySpec(body hcl.Body) (hcldec.Spec, hcl.Diagnostics) {
|
|
content, diags := body.Content(specSchemaUnlabelled)
|
|
|
|
spec := make(hcldec.TupleSpec, 0, len(content.Blocks))
|
|
for _, block := range content.Blocks {
|
|
elemSpec, elemDiags := decodeSpecBlock(block)
|
|
diags = append(diags, elemDiags...)
|
|
spec = append(spec, elemSpec)
|
|
}
|
|
|
|
return spec, diags
|
|
}
|
|
|
|
func decodeAttrSpec(body hcl.Body, impliedName string) (hcldec.Spec, hcl.Diagnostics) {
|
|
type content struct {
|
|
Name *string `hcl:"name"`
|
|
Type hcl.Expression `hcl:"type"`
|
|
Required *bool `hcl:"required"`
|
|
}
|
|
|
|
var args content
|
|
diags := gohcl.DecodeBody(body, nil, &args)
|
|
if diags.HasErrors() {
|
|
return errSpec, diags
|
|
}
|
|
|
|
spec := &hcldec.AttrSpec{
|
|
Name: impliedName,
|
|
}
|
|
|
|
if args.Required != nil {
|
|
spec.Required = *args.Required
|
|
}
|
|
if args.Name != nil {
|
|
spec.Name = *args.Name
|
|
}
|
|
|
|
var typeDiags hcl.Diagnostics
|
|
spec.Type, typeDiags = evalTypeExpr(args.Type)
|
|
diags = append(diags, typeDiags...)
|
|
|
|
if spec.Name == "" {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Missing name in attribute spec",
|
|
Detail: "The name attribute is required, to specify the attribute name that is expected in an input HCL file.",
|
|
Subject: body.MissingItemRange().Ptr(),
|
|
})
|
|
return errSpec, diags
|
|
}
|
|
|
|
return spec, diags
|
|
}
|
|
|
|
func decodeBlockSpec(body hcl.Body, impliedName string) (hcldec.Spec, hcl.Diagnostics) {
|
|
type content struct {
|
|
TypeName *string `hcl:"block_type"`
|
|
Required *bool `hcl:"required"`
|
|
Nested hcl.Body `hcl:",remain"`
|
|
}
|
|
|
|
var args content
|
|
diags := gohcl.DecodeBody(body, nil, &args)
|
|
if diags.HasErrors() {
|
|
return errSpec, diags
|
|
}
|
|
|
|
spec := &hcldec.BlockSpec{
|
|
TypeName: impliedName,
|
|
}
|
|
|
|
if args.Required != nil {
|
|
spec.Required = *args.Required
|
|
}
|
|
if args.TypeName != nil {
|
|
spec.TypeName = *args.TypeName
|
|
}
|
|
|
|
nested, nestedDiags := decodeBlockNestedSpec(args.Nested)
|
|
diags = append(diags, nestedDiags...)
|
|
spec.Nested = nested
|
|
|
|
return spec, diags
|
|
}
|
|
|
|
func decodeBlockListSpec(body hcl.Body, impliedName string) (hcldec.Spec, hcl.Diagnostics) {
|
|
type content struct {
|
|
TypeName *string `hcl:"block_type"`
|
|
MinItems *int `hcl:"min_items"`
|
|
MaxItems *int `hcl:"max_items"`
|
|
Nested hcl.Body `hcl:",remain"`
|
|
}
|
|
|
|
var args content
|
|
diags := gohcl.DecodeBody(body, nil, &args)
|
|
if diags.HasErrors() {
|
|
return errSpec, diags
|
|
}
|
|
|
|
spec := &hcldec.BlockListSpec{
|
|
TypeName: impliedName,
|
|
}
|
|
|
|
if args.MinItems != nil {
|
|
spec.MinItems = *args.MinItems
|
|
}
|
|
if args.MaxItems != nil {
|
|
spec.MaxItems = *args.MaxItems
|
|
}
|
|
if args.TypeName != nil {
|
|
spec.TypeName = *args.TypeName
|
|
}
|
|
|
|
nested, nestedDiags := decodeBlockNestedSpec(args.Nested)
|
|
diags = append(diags, nestedDiags...)
|
|
spec.Nested = nested
|
|
|
|
if spec.TypeName == "" {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Missing block_type in block_list 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 decodeBlockSetSpec(body hcl.Body, impliedName string) (hcldec.Spec, hcl.Diagnostics) {
|
|
type content struct {
|
|
TypeName *string `hcl:"block_type"`
|
|
MinItems *int `hcl:"min_items"`
|
|
MaxItems *int `hcl:"max_items"`
|
|
Nested hcl.Body `hcl:",remain"`
|
|
}
|
|
|
|
var args content
|
|
diags := gohcl.DecodeBody(body, nil, &args)
|
|
if diags.HasErrors() {
|
|
return errSpec, diags
|
|
}
|
|
|
|
spec := &hcldec.BlockSetSpec{
|
|
TypeName: impliedName,
|
|
}
|
|
|
|
if args.MinItems != nil {
|
|
spec.MinItems = *args.MinItems
|
|
}
|
|
if args.MaxItems != nil {
|
|
spec.MaxItems = *args.MaxItems
|
|
}
|
|
if args.TypeName != nil {
|
|
spec.TypeName = *args.TypeName
|
|
}
|
|
|
|
nested, nestedDiags := decodeBlockNestedSpec(args.Nested)
|
|
diags = append(diags, nestedDiags...)
|
|
spec.Nested = nested
|
|
|
|
if spec.TypeName == "" {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Missing block_type in block_set 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 decodeBlockMapSpec(body hcl.Body, impliedName string) (hcldec.Spec, hcl.Diagnostics) {
|
|
type content struct {
|
|
TypeName *string `hcl:"block_type"`
|
|
Labels []string `hcl:"labels"`
|
|
Nested hcl.Body `hcl:",remain"`
|
|
}
|
|
|
|
var args content
|
|
diags := gohcl.DecodeBody(body, nil, &args)
|
|
if diags.HasErrors() {
|
|
return errSpec, diags
|
|
}
|
|
|
|
spec := &hcldec.BlockMapSpec{
|
|
TypeName: impliedName,
|
|
}
|
|
|
|
if args.TypeName != nil {
|
|
spec.TypeName = *args.TypeName
|
|
}
|
|
spec.LabelNames = args.Labels
|
|
|
|
nested, nestedDiags := decodeBlockNestedSpec(args.Nested)
|
|
diags = append(diags, nestedDiags...)
|
|
spec.Nested = nested
|
|
|
|
if spec.TypeName == "" {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Missing block_type in block_map 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
|
|
}
|
|
if len(spec.LabelNames) < 1 {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Invalid block label name list",
|
|
Detail: "A block_map must have at least one label specified.",
|
|
Subject: body.MissingItemRange().Ptr(),
|
|
})
|
|
return errSpec, diags
|
|
}
|
|
|
|
return spec, diags
|
|
}
|
|
|
|
func decodeBlockNestedSpec(body hcl.Body) (hcldec.Spec, hcl.Diagnostics) {
|
|
content, diags := body.Content(specSchemaUnlabelled)
|
|
|
|
if len(content.Blocks) == 0 {
|
|
if diags.HasErrors() {
|
|
// If we already have errors then they probably explain
|
|
// why we have no blocks, so we'll skip our additional
|
|
// error message added below.
|
|
return errSpec, diags
|
|
}
|
|
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Missing spec block",
|
|
Detail: "A block spec must have exactly one child spec specifying how to decode block contents.",
|
|
Subject: body.MissingItemRange().Ptr(),
|
|
})
|
|
return errSpec, diags
|
|
}
|
|
|
|
if len(content.Blocks) > 1 {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Extraneous spec block",
|
|
Detail: "A block spec must have exactly one child spec specifying how to decode block contents.",
|
|
Subject: &content.Blocks[1].DefRange,
|
|
})
|
|
return errSpec, diags
|
|
}
|
|
|
|
spec, specDiags := decodeSpecBlock(content.Blocks[0])
|
|
diags = append(diags, specDiags...)
|
|
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"`
|
|
}
|
|
|
|
var args content
|
|
diags := gohcl.DecodeBody(body, specCtx, &args)
|
|
if diags.HasErrors() {
|
|
return errSpec, diags
|
|
}
|
|
|
|
return &hcldec.LiteralSpec{
|
|
Value: args.Value,
|
|
}, diags
|
|
}
|
|
|
|
func decodeDefaultSpec(body hcl.Body) (hcldec.Spec, hcl.Diagnostics) {
|
|
content, diags := body.Content(specSchemaUnlabelled)
|
|
|
|
if len(content.Blocks) == 0 {
|
|
if diags.HasErrors() {
|
|
// If we already have errors then they probably explain
|
|
// why we have no blocks, so we'll skip our additional
|
|
// error message added below.
|
|
return errSpec, diags
|
|
}
|
|
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Missing spec block",
|
|
Detail: "A default block must have at least one nested spec, each specifying a possible outcome.",
|
|
Subject: body.MissingItemRange().Ptr(),
|
|
})
|
|
return errSpec, diags
|
|
}
|
|
|
|
if len(content.Blocks) == 1 && !diags.HasErrors() {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagWarning,
|
|
Summary: "Useless default block",
|
|
Detail: "A default block with only one spec is equivalent to using that spec alone.",
|
|
Subject: &content.Blocks[1].DefRange,
|
|
})
|
|
}
|
|
|
|
var spec hcldec.Spec
|
|
for _, block := range content.Blocks {
|
|
candidateSpec, candidateDiags := decodeSpecBlock(block)
|
|
diags = append(diags, candidateDiags...)
|
|
if candidateDiags.HasErrors() {
|
|
continue
|
|
}
|
|
|
|
if spec == nil {
|
|
spec = candidateSpec
|
|
} else {
|
|
spec = &hcldec.DefaultSpec{
|
|
Primary: spec,
|
|
Default: candidateSpec,
|
|
}
|
|
}
|
|
}
|
|
|
|
return spec, diags
|
|
}
|
|
|
|
func decodeTransformSpec(body hcl.Body) (hcldec.Spec, hcl.Diagnostics) {
|
|
type content struct {
|
|
Result hcl.Expression `hcl:"result"`
|
|
Nested hcl.Body `hcl:",remain"`
|
|
}
|
|
|
|
var args content
|
|
diags := gohcl.DecodeBody(body, nil, &args)
|
|
if diags.HasErrors() {
|
|
return errSpec, diags
|
|
}
|
|
|
|
spec := &hcldec.TransformExprSpec{
|
|
Expr: args.Result,
|
|
VarName: "nested",
|
|
TransformCtx: specCtx,
|
|
}
|
|
|
|
nestedContent, nestedDiags := args.Nested.Content(specSchemaUnlabelled)
|
|
diags = append(diags, nestedDiags...)
|
|
|
|
if len(nestedContent.Blocks) != 1 {
|
|
if nestedDiags.HasErrors() {
|
|
// If we already have errors then they probably explain
|
|
// why we have the wrong number of blocks, so we'll skip our
|
|
// additional error message added below.
|
|
return errSpec, diags
|
|
}
|
|
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Invalid transform spec",
|
|
Detail: "A transform spec block must have exactly one nested spec block.",
|
|
Subject: body.MissingItemRange().Ptr(),
|
|
})
|
|
return errSpec, diags
|
|
}
|
|
|
|
nestedSpec, nestedDiags := decodeSpecBlock(nestedContent.Blocks[0])
|
|
diags = append(diags, nestedDiags...)
|
|
spec.Wrapped = nestedSpec
|
|
|
|
return spec, diags
|
|
}
|
|
|
|
var errSpec = &hcldec.LiteralSpec{
|
|
Value: cty.NullVal(cty.DynamicPseudoType),
|
|
}
|
|
|
|
var specBlockTypes = []string{
|
|
"object",
|
|
"array",
|
|
|
|
"literal",
|
|
|
|
"attr",
|
|
|
|
"block",
|
|
"block_list",
|
|
"block_map",
|
|
"block_set",
|
|
|
|
"default",
|
|
"transform",
|
|
}
|
|
|
|
var specSchemaUnlabelled *hcl.BodySchema
|
|
var specSchemaLabelled *hcl.BodySchema
|
|
|
|
var specSchemaLabelledLabels = []string{"key"}
|
|
|
|
func init() {
|
|
specSchemaLabelled = &hcl.BodySchema{
|
|
Blocks: make([]hcl.BlockHeaderSchema, 0, len(specBlockTypes)),
|
|
}
|
|
specSchemaUnlabelled = &hcl.BodySchema{
|
|
Blocks: make([]hcl.BlockHeaderSchema, 0, len(specBlockTypes)),
|
|
}
|
|
|
|
for _, name := range specBlockTypes {
|
|
specSchemaLabelled.Blocks = append(
|
|
specSchemaLabelled.Blocks,
|
|
hcl.BlockHeaderSchema{
|
|
Type: name,
|
|
LabelNames: specSchemaLabelledLabels,
|
|
},
|
|
)
|
|
specSchemaUnlabelled.Blocks = append(
|
|
specSchemaUnlabelled.Blocks,
|
|
hcl.BlockHeaderSchema{
|
|
Type: name,
|
|
},
|
|
)
|
|
}
|
|
}
|