From 87dd9671be4ebf5224a26d9ae6ee5417267db3b0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 7 Oct 2025 03:27:46 +0000 Subject: [PATCH] Fix TypeScript generator to handle inline/embedded structs correctly - Add hasActualTagMembers helper to recursively check for actual tag members - Add hasActualBodyMembers helper to recursively check for actual body members - Add hasActualNonBodyMembers helper to recursively check for actual non-body members - Update genParamsTypesIfNeed to use hasActualNonBodyMembers and hasActualTagMembers - Update hasRequestBody, hasRequestHeader, hasRequestPath, and pathHasParams to use new helpers - Add comprehensive test coverage for new helper functions Co-authored-by: kevwan <1918356+kevwan@users.noreply.github.com> --- tools/goctl/api/tsgen/genpacket.go | 8 +- tools/goctl/api/tsgen/util.go | 92 +++++++++- tools/goctl/api/tsgen/util_test.go | 265 +++++++++++++++++++++++++++++ 3 files changed, 357 insertions(+), 8 deletions(-) diff --git a/tools/goctl/api/tsgen/genpacket.go b/tools/goctl/api/tsgen/genpacket.go index 6635220a3..a713ac7cc 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 df6e95c4c..0ab6872bb 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 076dee421..45f3c85ed 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)) +}