Unflatten single objects from a list
A single json object with nested objects is flattened by the json parser to a list of objects sharing the parent key. If we're decoding into struct this was likely a mistake, and we need to re-expand the ast.
This commit is contained in:
parent
77eac88c9f
commit
d3228f113d
95
decoder.go
95
decoder.go
@ -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()
|
||||||
@ -419,6 +418,20 @@ func (d *decoder) decodeSlice(name string, node ast.Node, result reflect.Value)
|
|||||||
resultSliceType, 0, 0)
|
resultSliceType, 0, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// if node is an object that was decoded from ambiguous JSON and flattened
|
||||||
|
// as a list, decode into a single value in the slice.
|
||||||
|
if objAsList(node, resultElemType) {
|
||||||
|
val := reflect.Indirect(reflect.New(resultElemType))
|
||||||
|
err := d.decodeFlatObj(name+"[0]", node, val)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
result = reflect.Append(result, val)
|
||||||
|
set.Set(result)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// Figure out the items we'll be copying into the slice
|
// Figure out the items we'll be copying into the slice
|
||||||
var items []ast.Node
|
var items []ast.Node
|
||||||
switch n := node.(type) {
|
switch n := node.(type) {
|
||||||
@ -455,6 +468,85 @@ func (d *decoder) decodeSlice(name string, node ast.Node, result reflect.Value)
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// decode a flattened object into a single struct
|
||||||
|
func (d decoder) decodeFlatObj(name string, node ast.Node, result reflect.Value) error {
|
||||||
|
list := node.(*ast.ObjectList)
|
||||||
|
var keyToken token.Token
|
||||||
|
// get the key token, and strip it out of the key-val nodes
|
||||||
|
for _, item := range list.Items {
|
||||||
|
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: list,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := d.decode(name, newNode, result); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// objAsList detects if an ambiguous JSON object was flattened to a List which
|
||||||
|
// should be decoded into a struct.
|
||||||
|
func objAsList(node ast.Node, elemType reflect.Type) bool {
|
||||||
|
objList, ok := node.(*ast.ObjectList)
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// our target type must be a struct
|
||||||
|
switch elemType.Kind() {
|
||||||
|
case reflect.Ptr:
|
||||||
|
switch elemType.Elem().Kind() {
|
||||||
|
case reflect.Struct:
|
||||||
|
//OK
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
case reflect.Struct:
|
||||||
|
//OK
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldNames := make(map[string]bool)
|
||||||
|
prevKey := ""
|
||||||
|
|
||||||
|
for _, item := range objList.Items {
|
||||||
|
// a list value will have a key and field name
|
||||||
|
if len(item.Keys) != 2 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
key := item.Keys[0].Token.Value().(string)
|
||||||
|
if prevKey != "" && key != prevKey {
|
||||||
|
// the same object will have all the same key
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
prevKey = key
|
||||||
|
|
||||||
|
fieldName := item.Keys[1].Token.Value().(string)
|
||||||
|
|
||||||
|
if ok := fieldNames[fieldName]; ok {
|
||||||
|
// A single struct won't have duplicate fields.
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
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 +698,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 {
|
||||||
|
@ -867,7 +867,7 @@ func TestDecode_structureFlattened(t *testing.T) {
|
|||||||
jsonB := `
|
jsonB := `
|
||||||
{
|
{
|
||||||
"var_2": {
|
"var_2": {
|
||||||
"description": "an extra field is required",
|
"description": "Described",
|
||||||
"default": {
|
"default": {
|
||||||
"key1": "a",
|
"key1": "a",
|
||||||
"key2": "b"
|
"key2": "b"
|
||||||
@ -876,6 +876,7 @@ func TestDecode_structureFlattened(t *testing.T) {
|
|||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
|
// make sure we can also correctly extract the Name key
|
||||||
type V struct {
|
type V struct {
|
||||||
Name string `hcl:",key"`
|
Name string `hcl:",key"`
|
||||||
Description string
|
Description string
|
||||||
@ -888,21 +889,36 @@ func TestDecode_structureFlattened(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("err: %s", err)
|
t.Fatalf("err: %s", err)
|
||||||
}
|
}
|
||||||
|
if len(vA) != 1 {
|
||||||
|
t.Fatal("failed to decode jsonA")
|
||||||
|
}
|
||||||
|
|
||||||
err = Decode(&vB, jsonB)
|
err = Decode(&vB, jsonB)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("err: %s", err)
|
t.Fatalf("err: %s", err)
|
||||||
}
|
}
|
||||||
|
if len(vB) != 1 {
|
||||||
if len(vA) == 0 {
|
t.Fatal("failed to decode jsonB")
|
||||||
t.Fatal("failed to decode vA")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(vB) == 0 {
|
expectedA := []*V{
|
||||||
t.Fatal("failed to decode vB")
|
&V{
|
||||||
|
Name: "var_1",
|
||||||
|
Default: map[string]string{"key1": "a", "key2": "b"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
expectedB := []*V{
|
||||||
|
&V{
|
||||||
|
Name: "var_2",
|
||||||
|
Description: "Described",
|
||||||
|
Default: map[string]string{"key1": "a", "key2": "b"},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
if !reflect.DeepEqual(vA[0].Default, vB[0].Default) {
|
if !reflect.DeepEqual(vA, expectedA) {
|
||||||
t.Fatalf("defaults should match\n[0]: %#v\n[1]: %#v\n", vA[0], vB[0])
|
t.Fatalf("\nexpected: %#v\ngot: %#v\n", expectedA[0], vA[0])
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(vB, expectedB) {
|
||||||
|
t.Fatalf("\nexpected: %#v\ngot: %#v\n", expectedB[0], vB[0])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user