package gohcl import ( "encoding/json" "fmt" "reflect" "testing" "github.com/davecgh/go-spew/spew" "github.com/hashicorp/hcl/v2" hclJSON "github.com/hashicorp/hcl/v2/json" "github.com/zclconf/go-cty/cty" ) func TestDecodeBody(t *testing.T) { deepEquals := func(other interface{}) func(v interface{}) bool { return func(v interface{}) bool { return reflect.DeepEqual(v, other) } } type withNameExpression struct { 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"` } type withListofNestedBlocksNoPointers struct { Nested []withTwoAttributes `hcl:"nested,block"` } tests := []struct { Body map[string]interface{} Target func() interface{} Check func(v interface{}) bool DiagCount int }{ { map[string]interface{}{}, makeInstantiateType(struct{}{}), deepEquals(struct{}{}), 0, }, { map[string]interface{}{}, makeInstantiateType(struct { Name string `hcl:"name"` }{}), deepEquals(struct { Name string `hcl:"name"` }{}), 1, // name is required }, { map[string]interface{}{}, makeInstantiateType(struct { Name *string `hcl:"name"` }{}), deepEquals(struct { Name *string `hcl:"name"` }{}), 0, }, // name nil { map[string]interface{}{}, makeInstantiateType(struct { Name string `hcl:"name,optional"` }{}), deepEquals(struct { Name string `hcl:"name,optional"` }{}), 0, }, // name optional { map[string]interface{}{}, makeInstantiateType(withNameExpression{}), func(v interface{}) bool { if v == nil { return false } wne, valid := v.(withNameExpression) if !valid { return false } if wne.Name == nil { return false } nameVal, _ := wne.Name.Value(nil) if !nameVal.IsNull() { return false } return true }, 0, }, { map[string]interface{}{ "name": "Ermintrude", }, makeInstantiateType(withNameExpression{}), func(v interface{}) bool { if v == nil { return false } wne, valid := v.(withNameExpression) if !valid { return false } if wne.Name == nil { return false } nameVal, _ := wne.Name.Value(nil) if !nameVal.Equals(cty.StringVal("Ermintrude")).True() { return false } return true }, 0, }, { map[string]interface{}{ "name": "Ermintrude", }, makeInstantiateType(struct { Name string `hcl:"name"` }{}), deepEquals(struct { Name string `hcl:"name"` }{"Ermintrude"}), 0, }, { map[string]interface{}{ "name": "Ermintrude", "age": 23, }, makeInstantiateType(struct { Name string `hcl:"name"` }{}), deepEquals(struct { Name string `hcl:"name"` }{"Ermintrude"}), 1, // Extraneous "age" property }, { map[string]interface{}{ "name": "Ermintrude", "age": 50, }, makeInstantiateType(struct { Name string `hcl:"name"` Attrs hcl.Attributes `hcl:",remain"` }{}), func(gotI interface{}) bool { got := gotI.(struct { Name string `hcl:"name"` Attrs hcl.Attributes `hcl:",remain"` }) return got.Name == "Ermintrude" && len(got.Attrs) == 1 && got.Attrs["age"] != nil }, 0, }, { map[string]interface{}{ "name": "Ermintrude", "age": 50, }, makeInstantiateType(struct { Name string `hcl:"name"` Remain hcl.Body `hcl:",remain"` }{}), func(gotI interface{}) bool { got := gotI.(struct { Name string `hcl:"name"` Remain hcl.Body `hcl:",remain"` }) attrs, _ := got.Remain.JustAttributes() return got.Name == "Ermintrude" && len(attrs) == 1 && attrs["age"] != nil }, 0, }, { map[string]interface{}{ "name": "Ermintrude", "living": true, }, 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"` }{ Name: "Ermintrude", Remain: map[string]cty.Value{ "living": cty.True, }, }), 0, }, { map[string]interface{}{ "name": "Ermintrude", "age": 50, }, makeInstantiateType(struct { Name string `hcl:"name"` Body hcl.Body `hcl:",body"` Remain hcl.Body `hcl:",remain"` }{}), func(gotI interface{}) bool { got := gotI.(struct { Name string `hcl:"name"` Body hcl.Body `hcl:",body"` Remain hcl.Body `hcl:",remain"` }) attrs, _ := got.Body.JustAttributes() return got.Name == "Ermintrude" && len(attrs) == 2 && attrs["name"] != nil && attrs["age"] != nil }, 0, }, { map[string]interface{}{ "noodle": map[string]interface{}{}, }, makeInstantiateType(struct { Noodle struct{} `hcl:"noodle,block"` }{}), func(gotI interface{}) bool { // Generating no diagnostics is good enough for this one. return true }, 0, }, { map[string]interface{}{ "noodle": []map[string]interface{}{{}}, }, makeInstantiateType(struct { Noodle struct{} `hcl:"noodle,block"` }{}), func(gotI interface{}) bool { // Generating no diagnostics is good enough for this one. return true }, 0, }, { map[string]interface{}{ "noodle": []map[string]interface{}{{}, {}}, }, makeInstantiateType(struct { Noodle struct{} `hcl:"noodle,block"` }{}), func(gotI interface{}) bool { // Generating one diagnostic is good enough for this one. return true }, 1, }, { map[string]interface{}{}, makeInstantiateType(struct { Noodle struct{} `hcl:"noodle,block"` }{}), func(gotI interface{}) bool { // Generating one diagnostic is good enough for this one. return true }, 1, }, { map[string]interface{}{ "noodle": []map[string]interface{}{}, }, makeInstantiateType(struct { Noodle struct{} `hcl:"noodle,block"` }{}), func(gotI interface{}) bool { // Generating one diagnostic is good enough for this one. return true }, 1, }, { map[string]interface{}{ "noodle": map[string]interface{}{}, }, makeInstantiateType(struct { Noodle *struct{} `hcl:"noodle,block"` }{}), func(gotI interface{}) bool { return gotI.(struct { Noodle *struct{} `hcl:"noodle,block"` }).Noodle != nil }, 0, }, { map[string]interface{}{ "noodle": []map[string]interface{}{{}}, }, makeInstantiateType(struct { Noodle *struct{} `hcl:"noodle,block"` }{}), func(gotI interface{}) bool { return gotI.(struct { Noodle *struct{} `hcl:"noodle,block"` }).Noodle != nil }, 0, }, { map[string]interface{}{ "noodle": []map[string]interface{}{}, }, makeInstantiateType(struct { Noodle *struct{} `hcl:"noodle,block"` }{}), func(gotI interface{}) bool { return gotI.(struct { Noodle *struct{} `hcl:"noodle,block"` }).Noodle == nil }, 0, }, { map[string]interface{}{ "noodle": []map[string]interface{}{{}, {}}, }, makeInstantiateType(struct { Noodle *struct{} `hcl:"noodle,block"` }{}), func(gotI interface{}) bool { // Generating one diagnostic is good enough for this one. return true }, 1, }, { map[string]interface{}{ "noodle": []map[string]interface{}{}, }, makeInstantiateType(struct { Noodle []struct{} `hcl:"noodle,block"` }{}), func(gotI interface{}) bool { noodle := gotI.(struct { Noodle []struct{} `hcl:"noodle,block"` }).Noodle return len(noodle) == 0 }, 0, }, { map[string]interface{}{ "noodle": []map[string]interface{}{{}}, }, makeInstantiateType(struct { Noodle []struct{} `hcl:"noodle,block"` }{}), func(gotI interface{}) bool { noodle := gotI.(struct { Noodle []struct{} `hcl:"noodle,block"` }).Noodle return len(noodle) == 1 }, 0, }, { map[string]interface{}{ "noodle": []map[string]interface{}{{}, {}}, }, makeInstantiateType(struct { Noodle []struct{} `hcl:"noodle,block"` }{}), func(gotI interface{}) bool { noodle := gotI.(struct { Noodle []struct{} `hcl:"noodle,block"` }).Noodle return len(noodle) == 2 }, 0, }, { map[string]interface{}{ "noodle": map[string]interface{}{}, }, 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 // the JSON serialization detecting the missing level of // heirarchy for the label.) return true }, 2, }, { map[string]interface{}{ "noodle": map[string]interface{}{ "foo_foo": map[string]interface{}{}, }, }, makeInstantiateType(struct { Noodle struct { Name string `hcl:"name,label"` } `hcl:"noodle,block"` }{}), func(gotI interface{}) bool { noodle := gotI.(struct { Noodle struct { Name string `hcl:"name,label"` } `hcl:"noodle,block"` }).Noodle return noodle.Name == "foo_foo" }, 0, }, { map[string]interface{}{ "noodle": map[string]interface{}{ "foo_foo": map[string]interface{}{}, "bar_baz": map[string]interface{}{}, }, }, 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 }, 1, }, { map[string]interface{}{ "noodle": map[string]interface{}{ "foo_foo": map[string]interface{}{}, "bar_baz": map[string]interface{}{}, }, }, makeInstantiateType(struct { Noodles []struct { Name string `hcl:"name,label"` } `hcl:"noodle,block"` }{}), func(gotI interface{}) bool { noodles := gotI.(struct { Noodles []struct { Name string `hcl:"name,label"` } `hcl:"noodle,block"` }).Noodles return len(noodles) == 2 && (noodles[0].Name == "foo_foo" || noodles[0].Name == "bar_baz") && (noodles[1].Name == "foo_foo" || noodles[1].Name == "bar_baz") && noodles[0].Name != noodles[1].Name }, 0, }, { map[string]interface{}{ "noodle": map[string]interface{}{ "foo_foo": map[string]interface{}{ "type": "rice", }, }, }, 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 { Name string `hcl:"name,label"` Type string `hcl:"type"` } `hcl:"noodle,block"` }).Noodle return noodle.Name == "foo_foo" && noodle.Type == "rice" }, 0, }, { map[string]interface{}{ "name": "Ermintrude", "age": 34, }, makeInstantiateType(map[string]string(nil)), deepEquals(map[string]string{ "name": "Ermintrude", "age": "34", }), 0, }, { map[string]interface{}{ "name": "Ermintrude", "age": 89, }, 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 }, 0, }, { map[string]interface{}{ "name": "Ermintrude", "age": 13, }, 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 }, 0, }, { map[string]interface{}{ "name": "Ermintrude", "living": true, }, 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, }, { // Make sure decoding value slices works the same as pointer slices. map[string]interface{}{ "nested": []map[string]interface{}{ { "b": "bar", }, { "b": "baz", }, }, }, func() interface{} { return &withListofNestedBlocksNoPointers{ Nested: []withTwoAttributes{ { B: "foo", }, }, } }, func(gotI interface{}) bool { n := gotI.(withListofNestedBlocksNoPointers) return n.Nested[0].B == "bar" && len(n.Nested) == 2 }, 0, }, } for i, test := range tests { // For convenience here we're going to use the JSON parser // to process the given body. buf, err := json.Marshal(test.Body) if err != nil { t.Fatalf("error JSON-encoding body for test %d: %s", i, err) } t.Run(string(buf), func(t *testing.T) { file, diags := hclJSON.Parse(buf, "test.json") if len(diags) != 0 { t.Fatalf("diagnostics while parsing: %s", diags.Error()) } targetVal := reflect.ValueOf(test.Target()) diags = DecodeBody(file.Body, nil, targetVal.Interface()) if len(diags) != test.DiagCount { t.Errorf("wrong number of diagnostics %d; want %d", len(diags), test.DiagCount) for _, diag := range diags { t.Logf(" - %s", diag.Error()) } } got := targetVal.Elem().Interface() if !test.Check(got) { t.Errorf("wrong result\ngot: %s", spew.Sdump(got)) } }) } } func TestDecodeExpression(t *testing.T) { tests := []struct { Value cty.Value Target interface{} Want interface{} DiagCount int }{ { cty.StringVal("hello"), "", "hello", 0, }, { cty.StringVal("hello"), cty.NilVal, cty.StringVal("hello"), 0, }, { cty.NumberIntVal(2), "", "2", 0, }, { cty.StringVal("true"), false, true, 0, }, { cty.NullVal(cty.String), "", "", 1, // null value is not allowed }, { cty.UnknownVal(cty.String), "", "", 1, // value must be known }, { cty.ListVal([]cty.Value{cty.True}), false, false, 1, // bool required }, } for i, test := range tests { t.Run(fmt.Sprintf("%02d", i), func(t *testing.T) { expr := &fixedExpression{test.Value} targetVal := reflect.New(reflect.TypeOf(test.Target)) diags := DecodeExpression(expr, nil, targetVal.Interface()) if len(diags) != test.DiagCount { t.Errorf("wrong number of diagnostics %d; want %d", len(diags), test.DiagCount) for _, diag := range diags { t.Logf(" - %s", diag.Error()) } } got := targetVal.Elem().Interface() if !reflect.DeepEqual(got, test.Want) { t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.Want) } }) } } type fixedExpression struct { val cty.Value } func (e *fixedExpression) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) { return e.val, nil } func (e *fixedExpression) Range() (r hcl.Range) { return } func (e *fixedExpression) StartRange() (r hcl.Range) { return } func (e *fixedExpression) Variables() []hcl.Traversal { return nil } func makeInstantiateType(target interface{}) func() interface{} { return func() interface{} { return reflect.New(reflect.TypeOf(target)).Interface() } }