mirror of
https://github.com/zeromicro/go-zero.git
synced 2026-05-21 05:38:21 +08:00
Compare commits
31 Commits
tools/goct
...
copilot/fi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f43e67ecd7 | ||
|
|
6efaf92471 | ||
|
|
d9304b24ec | ||
|
|
ff7258bc52 | ||
|
|
42b5713621 | ||
|
|
59e9e54a21 | ||
|
|
cf21cb2b0b | ||
|
|
61e8894c31 | ||
|
|
7a6c3c8129 | ||
|
|
875fec3e1a | ||
|
|
60128c2100 | ||
|
|
ce6d0e3ea7 | ||
|
|
fa85c84af3 | ||
|
|
440884105e | ||
|
|
271f10598f | ||
|
|
cf55a88ce3 | ||
|
|
c1c786b14a | ||
|
|
988fb9d9bf | ||
|
|
d212c81bca | ||
|
|
bc43df2641 | ||
|
|
351b8cb37b | ||
|
|
0d681a2e29 | ||
|
|
5ea027c5de | ||
|
|
5de6112dcd | ||
|
|
4fb51723b7 | ||
|
|
06502d1115 | ||
|
|
3854d6dd00 | ||
|
|
895854913a | ||
|
|
ef753b8857 | ||
|
|
9c16fede73 | ||
|
|
ce11adb5e4 |
197
.github/copilot-instructions.md
vendored
Normal file
197
.github/copilot-instructions.md
vendored
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
# GitHub Copilot Instructions for go-zero
|
||||||
|
|
||||||
|
This document provides guidelines for GitHub Copilot when assisting with development in the go-zero project.
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
go-zero is a web and RPC framework with lots of built-in engineering practices designed to ensure the stability of busy services with resilience design. It has been serving sites with tens of millions of users for years.
|
||||||
|
|
||||||
|
### Key Architecture Components
|
||||||
|
|
||||||
|
- **REST API framework** (`rest/`) - HTTP service framework with middleware support
|
||||||
|
- **RPC framework** (`zrpc/`) - gRPC-based RPC framework with service discovery
|
||||||
|
- **Core utilities** (`core/`) - Foundational components including:
|
||||||
|
- Circuit breakers, rate limiters, load shedding
|
||||||
|
- Caching, stores (Redis, MongoDB, SQL)
|
||||||
|
- Concurrency control, metrics, tracing
|
||||||
|
- Configuration management
|
||||||
|
- **Code generation tool** (`tools/goctl/`) - CLI tool for generating code from API files
|
||||||
|
|
||||||
|
## Coding Standards and Conventions
|
||||||
|
|
||||||
|
### Code Style
|
||||||
|
|
||||||
|
1. **Follow Go conventions**: Use `gofmt` for formatting, follow effective Go practices
|
||||||
|
2. **Package naming**: Use lowercase, single-word package names when possible
|
||||||
|
3. **Error handling**: Always handle errors explicitly, use `errorx.BatchError` for multiple errors
|
||||||
|
4. **Context propagation**: Always pass `context.Context` as the first parameter for functions that may block
|
||||||
|
5. **Configuration structures**: Use struct tags with JSON annotations and default values
|
||||||
|
|
||||||
|
Example configuration pattern:
|
||||||
|
```go
|
||||||
|
type Config struct {
|
||||||
|
Host string `json:",default=0.0.0.0"`
|
||||||
|
Port int `json:",default=8080"`
|
||||||
|
Timeout int `json:",default=3000"`
|
||||||
|
Optional string `json:",optional"`
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Interface Design
|
||||||
|
|
||||||
|
1. **Small interfaces**: Follow Go's preference for small, focused interfaces
|
||||||
|
2. **Context methods**: Provide both context and non-context versions of methods
|
||||||
|
3. **Options pattern**: Use functional options for complex configuration
|
||||||
|
|
||||||
|
Example:
|
||||||
|
```go
|
||||||
|
func (c *Client) Get(key string, val any) error {
|
||||||
|
return c.GetCtx(context.Background(), key, val)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) GetCtx(ctx context.Context, key string, val any) error {
|
||||||
|
// implementation
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Testing Patterns
|
||||||
|
|
||||||
|
1. **Test file naming**: Use `*_test.go` suffix
|
||||||
|
2. **Test function naming**: Use `TestFunctionName` pattern
|
||||||
|
3. **Use testify/assert**: Prefer `assert` package for assertions
|
||||||
|
4. **Table-driven tests**: Use table-driven tests for multiple scenarios
|
||||||
|
5. **Mock interfaces**: Use `go.uber.org/mock` for mocking
|
||||||
|
6. **Test helpers**: Use `redistest`, `mongtest` helpers for database testing
|
||||||
|
|
||||||
|
Example test pattern:
|
||||||
|
```go
|
||||||
|
func TestSomething(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input string
|
||||||
|
expected string
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{"valid case", "input", "output", false},
|
||||||
|
{"error case", "bad", "", true},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result, err := SomeFunction(tt.input)
|
||||||
|
if tt.wantErr {
|
||||||
|
assert.Error(t, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, tt.expected, result)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Framework-Specific Guidelines
|
||||||
|
|
||||||
|
### REST API Development
|
||||||
|
|
||||||
|
1. **API Definition**: Use `.api` files to define REST APIs
|
||||||
|
2. **Handler pattern**: Separate business logic into logic packages
|
||||||
|
3. **Middleware**: Use built-in middlewares (tracing, logging, metrics, recovery)
|
||||||
|
4. **Response handling**: Use `httpx.WriteJson` for JSON responses
|
||||||
|
5. **Error handling**: Use `httpx.Error` for HTTP error responses
|
||||||
|
|
||||||
|
### RPC Development
|
||||||
|
|
||||||
|
1. **Protocol Buffers**: Use protobuf for service definitions
|
||||||
|
2. **Service discovery**: Integrate with etcd for service registration
|
||||||
|
3. **Load balancing**: Use built-in load balancing strategies
|
||||||
|
4. **Interceptors**: Implement interceptors for cross-cutting concerns
|
||||||
|
|
||||||
|
### Database Operations
|
||||||
|
|
||||||
|
1. **SQL operations**: Use `sqlx` package for database operations
|
||||||
|
2. **Caching**: Implement caching patterns with `cache` package
|
||||||
|
3. **Transactions**: Use proper transaction handling
|
||||||
|
4. **Connection pooling**: Configure appropriate connection pools
|
||||||
|
|
||||||
|
Example cache pattern:
|
||||||
|
```go
|
||||||
|
err := c.QueryRowCtx(ctx, &dest, key, func(ctx context.Context, conn sqlx.SqlConn) error {
|
||||||
|
return conn.QueryRowCtx(ctx, &dest, query, args...)
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Configuration Management
|
||||||
|
|
||||||
|
1. **YAML configuration**: Use YAML for configuration files
|
||||||
|
2. **Environment variables**: Support environment variable overrides
|
||||||
|
3. **Validation**: Include proper validation for configuration parameters
|
||||||
|
4. **Sensible defaults**: Provide reasonable default values
|
||||||
|
|
||||||
|
## Error Handling Best Practices
|
||||||
|
|
||||||
|
1. **Wrap errors**: Use `fmt.Errorf` with `%w` verb to wrap errors
|
||||||
|
2. **Custom errors**: Define custom error types when needed
|
||||||
|
3. **Error logging**: Log errors appropriately with context
|
||||||
|
4. **Graceful degradation**: Implement fallback mechanisms
|
||||||
|
|
||||||
|
## Performance Considerations
|
||||||
|
|
||||||
|
1. **Resource pools**: Use connection pools and worker pools
|
||||||
|
2. **Circuit breakers**: Implement circuit breaker patterns for external calls
|
||||||
|
3. **Rate limiting**: Apply rate limiting to protect services
|
||||||
|
4. **Load shedding**: Implement adaptive load shedding
|
||||||
|
5. **Metrics**: Add appropriate metrics and monitoring
|
||||||
|
|
||||||
|
## Security Guidelines
|
||||||
|
|
||||||
|
1. **Input validation**: Validate all input parameters
|
||||||
|
2. **SQL injection prevention**: Use parameterized queries
|
||||||
|
3. **Authentication**: Implement proper JWT token handling
|
||||||
|
4. **HTTPS**: Support TLS/HTTPS configurations
|
||||||
|
5. **CORS**: Configure CORS appropriately for web APIs
|
||||||
|
|
||||||
|
## Documentation Standards
|
||||||
|
|
||||||
|
1. **Package documentation**: Include package-level documentation
|
||||||
|
2. **Function documentation**: Document exported functions with examples
|
||||||
|
3. **API documentation**: Maintain API documentation in sync
|
||||||
|
4. **README updates**: Update README for significant changes
|
||||||
|
|
||||||
|
## Common Patterns to Follow
|
||||||
|
|
||||||
|
### Service Configuration
|
||||||
|
```go
|
||||||
|
type ServiceConf struct {
|
||||||
|
Name string
|
||||||
|
Log logx.LogConf
|
||||||
|
Mode string `json:",default=pro,options=[dev,test,pre,pro]"`
|
||||||
|
// ... other common fields
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Middleware Implementation
|
||||||
|
```go
|
||||||
|
func SomeMiddleware() rest.Middleware {
|
||||||
|
return func(next http.HandlerFunc) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Pre-processing
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
// Post-processing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Resource Management
|
||||||
|
Always implement proper resource cleanup using defer and context cancellation.
|
||||||
|
|
||||||
|
## Build and Test Commands
|
||||||
|
|
||||||
|
- Build: `go build ./...`
|
||||||
|
- Test: `go test ./...`
|
||||||
|
- Test with race detection: `go test -race ./...`
|
||||||
|
- Format: `gofmt -w .`
|
||||||
|
- Generate code: `goctl api go -api *.api -dir .`
|
||||||
|
|
||||||
|
Remember to run tests and ensure all checks pass before submitting changes. The project emphasizes high quality, performance, and reliability, so these should be primary considerations in all development work.
|
||||||
4
.github/workflows/go.yml
vendored
4
.github/workflows/go.yml
vendored
@@ -15,7 +15,7 @@ jobs:
|
|||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Set up Go 1.x
|
- name: Set up Go 1.x
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version-file: go.mod
|
go-version-file: go.mod
|
||||||
check-latest: true
|
check-latest: true
|
||||||
@@ -55,7 +55,7 @@ jobs:
|
|||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Set up Go 1.x
|
- name: Set up Go 1.x
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
# make sure Go version compatible with go-zero
|
# make sure Go version compatible with go-zero
|
||||||
go-version-file: go.mod
|
go-version-file: go.mod
|
||||||
|
|||||||
2
.github/workflows/issues.yml
vendored
2
.github/workflows/issues.yml
vendored
@@ -7,7 +7,7 @@ jobs:
|
|||||||
close-issues:
|
close-issues:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/stale@v9
|
- uses: actions/stale@v10
|
||||||
with:
|
with:
|
||||||
days-before-issue-stale: 365
|
days-before-issue-stale: 365
|
||||||
days-before-issue-close: 90
|
days-before-issue-close: 90
|
||||||
|
|||||||
2
.github/workflows/version-check.yml
vendored
2
.github/workflows/version-check.yml
vendored
@@ -13,7 +13,7 @@ jobs:
|
|||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version: '1.21'
|
go-version: '1.21'
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,9 @@ package mr
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"runtime/debug"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
|
|
||||||
@@ -183,12 +186,16 @@ func buildOptions(opts ...Option) *mapReduceOptions {
|
|||||||
return options
|
return options
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func buildPanicInfo(r any, stack []byte) string {
|
||||||
|
return fmt.Sprintf("%+v\n\n%s", r, strings.TrimSpace(string(stack)))
|
||||||
|
}
|
||||||
|
|
||||||
func buildSource[T any](generate GenerateFunc[T], panicChan *onceChan) chan T {
|
func buildSource[T any](generate GenerateFunc[T], panicChan *onceChan) chan T {
|
||||||
source := make(chan T)
|
source := make(chan T)
|
||||||
go func() {
|
go func() {
|
||||||
defer func() {
|
defer func() {
|
||||||
if r := recover(); r != nil {
|
if r := recover(); r != nil {
|
||||||
panicChan.write(r)
|
panicChan.write(buildPanicInfo(r, debug.Stack()))
|
||||||
}
|
}
|
||||||
close(source)
|
close(source)
|
||||||
}()
|
}()
|
||||||
@@ -235,7 +242,7 @@ func executeMappers[T, U any](mCtx mapperContext[T, U]) {
|
|||||||
defer func() {
|
defer func() {
|
||||||
if r := recover(); r != nil {
|
if r := recover(); r != nil {
|
||||||
atomic.AddInt32(&failed, 1)
|
atomic.AddInt32(&failed, 1)
|
||||||
mCtx.panicChan.write(r)
|
mCtx.panicChan.write(buildPanicInfo(r, debug.Stack()))
|
||||||
}
|
}
|
||||||
wg.Done()
|
wg.Done()
|
||||||
<-pool
|
<-pool
|
||||||
@@ -289,7 +296,7 @@ func mapReduceWithPanicChan[T, U, V any](source <-chan T, panicChan *onceChan, m
|
|||||||
defer func() {
|
defer func() {
|
||||||
drain(collector)
|
drain(collector)
|
||||||
if r := recover(); r != nil {
|
if r := recover(); r != nil {
|
||||||
panicChan.write(r)
|
panicChan.write(buildPanicInfo(r, debug.Stack()))
|
||||||
}
|
}
|
||||||
finish()
|
finish()
|
||||||
}()
|
}()
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package mr
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"runtime"
|
"runtime"
|
||||||
@@ -148,11 +149,28 @@ func TestForEach(t *testing.T) {
|
|||||||
|
|
||||||
assert.Equal(t, tasks/2, int(count))
|
assert.Equal(t, tasks/2, int(count))
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
t.Run("all", func(t *testing.T) {
|
func TestPanics(t *testing.T) {
|
||||||
defer goleak.VerifyNone(t)
|
defer goleak.VerifyNone(t)
|
||||||
|
|
||||||
|
const tasks = 1000
|
||||||
|
verify := func(t *testing.T, r any) {
|
||||||
|
panicStr := fmt.Sprintf("%v", r)
|
||||||
|
assert.Contains(t, panicStr, "foo")
|
||||||
|
assert.Contains(t, panicStr, "goroutine")
|
||||||
|
assert.Contains(t, panicStr, "runtime/debug.Stack")
|
||||||
|
panic(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("ForEach run panics", func(t *testing.T) {
|
||||||
|
assert.Panics(t, func() {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
verify(t, r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
assert.PanicsWithValue(t, "foo", func() {
|
|
||||||
ForEach(func(source chan<- int) {
|
ForEach(func(source chan<- int) {
|
||||||
for i := 0; i < tasks; i++ {
|
for i := 0; i < tasks; i++ {
|
||||||
source <- i
|
source <- i
|
||||||
@@ -162,28 +180,31 @@ func TestForEach(t *testing.T) {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
|
||||||
|
|
||||||
func TestGeneratePanic(t *testing.T) {
|
t.Run("ForEach generate panics", func(t *testing.T) {
|
||||||
defer goleak.VerifyNone(t)
|
assert.Panics(t, func() {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
verify(t, r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
t.Run("all", func(t *testing.T) {
|
|
||||||
assert.PanicsWithValue(t, "foo", func() {
|
|
||||||
ForEach(func(source chan<- int) {
|
ForEach(func(source chan<- int) {
|
||||||
panic("foo")
|
panic("foo")
|
||||||
}, func(item int) {
|
}, func(item int) {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
|
||||||
|
|
||||||
func TestMapperPanic(t *testing.T) {
|
|
||||||
defer goleak.VerifyNone(t)
|
|
||||||
|
|
||||||
const tasks = 1000
|
|
||||||
var run int32
|
var run int32
|
||||||
t.Run("all", func(t *testing.T) {
|
t.Run("Mapper panics", func(t *testing.T) {
|
||||||
assert.PanicsWithValue(t, "foo", func() {
|
assert.Panics(t, func() {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
verify(t, r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
_, _ = MapReduce(func(source chan<- int) {
|
_, _ = MapReduce(func(source chan<- int) {
|
||||||
for i := 0; i < tasks; i++ {
|
for i := 0; i < tasks; i++ {
|
||||||
source <- i
|
source <- i
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"runtime/debug"
|
||||||
|
"runtime/metrics"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -28,10 +30,29 @@ func displayStatsWithWriter(writer io.Writer, interval ...time.Duration) {
|
|||||||
ticker := time.NewTicker(duration)
|
ticker := time.NewTicker(duration)
|
||||||
defer ticker.Stop()
|
defer ticker.Stop()
|
||||||
for range ticker.C {
|
for range ticker.C {
|
||||||
var m runtime.MemStats
|
var (
|
||||||
runtime.ReadMemStats(&m)
|
alloc, totalAlloc, sys uint64
|
||||||
|
samples = []metrics.Sample{
|
||||||
|
{Name: "/memory/classes/heap/objects:bytes"},
|
||||||
|
{Name: "/gc/heap/allocs:bytes"},
|
||||||
|
{Name: "/memory/classes/total:bytes"},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
metrics.Read(samples)
|
||||||
|
|
||||||
|
if samples[0].Value.Kind() == metrics.KindUint64 {
|
||||||
|
alloc = samples[0].Value.Uint64()
|
||||||
|
}
|
||||||
|
if samples[1].Value.Kind() == metrics.KindUint64 {
|
||||||
|
totalAlloc = samples[1].Value.Uint64()
|
||||||
|
}
|
||||||
|
if samples[2].Value.Kind() == metrics.KindUint64 {
|
||||||
|
sys = samples[2].Value.Uint64()
|
||||||
|
}
|
||||||
|
var stats debug.GCStats
|
||||||
|
debug.ReadGCStats(&stats)
|
||||||
fmt.Fprintf(writer, "Goroutines: %d, Alloc: %vm, TotalAlloc: %vm, Sys: %vm, NumGC: %v\n",
|
fmt.Fprintf(writer, "Goroutines: %d, Alloc: %vm, TotalAlloc: %vm, Sys: %vm, NumGC: %v\n",
|
||||||
runtime.NumGoroutine(), m.Alloc/mega, m.TotalAlloc/mega, m.Sys/mega, m.NumGC)
|
runtime.NumGoroutine(), alloc/mega, totalAlloc/mega, sys/mega, stats.NumGC)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
package stat
|
package stat
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"runtime"
|
"runtime/debug"
|
||||||
|
"runtime/metrics"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -56,8 +57,28 @@ func bToMb(b uint64) float32 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func printUsage() {
|
func printUsage() {
|
||||||
var m runtime.MemStats
|
var (
|
||||||
runtime.ReadMemStats(&m)
|
alloc, totalAlloc, sys uint64
|
||||||
|
samples = []metrics.Sample{
|
||||||
|
{Name: "/memory/classes/heap/objects:bytes"},
|
||||||
|
{Name: "/gc/heap/allocs:bytes"},
|
||||||
|
{Name: "/memory/classes/total:bytes"},
|
||||||
|
}
|
||||||
|
stats debug.GCStats
|
||||||
|
)
|
||||||
|
metrics.Read(samples)
|
||||||
|
|
||||||
|
if samples[0].Value.Kind() == metrics.KindUint64 {
|
||||||
|
alloc = samples[0].Value.Uint64()
|
||||||
|
}
|
||||||
|
if samples[1].Value.Kind() == metrics.KindUint64 {
|
||||||
|
totalAlloc = samples[1].Value.Uint64()
|
||||||
|
}
|
||||||
|
if samples[2].Value.Kind() == metrics.KindUint64 {
|
||||||
|
sys = samples[2].Value.Uint64()
|
||||||
|
}
|
||||||
|
debug.ReadGCStats(&stats)
|
||||||
|
|
||||||
logx.Statf("CPU: %dm, MEMORY: Alloc=%.1fMi, TotalAlloc=%.1fMi, Sys=%.1fMi, NumGC=%d",
|
logx.Statf("CPU: %dm, MEMORY: Alloc=%.1fMi, TotalAlloc=%.1fMi, Sys=%.1fMi, NumGC=%d",
|
||||||
CpuUsage(), bToMb(m.Alloc), bToMb(m.TotalAlloc), bToMb(m.Sys), m.NumGC)
|
CpuUsage(), bToMb(alloc), bToMb(totalAlloc), bToMb(sys), stats.NumGC)
|
||||||
}
|
}
|
||||||
|
|||||||
2
go.mod
2
go.mod
@@ -16,7 +16,7 @@ require (
|
|||||||
github.com/jhump/protoreflect v1.17.0
|
github.com/jhump/protoreflect v1.17.0
|
||||||
github.com/pelletier/go-toml/v2 v2.2.2
|
github.com/pelletier/go-toml/v2 v2.2.2
|
||||||
github.com/prometheus/client_golang v1.21.1
|
github.com/prometheus/client_golang v1.21.1
|
||||||
github.com/redis/go-redis/v9 v9.12.1
|
github.com/redis/go-redis/v9 v9.15.0
|
||||||
github.com/spaolacci/murmur3 v1.1.0
|
github.com/spaolacci/murmur3 v1.1.0
|
||||||
github.com/stretchr/testify v1.11.1
|
github.com/stretchr/testify v1.11.1
|
||||||
go.etcd.io/etcd/api/v3 v3.5.15
|
go.etcd.io/etcd/api/v3 v3.5.15
|
||||||
|
|||||||
4
go.sum
4
go.sum
@@ -154,8 +154,8 @@ github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ
|
|||||||
github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
|
github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
|
||||||
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
|
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
|
||||||
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
|
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
|
||||||
github.com/redis/go-redis/v9 v9.12.1 h1:k5iquqv27aBtnTm2tIkROUDp8JBXhXZIVu1InSgvovg=
|
github.com/redis/go-redis/v9 v9.15.0 h1:2jdes0xJxer4h3NUZrZ4OGSntGlXp4WbXju2nOTRXto=
|
||||||
github.com/redis/go-redis/v9 v9.12.1/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw=
|
github.com/redis/go-redis/v9 v9.15.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw=
|
||||||
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
||||||
|
|||||||
@@ -304,6 +304,7 @@ go-zero 已被许多公司用于生产部署,接入场景如在线教育、电
|
|||||||
>106. 无锡盛算信息技术有限公司
|
>106. 无锡盛算信息技术有限公司
|
||||||
>107. 深圳市聚货通信息科技有限公司
|
>107. 深圳市聚货通信息科技有限公司
|
||||||
>108. 浙江银盾云科技有限公司
|
>108. 浙江银盾云科技有限公司
|
||||||
|
>109. 南京造世网络科技有限公司
|
||||||
|
|
||||||
如果贵公司也已使用 go-zero,欢迎在 [登记地址](https://github.com/zeromicro/go-zero/issues/602) 登记,仅仅为了推广,不做其它用途。
|
如果贵公司也已使用 go-zero,欢迎在 [登记地址](https://github.com/zeromicro/go-zero/issues/602) 登记,仅仅为了推广,不做其它用途。
|
||||||
|
|
||||||
|
|||||||
@@ -389,7 +389,9 @@ func buildSSERoutes(routes []Route) []Route {
|
|||||||
// because SSE requires the connection to be kept alive indefinitely.
|
// because SSE requires the connection to be kept alive indefinitely.
|
||||||
rc := http.NewResponseController(w)
|
rc := http.NewResponseController(w)
|
||||||
if err := rc.SetWriteDeadline(time.Time{}); err != nil {
|
if err := rc.SetWriteDeadline(time.Time{}); err != nil {
|
||||||
logc.Errorf(r.Context(), "set conn write deadline failed: %v", err)
|
// Some ResponseWriter implementations (like timeoutWriter) don't support SetWriteDeadline.
|
||||||
|
// This is expected behavior and doesn't affect SSE functionality.
|
||||||
|
logc.Debugf(r.Context(), "unable to clear write deadline for SSE connection: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
w.Header().Set(header.ContentType, header.ContentTypeEventStream)
|
w.Header().Set(header.ContentType, header.ContentTypeEventStream)
|
||||||
|
|||||||
@@ -24,12 +24,16 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
limitBodyBytes = 1024
|
limitBodyBytes = 1024
|
||||||
limitDetailedBodyBytes = 4096
|
limitDetailedBodyBytes = 4096
|
||||||
defaultSlowThreshold = time.Millisecond * 500
|
defaultSlowThreshold = time.Millisecond * 500
|
||||||
|
defaultSSESlowThreshold = time.Minute * 3
|
||||||
)
|
)
|
||||||
|
|
||||||
var slowThreshold = syncx.ForAtomicDuration(defaultSlowThreshold)
|
var (
|
||||||
|
slowThreshold = syncx.ForAtomicDuration(defaultSlowThreshold)
|
||||||
|
sseSlowThreshold = syncx.ForAtomicDuration(defaultSSESlowThreshold)
|
||||||
|
)
|
||||||
|
|
||||||
// LogHandler returns a middleware that logs http request and response.
|
// LogHandler returns a middleware that logs http request and response.
|
||||||
func LogHandler(next http.Handler) http.Handler {
|
func LogHandler(next http.Handler) http.Handler {
|
||||||
@@ -109,6 +113,11 @@ func SetSlowThreshold(threshold time.Duration) {
|
|||||||
slowThreshold.Set(threshold)
|
slowThreshold.Set(threshold)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetSSESlowThreshold sets the slow threshold for SSE requests.
|
||||||
|
func SetSSESlowThreshold(threshold time.Duration) {
|
||||||
|
sseSlowThreshold.Set(threshold)
|
||||||
|
}
|
||||||
|
|
||||||
func dumpRequest(r *http.Request) string {
|
func dumpRequest(r *http.Request) string {
|
||||||
reqContent, err := httputil.DumpRequest(r, true)
|
reqContent, err := httputil.DumpRequest(r, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -118,6 +127,14 @@ func dumpRequest(r *http.Request) string {
|
|||||||
return string(reqContent)
|
return string(reqContent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getSlowThreshold(r *http.Request) time.Duration {
|
||||||
|
if r.Header.Get(headerAccept) == valueSSE {
|
||||||
|
return sseSlowThreshold.Load()
|
||||||
|
} else {
|
||||||
|
return slowThreshold.Load()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func isOkResponse(code int) bool {
|
func isOkResponse(code int) bool {
|
||||||
// not server error
|
// not server error
|
||||||
return code < http.StatusInternalServerError
|
return code < http.StatusInternalServerError
|
||||||
@@ -129,7 +146,8 @@ func logBrief(r *http.Request, code int, timer *utils.ElapsedTimer, logs *intern
|
|||||||
logger := logx.WithContext(r.Context()).WithDuration(duration)
|
logger := logx.WithContext(r.Context()).WithDuration(duration)
|
||||||
buf.WriteString(fmt.Sprintf("[HTTP] %s - %s %s - %s - %s",
|
buf.WriteString(fmt.Sprintf("[HTTP] %s - %s %s - %s - %s",
|
||||||
wrapStatusCode(code), wrapMethod(r.Method), r.RequestURI, httpx.GetRemoteAddr(r), r.UserAgent()))
|
wrapStatusCode(code), wrapMethod(r.Method), r.RequestURI, httpx.GetRemoteAddr(r), r.UserAgent()))
|
||||||
if duration > slowThreshold.Load() {
|
|
||||||
|
if duration > getSlowThreshold(r) {
|
||||||
logger.Slowf("[HTTP] %s - %s %s - %s - %s - slowcall(%s)",
|
logger.Slowf("[HTTP] %s - %s %s - %s - %s - slowcall(%s)",
|
||||||
wrapStatusCode(code), wrapMethod(r.Method), r.RequestURI, httpx.GetRemoteAddr(r), r.UserAgent(),
|
wrapStatusCode(code), wrapMethod(r.Method), r.RequestURI, httpx.GetRemoteAddr(r), r.UserAgent(),
|
||||||
timex.ReprOfDuration(duration))
|
timex.ReprOfDuration(duration))
|
||||||
@@ -160,7 +178,8 @@ func logDetails(r *http.Request, response *detailLoggedResponseWriter, timer *ut
|
|||||||
logger := logx.WithContext(r.Context())
|
logger := logx.WithContext(r.Context())
|
||||||
buf.WriteString(fmt.Sprintf("[HTTP] %s - %d - %s - %s\n=> %s\n",
|
buf.WriteString(fmt.Sprintf("[HTTP] %s - %d - %s - %s\n=> %s\n",
|
||||||
r.Method, code, r.RemoteAddr, timex.ReprOfDuration(duration), dumpRequest(r)))
|
r.Method, code, r.RemoteAddr, timex.ReprOfDuration(duration), dumpRequest(r)))
|
||||||
if duration > slowThreshold.Load() {
|
|
||||||
|
if duration > getSlowThreshold(r) {
|
||||||
logger.Slowf("[HTTP] %s - %d - %s - slowcall(%s)\n=> %s\n", r.Method, code, r.RemoteAddr,
|
logger.Slowf("[HTTP] %s - %d - %s - slowcall(%s)\n=> %s\n", r.Method, code, r.RemoteAddr,
|
||||||
timex.ReprOfDuration(duration), dumpRequest(r))
|
timex.ReprOfDuration(duration), dumpRequest(r))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -88,6 +88,96 @@ func TestLogHandlerSlow(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestLogHandlerSSE(t *testing.T) {
|
||||||
|
handlers := []func(handler http.Handler) http.Handler{
|
||||||
|
LogHandler,
|
||||||
|
DetailedLogHandler,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, logHandler := range handlers {
|
||||||
|
t.Run("SSE request with normal duration", func(t *testing.T) {
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "http://localhost", http.NoBody)
|
||||||
|
req.Header.Set(headerAccept, valueSSE)
|
||||||
|
|
||||||
|
handler := logHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
time.Sleep(defaultSlowThreshold + time.Second)
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}))
|
||||||
|
|
||||||
|
resp := httptest.NewRecorder()
|
||||||
|
handler.ServeHTTP(resp, req)
|
||||||
|
assert.Equal(t, http.StatusOK, resp.Code)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("SSE request exceeding SSE threshold", func(t *testing.T) {
|
||||||
|
originalThreshold := sseSlowThreshold.Load()
|
||||||
|
SetSSESlowThreshold(time.Millisecond * 100)
|
||||||
|
defer SetSSESlowThreshold(originalThreshold)
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "http://localhost", http.NoBody)
|
||||||
|
req.Header.Set(headerAccept, valueSSE)
|
||||||
|
|
||||||
|
handler := logHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
time.Sleep(time.Millisecond * 150)
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}))
|
||||||
|
|
||||||
|
resp := httptest.NewRecorder()
|
||||||
|
handler.ServeHTTP(resp, req)
|
||||||
|
assert.Equal(t, http.StatusOK, resp.Code)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLogHandlerThresholdSelection(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
acceptHeader string
|
||||||
|
expectedIsSSE bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Regular HTTP request",
|
||||||
|
acceptHeader: "text/html",
|
||||||
|
expectedIsSSE: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "SSE request",
|
||||||
|
acceptHeader: valueSSE,
|
||||||
|
expectedIsSSE: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "No Accept header",
|
||||||
|
acceptHeader: "",
|
||||||
|
expectedIsSSE: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "http://localhost", http.NoBody)
|
||||||
|
if tt.acceptHeader != "" {
|
||||||
|
req.Header.Set(headerAccept, tt.acceptHeader)
|
||||||
|
}
|
||||||
|
|
||||||
|
SetSlowThreshold(time.Millisecond * 100)
|
||||||
|
SetSSESlowThreshold(time.Millisecond * 200)
|
||||||
|
defer func() {
|
||||||
|
SetSlowThreshold(defaultSlowThreshold)
|
||||||
|
SetSSESlowThreshold(defaultSSESlowThreshold)
|
||||||
|
}()
|
||||||
|
|
||||||
|
handler := LogHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
time.Sleep(time.Millisecond * 150)
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}))
|
||||||
|
|
||||||
|
resp := httptest.NewRecorder()
|
||||||
|
handler.ServeHTTP(resp, req)
|
||||||
|
assert.Equal(t, http.StatusOK, resp.Code)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestDetailedLogHandler_LargeBody(t *testing.T) {
|
func TestDetailedLogHandler_LargeBody(t *testing.T) {
|
||||||
lbuf := logtest.NewCollector(t)
|
lbuf := logtest.NewCollector(t)
|
||||||
|
|
||||||
@@ -139,6 +229,12 @@ func TestSetSlowThreshold(t *testing.T) {
|
|||||||
assert.Equal(t, time.Second, slowThreshold.Load())
|
assert.Equal(t, time.Second, slowThreshold.Load())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSetSSESlowThreshold(t *testing.T) {
|
||||||
|
assert.Equal(t, defaultSSESlowThreshold, sseSlowThreshold.Load())
|
||||||
|
SetSSESlowThreshold(time.Minute * 10)
|
||||||
|
assert.Equal(t, time.Minute*10, sseSlowThreshold.Load())
|
||||||
|
}
|
||||||
|
|
||||||
func TestWrapMethodWithColor(t *testing.T) {
|
func TestWrapMethodWithColor(t *testing.T) {
|
||||||
// no tty
|
// no tty
|
||||||
assert.Equal(t, http.MethodGet, wrapMethod(http.MethodGet))
|
assert.Equal(t, http.MethodGet, wrapMethod(http.MethodGet))
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ Port: 0
|
|||||||
Path: "/",
|
Path: "/",
|
||||||
Handler: nil,
|
Handler: nil,
|
||||||
}, WithJwt("thesecret"), WithSignature(SignatureConf{}),
|
}, WithJwt("thesecret"), WithSignature(SignatureConf{}),
|
||||||
WithJwtTransition("preivous", "thenewone"))
|
WithJwtTransition("previous", "thenewone"))
|
||||||
|
|
||||||
func() {
|
func() {
|
||||||
defer func() {
|
defer func() {
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
// Code scaffolded by goctl. Safe to edit.
|
||||||
|
// goctl {{.version}}
|
||||||
|
|
||||||
package config
|
package config
|
||||||
|
|
||||||
import {{.authImport}}
|
import {{.authImport}}
|
||||||
|
|||||||
@@ -118,6 +118,8 @@ func DoGenProjectWithModule(apiFile, dir, moduleName, style string, withTest boo
|
|||||||
if withTest {
|
if withTest {
|
||||||
logx.Must(genHandlersTest(dir, rootPkg, projectPkg, cfg, api))
|
logx.Must(genHandlersTest(dir, rootPkg, projectPkg, cfg, api))
|
||||||
logx.Must(genLogicTest(dir, rootPkg, projectPkg, cfg, api))
|
logx.Must(genLogicTest(dir, rootPkg, projectPkg, cfg, api))
|
||||||
|
logx.Must(genServiceContextTest(dir, rootPkg, projectPkg, cfg, api))
|
||||||
|
logx.Must(genIntegrationTest(dir, rootPkg, projectPkg, cfg, api))
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := backupAndSweep(apiFile); err != nil {
|
if err := backupAndSweep(apiFile); err != nil {
|
||||||
|
|||||||
181
tools/goctl/api/gogen/gencomment_test.go
Normal file
181
tools/goctl/api/gogen/gencomment_test.go
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
package gogen
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/zeromicro/go-zero/tools/goctl/internal/version"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestGenerationComments verifies that all generated files have appropriate generation comments
|
||||||
|
func TestGenerationComments(t *testing.T) {
|
||||||
|
// Create a temporary directory for our test
|
||||||
|
tempDir, err := os.MkdirTemp("", "goctl_test_")
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer os.RemoveAll(tempDir)
|
||||||
|
|
||||||
|
// Create a simple API spec for testing
|
||||||
|
apiContent := `
|
||||||
|
syntax = "v1"
|
||||||
|
|
||||||
|
type HelloRequest {
|
||||||
|
Name string ` + "`json:\"name\"`" + `
|
||||||
|
}
|
||||||
|
|
||||||
|
type HelloResponse {
|
||||||
|
Message string ` + "`json:\"message\"`" + `
|
||||||
|
}
|
||||||
|
|
||||||
|
service hello-api {
|
||||||
|
@handler helloHandler
|
||||||
|
post /hello (HelloRequest) returns (HelloResponse)
|
||||||
|
}`
|
||||||
|
|
||||||
|
// Write the API spec to a temporary file
|
||||||
|
apiFile := filepath.Join(tempDir, "test.api")
|
||||||
|
err = os.WriteFile(apiFile, []byte(apiContent), 0644)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Parse and generate the API files using the correct function signature
|
||||||
|
err = DoGenProject(apiFile, tempDir, "gozero", false)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Define expected files and their comment types
|
||||||
|
expectedFiles := map[string]string{
|
||||||
|
// Files that should have "DO NOT EDIT" comments (regenerated files)
|
||||||
|
"internal/types/types.go": "DO NOT EDIT",
|
||||||
|
|
||||||
|
// Files that should have "Safe to edit" comments (scaffolded files)
|
||||||
|
"internal/handler/hellohandler.go": "Safe to edit",
|
||||||
|
"internal/config/config.go": "Safe to edit",
|
||||||
|
"hello.go": "Safe to edit", // main file
|
||||||
|
"internal/svc/servicecontext.go": "Safe to edit",
|
||||||
|
"internal/logic/hellologic.go": "Safe to edit",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check each file for the correct generation comment
|
||||||
|
for filePath, expectedCommentType := range expectedFiles {
|
||||||
|
fullPath := filepath.Join(tempDir, filePath)
|
||||||
|
|
||||||
|
// Skip if file doesn't exist (some files might not be generated in all cases)
|
||||||
|
if _, err := os.Stat(fullPath); os.IsNotExist(err) {
|
||||||
|
t.Logf("File %s does not exist, skipping", filePath)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
content, err := os.ReadFile(fullPath)
|
||||||
|
require.NoError(t, err, "Failed to read file: %s", filePath)
|
||||||
|
|
||||||
|
contentStr := string(content)
|
||||||
|
lines := strings.Split(contentStr, "\n")
|
||||||
|
|
||||||
|
// Check that the file starts with proper generation comments
|
||||||
|
require.GreaterOrEqual(t, len(lines), 2, "File %s should have at least 2 lines", filePath)
|
||||||
|
|
||||||
|
if expectedCommentType == "DO NOT EDIT" {
|
||||||
|
assert.Contains(t, lines[0], "// Code generated by goctl. DO NOT EDIT.",
|
||||||
|
"File %s should have 'DO NOT EDIT' comment as first line", filePath)
|
||||||
|
} else if expectedCommentType == "Safe to edit" {
|
||||||
|
assert.Contains(t, lines[0], "// Code scaffolded by goctl. Safe to edit.",
|
||||||
|
"File %s should have 'Safe to edit' comment as first line", filePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that the second line contains the version
|
||||||
|
assert.Contains(t, lines[1], "// goctl",
|
||||||
|
"File %s should have version comment as second line", filePath)
|
||||||
|
assert.Contains(t, lines[1], version.BuildVersion,
|
||||||
|
"File %s should contain version %s in second line", filePath, version.BuildVersion)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestRoutesGenerationComment verifies routes files have "DO NOT EDIT" comment
|
||||||
|
func TestRoutesGenerationComment(t *testing.T) {
|
||||||
|
// Create a temporary directory for our test
|
||||||
|
tempDir, err := os.MkdirTemp("", "goctl_routes_test_")
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer os.RemoveAll(tempDir)
|
||||||
|
|
||||||
|
// Create an API spec with multiple handlers to ensure routes file is generated
|
||||||
|
apiContent := `
|
||||||
|
syntax = "v1"
|
||||||
|
|
||||||
|
type HelloRequest {
|
||||||
|
Name string ` + "`json:\"name\"`" + `
|
||||||
|
}
|
||||||
|
|
||||||
|
type HelloResponse {
|
||||||
|
Message string ` + "`json:\"message\"`" + `
|
||||||
|
}
|
||||||
|
|
||||||
|
service hello-api {
|
||||||
|
@handler helloHandler
|
||||||
|
post /hello (HelloRequest) returns (HelloResponse)
|
||||||
|
|
||||||
|
@handler worldHandler
|
||||||
|
get /world returns (HelloResponse)
|
||||||
|
}`
|
||||||
|
|
||||||
|
// Write the API spec to a temporary file
|
||||||
|
apiFile := filepath.Join(tempDir, "test.api")
|
||||||
|
err = os.WriteFile(apiFile, []byte(apiContent), 0644)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Generate the API files using the correct function signature
|
||||||
|
err = DoGenProject(apiFile, tempDir, "gozero", false)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Check the routes file specifically
|
||||||
|
routesFile := filepath.Join(tempDir, "internal/handler/routes.go")
|
||||||
|
if _, err := os.Stat(routesFile); os.IsNotExist(err) {
|
||||||
|
t.Skip("Routes file not generated, skipping test")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
content, err := os.ReadFile(routesFile)
|
||||||
|
require.NoError(t, err, "Failed to read routes.go")
|
||||||
|
|
||||||
|
contentStr := string(content)
|
||||||
|
lines := strings.Split(contentStr, "\n")
|
||||||
|
|
||||||
|
// Check that routes.go has "DO NOT EDIT" comment
|
||||||
|
require.GreaterOrEqual(t, len(lines), 2, "Routes file should have at least 2 lines")
|
||||||
|
assert.Contains(t, lines[0], "// Code generated by goctl. DO NOT EDIT.",
|
||||||
|
"Routes file should have 'DO NOT EDIT' comment")
|
||||||
|
assert.Contains(t, lines[1], "// goctl",
|
||||||
|
"Routes file should have version comment")
|
||||||
|
assert.Contains(t, lines[1], version.BuildVersion,
|
||||||
|
"Routes file should contain version %s", version.BuildVersion)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestVersionInTemplateData verifies that version is correctly passed to templates
|
||||||
|
func TestVersionInTemplateData(t *testing.T) {
|
||||||
|
// Test that BuildVersion is available
|
||||||
|
assert.NotEmpty(t, version.BuildVersion, "BuildVersion should not be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestCommentsFollowGoStandards verifies our comments follow Go community standards
|
||||||
|
func TestCommentsFollowGoStandards(t *testing.T) {
|
||||||
|
// Test the format of our generation comments
|
||||||
|
doNotEditComment := "// Code generated by goctl. DO NOT EDIT."
|
||||||
|
safeToEditComment := "// Code scaffolded by goctl. Safe to edit."
|
||||||
|
|
||||||
|
// Both should be valid Go comments
|
||||||
|
assert.True(t, strings.HasPrefix(doNotEditComment, "//"),
|
||||||
|
"DO NOT EDIT comment should start with //")
|
||||||
|
assert.True(t, strings.HasPrefix(safeToEditComment, "//"),
|
||||||
|
"Safe to edit comment should start with //")
|
||||||
|
|
||||||
|
// Should contain key information
|
||||||
|
assert.Contains(t, doNotEditComment, "goctl",
|
||||||
|
"DO NOT EDIT comment should mention goctl")
|
||||||
|
assert.Contains(t, safeToEditComment, "goctl",
|
||||||
|
"Safe to edit comment should mention goctl")
|
||||||
|
assert.Contains(t, doNotEditComment, "DO NOT EDIT",
|
||||||
|
"Should clearly state DO NOT EDIT")
|
||||||
|
assert.Contains(t, safeToEditComment, "Safe to edit",
|
||||||
|
"Should clearly state Safe to edit")
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
|
|
||||||
"github.com/zeromicro/go-zero/tools/goctl/api/spec"
|
"github.com/zeromicro/go-zero/tools/goctl/api/spec"
|
||||||
"github.com/zeromicro/go-zero/tools/goctl/config"
|
"github.com/zeromicro/go-zero/tools/goctl/config"
|
||||||
|
"github.com/zeromicro/go-zero/tools/goctl/internal/version"
|
||||||
"github.com/zeromicro/go-zero/tools/goctl/util/format"
|
"github.com/zeromicro/go-zero/tools/goctl/util/format"
|
||||||
"github.com/zeromicro/go-zero/tools/goctl/vars"
|
"github.com/zeromicro/go-zero/tools/goctl/vars"
|
||||||
)
|
)
|
||||||
@@ -61,6 +62,7 @@ func genConfig(dir, projectPkg string, cfg *config.Config, api *spec.ApiSpec) er
|
|||||||
"auth": strings.Join(auths, "\n"),
|
"auth": strings.Join(auths, "\n"),
|
||||||
"jwtTrans": strings.Join(jwtTransList, "\n"),
|
"jwtTrans": strings.Join(jwtTransList, "\n"),
|
||||||
"projectPkg": projectPkg,
|
"projectPkg": projectPkg,
|
||||||
|
"version": version.BuildVersion,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
|
|
||||||
"github.com/zeromicro/go-zero/tools/goctl/api/spec"
|
"github.com/zeromicro/go-zero/tools/goctl/api/spec"
|
||||||
"github.com/zeromicro/go-zero/tools/goctl/config"
|
"github.com/zeromicro/go-zero/tools/goctl/config"
|
||||||
|
"github.com/zeromicro/go-zero/tools/goctl/internal/version"
|
||||||
"github.com/zeromicro/go-zero/tools/goctl/util"
|
"github.com/zeromicro/go-zero/tools/goctl/util"
|
||||||
"github.com/zeromicro/go-zero/tools/goctl/util/format"
|
"github.com/zeromicro/go-zero/tools/goctl/util/format"
|
||||||
"github.com/zeromicro/go-zero/tools/goctl/util/pathx"
|
"github.com/zeromicro/go-zero/tools/goctl/util/pathx"
|
||||||
@@ -37,9 +38,11 @@ func genHandler(dir, rootPkg, projectPkg string, cfg *config.Config, group spec.
|
|||||||
}
|
}
|
||||||
|
|
||||||
var builtinTemplate = handlerTemplate
|
var builtinTemplate = handlerTemplate
|
||||||
|
var templateFile = handlerTemplateFile
|
||||||
sse := group.GetAnnotation("sse")
|
sse := group.GetAnnotation("sse")
|
||||||
if sse == "true" {
|
if sse == "true" {
|
||||||
builtinTemplate = sseHandlerTemplate
|
builtinTemplate = sseHandlerTemplate
|
||||||
|
templateFile = sseHandlerTemplateFile
|
||||||
}
|
}
|
||||||
|
|
||||||
return genFile(fileGenConfig{
|
return genFile(fileGenConfig{
|
||||||
@@ -48,7 +51,7 @@ func genHandler(dir, rootPkg, projectPkg string, cfg *config.Config, group spec.
|
|||||||
filename: filename + ".go",
|
filename: filename + ".go",
|
||||||
templateName: "handlerTemplate",
|
templateName: "handlerTemplate",
|
||||||
category: category,
|
category: category,
|
||||||
templateFile: handlerTemplateFile,
|
templateFile: templateFile,
|
||||||
builtinTemplate: builtinTemplate,
|
builtinTemplate: builtinTemplate,
|
||||||
data: map[string]any{
|
data: map[string]any{
|
||||||
"PkgName": pkgName,
|
"PkgName": pkgName,
|
||||||
@@ -64,6 +67,7 @@ func genHandler(dir, rootPkg, projectPkg string, cfg *config.Config, group spec.
|
|||||||
"HasDoc": len(route.JoinedDoc()) > 0,
|
"HasDoc": len(route.JoinedDoc()) > 0,
|
||||||
"Doc": getDoc(route.JoinedDoc()),
|
"Doc": getDoc(route.JoinedDoc()),
|
||||||
"projectPkg": projectPkg,
|
"projectPkg": projectPkg,
|
||||||
|
"version": version.BuildVersion,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
|
|
||||||
"github.com/zeromicro/go-zero/tools/goctl/api/spec"
|
"github.com/zeromicro/go-zero/tools/goctl/api/spec"
|
||||||
"github.com/zeromicro/go-zero/tools/goctl/config"
|
"github.com/zeromicro/go-zero/tools/goctl/config"
|
||||||
|
"github.com/zeromicro/go-zero/tools/goctl/internal/version"
|
||||||
"github.com/zeromicro/go-zero/tools/goctl/util"
|
"github.com/zeromicro/go-zero/tools/goctl/util"
|
||||||
"github.com/zeromicro/go-zero/tools/goctl/util/format"
|
"github.com/zeromicro/go-zero/tools/goctl/util/format"
|
||||||
"github.com/zeromicro/go-zero/tools/goctl/util/pathx"
|
"github.com/zeromicro/go-zero/tools/goctl/util/pathx"
|
||||||
@@ -51,6 +52,7 @@ func genHandlerTest(dir, rootPkg, projectPkg string, cfg *config.Config, group s
|
|||||||
"HasDoc": len(route.JoinedDoc()) > 0,
|
"HasDoc": len(route.JoinedDoc()) > 0,
|
||||||
"Doc": getDoc(route.JoinedDoc()),
|
"Doc": getDoc(route.JoinedDoc()),
|
||||||
"projectPkg": projectPkg,
|
"projectPkg": projectPkg,
|
||||||
|
"version": version.BuildVersion,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
42
tools/goctl/api/gogen/genintegrationtest.go
Normal file
42
tools/goctl/api/gogen/genintegrationtest.go
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
package gogen
|
||||||
|
|
||||||
|
import (
|
||||||
|
_ "embed"
|
||||||
|
|
||||||
|
"github.com/zeromicro/go-zero/tools/goctl/api/spec"
|
||||||
|
"github.com/zeromicro/go-zero/tools/goctl/config"
|
||||||
|
"github.com/zeromicro/go-zero/tools/goctl/internal/version"
|
||||||
|
"github.com/zeromicro/go-zero/tools/goctl/util/format"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed integration_test.tpl
|
||||||
|
var integrationTestTemplate string
|
||||||
|
|
||||||
|
func genIntegrationTest(dir, rootPkg, projectPkg string, cfg *config.Config, api *spec.ApiSpec) error {
|
||||||
|
serviceName := api.Service.Name
|
||||||
|
if len(serviceName) == 0 {
|
||||||
|
serviceName = "server"
|
||||||
|
}
|
||||||
|
|
||||||
|
filename, err := format.FileNamingFormat(cfg.NamingFormat, serviceName)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return genFile(fileGenConfig{
|
||||||
|
dir: dir,
|
||||||
|
subdir: "",
|
||||||
|
filename: filename + "_test.go",
|
||||||
|
templateName: "integrationTestTemplate",
|
||||||
|
category: category,
|
||||||
|
templateFile: integrationTestTemplateFile,
|
||||||
|
builtinTemplate: integrationTestTemplate,
|
||||||
|
data: map[string]any{
|
||||||
|
"projectPkg": projectPkg,
|
||||||
|
"serviceName": serviceName,
|
||||||
|
"version": version.BuildVersion,
|
||||||
|
"hasRoutes": len(api.Service.Routes()) > 0,
|
||||||
|
"routes": api.Service.Routes(),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
"github.com/zeromicro/go-zero/tools/goctl/api/parser/g4/gen/api"
|
"github.com/zeromicro/go-zero/tools/goctl/api/parser/g4/gen/api"
|
||||||
"github.com/zeromicro/go-zero/tools/goctl/api/spec"
|
"github.com/zeromicro/go-zero/tools/goctl/api/spec"
|
||||||
"github.com/zeromicro/go-zero/tools/goctl/config"
|
"github.com/zeromicro/go-zero/tools/goctl/config"
|
||||||
|
"github.com/zeromicro/go-zero/tools/goctl/internal/version"
|
||||||
"github.com/zeromicro/go-zero/tools/goctl/util/format"
|
"github.com/zeromicro/go-zero/tools/goctl/util/format"
|
||||||
"github.com/zeromicro/go-zero/tools/goctl/util/pathx"
|
"github.com/zeromicro/go-zero/tools/goctl/util/pathx"
|
||||||
"github.com/zeromicro/go-zero/tools/goctl/vars"
|
"github.com/zeromicro/go-zero/tools/goctl/vars"
|
||||||
@@ -60,9 +61,11 @@ func genLogicByRoute(dir, rootPkg, projectPkg string, cfg *config.Config, group
|
|||||||
|
|
||||||
subDir := getLogicFolderPath(group, route)
|
subDir := getLogicFolderPath(group, route)
|
||||||
builtinTemplate := logicTemplate
|
builtinTemplate := logicTemplate
|
||||||
|
templateFile := logicTemplateFile
|
||||||
sse := group.GetAnnotation("sse")
|
sse := group.GetAnnotation("sse")
|
||||||
if sse == "true" {
|
if sse == "true" {
|
||||||
builtinTemplate = sseLogicTemplate
|
builtinTemplate = sseLogicTemplate
|
||||||
|
templateFile = sseLogicTemplateFile
|
||||||
responseString = "error"
|
responseString = "error"
|
||||||
returnString = "return nil"
|
returnString = "return nil"
|
||||||
resp := responseGoTypeName(route, typesPacket)
|
resp := responseGoTypeName(route, typesPacket)
|
||||||
@@ -79,7 +82,7 @@ func genLogicByRoute(dir, rootPkg, projectPkg string, cfg *config.Config, group
|
|||||||
filename: goFile + ".go",
|
filename: goFile + ".go",
|
||||||
templateName: "logicTemplate",
|
templateName: "logicTemplate",
|
||||||
category: category,
|
category: category,
|
||||||
templateFile: logicTemplateFile,
|
templateFile: templateFile,
|
||||||
builtinTemplate: builtinTemplate,
|
builtinTemplate: builtinTemplate,
|
||||||
data: map[string]any{
|
data: map[string]any{
|
||||||
"pkgName": subDir[strings.LastIndex(subDir, "/")+1:],
|
"pkgName": subDir[strings.LastIndex(subDir, "/")+1:],
|
||||||
@@ -92,6 +95,7 @@ func genLogicByRoute(dir, rootPkg, projectPkg string, cfg *config.Config, group
|
|||||||
"hasDoc": len(route.JoinedDoc()) > 0,
|
"hasDoc": len(route.JoinedDoc()) > 0,
|
||||||
"doc": getDoc(route.JoinedDoc()),
|
"doc": getDoc(route.JoinedDoc()),
|
||||||
"projectPkg": projectPkg,
|
"projectPkg": projectPkg,
|
||||||
|
"version": version.BuildVersion,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
|
|
||||||
"github.com/zeromicro/go-zero/tools/goctl/api/spec"
|
"github.com/zeromicro/go-zero/tools/goctl/api/spec"
|
||||||
"github.com/zeromicro/go-zero/tools/goctl/config"
|
"github.com/zeromicro/go-zero/tools/goctl/config"
|
||||||
|
"github.com/zeromicro/go-zero/tools/goctl/internal/version"
|
||||||
"github.com/zeromicro/go-zero/tools/goctl/util/format"
|
"github.com/zeromicro/go-zero/tools/goctl/util/format"
|
||||||
"github.com/zeromicro/go-zero/tools/goctl/util/pathx"
|
"github.com/zeromicro/go-zero/tools/goctl/util/pathx"
|
||||||
)
|
)
|
||||||
@@ -74,6 +75,7 @@ func genLogicTestByRoute(dir, rootPkg, projectPkg string, cfg *config.Config, gr
|
|||||||
"hasDoc": len(route.JoinedDoc()) > 0,
|
"hasDoc": len(route.JoinedDoc()) > 0,
|
||||||
"doc": getDoc(route.JoinedDoc()),
|
"doc": getDoc(route.JoinedDoc()),
|
||||||
"projectPkg": projectPkg,
|
"projectPkg": projectPkg,
|
||||||
|
"version": version.BuildVersion,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
|
|
||||||
"github.com/zeromicro/go-zero/tools/goctl/api/spec"
|
"github.com/zeromicro/go-zero/tools/goctl/api/spec"
|
||||||
"github.com/zeromicro/go-zero/tools/goctl/config"
|
"github.com/zeromicro/go-zero/tools/goctl/config"
|
||||||
|
"github.com/zeromicro/go-zero/tools/goctl/internal/version"
|
||||||
"github.com/zeromicro/go-zero/tools/goctl/util/format"
|
"github.com/zeromicro/go-zero/tools/goctl/util/format"
|
||||||
"github.com/zeromicro/go-zero/tools/goctl/util/pathx"
|
"github.com/zeromicro/go-zero/tools/goctl/util/pathx"
|
||||||
"github.com/zeromicro/go-zero/tools/goctl/vars"
|
"github.com/zeromicro/go-zero/tools/goctl/vars"
|
||||||
@@ -39,6 +40,7 @@ func genMain(dir, rootPkg, projectPkg string, cfg *config.Config, api *spec.ApiS
|
|||||||
"importPackages": genMainImports(rootPkg),
|
"importPackages": genMainImports(rootPkg),
|
||||||
"serviceName": configName,
|
"serviceName": configName,
|
||||||
"projectPkg": projectPkg,
|
"projectPkg": projectPkg,
|
||||||
|
"version": version.BuildVersion,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
|
|
||||||
"github.com/zeromicro/go-zero/tools/goctl/api/spec"
|
"github.com/zeromicro/go-zero/tools/goctl/api/spec"
|
||||||
"github.com/zeromicro/go-zero/tools/goctl/config"
|
"github.com/zeromicro/go-zero/tools/goctl/config"
|
||||||
|
"github.com/zeromicro/go-zero/tools/goctl/internal/version"
|
||||||
"github.com/zeromicro/go-zero/tools/goctl/util/format"
|
"github.com/zeromicro/go-zero/tools/goctl/util/format"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -31,7 +32,8 @@ func genMiddleware(dir string, cfg *config.Config, api *spec.ApiSpec) error {
|
|||||||
templateFile: middlewareImplementCodeFile,
|
templateFile: middlewareImplementCodeFile,
|
||||||
builtinTemplate: middlewareImplementCode,
|
builtinTemplate: middlewareImplementCode,
|
||||||
data: map[string]string{
|
data: map[string]string{
|
||||||
"name": strings.Title(name),
|
"name": strings.Title(name),
|
||||||
|
"version": version.BuildVersion,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
159
tools/goctl/api/gogen/gensse_test.go
Normal file
159
tools/goctl/api/gogen/gensse_test.go
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
package gogen
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSSEGeneration(t *testing.T) {
|
||||||
|
// Create a temporary directory for test
|
||||||
|
dir := t.TempDir()
|
||||||
|
|
||||||
|
// Create a test API file with SSE annotation
|
||||||
|
apiContent := `syntax = "v1"
|
||||||
|
|
||||||
|
type SseReq {
|
||||||
|
Message string ` + "`json:\"message\"`" + `
|
||||||
|
}
|
||||||
|
|
||||||
|
type SseResp {
|
||||||
|
Data string ` + "`json:\"data\"`" + `
|
||||||
|
}
|
||||||
|
|
||||||
|
@server (
|
||||||
|
sse: true
|
||||||
|
)
|
||||||
|
service Test {
|
||||||
|
@handler Sse
|
||||||
|
get /sse (SseReq) returns (SseResp)
|
||||||
|
}
|
||||||
|
`
|
||||||
|
apiFile := filepath.Join(dir, "test.api")
|
||||||
|
err := os.WriteFile(apiFile, []byte(apiContent), 0644)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Generate code
|
||||||
|
err = DoGenProject(apiFile, dir, "gozero", false)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Read generated handler file
|
||||||
|
handlerPath := filepath.Join(dir, "internal/handler/ssehandler.go")
|
||||||
|
handlerContent, err := os.ReadFile(handlerPath)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Read generated logic file
|
||||||
|
logicPath := filepath.Join(dir, "internal/logic/sselogic.go")
|
||||||
|
logicContent, err := os.ReadFile(logicPath)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
handlerStr := string(handlerContent)
|
||||||
|
logicStr := string(logicContent)
|
||||||
|
|
||||||
|
// Verify SSE-specific patterns in handler
|
||||||
|
// Handler should call: err := l.Sse(&req, client)
|
||||||
|
assert.Contains(t, handlerStr, "err := l.Sse(&req, client)",
|
||||||
|
"Handler should call logic with client channel parameter")
|
||||||
|
|
||||||
|
// Handler should NOT have the regular pattern: resp, err := l.Sse(&req)
|
||||||
|
assert.NotContains(t, handlerStr, "resp, err := l.Sse(&req)",
|
||||||
|
"Handler should not use regular pattern with resp return")
|
||||||
|
|
||||||
|
// Handler should use threading.GoSafeCtx
|
||||||
|
assert.Contains(t, handlerStr, "threading.GoSafeCtx",
|
||||||
|
"Handler should use threading.GoSafeCtx for SSE")
|
||||||
|
|
||||||
|
// Handler should create client channel
|
||||||
|
assert.Contains(t, handlerStr, "client := make(chan",
|
||||||
|
"Handler should create client channel")
|
||||||
|
|
||||||
|
// Verify SSE-specific patterns in logic
|
||||||
|
// Logic should have signature: Sse(req *types.SseReq, client chan<- *types.SseResp) error
|
||||||
|
assert.Contains(t, logicStr, "func (l *SseLogic) Sse(req *types.SseReq, client chan<- *types.SseResp) error",
|
||||||
|
"Logic should have SSE signature with client channel parameter")
|
||||||
|
|
||||||
|
// Logic should NOT have regular signature: Sse(req *types.SseReq) (resp *types.SseResp, err error)
|
||||||
|
assert.NotContains(t, logicStr, "(resp *types.SseResp, err error)",
|
||||||
|
"Logic should not have regular signature with resp return")
|
||||||
|
|
||||||
|
t.Logf("Handler content:\n%s", handlerStr)
|
||||||
|
t.Logf("Logic content:\n%s", logicStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNonSSEGeneration(t *testing.T) {
|
||||||
|
// Create a temporary directory for test
|
||||||
|
dir := t.TempDir()
|
||||||
|
|
||||||
|
// Create a test API file WITHOUT SSE annotation
|
||||||
|
apiContent := `syntax = "v1"
|
||||||
|
|
||||||
|
type SseReq {
|
||||||
|
Message string ` + "`json:\"message\"`" + `
|
||||||
|
}
|
||||||
|
|
||||||
|
type SseResp {
|
||||||
|
Data string ` + "`json:\"data\"`" + `
|
||||||
|
}
|
||||||
|
|
||||||
|
service Test {
|
||||||
|
@handler Sse
|
||||||
|
get /sse (SseReq) returns (SseResp)
|
||||||
|
}
|
||||||
|
`
|
||||||
|
apiFile := filepath.Join(dir, "test.api")
|
||||||
|
err := os.WriteFile(apiFile, []byte(apiContent), 0644)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Generate code
|
||||||
|
err = DoGenProject(apiFile, dir, "gozero", false)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Read generated handler file
|
||||||
|
handlerPath := filepath.Join(dir, "internal/handler/ssehandler.go")
|
||||||
|
handlerContent, err := os.ReadFile(handlerPath)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Read generated logic file
|
||||||
|
logicPath := filepath.Join(dir, "internal/logic/sselogic.go")
|
||||||
|
logicContent, err := os.ReadFile(logicPath)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
handlerStr := string(handlerContent)
|
||||||
|
logicStr := string(logicContent)
|
||||||
|
|
||||||
|
// Verify regular (non-SSE) patterns in handler
|
||||||
|
// Handler should call: resp, err := l.Sse(&req)
|
||||||
|
assert.Contains(t, handlerStr, "resp, err := l.Sse(&req)",
|
||||||
|
"Handler should use regular pattern with resp return")
|
||||||
|
|
||||||
|
// Handler should NOT have SSE pattern: err := l.Sse(&req, client)
|
||||||
|
assert.NotContains(t, handlerStr, "err := l.Sse(&req, client)",
|
||||||
|
"Handler should not use SSE pattern")
|
||||||
|
|
||||||
|
// Handler should NOT use threading.GoSafeCtx
|
||||||
|
assert.NotContains(t, handlerStr, "threading.GoSafeCtx",
|
||||||
|
"Handler should not use threading.GoSafeCtx for regular routes")
|
||||||
|
|
||||||
|
// Verify regular (non-SSE) patterns in logic
|
||||||
|
// Logic should have signature: Sse(req *types.SseReq) (resp *types.SseResp, err error)
|
||||||
|
assert.Contains(t, logicStr, "(resp *types.SseResp, err error)",
|
||||||
|
"Logic should have regular signature with resp return")
|
||||||
|
|
||||||
|
// Logic should NOT have SSE signature with client parameter
|
||||||
|
linesToCheck := strings.Split(logicStr, "\n")
|
||||||
|
hasSSESignature := false
|
||||||
|
for _, line := range linesToCheck {
|
||||||
|
if strings.Contains(line, "func (l *SseLogic) Sse") && strings.Contains(line, "client chan<-") {
|
||||||
|
hasSSESignature = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert.False(t, hasSSESignature,
|
||||||
|
"Logic should not have SSE signature with client channel parameter")
|
||||||
|
|
||||||
|
t.Logf("Handler content:\n%s", handlerStr)
|
||||||
|
t.Logf("Logic content:\n%s", logicStr)
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
|
|
||||||
"github.com/zeromicro/go-zero/tools/goctl/api/spec"
|
"github.com/zeromicro/go-zero/tools/goctl/api/spec"
|
||||||
"github.com/zeromicro/go-zero/tools/goctl/config"
|
"github.com/zeromicro/go-zero/tools/goctl/config"
|
||||||
|
"github.com/zeromicro/go-zero/tools/goctl/internal/version"
|
||||||
"github.com/zeromicro/go-zero/tools/goctl/util/format"
|
"github.com/zeromicro/go-zero/tools/goctl/util/format"
|
||||||
"github.com/zeromicro/go-zero/tools/goctl/util/pathx"
|
"github.com/zeromicro/go-zero/tools/goctl/util/pathx"
|
||||||
"github.com/zeromicro/go-zero/tools/goctl/vars"
|
"github.com/zeromicro/go-zero/tools/goctl/vars"
|
||||||
@@ -54,6 +55,7 @@ func genServiceContext(dir, rootPkg, projectPkg string, cfg *config.Config, api
|
|||||||
"middleware": middlewareStr,
|
"middleware": middlewareStr,
|
||||||
"middlewareAssignment": middlewareAssignment,
|
"middlewareAssignment": middlewareAssignment,
|
||||||
"projectPkg": projectPkg,
|
"projectPkg": projectPkg,
|
||||||
|
"version": version.BuildVersion,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
34
tools/goctl/api/gogen/gensvctest.go
Normal file
34
tools/goctl/api/gogen/gensvctest.go
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
package gogen
|
||||||
|
|
||||||
|
import (
|
||||||
|
_ "embed"
|
||||||
|
|
||||||
|
"github.com/zeromicro/go-zero/tools/goctl/api/spec"
|
||||||
|
"github.com/zeromicro/go-zero/tools/goctl/config"
|
||||||
|
"github.com/zeromicro/go-zero/tools/goctl/internal/version"
|
||||||
|
"github.com/zeromicro/go-zero/tools/goctl/util/format"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed svc_test.tpl
|
||||||
|
var svcTestTemplate string
|
||||||
|
|
||||||
|
func genServiceContextTest(dir, rootPkg, projectPkg string, cfg *config.Config, api *spec.ApiSpec) error {
|
||||||
|
filename, err := format.FileNamingFormat(cfg.NamingFormat, contextFilename)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return genFile(fileGenConfig{
|
||||||
|
dir: dir,
|
||||||
|
subdir: contextDir,
|
||||||
|
filename: filename + "_test.go",
|
||||||
|
templateName: "svcTestTemplate",
|
||||||
|
category: category,
|
||||||
|
templateFile: svcTestTemplateFile,
|
||||||
|
builtinTemplate: svcTestTemplate,
|
||||||
|
data: map[string]any{
|
||||||
|
"projectPkg": projectPkg,
|
||||||
|
"version": version.BuildVersion,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -1,3 +1,6 @@
|
|||||||
|
// Code scaffolded by goctl. Safe to edit.
|
||||||
|
// goctl {{.version}}
|
||||||
|
|
||||||
package {{.PkgName}}
|
package {{.PkgName}}
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
// Code scaffolded by goctl. Safe to edit.
|
||||||
|
// goctl {{.version}}
|
||||||
|
|
||||||
package {{.PkgName}}
|
package {{.PkgName}}
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
120
tools/goctl/api/gogen/integration_test.tpl
Normal file
120
tools/goctl/api/gogen/integration_test.tpl
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
// Code scaffolded by goctl. Safe to edit.
|
||||||
|
// goctl {{.version}}
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"{{.projectPkg}}/internal/config"
|
||||||
|
"{{.projectPkg}}/internal/handler"
|
||||||
|
"{{.projectPkg}}/internal/svc"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/zeromicro/go-zero/rest"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMain(m *testing.M) {
|
||||||
|
// TODO: Add setup/teardown logic here if needed
|
||||||
|
m.Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestServerIntegration(t *testing.T) {
|
||||||
|
// Create test server
|
||||||
|
c := config.Config{
|
||||||
|
RestConf: rest.RestConf{
|
||||||
|
Host: "127.0.0.1",
|
||||||
|
Port: 0, // Use random available port
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
server := rest.MustNewServer(c.RestConf)
|
||||||
|
defer server.Stop()
|
||||||
|
|
||||||
|
ctx := svc.NewServiceContext(c)
|
||||||
|
handler.RegisterHandlers(server, ctx)
|
||||||
|
|
||||||
|
// Start server in background
|
||||||
|
go func() {
|
||||||
|
server.Start()
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Wait for server to start
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
method string
|
||||||
|
path string
|
||||||
|
body string
|
||||||
|
expectedStatus int
|
||||||
|
setup func()
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "health check",
|
||||||
|
method: "GET",
|
||||||
|
path: "/health",
|
||||||
|
expectedStatus: http.StatusNotFound, // Adjust based on actual routes
|
||||||
|
setup: func() {},
|
||||||
|
},
|
||||||
|
{{if .hasRoutes}}{{range .routes}}{
|
||||||
|
name: "{{.Method}} {{.Path}}",
|
||||||
|
method: "{{.Method}}",
|
||||||
|
path: "{{.Path}}",
|
||||||
|
expectedStatus: http.StatusOK, // TODO: Adjust expected status
|
||||||
|
setup: func() {
|
||||||
|
// TODO: Add setup logic for this endpoint
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{{end}}{{end}}{
|
||||||
|
name: "not found route",
|
||||||
|
method: "GET",
|
||||||
|
path: "/nonexistent",
|
||||||
|
expectedStatus: http.StatusNotFound,
|
||||||
|
setup: func() {},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
tt.setup()
|
||||||
|
|
||||||
|
req, err := http.NewRequest(tt.method, tt.path, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
rr := httptest.NewRecorder()
|
||||||
|
server.ServeHTTP(rr, req)
|
||||||
|
|
||||||
|
assert.Equal(t, tt.expectedStatus, rr.Code)
|
||||||
|
|
||||||
|
// TODO: Add response body assertions
|
||||||
|
t.Logf("Response: %s", rr.Body.String())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestServerLifecycle(t *testing.T) {
|
||||||
|
c := config.Config{
|
||||||
|
RestConf: rest.RestConf{
|
||||||
|
Host: "127.0.0.1",
|
||||||
|
Port: 0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
server := rest.MustNewServer(c.RestConf)
|
||||||
|
|
||||||
|
// Test server can start and stop without errors
|
||||||
|
ctx := svc.NewServiceContext(c)
|
||||||
|
handler.RegisterHandlers(server, ctx)
|
||||||
|
|
||||||
|
// In a real integration test, you might start the server in a goroutine
|
||||||
|
// and test actual HTTP requests, but for scaffolding we keep it simple
|
||||||
|
server.Stop()
|
||||||
|
|
||||||
|
// TODO: Add more lifecycle tests as needed
|
||||||
|
assert.True(t, true, "Server lifecycle test passed")
|
||||||
|
}
|
||||||
@@ -1,3 +1,6 @@
|
|||||||
|
// Code scaffolded by goctl. Safe to edit.
|
||||||
|
// goctl {{.version}}
|
||||||
|
|
||||||
package {{.pkgName}}
|
package {{.pkgName}}
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
// Code scaffolded by goctl. Safe to edit.
|
||||||
|
// goctl {{.version}}
|
||||||
|
|
||||||
package {{.pkgName}}
|
package {{.pkgName}}
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
// Code scaffolded by goctl. Safe to edit.
|
||||||
|
// goctl {{.version}}
|
||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
// Code scaffolded by goctl. Safe to edit.
|
||||||
|
// goctl {{.version}}
|
||||||
|
|
||||||
package middleware
|
package middleware
|
||||||
|
|
||||||
import "net/http"
|
import "net/http"
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
// Code scaffolded by goctl. Safe to edit.
|
||||||
|
// goctl {{.version}}
|
||||||
|
|
||||||
package {{.PkgName}}
|
package {{.PkgName}}
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -27,11 +30,10 @@ func {{.HandlerName}}(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
|||||||
// w.Header().Set("Cache-Control", "no-cache")
|
// w.Header().Set("Cache-Control", "no-cache")
|
||||||
// w.Header().Set("Connection", "keep-alive")
|
// w.Header().Set("Connection", "keep-alive")
|
||||||
client := make(chan {{.ResponseType}}, 16)
|
client := make(chan {{.ResponseType}}, 16)
|
||||||
defer func() {
|
|
||||||
close(client)
|
|
||||||
}()
|
|
||||||
l := {{.LogicName}}.New{{.LogicType}}(r.Context(), svcCtx)
|
l := {{.LogicName}}.New{{.LogicType}}(r.Context(), svcCtx)
|
||||||
threading.GoSafeCtx(r.Context(), func() {
|
threading.GoSafeCtx(r.Context(), func() {
|
||||||
|
defer close(client)
|
||||||
err := l.{{.Call}}({{if .HasRequest}}&req, {{end}}client)
|
err := l.{{.Call}}({{if .HasRequest}}&req, {{end}}client)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logc.Errorw(r.Context(), "{{.HandlerName}}", logc.Field("error", err))
|
logc.Errorw(r.Context(), "{{.HandlerName}}", logc.Field("error", err))
|
||||||
@@ -41,7 +43,10 @@ func {{.HandlerName}}(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
|||||||
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case data := <-client:
|
case data, ok := <-client:
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
output, err := json.Marshal(data)
|
output, err := json.Marshal(data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logc.Errorw(r.Context(), "{{.HandlerName}}", logc.Field("error", err))
|
logc.Errorw(r.Context(), "{{.HandlerName}}", logc.Field("error", err))
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
// Code scaffolded by goctl. Safe to edit.
|
||||||
|
// goctl {{.version}}
|
||||||
|
|
||||||
package {{.pkgName}}
|
package {{.pkgName}}
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
// Code scaffolded by goctl. Safe to edit.
|
||||||
|
// goctl {{.version}}
|
||||||
|
|
||||||
package svc
|
package svc
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
60
tools/goctl/api/gogen/svc_test.tpl
Normal file
60
tools/goctl/api/gogen/svc_test.tpl
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
// Code scaffolded by goctl. Safe to edit.
|
||||||
|
// goctl {{.version}}
|
||||||
|
|
||||||
|
package svc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"{{.projectPkg}}/internal/config"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNewServiceContext(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
config config.Config
|
||||||
|
setup func() config.Config
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "default config",
|
||||||
|
setup: func() config.Config {
|
||||||
|
return config.Config{}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid config",
|
||||||
|
setup: func() config.Config {
|
||||||
|
return config.Config{
|
||||||
|
// TODO: Add valid config values here
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
c := tt.setup()
|
||||||
|
svcCtx := NewServiceContext(c)
|
||||||
|
|
||||||
|
// Basic assertions
|
||||||
|
require.NotNil(t, svcCtx)
|
||||||
|
assert.Equal(t, c, svcCtx.Config)
|
||||||
|
|
||||||
|
// TODO: Add additional assertions for middleware and dependencies
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestServiceContext_Initialization(t *testing.T) {
|
||||||
|
c := config.Config{}
|
||||||
|
svcCtx := NewServiceContext(c)
|
||||||
|
|
||||||
|
// Verify service context is properly initialized
|
||||||
|
assert.NotNil(t, svcCtx)
|
||||||
|
assert.Equal(t, c, svcCtx.Config)
|
||||||
|
|
||||||
|
// TODO: Add tests for middleware initialization if any
|
||||||
|
// TODO: Add tests for external dependencies if any
|
||||||
|
}
|
||||||
@@ -22,6 +22,8 @@ const (
|
|||||||
routesTemplateFile = "routes.tpl"
|
routesTemplateFile = "routes.tpl"
|
||||||
routesAdditionTemplateFile = "route-addition.tpl"
|
routesAdditionTemplateFile = "route-addition.tpl"
|
||||||
typesTemplateFile = "types.tpl"
|
typesTemplateFile = "types.tpl"
|
||||||
|
svcTestTemplateFile = "svc_test.tpl"
|
||||||
|
integrationTestTemplateFile = "integration_test.tpl"
|
||||||
)
|
)
|
||||||
|
|
||||||
var templates = map[string]string{
|
var templates = map[string]string{
|
||||||
@@ -39,6 +41,8 @@ var templates = map[string]string{
|
|||||||
routesTemplateFile: routesTemplate,
|
routesTemplateFile: routesTemplate,
|
||||||
routesAdditionTemplateFile: routesAdditionTemplate,
|
routesAdditionTemplateFile: routesAdditionTemplate,
|
||||||
typesTemplateFile: typesTemplate,
|
typesTemplateFile: typesTemplate,
|
||||||
|
svcTestTemplateFile: svcTestTemplate,
|
||||||
|
integrationTestTemplateFile: integrationTestTemplate,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Category returns the category of the api files.
|
// Category returns the category of the api files.
|
||||||
|
|||||||
@@ -8,68 +8,66 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func getBoolFromKVOrDefault(properties map[string]string, key string, def bool) bool {
|
func getBoolFromKVOrDefault(properties map[string]string, key string, def bool) bool {
|
||||||
if len(properties) == 0 {
|
return getOrDefault(properties, key, def, func(str string, def bool) bool {
|
||||||
return def
|
res, err := strconv.ParseBool(str)
|
||||||
}
|
if err != nil {
|
||||||
md := metadata.New(properties)
|
return def
|
||||||
val := md.Get(key)
|
}
|
||||||
if len(val) == 0 {
|
|
||||||
return def
|
|
||||||
}
|
|
||||||
str := util.Unquote(val[0])
|
|
||||||
if len(str) == 0 {
|
|
||||||
return def
|
|
||||||
}
|
|
||||||
res, _ := strconv.ParseBool(str)
|
|
||||||
return res
|
|
||||||
}
|
|
||||||
|
|
||||||
func getStringFromKVOrDefault(properties map[string]string, key string, def string) string {
|
return res
|
||||||
if len(properties) == 0 {
|
})
|
||||||
return def
|
|
||||||
}
|
|
||||||
md := metadata.New(properties)
|
|
||||||
val := md.Get(key)
|
|
||||||
if len(val) == 0 {
|
|
||||||
return def
|
|
||||||
}
|
|
||||||
str := util.Unquote(val[0])
|
|
||||||
if len(str) == 0 {
|
|
||||||
return def
|
|
||||||
}
|
|
||||||
return str
|
|
||||||
}
|
|
||||||
|
|
||||||
func getListFromInfoOrDefault(properties map[string]string, key string, def []string) []string {
|
|
||||||
if len(properties) == 0 {
|
|
||||||
return def
|
|
||||||
}
|
|
||||||
md := metadata.New(properties)
|
|
||||||
val := md.Get(key)
|
|
||||||
if len(val) == 0 {
|
|
||||||
return def
|
|
||||||
}
|
|
||||||
|
|
||||||
str := util.Unquote(val[0])
|
|
||||||
if len(str) == 0 {
|
|
||||||
return def
|
|
||||||
}
|
|
||||||
resp := util.FieldsAndTrimSpace(str, commaRune)
|
|
||||||
if len(resp) == 0 {
|
|
||||||
return def
|
|
||||||
}
|
|
||||||
return resp
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func getFirstUsableString(def ...string) string {
|
func getFirstUsableString(def ...string) string {
|
||||||
if len(def) == 0 {
|
if len(def) == 0 {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, val := range def {
|
for _, val := range def {
|
||||||
str := util.Unquote(val)
|
str, err := strconv.Unquote(val)
|
||||||
if len(str) != 0 {
|
if err == nil && len(str) != 0 {
|
||||||
return str
|
return str
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getListFromInfoOrDefault(properties map[string]string, key string, def []string) []string {
|
||||||
|
return getOrDefault(properties, key, def, func(str string, def []string) []string {
|
||||||
|
resp := util.FieldsAndTrimSpace(str, commaRune)
|
||||||
|
if len(resp) == 0 {
|
||||||
|
return def
|
||||||
|
}
|
||||||
|
return resp
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// getOrDefault abstracts the common logic for fetching, unquoting, and defaulting.
|
||||||
|
func getOrDefault[T any](properties map[string]string, key string, def T, convert func(string, T) T) T {
|
||||||
|
if len(properties) == 0 {
|
||||||
|
return def
|
||||||
|
}
|
||||||
|
|
||||||
|
md := metadata.New(properties)
|
||||||
|
val := md.Get(key)
|
||||||
|
if len(val) == 0 {
|
||||||
|
return def
|
||||||
|
}
|
||||||
|
|
||||||
|
str := val[0]
|
||||||
|
if unquoted, err := strconv.Unquote(str); err == nil {
|
||||||
|
str = unquoted
|
||||||
|
}
|
||||||
|
if len(str) == 0 {
|
||||||
|
return def
|
||||||
|
}
|
||||||
|
|
||||||
|
return convert(str, def)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getStringFromKVOrDefault(properties map[string]string, key string, def string) string {
|
||||||
|
return getOrDefault(properties, key, def, func(str string, def string) string {
|
||||||
|
return str
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -21,6 +21,19 @@ func Test_getBoolFromKVOrDefault(t *testing.T) {
|
|||||||
assert.False(t, getBoolFromKVOrDefault(properties, "empty_value", false))
|
assert.False(t, getBoolFromKVOrDefault(properties, "empty_value", false))
|
||||||
assert.False(t, getBoolFromKVOrDefault(nil, "nil", false))
|
assert.False(t, getBoolFromKVOrDefault(nil, "nil", false))
|
||||||
assert.False(t, getBoolFromKVOrDefault(map[string]string{}, "empty", false))
|
assert.False(t, getBoolFromKVOrDefault(map[string]string{}, "empty", false))
|
||||||
|
|
||||||
|
// Test with unquoted values (as stored by RawText())
|
||||||
|
unquotedProperties := map[string]string{
|
||||||
|
"enabled": "true",
|
||||||
|
"disabled": "false",
|
||||||
|
"invalid": "notabool",
|
||||||
|
"empty_value": "",
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.True(t, getBoolFromKVOrDefault(unquotedProperties, "enabled", false))
|
||||||
|
assert.False(t, getBoolFromKVOrDefault(unquotedProperties, "disabled", true))
|
||||||
|
assert.False(t, getBoolFromKVOrDefault(unquotedProperties, "invalid", false))
|
||||||
|
assert.False(t, getBoolFromKVOrDefault(unquotedProperties, "empty_value", false))
|
||||||
}
|
}
|
||||||
|
|
||||||
func Test_getStringFromKVOrDefault(t *testing.T) {
|
func Test_getStringFromKVOrDefault(t *testing.T) {
|
||||||
@@ -34,6 +47,17 @@ func Test_getStringFromKVOrDefault(t *testing.T) {
|
|||||||
assert.Equal(t, "default", getStringFromKVOrDefault(properties, "missing", "default"))
|
assert.Equal(t, "default", getStringFromKVOrDefault(properties, "missing", "default"))
|
||||||
assert.Equal(t, "default", getStringFromKVOrDefault(nil, "nil", "default"))
|
assert.Equal(t, "default", getStringFromKVOrDefault(nil, "nil", "default"))
|
||||||
assert.Equal(t, "default", getStringFromKVOrDefault(map[string]string{}, "empty", "default"))
|
assert.Equal(t, "default", getStringFromKVOrDefault(map[string]string{}, "empty", "default"))
|
||||||
|
|
||||||
|
// Test with unquoted values (as stored by RawText())
|
||||||
|
unquotedProperties := map[string]string{
|
||||||
|
"name": "example",
|
||||||
|
"title": "Demo API",
|
||||||
|
"empty": "",
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, "example", getStringFromKVOrDefault(unquotedProperties, "name", "default"))
|
||||||
|
assert.Equal(t, "Demo API", getStringFromKVOrDefault(unquotedProperties, "title", "default"))
|
||||||
|
assert.Equal(t, "default", getStringFromKVOrDefault(unquotedProperties, "empty", "default"))
|
||||||
}
|
}
|
||||||
|
|
||||||
func Test_getListFromInfoOrDefault(t *testing.T) {
|
func Test_getListFromInfoOrDefault(t *testing.T) {
|
||||||
@@ -50,4 +74,18 @@ func Test_getListFromInfoOrDefault(t *testing.T) {
|
|||||||
assert.Equal(t, []string{"default"}, getListFromInfoOrDefault(map[string]string{
|
assert.Equal(t, []string{"default"}, getListFromInfoOrDefault(map[string]string{
|
||||||
"foo": ",,",
|
"foo": ",,",
|
||||||
}, "foo", []string{"default"}))
|
}, "foo", []string{"default"}))
|
||||||
|
|
||||||
|
// Test with unquoted values (as stored by RawText())
|
||||||
|
unquotedProperties := map[string]string{
|
||||||
|
"list": "a, b, c",
|
||||||
|
"schemes": "http,https",
|
||||||
|
"tags": "query",
|
||||||
|
"empty": "",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: FieldsAndTrimSpace doesn't actually trim the spaces from returned values
|
||||||
|
assert.Equal(t, []string{"a", " b", " c"}, getListFromInfoOrDefault(unquotedProperties, "list", []string{"default"}))
|
||||||
|
assert.Equal(t, []string{"http", "https"}, getListFromInfoOrDefault(unquotedProperties, "schemes", []string{"default"}))
|
||||||
|
assert.Equal(t, []string{"query"}, getListFromInfoOrDefault(unquotedProperties, "tags", []string{"default"}))
|
||||||
|
assert.Equal(t, []string{"default"}, getListFromInfoOrDefault(unquotedProperties, "empty", []string{"default"}))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,11 @@ func spec2Paths(ctx Context, srv apiSpec.Service) *spec.Paths {
|
|||||||
for _, route := range group.Routes {
|
for _, route := range group.Routes {
|
||||||
routPath := pathVariable2SwaggerVariable(ctx, route.Path)
|
routPath := pathVariable2SwaggerVariable(ctx, route.Path)
|
||||||
if len(prefix) > 0 && prefix != "." {
|
if len(prefix) > 0 && prefix != "." {
|
||||||
routPath = "/" + path.Clean(prefix) + routPath
|
if routPath == "/" {
|
||||||
|
routPath = "/" + path.Clean(prefix)
|
||||||
|
} else {
|
||||||
|
routPath = "/" + path.Clean(prefix) + routPath
|
||||||
|
}
|
||||||
}
|
}
|
||||||
pathItem := spec2Path(ctx, group, route)
|
pathItem := spec2Path(ctx, group, route)
|
||||||
existPathItem, ok := paths.Paths[routPath]
|
existPathItem, ok := paths.Paths[routPath]
|
||||||
|
|||||||
90
tools/goctl/api/swagger/path_test.go
Normal file
90
tools/goctl/api/swagger/path_test.go
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
package swagger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/zeromicro/go-zero/tools/goctl/api/spec"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSpec2PathsWithRootRoute(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
prefix string
|
||||||
|
routePath string
|
||||||
|
expectedPath string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "prefix with root route",
|
||||||
|
prefix: "/api/v1/shoppings",
|
||||||
|
routePath: "/",
|
||||||
|
expectedPath: "/api/v1/shoppings",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "prefix with sub route",
|
||||||
|
prefix: "/api/v1/shoppings",
|
||||||
|
routePath: "/list",
|
||||||
|
expectedPath: "/api/v1/shoppings/list",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty prefix with root route",
|
||||||
|
prefix: "",
|
||||||
|
routePath: "/",
|
||||||
|
expectedPath: "/",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty prefix with sub route",
|
||||||
|
prefix: "",
|
||||||
|
routePath: "/list",
|
||||||
|
expectedPath: "/list",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "prefix with trailing slash and root route",
|
||||||
|
prefix: "/api/v1/shoppings/",
|
||||||
|
routePath: "/",
|
||||||
|
expectedPath: "/api/v1/shoppings",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "prefix without leading slash and root route",
|
||||||
|
prefix: "api/v1/shoppings",
|
||||||
|
routePath: "/",
|
||||||
|
expectedPath: "/api/v1/shoppings",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "single level prefix with root route",
|
||||||
|
prefix: "/api",
|
||||||
|
routePath: "/",
|
||||||
|
expectedPath: "/api",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
srv := spec.Service{
|
||||||
|
Groups: []spec.Group{
|
||||||
|
{
|
||||||
|
Annotation: spec.Annotation{
|
||||||
|
Properties: map[string]string{
|
||||||
|
propertyKeyPrefix: tt.prefix,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Routes: []spec.Route{
|
||||||
|
{
|
||||||
|
Method: "get",
|
||||||
|
Path: tt.routePath,
|
||||||
|
Handler: "TestHandler",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := testingContext(t)
|
||||||
|
paths := spec2Paths(ctx, srv)
|
||||||
|
|
||||||
|
assert.Contains(t, paths.Paths, tt.expectedPath,
|
||||||
|
"Expected path %s not found in generated paths. Got: %v",
|
||||||
|
tt.expectedPath, paths.Paths)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -70,15 +70,40 @@ func propertiesFromType(ctx Context, tp apiSpec.Type) (spec.SchemaProperties, []
|
|||||||
switch sampleTypeFromGoType(ctx, member.Type) {
|
switch sampleTypeFromGoType(ctx, member.Type) {
|
||||||
case swaggerTypeArray:
|
case swaggerTypeArray:
|
||||||
schema.Items = itemsFromGoType(ctx, member.Type)
|
schema.Items = itemsFromGoType(ctx, member.Type)
|
||||||
|
// Special handling for arrays with useDefinitions
|
||||||
|
if ctx.UseDefinitions {
|
||||||
|
// For arrays, check if the array element (not the array itself) contains a struct
|
||||||
|
if arrayType, ok := member.Type.(apiSpec.ArrayType); ok {
|
||||||
|
if structName, containsStruct := containsStruct(arrayType.Value); containsStruct {
|
||||||
|
// Set the $ref inside the items, not at the schema level
|
||||||
|
schema.Items = &spec.SchemaOrArray{
|
||||||
|
Schema: &spec.Schema{
|
||||||
|
SchemaProps: spec.SchemaProps{
|
||||||
|
Ref: spec.MustCreateRef(getRefName(structName)),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
case swaggerTypeObject:
|
case swaggerTypeObject:
|
||||||
p, r := propertiesFromType(ctx, member.Type)
|
p, r := propertiesFromType(ctx, member.Type)
|
||||||
schema.Properties = p
|
schema.Properties = p
|
||||||
schema.Required = r
|
schema.Required = r
|
||||||
}
|
// For objects with useDefinitions, set $ref at schema level
|
||||||
if ctx.UseDefinitions {
|
if ctx.UseDefinitions {
|
||||||
structName, containsStruct := containsStruct(member.Type)
|
structName, containsStruct := containsStruct(member.Type)
|
||||||
if containsStruct {
|
if containsStruct {
|
||||||
schema.SchemaProps.Ref = spec.MustCreateRef(getRefName(structName))
|
schema.SchemaProps.Ref = spec.MustCreateRef(getRefName(structName))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
// For non-array, non-object types, apply useDefinitions logic
|
||||||
|
if ctx.UseDefinitions {
|
||||||
|
structName, containsStruct := containsStruct(member.Type)
|
||||||
|
if containsStruct {
|
||||||
|
schema.SchemaProps.Ref = spec.MustCreateRef(getRefName(structName))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package swagger
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/zeromicro/go-zero/tools/goctl/api/spec"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -23,3 +24,117 @@ func Test_pathVariable2SwaggerVariable(t *testing.T) {
|
|||||||
assert.Equal(t, tc.expected, result)
|
assert.Equal(t, tc.expected, result)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestArrayDefinitionsBug(t *testing.T) {
|
||||||
|
// Test case for the bug where array of structs with useDefinitions
|
||||||
|
// generates incorrect swagger JSON structure
|
||||||
|
|
||||||
|
// Context with useDefinitions enabled
|
||||||
|
ctx := Context{
|
||||||
|
UseDefinitions: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a test struct containing an array of structs
|
||||||
|
testStruct := spec.DefineStruct{
|
||||||
|
RawName: "TestStruct",
|
||||||
|
Members: []spec.Member{
|
||||||
|
{
|
||||||
|
Name: "ArrayField",
|
||||||
|
Type: spec.ArrayType{
|
||||||
|
Value: spec.DefineStruct{
|
||||||
|
RawName: "ItemStruct",
|
||||||
|
Members: []spec.Member{
|
||||||
|
{
|
||||||
|
Name: "ItemName",
|
||||||
|
Type: spec.PrimitiveType{RawName: "string"},
|
||||||
|
Tag: `json:"itemName"`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Tag: `json:"arrayField"`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get properties from the struct
|
||||||
|
properties, _ := propertiesFromType(ctx, testStruct)
|
||||||
|
|
||||||
|
// Check that we have the array field
|
||||||
|
assert.Contains(t, properties, "arrayField")
|
||||||
|
arrayField := properties["arrayField"]
|
||||||
|
|
||||||
|
// 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")
|
||||||
|
|
||||||
|
// 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")
|
||||||
|
assert.Equal(t, "#/definitions/ItemStruct", arrayField.Items.Schema.Ref.String())
|
||||||
|
|
||||||
|
// Verify there are no other properties in the items when using $ref
|
||||||
|
assert.Nil(t, arrayField.Items.Schema.Properties, "Items with $ref should not have properties")
|
||||||
|
assert.Empty(t, arrayField.Items.Schema.Required, "Items with $ref should not have required")
|
||||||
|
assert.Empty(t, arrayField.Items.Schema.Type, "Items with $ref should not have type")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestArrayWithoutDefinitions(t *testing.T) {
|
||||||
|
// Test that arrays work correctly when useDefinitions is false
|
||||||
|
ctx := Context{
|
||||||
|
UseDefinitions: false, // This is the default
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the same test struct
|
||||||
|
testStruct := spec.DefineStruct{
|
||||||
|
RawName: "TestStruct",
|
||||||
|
Members: []spec.Member{
|
||||||
|
{
|
||||||
|
Name: "ArrayField",
|
||||||
|
Type: spec.ArrayType{
|
||||||
|
Value: spec.DefineStruct{
|
||||||
|
RawName: "ItemStruct",
|
||||||
|
Members: []spec.Member{
|
||||||
|
{
|
||||||
|
Name: "ItemName",
|
||||||
|
Type: spec.PrimitiveType{RawName: "string"},
|
||||||
|
Tag: `json:"itemName"`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Tag: `json:"arrayField"`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
properties, _ := propertiesFromType(ctx, testStruct)
|
||||||
|
|
||||||
|
assert.Contains(t, properties, "arrayField")
|
||||||
|
arrayField := properties["arrayField"]
|
||||||
|
|
||||||
|
// Should be array type
|
||||||
|
assert.Equal(t, "array", arrayField.Type[0])
|
||||||
|
|
||||||
|
// Should have items with full schema, no $ref
|
||||||
|
assert.NotNil(t, arrayField.Items)
|
||||||
|
assert.NotNil(t, arrayField.Items.Schema)
|
||||||
|
|
||||||
|
// Should NOT have $ref at schema level
|
||||||
|
assert.Empty(t, arrayField.Ref.String(), "Schema should not have $ref when useDefinitions is false")
|
||||||
|
|
||||||
|
// Should NOT have $ref in items either
|
||||||
|
assert.Empty(t, arrayField.Items.Schema.Ref.String(), "Items should not have $ref when useDefinitions is false")
|
||||||
|
|
||||||
|
// Should have full schema properties in items
|
||||||
|
assert.Equal(t, "object", arrayField.Items.Schema.Type[0])
|
||||||
|
assert.Contains(t, arrayField.Items.Schema.Properties, "itemName")
|
||||||
|
assert.Equal(t, []string{"itemName"}, arrayField.Items.Schema.Required)
|
||||||
|
}
|
||||||
|
|||||||
@@ -10,13 +10,13 @@ require (
|
|||||||
github.com/go-sql-driver/mysql v1.9.0
|
github.com/go-sql-driver/mysql v1.9.0
|
||||||
github.com/gookit/color v1.6.0
|
github.com/gookit/color v1.6.0
|
||||||
github.com/iancoleman/strcase v0.3.0
|
github.com/iancoleman/strcase v0.3.0
|
||||||
github.com/spf13/cobra v1.9.1
|
github.com/spf13/cobra v1.10.1
|
||||||
github.com/spf13/pflag v1.0.7
|
github.com/spf13/pflag v1.0.10
|
||||||
github.com/stretchr/testify v1.11.1
|
github.com/stretchr/testify v1.11.1
|
||||||
github.com/withfig/autocomplete-tools/integrations/cobra v1.2.1
|
github.com/withfig/autocomplete-tools/integrations/cobra v1.2.1
|
||||||
github.com/zeromicro/antlr v0.0.1
|
github.com/zeromicro/antlr v0.0.1
|
||||||
github.com/zeromicro/ddl-parser v1.0.5
|
github.com/zeromicro/ddl-parser v1.0.5
|
||||||
github.com/zeromicro/go-zero v1.9.0
|
github.com/zeromicro/go-zero v1.9.1
|
||||||
golang.org/x/text v0.22.0
|
golang.org/x/text v0.22.0
|
||||||
google.golang.org/grpc v1.65.0
|
google.golang.org/grpc v1.65.0
|
||||||
google.golang.org/protobuf v1.36.5
|
google.golang.org/protobuf v1.36.5
|
||||||
@@ -47,8 +47,8 @@ require (
|
|||||||
github.com/google/go-cmp v0.6.0 // indirect
|
github.com/google/go-cmp v0.6.0 // indirect
|
||||||
github.com/google/gofuzz v1.2.0 // indirect
|
github.com/google/gofuzz v1.2.0 // indirect
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
github.com/grafana/pyroscope-go v1.2.4 // indirect
|
github.com/grafana/pyroscope-go v1.2.7 // indirect
|
||||||
github.com/grafana/pyroscope-go/godeltaprof v0.1.8 // indirect
|
github.com/grafana/pyroscope-go/godeltaprof v0.1.9 // indirect
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||||
@@ -72,7 +72,7 @@ require (
|
|||||||
github.com/prometheus/client_model v0.6.1 // indirect
|
github.com/prometheus/client_model v0.6.1 // indirect
|
||||||
github.com/prometheus/common v0.62.0 // indirect
|
github.com/prometheus/common v0.62.0 // indirect
|
||||||
github.com/prometheus/procfs v0.15.1 // indirect
|
github.com/prometheus/procfs v0.15.1 // indirect
|
||||||
github.com/redis/go-redis/v9 v9.12.1 // indirect
|
github.com/redis/go-redis/v9 v9.15.0 // indirect
|
||||||
github.com/spaolacci/murmur3 v1.1.0 // indirect
|
github.com/spaolacci/murmur3 v1.1.0 // indirect
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||||
github.com/yuin/gopher-lua v1.1.1 // indirect
|
github.com/yuin/gopher-lua v1.1.1 // indirect
|
||||||
|
|||||||
@@ -75,10 +75,10 @@ github.com/gookit/assert v0.1.1 h1:lh3GcawXe/p+cU7ESTZ5Ui3Sm/x8JWpIis4/1aF0mY0=
|
|||||||
github.com/gookit/assert v0.1.1/go.mod h1:jS5bmIVQZTIwk42uXl4lyj4iaaxx32tqH16CFj0VX2E=
|
github.com/gookit/assert v0.1.1/go.mod h1:jS5bmIVQZTIwk42uXl4lyj4iaaxx32tqH16CFj0VX2E=
|
||||||
github.com/gookit/color v1.6.0 h1:JjJXBTk1ETNyqyilJhkTXJYYigHG24TM9Xa2M1xAhRA=
|
github.com/gookit/color v1.6.0 h1:JjJXBTk1ETNyqyilJhkTXJYYigHG24TM9Xa2M1xAhRA=
|
||||||
github.com/gookit/color v1.6.0/go.mod h1:9ACFc7/1IpHGBW8RwuDm/0YEnhg3dwwXpoMsmtyHfjs=
|
github.com/gookit/color v1.6.0/go.mod h1:9ACFc7/1IpHGBW8RwuDm/0YEnhg3dwwXpoMsmtyHfjs=
|
||||||
github.com/grafana/pyroscope-go v1.2.4 h1:B22GMXz+O0nWLatxLuaP7o7L9dvP0clLvIpmeEQQM0Q=
|
github.com/grafana/pyroscope-go v1.2.7 h1:VWBBlqxjyR0Cwk2W6UrE8CdcdD80GOFNutj0Kb1T8ac=
|
||||||
github.com/grafana/pyroscope-go v1.2.4/go.mod h1:zzT9QXQAp2Iz2ZdS216UiV8y9uXJYQiGE1q8v1FyhqU=
|
github.com/grafana/pyroscope-go v1.2.7/go.mod h1:o/bpSLiJYYP6HQtvcoVKiE9s5RiNgjYTj1DhiddP2Pc=
|
||||||
github.com/grafana/pyroscope-go/godeltaprof v0.1.8 h1:iwOtYXeeVSAeYefJNaxDytgjKtUuKQbJqgAIjlnicKg=
|
github.com/grafana/pyroscope-go/godeltaprof v0.1.9 h1:c1Us8i6eSmkW+Ez05d3co8kasnuOY813tbMN8i/a3Og=
|
||||||
github.com/grafana/pyroscope-go/godeltaprof v0.1.8/go.mod h1:2+l7K7twW49Ct4wFluZD3tZ6e0SjanjcUUBPVD/UuGU=
|
github.com/grafana/pyroscope-go/godeltaprof v0.1.9/go.mod h1:2+l7K7twW49Ct4wFluZD3tZ6e0SjanjcUUBPVD/UuGU=
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0=
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0=
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k=
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k=
|
||||||
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw=
|
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw=
|
||||||
@@ -148,18 +148,18 @@ github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ
|
|||||||
github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
|
github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
|
||||||
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
|
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
|
||||||
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
|
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
|
||||||
github.com/redis/go-redis/v9 v9.12.1 h1:k5iquqv27aBtnTm2tIkROUDp8JBXhXZIVu1InSgvovg=
|
github.com/redis/go-redis/v9 v9.15.0 h1:2jdes0xJxer4h3NUZrZ4OGSntGlXp4WbXju2nOTRXto=
|
||||||
github.com/redis/go-redis/v9 v9.12.1/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw=
|
github.com/redis/go-redis/v9 v9.15.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw=
|
||||||
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
|
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
|
||||||
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
|
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
|
||||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
|
github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
|
||||||
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||||
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
|
github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s=
|
||||||
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
|
github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0=
|
||||||
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
github.com/spf13/pflag v1.0.7 h1:vN6T9TfwStFPFM5XzjsvmzZkLuaLX+HS+0SeFLRgU6M=
|
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
|
||||||
github.com/spf13/pflag v1.0.7/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
@@ -185,8 +185,8 @@ github.com/zeromicro/antlr v0.0.1 h1:CQpIn/dc0pUjgGQ81y98s/NGOm2Hfru2NNio2I9mQgk
|
|||||||
github.com/zeromicro/antlr v0.0.1/go.mod h1:nfpjEwFR6Q4xGDJMcZnCL9tEfQRgszMwu3rDz2Z+p5M=
|
github.com/zeromicro/antlr v0.0.1/go.mod h1:nfpjEwFR6Q4xGDJMcZnCL9tEfQRgszMwu3rDz2Z+p5M=
|
||||||
github.com/zeromicro/ddl-parser v1.0.5 h1:LaVqHdzMTjasua1yYpIYaksxKqRzFrEukj2Wi2EbWaQ=
|
github.com/zeromicro/ddl-parser v1.0.5 h1:LaVqHdzMTjasua1yYpIYaksxKqRzFrEukj2Wi2EbWaQ=
|
||||||
github.com/zeromicro/ddl-parser v1.0.5/go.mod h1:ISU/8NuPyEpl9pa17Py9TBPetMjtsiHrb9f5XGiYbo8=
|
github.com/zeromicro/ddl-parser v1.0.5/go.mod h1:ISU/8NuPyEpl9pa17Py9TBPetMjtsiHrb9f5XGiYbo8=
|
||||||
github.com/zeromicro/go-zero v1.9.0 h1:hlVtQCSHPszQdcwZTawzGwTej1G2mhHybYzMRLuwCt4=
|
github.com/zeromicro/go-zero v1.9.1 h1:GZCl4jun/ZgZHnSvX3SSNDHf+tEGmEQ8x2Z23xjHa9g=
|
||||||
github.com/zeromicro/go-zero v1.9.0/go.mod h1:TMyCxiaOjLQ3YxyYlJrejaQZF40RlzQ3FVvFu5EbcV4=
|
github.com/zeromicro/go-zero v1.9.1/go.mod h1:bHOl7Xr7EV/iHZWEqsUNJwFc/9WgAMrPpPagYvOaMtY=
|
||||||
go.etcd.io/etcd/api/v3 v3.5.15 h1:3KpLJir1ZEBrYuV2v+Twaa/e2MdDCEZ/70H+lzEiwsk=
|
go.etcd.io/etcd/api/v3 v3.5.15 h1:3KpLJir1ZEBrYuV2v+Twaa/e2MdDCEZ/70H+lzEiwsk=
|
||||||
go.etcd.io/etcd/api/v3 v3.5.15/go.mod h1:N9EhGzXq58WuMllgH9ZvnEr7SI9pS0k0+DHZezGp7jM=
|
go.etcd.io/etcd/api/v3 v3.5.15/go.mod h1:N9EhGzXq58WuMllgH9ZvnEr7SI9pS0k0+DHZezGp7jM=
|
||||||
go.etcd.io/etcd/client/pkg/v3 v3.5.15 h1:fo0HpWz/KlHGMCC+YejpiCmyWDEuIpnTDzpJLB5fWlA=
|
go.etcd.io/etcd/client/pkg/v3 v3.5.15 h1:fo0HpWz/KlHGMCC+YejpiCmyWDEuIpnTDzpJLB5fWlA=
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// BuildVersion is the version of goctl.
|
// BuildVersion is the version of goctl.
|
||||||
const BuildVersion = "1.9.0"
|
const BuildVersion = "1.9.1"
|
||||||
|
|
||||||
var tag = map[string]int{"pre-alpha": 0, "alpha": 1, "pre-beta": 2, "beta": 3, "released": 4, "": 5}
|
var tag = map[string]int{"pre-alpha": 0, "alpha": 1, "pre-beta": 2, "beta": 3, "released": 4, "": 5}
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import (
|
|||||||
"github.com/zeromicro/go-zero/tools/goctl/util/format"
|
"github.com/zeromicro/go-zero/tools/goctl/util/format"
|
||||||
)
|
)
|
||||||
|
|
||||||
// BeforeCommands run before comamnd run to show some migration notes
|
// BeforeCommands run before command run to show some migration notes
|
||||||
func BeforeCommands(dir, style string) error {
|
func BeforeCommands(dir, style string) error {
|
||||||
if err := migrateBefore1_3_4(dir, style); err != nil {
|
if err := migrateBefore1_3_4(dir, style); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -425,9 +425,12 @@ func (a *Analyzer) getType(expr *ast.BodyStmt, req bool) (spec.Type, error) {
|
|||||||
}
|
}
|
||||||
if body.LBrack != nil {
|
if body.LBrack != nil {
|
||||||
if body.Star != nil {
|
if body.Star != nil {
|
||||||
return spec.PointerType{
|
return spec.ArrayType{
|
||||||
RawName: rawText,
|
RawName: rawText,
|
||||||
Type: tp,
|
Value: spec.PointerType{
|
||||||
|
RawName: rawText,
|
||||||
|
Type: tp,
|
||||||
|
},
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
return spec.ArrayType{
|
return spec.ArrayType{
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ func Install(cacheDir string) (string, error) {
|
|||||||
case vars.OsLinux:
|
case vars.OsLinux:
|
||||||
downloadUrl = url[fmt.Sprintf("%s_%d", vars.OsLinux, bit)]
|
downloadUrl = url[fmt.Sprintf("%s_%d", vars.OsLinux, bit)]
|
||||||
default:
|
default:
|
||||||
return "", fmt.Errorf("unsupport OS: %q", goos)
|
return "", fmt.Errorf("unsupported OS: %q", goos)
|
||||||
}
|
}
|
||||||
|
|
||||||
err := downloader.Download(downloadUrl, tempFile)
|
err := downloader.Download(downloadUrl, tempFile)
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package util
|
package util
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"slices"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/zeromicro/go-zero/tools/goctl/util/console"
|
"github.com/zeromicro/go-zero/tools/goctl/util/console"
|
||||||
@@ -54,14 +56,9 @@ func Untitle(s string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Index returns the index where the item equal,it will return -1 if mismatched
|
// Index returns the index where the item equal,it will return -1 if mismatched
|
||||||
|
// Deprecated: use slices.Index instead
|
||||||
func Index(slice []string, item string) int {
|
func Index(slice []string, item string) int {
|
||||||
for i := range slice {
|
return slices.Index(slice, item)
|
||||||
if slice[i] == item {
|
|
||||||
return i
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return -1
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SafeString converts the input string into a safe naming style in golang
|
// SafeString converts the input string into a safe naming style in golang
|
||||||
@@ -134,21 +131,13 @@ func FieldsAndTrimSpace(s string, f func(r rune) bool) []string {
|
|||||||
return resp
|
return resp
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//Deprecated: This function implementation is incomplete and does not properly handle exceptional input cases.
|
||||||
|
//We strongly recommend using the standard library's strconv.Unquote function instead,
|
||||||
|
//which provides robust error handling and comprehensive support for various input formats.
|
||||||
func Unquote(s string) string {
|
func Unquote(s string) string {
|
||||||
if len(s) == 0 {
|
ns, err := strconv.Unquote(s)
|
||||||
return s
|
if err != nil {
|
||||||
|
return ""
|
||||||
}
|
}
|
||||||
left := s[0]
|
return ns
|
||||||
|
|
||||||
if left == '`' || left == '"' {
|
|
||||||
s = s[1:len(s)]
|
|
||||||
}
|
|
||||||
if len(s) == 0 {
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
right := s[len(s)-1]
|
|
||||||
if right == '`' || right == '"' {
|
|
||||||
s = s[0 : len(s)-1]
|
|
||||||
}
|
|
||||||
return s
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user