5e07d8e1f9
In most applications it's possible to fully evaluate configuration at the beginning and work only with resolved values after that, but in some unusual cases it's necessary to split parsing and decoding between two separate processes connected by a pipe or network connection. hclpack is intended to provide compact wire formats for sending bodies over the network such that they can be decoded and evaluated and get the same results. This is not something that can happen fully automatically because a hcl.Body is an abstract node rather than a physical construct, and so access to the original source code is required to construct such a representation, and to interpret any source ranges that emerged from the final evaluation.
257 lines
6.7 KiB
Go
257 lines
6.7 KiB
Go
package hclpack
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
"github.com/hashicorp/hcl2/hcl"
|
|
)
|
|
|
|
// Body is an implementation of hcl.Body.
|
|
type Body struct {
|
|
Attributes map[string]Attribute
|
|
ChildBlocks []Block
|
|
|
|
MissingItemRange_ hcl.Range
|
|
}
|
|
|
|
var _ hcl.Body = (*Body)(nil)
|
|
|
|
// Content is an implementation of the method of the same name on hcl.Body.
|
|
func (b *Body) Content(schema *hcl.BodySchema) (*hcl.BodyContent, hcl.Diagnostics) {
|
|
return b.content(schema, nil)
|
|
}
|
|
|
|
// PartialContent is an implementation of the method of the same name on hcl.Body.
|
|
//
|
|
// The returned "remain" body may share some backing objects with the receiver,
|
|
// so neither the receiver nor the returned remain body, or any descendent
|
|
// objects within them, may be mutated after this method is used.
|
|
func (b *Body) PartialContent(schema *hcl.BodySchema) (*hcl.BodyContent, hcl.Body, hcl.Diagnostics) {
|
|
remain := &Body{}
|
|
content, diags := b.content(schema, remain)
|
|
return content, remain, diags
|
|
}
|
|
|
|
func (b *Body) content(schema *hcl.BodySchema, remain *Body) (*hcl.BodyContent, hcl.Diagnostics) {
|
|
if b == nil {
|
|
b = &Body{} // We'll treat a nil body like an empty one, for convenience
|
|
}
|
|
var diags hcl.Diagnostics
|
|
|
|
var attrs map[string]*hcl.Attribute
|
|
var attrUsed map[string]struct{}
|
|
if len(b.Attributes) > 0 {
|
|
attrs = make(map[string]*hcl.Attribute, len(b.Attributes))
|
|
attrUsed = make(map[string]struct{}, len(b.Attributes))
|
|
}
|
|
for _, attrS := range schema.Attributes {
|
|
name := attrS.Name
|
|
attr, exists := b.Attributes[name]
|
|
if !exists {
|
|
if attrS.Required {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Missing required argument",
|
|
Detail: fmt.Sprintf("The argument %q is required, but no definition was found.", attrS.Name),
|
|
Subject: &b.MissingItemRange_,
|
|
})
|
|
}
|
|
continue
|
|
}
|
|
|
|
attrs[name] = attr.asHCLAttribute(name)
|
|
attrUsed[name] = struct{}{}
|
|
}
|
|
|
|
for name, attr := range b.Attributes {
|
|
if _, used := attrUsed[name]; used {
|
|
continue
|
|
}
|
|
if remain != nil {
|
|
remain.setAttribute(name, attr)
|
|
continue
|
|
}
|
|
var suggestions []string
|
|
for _, attrS := range schema.Attributes {
|
|
if _, defined := attrs[name]; defined {
|
|
continue
|
|
}
|
|
suggestions = append(suggestions, attrS.Name)
|
|
}
|
|
suggestion := nameSuggestion(name, suggestions)
|
|
if suggestion != "" {
|
|
suggestion = fmt.Sprintf(" Did you mean %q?", suggestion)
|
|
} else {
|
|
// Is there a block of the same name?
|
|
for _, blockS := range schema.Blocks {
|
|
if blockS.Type == name {
|
|
suggestion = fmt.Sprintf(" Did you mean to define a block of type %q?", name)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Unsupported argument",
|
|
Detail: fmt.Sprintf("An argument named %q is not expected here.%s", name, suggestion),
|
|
Subject: &attr.NameRange,
|
|
})
|
|
}
|
|
|
|
blocksWanted := make(map[string]hcl.BlockHeaderSchema)
|
|
for _, blockS := range schema.Blocks {
|
|
blocksWanted[blockS.Type] = blockS
|
|
}
|
|
|
|
var blocks []*hcl.Block
|
|
for _, block := range b.ChildBlocks {
|
|
blockTy := block.Type
|
|
blockS, wanted := blocksWanted[blockTy]
|
|
if !wanted {
|
|
if remain != nil {
|
|
remain.appendBlock(block)
|
|
continue
|
|
}
|
|
var suggestions []string
|
|
for _, blockS := range schema.Blocks {
|
|
suggestions = append(suggestions, blockS.Type)
|
|
}
|
|
suggestion := nameSuggestion(blockTy, suggestions)
|
|
if suggestion != "" {
|
|
suggestion = fmt.Sprintf(" Did you mean %q?", suggestion)
|
|
} else {
|
|
// Is there an attribute of the same name?
|
|
for _, attrS := range schema.Attributes {
|
|
if attrS.Name == blockTy {
|
|
suggestion = fmt.Sprintf(" Did you mean to define argument %q?", blockTy)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Unsupported block type",
|
|
Detail: fmt.Sprintf("Blocks of type %q are not expected here.%s", blockTy, suggestion),
|
|
Subject: &block.TypeRange,
|
|
})
|
|
continue
|
|
}
|
|
|
|
if len(block.Labels) != len(blockS.LabelNames) {
|
|
if len(blockS.LabelNames) == 0 {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: fmt.Sprintf("Extraneous label for %s", blockTy),
|
|
Detail: fmt.Sprintf(
|
|
"No labels are expected for %s blocks.", blockTy,
|
|
),
|
|
Subject: &block.DefRange,
|
|
Context: &block.DefRange,
|
|
})
|
|
} else {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: fmt.Sprintf("Wrong label count for %s", blockTy),
|
|
Detail: fmt.Sprintf(
|
|
"%s blocks expect %d label(s), but got %d.",
|
|
blockTy, len(blockS.LabelNames), len(block.Labels),
|
|
),
|
|
Subject: &block.DefRange,
|
|
Context: &block.DefRange,
|
|
})
|
|
}
|
|
continue
|
|
}
|
|
|
|
blocks = append(blocks, block.asHCLBlock())
|
|
}
|
|
|
|
return &hcl.BodyContent{
|
|
Attributes: attrs,
|
|
Blocks: blocks,
|
|
MissingItemRange: b.MissingItemRange_,
|
|
}, diags
|
|
}
|
|
|
|
// JustAttributes is an implementation of the method of the same name on hcl.Body.
|
|
func (b *Body) JustAttributes() (hcl.Attributes, hcl.Diagnostics) {
|
|
var diags hcl.Diagnostics
|
|
if len(b.ChildBlocks) > 0 {
|
|
for _, block := range b.ChildBlocks {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: fmt.Sprintf("Unexpected %s block", block.Type),
|
|
Detail: "Blocks are not allowed here.",
|
|
Context: &block.TypeRange,
|
|
})
|
|
}
|
|
// We'll continue processing anyway, and return any attributes we find
|
|
// so that the caller can do careful partial analysis.
|
|
}
|
|
|
|
if len(b.Attributes) == 0 {
|
|
return nil, diags
|
|
}
|
|
|
|
ret := make(hcl.Attributes, len(b.Attributes))
|
|
for n, a := range b.Attributes {
|
|
ret[n] = a.asHCLAttribute(n)
|
|
}
|
|
return ret, diags
|
|
}
|
|
|
|
// MissingItemRange is an implementation of the method of the same name on hcl.Body.
|
|
func (b *Body) MissingItemRange() hcl.Range {
|
|
return b.MissingItemRange_
|
|
}
|
|
|
|
func (b *Body) setAttribute(name string, attr Attribute) {
|
|
if b.Attributes == nil {
|
|
b.Attributes = make(map[string]Attribute)
|
|
}
|
|
b.Attributes[name] = attr
|
|
}
|
|
|
|
func (b *Body) appendBlock(block Block) {
|
|
b.ChildBlocks = append(b.ChildBlocks, block)
|
|
}
|
|
|
|
// Block represents a nested block within a body.
|
|
type Block struct {
|
|
Type string
|
|
Labels []string
|
|
Body Body
|
|
|
|
DefRange, TypeRange hcl.Range
|
|
LabelRanges []hcl.Range
|
|
}
|
|
|
|
func (b *Block) asHCLBlock() *hcl.Block {
|
|
return &hcl.Block{
|
|
Type: b.Type,
|
|
Labels: b.Labels,
|
|
Body: &b.Body,
|
|
|
|
TypeRange: b.TypeRange,
|
|
DefRange: b.DefRange,
|
|
LabelRanges: b.LabelRanges,
|
|
}
|
|
}
|
|
|
|
// Attribute represents an attribute definition within a body.
|
|
type Attribute struct {
|
|
Expr Expression
|
|
|
|
Range, NameRange hcl.Range
|
|
}
|
|
|
|
func (a *Attribute) asHCLAttribute(name string) *hcl.Attribute {
|
|
return &hcl.Attribute{
|
|
Name: name,
|
|
Expr: &a.Expr,
|
|
Range: a.Range,
|
|
NameRange: a.NameRange,
|
|
}
|
|
}
|