package typeexpr import ( "testing" "github.com/hashicorp/hcl/v2/gohcl" "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hclsyntax" "github.com/hashicorp/hcl/v2/json" "github.com/zclconf/go-cty/cty" ) func TestGetType(t *testing.T) { tests := []struct { Source string Constraint bool Want cty.Type WantError string }{ // keywords { `bool`, false, cty.Bool, "", }, { `number`, false, cty.Number, "", }, { `string`, false, cty.String, "", }, { `any`, false, cty.DynamicPseudoType, `The keyword "any" cannot be used in this type specification: an exact type is required.`, }, { `any`, true, cty.DynamicPseudoType, "", }, { `list`, false, cty.DynamicPseudoType, "The list type constructor requires one argument specifying the element type.", }, { `map`, false, cty.DynamicPseudoType, "The map type constructor requires one argument specifying the element type.", }, { `set`, false, cty.DynamicPseudoType, "The set type constructor requires one argument specifying the element type.", }, { `object`, false, cty.DynamicPseudoType, "The object type constructor requires one argument specifying the attribute types and values as a map.", }, { `tuple`, false, cty.DynamicPseudoType, "The tuple type constructor requires one argument specifying the element types as a list.", }, // constructors { `bool()`, false, cty.DynamicPseudoType, `Primitive type keyword "bool" does not expect arguments.`, }, { `number()`, false, cty.DynamicPseudoType, `Primitive type keyword "number" does not expect arguments.`, }, { `string()`, false, cty.DynamicPseudoType, `Primitive type keyword "string" does not expect arguments.`, }, { `any()`, false, cty.DynamicPseudoType, `Primitive type keyword "any" does not expect arguments.`, }, { `any()`, true, cty.DynamicPseudoType, `Primitive type keyword "any" does not expect arguments.`, }, { `list(string)`, false, cty.List(cty.String), ``, }, { `set(string)`, false, cty.Set(cty.String), ``, }, { `map(string)`, false, cty.Map(cty.String), ``, }, { `list()`, false, cty.DynamicPseudoType, `The list type constructor requires one argument specifying the element type.`, }, { `list(string, string)`, false, cty.DynamicPseudoType, `The list type constructor requires one argument specifying the element type.`, }, { `list(any)`, false, cty.List(cty.DynamicPseudoType), `The keyword "any" cannot be used in this type specification: an exact type is required.`, }, { `list(any)`, true, cty.List(cty.DynamicPseudoType), ``, }, { `object({})`, false, cty.EmptyObject, ``, }, { `object({name=string})`, false, cty.Object(map[string]cty.Type{"name": cty.String}), ``, }, { `object({"name"=string})`, false, cty.EmptyObject, `Object constructor map keys must be attribute names.`, }, { `object({name=nope})`, false, cty.Object(map[string]cty.Type{"name": cty.DynamicPseudoType}), `The keyword "nope" is not a valid type specification.`, }, { `object()`, false, cty.DynamicPseudoType, `The object type constructor requires one argument specifying the attribute types and values as a map.`, }, { `object(string)`, false, cty.DynamicPseudoType, `Object type constructor requires a map whose keys are attribute names and whose values are the corresponding attribute types.`, }, { `tuple([])`, false, cty.EmptyTuple, ``, }, { `tuple([string, bool])`, false, cty.Tuple([]cty.Type{cty.String, cty.Bool}), ``, }, { `tuple([nope])`, false, cty.Tuple([]cty.Type{cty.DynamicPseudoType}), `The keyword "nope" is not a valid type specification.`, }, { `tuple()`, false, cty.DynamicPseudoType, `The tuple type constructor requires one argument specifying the element types as a list.`, }, { `tuple(string)`, false, cty.DynamicPseudoType, `Tuple type constructor requires a list of element types.`, }, { `shwoop(string)`, false, cty.DynamicPseudoType, `Keyword "shwoop" is not a valid type constructor.`, }, { `list("string")`, false, cty.List(cty.DynamicPseudoType), `A type specification is either a primitive type keyword (bool, number, string) or a complex type constructor call, like list(string).`, }, // More interesting combinations { `list(object({}))`, false, cty.List(cty.EmptyObject), ``, }, { `list(map(tuple([])))`, false, cty.List(cty.Map(cty.EmptyTuple)), ``, }, } for _, test := range tests { t.Run(test.Source, func(t *testing.T) { expr, diags := hclsyntax.ParseExpression([]byte(test.Source), "", hcl.Pos{Line: 1, Column: 1}) if diags.HasErrors() { t.Fatalf("failed to parse: %s", diags) } got, diags := getType(expr, test.Constraint) if test.WantError == "" { for _, diag := range diags { t.Error(diag) } } else { found := false for _, diag := range diags { t.Log(diag) if diag.Severity == hcl.DiagError && diag.Detail == test.WantError { found = true } } if !found { t.Errorf("missing expected error detail message: %s", test.WantError) } } if !got.Equals(test.Want) { t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.Want) } }) } } func TestGetTypeJSON(t *testing.T) { // We have fewer test cases here because we're mainly exercising the // extra indirection in the JSON syntax package, which ultimately calls // into the native syntax parser (which we tested extensively in // TestGetType). tests := []struct { Source string Constraint bool Want cty.Type WantError string }{ { `{"expr":"bool"}`, false, cty.Bool, "", }, { `{"expr":"list(bool)"}`, false, cty.List(cty.Bool), "", }, { `{"expr":"list"}`, false, cty.DynamicPseudoType, "The list type constructor requires one argument specifying the element type.", }, } for _, test := range tests { t.Run(test.Source, func(t *testing.T) { file, diags := json.Parse([]byte(test.Source), "") if diags.HasErrors() { t.Fatalf("failed to parse: %s", diags) } type TestContent struct { Expr hcl.Expression `hcl:"expr"` } var content TestContent diags = gohcl.DecodeBody(file.Body, nil, &content) if diags.HasErrors() { t.Fatalf("failed to decode: %s", diags) } got, diags := getType(content.Expr, test.Constraint) if test.WantError == "" { for _, diag := range diags { t.Error(diag) } } else { found := false for _, diag := range diags { t.Log(diag) if diag.Severity == hcl.DiagError && diag.Detail == test.WantError { found = true } } if !found { t.Errorf("missing expected error detail message: %s", test.WantError) } } if !got.Equals(test.Want) { t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.Want) } }) } }