//go:build go1.18 // +build go1.18 package swag import ( "encoding/json" "fmt" "go/ast" "os" "path/filepath" "testing" "github.com/stretchr/testify/assert" ) type testLogger struct { Messages []string } func (t *testLogger) Printf(format string, v ...interface{}) { t.Messages = append(t.Messages, fmt.Sprintf(format, v...)) } func TestParseGenericsBasic(t *testing.T) { t.Parallel() searchDir := "testdata/generics_basic" expected, err := os.ReadFile(filepath.Join(searchDir, "expected.json")) assert.NoError(t, err) p := New() p.Overrides = map[string]string{ "types.Field[string]": "string", "types.DoubleField[string,string]": "[]string", "types.TrippleField[string,string]": "[][]string", } err = p.ParseAPI(searchDir, mainAPIFile, defaultParseDepth) assert.NoError(t, err) b, err := json.MarshalIndent(p.swagger, "", " ") assert.NoError(t, err) assert.Equal(t, string(expected), string(b)) } func TestParseGenericsArrays(t *testing.T) { t.Parallel() searchDir := "testdata/generics_arrays" expected, err := os.ReadFile(filepath.Join(searchDir, "expected.json")) assert.NoError(t, err) p := New() err = p.ParseAPI(searchDir, mainAPIFile, defaultParseDepth) assert.NoError(t, err) b, err := json.MarshalIndent(p.swagger, "", " ") assert.NoError(t, err) assert.Equal(t, string(expected), string(b)) } func TestParseGenericsNested(t *testing.T) { t.Parallel() searchDir := "testdata/generics_nested" expected, err := os.ReadFile(filepath.Join(searchDir, "expected.json")) assert.NoError(t, err) p := New() err = p.ParseAPI(searchDir, mainAPIFile, defaultParseDepth) assert.NoError(t, err) b, err := json.MarshalIndent(p.swagger, "", " ") assert.NoError(t, err) assert.Equal(t, string(expected), string(b)) } func TestParseGenericsMultiLevelNesting(t *testing.T) { t.Parallel() searchDir := "testdata/generics_multi_level_nesting" expected, err := os.ReadFile(filepath.Join(searchDir, "expected.json")) assert.NoError(t, err) p := New() err = p.ParseAPI(searchDir, mainAPIFile, defaultParseDepth) assert.NoError(t, err) b, err := json.MarshalIndent(p.swagger, "", " ") assert.NoError(t, err) assert.Equal(t, string(expected), string(b)) } func TestParseGenericsProperty(t *testing.T) { t.Parallel() searchDir := "testdata/generics_property" expected, err := os.ReadFile(filepath.Join(searchDir, "expected.json")) assert.NoError(t, err) p := New() err = p.ParseAPI(searchDir, mainAPIFile, defaultParseDepth) assert.NoError(t, err) b, err := json.MarshalIndent(p.swagger, "", " ") assert.NoError(t, err) assert.Equal(t, string(expected), string(b)) } func TestParseGenericsNames(t *testing.T) { t.Parallel() searchDir := "testdata/generics_names" expected, err := os.ReadFile(filepath.Join(searchDir, "expected.json")) assert.NoError(t, err) p := New() err = p.ParseAPI(searchDir, mainAPIFile, defaultParseDepth) assert.NoError(t, err) b, err := json.MarshalIndent(p.swagger, "", " ") assert.NoError(t, err) assert.Equal(t, string(expected), string(b)) } func TestParseGenericsPackageAlias(t *testing.T) { t.Parallel() searchDir := "testdata/generics_package_alias/internal" expected, err := os.ReadFile(filepath.Join(searchDir, "expected.json")) assert.NoError(t, err) p := New(SetParseDependency(1)) err = p.ParseAPI(searchDir, mainAPIFile, defaultParseDepth) assert.NoError(t, err) b, err := json.MarshalIndent(p.swagger, "", " ") assert.NoError(t, err) assert.Equal(t, string(expected), string(b)) } func TestParseGenericsFunctionScoped(t *testing.T) { t.Parallel() searchDir := "testdata/generics_function_scoped" expected, err := os.ReadFile(filepath.Join(searchDir, "expected.json")) assert.NoError(t, err) p := New() err = p.ParseAPI(searchDir, mainAPIFile, defaultParseDepth) assert.NoError(t, err) b, err := json.MarshalIndent(p.swagger, "", " ") assert.NoError(t, err) assert.Equal(t, string(expected), string(b)) } func TestParametrizeStruct(t *testing.T) { pd := PackagesDefinitions{ packages: make(map[string]*PackageDefinitions), uniqueDefinitions: make(map[string]*TypeSpecDef), } // valid typeSpec := pd.parametrizeGenericType( &ast.File{Name: &ast.Ident{Name: "test2"}}, &TypeSpecDef{ File: &ast.File{Name: &ast.Ident{Name: "test"}}, TypeSpec: &ast.TypeSpec{ Name: &ast.Ident{Name: "Field"}, TypeParams: &ast.FieldList{List: []*ast.Field{{Names: []*ast.Ident{{Name: "T"}}}, {Names: []*ast.Ident{{Name: "T2"}}}}}, Type: &ast.StructType{Struct: 100, Fields: &ast.FieldList{Opening: 101, Closing: 102}}, }, }, "test.Field[string, []string]") assert.NotNil(t, typeSpec) assert.Equal(t, "$test.Field-string-array_string", typeSpec.Name()) assert.Equal(t, "test.Field-string-array_string", typeSpec.TypeName()) // definition contains one type params, but two type params are provided typeSpec = pd.parametrizeGenericType( &ast.File{Name: &ast.Ident{Name: "test2"}}, &TypeSpecDef{ TypeSpec: &ast.TypeSpec{ Name: &ast.Ident{Name: "Field"}, TypeParams: &ast.FieldList{List: []*ast.Field{{Names: []*ast.Ident{{Name: "T"}}}}}, Type: &ast.StructType{Struct: 100, Fields: &ast.FieldList{Opening: 101, Closing: 102}}, }, }, "test.Field[string, string]") assert.Nil(t, typeSpec) // definition contains two type params, but only one is used typeSpec = pd.parametrizeGenericType( &ast.File{Name: &ast.Ident{Name: "test2"}}, &TypeSpecDef{ TypeSpec: &ast.TypeSpec{ Name: &ast.Ident{Name: "Field"}, TypeParams: &ast.FieldList{List: []*ast.Field{{Names: []*ast.Ident{{Name: "T"}}}, {Names: []*ast.Ident{{Name: "T2"}}}}}, Type: &ast.StructType{Struct: 100, Fields: &ast.FieldList{Opening: 101, Closing: 102}}, }, }, "test.Field[string]") assert.Nil(t, typeSpec) // name is not a valid type name typeSpec = pd.parametrizeGenericType( &ast.File{Name: &ast.Ident{Name: "test2"}}, &TypeSpecDef{ TypeSpec: &ast.TypeSpec{ Name: &ast.Ident{Name: "Field"}, TypeParams: &ast.FieldList{List: []*ast.Field{{Names: []*ast.Ident{{Name: "T"}}}, {Names: []*ast.Ident{{Name: "T2"}}}}}, Type: &ast.StructType{Struct: 100, Fields: &ast.FieldList{Opening: 101, Closing: 102}}, }, }, "test.Field[string") assert.Nil(t, typeSpec) typeSpec = pd.parametrizeGenericType( &ast.File{Name: &ast.Ident{Name: "test2"}}, &TypeSpecDef{ TypeSpec: &ast.TypeSpec{ Name: &ast.Ident{Name: "Field"}, TypeParams: &ast.FieldList{List: []*ast.Field{{Names: []*ast.Ident{{Name: "T"}}}, {Names: []*ast.Ident{{Name: "T2"}}}}}, Type: &ast.StructType{Struct: 100, Fields: &ast.FieldList{Opening: 101, Closing: 102}}, }, }, "test.Field[string, [string]") assert.Nil(t, typeSpec) typeSpec = pd.parametrizeGenericType( &ast.File{Name: &ast.Ident{Name: "test2"}}, &TypeSpecDef{ TypeSpec: &ast.TypeSpec{ Name: &ast.Ident{Name: "Field"}, TypeParams: &ast.FieldList{List: []*ast.Field{{Names: []*ast.Ident{{Name: "T"}}}, {Names: []*ast.Ident{{Name: "T2"}}}}}, Type: &ast.StructType{Struct: 100, Fields: &ast.FieldList{Opening: 101, Closing: 102}}, }, }, "test.Field[string, ]string]") assert.Nil(t, typeSpec) } func TestSplitGenericsTypeNames(t *testing.T) { t.Parallel() field, params := splitGenericsTypeName("test.Field") assert.Empty(t, field) assert.Nil(t, params) field, params = splitGenericsTypeName("test.Field]") assert.Empty(t, field) assert.Nil(t, params) field, params = splitGenericsTypeName("test.Field[string") assert.Empty(t, field) assert.Nil(t, params) field, params = splitGenericsTypeName("test.Field[string] ") assert.Equal(t, "test.Field", field) assert.Equal(t, []string{"string"}, params) field, params = splitGenericsTypeName("test.Field[string, []string]") assert.Equal(t, "test.Field", field) assert.Equal(t, []string{"string", "[]string"}, params) field, params = splitGenericsTypeName("test.Field[test.Field[ string, []string] ]") assert.Equal(t, "test.Field", field) assert.Equal(t, []string{"test.Field[string,[]string]"}, params) } func TestGetGenericFieldType(t *testing.T) { field, err := getFieldType( &ast.File{Name: &ast.Ident{Name: "test"}}, &ast.IndexListExpr{ X: &ast.Ident{Name: "types", Obj: &ast.Object{Decl: &ast.TypeSpec{Name: &ast.Ident{Name: "Field"}}}}, Indices: []ast.Expr{&ast.Ident{Name: "string"}}, }, nil, ) assert.NoError(t, err) assert.Equal(t, "test.Field[string]", field) field, err = getFieldType( &ast.File{Name: &ast.Ident{}}, &ast.IndexListExpr{ X: &ast.Ident{Name: "types", Obj: &ast.Object{Decl: &ast.TypeSpec{Name: &ast.Ident{Name: "Field"}}}}, Indices: []ast.Expr{&ast.Ident{Name: "string"}}, }, nil, ) assert.NoError(t, err) assert.Equal(t, "Field[string]", field) field, err = getFieldType( &ast.File{Name: &ast.Ident{Name: "test"}}, &ast.IndexListExpr{ X: &ast.Ident{Name: "types", Obj: &ast.Object{Decl: &ast.TypeSpec{Name: &ast.Ident{Name: "Field"}}}}, Indices: []ast.Expr{&ast.Ident{Name: "string"}, &ast.Ident{Name: "int"}}, }, nil, ) assert.NoError(t, err) assert.Equal(t, "test.Field[string,int]", field) field, err = getFieldType( &ast.File{Name: &ast.Ident{Name: "test"}}, &ast.IndexListExpr{ X: &ast.Ident{Name: "types", Obj: &ast.Object{Decl: &ast.TypeSpec{Name: &ast.Ident{Name: "Field"}}}}, Indices: []ast.Expr{&ast.Ident{Name: "string"}, &ast.ArrayType{Elt: &ast.Ident{Name: "int"}}}, }, nil, ) assert.NoError(t, err) assert.Equal(t, "test.Field[string,[]int]", field) field, err = getFieldType( &ast.File{Name: &ast.Ident{Name: "test"}}, &ast.IndexListExpr{ X: &ast.BadExpr{}, Indices: []ast.Expr{&ast.Ident{Name: "string"}, &ast.Ident{Name: "int"}}, }, nil, ) assert.Error(t, err) field, err = getFieldType( &ast.File{Name: &ast.Ident{Name: "test"}}, &ast.IndexListExpr{ X: &ast.Ident{Name: "types", Obj: &ast.Object{Decl: &ast.TypeSpec{Name: &ast.Ident{Name: "Field"}}}}, Indices: []ast.Expr{&ast.Ident{Name: "string"}, &ast.ArrayType{Elt: &ast.BadExpr{}}}, }, nil, ) assert.Error(t, err) field, err = getFieldType( &ast.File{Name: &ast.Ident{Name: "test"}}, &ast.IndexExpr{X: &ast.Ident{Name: "Field"}, Index: &ast.Ident{Name: "string"}}, nil, ) assert.NoError(t, err) assert.Equal(t, "test.Field[string]", field) field, err = getFieldType( &ast.File{Name: nil}, &ast.IndexExpr{X: &ast.Ident{Name: "Field"}, Index: &ast.Ident{Name: "string"}}, nil, ) assert.Error(t, err) field, err = getFieldType( &ast.File{Name: &ast.Ident{Name: "test"}}, &ast.IndexExpr{X: &ast.BadExpr{}, Index: &ast.Ident{Name: "string"}}, nil, ) assert.Error(t, err) field, err = getFieldType( &ast.File{Name: &ast.Ident{Name: "test"}}, &ast.IndexExpr{X: &ast.Ident{Name: "Field"}, Index: &ast.BadExpr{}}, nil, ) assert.Error(t, err) field, err = getFieldType( &ast.File{Name: &ast.Ident{Name: "test"}}, &ast.IndexExpr{X: &ast.SelectorExpr{X: &ast.Ident{Name: "field"}, Sel: &ast.Ident{Name: "Name"}}, Index: &ast.Ident{Name: "string"}}, nil, ) assert.NoError(t, err) assert.Equal(t, "field.Name[string]", field) } func TestGetGenericTypeName(t *testing.T) { field, err := getGenericTypeName( &ast.File{Name: &ast.Ident{Name: "test"}}, &ast.Ident{Name: "types", Obj: &ast.Object{Decl: &ast.TypeSpec{Name: &ast.Ident{Name: "Field"}}}}, ) assert.NoError(t, err) assert.Equal(t, "test.Field", field) field, err = getGenericTypeName( &ast.File{Name: &ast.Ident{Name: "test"}}, &ast.ArrayType{Elt: &ast.Ident{Name: "types", Obj: &ast.Object{Decl: &ast.TypeSpec{Name: &ast.Ident{Name: "Field"}}}}}, ) assert.NoError(t, err) assert.Equal(t, "test.Field", field) field, err = getGenericTypeName( &ast.File{Name: &ast.Ident{Name: "test"}}, &ast.SelectorExpr{X: &ast.Ident{Name: "field"}, Sel: &ast.Ident{Name: "Name"}}, ) assert.NoError(t, err) assert.Equal(t, "field.Name", field) _, err = getGenericTypeName( &ast.File{Name: &ast.Ident{Name: "test"}}, &ast.BadExpr{}, ) assert.Error(t, err) } func TestParseGenericTypeExpr(t *testing.T) { t.Parallel() parser := New() logger := &testLogger{} SetDebugger(logger)(parser) _, _ = parser.parseGenericTypeExpr(&ast.File{}, &ast.InterfaceType{}) assert.Empty(t, logger.Messages) _, _ = parser.parseGenericTypeExpr(&ast.File{}, &ast.StructType{}) assert.Empty(t, logger.Messages) _, _ = parser.parseGenericTypeExpr(&ast.File{}, &ast.Ident{}) assert.Empty(t, logger.Messages) _, _ = parser.parseGenericTypeExpr(&ast.File{}, &ast.StarExpr{}) assert.Empty(t, logger.Messages) _, _ = parser.parseGenericTypeExpr(&ast.File{}, &ast.SelectorExpr{}) assert.Empty(t, logger.Messages) _, _ = parser.parseGenericTypeExpr(&ast.File{}, &ast.ArrayType{}) assert.Empty(t, logger.Messages) _, _ = parser.parseGenericTypeExpr(&ast.File{}, &ast.MapType{}) assert.Empty(t, logger.Messages) _, _ = parser.parseGenericTypeExpr(&ast.File{}, &ast.FuncType{}) assert.Empty(t, logger.Messages) _, _ = parser.parseGenericTypeExpr(&ast.File{}, &ast.BadExpr{}) assert.NotEmpty(t, logger.Messages) assert.Len(t, logger.Messages, 1) parser.packages.uniqueDefinitions["field.Name[string]"] = &TypeSpecDef{ File: &ast.File{Name: &ast.Ident{Name: "test"}}, TypeSpec: &ast.TypeSpec{ Name: &ast.Ident{Name: "Field"}, TypeParams: &ast.FieldList{List: []*ast.Field{{Names: []*ast.Ident{{Name: "T"}}}}}, Type: &ast.StructType{Struct: 100, Fields: &ast.FieldList{Opening: 101, Closing: 102}}, }, } spec, err := parser.parseTypeExpr( &ast.File{Name: &ast.Ident{Name: "test"}}, &ast.IndexExpr{X: &ast.SelectorExpr{X: &ast.Ident{Name: "field"}, Sel: &ast.Ident{Name: "Name"}}, Index: &ast.Ident{Name: "string"}}, false, ) assert.NotNil(t, spec) assert.NoError(t, err) logger.Messages = []string{} spec, err = parser.parseTypeExpr( &ast.File{Name: &ast.Ident{Name: "test"}}, &ast.IndexExpr{X: &ast.BadExpr{}, Index: &ast.Ident{Name: "string"}}, false, ) assert.NotNil(t, spec) assert.Equal(t, "object", spec.SchemaProps.Type[0]) assert.NotEmpty(t, logger.Messages) assert.Len(t, logger.Messages, 1) logger.Messages = []string{} spec, err = parser.parseTypeExpr( &ast.File{Name: &ast.Ident{Name: "test"}}, &ast.BadExpr{}, false, ) assert.NotNil(t, spec) assert.Equal(t, "object", spec.SchemaProps.Type[0]) assert.NotEmpty(t, logger.Messages) assert.Len(t, logger.Messages, 1) }