diff --git a/decoder_test.go b/decoder_test.go index 3d1eee1..a53b15b 100644 --- a/decoder_test.go +++ b/decoder_test.go @@ -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", false, diff --git a/hcl/parser/parser.go b/hcl/parser/parser.go index 37a72ac..f46ed4c 100644 --- a/hcl/parser/parser.go +++ b/hcl/parser/parser.go @@ -79,6 +79,13 @@ func (p *Parser) objectList() (*ast.ObjectList, error) { } 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 } @@ -311,15 +318,20 @@ func (p *Parser) listType() (*ast.ListType, error) { needComma := false for { 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{ 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() if err != nil { return nil, err @@ -343,6 +355,18 @@ func (p *Parser) listType() (*ast.ListType, error) { needComma = false 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: // TODO(arslan) should we support? not supported by HCL yet case token.LBRACK: diff --git a/hcl/parser/parser_test.go b/hcl/parser/parser_test.go index cf8416c..ffbb9e1 100644 --- a/hcl/parser/parser_test.go +++ b/hcl/parser/parser_test.go @@ -6,6 +6,7 @@ import ( "path/filepath" "reflect" "runtime" + "strings" "testing" "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) { var literals = []struct { src string @@ -263,6 +316,10 @@ func TestParse(t *testing.T) { "multiple.hcl", false, }, + { + "object_list_comma.hcl", + false, + }, { "structure.hcl", false, @@ -279,10 +336,6 @@ func TestParse(t *testing.T) { "complex.hcl", false, }, - { - "assign_deep.hcl", - true, - }, { "types.hcl", false, diff --git a/hcl/parser/test-fixtures/object_list_comma.hcl b/hcl/parser/test-fixtures/object_list_comma.hcl new file mode 100644 index 0000000..1921ec8 --- /dev/null +++ b/hcl/parser/test-fixtures/object_list_comma.hcl @@ -0,0 +1 @@ +foo = {one = 1, two = 2} diff --git a/hcl/test-fixtures/assign_deep.hcl b/test-fixtures/assign_deep.hcl similarity index 100% rename from hcl/test-fixtures/assign_deep.hcl rename to test-fixtures/assign_deep.hcl diff --git a/test-fixtures/list_of_maps.hcl b/test-fixtures/list_of_maps.hcl new file mode 100644 index 0000000..985a33b --- /dev/null +++ b/test-fixtures/list_of_maps.hcl @@ -0,0 +1,4 @@ +foo = [ + {somekey1 = "someval1"}, + {somekey2 = "someval2", someextrakey = "someextraval"}, +]