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 (
|
||||
"github.com/apparentlymart/go-zcl/zcl"
|
||||
"github.com/apparentlymart/go-zcl/zcl/hclhil"
|
||||
"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
|
||||
// apps that previously used HCL and HIL directly.
|
||||
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
|
||||
// to ParseHCLHIL. An error diagnostic is returned if the given file cannot be
|
||||
// read.
|
||||
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
|
||||
|
Loading…
Reference in New Issue
Block a user