Compare commits

..

23 Commits

Author SHA1 Message Date
kevin
456b395860 use predefined endpoint separator 2020-08-25 18:36:30 +08:00
kevin
f3c367a323 add fatal to stderr 2020-08-25 16:59:14 +08:00
kevin
a32028c4fb add etcd deploy yaml 2020-08-25 16:32:01 +08:00
kevin
b4572fa064 add more tests 2020-08-24 23:09:46 +08:00
kevin
ccbabf6f58 add more tests 2020-08-24 18:18:58 +08:00
kevin
5989444227 add more tests 2020-08-23 22:33:20 +08:00
kevin
dc286a03f5 add more tests 2020-08-23 15:53:10 +08:00
kevin
b82c02ed16 add more tests 2020-08-22 23:08:33 +08:00
kevin
59ba4ecc5b accelerate tests 2020-08-21 23:24:07 +08:00
kevin
5e7b514ae2 make tests parallel 2020-08-21 23:15:45 +08:00
kevin
2b1466e41e add more tests 2020-08-21 23:09:35 +08:00
kevin
9c9f80518f update readme 2020-08-21 22:51:04 +08:00
kevin
25973d6b59 update doc, add architecture picture 2020-08-21 20:09:53 +08:00
kevin
6237d01948 make test stable 2020-08-21 16:57:17 +08:00
kevin
49316b113e update readme 2020-08-21 16:52:17 +08:00
kevin
6a673e8cb0 add more tests 2020-08-21 16:42:08 +08:00
kingxt
0efa28ddbd fix generate api demo (#19)
Co-authored-by: kingxt <dream4kingxt@163.com>
2020-08-21 13:47:35 +08:00
kevin
0b6a13fe84 add more tests 2020-08-20 22:53:18 +08:00
kevin
11aa6668e8 add more tests 2020-08-20 15:35:13 +08:00
kevin
267a283328 reorg imports 2020-08-20 10:46:39 +08:00
kevin
2d8366b30e update keywords.md 2020-08-20 10:44:14 +08:00
Keson
db83843558 gocctl model v20200819 (#18)
* rename snake、came method

* new: generate model from data source

* add change log md

* update model doc

* update  doc

* beauty code
2020-08-20 10:29:18 +08:00
kevin
50565c9765 update doc 2020-08-19 22:34:54 +08:00
58 changed files with 1667 additions and 198 deletions

View File

@@ -1,3 +1,4 @@
ignore:
- "example/*"
- "tools/*"
- "doc"
- "example"
- "tools"

View File

@@ -213,7 +213,10 @@ func TestTimingWheel_SetTimer(t *testing.T) {
}
for _, test := range tests {
test := test
t.Run(stringx.RandId(), func(t *testing.T) {
t.Parallel()
var count int32
ticker := timex.NewFakeTicker()
tick := func() {
@@ -291,7 +294,10 @@ func TestTimingWheel_SetAndMoveThenStart(t *testing.T) {
}
for _, test := range tests {
test := test
t.Run(stringx.RandId(), func(t *testing.T) {
t.Parallel()
var count int32
ticker := timex.NewFakeTicker()
tick := func() {
@@ -376,7 +382,10 @@ func TestTimingWheel_SetAndMoveTwice(t *testing.T) {
}
for _, test := range tests {
test := test
t.Run(stringx.RandId(), func(t *testing.T) {
t.Parallel()
var count int32
ticker := timex.NewFakeTicker()
tick := func() {
@@ -454,7 +463,10 @@ func TestTimingWheel_ElapsedAndSet(t *testing.T) {
}
for _, test := range tests {
test := test
t.Run(stringx.RandId(), func(t *testing.T) {
t.Parallel()
var count int32
ticker := timex.NewFakeTicker()
tick := func() {
@@ -542,7 +554,10 @@ func TestTimingWheel_ElapsedAndSetThenMove(t *testing.T) {
}
for _, test := range tests {
test := test
t.Run(stringx.RandId(), func(t *testing.T) {
t.Parallel()
var count int32
ticker := timex.NewFakeTicker()
tick := func() {

View File

@@ -0,0 +1,4 @@
apiVersion: v1
kind: Namespace
metadata:
name: discov

View File

@@ -0,0 +1,368 @@
apiVersion: v1
kind: Service
metadata:
name: etcd
namespace: discov
spec:
ports:
- name: etcd-port
port: 2379
protocol: TCP
targetPort: 2379
selector:
app: etcd
---
apiVersion: v1
kind: Pod
metadata:
labels:
app: etcd
etcd_node: etcd0
name: etcd0
namespace: discov
spec:
containers:
- command:
- /usr/local/bin/etcd
- --name
- etcd0
- --initial-advertise-peer-urls
- http://etcd0:2380
- --listen-peer-urls
- http://0.0.0.0:2380
- --listen-client-urls
- http://0.0.0.0:2379
- --advertise-client-urls
- http://etcd0:2379
- --initial-cluster
- etcd0=http://etcd0:2380,etcd1=http://etcd1:2380,etcd2=http://etcd2:2380,etcd3=http://etcd3:2380,etcd4=http://etcd4:2380
- --initial-cluster-state
- new
image: quay.io/coreos/etcd:latest
name: etcd0
ports:
- containerPort: 2379
name: client
protocol: TCP
- containerPort: 2380
name: server
protocol: TCP
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: app
operator: In
values:
- etcd
topologyKey: "kubernetes.io/hostname"
restartPolicy: Always
---
apiVersion: v1
kind: Service
metadata:
labels:
etcd_node: etcd0
name: etcd0
namespace: discov
spec:
ports:
- name: client
port: 2379
protocol: TCP
targetPort: 2379
- name: server
port: 2380
protocol: TCP
targetPort: 2380
selector:
etcd_node: etcd0
---
apiVersion: v1
kind: Pod
metadata:
labels:
app: etcd
etcd_node: etcd1
name: etcd1
namespace: discov
spec:
containers:
- command:
- /usr/local/bin/etcd
- --name
- etcd1
- --initial-advertise-peer-urls
- http://etcd1:2380
- --listen-peer-urls
- http://0.0.0.0:2380
- --listen-client-urls
- http://0.0.0.0:2379
- --advertise-client-urls
- http://etcd1:2379
- --initial-cluster
- etcd0=http://etcd0:2380,etcd1=http://etcd1:2380,etcd2=http://etcd2:2380,etcd3=http://etcd3:2380,etcd4=http://etcd4:2380
- --initial-cluster-state
- new
image: quay.io/coreos/etcd:latest
name: etcd1
ports:
- containerPort: 2379
name: client
protocol: TCP
- containerPort: 2380
name: server
protocol: TCP
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: app
operator: In
values:
- etcd
topologyKey: "kubernetes.io/hostname"
restartPolicy: Always
---
apiVersion: v1
kind: Service
metadata:
labels:
etcd_node: etcd1
name: etcd1
namespace: discov
spec:
ports:
- name: client
port: 2379
protocol: TCP
targetPort: 2379
- name: server
port: 2380
protocol: TCP
targetPort: 2380
selector:
etcd_node: etcd1
---
apiVersion: v1
kind: Pod
metadata:
labels:
app: etcd
etcd_node: etcd2
name: etcd2
namespace: discov
spec:
containers:
- command:
- /usr/local/bin/etcd
- --name
- etcd2
- --initial-advertise-peer-urls
- http://etcd2:2380
- --listen-peer-urls
- http://0.0.0.0:2380
- --listen-client-urls
- http://0.0.0.0:2379
- --advertise-client-urls
- http://etcd2:2379
- --initial-cluster
- etcd0=http://etcd0:2380,etcd1=http://etcd1:2380,etcd2=http://etcd2:2380,etcd3=http://etcd3:2380,etcd4=http://etcd4:2380
- --initial-cluster-state
- new
image: quay.io/coreos/etcd:latest
name: etcd2
ports:
- containerPort: 2379
name: client
protocol: TCP
- containerPort: 2380
name: server
protocol: TCP
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: app
operator: In
values:
- etcd
topologyKey: "kubernetes.io/hostname"
restartPolicy: Always
---
apiVersion: v1
kind: Service
metadata:
labels:
etcd_node: etcd2
name: etcd2
namespace: discov
spec:
ports:
- name: client
port: 2379
protocol: TCP
targetPort: 2379
- name: server
port: 2380
protocol: TCP
targetPort: 2380
selector:
etcd_node: etcd2
---
apiVersion: v1
kind: Pod
metadata:
labels:
app: etcd
etcd_node: etcd3
name: etcd3
namespace: discov
spec:
containers:
- command:
- /usr/local/bin/etcd
- --name
- etcd3
- --initial-advertise-peer-urls
- http://etcd3:2380
- --listen-peer-urls
- http://0.0.0.0:2380
- --listen-client-urls
- http://0.0.0.0:2379
- --advertise-client-urls
- http://etcd3:2379
- --initial-cluster
- etcd0=http://etcd0:2380,etcd1=http://etcd1:2380,etcd2=http://etcd2:2380,etcd3=http://etcd3:2380,etcd4=http://etcd4:2380
- --initial-cluster-state
- new
image: quay.io/coreos/etcd:latest
name: etcd3
ports:
- containerPort: 2379
name: client
protocol: TCP
- containerPort: 2380
name: server
protocol: TCP
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: app
operator: In
values:
- etcd
topologyKey: "kubernetes.io/hostname"
restartPolicy: Always
---
apiVersion: v1
kind: Service
metadata:
labels:
etcd_node: etcd3
name: etcd3
namespace: discov
spec:
ports:
- name: client
port: 2379
protocol: TCP
targetPort: 2379
- name: server
port: 2380
protocol: TCP
targetPort: 2380
selector:
etcd_node: etcd3
---
apiVersion: v1
kind: Pod
metadata:
labels:
app: etcd
etcd_node: etcd4
name: etcd4
namespace: discov
spec:
containers:
- command:
- /usr/local/bin/etcd
- --name
- etcd4
- --initial-advertise-peer-urls
- http://etcd4:2380
- --listen-peer-urls
- http://0.0.0.0:2380
- --listen-client-urls
- http://0.0.0.0:2379
- --advertise-client-urls
- http://etcd4:2379
- --initial-cluster
- etcd0=http://etcd0:2380,etcd1=http://etcd1:2380,etcd2=http://etcd2:2380,etcd3=http://etcd3:2380,etcd4=http://etcd4:2380
- --initial-cluster-state
- new
image: quay.io/coreos/etcd:latest
name: etcd4
ports:
- containerPort: 2379
name: client
protocol: TCP
- containerPort: 2380
name: server
protocol: TCP
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: app
operator: In
values:
- etcd
topologyKey: "kubernetes.io/hostname"
restartPolicy: Always
---
apiVersion: v1
kind: Service
metadata:
labels:
etcd_node: etcd4
name: etcd4
namespace: discov
spec:
ports:
- name: client
port: 2379
protocol: TCP
targetPort: 2379
- name: server
port: 2380
protocol: TCP
targetPort: 2380
selector:
etcd_node: etcd4

View File

@@ -4,9 +4,8 @@ import (
"os"
"testing"
"github.com/tal-tech/go-zero/core/fs"
"github.com/stretchr/testify/assert"
"github.com/tal-tech/go-zero/core/fs"
)
const (

View File

@@ -213,6 +213,7 @@ func Infof(format string, v ...interface{}) {
func Must(err error) {
if err != nil {
msg := formatWithCaller(err.Error(), 3)
log.Print(msg)
output(severeLog, levelFatal, msg)
os.Exit(1)
}

View File

@@ -195,6 +195,13 @@ ts需要指定webapi所在目录
goctl api dart -api user/user.api -dir ./src
```
## 根据mysql ddl或者datasource生成model文件
```shell script
$ goctl model mysql -src={filename} -dir={dir} -cache={true|false}
```
详情参考[model文档](https://github.com/tal-tech/go-zero/blob/master/tools/goctl/model/sql/README.MD)
## 根据定义好的简单go文件生成mongo代码文件(仅限golang使用)
```shell
goctl model mongo -src {{yourDir}}/xiao/service/xhb/user/model/usermodel.go -cache yes
@@ -218,7 +225,7 @@ type User struct {
o是改字段需要生产的操作函数 可以取得get,find,set 分别表示生成返回单个对象的查询方法,返回多个对象的查询方法,设置该字段方法
生成的目标文件会覆盖该简单go文件
## goctl rpc生成
## goctl rpc生成(业务剥离中,暂未开放)
命令 `goctl rpc proto -proto ${proto} -service ${serviceName} -project ${projectName} -dir ${directory} -shared ${shared}`
如: `goctl rpc proto -proto test.proto -service test -project xjy -dir .`

BIN
doc/images/architecture.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 333 KiB

View File

@@ -10,23 +10,28 @@
## 2. 关键词替换
支持关键词重叠,自动选用最长的关键词,代码示例如下:
```go
replacer := stringx.NewReplacer(map[string]string{
"PHP": "PPT",
"世界上": "吹牛",
"日本": "法国",
"日本的首都": "东京",
"东京": "日本的首都",
})
fmt.Println(replacer.Replace("PHP是世界上最好的语言"))
fmt.Println(replacer.Replace("日本的首都是东京"))
```
可以得到:
```
PPT是吹牛最好的语言
东京是日本的首都
```
示例代码见`example/stringx/replace/replace.go`
## 3. 查找敏感词
代码示例如下:
```go
filter := stringx.NewTrie([]string{
"AV演员",
@@ -47,6 +52,8 @@ fmt.Println(keywords)
## 4. 敏感词过滤
代码示例如下:
```go
filter := stringx.NewTrie([]string{
"AV演员",

View File

@@ -8,8 +8,9 @@ import (
func main() {
replacer := stringx.NewReplacer(map[string]string{
"PHP": "PPT",
"世界上": "吹牛",
"日本": "法国",
"日本的首都": "东京",
"东京": "日本的首都",
})
fmt.Println(replacer.Replace("PHP是世界上最好的语言"))
fmt.Println(replacer.Replace("日本的首都是东京"))
}

View File

@@ -6,6 +6,23 @@
[![Release](https://img.shields.io/github/v/release/tal-tech/go-zero.svg?style=flat-square)](https://github.com/tal-tech/go-zero)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
## 0. go-zero介绍
go-zero是一个集成了各种工程实践的web和rpc框架。通过弹性设计保障了大并发服务端的稳定性经受了充分的实战检验。
go-zero 包含极简的 API 定义和生成工具 goctl可以根据定义的 api 文件一键生成 Go, iOS, Android, Kotlin, Dart, TypeScript, JavaScript 代码,并可直接运行。
使用go-zero的好处
* 轻松获得支撑千万日活服务的稳定性
* 内建级联超时控制、限流、自适应熔断、自适应降载等微服务治理能力,无需配置和额外代码
* 微服务治理中间件可无缝集成到其它现有框架使用
* 极简的API描述一键生成各端代码
* 自动校验客户端请求参数合法性
* 大量微服务治理和并发工具包
<img src="doc/images/architecture.png" alt="架构图" width="1500" />
## 1. go-zero框架背景
18年初晓黑板后端在经过频繁的宕机后决定从`Java+MongoDB`的单体架构迁移到微服务架构,经过仔细思考和对比,我们决定:
@@ -57,33 +74,20 @@ go-zero是一个集成了各种工程实践的包含web和rpc框架有如下
![弹性设计](doc/images/resilience.jpg)
## 4. go-zero框架收益
* 保障大并发服务端的稳定性,经受了充分的实战检验
* 极简的API定义
* 一键生成Go, iOS, Android, Dart, TypeScript, JavaScript代码并可直接运行
* 服务端自动校验参数合法性
## 5. go-zero近期开发计划
## 4. go-zero近期开发计划
* 自动生成API mock server便于客户端开发
* 自动生成服务端功能测试
## 6. Installation
## 5. Installation
1. 在项目目录下通过如下命令安装:
在项目目录下通过如下命令安装:
```shell
go get -u github.com/tal-tech/go-zero
```
```shell
go get -u github.com/tal-tech/go-zero
```
2. 代码里导入go-zero
```go
import "github.com/tal-tech/go-zero"
```
## 7. Quick Start
## 6. Quick Start
1. 编译goctl工具
@@ -97,7 +101,7 @@ go-zero是一个集成了各种工程实践的包含web和rpc框架有如下
```go
type Request struct {
Name string `path:"name"`
Name string `path:"name,options=you|me"` // 框架自动验证请求参数是否合法
}
type Response struct {
@@ -173,17 +177,17 @@ go-zero是一个集成了各种工程实践的包含web和rpc框架有如下
...
```
## 8. Benchmark
## 7. Benchmark
![benchmark](doc/images/benchmark.png)
[测试代码见这里](https://github.com/smallnest/go-web-framework-benchmark)
## 9. 文档
## 8. 文档 (逐步完善中)
* [goctl使用帮助](doc/goctl.md)
* [关键字替换和敏感词过滤工具](doc/keywords.md)
## 10. 微信交流群
## 9. 微信交流群
添加我的微信kevwan请注明go-zero我拉进go-zero社区群🤝

View File

@@ -209,6 +209,6 @@ func (s *engine) use(middleware Middleware) {
func convertMiddleware(ware Middleware) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(ware(next.ServeHTTP))
return ware(next.ServeHTTP)
}
}

View File

@@ -1,6 +1,7 @@
package httpx
import (
"errors"
"net/http"
"strings"
"testing"
@@ -17,6 +18,24 @@ func init() {
logx.Disable()
}
func TestError(t *testing.T) {
const body = "foo"
w := tracedResponseWriter{
headers: make(map[string][]string),
}
Error(&w, errors.New(body))
assert.Equal(t, http.StatusBadRequest, w.code)
assert.Equal(t, body, strings.TrimSpace(w.builder.String()))
}
func TestOk(t *testing.T) {
w := tracedResponseWriter{
headers: make(map[string][]string),
}
Ok(&w)
assert.Equal(t, http.StatusOK, w.code)
}
func TestOkJson(t *testing.T) {
w := tracedResponseWriter{
headers: make(map[string][]string),

View File

@@ -0,0 +1,62 @@
package auth
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"google.golang.org/grpc/metadata"
)
func TestParseCredential(t *testing.T) {
tests := []struct {
name string
withNil bool
withEmptyMd bool
app string
token string
}{
{
name: "nil",
withNil: true,
},
{
name: "empty md",
withEmptyMd: true,
},
{
name: "empty",
},
{
name: "valid",
app: "foo",
token: "bar",
},
}
for _, test := range tests {
test := test
t.Run(test.name, func(t *testing.T) {
t.Parallel()
var ctx context.Context
if test.withNil {
ctx = context.Background()
} else if test.withEmptyMd {
ctx = metadata.NewIncomingContext(context.Background(), metadata.MD{})
} else {
md := metadata.New(map[string]string{
"app": test.app,
"token": test.token,
})
ctx = metadata.NewIncomingContext(context.Background(), md)
}
cred := ParseCredential(ctx)
assert.False(t, cred.RequireTransportSecurity())
m, err := cred.GetRequestMetadata(context.Background())
assert.Nil(t, err)
assert.Equal(t, test.app, m[appKey])
assert.Equal(t, test.token, m[tokenKey])
})
}
}

View File

@@ -3,7 +3,9 @@ package p2c
import (
"context"
"fmt"
"runtime"
"strconv"
"sync"
"testing"
"github.com/stretchr/testify/assert"
@@ -33,19 +35,31 @@ func TestP2cPicker_Pick(t *testing.T) {
tests := []struct {
name string
candidates int
threshold float64
}{
{
name: "single",
candidates: 1,
threshold: 0.9,
},
{
name: "two",
candidates: 2,
threshold: 0.5,
},
{
name: "multiple",
candidates: 100,
threshold: 0.95,
},
}
for _, test := range tests {
test := test
t.Run(test.name, func(t *testing.T) {
t.Parallel()
const total = 10000
builder := new(p2cPickerBuilder)
ready := make(map[resolver.Address]balancer.SubConn)
for i := 0; i < test.candidates; i++ {
@@ -55,7 +69,9 @@ func TestP2cPicker_Pick(t *testing.T) {
}
picker := builder.Build(ready)
for i := 0; i < 10000; i++ {
var wg sync.WaitGroup
wg.Add(total)
for i := 0; i < total; i++ {
_, done, err := picker.Pick(context.Background(), balancer.PickInfo{
FullMethodName: "/",
Ctx: context.Background(),
@@ -64,11 +80,16 @@ func TestP2cPicker_Pick(t *testing.T) {
if i%100 == 0 {
err = status.Error(codes.DeadlineExceeded, "deadline")
}
done(balancer.DoneInfo{
Err: err,
})
go func() {
runtime.Gosched()
done(balancer.DoneInfo{
Err: err,
})
wg.Done()
}()
}
wg.Wait()
dist := make(map[interface{}]int)
conns := picker.(*p2cPicker).conns
for _, conn := range conns {
@@ -76,7 +97,8 @@ func TestP2cPicker_Pick(t *testing.T) {
}
entropy := mathx.CalcEntropy(dist)
assert.True(t, entropy > .95, fmt.Sprintf("entropy is %f, less than .95", entropy))
assert.True(t, entropy > test.threshold, fmt.Sprintf("entropy is %f, less than %f",
entropy, test.threshold))
})
}
}

View File

@@ -0,0 +1,47 @@
package internal
import (
"context"
"sync/atomic"
"testing"
"github.com/stretchr/testify/assert"
"google.golang.org/grpc"
)
func TestWithStreamClientInterceptors(t *testing.T) {
opts := WithStreamClientInterceptors()
assert.NotNil(t, opts)
}
func TestWithUnaryClientInterceptors(t *testing.T) {
opts := WithUnaryClientInterceptors()
assert.NotNil(t, opts)
}
func TestChainStreamClientInterceptors_zero(t *testing.T) {
interceptors := chainStreamClientInterceptors()
_, err := interceptors(context.Background(), nil, new(grpc.ClientConn), "/foo",
func(ctx context.Context, desc *grpc.StreamDesc, cc *grpc.ClientConn, method string,
opts ...grpc.CallOption) (grpc.ClientStream, error) {
return nil, nil
})
assert.Nil(t, err)
}
func TestChainStreamClientInterceptors_one(t *testing.T) {
var called int32
interceptors := chainStreamClientInterceptors(func(ctx context.Context, desc *grpc.StreamDesc,
cc *grpc.ClientConn, method string, streamer grpc.Streamer, opts ...grpc.CallOption) (
grpc.ClientStream, error) {
atomic.AddInt32(&called, 1)
return nil, nil
})
_, err := interceptors(context.Background(), nil, new(grpc.ClientConn), "/foo",
func(ctx context.Context, desc *grpc.StreamDesc, cc *grpc.ClientConn, method string,
opts ...grpc.CallOption) (grpc.ClientStream, error) {
return nil, nil
})
assert.Nil(t, err)
assert.Equal(t, int32(1), atomic.LoadInt32(&called))
}

View File

@@ -1,12 +1,15 @@
package clientinterceptors
import (
"context"
"errors"
"testing"
"github.com/stretchr/testify/assert"
"github.com/tal-tech/go-zero/core/breaker"
"github.com/tal-tech/go-zero/core/stat"
rcodes "github.com/tal-tech/go-zero/rpcx/internal/codes"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
@@ -49,3 +52,30 @@ func TestBreakerInterceptorDeadlineExceeded(t *testing.T) {
assert.True(t, errs[err] > 0)
assert.True(t, errs[breaker.ErrServiceUnavailable] > 0)
}
func TestBreakerInterceptor(t *testing.T) {
tests := []struct {
name string
err error
}{
{
name: "nil",
err: nil,
},
{
name: "with error",
err: errors.New("mock"),
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
cc := new(grpc.ClientConn)
err := BreakerInterceptor(context.Background(), "/foo", nil, nil, cc,
func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn,
opts ...grpc.CallOption) error {
return test.err
})
assert.Equal(t, test.err, err)
})
}
}

View File

@@ -0,0 +1,37 @@
package clientinterceptors
import (
"context"
"errors"
"testing"
"github.com/stretchr/testify/assert"
"google.golang.org/grpc"
)
func TestDurationInterceptor(t *testing.T) {
tests := []struct {
name string
err error
}{
{
name: "nil",
err: nil,
},
{
name: "with error",
err: errors.New("mock"),
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
cc := new(grpc.ClientConn)
err := DurationInterceptor(context.Background(), "/foo", nil, nil, cc,
func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn,
opts ...grpc.CallOption) error {
return test.err
})
assert.Equal(t, test.err, err)
})
}
}

View File

@@ -0,0 +1,37 @@
package clientinterceptors
import (
"context"
"errors"
"testing"
"github.com/stretchr/testify/assert"
"google.golang.org/grpc"
)
func TestPromMetricInterceptor(t *testing.T) {
tests := []struct {
name string
err error
}{
{
name: "nil",
err: nil,
},
{
name: "with error",
err: errors.New("mock"),
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
cc := new(grpc.ClientConn)
err := PromMetricInterceptor(context.Background(), "/foo", nil, nil, cc,
func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn,
opts ...grpc.CallOption) error {
return test.err
})
assert.Equal(t, test.err, err)
})
}
}

View File

@@ -0,0 +1,50 @@
package clientinterceptors
import (
"context"
"strconv"
"sync"
"testing"
"time"
"github.com/stretchr/testify/assert"
"google.golang.org/grpc"
)
func TestTimeoutInterceptor(t *testing.T) {
timeouts := []time.Duration{0, time.Millisecond * 10}
for _, timeout := range timeouts {
t.Run(strconv.FormatInt(int64(timeout), 10), func(t *testing.T) {
interceptor := TimeoutInterceptor(timeout)
cc := new(grpc.ClientConn)
err := interceptor(context.Background(), "/foo", nil, nil, cc,
func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn,
opts ...grpc.CallOption) error {
return nil
},
)
assert.Nil(t, err)
})
}
}
func TestTimeoutInterceptor_timeout(t *testing.T) {
const timeout = time.Millisecond * 10
interceptor := TimeoutInterceptor(timeout)
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
defer cancel()
var wg sync.WaitGroup
wg.Add(1)
cc := new(grpc.ClientConn)
err := interceptor(ctx, "/foo", nil, nil, cc,
func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn,
opts ...grpc.CallOption) error {
defer wg.Done()
tm, ok := ctx.Deadline()
assert.True(t, ok)
assert.True(t, tm.Before(time.Now().Add(timeout+time.Millisecond)))
return nil
})
wg.Wait()
assert.Nil(t, err)
}

View File

@@ -0,0 +1,53 @@
package clientinterceptors
import (
"context"
"sync"
"sync/atomic"
"testing"
"github.com/stretchr/testify/assert"
"github.com/tal-tech/go-zero/core/trace"
"google.golang.org/grpc"
"google.golang.org/grpc/metadata"
)
func TestTracingInterceptor(t *testing.T) {
var run int32
var wg sync.WaitGroup
wg.Add(1)
cc := new(grpc.ClientConn)
err := TracingInterceptor(context.Background(), "/foo", nil, nil, cc,
func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn,
opts ...grpc.CallOption) error {
defer wg.Done()
atomic.AddInt32(&run, 1)
return nil
})
wg.Wait()
assert.Nil(t, err)
assert.Equal(t, int32(1), atomic.LoadInt32(&run))
}
func TestTracingInterceptor_GrpcFormat(t *testing.T) {
var run int32
var wg sync.WaitGroup
wg.Add(1)
md := metadata.New(map[string]string{
"foo": "bar",
})
carrier, err := trace.Inject(trace.GrpcFormat, md)
assert.Nil(t, err)
ctx, _ := trace.StartServerSpan(context.Background(), carrier, "user", "/foo")
cc := new(grpc.ClientConn)
err = TracingInterceptor(ctx, "/foo", nil, nil, cc,
func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn,
opts ...grpc.CallOption) error {
defer wg.Done()
atomic.AddInt32(&run, 1)
return nil
})
wg.Wait()
assert.Nil(t, err)
assert.Equal(t, int32(1), atomic.LoadInt32(&run))
}

View File

@@ -11,7 +11,9 @@ type directBuilder struct{}
func (d *directBuilder) Build(target resolver.Target, cc resolver.ClientConn, opts resolver.BuildOptions) (
resolver.Resolver, error) {
var addrs []resolver.Address
endpoints := strings.Split(target.Endpoint, EndpointSep)
endpoints := strings.FieldsFunc(target.Endpoint, func(r rune) bool {
return r == EndpointSepChar
})
for _, val := range subset(endpoints, subsetSize) {
addrs = append(addrs, resolver.Address{

View File

@@ -0,0 +1,52 @@
package resolver
import (
"fmt"
"strconv"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/tal-tech/go-zero/core/lang"
"github.com/tal-tech/go-zero/core/mathx"
"google.golang.org/grpc/resolver"
)
func TestDirectBuilder_Build(t *testing.T) {
tests := []int{
0,
1,
2,
subsetSize / 2,
subsetSize,
subsetSize * 2,
}
for _, test := range tests {
t.Run(strconv.Itoa(test), func(t *testing.T) {
var servers []string
for i := 0; i < test; i++ {
servers = append(servers, fmt.Sprintf("localhost:%d", i))
}
var b directBuilder
cc := new(mockedClientConn)
_, err := b.Build(resolver.Target{
Scheme: DirectScheme,
Endpoint: strings.Join(servers, ","),
}, cc, resolver.BuildOptions{})
assert.Nil(t, err)
size := mathx.MinInt(test, subsetSize)
assert.Equal(t, size, len(cc.state.Addresses))
m := make(map[string]lang.PlaceholderType)
for _, each := range cc.state.Addresses {
m[each.Addr] = lang.Placeholder
}
assert.Equal(t, size, len(m))
})
}
}
func TestDirectBuilder_Scheme(t *testing.T) {
var b directBuilder
assert.Equal(t, DirectScheme, b.Scheme())
}

View File

@@ -11,7 +11,9 @@ type discovBuilder struct{}
func (d *discovBuilder) Build(target resolver.Target, cc resolver.ClientConn, opts resolver.BuildOptions) (
resolver.Resolver, error) {
hosts := strings.Split(target.Authority, EndpointSep)
hosts := strings.FieldsFunc(target.Authority, func(r rune) bool {
return r == EndpointSepChar
})
sub, err := discov.NewSubscriber(hosts, target.Endpoint)
if err != nil {
return nil, err

View File

@@ -1,17 +1,22 @@
package resolver
import "google.golang.org/grpc/resolver"
import (
"fmt"
"google.golang.org/grpc/resolver"
)
const (
DirectScheme = "direct"
DiscovScheme = "discov"
EndpointSep = ","
subsetSize = 32
DirectScheme = "direct"
DiscovScheme = "discov"
EndpointSepChar = ','
subsetSize = 32
)
var (
dirBuilder directBuilder
disBuilder discovBuilder
EndpointSep = fmt.Sprintf("%c", EndpointSepChar)
dirBuilder directBuilder
disBuilder discovBuilder
)
func RegisterResolver() {

View File

@@ -0,0 +1,36 @@
package resolver
import (
"testing"
"google.golang.org/grpc/resolver"
"google.golang.org/grpc/serviceconfig"
)
func TestNopResolver(t *testing.T) {
// make sure ResolveNow & Close don't panic
var r nopResolver
r.ResolveNow(resolver.ResolveNowOptions{})
r.Close()
}
type mockedClientConn struct {
state resolver.State
}
func (m *mockedClientConn) UpdateState(state resolver.State) {
m.state = state
}
func (m *mockedClientConn) ReportError(err error) {
}
func (m *mockedClientConn) NewAddress(addresses []resolver.Address) {
}
func (m *mockedClientConn) NewServiceConfig(serviceConfig string) {
}
func (m *mockedClientConn) ParseServiceConfig(serviceConfigJSON string) *serviceconfig.ParseResult {
return nil
}

View File

@@ -0,0 +1,200 @@
package serverinterceptors
import (
"context"
"testing"
"github.com/alicebob/miniredis"
"github.com/stretchr/testify/assert"
"github.com/tal-tech/go-zero/core/stores/redis"
"github.com/tal-tech/go-zero/rpcx/internal/auth"
"google.golang.org/grpc"
"google.golang.org/grpc/metadata"
)
func TestStreamAuthorizeInterceptor(t *testing.T) {
tests := []struct {
name string
app string
token string
strict bool
hasError bool
}{
{
name: "strict=false",
strict: false,
hasError: false,
},
{
name: "strict=true",
strict: true,
hasError: true,
},
{
name: "strict=true,with token",
app: "foo",
token: "bar",
strict: true,
hasError: false,
},
{
name: "strict=true,with error token",
app: "foo",
token: "error",
strict: true,
hasError: true,
},
}
r := miniredis.NewMiniRedis()
assert.Nil(t, r.Start())
defer r.Close()
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
store := redis.NewRedis(r.Addr(), redis.NodeType)
if len(test.app) > 0 {
assert.Nil(t, store.Hset("apps", test.app, test.token))
defer store.Hdel("apps", test.app)
}
authenticator, err := auth.NewAuthenticator(store, "apps", test.strict)
assert.Nil(t, err)
interceptor := StreamAuthorizeInterceptor(authenticator)
md := metadata.New(map[string]string{
"app": "foo",
"token": "bar",
})
ctx := metadata.NewIncomingContext(context.Background(), md)
stream := mockedStream{ctx: ctx}
err = interceptor(nil, stream, nil, func(srv interface{}, stream grpc.ServerStream) error {
return nil
})
if test.hasError {
assert.NotNil(t, err)
} else {
assert.Nil(t, err)
}
})
}
}
func TestUnaryAuthorizeInterceptor(t *testing.T) {
tests := []struct {
name string
app string
token string
strict bool
hasError bool
}{
{
name: "strict=false",
strict: false,
hasError: false,
},
{
name: "strict=true",
strict: true,
hasError: true,
},
{
name: "strict=true,with token",
app: "foo",
token: "bar",
strict: true,
hasError: false,
},
{
name: "strict=true,with error token",
app: "foo",
token: "error",
strict: true,
hasError: true,
},
}
r := miniredis.NewMiniRedis()
assert.Nil(t, r.Start())
defer r.Close()
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
store := redis.NewRedis(r.Addr(), redis.NodeType)
if len(test.app) > 0 {
assert.Nil(t, store.Hset("apps", test.app, test.token))
defer store.Hdel("apps", test.app)
}
authenticator, err := auth.NewAuthenticator(store, "apps", test.strict)
assert.Nil(t, err)
interceptor := UnaryAuthorizeInterceptor(authenticator)
md := metadata.New(map[string]string{
"app": "foo",
"token": "bar",
})
ctx := metadata.NewIncomingContext(context.Background(), md)
_, err = interceptor(ctx, nil, nil,
func(ctx context.Context, req interface{}) (interface{}, error) {
return nil, nil
})
if test.hasError {
assert.NotNil(t, err)
} else {
assert.Nil(t, err)
}
if test.strict {
_, err = interceptor(context.Background(), nil, nil,
func(ctx context.Context, req interface{}) (interface{}, error) {
return nil, nil
})
assert.NotNil(t, err)
var md metadata.MD
ctx := metadata.NewIncomingContext(context.Background(), md)
_, err = interceptor(ctx, nil, nil,
func(ctx context.Context, req interface{}) (interface{}, error) {
return nil, nil
})
assert.NotNil(t, err)
md = metadata.New(map[string]string{
"app": "",
"token": "",
})
ctx = metadata.NewIncomingContext(context.Background(), md)
_, err = interceptor(ctx, nil, nil,
func(ctx context.Context, req interface{}) (interface{}, error) {
return nil, nil
})
assert.NotNil(t, err)
}
})
}
}
type mockedStream struct {
ctx context.Context
}
func (m mockedStream) SetHeader(md metadata.MD) error {
return nil
}
func (m mockedStream) SendHeader(md metadata.MD) error {
return nil
}
func (m mockedStream) SetTrailer(md metadata.MD) {
}
func (m mockedStream) Context() context.Context {
return m.ctx
}
func (m mockedStream) SendMsg(v interface{}) error {
return nil
}
func (m mockedStream) RecvMsg(v interface{}) error {
return nil
}

View File

@@ -0,0 +1,31 @@
package serverinterceptors
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/tal-tech/go-zero/core/logx"
"google.golang.org/grpc"
)
func init() {
logx.Disable()
}
func TestStreamCrashInterceptor(t *testing.T) {
err := StreamCrashInterceptor(nil, nil, nil, func(
srv interface{}, stream grpc.ServerStream) error {
panic("mock panic")
})
assert.NotNil(t, err)
}
func TestUnaryCrashInterceptor(t *testing.T) {
interceptor := UnaryCrashInterceptor()
_, err := interceptor(context.Background(), nil, nil,
func(ctx context.Context, req interface{}) (interface{}, error) {
panic("mock panic")
})
assert.NotNil(t, err)
}

View File

@@ -33,12 +33,12 @@ var (
)
func UnaryPromMetricInterceptor() grpc.UnaryServerInterceptor {
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (
interface{}, error) {
startTime := timex.Now()
resp, err := handler(ctx, req)
metricServerReqDur.Observe(int64(timex.Since(startTime)/time.Millisecond), info.FullMethod)
metricServerReqCodeTotal.Inc(info.FullMethod, strconv.Itoa(int(status.Code(err))))
return resp, err
}
}

View File

@@ -0,0 +1,19 @@
package serverinterceptors
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"google.golang.org/grpc"
)
func TestUnaryPromMetricInterceptor(t *testing.T) {
interceptor := UnaryPromMetricInterceptor()
_, err := interceptor(context.Background(), nil, &grpc.UnaryServerInfo{
FullMethod: "/",
}, func(ctx context.Context, req interface{}) (interface{}, error) {
return nil, nil
})
assert.Nil(t, err)
}

View File

@@ -0,0 +1,77 @@
package serverinterceptors
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/tal-tech/go-zero/core/load"
"github.com/tal-tech/go-zero/core/stat"
"google.golang.org/grpc"
)
func TestUnarySheddingInterceptor(t *testing.T) {
tests := []struct {
name string
allow bool
handleErr error
expect error
}{
{
name: "allow",
allow: true,
handleErr: nil,
expect: nil,
},
{
name: "allow",
allow: true,
handleErr: context.DeadlineExceeded,
expect: context.DeadlineExceeded,
},
{
name: "reject",
allow: false,
handleErr: nil,
expect: load.ErrServiceOverloaded,
},
}
for _, test := range tests {
test := test
t.Run(test.name, func(t *testing.T) {
t.Parallel()
shedder := mockedShedder{allow: test.allow}
metrics := stat.NewMetrics("mock")
interceptor := UnarySheddingInterceptor(shedder, metrics)
_, err := interceptor(context.Background(), nil, &grpc.UnaryServerInfo{
FullMethod: "/",
}, func(ctx context.Context, req interface{}) (interface{}, error) {
return nil, test.handleErr
})
assert.Equal(t, test.expect, err)
})
}
}
type mockedShedder struct {
allow bool
}
func (m mockedShedder) Allow() (load.Promise, error) {
if m.allow {
return mockedPromise{}, nil
} else {
return nil, load.ErrServiceOverloaded
}
}
type mockedPromise struct {
}
func (m mockedPromise) Pass() {
}
func (m mockedPromise) Fail() {
}

View File

@@ -0,0 +1,32 @@
package serverinterceptors
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/tal-tech/go-zero/core/stat"
"google.golang.org/grpc"
)
func TestUnaryStatInterceptor(t *testing.T) {
metrics := stat.NewMetrics("mock")
interceptor := UnaryStatInterceptor(metrics)
_, err := interceptor(context.Background(), nil, &grpc.UnaryServerInfo{
FullMethod: "/",
}, func(ctx context.Context, req interface{}) (interface{}, error) {
return nil, nil
})
assert.Nil(t, err)
}
func TestUnaryStatInterceptor_crash(t *testing.T) {
metrics := stat.NewMetrics("mock")
interceptor := UnaryStatInterceptor(metrics)
_, err := interceptor(context.Background(), nil, &grpc.UnaryServerInfo{
FullMethod: "/",
}, func(ctx context.Context, req interface{}) (interface{}, error) {
panic("error")
})
assert.NotNil(t, err)
}

View File

@@ -0,0 +1,41 @@
package serverinterceptors
import (
"context"
"sync"
"testing"
"time"
"github.com/stretchr/testify/assert"
"google.golang.org/grpc"
)
func TestUnaryTimeoutInterceptor(t *testing.T) {
interceptor := UnaryTimeoutInterceptor(time.Millisecond * 10)
_, err := interceptor(context.Background(), nil, &grpc.UnaryServerInfo{
FullMethod: "/",
}, func(ctx context.Context, req interface{}) (interface{}, error) {
return nil, nil
})
assert.Nil(t, err)
}
func TestUnaryTimeoutInterceptor_timeout(t *testing.T) {
const timeout = time.Millisecond * 10
interceptor := UnaryTimeoutInterceptor(timeout)
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
defer cancel()
var wg sync.WaitGroup
wg.Add(1)
_, err := interceptor(ctx, nil, &grpc.UnaryServerInfo{
FullMethod: "/",
}, func(ctx context.Context, req interface{}) (interface{}, error) {
defer wg.Done()
tm, ok := ctx.Deadline()
assert.True(t, ok)
assert.True(t, tm.Before(time.Now().Add(timeout+time.Millisecond)))
return nil, nil
})
wg.Wait()
assert.Nil(t, err)
}

View File

@@ -0,0 +1,48 @@
package serverinterceptors
import (
"context"
"sync"
"sync/atomic"
"testing"
"github.com/stretchr/testify/assert"
"github.com/tal-tech/go-zero/core/trace/tracespec"
"google.golang.org/grpc"
"google.golang.org/grpc/metadata"
)
func TestUnaryTracingInterceptor(t *testing.T) {
interceptor := UnaryTracingInterceptor("foo")
var run int32
var wg sync.WaitGroup
wg.Add(1)
_, err := interceptor(context.Background(), nil, &grpc.UnaryServerInfo{
FullMethod: "/",
}, func(ctx context.Context, req interface{}) (interface{}, error) {
defer wg.Done()
atomic.AddInt32(&run, 1)
return nil, nil
})
wg.Wait()
assert.Nil(t, err)
assert.Equal(t, int32(1), atomic.LoadInt32(&run))
}
func TestUnaryTracingInterceptor_GrpcFormat(t *testing.T) {
interceptor := UnaryTracingInterceptor("foo")
var wg sync.WaitGroup
wg.Add(1)
var md metadata.MD
ctx := metadata.NewIncomingContext(context.Background(), md)
_, err := interceptor(ctx, nil, &grpc.UnaryServerInfo{
FullMethod: "/",
}, func(ctx context.Context, req interface{}) (interface{}, error) {
defer wg.Done()
assert.True(t, len(ctx.Value(tracespec.TracingKey).(tracespec.Trace).TraceId()) > 0)
assert.True(t, len(ctx.Value(tracespec.TracingKey).(tracespec.Trace).SpanId()) > 0)
return nil, nil
})
wg.Wait()
assert.Nil(t, err)
}

View File

@@ -8,7 +8,8 @@ import (
)
func BuildDirectTarget(endpoints []string) string {
return fmt.Sprintf("%s:///%s", resolver.DirectScheme, strings.Join(endpoints, resolver.EndpointSep))
return fmt.Sprintf("%s:///%s", resolver.DirectScheme,
strings.Join(endpoints, resolver.EndpointSep))
}
func BuildDiscovTarget(endpoints []string, key string) string {

View File

@@ -0,0 +1,17 @@
package internal
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestBuildDirectTarget(t *testing.T) {
target := BuildDirectTarget([]string{"localhost:123", "localhost:456"})
assert.Equal(t, "direct:///localhost:123,localhost:456", target)
}
func TestBuildDiscovTarget(t *testing.T) {
target := BuildDiscovTarget([]string{"localhost:123", "localhost:456"}, "foo")
assert.Equal(t, "discov://localhost:123,localhost:456/foo", target)
}

View File

@@ -19,29 +19,24 @@ const apiTemplate = `info(
email: {{.gitEmail}}
)
type request struct{
type request struct {
// TODO: add members here and delete this comment
}
type response struct{
type response struct {
// TODO: add members here and delete this comment
}
@server(
port: // TODO: add port here and delete this comment
)
service {{.serviceName}} {
@server(
handler: // TODO: set handler name and delete this comment
)
// TODO: edit the below line
// get /users/id/:userId(request) returns(response)
get /users/id/:userId(request) returns(response)
@server(
handler: // TODO: set handler name and delete this comment
)
// TODO: edit the below line
// post /users/create(request)
post /users/create(request)
}
`

View File

@@ -191,25 +191,64 @@ var (
{
Name: "model",
Usage: "generate model code",
Flags: []cli.Flag{
cli.StringFlag{
Name: "src, s",
Usage: "the file path of the ddl source file",
},
cli.StringFlag{
Name: "dir, d",
Usage: "the target dir",
},
cli.BoolFlag{
Name: "cache, c",
Usage: "generate code with cache [optional]",
},
cli.BoolFlag{
Name: "idea",
Usage: "for idea plugin [optional]",
Subcommands: []cli.Command{
{
Name: "mysql",
Usage: `generate mysql model"`,
Subcommands: []cli.Command{
{
Name: "ddl",
Usage: `generate mysql model from ddl"`,
Flags: []cli.Flag{
cli.StringFlag{
Name: "src, s",
Usage: "the file path of the ddl source file",
},
cli.StringFlag{
Name: "dir, d",
Usage: "the target dir",
},
cli.BoolFlag{
Name: "cache, c",
Usage: "generate code with cache [optional]",
},
cli.BoolFlag{
Name: "idea",
Usage: "for idea plugin [optional]",
},
},
Action: command.MysqlDDL,
},
{
Name: "datasource",
Usage: `generate model from datasource"`,
Flags: []cli.Flag{
cli.StringFlag{
Name: "url",
Usage: `the data source of database,like "root:password@tcp(127.0.0.1:3306)/database"`,
},
cli.StringFlag{
Name: "table, t",
Usage: `source table,tables separated by commas,like "user,course"`,
},
cli.BoolFlag{
Name: "cache, c",
Usage: "generate code with cache [optional]",
},
cli.StringFlag{
Name: "dir, d",
Usage: "the target dir",
},
cli.BoolFlag{
Name: "idea",
Usage: "for idea plugin [optional]",
},
},
Action: command.MyDataSource,
},
},
},
},
Action: command.Mysql,
},
{
Name: "config",

View File

@@ -0,0 +1,10 @@
# Change log
# 2020-08-20
* 新增支持通过连接数据库生成model
* 支持数据库多表生成
* 优化stringx
# 2020-08-19
* 重构model代码生成逻辑
* 实现从ddl解析表信息生成代码

View File

@@ -4,21 +4,28 @@ goctl model 为go-zero下的工具模块中的组件之一目前支持识别m
# 快速开始
```
$ goctl model -src ./sql/user.sql -dir ./model -c true
```
* 通过ddl生成
详情用法请参考[example](https://github.com/tal-tech/go-zero/tools/goctl/model/sql/example)
```shell script
$ goctl model mysql ddl -src="./sql/user.sql" -dir="./sql/model" -c=true
```
执行上述命令后即可快速生成CURD代码。
执行上述命令后即可快速生成CURD代码。
```
model
│   ├── error.go
│   └── usermodel.go
```
```
model
│   ├── error.go
│   └── usermodel.go
```
* 通过datasource生成
```shell script
$ goctl model mysql datasource -url="user:password@tcp(127.0.0.1:3306)/database" -table="table1,table2" -dir="./model"
```
> 详情用法请参考[example](https://github.com/tal-tech/go-zero/tree/master/tools/goctl/model/sql/example)
> 注意这里的目录结构中有usercoursemodel.go目录在example中我为了体现带cache与不带cache代码的区别因此将sql文件分别使用了独立的sql文件(user.sql&course.sql)在实际项目开发中你可以将ddl建表语句放在一个sql文件中`goctl model`会自动解析并分割最终按照每个ddl建表语句为单位生成独立的go文件。
* 生成代码示例
@@ -174,22 +181,22 @@ model
# 用法
```
$ goctl model -h
$ goctl model mysql -h
```
```
NAME:
goctl model - generate model code
goctl model mysql - generate mysql model"
USAGE:
goctl model [command options] [arguments...]
goctl model mysql command [command options] [arguments...]
COMMANDS:
ddl generate mysql model from ddl"
datasource generate model from datasource"
OPTIONS:
--src value, -s value the file path of the ddl source file
--dir value, -d value the target dir
--cache, -c generate code with cache [optional]
--idea for idea plugin [optional]
--help, -h show help
```
# 生成规则
@@ -198,22 +205,43 @@ OPTIONS:
我们默认用户在建表时会创建createTime、updateTime字段(忽略大小写、下划线命名风格)且默认值均为`CURRENT_TIMESTAMP`而updateTime支持`ON UPDATE CURRENT_TIMESTAMP`,对于这两个字段生成`insert`、`update`时会被移除,不在赋值范畴内,当然,如果你不需要这两个字段那也无大碍。
* 带缓存模式
```
$ goctl model -src {filename} -dir {dir} -cache true
```
* ddl
```shell script
$ goctl model mysql -src={filename} -dir={dir} -cache=true
```
* datasource
```shell script
$ goctl model mysql datasource -url={datasource} -table={tables} -dir={dir} -cache=true
```
目前仅支持redis缓存如果选择带缓存模式即生成的`FindOne(ByXxx)`&`Delete`代码会生成带缓存逻辑的代码目前仅支持单索引字段除全文索引外对于联合索引我们默认认为不需要带缓存且不属于通用型代码因此没有放在代码生成行列如example中user表中的`id`、`name`、`mobile`字段均属于单字段索引。
* 不带缓存模式
```
$ goctl model -src {filename} -dir {dir}
```
* ddl
```shell script
$ goctl model -src={filename} -dir={dir}
```
* datasource
```shell script
$ goctl model mysql datasource -url={datasource} -table={tables} -dir={dir}
```
or
```
$ goctl model -src {filename} -dir {dir} -cache false
```
* ddl
```shell script
$ goctl model -src={filename} -dir={dir} -cache=false
```
* datasource
```shell script
$ goctl model mysql datasource -url={datasource} -table={tables} -dir={dir} -cache=false
```
生成代码仅基本的CURD结构。
# 缓存
@@ -238,10 +266,6 @@ OPTIONS:
# QA
* goctl model支持根据数据库连接后选择表生成代码吗
目前暂时不支持,在后面会向这个方向扩展。
* goctl model除了命令行模式支持插件模式吗
很快支持idea插件。

View File

@@ -1,24 +1,84 @@
package command
import (
"io/ioutil"
"path/filepath"
"strings"
"github.com/tal-tech/go-zero/core/collection"
"github.com/tal-tech/go-zero/core/logx"
"github.com/tal-tech/go-zero/core/stores/sqlx"
"github.com/tal-tech/go-zero/tools/goctl/model/sql/gen"
"github.com/tal-tech/go-zero/tools/goctl/model/sql/model"
"github.com/tal-tech/go-zero/tools/goctl/util/console"
"github.com/urfave/cli"
)
func Mysql(ctx *cli.Context) error {
src := ctx.String("src")
dir := ctx.String("dir")
cache := ctx.Bool("cache")
idea := ctx.Bool("idea")
var log console.Console
if idea {
log = console.NewIdeaConsole()
} else {
log = console.NewColorConsole()
const (
flagSrc = "src"
flagDir = "dir"
flagCache = "cache"
flagIdea = "idea"
flagUrl = "url"
flagTable = "table"
)
func MysqlDDL(ctx *cli.Context) error {
src := ctx.String(flagSrc)
dir := ctx.String(flagDir)
cache := ctx.Bool(flagCache)
idea := ctx.Bool(flagIdea)
log := console.NewConsole(idea)
fileSrc, err := filepath.Abs(src)
if err != nil {
return err
}
generator := gen.NewDefaultGenerator(src, dir, gen.WithConsoleOption(log))
err := generator.Start(cache)
data, err := ioutil.ReadFile(fileSrc)
if err != nil {
return err
}
source := string(data)
generator := gen.NewDefaultGenerator(source, dir, gen.WithConsoleOption(log))
err = generator.Start(cache)
if err != nil {
log.Error("%v", err)
}
return nil
}
func MyDataSource(ctx *cli.Context) error {
url := strings.TrimSpace(ctx.String(flagUrl))
dir := strings.TrimSpace(ctx.String(flagDir))
cache := ctx.Bool(flagCache)
idea := ctx.Bool(flagIdea)
table := strings.TrimSpace(ctx.String(flagTable))
log := console.NewConsole(idea)
if len(url) == 0 {
log.Error("%v", "expected data source of mysql, but is empty")
return nil
}
if len(table) == 0 {
log.Error("%v", "expected table(s), but nothing found")
return nil
}
logx.Disable()
conn := sqlx.NewMysql(url)
m := model.NewDDLModel(conn)
tables := collection.NewSet()
for _, item := range strings.Split(table, ",") {
item = strings.TrimSpace(item)
if len(item) == 0 {
continue
}
tables.AddStr(item)
}
ddl, err := m.ShowDDL(tables.KeysStr()...)
if err != nil {
log.Error("%v", err)
return nil
}
generator := gen.NewDefaultGenerator(strings.Join(ddl, "\n"), dir, gen.WithConsoleOption(log))
err = generator.Start(cache)
if err != nil {
log.Error("%v", err)
}

View File

@@ -1,4 +1,7 @@
#!/bin/bash
# generate usermodel with cache
goctl model -src ./sql/user.sql -dir ./model -c true
# generate model with cache from ddl
goctl model mysql ddl -src="./sql/user.sql" -dir="./sql/model" -c=true
# generate model with cache from data source
goctl model mysql datasource -url="user:password@tcp(127.0.0.1:3306)/database" -table="table1,table2" -dir="./model"

View File

@@ -27,14 +27,14 @@ func genDelete(table Table, withCache bool) (string, error) {
break
}
}
camel := table.Name.Snake2Camel()
camel := table.Name.ToCamel()
output, err := templatex.With("delete").
Parse(template.Delete).
Execute(map[string]interface{}{
"upperStartCamelObject": camel,
"withCache": withCache,
"containsIndexCache": containsIndexCache,
"lowerStartCamelPrimaryKey": stringx.From(table.PrimaryKey.Name.Snake2Camel()).LowerStart(),
"lowerStartCamelPrimaryKey": stringx.From(table.PrimaryKey.Name.ToCamel()).UnTitle(),
"dataType": table.PrimaryKey.DataType,
"keys": strings.Join(keySet.KeysStr(), "\n"),
"originalPrimaryKey": table.PrimaryKey.Name.Source(),

View File

@@ -28,7 +28,7 @@ func genField(field parser.Field) (string, error) {
output, err := templatex.With("types").
Parse(template.Field).
Execute(map[string]interface{}{
"name": field.Name.Snake2Camel(),
"name": field.Name.ToCamel(),
"type": field.DataType,
"tag": tag,
"hasComment": field.Comment != "",

View File

@@ -7,15 +7,15 @@ import (
)
func genFindOne(table Table, withCache bool) (string, error) {
camel := table.Name.Snake2Camel()
camel := table.Name.ToCamel()
output, err := templatex.With("findOne").
Parse(template.FindOne).
Execute(map[string]interface{}{
"withCache": withCache,
"upperStartCamelObject": camel,
"lowerStartCamelObject": stringx.From(camel).LowerStart(),
"lowerStartCamelObject": stringx.From(camel).UnTitle(),
"originalPrimaryKey": table.PrimaryKey.Name.Source(),
"lowerStartCamelPrimaryKey": stringx.From(table.PrimaryKey.Name.Snake2Camel()).LowerStart(),
"lowerStartCamelPrimaryKey": stringx.From(table.PrimaryKey.Name.ToCamel()).UnTitle(),
"dataType": table.PrimaryKey.DataType,
"cacheKey": table.CacheKey[table.PrimaryKey.Name.Source()].KeyExpression,
"cacheKeyVariable": table.CacheKey[table.PrimaryKey.Name.Source()].Variable,

View File

@@ -12,23 +12,23 @@ import (
func genFineOneByField(table Table, withCache bool) (string, error) {
t := templatex.With("findOneByField").Parse(template.FindOneByField)
var list []string
camelTableName := table.Name.Snake2Camel()
camelTableName := table.Name.ToCamel()
for _, field := range table.Fields {
if field.IsPrimaryKey || !field.IsKey {
continue
}
camelFieldName := field.Name.Snake2Camel()
camelFieldName := field.Name.ToCamel()
output, err := t.Execute(map[string]interface{}{
"upperStartCamelObject": camelTableName,
"upperField": camelFieldName,
"in": fmt.Sprintf("%s %s", stringx.From(camelFieldName).LowerStart(), field.DataType),
"in": fmt.Sprintf("%s %s", stringx.From(camelFieldName).UnTitle(), field.DataType),
"withCache": withCache,
"cacheKey": table.CacheKey[field.Name.Source()].KeyExpression,
"cacheKeyVariable": table.CacheKey[field.Name.Source()].Variable,
"primaryKeyLeft": table.CacheKey[table.PrimaryKey.Name.Source()].Left,
"lowerStartCamelObject": stringx.From(camelTableName).LowerStart(),
"lowerStartCamelField": stringx.From(camelFieldName).LowerStart(),
"upperStartCamelPrimaryKey": table.PrimaryKey.Name.Snake2Camel(),
"lowerStartCamelObject": stringx.From(camelTableName).UnTitle(),
"lowerStartCamelField": stringx.From(camelFieldName).UnTitle(),
"upperStartCamelPrimaryKey": table.PrimaryKey.Name.ToCamel(),
"originalField": field.Name.Source(),
"originalPrimaryField": table.PrimaryKey.Name.Source(),
})

View File

@@ -23,21 +23,17 @@ const (
type (
defaultGenerator struct {
source string
src string
dir string
console.Console
}
Option func(generator *defaultGenerator)
)
func NewDefaultGenerator(src, dir string, opt ...Option) *defaultGenerator {
if src == "" {
src = pwd
}
func NewDefaultGenerator(source, dir string, opt ...Option) *defaultGenerator {
if dir == "" {
dir = pwd
}
generator := &defaultGenerator{src: src, dir: dir}
generator := &defaultGenerator{source: source, dir: dir}
var optionList []Option
optionList = append(optionList, newDefaultOption())
optionList = append(optionList, opt...)
@@ -60,10 +56,6 @@ func newDefaultOption() Option {
}
func (g *defaultGenerator) Start(withCache bool) error {
fileSrc, err := filepath.Abs(g.src)
if err != nil {
return err
}
dirAbs, err := filepath.Abs(g.dir)
if err != nil {
return err
@@ -72,21 +64,16 @@ func (g *defaultGenerator) Start(withCache bool) error {
if err != nil {
return err
}
data, err := ioutil.ReadFile(fileSrc)
if err != nil {
return err
}
g.source = string(data)
modelList, err := g.genFromDDL(withCache)
if err != nil {
return err
}
for tableName, code := range modelList {
name := fmt.Sprintf("%smodel.go", strings.ToLower(stringx.From(tableName).Snake2Camel()))
name := fmt.Sprintf("%smodel.go", strings.ToLower(stringx.From(tableName).ToCamel()))
filename := filepath.Join(dirAbs, name)
if util.FileExists(filename) {
g.Warning("%s already exists,ignored.", name)
g.Warning("%s already exists, ignored.", name)
continue
}
err = ioutil.WriteFile(filename, []byte(code), os.ModePerm)

View File

@@ -12,7 +12,7 @@ func genInsert(table Table, withCache bool) (string, error) {
expressions := make([]string, 0)
expressionValues := make([]string, 0)
for _, filed := range table.Fields {
camel := filed.Name.Snake2Camel()
camel := filed.Name.ToCamel()
if camel == "CreateTime" || camel == "UpdateTime" {
continue
}
@@ -22,13 +22,13 @@ func genInsert(table Table, withCache bool) (string, error) {
expressions = append(expressions, "?")
expressionValues = append(expressionValues, "data."+camel)
}
camel := table.Name.Snake2Camel()
camel := table.Name.ToCamel()
output, err := templatex.With("insert").
Parse(template.Insert).
Execute(map[string]interface{}{
"withCache": withCache,
"upperStartCamelObject": camel,
"lowerStartCamelObject": stringx.From(camel).LowerStart(),
"lowerStartCamelObject": stringx.From(camel).UnTitle(),
"expression": strings.Join(expressions, ", "),
"expressionValues": strings.Join(expressionValues, ", "),
})

View File

@@ -26,14 +26,14 @@ type (
func genCacheKeys(table parser.Table) (map[string]Key, error) {
fields := table.Fields
m := make(map[string]Key)
camelTableName := table.Name.Snake2Camel()
lowerStartCamelTableName := stringx.From(camelTableName).LowerStart()
camelTableName := table.Name.ToCamel()
lowerStartCamelTableName := stringx.From(camelTableName).UnTitle()
for _, field := range fields {
if !field.IsKey {
continue
}
camelFieldName := field.Name.Snake2Camel()
lowerStartCamelFieldName := stringx.From(camelFieldName).LowerStart()
camelFieldName := field.Name.ToCamel()
lowerStartCamelFieldName := stringx.From(camelFieldName).UnTitle()
left := fmt.Sprintf("cache%s%sPrefix", camelTableName, camelFieldName)
right := fmt.Sprintf("cache#%s#%s#", camelTableName, lowerStartCamelFieldName)
variable := fmt.Sprintf("%s%sKey", lowerStartCamelTableName, camelFieldName)

View File

@@ -10,7 +10,7 @@ func genNew(table Table, withCache bool) (string, error) {
Parse(template.New).
Execute(map[string]interface{}{
"withCache": withCache,
"upperStartCamelObject": table.Name.Snake2Camel(),
"upperStartCamelObject": table.Name.ToCamel(),
})
if err != nil {
return "", err

View File

@@ -15,7 +15,7 @@ func genTypes(table Table, withCache bool) (string, error) {
Parse(template.Types).
Execute(map[string]interface{}{
"withCache": withCache,
"upperStartCamelObject": table.Name.Snake2Camel(),
"upperStartCamelObject": table.Name.ToCamel(),
"fields": fieldsString,
})
if err != nil {

View File

@@ -11,7 +11,7 @@ import (
func genUpdate(table Table, withCache bool) (string, error) {
expressionValues := make([]string, 0)
for _, filed := range table.Fields {
camel := filed.Name.Snake2Camel()
camel := filed.Name.ToCamel()
if camel == "CreateTime" || camel == "UpdateTime" {
continue
}
@@ -20,8 +20,8 @@ func genUpdate(table Table, withCache bool) (string, error) {
}
expressionValues = append(expressionValues, "data."+camel)
}
expressionValues = append(expressionValues, "data."+table.PrimaryKey.Name.Snake2Camel())
camelTableName := table.Name.Snake2Camel()
expressionValues = append(expressionValues, "data."+table.PrimaryKey.Name.ToCamel())
camelTableName := table.Name.ToCamel()
output, err := templatex.With("update").
Parse(template.Update).
Execute(map[string]interface{}{
@@ -29,7 +29,7 @@ func genUpdate(table Table, withCache bool) (string, error) {
"upperStartCamelObject": camelTableName,
"primaryCacheKey": table.CacheKey[table.PrimaryKey.Name.Source()].DataKeyExpression,
"primaryKeyVariable": table.CacheKey[table.PrimaryKey.Name.Source()].Variable,
"lowerStartCamelObject": stringx.From(camelTableName).LowerStart(),
"lowerStartCamelObject": stringx.From(camelTableName).UnTitle(),
"originalPrimaryKey": table.PrimaryKey.Name.Source(),
"expressionValues": strings.Join(expressionValues, ", "),
})

View File

@@ -13,12 +13,12 @@ func genVars(table Table, withCache bool) (string, error) {
for _, v := range table.CacheKey {
keys = append(keys, v.VarExpression)
}
camel := table.Name.Snake2Camel()
camel := table.Name.ToCamel()
output, err := templatex.With("var").
Parse(template.Vars).
GoFmt(true).
Execute(map[string]interface{}{
"lowerStartCamelObject": stringx.From(camel).LowerStart(),
"lowerStartCamelObject": stringx.From(camel).UnTitle(),
"upperStartCamelObject": camel,
"cacheKeys": strings.Join(keys, "\r\n"),
"autoIncrement": table.PrimaryKey.AutoIncrement,

View File

@@ -0,0 +1,33 @@
package model
import (
"github.com/tal-tech/go-zero/core/stores/sqlx"
)
type (
DDLModel struct {
conn sqlx.SqlConn
}
DDL struct {
Table string `db:"Table"`
DDL string `db:"Create Table"`
}
)
func NewDDLModel(conn sqlx.SqlConn) *DDLModel {
return &DDLModel{conn: conn}
}
func (m *DDLModel) ShowDDL(table ...string) ([]string, error) {
var ddl []string
for _, t := range table {
query := `show create table ` + t
var resp DDL
err := m.conn.QueryRow(&resp, query)
if err != nil {
return nil, err
}
ddl = append(ddl, resp.DDL)
}
return ddl, nil
}

View File

@@ -81,7 +81,7 @@ func Parse(ddl string) (*Table, error) {
}
column := index.Columns[0]
columnName := column.Column.String()
camelColumnName := stringx.From(columnName).Snake2Camel()
camelColumnName := stringx.From(columnName).ToCamel()
// by default, createTime|updateTime findOne is not used.
if camelColumnName == "CreateTime" || camelColumnName == "UpdateTime" {
continue

View File

@@ -19,6 +19,13 @@ type (
}
)
func NewConsole(idea bool) Console {
if idea {
return NewIdeaConsole()
}
return NewColorConsole()
}
func NewColorConsole() *colorConsole {
return &colorConsole{}
}

View File

@@ -6,10 +6,6 @@ import (
"unicode"
)
const (
emptyString = ""
)
type (
String struct {
source string
@@ -31,15 +27,9 @@ func (s String) IsEmptyOrSpace() bool {
}
func (s String) Lower() string {
if s.IsEmptyOrSpace() {
return s.source
}
return strings.ToLower(s.source)
}
func (s String) Upper() string {
if s.IsEmptyOrSpace() {
return s.source
}
return strings.ToUpper(s.source)
}
func (s String) Title() string {
@@ -50,10 +40,7 @@ func (s String) Title() string {
}
// snake->camel(upper start)
func (s String) Snake2Camel() string {
if s.IsEmptyOrSpace() {
return s.source
}
func (s String) ToCamel() string {
list := s.splitBy(func(r rune) bool {
return r == '_'
}, true)
@@ -65,10 +52,7 @@ func (s String) Snake2Camel() string {
}
// camel->snake
func (s String) Camel2Snake() string {
if s.IsEmptyOrSpace() {
return s.source
}
func (s String) ToSnake() string {
list := s.splitBy(func(r rune) bool {
return unicode.IsUpper(r)
}, false)
@@ -80,7 +64,7 @@ func (s String) Camel2Snake() string {
}
// return original string if rune is not letter at index 0
func (s String) LowerStart() string {
func (s String) UnTitle() string {
if s.IsEmptyOrSpace() {
return s.source
}

View File

@@ -17,26 +17,26 @@ func TestString_IsEmptyOrSpace(t *testing.T) {
}
func TestString_Snake2Camel(t *testing.T) {
ret := From("____this_is_snake").Snake2Camel()
ret := From("____this_is_snake").ToCamel()
assert.Equal(t, "ThisIsSnake", ret)
ret2 := From("测试_test_Data").Snake2Camel()
ret2 := From("测试_test_Data").ToCamel()
assert.Equal(t, "测试TestData", ret2)
ret3 := From("___").Snake2Camel()
ret3 := From("___").ToCamel()
assert.Equal(t, "", ret3)
ret4 := From("testData_").Snake2Camel()
ret4 := From("testData_").ToCamel()
assert.Equal(t, "TestData", ret4)
ret5 := From("testDataTestData").Snake2Camel()
ret5 := From("testDataTestData").ToCamel()
assert.Equal(t, "TestDataTestData", ret5)
}
func TestString_Camel2Snake(t *testing.T) {
ret := From("ThisIsCCCamel").Camel2Snake()
ret := From("ThisIsCCCamel").ToSnake()
assert.Equal(t, "this_is_c_c_camel", ret)
ret2 := From("测试Test_Data_test_data").Camel2Snake()
ret2 := From("测试Test_Data_test_data").ToSnake()
assert.Equal(t, "测试_test__data_test_data", ret2)
}