Merge pull request #152 from hashicorp/jbardin/undecoded

missing fields when decoding JSON
This commit is contained in:
James Bardin 2016-09-16 09:01:00 -04:00 committed by GitHub
commit ef8133da8c
5 changed files with 299 additions and 2 deletions

View File

@ -1,3 +1,3 @@
sudo: false sudo: false
language: go language: go
go: 1.5 go: 1.7

View File

@ -6,6 +6,7 @@ fmt: generate
go fmt ./... go fmt ./...
test: generate test: generate
go get -t ./...
go test $(TEST) $(TESTARGS) go test $(TEST) $(TESTARGS)
generate: generate:

View File

@ -12,5 +12,8 @@ install:
go version go version
go env go env
go get -t ./...
build_script: build_script:
- cmd: go test -v ./... - cmd: go test -v ./...

View File

@ -409,7 +409,6 @@ func (d *decoder) decodeSlice(name string, node ast.Node, result reflect.Value)
if result.Kind() == reflect.Interface { if result.Kind() == reflect.Interface {
result = result.Elem() result = result.Elem()
} }
// Create the slice if it isn't nil // Create the slice if it isn't nil
resultType := result.Type() resultType := result.Type()
resultElemType := resultType.Elem() resultElemType := resultType.Elem()
@ -443,6 +442,12 @@ func (d *decoder) decodeSlice(name string, node ast.Node, result reflect.Value)
// Decode // Decode
val := reflect.Indirect(reflect.New(resultElemType)) val := reflect.Indirect(reflect.New(resultElemType))
// if item is an object that was decoded from ambiguous JSON and
// flattened, make sure it's expanded if it needs to decode into a
// defined structure.
item := expandObject(item, val)
if err := d.decode(fieldName, item, val); err != nil { if err := d.decode(fieldName, item, val); err != nil {
return err return err
} }
@ -455,6 +460,57 @@ func (d *decoder) decodeSlice(name string, node ast.Node, result reflect.Value)
return nil return nil
} }
// expandObject detects if an ambiguous JSON object was flattened to a List which
// should be decoded into a struct, and expands the ast to properly deocode.
func expandObject(node ast.Node, result reflect.Value) ast.Node {
item, ok := node.(*ast.ObjectItem)
if !ok {
return node
}
elemType := result.Type()
// our target type must be a struct
switch elemType.Kind() {
case reflect.Ptr:
switch elemType.Elem().Kind() {
case reflect.Struct:
//OK
default:
return node
}
case reflect.Struct:
//OK
default:
return node
}
// A list value will have a key and field name. If it had more fields,
// it wouldn't have been flattened.
if len(item.Keys) != 2 {
return node
}
keyToken := item.Keys[0].Token
item.Keys = item.Keys[1:]
// we need to un-flatten the ast enough to decode
newNode := &ast.ObjectItem{
Keys: []*ast.ObjectKey{
&ast.ObjectKey{
Token: keyToken,
},
},
Val: &ast.ObjectType{
List: &ast.ObjectList{
Items: []*ast.ObjectItem{item},
},
},
}
return newNode
}
func (d *decoder) decodeString(name string, node ast.Node, result reflect.Value) error { func (d *decoder) decodeString(name string, node ast.Node, result reflect.Value) error {
switch n := node.(type) { switch n := node.(type) {
case *ast.LiteralType: case *ast.LiteralType:
@ -606,6 +662,7 @@ func (d *decoder) decodeStruct(name string, node ast.Node, result reflect.Value)
// match (only object with the field), then we decode it exactly. // match (only object with the field), then we decode it exactly.
// If it is a prefix match, then we decode the matches. // If it is a prefix match, then we decode the matches.
filter := list.Filter(fieldName) filter := list.Filter(fieldName)
prefixMatches := filter.Children() prefixMatches := filter.Children()
matches := filter.Elem() matches := filter.Elem()
if len(matches.Items) == 0 && len(prefixMatches.Items) == 0 { if len(matches.Items) == 0 && len(prefixMatches.Items) == 0 {

View File

@ -6,6 +6,7 @@ import (
"reflect" "reflect"
"testing" "testing"
"github.com/davecgh/go-spew/spew"
"github.com/hashicorp/hcl/hcl/ast" "github.com/hashicorp/hcl/hcl/ast"
"github.com/hashicorp/hcl/testhelper" "github.com/hashicorp/hcl/testhelper"
) )
@ -851,3 +852,238 @@ func TestDecode_topLevelKeys(t *testing.T) {
t.Errorf("bad source: %s", templates.Templates[1].Source) t.Errorf("bad source: %s", templates.Templates[1].Source)
} }
} }
func TestDecode_flattenedJSON(t *testing.T) {
// make sure we can also correctly extract a Name key too
type V struct {
Name string `hcl:",key"`
Description string
Default map[string]string
}
type Vars struct {
Variable []*V
}
cases := []struct {
JSON string
Out interface{}
Expected interface{}
}{
{ // Nested object, no sibling keys
JSON: `
{
"var_name": {
"default": {
"key1": "a",
"key2": "b"
}
}
}
`,
Out: &[]*V{},
Expected: &[]*V{
&V{
Name: "var_name",
Default: map[string]string{"key1": "a", "key2": "b"},
},
},
},
{ // Nested object with a sibling key (this worked previously)
JSON: `
{
"var_name": {
"description": "Described",
"default": {
"key1": "a",
"key2": "b"
}
}
}
`,
Out: &[]*V{},
Expected: &[]*V{
&V{
Name: "var_name",
Description: "Described",
Default: map[string]string{"key1": "a", "key2": "b"},
},
},
},
{ // Multiple nested objects, one with a sibling key
JSON: `
{
"variable": {
"var_1": {
"default": {
"key1": "a",
"key2": "b"
}
},
"var_2": {
"description": "Described",
"default": {
"key1": "a",
"key2": "b"
}
}
}
}
`,
Out: &Vars{},
Expected: &Vars{
Variable: []*V{
&V{
Name: "var_1",
Default: map[string]string{"key1": "a", "key2": "b"},
},
&V{
Name: "var_2",
Description: "Described",
Default: map[string]string{"key1": "a", "key2": "b"},
},
},
},
},
{ // Nested object to maps
JSON: `
{
"variable": {
"var_name": {
"description": "Described",
"default": {
"key1": "a",
"key2": "b"
}
}
}
}
`,
Out: &[]map[string]interface{}{},
Expected: &[]map[string]interface{}{
{
"variable": []map[string]interface{}{
{
"var_name": []map[string]interface{}{
{
"description": "Described",
"default": []map[string]interface{}{
{
"key1": "a",
"key2": "b",
},
},
},
},
},
},
},
},
},
{ // Nested object to maps without a sibling key should decode the same as above
JSON: `
{
"variable": {
"var_name": {
"default": {
"key1": "a",
"key2": "b"
}
}
}
}
`,
Out: &[]map[string]interface{}{},
Expected: &[]map[string]interface{}{
{
"variable": []map[string]interface{}{
{
"var_name": []map[string]interface{}{
{
"default": []map[string]interface{}{
{
"key1": "a",
"key2": "b",
},
},
},
},
},
},
},
},
},
{ // Nested objects, one with a sibling key, and one without
JSON: `
{
"variable": {
"var_1": {
"default": {
"key1": "a",
"key2": "b"
}
},
"var_2": {
"description": "Described",
"default": {
"key1": "a",
"key2": "b"
}
}
}
}
`,
Out: &[]map[string]interface{}{},
Expected: &[]map[string]interface{}{
{
"variable": []map[string]interface{}{
{
"var_1": []map[string]interface{}{
{
"default": []map[string]interface{}{
{
"key1": "a",
"key2": "b",
},
},
},
},
},
},
},
{
"variable": []map[string]interface{}{
{
"var_2": []map[string]interface{}{
{
"description": "Described",
"default": []map[string]interface{}{
{
"key1": "a",
"key2": "b",
},
},
},
},
},
},
},
},
},
}
for i, tc := range cases {
err := Decode(tc.Out, tc.JSON)
if err != nil {
t.Fatalf("[%d] err: %s", i, err)
}
if !reflect.DeepEqual(tc.Out, tc.Expected) {
t.Fatalf("[%d]\ngot: %s\nexpected: %s\n", i, spew.Sdump(tc.Out), spew.Sdump(tc.Expected))
}
}
}