mirror of
https://github.com/zeromicro/go-zero.git
synced 2026-07-22 16:36:52 +08:00
Compare commits
8 Commits
99515480cf
...
dependabot
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
af17f9b8ab | ||
|
|
84313f2e92 | ||
|
|
394ffcc19a | ||
|
|
565bcb3f21 | ||
|
|
6a6b81ef20 | ||
|
|
35a7ca9d98 | ||
|
|
f910257ec9 | ||
|
|
d318de1212 |
4
.github/workflows/go.yml
vendored
4
.github/workflows/go.yml
vendored
@@ -15,7 +15,7 @@ jobs:
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Set up Go 1.x
|
||||
uses: actions/setup-go@v6
|
||||
uses: actions/setup-go@v7
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
check-latest: true
|
||||
@@ -55,7 +55,7 @@ jobs:
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Set up Go 1.x
|
||||
uses: actions/setup-go@v6
|
||||
uses: actions/setup-go@v7
|
||||
with:
|
||||
# make sure Go version compatible with go-zero
|
||||
go-version-file: go.mod
|
||||
|
||||
2
.github/workflows/reviewdog.yml
vendored
2
.github/workflows/reviewdog.yml
vendored
@@ -6,7 +6,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/setup-go@v6
|
||||
- uses: actions/setup-go@v7
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
check-latest: true
|
||||
|
||||
2
.github/workflows/version-check.yml
vendored
2
.github/workflows/version-check.yml
vendored
@@ -13,7 +13,7 @@ jobs:
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v6
|
||||
uses: actions/setup-go@v7
|
||||
with:
|
||||
go-version: '1.21'
|
||||
|
||||
|
||||
@@ -931,6 +931,113 @@ func TestUnmarshalJsonArray(t *testing.T) {
|
||||
assert.Equal(t, 18, v[0].Age)
|
||||
}
|
||||
|
||||
func TestUnmarshalJsonBytesPointerSliceUint64(t *testing.T) {
|
||||
t.Run("with values", func(t *testing.T) {
|
||||
var c struct {
|
||||
IDs *[]uint64 `json:"ids,optional"`
|
||||
}
|
||||
content := []byte(`{"ids":[9000,9001]}`)
|
||||
|
||||
assert.Nil(t, UnmarshalJsonBytes(content, &c))
|
||||
assert.NotNil(t, c.IDs)
|
||||
assert.Equal(t, []uint64{9000, 9001}, *c.IDs)
|
||||
})
|
||||
|
||||
t.Run("omitted", func(t *testing.T) {
|
||||
var c struct {
|
||||
IDs *[]uint64 `json:"ids,optional"`
|
||||
}
|
||||
content := []byte(`{}`)
|
||||
|
||||
assert.Nil(t, UnmarshalJsonBytes(content, &c))
|
||||
assert.Nil(t, c.IDs)
|
||||
})
|
||||
|
||||
t.Run("null", func(t *testing.T) {
|
||||
var c struct {
|
||||
IDs *[]uint64 `json:"ids,optional"`
|
||||
}
|
||||
content := []byte(`{"ids":null}`)
|
||||
|
||||
assert.Nil(t, UnmarshalJsonBytes(content, &c))
|
||||
assert.Nil(t, c.IDs)
|
||||
})
|
||||
|
||||
t.Run("empty array", func(t *testing.T) {
|
||||
var c struct {
|
||||
IDs *[]uint64 `json:"ids,optional"`
|
||||
}
|
||||
content := []byte(`{"ids":[]}`)
|
||||
|
||||
assert.Nil(t, UnmarshalJsonBytes(content, &c))
|
||||
assert.NotNil(t, c.IDs)
|
||||
assert.Equal(t, []uint64{}, *c.IDs)
|
||||
})
|
||||
}
|
||||
|
||||
func TestUnmarshalJsonBytesPointerSliceOtherTypes(t *testing.T) {
|
||||
t.Run("pointer to []string", func(t *testing.T) {
|
||||
var c struct {
|
||||
Names *[]string `json:"names,optional"`
|
||||
}
|
||||
content := []byte(`{"names":["a","b"]}`)
|
||||
|
||||
assert.Nil(t, UnmarshalJsonBytes(content, &c))
|
||||
assert.NotNil(t, c.Names)
|
||||
assert.Equal(t, []string{"a", "b"}, *c.Names)
|
||||
})
|
||||
|
||||
t.Run("pointer to []int", func(t *testing.T) {
|
||||
var c struct {
|
||||
Values *[]int `json:"values,optional"`
|
||||
}
|
||||
content := []byte(`{"values":[1,2,3]}`)
|
||||
|
||||
assert.Nil(t, UnmarshalJsonBytes(content, &c))
|
||||
assert.NotNil(t, c.Values)
|
||||
assert.Equal(t, []int{1, 2, 3}, *c.Values)
|
||||
})
|
||||
}
|
||||
|
||||
func TestUnmarshalJsonBytesPointerSliceStruct(t *testing.T) {
|
||||
type Item struct {
|
||||
Name string `json:"name"`
|
||||
Age int `json:"age"`
|
||||
}
|
||||
|
||||
t.Run("with values", func(t *testing.T) {
|
||||
var c struct {
|
||||
Items *[]Item `json:"items,optional"`
|
||||
}
|
||||
content := []byte(`{"items":[{"name":"alice","age":30},{"name":"bob","age":25}]}`)
|
||||
|
||||
assert.Nil(t, UnmarshalJsonBytes(content, &c))
|
||||
assert.NotNil(t, c.Items)
|
||||
assert.Equal(t, []Item{{Name: "alice", Age: 30}, {Name: "bob", Age: 25}}, *c.Items)
|
||||
})
|
||||
|
||||
t.Run("omitted", func(t *testing.T) {
|
||||
var c struct {
|
||||
Items *[]Item `json:"items,optional"`
|
||||
}
|
||||
content := []byte(`{}`)
|
||||
|
||||
assert.Nil(t, UnmarshalJsonBytes(content, &c))
|
||||
assert.Nil(t, c.Items)
|
||||
})
|
||||
|
||||
t.Run("empty array", func(t *testing.T) {
|
||||
var c struct {
|
||||
Items *[]Item `json:"items,optional"`
|
||||
}
|
||||
content := []byte(`{"items":[]}`)
|
||||
|
||||
assert.Nil(t, UnmarshalJsonBytes(content, &c))
|
||||
assert.NotNil(t, c.Items)
|
||||
assert.Equal(t, []Item{}, *c.Items)
|
||||
})
|
||||
}
|
||||
|
||||
func TestUnmarshalJsonBytesError(t *testing.T) {
|
||||
var v []struct {
|
||||
Name string `json:"name"`
|
||||
|
||||
@@ -142,11 +142,11 @@ func (u *Unmarshaler) fillSlice(fieldType reflect.Type, value reflect.Value,
|
||||
return nil
|
||||
}
|
||||
|
||||
baseType := fieldType.Elem()
|
||||
baseType := Deref(fieldType).Elem()
|
||||
dereffedBaseType := Deref(baseType)
|
||||
dereffedBaseKind := dereffedBaseType.Kind()
|
||||
if refValue.Len() == 0 {
|
||||
value.Set(reflect.MakeSlice(reflect.SliceOf(baseType), 0, 0))
|
||||
SetValue(fieldType, value, reflect.MakeSlice(reflect.SliceOf(baseType), 0, 0))
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -179,7 +179,7 @@ func (u *Unmarshaler) fillSlice(fieldType reflect.Type, value reflect.Value,
|
||||
}
|
||||
|
||||
if valid {
|
||||
value.Set(conv)
|
||||
SetValue(fieldType, value, conv)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -201,7 +201,7 @@ func (u *Unmarshaler) fillSliceFromString(fieldType reflect.Type, value reflect.
|
||||
return errUnsupportedType
|
||||
}
|
||||
|
||||
baseFieldType := fieldType.Elem()
|
||||
baseFieldType := Deref(fieldType).Elem()
|
||||
baseFieldKind := baseFieldType.Kind()
|
||||
conv := reflect.MakeSlice(reflect.SliceOf(baseFieldType), len(slice), cap(slice))
|
||||
|
||||
@@ -211,7 +211,7 @@ func (u *Unmarshaler) fillSliceFromString(fieldType reflect.Type, value reflect.
|
||||
}
|
||||
}
|
||||
|
||||
value.Set(conv)
|
||||
SetValue(fieldType, value, conv)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
4
go.mod
4
go.mod
@@ -15,7 +15,7 @@ require (
|
||||
github.com/jackc/pgx/v5 v5.8.0
|
||||
github.com/jhump/protoreflect v1.18.0
|
||||
github.com/modelcontextprotocol/go-sdk v1.4.0
|
||||
github.com/pelletier/go-toml/v2 v2.4.2
|
||||
github.com/pelletier/go-toml/v2 v2.4.3
|
||||
github.com/prometheus/client_golang v1.23.2
|
||||
github.com/redis/go-redis/v9 v9.21.0
|
||||
github.com/spaolacci/murmur3 v1.1.0
|
||||
@@ -23,7 +23,7 @@ require (
|
||||
github.com/titanous/json5 v1.0.0
|
||||
go.etcd.io/etcd/api/v3 v3.5.21
|
||||
go.etcd.io/etcd/client/v3 v3.5.21
|
||||
go.mongodb.org/mongo-driver/v2 v2.7.0
|
||||
go.mongodb.org/mongo-driver/v2 v2.8.0
|
||||
go.opentelemetry.io/otel v1.40.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0
|
||||
|
||||
8
go.sum
8
go.sum
@@ -147,8 +147,8 @@ github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4=
|
||||
github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog=
|
||||
github.com/openzipkin/zipkin-go v0.4.3 h1:9EGwpqkgnwdEIJ+Od7QVSEIH+ocmm5nPat0G7sjsSdg=
|
||||
github.com/openzipkin/zipkin-go v0.4.3/go.mod h1:M9wCJZFWCo2RiY+o1eBCEMe0Dp2S5LDHcMZmk3RmK7c=
|
||||
github.com/pelletier/go-toml/v2 v2.4.2 h1:M2fKKbmyvI+hGId/D0W64qDBMVhJnNR10O5gIbMc//Q=
|
||||
github.com/pelletier/go-toml/v2 v2.4.2/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
github.com/pelletier/go-toml/v2 v2.4.3 h1:GTRvJQutkOSftxIFD5xw9aepkYNuPWmVJpffdDPYVpY=
|
||||
github.com/pelletier/go-toml/v2 v2.4.3/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
github.com/petermattis/goid v0.0.0-20260113132338-7c7de50cc741 h1:KPpdlQLZcHfTMQRi6bFQ7ogNO0ltFT4PmtwTLW4W+14=
|
||||
github.com/petermattis/goid v0.0.0-20260113132338-7c7de50cc741/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
@@ -224,8 +224,8 @@ go.etcd.io/etcd/client/pkg/v3 v3.5.21 h1:lPBu71Y7osQmzlflM9OfeIV2JlmpBjqBNlLtcoB
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.5.21/go.mod h1:BgqT/IXPjK9NkeSDjbzwsHySX3yIle2+ndz28nVsjUs=
|
||||
go.etcd.io/etcd/client/v3 v3.5.21 h1:T6b1Ow6fNjOLOtM0xSoKNQt1ASPCLWrF9XMHcH9pEyY=
|
||||
go.etcd.io/etcd/client/v3 v3.5.21/go.mod h1:mFYy67IOqmbRf/kRUvsHixzo3iG+1OF2W2+jVIQRAnU=
|
||||
go.mongodb.org/mongo-driver/v2 v2.7.0 h1:RO+zqavD2/GCL3cxOMyZhx6R9Irzr8/6gsoqx5tcY/c=
|
||||
go.mongodb.org/mongo-driver/v2 v2.7.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
|
||||
go.mongodb.org/mongo-driver/v2 v2.8.0 h1:CxWDGQYY8QQwNjAl/aq2sfWakdnWZynnqJ9F4DhHbP8=
|
||||
go.mongodb.org/mongo-driver/v2 v2.8.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms=
|
||||
|
||||
@@ -54,6 +54,8 @@ const (
|
||||
propertyKeyDeprecated = "deprecated"
|
||||
propertyKeyPrefix = "prefix"
|
||||
propertyKeyAuthType = "authType"
|
||||
propertyKeyRespCode = "respCode"
|
||||
propertyKeyResponses = "responses"
|
||||
propertyKeyHost = "host"
|
||||
propertyKeyBasePath = "basePath"
|
||||
propertyKeyWrapCodeMsg = "wrapCodeMsg"
|
||||
|
||||
@@ -82,6 +82,7 @@ type (
|
||||
service Swagger {
|
||||
@doc (
|
||||
description: "form demo"
|
||||
respCode: "201" // HTTP status code corresponding to Swagger
|
||||
)
|
||||
@handler form
|
||||
post /form (FormReq) returns (FormResp)
|
||||
@@ -102,6 +103,27 @@ type (
|
||||
Language string `json:"language"`
|
||||
Gender string `json:"gender"`
|
||||
}
|
||||
EmbeddedUser {
|
||||
UserId int `json:"userId,example=10"`
|
||||
Username string `json:"username,example=keson.an"`
|
||||
}
|
||||
EmbeddedAudit {
|
||||
TraceId string `json:"traceId,example=trace-001"`
|
||||
CreatedBy string `json:"createdBy,optional,example=system"`
|
||||
}
|
||||
EmbeddedProfile {
|
||||
EmbeddedUser
|
||||
*EmbeddedAudit
|
||||
Nickname string `json:"nickname,optional,example=keson"`
|
||||
}
|
||||
EmbeddedJsonReq {
|
||||
EmbeddedProfile
|
||||
RequestId string `json:"requestId,example=req-001"`
|
||||
}
|
||||
EmbeddedJsonResp {
|
||||
EmbeddedProfile
|
||||
Success bool `json:"success,example=true"`
|
||||
}
|
||||
ComplexJsonLevel2 {
|
||||
// basic
|
||||
Integer int `json:"integer,example=1"`
|
||||
@@ -237,5 +259,10 @@ service Swagger {
|
||||
)
|
||||
@handler jsonComplex
|
||||
post /json/complex (ComplexJsonReq) returns (ComplexJsonResp)
|
||||
}
|
||||
|
||||
@doc (
|
||||
description: "embedded json request body API"
|
||||
)
|
||||
@handler jsonEmbedded
|
||||
post /json/embedded (EmbeddedJsonReq) returns (EmbeddedJsonResp)
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"201": {
|
||||
"description": "",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
|
||||
@@ -84,6 +84,7 @@ type (
|
||||
service Swagger {
|
||||
@doc (
|
||||
description: "form 接口"
|
||||
respCode: "201" // 对应 Swagger 的 HTTP 状态码
|
||||
)
|
||||
@handler form
|
||||
post /form (FormReq) returns (FormResp)
|
||||
@@ -244,4 +245,3 @@ service Swagger {
|
||||
@handler jsonComplex
|
||||
post /json/complex (ComplexJsonReq) returns (ComplexJsonResp)
|
||||
}
|
||||
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"201": {
|
||||
"description": "",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
|
||||
@@ -2,64 +2,108 @@ package swagger
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/go-openapi/spec"
|
||||
apiSpec "github.com/zeromicro/go-zero/tools/goctl/api/spec"
|
||||
)
|
||||
|
||||
func jsonResponseFromType(ctx Context, atDoc apiSpec.AtDoc, tp apiSpec.Type) *spec.Responses {
|
||||
statusCode := responseStatusCode(atDoc)
|
||||
var response spec.Response
|
||||
if tp == nil {
|
||||
return &spec.Responses{
|
||||
ResponsesProps: spec.ResponsesProps{
|
||||
StatusCodeResponses: map[int]spec.Response{
|
||||
http.StatusOK: {
|
||||
ResponseProps: spec.ResponseProps{
|
||||
Description: "",
|
||||
Schema: &spec.Schema{},
|
||||
},
|
||||
},
|
||||
},
|
||||
response = spec.Response{
|
||||
ResponseProps: spec.ResponseProps{
|
||||
Description: "",
|
||||
},
|
||||
}
|
||||
}
|
||||
props := spec.SchemaProps{
|
||||
AdditionalProperties: mapFromGoType(ctx, tp),
|
||||
Items: itemsFromGoType(ctx, tp),
|
||||
}
|
||||
if ctx.UseDefinitions {
|
||||
structName, ok := containsStruct(tp)
|
||||
if ok {
|
||||
props.Ref = spec.MustCreateRef(getRefName(structName))
|
||||
return &spec.Responses{
|
||||
ResponsesProps: spec.ResponsesProps{
|
||||
StatusCodeResponses: map[int]spec.Response{
|
||||
http.StatusOK: {
|
||||
ResponseProps: spec.ResponseProps{
|
||||
Schema: &spec.Schema{
|
||||
SchemaProps: wrapCodeMsgProps(ctx, props, atDoc),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
} else {
|
||||
props := spec.SchemaProps{
|
||||
AdditionalProperties: mapFromGoType(ctx, tp),
|
||||
Items: itemsFromGoType(ctx, tp),
|
||||
}
|
||||
}
|
||||
|
||||
p, _ := propertiesFromType(ctx, tp)
|
||||
props.Type = typeFromGoType(ctx, tp)
|
||||
props.Properties = p
|
||||
return &spec.Responses{
|
||||
ResponsesProps: spec.ResponsesProps{
|
||||
StatusCodeResponses: map[int]spec.Response{
|
||||
http.StatusOK: {
|
||||
if ctx.UseDefinitions {
|
||||
structName, ok := containsStruct(tp)
|
||||
if ok {
|
||||
props.Ref = spec.MustCreateRef(getRefName(structName))
|
||||
response = spec.Response{
|
||||
ResponseProps: spec.ResponseProps{
|
||||
Schema: &spec.Schema{
|
||||
SchemaProps: wrapCodeMsgProps(ctx, props, atDoc),
|
||||
},
|
||||
},
|
||||
}
|
||||
return responsesFromStatusCode(atDoc, statusCode, response)
|
||||
}
|
||||
}
|
||||
|
||||
p, _ := propertiesFromType(ctx, tp)
|
||||
props.Type = typeFromGoType(ctx, tp)
|
||||
props.Properties = p
|
||||
response = spec.Response{
|
||||
ResponseProps: spec.ResponseProps{
|
||||
Schema: &spec.Schema{
|
||||
SchemaProps: wrapCodeMsgProps(ctx, props, atDoc),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return responsesFromStatusCode(atDoc, statusCode, response)
|
||||
}
|
||||
|
||||
func responsesFromStatusCode(atDoc apiSpec.AtDoc, statusCode int, response spec.Response) *spec.Responses {
|
||||
statusCodeResponses := map[int]spec.Response{
|
||||
statusCode: response,
|
||||
}
|
||||
for code, description := range responseDescriptions(atDoc) {
|
||||
if code == statusCode {
|
||||
responseWithDescription := response
|
||||
responseWithDescription.Description = description
|
||||
statusCodeResponses[code] = responseWithDescription
|
||||
} else {
|
||||
statusCodeResponses[code] = spec.Response{
|
||||
ResponseProps: spec.ResponseProps{
|
||||
Description: description,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &spec.Responses{
|
||||
ResponsesProps: spec.ResponsesProps{
|
||||
StatusCodeResponses: statusCodeResponses,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func responseStatusCode(atDoc apiSpec.AtDoc) int {
|
||||
return getOrDefault(atDoc.Properties, propertyKeyRespCode, http.StatusOK, func(str string, def int) int {
|
||||
statusCode, err := strconv.Atoi(str)
|
||||
if err != nil || statusCode < http.StatusContinue || statusCode > 599 {
|
||||
return def
|
||||
}
|
||||
|
||||
return statusCode
|
||||
})
|
||||
}
|
||||
|
||||
func responseDescriptions(atDoc apiSpec.AtDoc) map[int]string {
|
||||
descriptions := make(map[int]string)
|
||||
for _, item := range strings.Split(getStringFromKVOrDefault(atDoc.Properties, propertyKeyResponses, ""), "<br>") {
|
||||
codeText, description, ok := strings.Cut(item, "-")
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
code, err := strconv.Atoi(strings.TrimSpace(codeText))
|
||||
if err != nil || code < http.StatusContinue || code > 599 {
|
||||
continue
|
||||
}
|
||||
|
||||
descriptions[code] = strings.TrimSpace(description)
|
||||
}
|
||||
|
||||
return descriptions
|
||||
}
|
||||
|
||||
92
tools/goctl/api/swagger/response_test.go
Normal file
92
tools/goctl/api/swagger/response_test.go
Normal file
@@ -0,0 +1,92 @@
|
||||
package swagger
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/zeromicro/go-zero/tools/goctl/api/spec"
|
||||
)
|
||||
|
||||
func TestJsonResponseFromTypeStatusCode(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
properties map[string]string
|
||||
response spec.Type
|
||||
want int
|
||||
}{
|
||||
{
|
||||
name: "defaults to ok",
|
||||
response: spec.PrimitiveType{RawName: "string"},
|
||||
want: http.StatusOK,
|
||||
},
|
||||
{
|
||||
name: "uses custom status code",
|
||||
properties: map[string]string{
|
||||
propertyKeyRespCode: "201",
|
||||
},
|
||||
response: spec.PrimitiveType{RawName: "string"},
|
||||
want: http.StatusCreated,
|
||||
},
|
||||
{
|
||||
name: "supports quoted custom status code",
|
||||
properties: map[string]string{
|
||||
propertyKeyRespCode: `"204"`,
|
||||
},
|
||||
want: http.StatusNoContent,
|
||||
},
|
||||
{
|
||||
name: "defaults for invalid status code",
|
||||
properties: map[string]string{
|
||||
propertyKeyRespCode: "600",
|
||||
},
|
||||
response: spec.PrimitiveType{RawName: "string"},
|
||||
want: http.StatusOK,
|
||||
},
|
||||
{
|
||||
name: "defaults for non-numeric status code",
|
||||
properties: map[string]string{
|
||||
propertyKeyRespCode: "created",
|
||||
},
|
||||
response: spec.PrimitiveType{RawName: "string"},
|
||||
want: http.StatusOK,
|
||||
},
|
||||
{
|
||||
name: "uses custom status code without response body",
|
||||
properties: map[string]string{
|
||||
propertyKeyRespCode: "204",
|
||||
},
|
||||
want: http.StatusNoContent,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
responses := jsonResponseFromType(testingContext(t), spec.AtDoc{
|
||||
Properties: test.properties,
|
||||
}, test.response)
|
||||
|
||||
assert.Len(t, responses.StatusCodeResponses, 1)
|
||||
assert.Contains(t, responses.StatusCodeResponses, test.want)
|
||||
if test.response == nil {
|
||||
assert.Nil(t, responses.StatusCodeResponses[test.want].Schema)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestJsonResponseFromTypeMultipleStatusCodes(t *testing.T) {
|
||||
responses := jsonResponseFromType(testingContext(t), spec.AtDoc{
|
||||
Properties: map[string]string{
|
||||
propertyKeyResponses: "200-OK<br>401-Unauthorized<br>404-User not found",
|
||||
},
|
||||
}, spec.PrimitiveType{RawName: "string"})
|
||||
|
||||
assert.Len(t, responses.StatusCodeResponses, 3)
|
||||
assert.Equal(t, "OK", responses.StatusCodeResponses[http.StatusOK].Description)
|
||||
assert.NotNil(t, responses.StatusCodeResponses[http.StatusOK].Schema)
|
||||
assert.Equal(t, "Unauthorized", responses.StatusCodeResponses[http.StatusUnauthorized].Description)
|
||||
assert.Nil(t, responses.StatusCodeResponses[http.StatusUnauthorized].Schema)
|
||||
assert.Equal(t, "User not found", responses.StatusCodeResponses[http.StatusNotFound].Description)
|
||||
assert.Nil(t, responses.StatusCodeResponses[http.StatusNotFound].Schema)
|
||||
}
|
||||
@@ -202,6 +202,8 @@ func expandMembers(ctx Context, tp apiSpec.Type) []apiSpec.Member {
|
||||
}
|
||||
members = append(members, v)
|
||||
}
|
||||
case apiSpec.PointerType:
|
||||
members = expandMembers(ctx, val.Type)
|
||||
}
|
||||
|
||||
return members
|
||||
|
||||
@@ -3,8 +3,8 @@ package swagger
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/zeromicro/go-zero/tools/goctl/api/spec"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/zeromicro/go-zero/tools/goctl/api/spec"
|
||||
)
|
||||
|
||||
func Test_pathVariable2SwaggerVariable(t *testing.T) {
|
||||
@@ -66,7 +66,7 @@ func TestArrayDefinitionsBug(t *testing.T) {
|
||||
|
||||
// Verify the array field has correct structure
|
||||
assert.Equal(t, "array", arrayField.Type[0])
|
||||
|
||||
|
||||
// Check that we have items
|
||||
assert.NotNil(t, arrayField.Items, "Array should have items defined")
|
||||
assert.NotNil(t, arrayField.Items.Schema, "Array items should have schema")
|
||||
@@ -74,7 +74,7 @@ func TestArrayDefinitionsBug(t *testing.T) {
|
||||
// The FIX: $ref should be inside items, not at schema level
|
||||
hasRef := arrayField.Ref.String() != ""
|
||||
assert.False(t, hasRef, "Schema level should NOT have $ref")
|
||||
|
||||
|
||||
// The $ref should be in the items
|
||||
hasItemsRef := arrayField.Items.Schema.Ref.String() != ""
|
||||
assert.True(t, hasItemsRef, "Items should have $ref")
|
||||
@@ -138,3 +138,55 @@ func TestArrayWithoutDefinitions(t *testing.T) {
|
||||
assert.Contains(t, arrayField.Items.Schema.Properties, "itemName")
|
||||
assert.Equal(t, []string{"itemName"}, arrayField.Items.Schema.Required)
|
||||
}
|
||||
|
||||
func TestPropertiesFromTypeInlinePointerMembers(t *testing.T) {
|
||||
ctx := testingContext(t)
|
||||
|
||||
baseStruct := spec.DefineStruct{
|
||||
RawName: "EmbeddedUser",
|
||||
Members: []spec.Member{
|
||||
{
|
||||
Name: "UserId",
|
||||
Type: spec.PrimitiveType{RawName: "int"},
|
||||
Tag: `json:"userId"`,
|
||||
},
|
||||
},
|
||||
}
|
||||
auditStruct := spec.DefineStruct{
|
||||
RawName: "EmbeddedAudit",
|
||||
Members: []spec.Member{
|
||||
{
|
||||
Name: "TraceId",
|
||||
Type: spec.PrimitiveType{RawName: "string"},
|
||||
Tag: `json:"traceId"`,
|
||||
},
|
||||
},
|
||||
}
|
||||
testStruct := spec.DefineStruct{
|
||||
RawName: "EmbeddedProfile",
|
||||
Members: []spec.Member{
|
||||
{
|
||||
Type: baseStruct,
|
||||
IsInline: true,
|
||||
},
|
||||
{
|
||||
Type: spec.PointerType{
|
||||
Type: auditStruct,
|
||||
},
|
||||
IsInline: true,
|
||||
},
|
||||
{
|
||||
Name: "Nickname",
|
||||
Type: spec.PrimitiveType{RawName: "string"},
|
||||
Tag: `json:"nickname,optional"`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
properties, required := propertiesFromType(ctx, testStruct)
|
||||
|
||||
assert.Contains(t, properties, "userId")
|
||||
assert.Contains(t, properties, "traceId")
|
||||
assert.Contains(t, properties, "nickname")
|
||||
assert.ElementsMatch(t, []string{"userId", "traceId"}, required)
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
module github.com/zeromicro/go-zero/tools/goctl
|
||||
|
||||
go 1.24.0
|
||||
go 1.25.0
|
||||
|
||||
require (
|
||||
github.com/DATA-DOG/go-sqlmock v1.5.2
|
||||
github.com/emicklei/proto v1.14.3
|
||||
github.com/fatih/structtag v1.2.0
|
||||
github.com/go-openapi/spec v0.21.1-0.20250328170532-a3928469592e
|
||||
github.com/go-openapi/spec v0.22.9
|
||||
github.com/go-sql-driver/mysql v1.10.0
|
||||
github.com/gookit/color v1.6.1
|
||||
github.com/iancoleman/strcase v0.3.0
|
||||
@@ -38,9 +38,16 @@ require (
|
||||
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.21.1 // indirect
|
||||
github.com/go-openapi/jsonreference v0.21.0 // indirect
|
||||
github.com/go-openapi/jsonpointer v1.0.0 // indirect
|
||||
github.com/go-openapi/jsonreference v1.0.0 // indirect
|
||||
github.com/go-openapi/swag v0.23.1 // indirect
|
||||
github.com/go-openapi/swag/conv v0.27.3 // indirect
|
||||
github.com/go-openapi/swag/jsonutils v0.27.3 // indirect
|
||||
github.com/go-openapi/swag/loading v0.27.3 // indirect
|
||||
github.com/go-openapi/swag/pools v0.27.3 // indirect
|
||||
github.com/go-openapi/swag/stringutils v0.27.3 // indirect
|
||||
github.com/go-openapi/swag/typeutils v0.27.3 // indirect
|
||||
github.com/go-openapi/swag/yamlutils v0.27.3 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang/protobuf v1.5.4 // indirect
|
||||
github.com/google/gnostic-models v0.7.0 // indirect
|
||||
|
||||
@@ -41,14 +41,34 @@ github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-openapi/jsonpointer v0.21.1 h1:whnzv/pNXtK2FbX/W9yJfRmE2gsmkfahjMKB0fZvcic=
|
||||
github.com/go-openapi/jsonpointer v0.21.1/go.mod h1:50I1STOfbY1ycR8jGz8DaMeLCdXiI6aDteEdRNNzpdk=
|
||||
github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ=
|
||||
github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4=
|
||||
github.com/go-openapi/spec v0.21.1-0.20250328170532-a3928469592e h1:auobAirzhPsLHMso0NVMqK0QunuLDYCK83KnaVUM/RU=
|
||||
github.com/go-openapi/spec v0.21.1-0.20250328170532-a3928469592e/go.mod h1:NAKTe9SplQBxIUlHlsuId1jk1I7bWTVV/2q/GtdRi6g=
|
||||
github.com/go-openapi/jsonpointer v1.0.0 h1:kR9tHqY0CtZaOPVFm622dPVNhrvYpwr4uCxgL3h1H8s=
|
||||
github.com/go-openapi/jsonpointer v1.0.0/go.mod h1:Z3rw7dWu1p9IgitXCFamSlA5lmDiklEB6vkaxcNZW5Y=
|
||||
github.com/go-openapi/jsonreference v1.0.0 h1:jlmTr6torcd1YgDQvSfNmRtKzYDO4FGBkrAdlAVWnpY=
|
||||
github.com/go-openapi/jsonreference v1.0.0/go.mod h1:jtwdyGbJk0Xhe5Y+rwtglQP6Sb1WZST4rT32LWB+sv0=
|
||||
github.com/go-openapi/spec v0.22.9 h1:/vKIFDcGKp0ktZWGbym/tJEWbk6/XOEmAVU0kqKMH+w=
|
||||
github.com/go-openapi/spec v0.22.9/go.mod h1:b/mNUYIOQOyIiUzUzXEE8xzyZqf93KvM9hQGP91yfl0=
|
||||
github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU=
|
||||
github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0=
|
||||
github.com/go-openapi/swag/conv v0.27.3 h1:iqJFmGEjmX3AY0lSszABFqRVqOSt99XS0LzNIMJYuhU=
|
||||
github.com/go-openapi/swag/conv v0.27.3/go.mod h1:nPRmN6jgNme99hpf+nM0auDZGALWIqlwhisKPK/bQhQ=
|
||||
github.com/go-openapi/swag/jsonutils v0.27.3 h1:1DEz+O82frtSMBcos/7XIn1GnpNTbsD4Bru4Dc/uhRc=
|
||||
github.com/go-openapi/swag/jsonutils v0.27.3/go.mod h1:qiDCoQvzkMxrV3G8FLEdIU5L+EFYc0zcDOHWT3Yofvo=
|
||||
github.com/go-openapi/swag/jsonutils/fixtures_test v0.27.3 h1:h/eT9kmGCDdFLJF29lOhzLtF0FmP1AX2MhLJWVebsb8=
|
||||
github.com/go-openapi/swag/jsonutils/fixtures_test v0.27.3/go.mod h1:mofwUWx70wvskwESqRJ//k/9kURmCgyJl5m5Ppoh5kY=
|
||||
github.com/go-openapi/swag/loading v0.27.3 h1:L9nQkEgzU7QgFQL+pLEMfGUKxeM4pWwGwbET9Z3weW0=
|
||||
github.com/go-openapi/swag/loading v0.27.3/go.mod h1:rJ0NeaKsF4CVPnMGjPQl7JlSHzvD0bc2DKXLss1hiuE=
|
||||
github.com/go-openapi/swag/pools v0.27.3 h1:gXjImP3F6/56wRRcFgEPld084Y6u2gs21ikPBt8NKBk=
|
||||
github.com/go-openapi/swag/pools v0.27.3/go.mod h1:kVQefhSK5RWuRe7BXsL8htgBPAMpN7HDGpGEknqugeE=
|
||||
github.com/go-openapi/swag/stringutils v0.27.3 h1:Ru28hnbAvN5wycALQYy8IobHvASq+FUFMlp1QzLM0JI=
|
||||
github.com/go-openapi/swag/stringutils v0.27.3/go.mod h1:lzRN95CxXmA03XcDWHLOb6nOMcxCqR5rGY0lOgsfRoM=
|
||||
github.com/go-openapi/swag/typeutils v0.27.3 h1:l6SSrx5eR5/WVwrGNzN6bQ9WqL04mrxNBl9YgQ3rcJ4=
|
||||
github.com/go-openapi/swag/typeutils v0.27.3/go.mod h1:Srm0xFNRZ1Y+vCxJclo5qzx8aj+1pAKda/YfFPrG0dQ=
|
||||
github.com/go-openapi/swag/yamlutils v0.27.3 h1:cRFCAoYtslYn9L9T0xWryHy1t7c1MACC+DMj3CLvwvs=
|
||||
github.com/go-openapi/swag/yamlutils v0.27.3/go.mod h1:6JYBGj8sw/NawMllyZY+cTA8Mzk2etS3ZBASdcyPsiU=
|
||||
github.com/go-openapi/testify/enable/yaml/v2 v2.6.0 h1:gGHwAJ0R/5jU8BEGDbfRNR3hL68dAVi84WuOApp29B0=
|
||||
github.com/go-openapi/testify/enable/yaml/v2 v2.6.0/go.mod h1:tY+St1SGq4NFl0QIqdTY4aEdbChAHxhyB77XQi9iJCo=
|
||||
github.com/go-openapi/testify/v2 v2.6.0 h1:5PKH2HE7YJ/LuRPQGvSxBRlFXNQhSetBLlGAgUEu3ug=
|
||||
github.com/go-openapi/testify/v2 v2.6.0/go.mod h1:SgsVHtfooshd0tublTtJ50FPKhujf47YRqauXXOUxfw=
|
||||
github.com/go-sql-driver/mysql v1.10.0 h1:Q+1LV8DkHJvSYAdR83XzuhDaTykuDx0l6fkXxoWCWfw=
|
||||
github.com/go-sql-driver/mysql v1.10.0/go.mod h1:M+cqaI7+xxXGG9swrdeUIoPG3Y3KCkF0pZej+SK+nWk=
|
||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
|
||||
|
||||
@@ -67,12 +67,9 @@ func (g *Generator) genCallGroup(ctx DirContext, proto parser.Proto, cfg *conf.C
|
||||
serviceName := stringx.From(service.Name).ToCamel()
|
||||
|
||||
// Collect only the message types actually used by this service's RPCs,
|
||||
// so that each client file only aliases its own request/response types.
|
||||
usedTypes := collection.NewSet[string]()
|
||||
for _, rpc := range service.RPC {
|
||||
usedTypes.Add(parser.CamelCase(rpc.RequestType))
|
||||
usedTypes.Add(parser.CamelCase(rpc.ReturnsType))
|
||||
}
|
||||
// so that each client file only aliases its own request/response types
|
||||
// and their same-file message dependencies.
|
||||
usedTypes := collectServiceUsedTypes(proto.Message, service)
|
||||
|
||||
alias := collection.NewSet[string]()
|
||||
var hasSameNameBetweenMessageAndService bool
|
||||
@@ -337,17 +334,85 @@ func (g *Generator) getInterfaceFuncs(goPackage, mainGoPackage string, service p
|
||||
return functions, nil
|
||||
}
|
||||
|
||||
// collectServiceUsedTypes returns the set of CamelCase message names that are
|
||||
// reachable from any of the service's RPC request or response types via field
|
||||
// references within the same proto file. This ensures per-service client files
|
||||
// alias their own request/response types and all transitively-referenced message
|
||||
// types, but never unrelated messages from other services.
|
||||
func collectServiceUsedTypes(messages []parser.Message, service parser.Service) *collection.Set[string] {
|
||||
messageByName := make(map[string]*proto.Message, len(messages))
|
||||
for _, item := range messages {
|
||||
msgName := parser.CamelCase(getMessageName(*item.Message))
|
||||
messageByName[msgName] = item.Message
|
||||
}
|
||||
|
||||
usedTypes := collection.NewSet[string]()
|
||||
for _, rpc := range service.RPC {
|
||||
collectMessageDependencies(rpc.RequestType, messageByName, usedTypes)
|
||||
collectMessageDependencies(rpc.ReturnsType, messageByName, usedTypes)
|
||||
}
|
||||
|
||||
return usedTypes
|
||||
}
|
||||
|
||||
// collectMessageDependencies recursively adds protoType and all message types
|
||||
// referenced by its fields into usedTypes, looking up messages by CamelCase
|
||||
// name in messageByName. The cycle guard (usedTypes.Contains) prevents
|
||||
// infinite recursion on circular field references.
|
||||
func collectMessageDependencies(protoType string, messageByName map[string]*proto.Message,
|
||||
usedTypes *collection.Set[string]) {
|
||||
for _, candidate := range messageTypeCandidates(protoType) {
|
||||
msg, ok := messageByName[candidate]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if usedTypes.Contains(candidate) {
|
||||
return
|
||||
}
|
||||
|
||||
usedTypes.Add(candidate)
|
||||
for _, elem := range msg.Elements {
|
||||
switch field := elem.(type) {
|
||||
case *proto.NormalField:
|
||||
collectMessageDependencies(field.Type, messageByName, usedTypes)
|
||||
case *proto.MapField:
|
||||
// Map key types are always scalars in proto3; only the value type
|
||||
// can be a message.
|
||||
collectMessageDependencies(field.Type, messageByName, usedTypes)
|
||||
case *proto.Oneof:
|
||||
for _, oneofElem := range field.Elements {
|
||||
if oneofField, ok := oneofElem.(*proto.OneOfField); ok {
|
||||
collectMessageDependencies(oneofField.Type, messageByName, usedTypes)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// messageTypeCandidates returns the CamelCase lookup keys to try for a proto
|
||||
// field type. Two candidates are produced to handle both simple names
|
||||
// ("MyMsg") and dotted/qualified names ("pkg.MyMsg" → "PkgMyMsg").
|
||||
func messageTypeCandidates(protoType string) []string {
|
||||
protoType = strings.TrimPrefix(protoType, ".")
|
||||
return []string{
|
||||
parser.CamelCase(protoType),
|
||||
parser.CamelCase(strings.ReplaceAll(protoType, ".", "_")),
|
||||
}
|
||||
}
|
||||
|
||||
// buildExtraImportLines converts a set of import paths into quoted import lines
|
||||
// for use in the call.tpl {{.extraImports}} placeholder.
|
||||
func buildExtraImportLines(extraImports *collection.Set[string]) string {
|
||||
if extraImports.Count() == 0 {
|
||||
return ""
|
||||
}
|
||||
keys := extraImports.Keys()
|
||||
sort.Strings(keys)
|
||||
lines := make([]string, 0, len(keys))
|
||||
for _, k := range keys {
|
||||
lines = append(lines, fmt.Sprintf(`"%s"`, k))
|
||||
}
|
||||
return strings.Join(lines, "\n\t")
|
||||
if extraImports.Count() == 0 {
|
||||
return ""
|
||||
}
|
||||
keys := extraImports.Keys()
|
||||
sort.Strings(keys)
|
||||
lines := make([]string, 0, len(keys))
|
||||
for _, k := range keys {
|
||||
lines = append(lines, fmt.Sprintf(`"%s"`, k))
|
||||
}
|
||||
return strings.Join(lines, "\n\t")
|
||||
}
|
||||
|
||||
@@ -34,50 +34,261 @@ func (m *mockDirContext) GetMain() Dir { return Dir{} }
|
||||
func (m *mockDirContext) GetServiceName() stringx.String { return stringx.From("test") }
|
||||
func (m *mockDirContext) SetPbDir(pbDir, grpcDir string) {}
|
||||
|
||||
// TestGenCallGroup_OnlyUsedTypesAliased verifies that in multi-service mode each
|
||||
// generated client file contains type aliases only for the message types actually
|
||||
// used by that service's RPCs (fix for issue #5481).
|
||||
// newTestDirContext builds a mockDirContext that writes generated files under
|
||||
// callBase, with a pb directory that differs (so alias generation is triggered).
|
||||
func newTestDirContext(t *testing.T, callBase, pbBase string, services ...string) *mockDirContext {
|
||||
t.Helper()
|
||||
for _, svc := range services {
|
||||
require.NoError(t, os.MkdirAll(filepath.Join(callBase, strings.ToLower(svc)), 0755))
|
||||
}
|
||||
require.NoError(t, os.MkdirAll(pbBase, 0755))
|
||||
return &mockDirContext{
|
||||
callDir: Dir{
|
||||
Filename: callBase,
|
||||
Package: "example.com/test/call",
|
||||
Base: "call",
|
||||
GetChildPackage: func(childPath string) (string, error) {
|
||||
return filepath.Join(callBase, strings.ToLower(childPath)), nil
|
||||
},
|
||||
},
|
||||
pbDir: Dir{Filename: pbBase, Package: "example.com/test/pb", Base: "pb"},
|
||||
protoGo: Dir{
|
||||
// Must differ from service dir names so isCallPkgSameToPbPkg stays
|
||||
// false and alias generation is triggered.
|
||||
Filename: pbBase,
|
||||
Package: "example.com/test/pb",
|
||||
Base: "pb",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// ---- unit tests for collectServiceUsedTypes --------------------------------
|
||||
|
||||
// TestCollectServiceUsedTypes_DirectOnly verifies that request and response
|
||||
// types with no message fields are collected as-is.
|
||||
func TestCollectServiceUsedTypes_DirectOnly(t *testing.T) {
|
||||
messages := []parser.Message{
|
||||
{Message: &proto.Message{Name: "AReq"}},
|
||||
{Message: &proto.Message{Name: "AResp"}},
|
||||
{Message: &proto.Message{Name: "Unrelated"}},
|
||||
}
|
||||
service := parser.Service{
|
||||
Service: &proto.Service{Name: "ServiceA"},
|
||||
RPC: []*parser.RPC{
|
||||
{RPC: &proto.RPC{Name: "Do", RequestType: "AReq", ReturnsType: "AResp"}},
|
||||
},
|
||||
}
|
||||
|
||||
got := collectServiceUsedTypes(messages, service)
|
||||
|
||||
assert.True(t, got.Contains("AReq"))
|
||||
assert.True(t, got.Contains("AResp"))
|
||||
assert.False(t, got.Contains("Unrelated"), "unrelated message must not be collected")
|
||||
}
|
||||
|
||||
// TestCollectServiceUsedTypes_NestedNormalField verifies that a message type
|
||||
// referenced via a NormalField inside a response is transitively collected
|
||||
// (regression test for issue #5618).
|
||||
func TestCollectServiceUsedTypes_NestedNormalField(t *testing.T) {
|
||||
messages := []parser.Message{
|
||||
{Message: &proto.Message{Name: "AReq"}},
|
||||
{Message: &proto.Message{
|
||||
Name: "AResp",
|
||||
Elements: []proto.Visitee{
|
||||
&proto.NormalField{Field: &proto.Field{Name: "items", Type: "AItem"}},
|
||||
},
|
||||
}},
|
||||
{Message: &proto.Message{Name: "AItem"}},
|
||||
}
|
||||
service := parser.Service{
|
||||
Service: &proto.Service{Name: "ServiceA"},
|
||||
RPC: []*parser.RPC{
|
||||
{RPC: &proto.RPC{Name: "List", RequestType: "AReq", ReturnsType: "AResp"}},
|
||||
},
|
||||
}
|
||||
|
||||
got := collectServiceUsedTypes(messages, service)
|
||||
|
||||
assert.True(t, got.Contains("AReq"))
|
||||
assert.True(t, got.Contains("AResp"))
|
||||
assert.True(t, got.Contains("AItem"), "field type AItem must be transitively collected")
|
||||
}
|
||||
|
||||
// TestCollectServiceUsedTypes_MapValueField verifies that the value type of a
|
||||
// MapField inside a response message is transitively collected.
|
||||
func TestCollectServiceUsedTypes_MapValueField(t *testing.T) {
|
||||
messages := []parser.Message{
|
||||
{Message: &proto.Message{Name: "AReq"}},
|
||||
{Message: &proto.Message{
|
||||
Name: "AResp",
|
||||
Elements: []proto.Visitee{
|
||||
&proto.MapField{KeyType: "string", Field: &proto.Field{Name: "index", Type: "AItem"}},
|
||||
},
|
||||
}},
|
||||
{Message: &proto.Message{Name: "AItem"}},
|
||||
}
|
||||
service := parser.Service{
|
||||
Service: &proto.Service{Name: "ServiceA"},
|
||||
RPC: []*parser.RPC{
|
||||
{RPC: &proto.RPC{Name: "GetMap", RequestType: "AReq", ReturnsType: "AResp"}},
|
||||
},
|
||||
}
|
||||
|
||||
got := collectServiceUsedTypes(messages, service)
|
||||
|
||||
assert.True(t, got.Contains("AResp"))
|
||||
assert.True(t, got.Contains("AItem"), "map value type AItem must be transitively collected")
|
||||
}
|
||||
|
||||
// TestCollectServiceUsedTypes_OneofField verifies that message types referenced
|
||||
// inside a Oneof element are transitively collected.
|
||||
func TestCollectServiceUsedTypes_OneofField(t *testing.T) {
|
||||
oneof := &proto.Oneof{Name: "result"}
|
||||
oneof.Elements = []proto.Visitee{
|
||||
&proto.OneOfField{Field: &proto.Field{Name: "success", Type: "SuccessMsg"}},
|
||||
&proto.OneOfField{Field: &proto.Field{Name: "failure", Type: "FailureMsg"}},
|
||||
}
|
||||
messages := []parser.Message{
|
||||
{Message: &proto.Message{Name: "AReq"}},
|
||||
{Message: &proto.Message{
|
||||
Name: "AResp",
|
||||
Elements: []proto.Visitee{oneof},
|
||||
}},
|
||||
{Message: &proto.Message{Name: "SuccessMsg"}},
|
||||
{Message: &proto.Message{Name: "FailureMsg"}},
|
||||
}
|
||||
service := parser.Service{
|
||||
Service: &proto.Service{Name: "ServiceA"},
|
||||
RPC: []*parser.RPC{
|
||||
{RPC: &proto.RPC{Name: "Do", RequestType: "AReq", ReturnsType: "AResp"}},
|
||||
},
|
||||
}
|
||||
|
||||
got := collectServiceUsedTypes(messages, service)
|
||||
|
||||
assert.True(t, got.Contains("AResp"))
|
||||
assert.True(t, got.Contains("SuccessMsg"), "oneof field type SuccessMsg must be collected")
|
||||
assert.True(t, got.Contains("FailureMsg"), "oneof field type FailureMsg must be collected")
|
||||
}
|
||||
|
||||
// TestCollectServiceUsedTypes_MultiLevelTransitive verifies that a chain
|
||||
// AResp → BMsg → CMsg is fully collected (multi-level transitivity).
|
||||
func TestCollectServiceUsedTypes_MultiLevelTransitive(t *testing.T) {
|
||||
messages := []parser.Message{
|
||||
{Message: &proto.Message{Name: "AReq"}},
|
||||
{Message: &proto.Message{
|
||||
Name: "AResp",
|
||||
Elements: []proto.Visitee{
|
||||
&proto.NormalField{Field: &proto.Field{Name: "b", Type: "BMsg"}},
|
||||
},
|
||||
}},
|
||||
{Message: &proto.Message{
|
||||
Name: "BMsg",
|
||||
Elements: []proto.Visitee{
|
||||
&proto.NormalField{Field: &proto.Field{Name: "c", Type: "CMsg"}},
|
||||
},
|
||||
}},
|
||||
{Message: &proto.Message{Name: "CMsg"}},
|
||||
}
|
||||
service := parser.Service{
|
||||
Service: &proto.Service{Name: "ServiceA"},
|
||||
RPC: []*parser.RPC{
|
||||
{RPC: &proto.RPC{Name: "Do", RequestType: "AReq", ReturnsType: "AResp"}},
|
||||
},
|
||||
}
|
||||
|
||||
got := collectServiceUsedTypes(messages, service)
|
||||
|
||||
assert.True(t, got.Contains("AReq"))
|
||||
assert.True(t, got.Contains("AResp"))
|
||||
assert.True(t, got.Contains("BMsg"), "BMsg must be transitively collected via AResp")
|
||||
assert.True(t, got.Contains("CMsg"), "CMsg must be transitively collected via BMsg")
|
||||
}
|
||||
|
||||
// TestCollectServiceUsedTypes_CycleDetection verifies that circular field
|
||||
// references (AResp ↔ BMsg) do not cause infinite recursion.
|
||||
func TestCollectServiceUsedTypes_CycleDetection(t *testing.T) {
|
||||
messages := []parser.Message{
|
||||
{Message: &proto.Message{Name: "AReq"}},
|
||||
{Message: &proto.Message{
|
||||
Name: "AResp",
|
||||
Elements: []proto.Visitee{
|
||||
&proto.NormalField{Field: &proto.Field{Name: "b", Type: "BMsg"}},
|
||||
},
|
||||
}},
|
||||
{Message: &proto.Message{
|
||||
Name: "BMsg",
|
||||
Elements: []proto.Visitee{
|
||||
// circular back-reference to AResp
|
||||
&proto.NormalField{Field: &proto.Field{Name: "a", Type: "AResp"}},
|
||||
},
|
||||
}},
|
||||
}
|
||||
service := parser.Service{
|
||||
Service: &proto.Service{Name: "ServiceA"},
|
||||
RPC: []*parser.RPC{
|
||||
{RPC: &proto.RPC{Name: "Do", RequestType: "AReq", ReturnsType: "AResp"}},
|
||||
},
|
||||
}
|
||||
|
||||
// Must not panic or loop; both messages are reachable.
|
||||
got := collectServiceUsedTypes(messages, service)
|
||||
|
||||
assert.True(t, got.Contains("AResp"))
|
||||
assert.True(t, got.Contains("BMsg"))
|
||||
}
|
||||
|
||||
// TestCollectServiceUsedTypes_ExcludesUnrelatedService verifies that messages
|
||||
// belonging only to another service are not included.
|
||||
func TestCollectServiceUsedTypes_ExcludesUnrelatedService(t *testing.T) {
|
||||
messages := []parser.Message{
|
||||
{Message: &proto.Message{Name: "AReq"}},
|
||||
{Message: &proto.Message{Name: "AResp"}},
|
||||
{Message: &proto.Message{Name: "BReq"}},
|
||||
{Message: &proto.Message{Name: "BResp"}},
|
||||
}
|
||||
service := parser.Service{
|
||||
Service: &proto.Service{Name: "ServiceA"},
|
||||
RPC: []*parser.RPC{
|
||||
{RPC: &proto.RPC{Name: "DoA", RequestType: "AReq", ReturnsType: "AResp"}},
|
||||
},
|
||||
}
|
||||
|
||||
got := collectServiceUsedTypes(messages, service)
|
||||
|
||||
assert.True(t, got.Contains("AReq"))
|
||||
assert.True(t, got.Contains("AResp"))
|
||||
assert.False(t, got.Contains("BReq"), "BReq belongs to ServiceB and must be excluded")
|
||||
assert.False(t, got.Contains("BResp"), "BResp belongs to ServiceB and must be excluded")
|
||||
}
|
||||
|
||||
// ---- integration tests via genCallGroup ------------------------------------
|
||||
|
||||
// TestGenCallGroup_OnlyUsedTypesAliased verifies that in multi-service mode
|
||||
// each generated client file aliases only its own request/response types and
|
||||
// their transitive field dependencies (fix for issues #5481 and #5618).
|
||||
func TestGenCallGroup_OnlyUsedTypesAliased(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
callBase := filepath.Join(tmpDir, "call")
|
||||
pbBase := filepath.Join(tmpDir, "pb")
|
||||
|
||||
// Pre-create subdirs that genCallGroup will write into.
|
||||
require.NoError(t, os.MkdirAll(filepath.Join(callBase, "servicea"), 0755))
|
||||
require.NoError(t, os.MkdirAll(filepath.Join(callBase, "serviceb"), 0755))
|
||||
require.NoError(t, os.MkdirAll(pbBase, 0755))
|
||||
mctx := newTestDirContext(t, callBase, pbBase, "ServiceA", "ServiceB")
|
||||
|
||||
mctx := &mockDirContext{
|
||||
callDir: Dir{
|
||||
Filename: callBase,
|
||||
Package: "example.com/multitest/call",
|
||||
Base: "call",
|
||||
GetChildPackage: func(childPath string) (string, error) {
|
||||
// Return a package path whose Base() is the lowercase service name.
|
||||
return filepath.Join(callBase, strings.ToLower(childPath)), nil
|
||||
},
|
||||
},
|
||||
pbDir: Dir{
|
||||
Filename: pbBase,
|
||||
Package: "example.com/multitest/pb",
|
||||
Base: "pb",
|
||||
},
|
||||
protoGo: Dir{
|
||||
// Must differ from "servicea"/"serviceb" so isCallPkgSameToPbPkg stays false
|
||||
// and alias generation is triggered.
|
||||
Filename: pbBase,
|
||||
Package: "example.com/multitest/pb",
|
||||
Base: "pb",
|
||||
},
|
||||
}
|
||||
|
||||
// Proto with two services that use completely disjoint message types.
|
||||
// ServiceA: AResp contains a NormalField of type AItem (issue #5618).
|
||||
// ServiceB: BResp has no nested message fields.
|
||||
// AItem must appear in ServiceA's file but not ServiceB's.
|
||||
protoData := parser.Proto{
|
||||
Name: "multi.proto",
|
||||
PbPackage: "pb",
|
||||
Message: []parser.Message{
|
||||
{Message: &proto.Message{Name: "AReq"}},
|
||||
{Message: &proto.Message{Name: "AResp"}},
|
||||
{Message: &proto.Message{
|
||||
Name: "AResp",
|
||||
Elements: []proto.Visitee{
|
||||
&proto.NormalField{Field: &proto.Field{Name: "items", Type: "AItem"}},
|
||||
},
|
||||
}},
|
||||
{Message: &proto.Message{Name: "AItem"}},
|
||||
{Message: &proto.Message{Name: "BReq"}},
|
||||
{Message: &proto.Message{Name: "BResp"}},
|
||||
},
|
||||
@@ -99,29 +310,163 @@ func TestGenCallGroup_OnlyUsedTypesAliased(t *testing.T) {
|
||||
|
||||
cfg, err := conf.NewConfig("")
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, NewGenerator("gozero", false).genCallGroup(mctx, protoData, cfg))
|
||||
|
||||
g := NewGenerator("gozero", false)
|
||||
require.NoError(t, g.genCallGroup(mctx, protoData, cfg))
|
||||
aFile := normalizeWS(readGenFile(t, callBase, "servicea", "servicea.go"))
|
||||
assert.Contains(t, aFile, "AReq = pb.AReq", "ServiceA must alias AReq")
|
||||
assert.Contains(t, aFile, "AResp = pb.AResp", "ServiceA must alias AResp")
|
||||
assert.Contains(t, aFile, "AItem = pb.AItem", "ServiceA must alias AItem (transitive NormalField)")
|
||||
assert.NotContains(t, aFile, "BReq = pb.BReq", "ServiceA must not alias BReq")
|
||||
assert.NotContains(t, aFile, "BResp = pb.BResp", "ServiceA must not alias BResp")
|
||||
|
||||
// servicea/servicea.go — aliases for AReq/AResp only
|
||||
aContent, err := os.ReadFile(filepath.Join(callBase, "servicea", "servicea.go"))
|
||||
bFile := normalizeWS(readGenFile(t, callBase, "serviceb", "serviceb.go"))
|
||||
assert.Contains(t, bFile, "BReq = pb.BReq", "ServiceB must alias BReq")
|
||||
assert.Contains(t, bFile, "BResp = pb.BResp", "ServiceB must alias BResp")
|
||||
assert.NotContains(t, bFile, "AReq = pb.AReq", "ServiceB must not alias AReq")
|
||||
assert.NotContains(t, bFile, "AResp = pb.AResp", "ServiceB must not alias AResp")
|
||||
assert.NotContains(t, bFile, "AItem = pb.AItem", "ServiceB must not alias AItem")
|
||||
}
|
||||
|
||||
// TestGenCallGroup_MapValueAliased verifies that the value type of a MapField
|
||||
// inside a service response is included in the generated aliases.
|
||||
func TestGenCallGroup_MapValueAliased(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
callBase := filepath.Join(tmpDir, "call")
|
||||
pbBase := filepath.Join(tmpDir, "pb")
|
||||
|
||||
mctx := newTestDirContext(t, callBase, pbBase, "ServiceA")
|
||||
|
||||
protoData := parser.Proto{
|
||||
Name: "map.proto",
|
||||
PbPackage: "pb",
|
||||
Message: []parser.Message{
|
||||
{Message: &proto.Message{Name: "AReq"}},
|
||||
{Message: &proto.Message{
|
||||
Name: "AResp",
|
||||
Elements: []proto.Visitee{
|
||||
&proto.MapField{KeyType: "string", Field: &proto.Field{Name: "index", Type: "AItem"}},
|
||||
},
|
||||
}},
|
||||
{Message: &proto.Message{Name: "AItem"}},
|
||||
},
|
||||
Service: parser.Services{
|
||||
{
|
||||
Service: &proto.Service{Name: "ServiceA"},
|
||||
RPC: []*parser.RPC{
|
||||
{RPC: &proto.RPC{Name: "GetMap", RequestType: "AReq", ReturnsType: "AResp"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
cfg, err := conf.NewConfig("")
|
||||
require.NoError(t, err)
|
||||
aFile := normalizeWS(string(aContent))
|
||||
require.NoError(t, NewGenerator("gozero", false).genCallGroup(mctx, protoData, cfg))
|
||||
|
||||
assert.Contains(t, aFile, "AReq = pb.AReq", "ServiceA file should alias AReq")
|
||||
assert.Contains(t, aFile, "AResp = pb.AResp", "ServiceA file should alias AResp")
|
||||
assert.NotContains(t, aFile, "BReq = pb.BReq", "ServiceA file must not alias BReq")
|
||||
assert.NotContains(t, aFile, "BResp = pb.BResp", "ServiceA file must not alias BResp")
|
||||
aFile := normalizeWS(readGenFile(t, callBase, "servicea", "servicea.go"))
|
||||
assert.Contains(t, aFile, "AResp = pb.AResp")
|
||||
assert.Contains(t, aFile, "AItem = pb.AItem", "map value type AItem must be aliased")
|
||||
}
|
||||
|
||||
// serviceb/serviceb.go — aliases for BReq/BResp only
|
||||
bContent, err := os.ReadFile(filepath.Join(callBase, "serviceb", "serviceb.go"))
|
||||
// TestGenCallGroup_OneofAliased verifies that message types referenced inside a
|
||||
// Oneof element are included in the generated aliases.
|
||||
func TestGenCallGroup_OneofAliased(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
callBase := filepath.Join(tmpDir, "call")
|
||||
pbBase := filepath.Join(tmpDir, "pb")
|
||||
|
||||
mctx := newTestDirContext(t, callBase, pbBase, "ServiceA")
|
||||
|
||||
oneof := &proto.Oneof{Name: "result"}
|
||||
oneof.Elements = []proto.Visitee{
|
||||
&proto.OneOfField{Field: &proto.Field{Name: "ok", Type: "SuccessMsg"}},
|
||||
&proto.OneOfField{Field: &proto.Field{Name: "err", Type: "FailureMsg"}},
|
||||
}
|
||||
protoData := parser.Proto{
|
||||
Name: "oneof.proto",
|
||||
PbPackage: "pb",
|
||||
Message: []parser.Message{
|
||||
{Message: &proto.Message{Name: "AReq"}},
|
||||
{Message: &proto.Message{
|
||||
Name: "AResp",
|
||||
Elements: []proto.Visitee{oneof},
|
||||
}},
|
||||
{Message: &proto.Message{Name: "SuccessMsg"}},
|
||||
{Message: &proto.Message{Name: "FailureMsg"}},
|
||||
},
|
||||
Service: parser.Services{
|
||||
{
|
||||
Service: &proto.Service{Name: "ServiceA"},
|
||||
RPC: []*parser.RPC{
|
||||
{RPC: &proto.RPC{Name: "Do", RequestType: "AReq", ReturnsType: "AResp"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
cfg, err := conf.NewConfig("")
|
||||
require.NoError(t, err)
|
||||
bFile := normalizeWS(string(bContent))
|
||||
require.NoError(t, NewGenerator("gozero", false).genCallGroup(mctx, protoData, cfg))
|
||||
|
||||
assert.Contains(t, bFile, "BReq = pb.BReq", "ServiceB file should alias BReq")
|
||||
assert.Contains(t, bFile, "BResp = pb.BResp", "ServiceB file should alias BResp")
|
||||
assert.NotContains(t, bFile, "AReq = pb.AReq", "ServiceB file must not alias AReq")
|
||||
assert.NotContains(t, bFile, "AResp = pb.AResp", "ServiceB file must not alias AResp")
|
||||
aFile := normalizeWS(readGenFile(t, callBase, "servicea", "servicea.go"))
|
||||
assert.Contains(t, aFile, "SuccessMsg = pb.SuccessMsg", "oneof type SuccessMsg must be aliased")
|
||||
assert.Contains(t, aFile, "FailureMsg = pb.FailureMsg", "oneof type FailureMsg must be aliased")
|
||||
}
|
||||
|
||||
// TestGenCallGroup_MultiLevelTransitiveAliased verifies that a dependency chain
|
||||
// AResp → BMsg → CMsg causes all three types to be aliased in the client file.
|
||||
func TestGenCallGroup_MultiLevelTransitiveAliased(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
callBase := filepath.Join(tmpDir, "call")
|
||||
pbBase := filepath.Join(tmpDir, "pb")
|
||||
|
||||
mctx := newTestDirContext(t, callBase, pbBase, "ServiceA")
|
||||
|
||||
protoData := parser.Proto{
|
||||
Name: "transitive.proto",
|
||||
PbPackage: "pb",
|
||||
Message: []parser.Message{
|
||||
{Message: &proto.Message{Name: "AReq"}},
|
||||
{Message: &proto.Message{
|
||||
Name: "AResp",
|
||||
Elements: []proto.Visitee{
|
||||
&proto.NormalField{Field: &proto.Field{Name: "b", Type: "BMsg"}},
|
||||
},
|
||||
}},
|
||||
{Message: &proto.Message{
|
||||
Name: "BMsg",
|
||||
Elements: []proto.Visitee{
|
||||
&proto.NormalField{Field: &proto.Field{Name: "c", Type: "CMsg"}},
|
||||
},
|
||||
}},
|
||||
{Message: &proto.Message{Name: "CMsg"}},
|
||||
},
|
||||
Service: parser.Services{
|
||||
{
|
||||
Service: &proto.Service{Name: "ServiceA"},
|
||||
RPC: []*parser.RPC{
|
||||
{RPC: &proto.RPC{Name: "Do", RequestType: "AReq", ReturnsType: "AResp"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
cfg, err := conf.NewConfig("")
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, NewGenerator("gozero", false).genCallGroup(mctx, protoData, cfg))
|
||||
|
||||
aFile := normalizeWS(readGenFile(t, callBase, "servicea", "servicea.go"))
|
||||
assert.Contains(t, aFile, "AResp = pb.AResp")
|
||||
assert.Contains(t, aFile, "BMsg = pb.BMsg", "BMsg must be transitively aliased via AResp")
|
||||
assert.Contains(t, aFile, "CMsg = pb.CMsg", "CMsg must be transitively aliased via BMsg")
|
||||
}
|
||||
|
||||
// readGenFile reads a generated file relative to callBase and returns its content.
|
||||
func readGenFile(t *testing.T, callBase string, parts ...string) string {
|
||||
t.Helper()
|
||||
content, err := os.ReadFile(filepath.Join(append([]string{callBase}, parts...)...))
|
||||
require.NoError(t, err)
|
||||
return string(content)
|
||||
}
|
||||
|
||||
// normalizeWS replaces runs of whitespace with a single space.
|
||||
|
||||
Reference in New Issue
Block a user