mirror of
https://github.com/zeromicro/go-zero.git
synced 2026-08-25 17:42:46 +08:00
Compare commits
7 Commits
f910257ec9
...
v1.10.3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
925f8a2bcc | ||
|
|
36f2619756 | ||
|
|
84313f2e92 | ||
|
|
394ffcc19a | ||
|
|
565bcb3f21 | ||
|
|
6a6b81ef20 | ||
|
|
35a7ca9d98 |
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'
|
||||
|
||||
|
||||
@@ -2,11 +2,12 @@ package collection
|
||||
|
||||
import "sync"
|
||||
|
||||
const queueGrowThreshold = 256
|
||||
|
||||
// A Queue is a FIFO queue.
|
||||
type Queue struct {
|
||||
lock sync.Mutex
|
||||
elements []any
|
||||
size int
|
||||
head int
|
||||
tail int
|
||||
count int
|
||||
@@ -14,9 +15,12 @@ type Queue struct {
|
||||
|
||||
// NewQueue returns a Queue object.
|
||||
func NewQueue(size int) *Queue {
|
||||
if size < 1 {
|
||||
panic("size must be greater than 0")
|
||||
}
|
||||
|
||||
return &Queue{
|
||||
elements: make([]any, size),
|
||||
size: size,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,12 +38,12 @@ func (q *Queue) Put(element any) {
|
||||
q.lock.Lock()
|
||||
defer q.lock.Unlock()
|
||||
|
||||
if q.head == q.tail && q.count > 0 {
|
||||
nodes := make([]any, len(q.elements)+q.size)
|
||||
copy(nodes, q.elements[q.head:])
|
||||
copy(nodes[len(q.elements)-q.head:], q.elements[:q.head])
|
||||
if q.count == len(q.elements) {
|
||||
nodes := make([]any, nextQueueCapacity(len(q.elements)))
|
||||
n := copy(nodes, q.elements[q.head:])
|
||||
copy(nodes[n:], q.elements[:q.head])
|
||||
q.head = 0
|
||||
q.tail = len(q.elements)
|
||||
q.tail = q.count
|
||||
q.elements = nodes
|
||||
}
|
||||
|
||||
@@ -58,8 +62,19 @@ func (q *Queue) Take() (any, bool) {
|
||||
}
|
||||
|
||||
element := q.elements[q.head]
|
||||
q.elements[q.head] = nil
|
||||
q.head = (q.head + 1) % len(q.elements)
|
||||
q.count--
|
||||
|
||||
return element, true
|
||||
}
|
||||
|
||||
func nextQueueCapacity(capacity int) int {
|
||||
if capacity < queueGrowThreshold {
|
||||
return capacity << 1
|
||||
}
|
||||
|
||||
// Use a growth curve similar to Go slices: double small queues, then
|
||||
// transition smoothly toward 1.25x growth for larger queues.
|
||||
return capacity + ((capacity + 3*queueGrowThreshold) >> 2)
|
||||
}
|
||||
|
||||
@@ -6,96 +6,263 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestFifo(t *testing.T) {
|
||||
elements := [][]byte{
|
||||
[]byte("hello"),
|
||||
[]byte("world"),
|
||||
[]byte("again"),
|
||||
}
|
||||
queue := NewQueue(8)
|
||||
for i := range elements {
|
||||
queue.Put(elements[i])
|
||||
func TestQueueOrder(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
size int
|
||||
initial []int
|
||||
takeBefore int
|
||||
additional []int
|
||||
wantCapacity int
|
||||
}{
|
||||
{
|
||||
name: "within initial capacity",
|
||||
size: 8,
|
||||
initial: []int{1, 2, 3},
|
||||
wantCapacity: 8,
|
||||
},
|
||||
{
|
||||
name: "grow from beginning",
|
||||
size: 2,
|
||||
initial: []int{1, 2, 3},
|
||||
wantCapacity: 4,
|
||||
},
|
||||
{
|
||||
name: "grow after wrapping",
|
||||
size: 4,
|
||||
initial: []int{1, 2, 3, 4},
|
||||
takeBefore: 1,
|
||||
additional: []int{5, 6},
|
||||
wantCapacity: 8,
|
||||
},
|
||||
{
|
||||
name: "grow repeatedly",
|
||||
size: 1,
|
||||
initial: sequence(20),
|
||||
wantCapacity: 32,
|
||||
},
|
||||
{
|
||||
name: "grow above threshold",
|
||||
size: queueGrowThreshold,
|
||||
initial: sequence(queueGrowThreshold + 1),
|
||||
wantCapacity: queueGrowThreshold * 2,
|
||||
},
|
||||
{
|
||||
name: "grow well above threshold",
|
||||
size: 1024,
|
||||
initial: sequence(1025),
|
||||
wantCapacity: 1472,
|
||||
},
|
||||
}
|
||||
|
||||
for _, element := range elements {
|
||||
body, ok := queue.Take()
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, string(element), string(body.([]byte)))
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
queue := NewQueue(test.size)
|
||||
assert.True(t, queue.Empty())
|
||||
|
||||
for _, value := range test.initial {
|
||||
queue.Put(value)
|
||||
}
|
||||
for i := 0; i < test.takeBefore; i++ {
|
||||
value, ok := queue.Take()
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, test.initial[i], value)
|
||||
}
|
||||
for _, value := range test.additional {
|
||||
queue.Put(value)
|
||||
}
|
||||
|
||||
want := append([]int(nil), test.initial[test.takeBefore:]...)
|
||||
want = append(want, test.additional...)
|
||||
for _, expected := range want {
|
||||
actual, ok := queue.Take()
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, expected, actual)
|
||||
}
|
||||
|
||||
assert.Equal(t, test.wantCapacity, len(queue.elements))
|
||||
assert.True(t, queue.Empty())
|
||||
_, ok := queue.Take()
|
||||
assert.False(t, ok)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTakeTooMany(t *testing.T) {
|
||||
elements := [][]byte{
|
||||
[]byte("hello"),
|
||||
[]byte("world"),
|
||||
[]byte("again"),
|
||||
}
|
||||
queue := NewQueue(8)
|
||||
for i := range elements {
|
||||
queue.Put(elements[i])
|
||||
func TestQueueTakeClearsElement(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
size int
|
||||
operations string
|
||||
}{
|
||||
{
|
||||
name: "take from beginning",
|
||||
size: 2,
|
||||
operations: "ppt",
|
||||
},
|
||||
{
|
||||
name: "take after wrapping",
|
||||
size: 2,
|
||||
operations: "pptptt",
|
||||
},
|
||||
{
|
||||
name: "take after growing",
|
||||
size: 2,
|
||||
operations: "pppt",
|
||||
},
|
||||
}
|
||||
|
||||
for range elements {
|
||||
queue.Take()
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
queue := NewQueue(test.size)
|
||||
value := 0
|
||||
|
||||
assert.True(t, queue.Empty())
|
||||
_, ok := queue.Take()
|
||||
assert.False(t, ok)
|
||||
}
|
||||
|
||||
func TestPutMore(t *testing.T) {
|
||||
elements := [][]byte{
|
||||
[]byte("hello"),
|
||||
[]byte("world"),
|
||||
[]byte("again"),
|
||||
}
|
||||
queue := NewQueue(2)
|
||||
for i := range elements {
|
||||
queue.Put(elements[i])
|
||||
}
|
||||
|
||||
for _, element := range elements {
|
||||
body, ok := queue.Take()
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, string(element), string(body.([]byte)))
|
||||
for _, operation := range test.operations {
|
||||
switch operation {
|
||||
case 'p':
|
||||
value++
|
||||
element := new(int)
|
||||
*element = value
|
||||
queue.Put(element)
|
||||
case 't':
|
||||
index := queue.head
|
||||
_, ok := queue.Take()
|
||||
assert.True(t, ok)
|
||||
assert.Nil(t, queue.elements[index])
|
||||
default:
|
||||
t.Fatalf("unknown operation: %q", operation)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPutMoreWithHeaderNotZero(t *testing.T) {
|
||||
elements := [][]byte{
|
||||
[]byte("hello"),
|
||||
[]byte("world"),
|
||||
[]byte("again"),
|
||||
}
|
||||
queue := NewQueue(4)
|
||||
for i := range elements {
|
||||
queue.Put(elements[i])
|
||||
func TestNewQueueWithInvalidSize(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
size int
|
||||
}{
|
||||
{
|
||||
name: "zero",
|
||||
},
|
||||
{
|
||||
name: "negative",
|
||||
size: -1,
|
||||
},
|
||||
}
|
||||
|
||||
// take 1
|
||||
body, ok := queue.Take()
|
||||
assert.True(t, ok)
|
||||
element, ok := body.([]byte)
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, element, []byte("hello"))
|
||||
|
||||
// put more
|
||||
queue.Put([]byte("b4"))
|
||||
queue.Put([]byte("b5")) // will store in elements[0]
|
||||
queue.Put([]byte("b6")) // cause expansion
|
||||
|
||||
results := [][]byte{
|
||||
[]byte("world"),
|
||||
[]byte("again"),
|
||||
[]byte("b4"),
|
||||
[]byte("b5"),
|
||||
[]byte("b6"),
|
||||
}
|
||||
|
||||
for _, element := range results {
|
||||
body, ok := queue.Take()
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, string(element), string(body.([]byte)))
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
assert.Panics(t, func() {
|
||||
NewQueue(test.size)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkQueueGrowth(b *testing.B) {
|
||||
tests := []struct {
|
||||
name string
|
||||
size int
|
||||
count int
|
||||
}{
|
||||
{
|
||||
name: "initial_1_count_256",
|
||||
size: 1,
|
||||
count: 256,
|
||||
},
|
||||
{
|
||||
name: "initial_1_count_4096",
|
||||
size: 1,
|
||||
count: 4096,
|
||||
},
|
||||
{
|
||||
name: "initial_1_count_65536",
|
||||
size: 1,
|
||||
count: 65536,
|
||||
},
|
||||
{
|
||||
name: "initial_8_count_4096",
|
||||
size: 8,
|
||||
count: 4096,
|
||||
},
|
||||
{
|
||||
name: "initial_256_count_4096",
|
||||
size: 256,
|
||||
count: 4096,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
b.Run(test.name, func(b *testing.B) {
|
||||
elements := make([]any, test.count)
|
||||
for i := range elements {
|
||||
elements[i] = i
|
||||
}
|
||||
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
queue := NewQueue(test.size)
|
||||
for _, element := range elements {
|
||||
queue.Put(element)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkQueueWrappedGrowth(b *testing.B) {
|
||||
tests := []struct {
|
||||
name string
|
||||
size int
|
||||
}{
|
||||
{
|
||||
name: "capacity_8",
|
||||
size: 8,
|
||||
},
|
||||
{
|
||||
name: "capacity_256",
|
||||
size: 256,
|
||||
},
|
||||
{
|
||||
name: "capacity_4096",
|
||||
size: 4096,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
b.Run(test.name, func(b *testing.B) {
|
||||
elements := make([]any, test.size)
|
||||
for i := range elements {
|
||||
elements[i] = i
|
||||
}
|
||||
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
queue := NewQueue(test.size)
|
||||
for _, element := range elements {
|
||||
queue.Put(element)
|
||||
}
|
||||
for j := 0; j < test.size/2; j++ {
|
||||
queue.Take()
|
||||
}
|
||||
for j := 0; j < test.size/2; j++ {
|
||||
queue.Put(elements[j])
|
||||
}
|
||||
|
||||
// The queue is full and wrapped. One more Put triggers growth
|
||||
// and copies both sides of the ring into FIFO order.
|
||||
queue.Put(elements[0])
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func sequence(size int) []int {
|
||||
values := make([]int, size)
|
||||
for i := range values {
|
||||
values[i] = i
|
||||
}
|
||||
return values
|
||||
}
|
||||
|
||||
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=
|
||||
|
||||
69
tools/goctl/api/parser/inline_tag_test.go
Normal file
69
tools/goctl/api/parser/inline_tag_test.go
Normal file
@@ -0,0 +1,69 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/zeromicro/go-zero/tools/goctl/api/spec"
|
||||
)
|
||||
|
||||
const inlineTagAPI = `
|
||||
syntax = "v1"
|
||||
|
||||
type (
|
||||
Auth {
|
||||
Token string ` + "`header:\"Authorization\"`" + `
|
||||
}
|
||||
Middle {
|
||||
Auth
|
||||
}
|
||||
PointerRequest {
|
||||
*Auth
|
||||
}
|
||||
NestedRequest {
|
||||
Middle
|
||||
}
|
||||
RecursiveRequest {
|
||||
Token string ` + "`header:\"X-Token\"`" + `
|
||||
*RecursiveRequest
|
||||
}
|
||||
)
|
||||
|
||||
service test-api {
|
||||
@handler Pointer
|
||||
get /pointer (PointerRequest)
|
||||
|
||||
@handler Nested
|
||||
get /nested (NestedRequest)
|
||||
|
||||
@handler Recursive
|
||||
get /recursive (RecursiveRequest)
|
||||
}
|
||||
`
|
||||
|
||||
func TestParseContentResolvesInlineTypesForTagLookup(t *testing.T) {
|
||||
apiSpec, err := ParseContent(inlineTagAPI)
|
||||
require.NoError(t, err)
|
||||
|
||||
for _, name := range []string{"PointerRequest", "NestedRequest", "RecursiveRequest"} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
tp := findStructByName(t, apiSpec.Types, name)
|
||||
require.NotEmpty(t, tp.GetTagMembers("header"))
|
||||
require.Empty(t, tp.GetTagMembers("path"))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func findStructByName(t *testing.T, types []spec.Type, name string) spec.DefineStruct {
|
||||
t.Helper()
|
||||
for _, tp := range types {
|
||||
if tp.Name() == name {
|
||||
defined, ok := tp.(spec.DefineStruct)
|
||||
require.True(t, ok)
|
||||
return defined
|
||||
}
|
||||
}
|
||||
|
||||
t.Fatalf("type %s not found", name)
|
||||
return spec.DefineStruct{}
|
||||
}
|
||||
@@ -145,6 +145,17 @@ func (p parser) fillTypes() error {
|
||||
case spec.DefineStruct:
|
||||
var members []spec.Member
|
||||
for _, member := range v.Members {
|
||||
if member.IsInline {
|
||||
tp, err := p.resolveInlineType(member.Type, map[string]bool{v.RawName: true})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
member.Type = tp
|
||||
members = append(members, member)
|
||||
continue
|
||||
}
|
||||
|
||||
switch v := member.Type.(type) {
|
||||
case spec.DefineStruct:
|
||||
tp, err := p.findDefinedType(v.RawName)
|
||||
@@ -167,6 +178,62 @@ func (p parser) fillTypes() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p parser) resolveInlineType(tp spec.Type, resolving map[string]bool) (spec.Type, error) {
|
||||
switch v := tp.(type) {
|
||||
case spec.DefineStruct:
|
||||
if resolving[v.RawName] {
|
||||
return v, nil
|
||||
}
|
||||
|
||||
tp, err := p.findDefinedType(v.RawName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defined, ok := (*tp).(spec.DefineStruct)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("type %s is not a struct", v.RawName)
|
||||
}
|
||||
|
||||
resolving[v.RawName] = true
|
||||
defer delete(resolving, v.RawName)
|
||||
for i := range defined.Members {
|
||||
if !defined.Members[i].IsInline {
|
||||
continue
|
||||
}
|
||||
|
||||
resolved, err := p.resolveInlineType(defined.Members[i].Type, resolving)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defined.Members[i].Type = resolved
|
||||
}
|
||||
return defined, nil
|
||||
case spec.NestedStruct:
|
||||
for i := range v.Members {
|
||||
if !v.Members[i].IsInline {
|
||||
continue
|
||||
}
|
||||
|
||||
resolved, err := p.resolveInlineType(v.Members[i].Type, resolving)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
v.Members[i].Type = resolved
|
||||
}
|
||||
return v, nil
|
||||
case spec.PointerType:
|
||||
resolved, err := p.resolveInlineType(v.Type, resolving)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
v.Type = resolved
|
||||
return v, nil
|
||||
default:
|
||||
return tp, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (p parser) findDefinedType(name string) (*spec.Type, error) {
|
||||
for _, item := range p.spec.Types {
|
||||
if _, ok := item.(spec.DefineStruct); ok {
|
||||
|
||||
@@ -139,18 +139,43 @@ func (m Member) IsFormMember() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// IsTagMember returns true if contains given tag
|
||||
// IsTagMember returns true if the member contains the given tag.
|
||||
// For inline members, it recursively checks the members of the referenced
|
||||
// struct, since inline members themselves carry no tag and any matching tag
|
||||
// must live on one of their children. This avoids spuriously reporting the
|
||||
// presence of a tag (e.g. `header`) for an inline struct whose children do
|
||||
// not actually use that tag. See go-zero #4800.
|
||||
func (m Member) IsTagMember(tagKey string) bool {
|
||||
if m.IsInline {
|
||||
return true
|
||||
}
|
||||
|
||||
tags := m.Tags()
|
||||
for _, tag := range tags {
|
||||
if tag.Key == tagKey {
|
||||
return true
|
||||
}
|
||||
}
|
||||
if m.IsInline {
|
||||
return typeContainsTag(m.Type, tagKey)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func typeContainsTag(tp Type, tagKey string) bool {
|
||||
var members []Member
|
||||
switch v := tp.(type) {
|
||||
case DefineStruct:
|
||||
members = v.Members
|
||||
case NestedStruct:
|
||||
members = v.Members
|
||||
case PointerType:
|
||||
return typeContainsTag(v.Type, tagKey)
|
||||
default:
|
||||
return false
|
||||
}
|
||||
|
||||
for _, child := range members {
|
||||
if child.IsTagMember(tagKey) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
167
tools/goctl/api/spec/fn_test.go
Normal file
167
tools/goctl/api/spec/fn_test.go
Normal file
@@ -0,0 +1,167 @@
|
||||
package spec
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestMember_IsTagMember(t *testing.T) {
|
||||
t.Run("non-inline member with matching tag returns true", func(t *testing.T) {
|
||||
m := Member{Tag: `header:"Authorization"`}
|
||||
assert.True(t, m.IsTagMember("header"))
|
||||
})
|
||||
|
||||
t.Run("non-inline member without matching tag returns false", func(t *testing.T) {
|
||||
m := Member{Tag: `json:"username"`}
|
||||
assert.False(t, m.IsTagMember("header"))
|
||||
assert.False(t, m.IsTagMember("path"))
|
||||
assert.False(t, m.IsTagMember("form"))
|
||||
})
|
||||
|
||||
t.Run("non-inline member without any tag returns false", func(t *testing.T) {
|
||||
m := Member{}
|
||||
assert.False(t, m.IsTagMember("header"))
|
||||
})
|
||||
|
||||
t.Run("inline struct without matching child tag returns false (#4800)", func(t *testing.T) {
|
||||
m := Member{
|
||||
Name: "Pagination",
|
||||
IsInline: true,
|
||||
Type: DefineStruct{
|
||||
RawName: "Pagination",
|
||||
Members: []Member{
|
||||
{Name: "Page", Tag: `json:"page"`},
|
||||
{Name: "PageSize", Tag: `json:"pageSize"`},
|
||||
},
|
||||
},
|
||||
}
|
||||
assert.False(t, m.IsTagMember("header"))
|
||||
assert.False(t, m.IsTagMember("path"))
|
||||
assert.False(t, m.IsTagMember("form"))
|
||||
})
|
||||
|
||||
t.Run("inline struct whose child has matching tag returns true", func(t *testing.T) {
|
||||
m := Member{
|
||||
Name: "Auth",
|
||||
IsInline: true,
|
||||
Type: DefineStruct{
|
||||
RawName: "Auth",
|
||||
Members: []Member{
|
||||
{Name: "Token", Tag: `header:"Authorization"`},
|
||||
},
|
||||
},
|
||||
}
|
||||
assert.True(t, m.IsTagMember("header"))
|
||||
})
|
||||
|
||||
t.Run("nested inline structs are recursed", func(t *testing.T) {
|
||||
m := Member{
|
||||
Name: "Outer",
|
||||
IsInline: true,
|
||||
Type: DefineStruct{
|
||||
RawName: "Outer",
|
||||
Members: []Member{
|
||||
{
|
||||
Name: "Inner",
|
||||
IsInline: true,
|
||||
Type: DefineStruct{
|
||||
RawName: "Inner",
|
||||
Members: []Member{
|
||||
{Name: "Token", Tag: `header:"X-Token"`},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
assert.True(t, m.IsTagMember("header"))
|
||||
})
|
||||
|
||||
t.Run("nested inline structs without matching child return false", func(t *testing.T) {
|
||||
m := Member{
|
||||
Name: "Outer",
|
||||
IsInline: true,
|
||||
Type: DefineStruct{
|
||||
RawName: "Outer",
|
||||
Members: []Member{
|
||||
{
|
||||
Name: "Inner",
|
||||
IsInline: true,
|
||||
Type: DefineStruct{
|
||||
RawName: "Inner",
|
||||
Members: []Member{
|
||||
{Name: "Page", Tag: `json:"page"`},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
assert.False(t, m.IsTagMember("header"))
|
||||
})
|
||||
|
||||
t.Run("inline NestedStruct whose child has matching tag returns true", func(t *testing.T) {
|
||||
m := Member{
|
||||
Name: "Auth",
|
||||
IsInline: true,
|
||||
Type: NestedStruct{
|
||||
RawName: "Auth",
|
||||
Members: []Member{
|
||||
{Name: "Token", Tag: `header:"Authorization"`},
|
||||
},
|
||||
},
|
||||
}
|
||||
assert.True(t, m.IsTagMember("header"))
|
||||
})
|
||||
|
||||
t.Run("inline PointerType whose child has matching tag returns true", func(t *testing.T) {
|
||||
m := Member{
|
||||
Name: "Auth",
|
||||
IsInline: true,
|
||||
Type: PointerType{
|
||||
RawName: "*Auth",
|
||||
Type: DefineStruct{
|
||||
RawName: "Auth",
|
||||
Members: []Member{
|
||||
{Name: "Token", Tag: `header:"Authorization"`},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
assert.True(t, m.IsTagMember("header"))
|
||||
assert.False(t, m.IsTagMember("path"))
|
||||
})
|
||||
|
||||
t.Run("empty inline struct returns false", func(t *testing.T) {
|
||||
m := Member{
|
||||
Name: "Empty",
|
||||
IsInline: true,
|
||||
Type: DefineStruct{RawName: "Empty"},
|
||||
}
|
||||
assert.False(t, m.IsTagMember("header"))
|
||||
})
|
||||
}
|
||||
|
||||
func TestDefineStruct_GetTagMembers_InlineRegression(t *testing.T) {
|
||||
s := DefineStruct{
|
||||
RawName: "QueryUserListReq",
|
||||
Members: []Member{
|
||||
{Name: "Username", Tag: `json:"username,optional"`},
|
||||
{
|
||||
Name: "Pagination",
|
||||
IsInline: true,
|
||||
Type: DefineStruct{
|
||||
RawName: "Pagination",
|
||||
Members: []Member{
|
||||
{Name: "Page", Tag: `json:"page"`},
|
||||
{Name: "PageSize", Tag: `json:"pageSize"`},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
assert.Empty(t, s.GetTagMembers("header"),
|
||||
"inline struct without header-tagged children must not match header (#4800)")
|
||||
assert.Empty(t, s.GetTagMembers("path"))
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -349,6 +349,17 @@ func (a *Analyzer) fillTypes() error {
|
||||
case spec.DefineStruct:
|
||||
var members []spec.Member
|
||||
for _, member := range v.Members {
|
||||
if member.IsInline {
|
||||
tp, err := a.resolveInlineType(member.Type, map[string]bool{v.RawName: true})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
member.Type = tp
|
||||
members = append(members, member)
|
||||
continue
|
||||
}
|
||||
|
||||
switch v := member.Type.(type) {
|
||||
case spec.DefineStruct:
|
||||
tp, err := a.findDefinedType(v.RawName)
|
||||
@@ -371,6 +382,62 @@ func (a *Analyzer) fillTypes() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *Analyzer) resolveInlineType(tp spec.Type, resolving map[string]bool) (spec.Type, error) {
|
||||
switch v := tp.(type) {
|
||||
case spec.DefineStruct:
|
||||
if resolving[v.RawName] {
|
||||
return v, nil
|
||||
}
|
||||
|
||||
tp, err := a.findDefinedType(v.RawName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defined, ok := tp.(spec.DefineStruct)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("type %s is not a struct", v.RawName)
|
||||
}
|
||||
|
||||
resolving[v.RawName] = true
|
||||
defer delete(resolving, v.RawName)
|
||||
for i := range defined.Members {
|
||||
if !defined.Members[i].IsInline {
|
||||
continue
|
||||
}
|
||||
|
||||
resolved, err := a.resolveInlineType(defined.Members[i].Type, resolving)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defined.Members[i].Type = resolved
|
||||
}
|
||||
return defined, nil
|
||||
case spec.NestedStruct:
|
||||
for i := range v.Members {
|
||||
if !v.Members[i].IsInline {
|
||||
continue
|
||||
}
|
||||
|
||||
resolved, err := a.resolveInlineType(v.Members[i].Type, resolving)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
v.Members[i].Type = resolved
|
||||
}
|
||||
return v, nil
|
||||
case spec.PointerType:
|
||||
resolved, err := a.resolveInlineType(v.Type, resolving)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
v.Type = resolved
|
||||
return v, nil
|
||||
default:
|
||||
return tp, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Analyzer) fillTypeExpr(expr *ast.TypeExpr) error {
|
||||
head, _ := expr.CommentGroup()
|
||||
switch val := expr.DataType.(type) {
|
||||
|
||||
69
tools/goctl/pkg/parser/api/parser/inline_tag_test.go
Normal file
69
tools/goctl/pkg/parser/api/parser/inline_tag_test.go
Normal file
@@ -0,0 +1,69 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/zeromicro/go-zero/tools/goctl/api/spec"
|
||||
)
|
||||
|
||||
const inlineTagAPI = `
|
||||
syntax = "v1"
|
||||
|
||||
type (
|
||||
Auth {
|
||||
Token string ` + "`header:\"Authorization\"`" + `
|
||||
}
|
||||
Middle {
|
||||
Auth
|
||||
}
|
||||
PointerRequest {
|
||||
*Auth
|
||||
}
|
||||
NestedRequest {
|
||||
Middle
|
||||
}
|
||||
RecursiveRequest {
|
||||
Token string ` + "`header:\"X-Token\"`" + `
|
||||
*RecursiveRequest
|
||||
}
|
||||
)
|
||||
|
||||
service test-api {
|
||||
@handler Pointer
|
||||
get /pointer (PointerRequest)
|
||||
|
||||
@handler Nested
|
||||
get /nested (NestedRequest)
|
||||
|
||||
@handler Recursive
|
||||
get /recursive (RecursiveRequest)
|
||||
}
|
||||
`
|
||||
|
||||
func TestParseResolvesInlineTypesForTagLookup(t *testing.T) {
|
||||
apiSpec, err := Parse("inline.api", inlineTagAPI)
|
||||
require.NoError(t, err)
|
||||
|
||||
for _, name := range []string{"PointerRequest", "NestedRequest", "RecursiveRequest"} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
tp := findStructByName(t, apiSpec.Types, name)
|
||||
require.NotEmpty(t, tp.GetTagMembers("header"))
|
||||
require.Empty(t, tp.GetTagMembers("path"))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func findStructByName(t *testing.T, types []spec.Type, name string) spec.DefineStruct {
|
||||
t.Helper()
|
||||
for _, tp := range types {
|
||||
if tp.Name() == name {
|
||||
defined, ok := tp.(spec.DefineStruct)
|
||||
require.True(t, ok)
|
||||
return defined
|
||||
}
|
||||
}
|
||||
|
||||
t.Fatalf("type %s not found", name)
|
||||
return spec.DefineStruct{}
|
||||
}
|
||||
Reference in New Issue
Block a user