Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 0 additions & 17 deletions tools/goctl/api/gogen/jwt.api

This file was deleted.

163 changes: 163 additions & 0 deletions tools/goctl/api/tsgen/gen_test.go
Original file line number Diff line number Diff line change
@@ -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<components.GetListResp>(`/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<components.GetListResp>(`/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<components.GetListResp>(`/getListCommon`, params)")

// Verify getListNoReq function signature and call
assert.Contains(t, handler, "export function getListNoReq()")
assert.Contains(t, handler, "return webapi.get<components.GetListResp>(`/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")
}
8 changes: 4 additions & 4 deletions tools/goctl/api/tsgen/genpacket.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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 {
Expand All @@ -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 {
Expand All @@ -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)
}
92 changes: 88 additions & 4 deletions tools/goctl/api/tsgen/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand All @@ -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
Expand Down Expand Up @@ -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
}
Loading
Loading