package hcldec import ( "fmt" "reflect" "testing" "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hclsyntax" "github.com/zclconf/go-cty/cty" ) func TestDecode(t *testing.T) { tests := []struct { config string spec Spec ctx *hcl.EvalContext want cty.Value diagCount int }{ { ``, &ObjectSpec{}, nil, cty.EmptyObjectVal, 0, }, { "a = 1\n", &ObjectSpec{}, nil, cty.EmptyObjectVal, 1, // attribute named "a" is not expected here }, { "a = 1\n", &ObjectSpec{ "a": &AttrSpec{ Name: "a", Type: cty.Number, }, }, nil, cty.ObjectVal(map[string]cty.Value{ "a": cty.NumberIntVal(1), }), 0, }, { "a = 1\n", &AttrSpec{ Name: "a", Type: cty.Number, }, nil, cty.NumberIntVal(1), 0, }, { "a = 1\n", &DefaultSpec{ Primary: &AttrSpec{ Name: "a", Type: cty.Number, }, Default: &LiteralSpec{ Value: cty.NumberIntVal(10), }, }, nil, cty.NumberIntVal(1), 0, }, { "", &DefaultSpec{ Primary: &AttrSpec{ Name: "a", Type: cty.Number, }, Default: &LiteralSpec{ Value: cty.NumberIntVal(10), }, }, nil, cty.NumberIntVal(10), 0, }, { "a = 1\n", ObjectSpec{ "foo": &DefaultSpec{ Primary: &AttrSpec{ Name: "a", Type: cty.Number, }, Default: &LiteralSpec{ Value: cty.NumberIntVal(10), }, }, }, nil, cty.ObjectVal(map[string]cty.Value{"foo": cty.NumberIntVal(1)}), 0, }, { "a = \"1\"\n", &AttrSpec{ Name: "a", Type: cty.Number, }, nil, cty.NumberIntVal(1), 0, }, { "a = true\n", &AttrSpec{ Name: "a", Type: cty.Number, }, nil, cty.UnknownVal(cty.Number), 1, // incorrect type - number required. }, { ``, &AttrSpec{ Name: "a", Type: cty.Number, Required: true, }, nil, cty.NullVal(cty.Number), 1, // attribute "a" is required }, { ` b { } `, &BlockSpec{ TypeName: "b", Nested: ObjectSpec{}, }, nil, cty.EmptyObjectVal, 0, }, { ` b "baz" { } `, &BlockSpec{ TypeName: "b", Nested: &BlockLabelSpec{ Index: 0, Name: "name", }, }, nil, cty.StringVal("baz"), 0, }, { ` b "baz" {} b "foo" {} `, &BlockSpec{ TypeName: "b", Nested: &BlockLabelSpec{ Index: 0, Name: "name", }, }, nil, cty.StringVal("baz"), 1, // duplicate "b" block }, { ` b { } `, &BlockSpec{ TypeName: "b", Nested: &BlockLabelSpec{ Index: 0, Name: "name", }, }, nil, cty.NullVal(cty.String), 1, // missing name label }, { ``, &BlockSpec{ TypeName: "b", Nested: ObjectSpec{}, }, nil, cty.NullVal(cty.EmptyObject), 0, }, { "a {}\n", &BlockSpec{ TypeName: "b", Nested: ObjectSpec{}, }, nil, cty.NullVal(cty.EmptyObject), 1, // blocks of type "a" are not supported }, { ``, &BlockSpec{ TypeName: "b", Nested: ObjectSpec{}, Required: true, }, nil, cty.NullVal(cty.EmptyObject), 1, // a block of type "b" is required }, { ` b {} b {} `, &BlockSpec{ TypeName: "b", Nested: ObjectSpec{}, Required: true, }, nil, cty.EmptyObjectVal, 1, // only one "b" block is allowed }, { ` b { } `, &BlockAttrsSpec{ TypeName: "b", ElementType: cty.String, }, nil, cty.MapValEmpty(cty.String), 0, }, { ` b { hello = "world" } `, &BlockAttrsSpec{ TypeName: "b", ElementType: cty.String, }, nil, cty.MapVal(map[string]cty.Value{ "hello": cty.StringVal("world"), }), 0, }, { ` b { hello = true } `, &BlockAttrsSpec{ TypeName: "b", ElementType: cty.String, }, nil, cty.MapVal(map[string]cty.Value{ "hello": cty.StringVal("true"), }), 0, }, { ` b { hello = true goodbye = 5 } `, &BlockAttrsSpec{ TypeName: "b", ElementType: cty.String, }, nil, cty.MapVal(map[string]cty.Value{ "hello": cty.StringVal("true"), "goodbye": cty.StringVal("5"), }), 0, }, { ``, &BlockAttrsSpec{ TypeName: "b", ElementType: cty.String, }, nil, cty.NullVal(cty.Map(cty.String)), 0, }, { ``, &BlockAttrsSpec{ TypeName: "b", ElementType: cty.String, Required: true, }, nil, cty.NullVal(cty.Map(cty.String)), 1, // missing b block }, { ` b { } b { } `, &BlockAttrsSpec{ TypeName: "b", ElementType: cty.String, }, nil, cty.MapValEmpty(cty.String), 1, // duplicate b block }, { ` b { } b { } `, &BlockAttrsSpec{ TypeName: "b", ElementType: cty.String, Required: true, }, nil, cty.MapValEmpty(cty.String), 1, // duplicate b block }, { ` b {} b {} `, &BlockListSpec{ TypeName: "b", Nested: ObjectSpec{}, }, nil, cty.ListVal([]cty.Value{cty.EmptyObjectVal, cty.EmptyObjectVal}), 0, }, { ``, &BlockListSpec{ TypeName: "b", Nested: ObjectSpec{}, }, nil, cty.ListValEmpty(cty.EmptyObject), 0, }, { ` b "foo" {} b "bar" {} `, &BlockListSpec{ TypeName: "b", Nested: &BlockLabelSpec{ Name: "name", Index: 0, }, }, nil, cty.ListVal([]cty.Value{cty.StringVal("foo"), cty.StringVal("bar")}), 0, }, { ` b {} b {} b {} `, &BlockListSpec{ TypeName: "b", Nested: ObjectSpec{}, MaxItems: 2, }, nil, cty.ListVal([]cty.Value{cty.EmptyObjectVal, cty.EmptyObjectVal, cty.EmptyObjectVal}), 1, // too many b blocks }, { ` b {} b {} `, &BlockListSpec{ TypeName: "b", Nested: ObjectSpec{}, MinItems: 10, }, nil, cty.ListVal([]cty.Value{cty.EmptyObjectVal, cty.EmptyObjectVal}), 1, // insufficient b blocks }, { ` b { a = true } b { a = 1 } `, &BlockListSpec{ TypeName: "b", Nested: &AttrSpec{ Name: "a", Type: cty.DynamicPseudoType, }, }, nil, cty.DynamicVal, 1, // Unconsistent argument types in b blocks }, { ` b { a = true } b { a = "not a bool" } `, &BlockListSpec{ TypeName: "b", Nested: &AttrSpec{ Name: "a", Type: cty.DynamicPseudoType, }, }, nil, cty.ListVal([]cty.Value{ cty.StringVal("true"), // type unification generalizes all the values to strings cty.StringVal("not a bool"), }), 0, }, { ` b {} b {} `, &BlockSetSpec{ TypeName: "b", Nested: ObjectSpec{}, MaxItems: 2, }, nil, cty.SetVal([]cty.Value{cty.EmptyObjectVal, cty.EmptyObjectVal}), 0, }, { ` b "foo" "bar" {} b "bar" "baz" {} `, &BlockSetSpec{ TypeName: "b", Nested: TupleSpec{ &BlockLabelSpec{ Name: "name", Index: 1, }, &BlockLabelSpec{ Name: "type", Index: 0, }, }, }, nil, cty.SetVal([]cty.Value{ cty.TupleVal([]cty.Value{cty.StringVal("bar"), cty.StringVal("foo")}), cty.TupleVal([]cty.Value{cty.StringVal("baz"), cty.StringVal("bar")}), }), 0, }, { ` b { a = true } b { a = 1 } `, &BlockSetSpec{ TypeName: "b", Nested: &AttrSpec{ Name: "a", Type: cty.DynamicPseudoType, }, }, nil, cty.DynamicVal, 1, // Unconsistent argument types in b blocks }, { ` b { a = true } b { a = "not a bool" } `, &BlockSetSpec{ TypeName: "b", Nested: &AttrSpec{ Name: "a", Type: cty.DynamicPseudoType, }, }, nil, cty.SetVal([]cty.Value{ cty.StringVal("true"), // type unification generalizes all the values to strings cty.StringVal("not a bool"), }), 0, }, { ` b "foo" {} b "bar" {} `, &BlockMapSpec{ TypeName: "b", LabelNames: []string{"key"}, Nested: ObjectSpec{}, }, nil, cty.MapVal(map[string]cty.Value{"foo": cty.EmptyObjectVal, "bar": cty.EmptyObjectVal}), 0, }, { ` b "foo" "bar" {} b "bar" "baz" {} `, &BlockMapSpec{ TypeName: "b", LabelNames: []string{"key1", "key2"}, Nested: ObjectSpec{}, }, nil, cty.MapVal(map[string]cty.Value{ "foo": cty.MapVal(map[string]cty.Value{ "bar": cty.EmptyObjectVal, }), "bar": cty.MapVal(map[string]cty.Value{ "baz": cty.EmptyObjectVal, }), }), 0, }, { ` b "foo" "bar" {} b "bar" "bar" {} `, &BlockMapSpec{ TypeName: "b", LabelNames: []string{"key1", "key2"}, Nested: ObjectSpec{}, }, nil, cty.MapVal(map[string]cty.Value{ "foo": cty.MapVal(map[string]cty.Value{ "bar": cty.EmptyObjectVal, }), "bar": cty.MapVal(map[string]cty.Value{ "bar": cty.EmptyObjectVal, }), }), 0, }, { ` b "foo" "bar" {} b "foo" "baz" {} `, &BlockMapSpec{ TypeName: "b", LabelNames: []string{"key1", "key2"}, Nested: ObjectSpec{}, }, nil, cty.MapVal(map[string]cty.Value{ "foo": cty.MapVal(map[string]cty.Value{ "bar": cty.EmptyObjectVal, "baz": cty.EmptyObjectVal, }), }), 0, }, { ` b "foo" "bar" {} `, &BlockMapSpec{ TypeName: "b", LabelNames: []string{"key"}, Nested: ObjectSpec{}, }, nil, cty.MapValEmpty(cty.EmptyObject), 1, // too many labels }, { ` b "bar" {} `, &BlockMapSpec{ TypeName: "b", LabelNames: []string{"key1", "key2"}, Nested: ObjectSpec{}, }, nil, cty.MapValEmpty(cty.EmptyObject), 1, // not enough labels }, { ` b "foo" {} b "foo" {} `, &BlockMapSpec{ TypeName: "b", LabelNames: []string{"key"}, Nested: ObjectSpec{}, }, nil, cty.MapVal(map[string]cty.Value{"foo": cty.EmptyObjectVal}), 1, // duplicate b block }, { ` b "foo" "bar" {} b "foo" "bar" {} `, &BlockMapSpec{ TypeName: "b", LabelNames: []string{"key1", "key2"}, Nested: ObjectSpec{}, }, nil, cty.MapVal(map[string]cty.Value{"foo": cty.MapVal(map[string]cty.Value{"bar": cty.EmptyObjectVal})}), 1, // duplicate b block }, { ` b "foo" "bar" {} b "bar" "baz" {} `, &BlockMapSpec{ TypeName: "b", LabelNames: []string{"type"}, Nested: &BlockLabelSpec{ Name: "name", Index: 0, }, }, nil, cty.MapVal(map[string]cty.Value{ "foo": cty.StringVal("bar"), "bar": cty.StringVal("baz"), }), 0, }, { ` b "foo" {} `, &BlockMapSpec{ TypeName: "b", LabelNames: []string{"type"}, Nested: &BlockLabelSpec{ Name: "name", Index: 0, }, }, nil, cty.MapValEmpty(cty.String), 1, // missing name }, { ` b {} b {} `, &BlockTupleSpec{ TypeName: "b", Nested: ObjectSpec{}, }, nil, cty.TupleVal([]cty.Value{cty.EmptyObjectVal, cty.EmptyObjectVal}), 0, }, { ``, &BlockTupleSpec{ TypeName: "b", Nested: ObjectSpec{}, }, nil, cty.EmptyTupleVal, 0, }, { ` b "foo" {} b "bar" {} `, &BlockTupleSpec{ TypeName: "b", Nested: &BlockLabelSpec{ Name: "name", Index: 0, }, }, nil, cty.TupleVal([]cty.Value{cty.StringVal("foo"), cty.StringVal("bar")}), 0, }, { ` b {} b {} b {} `, &BlockTupleSpec{ TypeName: "b", Nested: ObjectSpec{}, MaxItems: 2, }, nil, cty.TupleVal([]cty.Value{cty.EmptyObjectVal, cty.EmptyObjectVal, cty.EmptyObjectVal}), 1, // too many b blocks }, { ` b {} b {} `, &BlockTupleSpec{ TypeName: "b", Nested: ObjectSpec{}, MinItems: 10, }, nil, cty.TupleVal([]cty.Value{cty.EmptyObjectVal, cty.EmptyObjectVal}), 1, // insufficient b blocks }, { ` b { a = true } b { a = 1 } `, &BlockTupleSpec{ TypeName: "b", Nested: &AttrSpec{ Name: "a", Type: cty.DynamicPseudoType, }, }, nil, cty.TupleVal([]cty.Value{ cty.True, cty.NumberIntVal(1), }), 0, }, { ` b { a = true } b { a = "not a bool" } `, &BlockTupleSpec{ TypeName: "b", Nested: &AttrSpec{ Name: "a", Type: cty.DynamicPseudoType, }, }, nil, cty.TupleVal([]cty.Value{ cty.True, cty.StringVal("not a bool"), }), 0, }, { ` b "foo" {} b "bar" {} `, &BlockObjectSpec{ TypeName: "b", LabelNames: []string{"key"}, Nested: ObjectSpec{}, }, nil, cty.ObjectVal(map[string]cty.Value{"foo": cty.EmptyObjectVal, "bar": cty.EmptyObjectVal}), 0, }, { ` b "foo" "bar" {} b "bar" "baz" {} `, &BlockObjectSpec{ TypeName: "b", LabelNames: []string{"key1", "key2"}, Nested: ObjectSpec{}, }, nil, cty.ObjectVal(map[string]cty.Value{ "foo": cty.ObjectVal(map[string]cty.Value{ "bar": cty.EmptyObjectVal, }), "bar": cty.ObjectVal(map[string]cty.Value{ "baz": cty.EmptyObjectVal, }), }), 0, }, { ` b "foo" "bar" {} b "bar" "bar" {} `, &BlockObjectSpec{ TypeName: "b", LabelNames: []string{"key1", "key2"}, Nested: ObjectSpec{}, }, nil, cty.ObjectVal(map[string]cty.Value{ "foo": cty.ObjectVal(map[string]cty.Value{ "bar": cty.EmptyObjectVal, }), "bar": cty.ObjectVal(map[string]cty.Value{ "bar": cty.EmptyObjectVal, }), }), 0, }, { ` b "foo" "bar" {} b "foo" "baz" {} `, &BlockObjectSpec{ TypeName: "b", LabelNames: []string{"key1", "key2"}, Nested: ObjectSpec{}, }, nil, cty.ObjectVal(map[string]cty.Value{ "foo": cty.ObjectVal(map[string]cty.Value{ "bar": cty.EmptyObjectVal, "baz": cty.EmptyObjectVal, }), }), 0, }, { ` b "foo" "bar" {} `, &BlockObjectSpec{ TypeName: "b", LabelNames: []string{"key"}, Nested: ObjectSpec{}, }, nil, cty.EmptyObjectVal, 1, // too many labels }, { ` b "bar" {} `, &BlockObjectSpec{ TypeName: "b", LabelNames: []string{"key1", "key2"}, Nested: ObjectSpec{}, }, nil, cty.EmptyObjectVal, 1, // not enough labels }, { ` b "foo" {} b "foo" {} `, &BlockObjectSpec{ TypeName: "b", LabelNames: []string{"key"}, Nested: ObjectSpec{}, }, nil, cty.ObjectVal(map[string]cty.Value{"foo": cty.EmptyObjectVal}), 1, // duplicate b block }, { ` b "foo" "bar" {} b "foo" "bar" {} `, &BlockObjectSpec{ TypeName: "b", LabelNames: []string{"key1", "key2"}, Nested: ObjectSpec{}, }, nil, cty.ObjectVal(map[string]cty.Value{"foo": cty.ObjectVal(map[string]cty.Value{"bar": cty.EmptyObjectVal})}), 1, // duplicate b block }, { ` b "foo" "bar" {} b "bar" "baz" {} `, &BlockObjectSpec{ TypeName: "b", LabelNames: []string{"type"}, Nested: &BlockLabelSpec{ Name: "name", Index: 0, }, }, nil, cty.ObjectVal(map[string]cty.Value{ "foo": cty.StringVal("bar"), "bar": cty.StringVal("baz"), }), 0, }, { ` b "foo" {} `, &BlockObjectSpec{ TypeName: "b", LabelNames: []string{"type"}, Nested: &BlockLabelSpec{ Name: "name", Index: 0, }, }, nil, cty.EmptyObjectVal, 1, // missing name }, { ` b "foo" { arg = true } b "bar" { arg = 1 } `, &BlockObjectSpec{ TypeName: "b", LabelNames: []string{"type"}, Nested: &AttrSpec{ Name: "arg", Type: cty.DynamicPseudoType, }, }, nil, cty.ObjectVal(map[string]cty.Value{ "foo": cty.True, "bar": cty.NumberIntVal(1), }), 0, }, } for i, test := range tests { t.Run(fmt.Sprintf("%02d-%s", i, test.config), func(t *testing.T) { file, parseDiags := hclsyntax.ParseConfig([]byte(test.config), "", hcl.Pos{Line: 1, Column: 1, Byte: 0}) body := file.Body got, valDiags := Decode(body, test.spec, test.ctx) var diags hcl.Diagnostics diags = append(diags, parseDiags...) diags = append(diags, valDiags...) 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()) } } if !got.RawEquals(test.want) { t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.want) } }) } } func TestSourceRange(t *testing.T) { tests := []struct { config string spec Spec want hcl.Range }{ { "a = 1\n", &AttrSpec{ Name: "a", }, hcl.Range{ Start: hcl.Pos{Line: 1, Column: 5, Byte: 4}, End: hcl.Pos{Line: 1, Column: 6, Byte: 5}, }, }, { ` b { a = 1 } `, &BlockSpec{ TypeName: "b", Nested: &AttrSpec{ Name: "a", }, }, hcl.Range{ Start: hcl.Pos{Line: 3, Column: 7, Byte: 11}, End: hcl.Pos{Line: 3, Column: 8, Byte: 12}, }, }, { ` b { c { a = 1 } } `, &BlockSpec{ TypeName: "b", Nested: &BlockSpec{ TypeName: "c", Nested: &AttrSpec{ Name: "a", }, }, }, hcl.Range{ Start: hcl.Pos{Line: 4, Column: 9, Byte: 19}, End: hcl.Pos{Line: 4, Column: 10, Byte: 20}, }, }, } for i, test := range tests { t.Run(fmt.Sprintf("%02d-%s", i, test.config), func(t *testing.T) { file, diags := hclsyntax.ParseConfig([]byte(test.config), "", hcl.Pos{Line: 1, Column: 1, Byte: 0}) if len(diags) != 0 { t.Errorf("wrong number of diagnostics %d; want %d", len(diags), 0) for _, diag := range diags { t.Logf(" - %s", diag.Error()) } } body := file.Body got := SourceRange(body, test.spec) if !reflect.DeepEqual(got, test.want) { t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.want) } }) } }