diff --git a/gohcl/decode.go b/gohcl/decode.go index 7ba08ee..f0d589d 100644 --- a/gohcl/decode.go +++ b/gohcl/decode.go @@ -147,7 +147,9 @@ func decodeBodyToStruct(body hcl.Body, ctx *hcl.EvalContext, val reflect.Value) if len(blocks) == 0 { if isSlice || isPtr { - val.Field(fieldIdx).Set(reflect.Zero(field.Type)) + if val.Field(fieldIdx).IsNil() { + val.Field(fieldIdx).Set(reflect.Zero(field.Type)) + } } else { diags = append(diags, &hcl.Diagnostic{ Severity: hcl.DiagError, @@ -166,11 +168,20 @@ func decodeBodyToStruct(body hcl.Body, ctx *hcl.EvalContext, val reflect.Value) if isPtr { elemType = reflect.PtrTo(ty) } - sli := reflect.MakeSlice(reflect.SliceOf(elemType), len(blocks), len(blocks)) + sli := val.Field(fieldIdx) + if sli.IsNil() { + sli = reflect.MakeSlice(reflect.SliceOf(elemType), len(blocks), len(blocks)) + } for i, block := range blocks { if isPtr { - v := reflect.New(ty) + if i >= sli.Len() { + sli = reflect.Append(sli, reflect.New(ty)) + } + v := sli.Index(i) + if v.IsNil() { + v = reflect.New(ty) + } diags = append(diags, decodeBlockToValue(block, ctx, v.Elem())...) sli.Index(i).Set(v) } else { @@ -178,12 +189,19 @@ func decodeBodyToStruct(body hcl.Body, ctx *hcl.EvalContext, val reflect.Value) } } + if sli.Len() > len(blocks) { + sli.SetLen(len(blocks)) + } + val.Field(fieldIdx).Set(sli) default: block := blocks[0] if isPtr { - v := reflect.New(ty) + v := val.Field(fieldIdx) + if v.IsNil() { + v = reflect.New(ty) + } diags = append(diags, decodeBlockToValue(block, ctx, v.Elem())...) val.Field(fieldIdx).Set(v) } else { diff --git a/gohcl/decode_test.go b/gohcl/decode_test.go index b0a9fb6..8f065c4 100644 --- a/gohcl/decode_test.go +++ b/gohcl/decode_test.go @@ -23,23 +23,37 @@ func TestDecodeBody(t *testing.T) { Name hcl.Expression `hcl:"name"` } + type withTwoAttributes struct { + A string `hcl:"a,optional"` + B string `hcl:"b,optional"` + } + + type withNestedBlock struct { + Plain string `hcl:"plain,optional"` + Nested *withTwoAttributes `hcl:"nested,block"` + } + + type withListofNestedBlocks struct { + Nested []*withTwoAttributes `hcl:"nested,block"` + } + tests := []struct { Body map[string]interface{} - Target interface{} + Target func() interface{} Check func(v interface{}) bool DiagCount int }{ { map[string]interface{}{}, - struct{}{}, + makeInstantiateType(struct{}{}), deepEquals(struct{}{}), 0, }, { map[string]interface{}{}, - struct { + makeInstantiateType(struct { Name string `hcl:"name"` - }{}, + }{}), deepEquals(struct { Name string `hcl:"name"` }{}), @@ -47,9 +61,9 @@ func TestDecodeBody(t *testing.T) { }, { map[string]interface{}{}, - struct { + makeInstantiateType(struct { Name *string `hcl:"name"` - }{}, + }{}), deepEquals(struct { Name *string `hcl:"name"` }{}), @@ -57,9 +71,9 @@ func TestDecodeBody(t *testing.T) { }, // name nil { map[string]interface{}{}, - struct { + makeInstantiateType(struct { Name string `hcl:"name,optional"` - }{}, + }{}), deepEquals(struct { Name string `hcl:"name,optional"` }{}), @@ -67,7 +81,7 @@ func TestDecodeBody(t *testing.T) { }, // name optional { map[string]interface{}{}, - withNameExpression{}, + makeInstantiateType(withNameExpression{}), func(v interface{}) bool { if v == nil { return false @@ -95,7 +109,7 @@ func TestDecodeBody(t *testing.T) { map[string]interface{}{ "name": "Ermintrude", }, - withNameExpression{}, + makeInstantiateType(withNameExpression{}), func(v interface{}) bool { if v == nil { return false @@ -123,9 +137,9 @@ func TestDecodeBody(t *testing.T) { map[string]interface{}{ "name": "Ermintrude", }, - struct { + makeInstantiateType(struct { Name string `hcl:"name"` - }{}, + }{}), deepEquals(struct { Name string `hcl:"name"` }{"Ermintrude"}), @@ -136,9 +150,9 @@ func TestDecodeBody(t *testing.T) { "name": "Ermintrude", "age": 23, }, - struct { + makeInstantiateType(struct { Name string `hcl:"name"` - }{}, + }{}), deepEquals(struct { Name string `hcl:"name"` }{"Ermintrude"}), @@ -149,10 +163,10 @@ func TestDecodeBody(t *testing.T) { "name": "Ermintrude", "age": 50, }, - struct { + makeInstantiateType(struct { Name string `hcl:"name"` Attrs hcl.Attributes `hcl:",remain"` - }{}, + }{}), func(gotI interface{}) bool { got := gotI.(struct { Name string `hcl:"name"` @@ -167,10 +181,10 @@ func TestDecodeBody(t *testing.T) { "name": "Ermintrude", "age": 50, }, - struct { + makeInstantiateType(struct { Name string `hcl:"name"` Remain hcl.Body `hcl:",remain"` - }{}, + }{}), func(gotI interface{}) bool { got := gotI.(struct { Name string `hcl:"name"` @@ -188,10 +202,10 @@ func TestDecodeBody(t *testing.T) { "name": "Ermintrude", "living": true, }, - struct { + makeInstantiateType(struct { Name string `hcl:"name"` Remain map[string]cty.Value `hcl:",remain"` - }{}, + }{}), deepEquals(struct { Name string `hcl:"name"` Remain map[string]cty.Value `hcl:",remain"` @@ -207,9 +221,9 @@ func TestDecodeBody(t *testing.T) { map[string]interface{}{ "noodle": map[string]interface{}{}, }, - struct { + makeInstantiateType(struct { Noodle struct{} `hcl:"noodle,block"` - }{}, + }{}), func(gotI interface{}) bool { // Generating no diagnostics is good enough for this one. return true @@ -220,9 +234,9 @@ func TestDecodeBody(t *testing.T) { map[string]interface{}{ "noodle": []map[string]interface{}{{}}, }, - struct { + makeInstantiateType(struct { Noodle struct{} `hcl:"noodle,block"` - }{}, + }{}), func(gotI interface{}) bool { // Generating no diagnostics is good enough for this one. return true @@ -233,9 +247,9 @@ func TestDecodeBody(t *testing.T) { map[string]interface{}{ "noodle": []map[string]interface{}{{}, {}}, }, - struct { + makeInstantiateType(struct { Noodle struct{} `hcl:"noodle,block"` - }{}, + }{}), func(gotI interface{}) bool { // Generating one diagnostic is good enough for this one. return true @@ -244,9 +258,9 @@ func TestDecodeBody(t *testing.T) { }, { map[string]interface{}{}, - struct { + makeInstantiateType(struct { Noodle struct{} `hcl:"noodle,block"` - }{}, + }{}), func(gotI interface{}) bool { // Generating one diagnostic is good enough for this one. return true @@ -257,9 +271,9 @@ func TestDecodeBody(t *testing.T) { map[string]interface{}{ "noodle": []map[string]interface{}{}, }, - struct { + makeInstantiateType(struct { Noodle struct{} `hcl:"noodle,block"` - }{}, + }{}), func(gotI interface{}) bool { // Generating one diagnostic is good enough for this one. return true @@ -270,9 +284,9 @@ func TestDecodeBody(t *testing.T) { map[string]interface{}{ "noodle": map[string]interface{}{}, }, - struct { + makeInstantiateType(struct { Noodle *struct{} `hcl:"noodle,block"` - }{}, + }{}), func(gotI interface{}) bool { return gotI.(struct { Noodle *struct{} `hcl:"noodle,block"` @@ -284,9 +298,9 @@ func TestDecodeBody(t *testing.T) { map[string]interface{}{ "noodle": []map[string]interface{}{{}}, }, - struct { + makeInstantiateType(struct { Noodle *struct{} `hcl:"noodle,block"` - }{}, + }{}), func(gotI interface{}) bool { return gotI.(struct { Noodle *struct{} `hcl:"noodle,block"` @@ -298,9 +312,9 @@ func TestDecodeBody(t *testing.T) { map[string]interface{}{ "noodle": []map[string]interface{}{}, }, - struct { + makeInstantiateType(struct { Noodle *struct{} `hcl:"noodle,block"` - }{}, + }{}), func(gotI interface{}) bool { return gotI.(struct { Noodle *struct{} `hcl:"noodle,block"` @@ -312,9 +326,9 @@ func TestDecodeBody(t *testing.T) { map[string]interface{}{ "noodle": []map[string]interface{}{{}, {}}, }, - struct { + makeInstantiateType(struct { Noodle *struct{} `hcl:"noodle,block"` - }{}, + }{}), func(gotI interface{}) bool { // Generating one diagnostic is good enough for this one. return true @@ -325,9 +339,9 @@ func TestDecodeBody(t *testing.T) { map[string]interface{}{ "noodle": []map[string]interface{}{}, }, - struct { + makeInstantiateType(struct { Noodle []struct{} `hcl:"noodle,block"` - }{}, + }{}), func(gotI interface{}) bool { noodle := gotI.(struct { Noodle []struct{} `hcl:"noodle,block"` @@ -340,9 +354,9 @@ func TestDecodeBody(t *testing.T) { map[string]interface{}{ "noodle": []map[string]interface{}{{}}, }, - struct { + makeInstantiateType(struct { Noodle []struct{} `hcl:"noodle,block"` - }{}, + }{}), func(gotI interface{}) bool { noodle := gotI.(struct { Noodle []struct{} `hcl:"noodle,block"` @@ -355,9 +369,9 @@ func TestDecodeBody(t *testing.T) { map[string]interface{}{ "noodle": []map[string]interface{}{{}, {}}, }, - struct { + makeInstantiateType(struct { Noodle []struct{} `hcl:"noodle,block"` - }{}, + }{}), func(gotI interface{}) bool { noodle := gotI.(struct { Noodle []struct{} `hcl:"noodle,block"` @@ -370,11 +384,11 @@ func TestDecodeBody(t *testing.T) { map[string]interface{}{ "noodle": map[string]interface{}{}, }, - struct { + makeInstantiateType(struct { Noodle struct { Name string `hcl:"name,label"` } `hcl:"noodle,block"` - }{}, + }{}), func(gotI interface{}) bool { // Generating two diagnostics is good enough for this one. // (one for the missing noodle block and the other for @@ -390,11 +404,11 @@ func TestDecodeBody(t *testing.T) { "foo_foo": map[string]interface{}{}, }, }, - struct { + makeInstantiateType(struct { Noodle struct { Name string `hcl:"name,label"` } `hcl:"noodle,block"` - }{}, + }{}), func(gotI interface{}) bool { noodle := gotI.(struct { Noodle struct { @@ -412,11 +426,11 @@ func TestDecodeBody(t *testing.T) { "bar_baz": map[string]interface{}{}, }, }, - struct { + makeInstantiateType(struct { Noodle struct { Name string `hcl:"name,label"` } `hcl:"noodle,block"` - }{}, + }{}), func(gotI interface{}) bool { // One diagnostic is enough for this one. return true @@ -430,11 +444,11 @@ func TestDecodeBody(t *testing.T) { "bar_baz": map[string]interface{}{}, }, }, - struct { + makeInstantiateType(struct { Noodles []struct { Name string `hcl:"name,label"` } `hcl:"noodle,block"` - }{}, + }{}), func(gotI interface{}) bool { noodles := gotI.(struct { Noodles []struct { @@ -453,12 +467,12 @@ func TestDecodeBody(t *testing.T) { }, }, }, - struct { + makeInstantiateType(struct { Noodle struct { Name string `hcl:"name,label"` Type string `hcl:"type"` } `hcl:"noodle,block"` - }{}, + }{}), func(gotI interface{}) bool { noodle := gotI.(struct { Noodle struct { @@ -476,7 +490,7 @@ func TestDecodeBody(t *testing.T) { "name": "Ermintrude", "age": 34, }, - map[string]string(nil), + makeInstantiateType(map[string]string(nil)), deepEquals(map[string]string{ "name": "Ermintrude", "age": "34", @@ -488,7 +502,7 @@ func TestDecodeBody(t *testing.T) { "name": "Ermintrude", "age": 89, }, - map[string]*hcl.Attribute(nil), + makeInstantiateType(map[string]*hcl.Attribute(nil)), func(gotI interface{}) bool { got := gotI.(map[string]*hcl.Attribute) return len(got) == 2 && got["name"] != nil && got["age"] != nil @@ -500,7 +514,7 @@ func TestDecodeBody(t *testing.T) { "name": "Ermintrude", "age": 13, }, - map[string]hcl.Expression(nil), + makeInstantiateType(map[string]hcl.Expression(nil)), func(gotI interface{}) bool { got := gotI.(map[string]hcl.Expression) return len(got) == 2 && got["name"] != nil && got["age"] != nil @@ -512,13 +526,103 @@ func TestDecodeBody(t *testing.T) { "name": "Ermintrude", "living": true, }, - map[string]cty.Value(nil), + makeInstantiateType(map[string]cty.Value(nil)), deepEquals(map[string]cty.Value{ "name": cty.StringVal("Ermintrude"), "living": cty.True, }), 0, }, + { + // Retain "nested" block while decoding + map[string]interface{}{ + "plain": "foo", + }, + func() interface{} { + return &withNestedBlock{ + Plain: "bar", + Nested: &withTwoAttributes{ + A: "bar", + }, + } + }, + func(gotI interface{}) bool { + foo := gotI.(withNestedBlock) + return foo.Plain == "foo" && foo.Nested != nil && foo.Nested.A == "bar" + }, + 0, + }, + { + // Retain values in "nested" block while decoding + map[string]interface{}{ + "nested": map[string]interface{}{ + "a": "foo", + }, + }, + func() interface{} { + return &withNestedBlock{ + Nested: &withTwoAttributes{ + B: "bar", + }, + } + }, + func(gotI interface{}) bool { + foo := gotI.(withNestedBlock) + return foo.Nested.A == "foo" && foo.Nested.B == "bar" + }, + 0, + }, + { + // Retain values in "nested" block list while decoding + map[string]interface{}{ + "nested": []map[string]interface{}{ + { + "a": "foo", + }, + }, + }, + func() interface{} { + return &withListofNestedBlocks{ + Nested: []*withTwoAttributes{ + &withTwoAttributes{ + B: "bar", + }, + }, + } + }, + func(gotI interface{}) bool { + n := gotI.(withListofNestedBlocks) + return n.Nested[0].A == "foo" && n.Nested[0].B == "bar" + }, + 0, + }, + { + // Remove additional elements from the list while decoding nested blocks + map[string]interface{}{ + "nested": []map[string]interface{}{ + { + "a": "foo", + }, + }, + }, + func() interface{} { + return &withListofNestedBlocks{ + Nested: []*withTwoAttributes{ + &withTwoAttributes{ + B: "bar", + }, + &withTwoAttributes{ + B: "bar", + }, + }, + } + }, + func(gotI interface{}) bool { + n := gotI.(withListofNestedBlocks) + return len(n.Nested) == 1 + }, + 0, + }, } for i, test := range tests { @@ -535,7 +639,7 @@ func TestDecodeBody(t *testing.T) { t.Fatalf("diagnostics while parsing: %s", diags.Error()) } - targetVal := reflect.New(reflect.TypeOf(test.Target)) + targetVal := reflect.ValueOf(test.Target()) diags = DecodeBody(file.Body, nil, targetVal.Interface()) if len(diags) != test.DiagCount { @@ -643,3 +747,9 @@ func (e *fixedExpression) StartRange() (r hcl.Range) { func (e *fixedExpression) Variables() []hcl.Traversal { return nil } + +func makeInstantiateType(target interface{}) func() interface{} { + return func() interface{} { + return reflect.New(reflect.TypeOf(target)).Interface() + } +}