Merge pull request #137 from hashicorp/f-list-of-maps

hcl/parser: Support lists of objects
This commit is contained in:
Paul Hinze 2016-07-08 09:13:38 -05:00 committed by GitHub
commit 364df43084
10 changed files with 147 additions and 12 deletions

View File

@ -81,9 +81,20 @@ FOO
* Boolean values: `true`, `false` * Boolean values: `true`, `false`
* Arrays can be made by wrapping it in `[]`. Example: * Arrays can be made by wrapping it in `[]`. Example:
`["foo", "bar", 42]`. Arrays can contain primitives `["foo", "bar", 42]`. Arrays can contain primitives,
and other arrays, but cannot contain objects. Objects must other arrays, and objects. As an alternative, lists
use the block syntax shown below. of objects can be created with repeated blocks, using
this structure:
```hcl
service {
key = "value"
}
service {
key = "value"
}
```
Objects and nested objects are created using the structure shown below: Objects and nested objects are created using the structure shown below:

View File

@ -194,6 +194,27 @@ func TestDecode_interface(t *testing.T) {
}, },
}, },
}, },
{
"list_of_maps.hcl",
false,
map[string]interface{}{
"foo": []interface{}{
map[string]interface{}{"somekey1": "someval1"},
map[string]interface{}{"somekey2": "someval2", "someextrakey": "someextraval"},
},
},
},
{
"assign_deep.hcl",
false,
map[string]interface{}{
"resource": []interface{}{
map[string]interface{}{
"foo": []interface{}{
map[string]interface{}{
"bar": []map[string]interface{}{
map[string]interface{}{}}}}}}},
},
{ {
"structure_list.hcl", "structure_list.hcl",
false, false,

View File

@ -79,6 +79,13 @@ func (p *Parser) objectList() (*ast.ObjectList, error) {
} }
node.Add(n) node.Add(n)
// object lists can be optionally comma-delimited e.g. when a list of maps
// is being expressed, so a comma is allowed here - it's simply consumed
tok := p.scan()
if tok.Type != token.COMMA {
p.unscan()
}
} }
return node, nil return node, nil
} }
@ -311,15 +318,20 @@ func (p *Parser) listType() (*ast.ListType, error) {
needComma := false needComma := false
for { for {
tok := p.scan() tok := p.scan()
switch tok.Type {
case token.NUMBER, token.FLOAT, token.STRING, token.HEREDOC:
if needComma { if needComma {
switch tok.Type {
case token.COMMA, token.RBRACK:
default:
return nil, &PosError{ return nil, &PosError{
Pos: tok.Pos, Pos: tok.Pos,
Err: fmt.Errorf("unexpected token: %s. Expecting %s", tok.Type, token.COMMA), Err: fmt.Errorf(
"error parsing list, expected comma or list end, got: %s",
tok.Type),
} }
} }
}
switch tok.Type {
case token.NUMBER, token.FLOAT, token.STRING, token.HEREDOC:
node, err := p.literalType() node, err := p.literalType()
if err != nil { if err != nil {
return nil, err return nil, err
@ -343,6 +355,18 @@ func (p *Parser) listType() (*ast.ListType, error) {
needComma = false needComma = false
continue continue
case token.LBRACE:
// Looks like a nested object, so parse it out
node, err := p.objectType()
if err != nil {
return nil, &PosError{
Pos: tok.Pos,
Err: fmt.Errorf(
"error while trying to parse object within list: %s", err),
}
}
l.Add(node)
needComma = true
case token.BOOL: case token.BOOL:
// TODO(arslan) should we support? not supported by HCL yet // TODO(arslan) should we support? not supported by HCL yet
case token.LBRACK: case token.LBRACK:

View File

@ -6,6 +6,7 @@ import (
"path/filepath" "path/filepath"
"reflect" "reflect"
"runtime" "runtime"
"strings"
"testing" "testing"
"github.com/hashicorp/hcl/hcl/ast" "github.com/hashicorp/hcl/hcl/ast"
@ -99,6 +100,58 @@ EOF
} }
} }
func TestListOfMaps(t *testing.T) {
src := `foo = [
{key = "bar"},
{key = "baz", key2 = "qux"},
]`
p := newParser([]byte(src))
file, err := p.Parse()
if err != nil {
t.Fatalf("err: %s", err)
}
// Here we make all sorts of assumptions about the input structure w/ type
// assertions. The intent is only for this to be a "smoke test" ensuring
// parsing actually performed its duty - giving this test something a bit
// more robust than _just_ "no error occurred".
expected := []string{`"bar"`, `"baz"`, `"qux"`}
actual := make([]string, 0, 3)
ol := file.Node.(*ast.ObjectList)
objItem := ol.Items[0]
list := objItem.Val.(*ast.ListType)
for _, node := range list.List {
obj := node.(*ast.ObjectType)
for _, item := range obj.List.Items {
val := item.Val.(*ast.LiteralType)
actual = append(actual, val.Token.Text)
}
}
if !reflect.DeepEqual(expected, actual) {
t.Fatalf("Expected: %#v, got %#v", expected, actual)
}
}
func TestListOfMaps_requiresComma(t *testing.T) {
src := `foo = [
{key = "bar"}
{key = "baz"}
]`
p := newParser([]byte(src))
_, err := p.Parse()
if err == nil {
t.Fatalf("Expected error, got none!")
}
expected := "error parsing list, expected comma or list end"
if !strings.Contains(err.Error(), expected) {
t.Fatalf("Expected err:\n %s\nTo contain:\n %s\n", err, expected)
}
}
func TestObjectType(t *testing.T) { func TestObjectType(t *testing.T) {
var literals = []struct { var literals = []struct {
src string src string
@ -263,6 +316,10 @@ func TestParse(t *testing.T) {
"multiple.hcl", "multiple.hcl",
false, false,
}, },
{
"object_list_comma.hcl",
false,
},
{ {
"structure.hcl", "structure.hcl",
false, false,
@ -279,10 +336,6 @@ func TestParse(t *testing.T) {
"complex.hcl", "complex.hcl",
false, false,
}, },
{
"assign_deep.hcl",
true,
},
{ {
"types.hcl", "types.hcl",
false, false,

View File

@ -0,0 +1 @@
foo = {one = 1, two = 2}

View File

@ -35,6 +35,7 @@ var data = []entry{
{"comment_aligned.input", "comment_aligned.golden"}, {"comment_aligned.input", "comment_aligned.golden"},
{"comment_standalone.input", "comment_standalone.golden"}, {"comment_standalone.input", "comment_standalone.golden"},
{"empty_block.input", "empty_block.golden"}, {"empty_block.input", "empty_block.golden"},
{"list_of_objects.input", "list_of_objects.golden"},
} }
func TestFiles(t *testing.T) { func TestFiles(t *testing.T) {

View File

@ -0,0 +1,10 @@
list_of_objects = [
{
key1 = "value1"
key2 = "value2"
},
{
key3 = "value3"
key4 = "value4"
},
]

View File

@ -0,0 +1,10 @@
list_of_objects = [
{
key1 = "value1"
key2 = "value2"
},
{
key3 = "value3"
key4 = "value4"
}
]

View File

@ -0,0 +1,4 @@
foo = [
{somekey1 = "someval1"},
{somekey2 = "someval2", someextrakey = "someextraval"},
]