package main import ( "fmt" "github.com/hashicorp/hcl/v2/ext/userfunc" "github.com/hashicorp/hcl/v2/gohcl" "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/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 } if hcldec.ImpliedType(spec).HasDynamicTypes() { diags = append(diags, &hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Invalid block_map spec", Detail: "A block_map spec may not contain attributes with type 'any'.", Subject: body.MissingItemRange().Ptr(), }) } 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, }, ) } }