diff --git a/tools/goctl/api/gogen/jwt.api b/tools/goctl/api/gogen/jwt.api deleted file mode 100755 index a8763c0d4293..000000000000 --- a/tools/goctl/api/gogen/jwt.api +++ /dev/null @@ -1,17 +0,0 @@ -type Request { - Name string `path:"name,options=you|me"` -} - -type Response { - Message string `json:"message"` -} - -@server( - jwt: Auth - jwtTransition: Trans - middleware: TokenValidate -) -service A-api { - @handler GreetHandler - get /greet/from/:name(Request) returns (Response) -} diff --git a/tools/goctl/api/tsgen/gen_test.go b/tools/goctl/api/tsgen/gen_test.go new file mode 100644 index 000000000000..b069253a239a --- /dev/null +++ b/tools/goctl/api/tsgen/gen_test.go @@ -0,0 +1,163 @@ +package tsgen + +import ( + "os" + "path/filepath" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/zeromicro/go-zero/tools/goctl/api/parser" +) + +func TestGenWithInlineStructs(t *testing.T) { + // Create a temporary directory for the test + tmpDir := t.TempDir() + apiFile := filepath.Join(tmpDir, "test.api") + + // Write the test API file + apiContent := `syntax = "v1" + +info ( + title: "Test ts generator" + desc: "Test inline struct handling" + author: "test" + version: "v1" +) + +// common pagination request +type PaginationReq { + PageNum int ` + "`form:\"pageNum\"`" + ` + PageSize int ` + "`form:\"pageSize\"`" + ` +} + +// base response +type BaseResp { + Code int64 ` + "`json:\"code\"`" + ` + Msg string ` + "`json:\"msg\"`" + ` +} + +// common req +type GetListCommonReq { + Sth string ` + "`form:\"sth\"`" + ` + PageNum int ` + "`form:\"pageNum\"`" + ` + PageSize int ` + "`form:\"pageSize\"`" + ` +} + +// bad req to ts - inline struct with form tags +type GetListBadReq { + Sth string ` + "`form:\"sth\"`" + ` + PaginationReq +} + +// bad req to ts 2 - only inline struct with form tags +type GetListBad2Req { + PaginationReq +} + +// GetListResp - inline struct with json tags +type GetListResp { + BaseResp +} + +service test-api { + @doc "common req" + @handler getListCommon + get /getListCommon (GetListCommonReq) returns (GetListResp) + + @doc "bad req" + @handler getListBad + get /getListBad (GetListBadReq) returns (GetListResp) + + @doc "bad req 2" + @handler getListBad2 + get /getListBad2 (GetListBad2Req) returns (GetListResp) + + @doc "no req" + @handler getListNoReq + get /getListNoReq returns (GetListResp) +}` + + err := os.WriteFile(apiFile, []byte(apiContent), 0644) + assert.NoError(t, err) + + // Parse the API file + api, err := parser.Parse(apiFile) + assert.NoError(t, err) + + // Generate TypeScript files + outputDir := filepath.Join(tmpDir, "output") + err = os.MkdirAll(outputDir, 0755) + assert.NoError(t, err) + + // Generate the files directly + api.Service = api.Service.JoinPrefix() + err = genRequest(outputDir) + assert.NoError(t, err) + err = genHandler(outputDir, ".", "webapi", api, false) + assert.NoError(t, err) + err = genComponents(outputDir, api) + assert.NoError(t, err) + + // Read generated handler file + handlerFile := filepath.Join(outputDir, "test.ts") + handlerContent, err := os.ReadFile(handlerFile) + assert.NoError(t, err) + handler := string(handlerContent) + + // Read generated components file + componentsFile := filepath.Join(outputDir, "testComponents.ts") + componentsContent, err := os.ReadFile(componentsFile) + assert.NoError(t, err) + components := string(componentsContent) + + // Verify getListBad function signature and call + assert.Contains(t, handler, "export function getListBad(params: components.GetListBadReqParams)") + assert.Contains(t, handler, "return webapi.get(`/getListBad`, params)") + // Should NOT contain 4 arguments + assert.NotContains(t, handler, "getListBad`, params, req, headers") + + // Verify getListBad2 function signature and call + assert.Contains(t, handler, "export function getListBad2(params: components.GetListBad2ReqParams)") + assert.Contains(t, handler, "return webapi.get(`/getListBad2`, params)") + // Should NOT reference non-existent headers + assert.NotContains(t, handler, "GetListBad2ReqHeaders") + + // Verify getListCommon function signature and call + assert.Contains(t, handler, "export function getListCommon(params: components.GetListCommonReqParams)") + assert.Contains(t, handler, "return webapi.get(`/getListCommon`, params)") + + // Verify getListNoReq function signature and call + assert.Contains(t, handler, "export function getListNoReq()") + assert.Contains(t, handler, "return webapi.get(`/getListNoReq`)") + + // Verify GetListBadReqParams contains flattened fields + assert.Contains(t, components, "export interface GetListBadReqParams") + // Count occurrences of fields in GetListBadReqParams + paramsStart := strings.Index(components, "export interface GetListBadReqParams") + paramsEnd := strings.Index(components[paramsStart:], "}") + paramsSection := components[paramsStart : paramsStart+paramsEnd] + assert.Contains(t, paramsSection, "sth: string") + assert.Contains(t, paramsSection, "pageNum: number") + assert.Contains(t, paramsSection, "pageSize: number") + + // Verify GetListBad2ReqParams contains flattened fields from inline PaginationReq + assert.Contains(t, components, "export interface GetListBad2ReqParams") + params2Start := strings.Index(components, "export interface GetListBad2ReqParams") + params2End := strings.Index(components[params2Start:], "}") + params2Section := components[params2Start : params2Start+params2End] + assert.Contains(t, params2Section, "pageNum: number") + assert.Contains(t, params2Section, "pageSize: number") + + // Verify no empty Headers interfaces are generated + assert.NotContains(t, components, "GetListBadReqHeaders") + assert.NotContains(t, components, "GetListBad2ReqHeaders") + + // Verify GetListResp contains flattened fields from BaseResp + assert.Contains(t, components, "export interface GetListResp") + respStart := strings.Index(components, "export interface GetListResp") + respEnd := strings.Index(components[respStart:], "}") + respSection := components[respStart : respStart+respEnd] + assert.Contains(t, respSection, "code: number") + assert.Contains(t, respSection, "msg: string") +} diff --git a/tools/goctl/api/tsgen/genpacket.go b/tools/goctl/api/tsgen/genpacket.go index 6635220a3c56..a713ac7ccaef 100644 --- a/tools/goctl/api/tsgen/genpacket.go +++ b/tools/goctl/api/tsgen/genpacket.go @@ -212,7 +212,7 @@ func pathHasParams(route spec.Route) bool { return false } - return len(ds.Members) != len(ds.GetBodyMembers()) + return hasActualNonBodyMembers(ds) } func hasRequestBody(route spec.Route) bool { @@ -221,7 +221,7 @@ func hasRequestBody(route spec.Route) bool { return false } - return len(route.RequestTypeName()) > 0 && len(ds.GetBodyMembers()) > 0 + return len(route.RequestTypeName()) > 0 && hasActualBodyMembers(ds) } func hasRequestPath(route spec.Route) bool { @@ -230,7 +230,7 @@ func hasRequestPath(route spec.Route) bool { return false } - return len(route.RequestTypeName()) > 0 && len(ds.GetTagMembers(pathTagKey)) > 0 + return len(route.RequestTypeName()) > 0 && hasActualTagMembers(ds, pathTagKey) } func hasRequestHeader(route spec.Route) bool { @@ -239,5 +239,5 @@ func hasRequestHeader(route spec.Route) bool { return false } - return len(route.RequestTypeName()) > 0 && len(ds.GetTagMembers(headerTagKey)) > 0 + return len(route.RequestTypeName()) > 0 && hasActualTagMembers(ds, headerTagKey) } diff --git a/tools/goctl/api/tsgen/util.go b/tools/goctl/api/tsgen/util.go index df6e95c4c979..0ab6872bb16a 100644 --- a/tools/goctl/api/tsgen/util.go +++ b/tools/goctl/api/tsgen/util.go @@ -164,13 +164,13 @@ func writeType(writer io.Writer, tp spec.Type) error { } func genParamsTypesIfNeed(writer io.Writer, tp spec.Type) error { - definedType, ok := tp.(spec.DefineStruct) + _, ok := tp.(spec.DefineStruct) if !ok { return errors.New("no members of type " + tp.Name()) } - members := definedType.GetNonBodyMembers() - if len(members) == 0 { + // Check if there are actual non-body members (recursively through inline structs) + if !hasActualNonBodyMembers(tp) { return nil } @@ -180,7 +180,7 @@ func genParamsTypesIfNeed(writer io.Writer, tp spec.Type) error { } fmt.Fprintf(writer, "}\n") - if len(definedType.GetTagMembers(headerTagKey)) > 0 { + if hasActualTagMembers(tp, headerTagKey) { fmt.Fprintf(writer, "export interface %sHeaders {\n", util.Title(tp.Name())) if err := writeTagMembers(writer, tp, headerTagKey); err != nil { return err @@ -247,3 +247,87 @@ func writeTagMembers(writer io.Writer, tp spec.Type, tagKey string) error { } return nil } + +// hasActualTagMembers checks if a type has actual members with the given tag, +// recursively checking inline/embedded structs +func hasActualTagMembers(tp spec.Type, tagKey string) bool { + definedType, ok := tp.(spec.DefineStruct) + if !ok { + pointType, ok := tp.(spec.PointerType) + if ok { + return hasActualTagMembers(pointType.Type, tagKey) + } + return false + } + + for _, m := range definedType.Members { + if m.IsInline { + // Recursively check inline members + if hasActualTagMembers(m.Type, tagKey) { + return true + } + } else { + // Check non-inline members for the tag + if m.IsTagMember(tagKey) { + return true + } + } + } + return false +} + +// hasActualBodyMembers checks if a type has actual body members (json tags), +// recursively checking inline/embedded structs +func hasActualBodyMembers(tp spec.Type) bool { + definedType, ok := tp.(spec.DefineStruct) + if !ok { + pointType, ok := tp.(spec.PointerType) + if ok { + return hasActualBodyMembers(pointType.Type) + } + return false + } + + for _, m := range definedType.Members { + if m.IsInline { + // Recursively check inline members + if hasActualBodyMembers(m.Type) { + return true + } + } else { + // Check non-inline members for json tag + if m.IsBodyMember() { + return true + } + } + } + return false +} + +// hasActualNonBodyMembers checks if a type has actual non-body members (form, path, header tags), +// recursively checking inline/embedded structs +func hasActualNonBodyMembers(tp spec.Type) bool { + definedType, ok := tp.(spec.DefineStruct) + if !ok { + pointType, ok := tp.(spec.PointerType) + if ok { + return hasActualNonBodyMembers(pointType.Type) + } + return false + } + + for _, m := range definedType.Members { + if m.IsInline { + // Recursively check inline members + if hasActualNonBodyMembers(m.Type) { + return true + } + } else { + // Check non-inline members for non-body tags + if !m.IsBodyMember() { + return true + } + } + } + return false +} diff --git a/tools/goctl/api/tsgen/util_test.go b/tools/goctl/api/tsgen/util_test.go index 076dee4217f9..45f3c85ed465 100644 --- a/tools/goctl/api/tsgen/util_test.go +++ b/tools/goctl/api/tsgen/util_test.go @@ -37,3 +37,268 @@ func TestGenTsType(t *testing.T) { } assert.Equal(t, `1 | 3 | 4 | 123`, ty) } + +func TestHasActualTagMembers(t *testing.T) { + // Test with no members + emptyStruct := spec.DefineStruct{ + RawName: "Empty", + Members: []spec.Member{}, + } + assert.False(t, hasActualTagMembers(emptyStruct, "form")) + assert.False(t, hasActualTagMembers(emptyStruct, "header")) + + // Test with direct form members + directFormStruct := spec.DefineStruct{ + RawName: "DirectForm", + Members: []spec.Member{ + { + Name: "Field1", + Type: spec.PrimitiveType{RawName: "string"}, + Tag: `form:"field1"`, + }, + }, + } + assert.True(t, hasActualTagMembers(directFormStruct, "form")) + assert.False(t, hasActualTagMembers(directFormStruct, "header")) + + // Test with inline struct containing form members + inlineFormStruct := spec.DefineStruct{ + RawName: "PaginationReq", + Members: []spec.Member{ + { + Name: "PageNum", + Type: spec.PrimitiveType{RawName: "int"}, + Tag: `form:"pageNum"`, + }, + { + Name: "PageSize", + Type: spec.PrimitiveType{RawName: "int"}, + Tag: `form:"pageSize"`, + }, + }, + } + parentStruct := spec.DefineStruct{ + RawName: "ParentReq", + Members: []spec.Member{ + { + Name: "", + Type: inlineFormStruct, + IsInline: true, + }, + }, + } + assert.True(t, hasActualTagMembers(parentStruct, "form")) + assert.False(t, hasActualTagMembers(parentStruct, "header")) + + // Test with both direct and inline members + mixedStruct := spec.DefineStruct{ + RawName: "MixedReq", + Members: []spec.Member{ + { + Name: "Sth", + Type: spec.PrimitiveType{RawName: "string"}, + Tag: `form:"sth"`, + }, + { + Name: "", + Type: inlineFormStruct, + IsInline: true, + }, + }, + } + assert.True(t, hasActualTagMembers(mixedStruct, "form")) + assert.False(t, hasActualTagMembers(mixedStruct, "header")) + + // Test with inline struct containing only json members (body members) + inlineJsonStruct := spec.DefineStruct{ + RawName: "JsonStruct", + Members: []spec.Member{ + { + Name: "Code", + Type: spec.PrimitiveType{RawName: "int64"}, + Tag: `json:"code"`, + }, + { + Name: "Msg", + Type: spec.PrimitiveType{RawName: "string"}, + Tag: `json:"msg"`, + }, + }, + } + parentJsonStruct := spec.DefineStruct{ + RawName: "ParentResp", + Members: []spec.Member{ + { + Name: "", + Type: inlineJsonStruct, + IsInline: true, + }, + }, + } + assert.False(t, hasActualTagMembers(parentJsonStruct, "form")) + assert.False(t, hasActualTagMembers(parentJsonStruct, "header")) +} + +func TestHasActualBodyMembers(t *testing.T) { + // Test with no members + emptyStruct := spec.DefineStruct{ + RawName: "Empty", + Members: []spec.Member{}, + } + assert.False(t, hasActualBodyMembers(emptyStruct)) + + // Test with direct json members + directJsonStruct := spec.DefineStruct{ + RawName: "DirectJson", + Members: []spec.Member{ + { + Name: "Code", + Type: spec.PrimitiveType{RawName: "int64"}, + Tag: `json:"code"`, + }, + }, + } + assert.True(t, hasActualBodyMembers(directJsonStruct)) + + // Test with inline struct containing json members + inlineJsonStruct := spec.DefineStruct{ + RawName: "BaseResp", + Members: []spec.Member{ + { + Name: "Code", + Type: spec.PrimitiveType{RawName: "int64"}, + Tag: `json:"code"`, + }, + { + Name: "Msg", + Type: spec.PrimitiveType{RawName: "string"}, + Tag: `json:"msg"`, + }, + }, + } + parentStruct := spec.DefineStruct{ + RawName: "ParentResp", + Members: []spec.Member{ + { + Name: "", + Type: inlineJsonStruct, + IsInline: true, + }, + }, + } + assert.True(t, hasActualBodyMembers(parentStruct)) + + // Test with inline struct containing only form members (not body members) + inlineFormStruct := spec.DefineStruct{ + RawName: "PaginationReq", + Members: []spec.Member{ + { + Name: "PageNum", + Type: spec.PrimitiveType{RawName: "int"}, + Tag: `form:"pageNum"`, + }, + }, + } + parentFormStruct := spec.DefineStruct{ + RawName: "ParentReq", + Members: []spec.Member{ + { + Name: "", + Type: inlineFormStruct, + IsInline: true, + }, + }, + } + assert.False(t, hasActualBodyMembers(parentFormStruct)) +} + +func TestHasActualNonBodyMembers(t *testing.T) { + // Test with no members + emptyStruct := spec.DefineStruct{ + RawName: "Empty", + Members: []spec.Member{}, + } + assert.False(t, hasActualNonBodyMembers(emptyStruct)) + + // Test with direct form members + directFormStruct := spec.DefineStruct{ + RawName: "DirectForm", + Members: []spec.Member{ + { + Name: "Field1", + Type: spec.PrimitiveType{RawName: "string"}, + Tag: `form:"field1"`, + }, + }, + } + assert.True(t, hasActualNonBodyMembers(directFormStruct)) + + // Test with inline struct containing form members + inlineFormStruct := spec.DefineStruct{ + RawName: "PaginationReq", + Members: []spec.Member{ + { + Name: "PageNum", + Type: spec.PrimitiveType{RawName: "int"}, + Tag: `form:"pageNum"`, + }, + { + Name: "PageSize", + Type: spec.PrimitiveType{RawName: "int"}, + Tag: `form:"pageSize"`, + }, + }, + } + parentStruct := spec.DefineStruct{ + RawName: "ParentReq", + Members: []spec.Member{ + { + Name: "", + Type: inlineFormStruct, + IsInline: true, + }, + }, + } + assert.True(t, hasActualNonBodyMembers(parentStruct)) + + // Test with inline struct containing only json members (body members) + inlineJsonStruct := spec.DefineStruct{ + RawName: "BaseResp", + Members: []spec.Member{ + { + Name: "Code", + Type: spec.PrimitiveType{RawName: "int64"}, + Tag: `json:"code"`, + }, + }, + } + parentJsonStruct := spec.DefineStruct{ + RawName: "ParentResp", + Members: []spec.Member{ + { + Name: "", + Type: inlineJsonStruct, + IsInline: true, + }, + }, + } + assert.False(t, hasActualNonBodyMembers(parentJsonStruct)) + + // Test with both direct and inline non-body members + mixedStruct := spec.DefineStruct{ + RawName: "MixedReq", + Members: []spec.Member{ + { + Name: "Sth", + Type: spec.PrimitiveType{RawName: "string"}, + Tag: `form:"sth"`, + }, + { + Name: "", + Type: inlineFormStruct, + IsInline: true, + }, + }, + } + assert.True(t, hasActualNonBodyMembers(mixedStruct)) +}