hclhil: Parsing and JustAttributes for HCL
This commit is contained in:
parent
c5df265cd0
commit
764e4c465b
29
zcl/hclhil/parser.go
Normal file
29
zcl/hclhil/parser.go
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
package hclhil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/apparentlymart/go-zcl/zcl"
|
||||||
|
"github.com/hashicorp/hcl"
|
||||||
|
hclast "github.com/hashicorp/hcl/hcl/ast"
|
||||||
|
)
|
||||||
|
|
||||||
|
func parse(src []byte, filename string) (*zcl.File, zcl.Diagnostics) {
|
||||||
|
hclFile, err := hcl.ParseBytes(src)
|
||||||
|
if err != nil {
|
||||||
|
return nil, zcl.Diagnostics{
|
||||||
|
&zcl.Diagnostic{
|
||||||
|
Severity: zcl.DiagError,
|
||||||
|
Summary: "Syntax error in configuration",
|
||||||
|
Detail: fmt.Sprintf("The file %q could not be parsed: %s", filename, err),
|
||||||
|
Subject: errorRange(err),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &zcl.File{
|
||||||
|
Body: &body{
|
||||||
|
oli: hclFile.Node.(*hclast.ObjectList),
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
51
zcl/hclhil/public.go
Normal file
51
zcl/hclhil/public.go
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
package hclhil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/apparentlymart/go-zcl/zcl"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Parse attempts to parse the given buffer as HCL with HIL expressions and,
|
||||||
|
// if successful, returns a zcl.File for the zcl configuration represented by
|
||||||
|
// it.
|
||||||
|
//
|
||||||
|
// The returned file is valid only if the returned diagnostics returns false
|
||||||
|
// from its HasErrors method. If HasErrors returns true, the file represents
|
||||||
|
// the subset of data that was able to be parsed, which may be none.
|
||||||
|
func Parse(src []byte, filename string) (*zcl.File, zcl.Diagnostics) {
|
||||||
|
return parse(src, filename)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseFile is a convenience wrapper around Parse that first attempts to load
|
||||||
|
// data from the given filename, passing the result to Parse if successful.
|
||||||
|
//
|
||||||
|
// If the file cannot be read, an error diagnostic with nil context is returned.
|
||||||
|
func ParseFile(filename string) (*zcl.File, zcl.Diagnostics) {
|
||||||
|
f, err := os.Open(filename)
|
||||||
|
if err != nil {
|
||||||
|
return nil, zcl.Diagnostics{
|
||||||
|
{
|
||||||
|
Severity: zcl.DiagError,
|
||||||
|
Summary: "Failed to open file",
|
||||||
|
Detail: fmt.Sprintf("The file %q could not be opened.", filename),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
src, err := ioutil.ReadAll(f)
|
||||||
|
if err != nil {
|
||||||
|
return nil, zcl.Diagnostics{
|
||||||
|
{
|
||||||
|
Severity: zcl.DiagError,
|
||||||
|
Summary: "Failed to read file",
|
||||||
|
Detail: fmt.Sprintf("The file %q was opened, but an error occured while reading it.", filename),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Parse(src, filename)
|
||||||
|
}
|
39
zcl/hclhil/shim.go
Normal file
39
zcl/hclhil/shim.go
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
package hclhil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/apparentlymart/go-zcl/zcl"
|
||||||
|
hclparser "github.com/hashicorp/hcl/hcl/parser"
|
||||||
|
hcltoken "github.com/hashicorp/hcl/hcl/token"
|
||||||
|
)
|
||||||
|
|
||||||
|
// errorRange attempts to extract a source range from the given error,
|
||||||
|
// returning a pointer to the range if possible or nil if not.
|
||||||
|
//
|
||||||
|
// errorRange understands HCL's "PosError" type, which wraps an error
|
||||||
|
// with a source position.
|
||||||
|
func errorRange(err error) *zcl.Range {
|
||||||
|
if perr, ok := err.(*hclparser.PosError); ok {
|
||||||
|
rng := rangeFromHCLPos(perr.Pos)
|
||||||
|
return &rng
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func rangeFromHCLPos(pos hcltoken.Pos) zcl.Range {
|
||||||
|
// HCL only marks single positions rather than ranges, so we adapt this
|
||||||
|
// by creating a single-character range at the given position.
|
||||||
|
return zcl.Range{
|
||||||
|
Filename: pos.Filename,
|
||||||
|
Start: zcl.Pos{
|
||||||
|
Byte: pos.Offset,
|
||||||
|
Line: pos.Line,
|
||||||
|
Column: pos.Column,
|
||||||
|
},
|
||||||
|
End: zcl.Pos{
|
||||||
|
Byte: pos.Offset + 1,
|
||||||
|
Line: pos.Line,
|
||||||
|
Column: pos.Column + 1,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
106
zcl/hclhil/structure.go
Normal file
106
zcl/hclhil/structure.go
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
package hclhil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/apparentlymart/go-cty/cty"
|
||||||
|
"github.com/apparentlymart/go-zcl/zcl"
|
||||||
|
hclast "github.com/hashicorp/hcl/hcl/ast"
|
||||||
|
)
|
||||||
|
|
||||||
|
// body is our implementation of zcl.Body in terms of an HCL ObjectList
|
||||||
|
type body struct {
|
||||||
|
oli *hclast.ObjectList
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *body) Content(schema *zcl.BodySchema) (*zcl.BodyContent, zcl.Diagnostics) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *body) PartialContent(schema *zcl.BodySchema) (*zcl.BodyContent, zcl.Body, zcl.Diagnostics) {
|
||||||
|
return nil, nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *body) JustAttributes() (zcl.Attributes, zcl.Diagnostics) {
|
||||||
|
items := b.oli.Items
|
||||||
|
attrs := make(zcl.Attributes)
|
||||||
|
var diags zcl.Diagnostics
|
||||||
|
|
||||||
|
for _, item := range items {
|
||||||
|
if len(item.Keys) == 0 {
|
||||||
|
// Should never happen, since we don't use b.oli.Filter
|
||||||
|
diags = append(diags, &zcl.Diagnostic{
|
||||||
|
Severity: zcl.DiagError,
|
||||||
|
Summary: "Invalid item",
|
||||||
|
Detail: "Somehow we have an HCL item with no keys. This should never happen.",
|
||||||
|
Context: rangeFromHCLPos(item.Pos()).Ptr(),
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if len(item.Keys) > 1 {
|
||||||
|
name := item.Keys[0].Token.Value().(string)
|
||||||
|
diags = append(diags, &zcl.Diagnostic{
|
||||||
|
Severity: zcl.DiagError,
|
||||||
|
Summary: fmt.Sprintf("Unexpected %s block", name),
|
||||||
|
Detail: "Blocks are not allowed here.",
|
||||||
|
Context: rangeFromHCLPos(item.Pos()).Ptr(),
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
name := item.Keys[0].Token.Value().(string)
|
||||||
|
|
||||||
|
if item.Assign.Line == 0 {
|
||||||
|
diags = append(diags, &zcl.Diagnostic{
|
||||||
|
Severity: zcl.DiagWarning,
|
||||||
|
Summary: "Block syntax used for attribute",
|
||||||
|
Detail: fmt.Sprintf("Attribute %q is defined using block syntax, which is deprecated. Use an equals sign after the attribute name instead.", name),
|
||||||
|
Context: rangeFromHCLPos(item.Pos()).Ptr(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if attrs[name] != nil {
|
||||||
|
diags = append(diags, &zcl.Diagnostic{
|
||||||
|
Severity: zcl.DiagError,
|
||||||
|
Summary: "Duplicate attribute definition",
|
||||||
|
Detail: fmt.Sprintf(
|
||||||
|
"Attribute %q was previously defined at %s",
|
||||||
|
name, attrs[name].NameRange.String(),
|
||||||
|
),
|
||||||
|
Context: rangeFromHCLPos(item.Pos()).Ptr(),
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
attrs[name] = &zcl.Attribute{
|
||||||
|
Name: name,
|
||||||
|
Expr: &expression{src: item.Val},
|
||||||
|
Range: rangeFromHCLPos(item.Pos()),
|
||||||
|
NameRange: rangeFromHCLPos(item.Keys[0].Pos()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return attrs, diags
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *body) MissingItemRange() zcl.Range {
|
||||||
|
return rangeFromHCLPos(b.oli.Pos())
|
||||||
|
}
|
||||||
|
|
||||||
|
// body is our implementation of zcl.Body in terms of an HCL node, which may
|
||||||
|
// internally have strings to be interpreted as HIL templates.
|
||||||
|
type expression struct {
|
||||||
|
src hclast.Node
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *expression) Value(ctx *zcl.EvalContext) (cty.Value, zcl.Diagnostics) {
|
||||||
|
// TODO: Implement
|
||||||
|
return cty.NilVal, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *expression) Range() zcl.Range {
|
||||||
|
return rangeFromHCLPos(e.src.Pos())
|
||||||
|
}
|
||||||
|
func (e *expression) StartRange() zcl.Range {
|
||||||
|
return rangeFromHCLPos(e.src.Pos())
|
||||||
|
}
|
179
zcl/hclhil/structure_test.go
Normal file
179
zcl/hclhil/structure_test.go
Normal file
@ -0,0 +1,179 @@
|
|||||||
|
package hclhil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/apparentlymart/go-zcl/zcl"
|
||||||
|
"github.com/davecgh/go-spew/spew"
|
||||||
|
hclast "github.com/hashicorp/hcl/hcl/ast"
|
||||||
|
hcltoken "github.com/hashicorp/hcl/hcl/token"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBodyJustAttributes(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
Source string
|
||||||
|
Want zcl.Attributes
|
||||||
|
DiagCount int
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
``,
|
||||||
|
zcl.Attributes{},
|
||||||
|
0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
`foo = "a"`,
|
||||||
|
zcl.Attributes{
|
||||||
|
"foo": &zcl.Attribute{
|
||||||
|
Name: "foo",
|
||||||
|
Expr: &expression{
|
||||||
|
src: &hclast.LiteralType{
|
||||||
|
Token: hcltoken.Token{
|
||||||
|
Type: hcltoken.STRING,
|
||||||
|
Pos: hcltoken.Pos{
|
||||||
|
Offset: 6,
|
||||||
|
Line: 1,
|
||||||
|
Column: 7,
|
||||||
|
},
|
||||||
|
Text: `"a"`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Range: zcl.Range{
|
||||||
|
Start: zcl.Pos{Byte: 0, Line: 1, Column: 1},
|
||||||
|
End: zcl.Pos{Byte: 1, Line: 1, Column: 2},
|
||||||
|
},
|
||||||
|
NameRange: zcl.Range{
|
||||||
|
Start: zcl.Pos{Byte: 0, Line: 1, Column: 1},
|
||||||
|
End: zcl.Pos{Byte: 1, Line: 1, Column: 2},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
`foo = {}`,
|
||||||
|
zcl.Attributes{
|
||||||
|
"foo": &zcl.Attribute{
|
||||||
|
Name: "foo",
|
||||||
|
Expr: &expression{
|
||||||
|
src: &hclast.ObjectType{
|
||||||
|
List: &hclast.ObjectList{},
|
||||||
|
Lbrace: hcltoken.Pos{
|
||||||
|
Offset: 6,
|
||||||
|
Line: 1,
|
||||||
|
Column: 7,
|
||||||
|
},
|
||||||
|
Rbrace: hcltoken.Pos{
|
||||||
|
Offset: 7,
|
||||||
|
Line: 1,
|
||||||
|
Column: 8,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Range: zcl.Range{
|
||||||
|
Start: zcl.Pos{Byte: 0, Line: 1, Column: 1},
|
||||||
|
End: zcl.Pos{Byte: 1, Line: 1, Column: 2},
|
||||||
|
},
|
||||||
|
NameRange: zcl.Range{
|
||||||
|
Start: zcl.Pos{Byte: 0, Line: 1, Column: 1},
|
||||||
|
End: zcl.Pos{Byte: 1, Line: 1, Column: 2},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
`foo {}`,
|
||||||
|
zcl.Attributes{
|
||||||
|
"foo": &zcl.Attribute{
|
||||||
|
Name: "foo",
|
||||||
|
Expr: &expression{
|
||||||
|
src: &hclast.ObjectType{
|
||||||
|
List: &hclast.ObjectList{},
|
||||||
|
Lbrace: hcltoken.Pos{
|
||||||
|
Offset: 4,
|
||||||
|
Line: 1,
|
||||||
|
Column: 5,
|
||||||
|
},
|
||||||
|
Rbrace: hcltoken.Pos{
|
||||||
|
Offset: 5,
|
||||||
|
Line: 1,
|
||||||
|
Column: 6,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Range: zcl.Range{
|
||||||
|
Start: zcl.Pos{Byte: 0, Line: 1, Column: 1},
|
||||||
|
End: zcl.Pos{Byte: 1, Line: 1, Column: 2},
|
||||||
|
},
|
||||||
|
NameRange: zcl.Range{
|
||||||
|
Start: zcl.Pos{Byte: 0, Line: 1, Column: 1},
|
||||||
|
End: zcl.Pos{Byte: 1, Line: 1, Column: 2},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
1, // warning about using block syntax
|
||||||
|
},
|
||||||
|
{
|
||||||
|
`foo "bar" {}`,
|
||||||
|
zcl.Attributes{},
|
||||||
|
1, // blocks are not allowed here
|
||||||
|
},
|
||||||
|
{
|
||||||
|
`
|
||||||
|
foo = 1
|
||||||
|
foo = 2
|
||||||
|
`,
|
||||||
|
zcl.Attributes{
|
||||||
|
"foo": &zcl.Attribute{
|
||||||
|
Name: "foo",
|
||||||
|
Expr: &expression{
|
||||||
|
src: &hclast.LiteralType{
|
||||||
|
Token: hcltoken.Token{
|
||||||
|
Type: hcltoken.NUMBER,
|
||||||
|
Pos: hcltoken.Pos{
|
||||||
|
Offset: 14,
|
||||||
|
Line: 2,
|
||||||
|
Column: 14,
|
||||||
|
},
|
||||||
|
Text: `1`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Range: zcl.Range{
|
||||||
|
Start: zcl.Pos{Byte: 8, Line: 2, Column: 8},
|
||||||
|
End: zcl.Pos{Byte: 9, Line: 2, Column: 9},
|
||||||
|
},
|
||||||
|
NameRange: zcl.Range{
|
||||||
|
Start: zcl.Pos{Byte: 8, Line: 2, Column: 8},
|
||||||
|
End: zcl.Pos{Byte: 9, Line: 2, Column: 9},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
1, // duplicate definition of foo
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, test := range tests {
|
||||||
|
t.Run(fmt.Sprintf("%02d", i), func(t *testing.T) {
|
||||||
|
file, diags := Parse([]byte(test.Source), "test.hcl")
|
||||||
|
if len(diags) != 0 {
|
||||||
|
t.Fatalf("diagnostics from parse: %s", diags.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
got, diags := file.Body.JustAttributes()
|
||||||
|
if len(diags) != test.DiagCount {
|
||||||
|
t.Errorf("wrong number of diagnostics %d; want %d", len(diags), test.DiagCount)
|
||||||
|
for _, diag := range diags {
|
||||||
|
t.Logf(" - %s", diag.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(got, test.Want) {
|
||||||
|
t.Errorf("wrong result\ngot: %s\nwant: %s", spew.Sdump(got), spew.Sdump(test.Want))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -2,6 +2,7 @@ package parser
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/apparentlymart/go-zcl/zcl"
|
"github.com/apparentlymart/go-zcl/zcl"
|
||||||
|
"github.com/apparentlymart/go-zcl/zcl/hclhil"
|
||||||
"github.com/apparentlymart/go-zcl/zcl/json"
|
"github.com/apparentlymart/go-zcl/zcl/json"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -54,14 +55,26 @@ func (p *Parser) ParseJSONFile(filename string) (*zcl.File, zcl.Diagnostics) {
|
|||||||
// This HCL/HIL parser is a compatibility interface to ease migration for
|
// This HCL/HIL parser is a compatibility interface to ease migration for
|
||||||
// apps that previously used HCL and HIL directly.
|
// apps that previously used HCL and HIL directly.
|
||||||
func (p *Parser) ParseHCLHIL(src []byte, filename string) (*zcl.File, zcl.Diagnostics) {
|
func (p *Parser) ParseHCLHIL(src []byte, filename string) (*zcl.File, zcl.Diagnostics) {
|
||||||
return nil, nil
|
if existing := p.files[filename]; existing != nil {
|
||||||
|
return existing, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
file, diags := hclhil.Parse(src, filename)
|
||||||
|
p.files[filename] = file
|
||||||
|
return file, diags
|
||||||
}
|
}
|
||||||
|
|
||||||
// ParseHCLHILFile reads the given filename and parses it as HCL/HIL, similarly
|
// ParseHCLHILFile reads the given filename and parses it as HCL/HIL, similarly
|
||||||
// to ParseHCLHIL. An error diagnostic is returned if the given file cannot be
|
// to ParseHCLHIL. An error diagnostic is returned if the given file cannot be
|
||||||
// read.
|
// read.
|
||||||
func (p *Parser) ParseHCLHILFile(filename string) (*zcl.File, zcl.Diagnostics) {
|
func (p *Parser) ParseHCLHILFile(filename string) (*zcl.File, zcl.Diagnostics) {
|
||||||
return nil, nil
|
if existing := p.files[filename]; existing != nil {
|
||||||
|
return existing, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
file, diags := hclhil.ParseFile(filename)
|
||||||
|
p.files[filename] = file
|
||||||
|
return file, diags
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddFile allows a caller to record in a parser a file that was parsed some
|
// AddFile allows a caller to record in a parser a file that was parsed some
|
||||||
|
Loading…
x
Reference in New Issue
Block a user