Start the decoder

This commit is contained in:
Mitchell Hashimoto 2014-08-02 15:44:45 -07:00
parent 3a02b49a51
commit 59e0ca9d5a
10 changed files with 320 additions and 104 deletions

View File

@ -17,6 +17,13 @@ type Node interface {
Accept(Visitor)
}
// KeyedNode is a node that has a key associated with it.
type KeyedNode interface {
Node
Key() string
}
// Visitor is the interface that must be implemented by any
// structures who want to be visited as part of the visitor pattern
// on the AST.
@ -29,14 +36,14 @@ type Visitor interface {
// be validated/removed at a semantic check, rather than at a
// syntax level.
type ObjectNode struct {
Key string
Elem []Node
K string
Elem []KeyedNode
}
// AssignmentNode represents a direct assignment with an equals
// sign.
type AssignmentNode struct {
Key string
K string
Value Node
}
@ -52,24 +59,32 @@ type LiteralNode struct {
}
func (n ObjectNode) Accept(v Visitor) {
v.Visit(n)
for _, e := range n.Elem {
e.Accept(v)
}
}
v.Visit(n)
func (n ObjectNode) Key() string {
return n.K
}
func (n AssignmentNode) Accept(v Visitor) {
n.Value.Accept(v)
v.Visit(n)
n.Value.Accept(v)
}
func (n AssignmentNode) Key() string {
return n.K
}
func (n ListNode) Accept(v Visitor) {
v.Visit(n)
for _, e := range n.Elem {
e.Accept(v)
}
v.Visit(n)
}
func (n LiteralNode) Accept(v Visitor) {

View File

@ -7,13 +7,13 @@ import (
func TestAssignmentNode_accept(t *testing.T) {
n := AssignmentNode{
Key: "foo",
K: "foo",
Value: LiteralNode{Value: "foo"},
}
expected := []Node{
n.Value,
n,
n.Value,
}
v := new(MockVisitor)
@ -33,9 +33,9 @@ func TestListNode_accept(t *testing.T) {
}
expected := []Node{
n,
n.Elem[0],
n.Elem[1],
n,
}
v := new(MockVisitor)
@ -48,17 +48,19 @@ func TestListNode_accept(t *testing.T) {
func TestObjectNode_accept(t *testing.T) {
n := ObjectNode{
Key: "foo",
Elem: []Node{
LiteralNode{Value: "foo"},
LiteralNode{Value: "bar"},
K: "foo",
Elem: []KeyedNode{
AssignmentNode{K: "foo", Value: LiteralNode{Value: "foo"}},
AssignmentNode{K: "bar", Value: LiteralNode{Value: "bar"}},
},
}
expected := []Node{
n.Elem[0],
n.Elem[1],
n,
n.Elem[0],
n.Elem[0].(AssignmentNode).Value,
n.Elem[1],
n.Elem[1].(AssignmentNode).Value,
}
v := new(MockVisitor)

144
decoder.go Normal file
View File

@ -0,0 +1,144 @@
package hcl
import (
"fmt"
"reflect"
"github.com/hashicorp/hcl/ast"
)
// Decode reads the given input and decodes it into the structure
// given by `out`.
func Decode(out interface{}, in string) error {
obj, err := Parse(in)
if err != nil {
return err
}
return DecodeAST(out, obj)
}
// DecodeAST is a lower-level version of Decode. It decodes a
// raw AST into the given output.
func DecodeAST(out interface{}, obj *ast.ObjectNode) error {
return decode("", *obj, reflect.ValueOf(out).Elem())
}
func decode(name string, n ast.Node, result reflect.Value) error {
switch result.Kind() {
case reflect.Interface:
// When we see an interface, we make our own thing
return decodeInterface(name, n, result)
case reflect.Map:
return decodeMap(name, n, result)
case reflect.String:
return decodeString(name, n, result)
default:
return fmt.Errorf("%s: unknown kind: %s", name, result.Kind())
}
return nil
}
func decodeInterface(name string, raw ast.Node, result reflect.Value) error {
var set reflect.Value
switch n := raw.(type) {
case ast.ObjectNode:
result := make(map[string]interface{})
set = reflect.ValueOf(result)
case ast.LiteralNode:
switch n.Type {
case ast.ValueTypeString:
set = reflect.Indirect(reflect.New(reflect.TypeOf("")))
default:
return fmt.Errorf(
"%s: unknown literal type: %s",
name, n.Type)
}
default:
return fmt.Errorf(
"%s: cannot decode into interface: %T",
name, raw)
}
// Revisit the node so that we can use the newly instantiated
// thing and populate it.
if err := decode(name, raw, set); err != nil {
return err
}
// Set the result to what its supposed to be, then reset
// result so we don't reflect into this method anymore.
result.Set(set)
return nil
}
func decodeMap(name string, raw ast.Node, result reflect.Value) error {
obj, ok := raw.(ast.ObjectNode)
if !ok {
return fmt.Errorf("%s: not an object type", name)
}
resultType := result.Type()
resultElemType := resultType.Elem()
resultKeyType := resultType.Key()
if resultKeyType.Kind() != reflect.String {
return fmt.Errorf(
"%s: map must have string keys", name)
}
// Make a map if it is nil
resultMap := result
if result.IsNil() {
resultMap = reflect.MakeMap(
reflect.MapOf(resultKeyType, resultElemType))
}
// Go through each element and decode it.
for _, elem := range obj.Elem {
n := elem.(ast.AssignmentNode)
// Make the field name
fieldName := fmt.Sprintf("%s[%s]", name, n.Key())
// Get the key/value as reflection values
key := reflect.ValueOf(n.Key())
val := reflect.Indirect(reflect.New(resultElemType))
// If we have a pre-existing value in the map, use that
oldVal := resultMap.MapIndex(key)
if oldVal.IsValid() {
val.Set(oldVal)
}
// Decode!
if err := decode(fieldName, n.Value, val); err != nil {
return err
}
// Set the value on the map
resultMap.SetMapIndex(key, val)
}
// Set the final map
result.Set(resultMap)
return nil
}
func decodeString(name string, raw ast.Node, result reflect.Value) error {
n, ok := raw.(ast.LiteralNode)
if !ok {
return fmt.Errorf("%s: not a literal type", name)
}
switch n.Type {
case ast.ValueTypeString:
println(n.Value.(string))
result.SetString(n.Value.(string))
default:
return fmt.Errorf("%s: unknown type %s", name, n.Type)
}
return nil
}

41
decoder_test.go Normal file
View File

@ -0,0 +1,41 @@
package hcl
import (
"io/ioutil"
"path/filepath"
"reflect"
"testing"
)
func TestDecode(t *testing.T) {
cases := []struct {
File string
Err bool
Out interface{}
}{
{
"basic.hcl",
false,
map[string]interface{}{
"foo": "bar",
},
},
}
for _, tc := range cases {
d, err := ioutil.ReadFile(filepath.Join(fixtureDir, tc.File))
if err != nil {
t.Fatalf("err: %s", err)
}
var out map[string]interface{}
err = Decode(&out, string(d))
if (err != nil) != tc.Err {
t.Fatalf("Input: %s\n\nError: %s", tc.File, err)
}
if !reflect.DeepEqual(out, tc.Out) {
t.Fatalf("Input: %s\n\n%#v", tc.File, out)
}
}
}

View File

@ -11,14 +11,18 @@ import (
%union {
list []ast.Node
klist []ast.KeyedNode
kitem ast.KeyedNode
listitem ast.Node
num int
obj ast.ObjectNode
str string
}
%type <list> list objectlist
%type <listitem> listitem objectitem
%type <list> list
%type <klist> objectlist
%type <kitem> objectitem
%type <listitem> listitem
%type <obj> block object
%type <str> blockId
@ -32,7 +36,7 @@ top:
objectlist
{
hclResult = &ast.ObjectNode{
Key: "",
K: "",
Elem: $1,
}
}
@ -40,7 +44,7 @@ top:
objectlist:
objectitem
{
$$ = []ast.Node{$1}
$$ = []ast.KeyedNode{$1}
}
| objectitem objectlist
{
@ -61,7 +65,7 @@ objectitem:
IDENTIFIER EQUAL NUMBER
{
$$ = ast.AssignmentNode{
Key: $1,
K: $1,
Value: ast.LiteralNode{
Type: ast.ValueTypeInt,
Value: $3,
@ -71,7 +75,7 @@ objectitem:
| IDENTIFIER EQUAL STRING
{
$$ = ast.AssignmentNode{
Key: $1,
K: $1,
Value: ast.LiteralNode{
Type: ast.ValueTypeString,
Value: $3,
@ -81,14 +85,14 @@ objectitem:
| IDENTIFIER EQUAL object
{
$$ = ast.AssignmentNode{
Key: $1,
K: $1,
Value: $3,
}
}
| IDENTIFIER EQUAL LEFTBRACKET list RIGHTBRACKET
{
$$ = ast.AssignmentNode{
Key: $1,
K: $1,
Value: ast.ListNode{Elem: $4},
}
}
@ -101,13 +105,13 @@ block:
blockId object
{
$$ = $2
$$.Key = $1
$$.K = $1
}
| blockId block
{
$$ = ast.ObjectNode{
Key: $1,
Elem: []ast.Node{$2},
K: $1,
Elem: []ast.KeyedNode{$2},
}
}

View File

@ -12,6 +12,8 @@ import (
type hclSymType struct {
yys int
list []ast.Node
klist []ast.KeyedNode
kitem ast.KeyedNode
listitem ast.Node
num int
obj ast.ObjectNode
@ -47,7 +49,7 @@ const hclEofCode = 1
const hclErrCode = 2
const hclMaxDepth = 200
//line parse.y:150
//line parse.y:154
//line yacctab:1
var hclExca = []int{
@ -79,12 +81,12 @@ var hclPact = []int{
}
var hclPgo = []int{
0, 29, 4, 0, 28, 25, 12, 26, 5,
0, 29, 4, 28, 0, 25, 12, 26, 5,
}
var hclR1 = []int{
0, 8, 2, 2, 6, 6, 4, 4, 4, 4,
4, 5, 5, 7, 7, 1, 1, 3, 3,
0, 8, 2, 2, 6, 6, 3, 3, 3, 3,
3, 5, 5, 7, 7, 1, 1, 4, 4,
}
var hclR2 = []int{
@ -93,9 +95,9 @@ var hclR2 = []int{
}
var hclChk = []int{
-1000, -8, -2, -4, 6, -5, -7, 9, -2, 7,
-1000, -8, -2, -3, 6, -5, -7, 9, -2, 7,
-6, -5, 10, 6, 4, 9, -6, 12, -2, 11,
-1, -3, 4, 9, 11, 13, 5, -3,
-1, -4, 4, 9, 11, 13, 5, -4,
}
var hclDef = []int{
@ -342,38 +344,38 @@ hcldefault:
switch hclnt {
case 1:
//line parse.y:33
//line parse.y:37
{
hclResult = &ast.ObjectNode{
Key: "",
Elem: hclS[hclpt-0].list,
K: "",
Elem: hclS[hclpt-0].klist,
}
}
case 2:
//line parse.y:42
{
hclVAL.list = []ast.Node{hclS[hclpt-0].listitem}
}
case 3:
//line parse.y:46
{
hclVAL.list = append(hclS[hclpt-0].list, hclS[hclpt-1].listitem)
hclVAL.klist = []ast.KeyedNode{hclS[hclpt-0].kitem}
}
case 3:
//line parse.y:50
{
hclVAL.klist = append(hclS[hclpt-0].klist, hclS[hclpt-1].kitem)
}
case 4:
//line parse.y:52
//line parse.y:56
{
hclVAL.obj = ast.ObjectNode{Elem: hclS[hclpt-1].list}
hclVAL.obj = ast.ObjectNode{Elem: hclS[hclpt-1].klist}
}
case 5:
//line parse.y:56
//line parse.y:60
{
hclVAL.obj = ast.ObjectNode{}
}
case 6:
//line parse.y:62
//line parse.y:66
{
hclVAL.listitem = ast.AssignmentNode{
Key: hclS[hclpt-2].str,
hclVAL.kitem = ast.AssignmentNode{
K: hclS[hclpt-2].str,
Value: ast.LiteralNode{
Type: ast.ValueTypeInt,
Value: hclS[hclpt-0].num,
@ -381,10 +383,10 @@ hcldefault:
}
}
case 7:
//line parse.y:72
//line parse.y:76
{
hclVAL.listitem = ast.AssignmentNode{
Key: hclS[hclpt-2].str,
hclVAL.kitem = ast.AssignmentNode{
K: hclS[hclpt-2].str,
Value: ast.LiteralNode{
Type: ast.ValueTypeString,
Value: hclS[hclpt-0].str,
@ -392,62 +394,62 @@ hcldefault:
}
}
case 8:
//line parse.y:82
//line parse.y:86
{
hclVAL.listitem = ast.AssignmentNode{
Key: hclS[hclpt-2].str,
hclVAL.kitem = ast.AssignmentNode{
K: hclS[hclpt-2].str,
Value: hclS[hclpt-0].obj,
}
}
case 9:
//line parse.y:89
//line parse.y:93
{
hclVAL.listitem = ast.AssignmentNode{
Key: hclS[hclpt-4].str,
hclVAL.kitem = ast.AssignmentNode{
K: hclS[hclpt-4].str,
Value: ast.ListNode{Elem: hclS[hclpt-1].list},
}
}
case 10:
//line parse.y:96
//line parse.y:100
{
hclVAL.listitem = hclS[hclpt-0].obj
hclVAL.kitem = hclS[hclpt-0].obj
}
case 11:
//line parse.y:102
//line parse.y:106
{
hclVAL.obj = hclS[hclpt-0].obj
hclVAL.obj.Key = hclS[hclpt-1].str
hclVAL.obj.K = hclS[hclpt-1].str
}
case 12:
//line parse.y:107
//line parse.y:111
{
hclVAL.obj = ast.ObjectNode{
Key: hclS[hclpt-1].str,
Elem: []ast.Node{hclS[hclpt-0].obj},
K: hclS[hclpt-1].str,
Elem: []ast.KeyedNode{hclS[hclpt-0].obj},
}
}
case 13:
//line parse.y:116
{
hclVAL.str = hclS[hclpt-0].str
}
case 14:
//line parse.y:120
{
hclVAL.str = hclS[hclpt-0].str
}
case 14:
//line parse.y:124
{
hclVAL.str = hclS[hclpt-0].str
}
case 15:
//line parse.y:126
//line parse.y:130
{
hclVAL.list = []ast.Node{hclS[hclpt-0].listitem}
}
case 16:
//line parse.y:130
//line parse.y:134
{
hclVAL.list = append(hclS[hclpt-2].list, hclS[hclpt-0].listitem)
}
case 17:
//line parse.y:136
//line parse.y:140
{
hclVAL.listitem = ast.LiteralNode{
Type: ast.ValueTypeInt,
@ -455,7 +457,7 @@ hcldefault:
}
}
case 18:
//line parse.y:143
//line parse.y:147
{
hclVAL.listitem = ast.LiteralNode{
Type: ast.ValueTypeString,

4
hcl_test.go Normal file
View File

@ -0,0 +1,4 @@
package hcl
// This is the directory where our test fixtures are.
const fixtureDir = "./test-fixtures"

View File

@ -13,6 +13,7 @@ import (
array ast.ListNode
assign ast.AssignmentNode
item ast.Node
klist []ast.KeyedNode
list []ast.Node
num int
str string
@ -22,7 +23,8 @@ import (
%type <array> array
%type <assign> pair
%type <item> value
%type <list> elements members
%type <klist> members
%type <list> elements
%type <obj> object
%token <num> NUMBER
@ -52,7 +54,7 @@ object:
members:
pair
{
$$ = []ast.Node{$1}
$$ = []ast.KeyedNode{$1}
}
| pair COMMA members
{
@ -63,7 +65,7 @@ pair:
STRING COLON value
{
$$ = ast.AssignmentNode{
Key: $1,
K: $1,
Value: $3,
}
}

View File

@ -14,6 +14,7 @@ type jsonSymType struct {
array ast.ListNode
assign ast.AssignmentNode
item ast.Node
klist []ast.KeyedNode
list []ast.Node
num int
str string
@ -57,7 +58,7 @@ const jsonEofCode = 1
const jsonErrCode = 2
const jsonMaxDepth = 200
//line parse.y:136
//line parse.y:138
//line yacctab:1
var jsonExca = []int{
@ -89,12 +90,12 @@ var jsonPact = []int{
}
var jsonPgo = []int{
0, 34, 32, 17, 0, 23, 29, 31,
0, 34, 32, 17, 23, 0, 29, 31,
}
var jsonR1 = []int{
0, 7, 6, 6, 5, 5, 2, 3, 3, 3,
3, 3, 3, 3, 1, 1, 4, 4,
0, 7, 6, 6, 4, 4, 2, 3, 3, 3,
3, 3, 3, 3, 1, 1, 5, 5,
}
var jsonR2 = []int{
@ -103,9 +104,9 @@ var jsonR2 = []int{
}
var jsonChk = []int{
-1000, -7, -6, 11, -5, 12, -2, 10, 12, 6,
5, -5, -3, 10, 4, -6, -1, 15, 16, 17,
13, 14, -4, -3, 14, 6, -4,
-1000, -7, -6, 11, -4, 12, -2, 10, 12, 6,
5, -4, -3, 10, 4, -6, -1, 15, 16, 17,
13, 14, -5, -3, 14, 6, -5,
}
var jsonDef = []int{
@ -352,41 +353,41 @@ jsondefault:
switch jsonnt {
case 1:
//line parse.y:37
//line parse.y:39
{
obj := jsonS[jsonpt-0].obj
jsonResult = &obj
}
case 2:
//line parse.y:44
//line parse.y:46
{
jsonVAL.obj = ast.ObjectNode{Elem: jsonS[jsonpt-1].list}
jsonVAL.obj = ast.ObjectNode{Elem: jsonS[jsonpt-1].klist}
}
case 3:
//line parse.y:48
//line parse.y:50
{
jsonVAL.obj = ast.ObjectNode{}
}
case 4:
//line parse.y:54
//line parse.y:56
{
jsonVAL.list = []ast.Node{jsonS[jsonpt-0].assign}
jsonVAL.klist = []ast.KeyedNode{jsonS[jsonpt-0].assign}
}
case 5:
//line parse.y:58
//line parse.y:60
{
jsonVAL.list = append(jsonS[jsonpt-0].list, jsonS[jsonpt-2].assign)
jsonVAL.klist = append(jsonS[jsonpt-0].klist, jsonS[jsonpt-2].assign)
}
case 6:
//line parse.y:64
//line parse.y:66
{
jsonVAL.assign = ast.AssignmentNode{
Key: jsonS[jsonpt-2].str,
K: jsonS[jsonpt-2].str,
Value: jsonS[jsonpt-0].item,
}
}
case 7:
//line parse.y:73
//line parse.y:75
{
jsonVAL.item = ast.LiteralNode{
Type: ast.ValueTypeString,
@ -394,7 +395,7 @@ jsondefault:
}
}
case 8:
//line parse.y:80
//line parse.y:82
{
jsonVAL.item = ast.LiteralNode{
Type: ast.ValueTypeInt,
@ -402,17 +403,17 @@ jsondefault:
}
}
case 9:
//line parse.y:87
//line parse.y:89
{
jsonVAL.item = jsonS[jsonpt-0].obj
}
case 10:
//line parse.y:91
//line parse.y:93
{
jsonVAL.item = jsonS[jsonpt-0].array
}
case 11:
//line parse.y:95
//line parse.y:97
{
jsonVAL.item = ast.LiteralNode{
Type: ast.ValueTypeBool,
@ -420,7 +421,7 @@ jsondefault:
}
}
case 12:
//line parse.y:102
//line parse.y:104
{
jsonVAL.item = ast.LiteralNode{
Type: ast.ValueTypeBool,
@ -428,7 +429,7 @@ jsondefault:
}
}
case 13:
//line parse.y:109
//line parse.y:111
{
jsonVAL.item = ast.LiteralNode{
Type: ast.ValueTypeNil,
@ -436,22 +437,22 @@ jsondefault:
}
}
case 14:
//line parse.y:118
//line parse.y:120
{
jsonVAL.array = ast.ListNode{}
}
case 15:
//line parse.y:122
//line parse.y:124
{
jsonVAL.array = ast.ListNode{Elem: jsonS[jsonpt-1].list}
}
case 16:
//line parse.y:128
//line parse.y:130
{
jsonVAL.list = []ast.Node{jsonS[jsonpt-0].item}
}
case 17:
//line parse.y:132
//line parse.y:134
{
jsonVAL.list = append(jsonS[jsonpt-0].list, jsonS[jsonpt-2].item)
}

1
test-fixtures/basic.hcl Normal file
View File

@ -0,0 +1 @@
foo = "bar"