rpc service generation (#26)

* add execute files

* add protoc-osx

* add rpc generation

* add rpc generation

* add: rpc template generation

* update usage

* fixed env prepare for project in go path

* optimize gomod cache

* add README.md

* format error

* reactor templatex.go

* remove waste code
master
Keson 4 years ago committed by GitHub
parent 71bbf91a63
commit db16115037
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -3,12 +3,12 @@ module github.com/tal-tech/go-zero
go 1.14
require (
9fans.net/go v0.0.2 // indirect
github.com/DATA-DOG/go-sqlmock v1.4.1
github.com/alicebob/gopher-json v0.0.0-20180125190556-5a6b3ba71ee6 // indirect
github.com/alicebob/miniredis v2.5.0+incompatible
github.com/dchest/siphash v1.2.1
github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/dsymonds/gotoc v0.0.0-20160928043926-5aebcfc91819
github.com/fatih/color v1.9.0 // indirect
github.com/frankban/quicktest v1.7.2 // indirect
github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8
@ -56,7 +56,7 @@ require (
golang.org/x/tools v0.0.0-20200410132612-ae9902aceb98 // indirect
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f // indirect
google.golang.org/grpc v1.29.1
google.golang.org/protobuf v1.25.0 // indirect
google.golang.org/protobuf v1.25.0
gopkg.in/cheggaaa/pb.v1 v1.0.28
gopkg.in/yaml.v2 v2.2.8
honnef.co/go/tools v0.0.1-2020.1.4 // indirect

@ -1,5 +1,3 @@
9fans.net/go v0.0.2 h1:RYM6lWITV8oADrwLfdzxmt8ucfW6UtP9v1jg4qAbqts=
9fans.net/go v0.0.2/go.mod h1:lfPdxjq9v8pVQXUMBCx5EO5oLXWQFlKRQgs1kEkjoIM=
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
@ -50,6 +48,8 @@ github.com/dchest/siphash v1.2.1 h1:4cLinnzVJDKxTCl9B01807Yiy+W7ZzVHj/KIroQRvT4=
github.com/dchest/siphash v1.2.1/go.mod h1:q+IRvb2gOSrUnYoPqHiyHXS0FOBBOdl6tONBlVnOnt4=
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dsymonds/gotoc v0.0.0-20160928043926-5aebcfc91819 h1:9778zj477h/VauD8kHbOtbytW2KGQefJ/wUGE5w+mzw=
github.com/dsymonds/gotoc v0.0.0-20160928043926-5aebcfc91819/go.mod h1:MvzMVHq8BH2Ji/o8TGDocVA70byvLrAgFTxkEnmjO4Y=
github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4 h1:qk/FSDDxo05wdJH28W+p5yivv7LuLYLRXPPD8KQCtZs=
github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=

@ -4,6 +4,8 @@ import (
"fmt"
"os"
"github.com/urfave/cli"
"github.com/tal-tech/go-zero/core/logx"
"github.com/tal-tech/go-zero/tools/goctl/api/apigen"
"github.com/tal-tech/go-zero/tools/goctl/api/dartgen"
@ -17,8 +19,8 @@ import (
"github.com/tal-tech/go-zero/tools/goctl/configgen"
"github.com/tal-tech/go-zero/tools/goctl/docker"
"github.com/tal-tech/go-zero/tools/goctl/feature"
"github.com/tal-tech/go-zero/tools/goctl/model/sql/command"
"github.com/urfave/cli"
model "github.com/tal-tech/go-zero/tools/goctl/model/sql/command"
rpc "github.com/tal-tech/go-zero/tools/goctl/rpc/command"
)
var (
@ -188,6 +190,54 @@ var (
},
Action: docker.DockerCommand,
},
{
Name: "rpc",
Usage: "generate rpc code",
Subcommands: []cli.Command{
{
Name: "template",
Usage: `generate proto template"`,
Flags: []cli.Flag{
cli.StringFlag{
Name: "out, o",
Usage: "the target path of proto",
},
cli.BoolFlag{
Name: "idea",
Usage: "whether the command execution environment is from idea plugin. [option]",
},
},
Action: rpc.RpcTemplate,
},
{
Name: "proto",
Usage: `generate rpc from proto"`,
Flags: []cli.Flag{
cli.StringFlag{
Name: "src, s",
Usage: "the file path of the proto source file",
},
cli.StringFlag{
Name: "dir, d",
Usage: `the target path of the code,default path is "${pwd}". [option]`,
},
cli.StringFlag{
Name: "service, srv",
Usage: `the name of rpc service. [option]`,
},
cli.StringFlag{
Name: "shared",
Usage: `the dir of the shared file,default path is "${pwd}/shared. [option]"`,
},
cli.BoolFlag{
Name: "idea",
Usage: "whether the command execution environment is from idea plugin. [option]",
},
},
Action: rpc.Rpc,
},
},
},
{
Name: "model",
Usage: "generate model code",
@ -217,7 +267,7 @@ var (
Usage: "for idea plugin [optional]",
},
},
Action: command.MysqlDDL,
Action: model.MysqlDDL,
},
{
Name: "datasource",
@ -244,7 +294,7 @@ var (
Usage: "for idea plugin [optional]",
},
},
Action: command.MyDataSource,
Action: model.MyDataSource,
},
},
},

@ -5,8 +5,8 @@ import (
"github.com/tal-tech/go-zero/core/collection"
"github.com/tal-tech/go-zero/tools/goctl/model/sql/template"
"github.com/tal-tech/go-zero/tools/goctl/util"
"github.com/tal-tech/go-zero/tools/goctl/util/stringx"
"github.com/tal-tech/go-zero/tools/goctl/util/templatex"
)
func genDelete(table Table, withCache bool) (string, error) {
@ -28,7 +28,7 @@ func genDelete(table Table, withCache bool) (string, error) {
}
}
camel := table.Name.ToCamel()
output, err := templatex.With("delete").
output, err := util.With("delete").
Parse(template.Delete).
Execute(map[string]interface{}{
"upperStartCamelObject": camel,

@ -5,7 +5,7 @@ import (
"github.com/tal-tech/go-zero/tools/goctl/model/sql/parser"
"github.com/tal-tech/go-zero/tools/goctl/model/sql/template"
"github.com/tal-tech/go-zero/tools/goctl/util/templatex"
"github.com/tal-tech/go-zero/tools/goctl/util"
)
func genFields(fields []parser.Field) (string, error) {
@ -25,7 +25,7 @@ func genField(field parser.Field) (string, error) {
if err != nil {
return "", err
}
output, err := templatex.With("types").
output, err := util.With("types").
Parse(template.Field).
Execute(map[string]interface{}{
"name": field.Name.ToCamel(),

@ -2,13 +2,13 @@ package gen
import (
"github.com/tal-tech/go-zero/tools/goctl/model/sql/template"
"github.com/tal-tech/go-zero/tools/goctl/util"
"github.com/tal-tech/go-zero/tools/goctl/util/stringx"
"github.com/tal-tech/go-zero/tools/goctl/util/templatex"
)
func genFindOne(table Table, withCache bool) (string, error) {
camel := table.Name.ToCamel()
output, err := templatex.With("findOne").
output, err := util.With("findOne").
Parse(template.FindOne).
Execute(map[string]interface{}{
"withCache": withCache,

@ -5,12 +5,12 @@ import (
"strings"
"github.com/tal-tech/go-zero/tools/goctl/model/sql/template"
"github.com/tal-tech/go-zero/tools/goctl/util"
"github.com/tal-tech/go-zero/tools/goctl/util/stringx"
"github.com/tal-tech/go-zero/tools/goctl/util/templatex"
)
func genFineOneByField(table Table, withCache bool) (string, error) {
t := templatex.With("findOneByField").Parse(template.FindOneByField)
t := util.With("findOneByField").Parse(template.FindOneByField)
var list []string
camelTableName := table.Name.ToCamel()
for _, field := range table.Fields {

@ -12,7 +12,6 @@ import (
"github.com/tal-tech/go-zero/tools/goctl/util"
"github.com/tal-tech/go-zero/tools/goctl/util/console"
"github.com/tal-tech/go-zero/tools/goctl/util/stringx"
"github.com/tal-tech/go-zero/tools/goctl/util/templatex"
)
const (
@ -119,7 +118,7 @@ type (
)
func (g *defaultGenerator) genModel(in parser.Table, withCache bool) (string, error) {
t := templatex.With("model").
t := util.With("model").
Parse(template.Model).
GoFmt(true)

@ -4,8 +4,8 @@ import (
"strings"
"github.com/tal-tech/go-zero/tools/goctl/model/sql/template"
"github.com/tal-tech/go-zero/tools/goctl/util"
"github.com/tal-tech/go-zero/tools/goctl/util/stringx"
"github.com/tal-tech/go-zero/tools/goctl/util/templatex"
)
func genInsert(table Table, withCache bool) (string, error) {
@ -23,7 +23,7 @@ func genInsert(table Table, withCache bool) (string, error) {
expressionValues = append(expressionValues, "data."+camel)
}
camel := table.Name.ToCamel()
output, err := templatex.With("insert").
output, err := util.With("insert").
Parse(template.Insert).
Execute(map[string]interface{}{
"withCache": withCache,

@ -2,11 +2,11 @@ package gen
import (
"github.com/tal-tech/go-zero/tools/goctl/model/sql/template"
"github.com/tal-tech/go-zero/tools/goctl/util/templatex"
"github.com/tal-tech/go-zero/tools/goctl/util"
)
func genNew(table Table, withCache bool) (string, error) {
output, err := templatex.With("new").
output, err := util.With("new").
Parse(template.New).
Execute(map[string]interface{}{
"withCache": withCache,

@ -2,14 +2,14 @@ package gen
import (
"github.com/tal-tech/go-zero/tools/goctl/model/sql/template"
"github.com/tal-tech/go-zero/tools/goctl/util/templatex"
"github.com/tal-tech/go-zero/tools/goctl/util"
)
func genTag(in string) (string, error) {
if in == "" {
return in, nil
}
output, err := templatex.With("tag").
output, err := util.With("tag").
Parse(template.Tag).
Execute(map[string]interface{}{
"field": in,

@ -2,7 +2,7 @@ package gen
import (
"github.com/tal-tech/go-zero/tools/goctl/model/sql/template"
"github.com/tal-tech/go-zero/tools/goctl/util/templatex"
"github.com/tal-tech/go-zero/tools/goctl/util"
)
func genTypes(table Table, withCache bool) (string, error) {
@ -11,7 +11,7 @@ func genTypes(table Table, withCache bool) (string, error) {
if err != nil {
return "", err
}
output, err := templatex.With("types").
output, err := util.With("types").
Parse(template.Types).
Execute(map[string]interface{}{
"withCache": withCache,

@ -4,8 +4,8 @@ import (
"strings"
"github.com/tal-tech/go-zero/tools/goctl/model/sql/template"
"github.com/tal-tech/go-zero/tools/goctl/util"
"github.com/tal-tech/go-zero/tools/goctl/util/stringx"
"github.com/tal-tech/go-zero/tools/goctl/util/templatex"
)
func genUpdate(table Table, withCache bool) (string, error) {
@ -22,7 +22,7 @@ func genUpdate(table Table, withCache bool) (string, error) {
}
expressionValues = append(expressionValues, "data."+table.PrimaryKey.Name.ToCamel())
camelTableName := table.Name.ToCamel()
output, err := templatex.With("update").
output, err := util.With("update").
Parse(template.Update).
Execute(map[string]interface{}{
"withCache": withCache,

@ -4,8 +4,8 @@ import (
"strings"
"github.com/tal-tech/go-zero/tools/goctl/model/sql/template"
"github.com/tal-tech/go-zero/tools/goctl/util"
"github.com/tal-tech/go-zero/tools/goctl/util/stringx"
"github.com/tal-tech/go-zero/tools/goctl/util/templatex"
)
func genVars(table Table, withCache bool) (string, error) {
@ -14,7 +14,7 @@ func genVars(table Table, withCache bool) (string, error) {
keys = append(keys, v.VarExpression)
}
camel := table.Name.ToCamel()
output, err := templatex.With("var").
output, err := util.With("var").
Parse(template.Vars).
GoFmt(true).
Execute(map[string]interface{}{

@ -0,0 +1,6 @@
# Change log
# 2020-08-27
* 新增支持rpc模板生成
* 新增支持rpc服务生成

@ -0,0 +1,161 @@
# Rpc Generation
Goctl Rpc是`goctl`脚手架下的一个rpc服务代码生成模块支持proto模板生成和rpc服务代码生成通过此工具生成代码你只需要关注业务逻辑编写而不用去编写一些重复性的代码。这使得我们把精力重心放在业务上从而加快了开发效率且降低了代码出错率。
# 特性
* 简单易用
* 快速提升开发效率
* 出错率低
# 快速开始
### 生成proto模板
```shell script
$ goctl rpc template -o=user.proto
```
```golang
syntax = "proto3";
package remoteuser;
message Request {
// 用户名
string username = 1;
// 用户密码
string password = 2;
}
message Response {
// 用户名称
string name = 1;
// 用户性别
string gender = 2;
}
service User{
// 登录
rpc Login(Request)returns(Response);
}
```
### 生成rpc服务代码
生成user rpc服务
```
$ goctl rpc proto -src=user.proto
```
代码tree
```
user
├── etc
│   └── user.json
├── internal
│   ├── config
│   │   └── config.go
│   ├── handler
│   │   ├── loginhandler.go
│   │   └── userhandler.go
│   ├── logic
│   │   └── loginlogic.go
│   └── svc
│   └── servicecontext.go
├── pb
│   └── user.pb.go
├── shared
│   ├── mockusermodel.go
│   ├── types.go
│   └── usermodel.go
├── user.go
└── user.proto
```
# 准备工作
* 安装了go环境
* 安装了protoc并且已经设置环境变量
# protoc-gen-go
在使用goctl生成rpc服务代码时我们默认会根据开发人员正在开发的工程依赖的`github.com/golang/protobuf`自动将插件重新`go install`到`${GOPATH}/bin`中,
寻找方法:
### go mod 工程
对于`$ go version`不低于1.5版本的的工程,会优先寻找`$ go env GOMODCACHE`目录,如果没有则去`${GOPATH}`中查找(见下文),而低于1.5版本的则会优先从`${GOPATH}/pkg/mod`目录下查找,否则也从`% GOPATH`中查找(见下文)
### go path工程
对于没有使用go mod的工程则默认当作在`${GOPATH}`中处理暂不支持用户自定义的GOPATH而这种情况下则会默认从`${GOPATH}/src`中查找
> 注意:
* 对于以上两种工程如果没有在对应目录查找到`protoc-gen-go`则会提示相应错误,尽管`protoc-gen-go`可能在其他已经设置环境变量的目录中,这个将在后面版本进行优化。
* 对于go mod工程如果工程没有依赖`github.com/golang/protobuf`则需要提前引入。
### 好处
* 保证grpc代码生成规范的一致性
# 用法
```shell script
$ goctl rpc proto -h
```
```shell script
NAME:
goctl rpc proto - generate rpc from proto"
USAGE:
goctl rpc proto [command options] [arguments...]
OPTIONS:
--src value, -s value the file path of the proto source file
--dir value, -d value the target path of the code,default path is "${pwd}". [option]
--service value, --srv value the name of rpc service. [option]
--shared value the dir of the shared file,default path is "${pwd}/shared. [option]"
--idea whether the command execution environment is from idea plugin. [option]
```
* 参数说明
* --src 必填proto数据源目前暂时支持单个proto文件生成这里不支持不建议外部依赖
* --dir 非必填默认为proto文件所在目录生成代码的目标目录
* --service 服务名称非必填默认为proto文件所在目录名称但是如果proto所在目录为一下结构
```shell script
user
├── cmd
│   └── rpc
│   └── user.proto
```
则服务名称亦为user而非proto所在文件夹名称了这里推荐使用这种结构可以方便在同一个服务名下建立不同类型的服务(api、rpc、mq等),便于代码管理与维护。
* --shared 非必填,默认为$dir(xxx.proto)/sharedrpc client逻辑代码存放目录。
> 注意这里的shared文件夹名称将会是代码中的package名称。
* --idea 非必填是否为idea插件中执行保留字段终端执行可以忽略
# 开发人员需要做什么
关注业务代码编写将重复性、与业务无关的工作交给goctl生成好rpc服务代码后开饭人员仅需要修改
* 服务中的配置文件编写(etc/xx.json、internal/config/config.go)
* 服务中业务逻辑编写(internal/logic/xxlogic.go)
* 服务中资源上下文的编写(internal/svc/servicecontext.go)
# 扩展
对于需要进行rpc mock的开发人员在安装了`mockgen`工具的前提下可以在rpc的shared文件中生成好对应的mock文件。
# 注意事项
* proto不支持暂多文件同时生成
* proto不支持外部依赖包引入message不支持inline
* 目前main文件、shared文件、handler文件会被强制覆盖而和开发人员手动需要编写的则不会覆盖生成这一类在代码头部均有
```shell script
// Code generated by goctl. DO NOT EDIT.
// Source: xxx.proto
```
的标识,请注意不要将也写业务性代码写在里面。
# 下一步规划
* 尽快支持windows端rpc生成

@ -0,0 +1,23 @@
package command
import (
"github.com/urfave/cli"
"github.com/tal-tech/go-zero/tools/goctl/rpc/ctx"
"github.com/tal-tech/go-zero/tools/goctl/rpc/goen"
)
func Rpc(c *cli.Context) error {
rpcCtx := ctx.MustCreateRpcContextFromCli(c)
generator := gogen.NewDefaultRpcGenerator(rpcCtx)
rpcCtx.Must(generator.Generate())
return nil
}
func RpcTemplate(c *cli.Context) error {
out := c.String("out")
idea := c.Bool("idea")
generator := gogen.NewRpcTemplate(out, idea)
generator.MustGenerate()
return nil
}

@ -0,0 +1,111 @@
package ctx
import (
"fmt"
"path/filepath"
"runtime"
"strings"
"github.com/urfave/cli"
"github.com/tal-tech/go-zero/core/logx"
"github.com/tal-tech/go-zero/tools/goctl/util"
"github.com/tal-tech/go-zero/tools/goctl/util/console"
"github.com/tal-tech/go-zero/tools/goctl/util/stringx"
)
const (
flagSrc = "src"
flagDir = "dir"
flagShared = "shared"
flagService = "service"
flagIdea = "idea"
)
type (
RpcContext struct {
ProjectPath string
ProjectName stringx.String
ServiceName stringx.String
CurrentPath string
Module string
ProtoFileSrc string
ProtoSource string
TargetDir string
SharedDir string
GoPath string
console.Console
}
)
func MustCreateRpcContext(protoSrc, targetDir, sharedDir, serviceName string, idea bool) *RpcContext {
log := console.NewConsole(idea)
info, err := prepare(log)
log.Must(err)
if stringx.From(protoSrc).IsEmptyOrSpace() {
log.Fatalln("expected proto source, but nothing found")
}
srcFp, err := filepath.Abs(protoSrc)
log.Must(err)
if !util.FileExists(srcFp) {
log.Fatalln("%s is not exists", srcFp)
}
current := filepath.Dir(srcFp)
if stringx.From(targetDir).IsEmptyOrSpace() {
targetDir = current
}
if stringx.From(sharedDir).IsEmptyOrSpace() {
sharedDir = filepath.Join(current, "shared")
}
targetDirFp, err := filepath.Abs(targetDir)
log.Must(err)
sharedFp, err := filepath.Abs(sharedDir)
log.Must(err)
if stringx.From(serviceName).IsEmptyOrSpace() {
serviceName = getServiceFromRpcStructure(targetDirFp)
}
serviceNameString := stringx.From(serviceName)
if serviceNameString.IsEmptyOrSpace() {
log.Fatalln("service name is not found")
}
return &RpcContext{
ProjectPath: info.Path,
ProjectName: stringx.From(info.Name),
ServiceName: serviceNameString,
CurrentPath: current,
Module: info.GoMod.Module,
ProtoFileSrc: srcFp,
ProtoSource: filepath.Base(srcFp),
TargetDir: targetDirFp,
SharedDir: sharedFp,
GoPath: info.GoPath,
Console: log,
}
}
func MustCreateRpcContextFromCli(ctx *cli.Context) *RpcContext {
os := runtime.GOOS
switch os {
case "darwin":
case "windows":
logx.Must(fmt.Errorf("windows will support soon"))
default:
logx.Must(fmt.Errorf("unexpected os: %s", os))
}
protoSrc := ctx.String(flagSrc)
targetDir := ctx.String(flagDir)
sharedDir := ctx.String(flagShared)
serviceName := ctx.String(flagService)
idea := ctx.Bool(flagIdea)
return MustCreateRpcContext(protoSrc, targetDir, sharedDir, serviceName, idea)
}
func getServiceFromRpcStructure(targetDir string) string {
targetDir = filepath.Clean(targetDir)
suffix := filepath.Join("cmd", "rpc")
return filepath.Base(strings.TrimSuffix(targetDir, suffix))
}

@ -0,0 +1,208 @@
package ctx
import (
"errors"
"fmt"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"regexp"
"runtime"
"strings"
"github.com/tal-tech/go-zero/tools/goctl/rpc/execx"
"github.com/tal-tech/go-zero/tools/goctl/util"
"github.com/tal-tech/go-zero/tools/goctl/util/console"
)
var (
errProtobufNotFound = errors.New("github.com/golang/protobuf is not found,please ensure you has already [go get github.com/golang/protobuf]")
)
const (
constGo = "go"
constProtoC = "protoc"
constGoModOn = "go env GO111MODULE"
constGoMod = "go env GOMOD"
constGoModCache = "go env GOMODCACHE"
constGoPath = "go env GOPATH"
constProtoCGenGo = "protoc-gen-go"
)
type (
Project struct {
Path string
Name string
GoPath string
Protobuf Protobuf
GoMod GoMod
}
GoMod struct {
ModOn bool
GoModCache string
GoMod string
Module string
}
Protobuf struct {
Path string
}
)
func prepare(log console.Console) (*Project, error) {
log.Info("check go env ...")
_, err := exec.LookPath(constGo)
if err != nil {
return nil, err
}
_, err = exec.LookPath(constProtoC)
if err != nil {
return nil, err
}
var (
goModOn bool
goMod, goModCache, module string
goPath string
name, path string
protobufModule string
)
ret, err := execx.Run(constGoModOn)
if err != nil {
return nil, err
}
goModOn = strings.TrimSpace(ret) == "on"
ret, err = execx.Run(constGoMod)
if err != nil {
return nil, err
}
goMod = strings.TrimSpace(ret)
ret, err = execx.Run(constGoModCache)
if err != nil {
return nil, err
}
goModCache = strings.TrimSpace(ret)
ret, err = execx.Run(constGoPath)
if err != nil {
return nil, err
}
goPath = strings.TrimSpace(ret)
src := filepath.Join(goPath, "src")
if len(goMod) > 0 {
if goModCache == "" {
goModCache = filepath.Join(goPath, "pkg", "mod")
}
path = filepath.Dir(goMod)
name = filepath.Base(path)
data, err := ioutil.ReadFile(goMod)
if err != nil {
return nil, err
}
module, err = matchModule(data)
if err != nil {
return nil, err
}
protobufModule, err = matchProtoBuf(data)
if err != nil {
return nil, err
}
} else {
if goModCache == "" {
goModCache = src
}
pwd, err := os.Getwd()
if err != nil {
return nil, err
}
if !strings.HasPrefix(pwd, src) {
return nil, fmt.Errorf("%s: project is not in go mod and go path", pwd)
}
r := strings.TrimPrefix(pwd, src+string(filepath.Separator))
name = filepath.Dir(r)
if name == "." {
name = r
}
path = filepath.Join(src, name)
module = name
}
protobuf := filepath.Join(goModCache, protobufModule)
if !util.FileExists(protobuf) {
return nil, fmt.Errorf("expected protobuf module in path: %s,please ensure you has already [go get github.com/golang/protobuf]", protobuf)
}
var protoCGenGoFilename string
os := runtime.GOOS
switch os {
case "darwin":
protoCGenGoFilename = filepath.Join(goPath, "bin", "protoc-gen-go")
case "windows":
protoCGenGoFilename = filepath.Join(goPath, "bin", "protoc-gen-go.exe")
default:
return nil, fmt.Errorf("unexpeted os: %s", os)
}
if !util.FileExists(protoCGenGoFilename) {
sh := "go install " + filepath.Join(protobuf, constProtoCGenGo)
log.Warning(sh)
stdout, err := execx.Run(sh)
if err != nil {
return nil, err
}
log.Info(stdout)
}
if !util.FileExists(protoCGenGoFilename) {
return nil, fmt.Errorf("protoc-gen-go is not found")
}
return &Project{
Name: name,
Path: path,
GoPath: goPath,
Protobuf: Protobuf{
Path: protobuf,
},
GoMod: GoMod{
ModOn: goModOn,
GoModCache: goModCache,
GoMod: goMod,
Module: module,
},
}, nil
}
// github.com/golang/protobuf@{version}
func matchProtoBuf(data []byte) (string, error) {
text := string(data)
re := regexp.MustCompile(`(?m)(github.com/golang/protobuf)\s+(v[0-9.]+)`)
matches := re.FindAllStringSubmatch(text, -1)
if len(matches) == 0 {
return "", errProtobufNotFound
}
groups := matches[0]
if len(groups) < 3 {
return "", errProtobufNotFound
}
return fmt.Sprintf("%s@%s", groups[1], groups[2]), nil
}
func matchModule(data []byte) (string, error) {
text := string(data)
re := regexp.MustCompile(`(?m)^\s*module\s+[a-z0-9/\-.]+$`)
matches := re.FindAllString(text, -1)
if len(matches) == 1 {
target := matches[0]
index := strings.Index(target, "module")
return strings.TrimSpace(target[index+6:]), nil
}
return "", nil
}

@ -0,0 +1,34 @@
package execx
import (
"bytes"
"errors"
"fmt"
"os/exec"
"runtime"
)
func Run(arg string) (string, error) {
goos := runtime.GOOS
var cmd *exec.Cmd
switch goos {
case "darwin":
cmd = exec.Command("sh", "-c", arg)
case "windows":
cmd = exec.Command("cmd.exe", "/c", arg)
default:
return "", fmt.Errorf("unexpected os: %v", goos)
}
dtsout := new(bytes.Buffer)
stderr := new(bytes.Buffer)
cmd.Stdout = dtsout
cmd.Stderr = stderr
err := cmd.Run()
if err != nil {
if stderr.Len() > 0 {
return "", errors.New(stderr.String())
}
return "", err
}
return dtsout.String(), nil
}

@ -0,0 +1,89 @@
package gogen
import (
"github.com/tal-tech/go-zero/tools/goctl/rpc/ctx"
"github.com/tal-tech/go-zero/tools/goctl/rpc/parser"
)
const (
dirTarget = "dirTarget"
dirConfig = "config"
dirEtc = "etc"
dirSvc = "svc"
dirShared = "shared"
dirHandler = "handler"
dirLogic = "logic"
dirPb = "pb"
dirInternal = "internal"
fileConfig = "config.go"
fileServiceContext = "servicecontext.go"
)
type (
defaultRpcGenerator struct {
dirM map[string]string
Ctx *ctx.RpcContext
ast *parser.PbAst
}
)
func NewDefaultRpcGenerator(ctx *ctx.RpcContext) *defaultRpcGenerator {
return &defaultRpcGenerator{
Ctx: ctx,
}
}
func (g *defaultRpcGenerator) Generate() (err error) {
g.Ctx.Info("code generating...")
defer func() {
if err == nil {
g.Ctx.Success("Done.")
}
}()
err = g.createDir()
if err != nil {
return
}
err = g.genEtc()
if err != nil {
return
}
err = g.genPb()
if err != nil {
return
}
err = g.genConfig()
if err != nil {
return
}
err = g.genSvc()
if err != nil {
return
}
err = g.genLogic()
if err != nil {
return
}
err = g.genRemoteHandler()
if err != nil {
return
}
err = g.genMain()
if err != nil {
return
}
err = g.genShared()
if err != nil {
return
}
return nil
}

@ -0,0 +1,29 @@
package gogen
import (
"io/ioutil"
"os"
"path/filepath"
"github.com/tal-tech/go-zero/tools/goctl/util"
)
var configTemplate = `package config
import "github.com/tal-tech/go-zero/rpcx"
type (
Config struct {
rpcx.RpcServerConf
}
)
`
func (g *defaultRpcGenerator) genConfig() error {
configPath := g.dirM[dirConfig]
fileName := filepath.Join(configPath, fileConfig)
if util.FileExists(fileName) {
return nil
}
return ioutil.WriteFile(fileName, []byte(configTemplate), os.ModePerm)
}

@ -0,0 +1,45 @@
package gogen
import (
"path/filepath"
"strings"
"github.com/tal-tech/go-zero/tools/goctl/util"
)
// target
// ├── etc
// ├── internal
// │   ├── config
// │   ├── handler
// │   ├── logic
// │   ├── pb
// │   └── svc
func (g *defaultRpcGenerator) createDir() error {
ctx := g.Ctx
m := make(map[string]string)
m[dirTarget] = ctx.TargetDir
m[dirEtc] = filepath.Join(ctx.TargetDir, dirEtc)
m[dirInternal] = filepath.Join(ctx.TargetDir, dirInternal)
m[dirConfig] = filepath.Join(ctx.TargetDir, dirInternal, dirConfig)
m[dirHandler] = filepath.Join(ctx.TargetDir, dirInternal, dirHandler)
m[dirLogic] = filepath.Join(ctx.TargetDir, dirInternal, dirLogic)
m[dirPb] = filepath.Join(ctx.TargetDir, dirPb)
m[dirSvc] = filepath.Join(ctx.TargetDir, dirInternal, dirSvc)
m[dirShared] = g.Ctx.SharedDir
for _, d := range m {
err := util.MkdirIfNotExist(d)
if err != nil {
return err
}
}
g.dirM = m
return nil
}
func (g *defaultRpcGenerator) mustGetPackage(dir string) string {
target := g.dirM[dir]
projectPath := g.Ctx.ProjectPath
relativePath := strings.TrimPrefix(target, projectPath)
return g.Ctx.Module + relativePath
}

@ -0,0 +1,34 @@
package gogen
import (
"fmt"
"path/filepath"
"github.com/tal-tech/go-zero/tools/goctl/util"
)
var etcTemplate = `{
"Name": "{{.serviceName}}.rpc",
"Log": {
"Mode": "console"
},
"ListenOn": "127.0.0.1:8080",
"Etcd": {
"Hosts": ["127.0.0.1:6379"],
"Key": "{{.serviceName}}.rpc"
}
}
`
func (g *defaultRpcGenerator) genEtc() error {
etdDir := g.dirM[dirEtc]
fileName := filepath.Join(etdDir, fmt.Sprintf("%v.json", g.Ctx.ServiceName.Lower()))
if util.FileExists(fileName) {
return nil
}
return util.With("etc").
Parse(etcTemplate).
SaveTo(map[string]interface{}{
"serviceName": g.Ctx.ServiceName.Lower(),
}, fileName, false)
}

@ -0,0 +1,109 @@
package gogen
import (
"fmt"
"path/filepath"
"strings"
"github.com/tal-tech/go-zero/tools/goctl/util"
)
var (
remoteTemplate = `{{.head}}
package handler
import (
{{.imports}}
)
type (
{{.types}}
)
{{.newFuncs}}
`
functionTemplate = `{{.head}}
package handler
import (
"context"
{{.imports}}
)
{{if .hasComment}}{{.comment}}{{end}}
func (s *{{.server}}Server) {{.method}} (ctx context.Context, in *{{.package}}.{{.request}}) (*{{.package}}.{{.response}}, error) {
l:=logic.New{{.logicName}}(ctx,s.svcCtx)
return l.{{.method}}(in)
}
`
typeFmt = `%sServer struct {
svcCtx *svc.ServiceContext
}`
newFuncFmt = `func New%sServer(svcCtx *svc.ServiceContext) *%sServer {
return &%sServer{
svcCtx: svcCtx,
}
}`
)
func (g *defaultRpcGenerator) genRemoteHandler() error {
handlerPath := g.dirM[dirHandler]
serverGo := fmt.Sprintf("%vhandler.go", g.Ctx.ServiceName.Lower())
fileName := filepath.Join(handlerPath, serverGo)
file := g.ast
svcImport := fmt.Sprintf(`"%v"`, g.mustGetPackage(dirSvc))
types := make([]string, 0)
newFuncs := make([]string, 0)
head := util.GetHead(g.Ctx.ProtoSource)
for _, service := range file.Service {
types = append(types, fmt.Sprintf(typeFmt, service.Name.Title()))
newFuncs = append(newFuncs, fmt.Sprintf(newFuncFmt, service.Name.Title(), service.Name.Title(), service.Name.Title()))
}
err := util.With("server").GoFmt(true).Parse(remoteTemplate).SaveTo(map[string]interface{}{
"head": head,
"types": strings.Join(types, "\n"),
"newFuncs": strings.Join(newFuncs, "\n"),
"imports": svcImport,
}, fileName, true)
if err != nil {
return err
}
return g.genFunctions()
}
func (g *defaultRpcGenerator) genFunctions() error {
handlerPath := g.dirM[dirHandler]
file := g.ast
pkg := file.Package
head := util.GetHead(g.Ctx.ProtoSource)
handlerImports := make([]string, 0)
pbImport := fmt.Sprintf(`%v "%v"`, pkg, g.mustGetPackage(dirPb))
handlerImports = append(handlerImports, pbImport, fmt.Sprintf(`"%v"`, g.mustGetPackage(dirLogic)))
for _, service := range file.Service {
for _, method := range service.Funcs {
handlerName := fmt.Sprintf("%shandler.go", method.Name.Lower())
filename := filepath.Join(handlerPath, handlerName)
// override
err := util.With("func").GoFmt(true).Parse(functionTemplate).SaveTo(map[string]interface{}{
"head": head,
"server": service.Name.Title(),
"imports": strings.Join(handlerImports, "\r\n"),
"logicName": fmt.Sprintf("%sLogic", method.Name.Title()),
"method": method.Name.Title(),
"package": pkg,
"request": method.InType,
"response": method.OutType,
"hasComment": len(method.Document),
"comment": strings.Join(method.Document, "\r\n"),
}, filename, true)
if err != nil {
return err
}
}
}
return nil
}

@ -0,0 +1,95 @@
package gogen
import (
"fmt"
"path/filepath"
"strings"
"github.com/tal-tech/go-zero/core/collection"
"github.com/tal-tech/go-zero/tools/goctl/rpc/parser"
"github.com/tal-tech/go-zero/tools/goctl/util"
)
var (
logicTemplate = `package logic
import (
"context"
{{.imports}}
"github.com/tal-tech/go-zero/core/logx"
)
type (
{{.logicName}} struct {
ctx context.Context
logx.Logger
// todo: add your logic here and delete this line
}
)
func New{{.logicName}}(ctx context.Context,svcCtx *svc.ServiceContext) *{{.logicName}} {
return &{{.logicName}}{
ctx: ctx,
Logger: logx.WithContext(ctx),
// todo: add your logic here and delete this line
}
}
{{.functions}}
`
logicFunctionTemplate = `{{if .hasComment}}{{.comment}}{{end}}
func (l *{{.logicName}}) {{.method}} (in *{{.package}}.{{.request}}) (*{{.package}}.{{.response}}, error) {
var resp {{.package}}.{{.response}}
// todo: add your logic here and delete this line
return &resp,nil
}
`
)
func (g *defaultRpcGenerator) genLogic() error {
logicPath := g.dirM[dirLogic]
protoPkg := g.ast.Package
service := g.ast.Service
for _, item := range service {
for _, method := range item.Funcs {
logicName := fmt.Sprintf("%slogic.go", method.Name.Lower())
filename := filepath.Join(logicPath, logicName)
functions, err := genLogicFunction(protoPkg, method)
if err != nil {
return err
}
imports := collection.NewSet()
pbImport := fmt.Sprintf(`%v "%v"`, protoPkg, g.mustGetPackage(dirPb))
svcImport := fmt.Sprintf(`"%v"`, g.mustGetPackage(dirSvc))
imports.AddStr(pbImport, svcImport)
err = util.With("logic").GoFmt(true).Parse(logicTemplate).SaveTo(map[string]interface{}{
"logicName": fmt.Sprintf("%sLogic", method.Name.Title()),
"functions": functions,
"imports": strings.Join(imports.KeysStr(), "\r\n"),
}, filename, false)
if err != nil {
return err
}
}
}
return nil
}
func genLogicFunction(packageName string, method *parser.Func) (string, error) {
var functions = make([]string, 0)
buffer, err := util.With("fun").Parse(logicFunctionTemplate).Execute(map[string]interface{}{
"logicName": fmt.Sprintf("%sLogic", method.Name.Title()),
"method": method.Name.Title(),
"package": packageName,
"request": method.InType,
"response": method.OutType,
"hasComment": len(method.Document) > 0,
"comment": strings.Join(method.Document, "\r\n"),
})
if err != nil {
return "", err
}
functions = append(functions, buffer.String())
return strings.Join(functions, "\n"), nil
}

@ -0,0 +1,84 @@
package gogen
import (
"fmt"
"path/filepath"
"strings"
"github.com/tal-tech/go-zero/tools/goctl/rpc/parser"
"github.com/tal-tech/go-zero/tools/goctl/util"
)
var mainTemplate = `{{.head}}
package main
import (
"flag"
"fmt"
"log"
"google.golang.org/grpc"
"github.com/tal-tech/go-zero/core/conf"
"github.com/tal-tech/go-zero/rpcx"
{{.imports}}
)
var configFile = flag.String("f", "etc/{{.serviceName}}.json", "the config file")
func main() {
flag.Parse()
var c config.Config
conf.MustLoad(*configFile, &c)
ctx := svc.NewServiceContext(c)
{{.srv}}
s, err := rpcx.NewServer(c.RpcServerConf, func(grpcServer *grpc.Server) {
{{.registers}}
})
if err != nil {
log.Fatal(err)
}
fmt.Printf("Starting rpc server at %s...\n", c.ListenOn)
s.Start()
}
`
func (g *defaultRpcGenerator) genMain() error {
mainPath := g.dirM[dirTarget]
file := g.ast
pkg := file.Package
fileName := filepath.Join(mainPath, fmt.Sprintf("%v.go", g.Ctx.ServiceName.Lower()))
imports := make([]string, 0)
pbImport := fmt.Sprintf(`%v "%v"`, pkg, g.mustGetPackage(dirPb))
svcImport := fmt.Sprintf(`"%v"`, g.mustGetPackage(dirSvc))
remoteImport := fmt.Sprintf(`"%v"`, g.mustGetPackage(dirHandler))
configImport := fmt.Sprintf(`"%v"`, g.mustGetPackage(dirConfig))
imports = append(imports, configImport, pbImport, remoteImport, svcImport)
srv, registers := g.genServer(pkg, file.Service)
head := util.GetHead(g.Ctx.ProtoSource)
return util.With("main").GoFmt(true).Parse(mainTemplate).SaveTo(map[string]interface{}{
"head": head,
"package": pkg,
"serviceName": g.Ctx.ServiceName.Lower(),
"srv": srv,
"registers": registers,
"imports": strings.Join(imports, "\r\n"),
}, fileName, true)
}
func (g *defaultRpcGenerator) genServer(pkg string, list []*parser.RpcService) (string, string) {
list1 := make([]string, 0)
list2 := make([]string, 0)
for _, item := range list {
name := item.Name.UnTitle()
list1 = append(list1, fmt.Sprintf("%sSrv := handler.New%sServer(ctx)", name, item.Name.Title()))
list2 = append(list2, fmt.Sprintf("%s.Register%sServer(grpcServer, %sSrv)", pkg, item.Name.Title(), name))
}
return strings.Join(list1, "\n"), strings.Join(list2, "\n")
}

@ -0,0 +1,85 @@
package gogen
import (
"errors"
"fmt"
"io/ioutil"
"path/filepath"
"strings"
"github.com/dsymonds/gotoc/parser"
"github.com/tal-tech/go-zero/core/lang"
"github.com/tal-tech/go-zero/tools/goctl/rpc/execx"
astParser "github.com/tal-tech/go-zero/tools/goctl/rpc/parser"
"github.com/tal-tech/go-zero/tools/goctl/util/stringx"
)
func (g *defaultRpcGenerator) genPb() error {
importPath, filename := filepath.Split(g.Ctx.ProtoFileSrc)
tree, err := parser.ParseFiles([]string{filename}, []string{importPath})
if err != nil {
return err
}
if len(tree.Files) == 0 {
return errors.New("proto ast parse failed")
}
file := tree.Files[0]
if len(file.Package) == 0 {
return errors.New("expected package, but nothing found")
}
targetStruct := make(map[string]lang.PlaceholderType)
for _, item := range file.Messages {
if len(item.Messages) > 0 {
return fmt.Errorf(`line %v: unexpected inner message near: "%v""`, item.Messages[0].Position.Line, item.Messages[0].Name)
}
name := stringx.From(item.Name)
if _, ok := targetStruct[name.Lower()]; ok {
return fmt.Errorf("line %v: duplicate %v", item.Position.Line, name)
}
targetStruct[name.Lower()] = lang.Placeholder
}
pbPath := g.dirM[dirPb]
protoFileName := filepath.Base(g.Ctx.ProtoFileSrc)
err = g.protocGenGo(pbPath)
if err != nil {
return err
}
pbGo := strings.TrimSuffix(protoFileName, ".proto") + ".pb.go"
pbFile := filepath.Join(pbPath, pbGo)
bts, err := ioutil.ReadFile(pbFile)
if err != nil {
return err
}
aspParser := astParser.NewAstParser(bts, targetStruct, g.Ctx.Console)
ast, err := aspParser.Parse()
if err != nil {
return err
}
if len(ast.Service) == 0 {
return fmt.Errorf("service not found")
}
g.ast = ast
return nil
}
func (g *defaultRpcGenerator) protocGenGo(target string) error {
src := filepath.Dir(g.Ctx.ProtoFileSrc)
sh := fmt.Sprintf(`export PATH=%s:$PATH
protoc -I=%s --go_out=plugins=grpc:%s %s`, filepath.Join(g.Ctx.GoPath, "bin"), src, target, g.Ctx.ProtoFileSrc)
stdout, err := execx.Run(sh)
if err != nil {
return err
}
g.Ctx.Info(stdout)
return nil
}

@ -0,0 +1,216 @@
package gogen
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"github.com/tal-tech/go-zero/tools/goctl/rpc/execx"
"github.com/tal-tech/go-zero/tools/goctl/rpc/parser"
"github.com/tal-tech/go-zero/tools/goctl/util"
)
var (
sharedTemplateText = `{{.head}}
//go:generate mockgen -destination ./mock{{.name}}model.go -package {{.filePackage}} -source $GOFILE
package {{.filePackage}}
import (
"context"
{{.package}}
"github.com/tal-tech/go-zero/core/jsonx"
"github.com/tal-tech/go-zero/rpcx"
)
type (
{{.serviceName}}Model interface {
{{.interface}}
}
default{{.serviceName}}Model struct {
cli rpcx.Client
}
)
func NewDefault{{.serviceName}}Model(cli rpcx.Client) {{.serviceName}}Model {
return &default{{.serviceName}}Model{
cli: cli,
}
}
{{.functions}}
`
sharedTemplateTypes = `{{.head}}
package {{.filePackage}}
import (
"errors"
)
var (
errJsonConvert = errors.New("json convert error")
)
{{.types}}
`
sharedInterfaceFunctionTemplate = `{{if .hasComment}}{{.comment}}
{{end}}{{.method}}(ctx context.Context,in *{{.pbRequest}}) {{if .hasResponse}}(*{{.pbResponse}},{{end}} error{{if .hasResponse}}){{end}}`
sharedFunctionTemplate = `
{{if .hasComment}}{{.comment}}{{end}}
func (m *default{{.rpcServiceName}}Model) {{.method}}(ctx context.Context,in *{{.pbRequest}}) {{if .hasResponse}}(*{{.pbResponse}},{{end}} error{{if .hasResponse}}){{end}} {
conn:= m.cli.Conn()
client := {{.package}}.New{{.rpcServiceName}}Client(conn)
var request {{.package}}.{{.pbRequest}}
bts, err := jsonx.Marshal(in)
if err != nil {
return {{if .hasResponse}}nil,{{end}}errJsonConvert
}
err = jsonx.Unmarshal(bts, &request)
if err != nil {
return {{if .hasResponse}}nil,{{end}}errJsonConvert
}
{{if .hasResponse}}resp,err:={{else}}_,err={{end}}client.{{.method}}(ctx, &request)
{{if .hasResponse}}if err!=nil{
return nil,err
}
var ret {{.pbResponse}}
bts,err=jsonx.Marshal(resp)
if err!=nil{
return nil,errJsonConvert
}
err=jsonx.Unmarshal(bts,&ret)
if err!=nil{
return nil,errJsonConvert
}
return &ret, nil{{else}}if err!=nil {
return err
}
return nil{{end}}
}`
)
func (g *defaultRpcGenerator) genShared() error {
sharePackage := filepath.Base(g.Ctx.SharedDir)
file := g.ast
typeCode, err := file.GenTypesCode()
if err != nil {
return err
}
pbPkg := file.Package
remotePackage := fmt.Sprintf(`%v "%v"`, pbPkg, g.mustGetPackage(dirPb))
filename := filepath.Join(g.Ctx.SharedDir, "types.go")
head := util.GetHead(g.Ctx.ProtoSource)
err = util.With("types").GoFmt(true).Parse(sharedTemplateTypes).SaveTo(map[string]interface{}{
"head": head,
"filePackage": sharePackage,
"pbPkg": pbPkg,
"serviceName": g.Ctx.ServiceName.Title(),
"lowerStartServiceName": g.Ctx.ServiceName.UnTitle(),
"types": typeCode,
}, filename, true)
for _, service := range file.Service {
filename := filepath.Join(g.Ctx.SharedDir, fmt.Sprintf("%smodel.go", service.Name.Lower()))
functions, err := g.getFuncs(service)
if err != nil {
return err
}
iFunctions, err := g.getInterfaceFuncs(service)
if err != nil {
return err
}
mockFile := filepath.Join(g.Ctx.SharedDir, fmt.Sprintf("mock%smodel.go", service.Name.Lower()))
os.Remove(mockFile)
err = util.With("shared").GoFmt(true).Parse(sharedTemplateText).SaveTo(map[string]interface{}{
"name": service.Name.Lower(),
"head": head,
"filePackage": sharePackage,
"pbPkg": pbPkg,
"package": remotePackage,
"serviceName": service.Name.Title(),
"functions": strings.Join(functions, "\n"),
"interface": strings.Join(iFunctions, "\n"),
}, filename, true)
if err != nil {
return err
}
}
// if mockgen is already installed, it will generate code of gomock for shared files
_, err = exec.LookPath("mockgen")
if err != nil {
g.Ctx.Warning("warning:mockgen is not found")
} else {
execx.Run(fmt.Sprintf("cd %s \ngo generate", g.Ctx.SharedDir))
}
return nil
}
func (g *defaultRpcGenerator) getFuncs(service *parser.RpcService) ([]string, error) {
file := g.ast
pkgName := file.Package
functions := make([]string, 0)
for _, method := range service.Funcs {
data, found := file.Strcuts[strings.ToLower(method.OutType)]
if found {
found = len(data.Field) > 0
}
var comment string
if len(method.Document) > 0 {
comment = method.Document[0]
}
buffer, err := util.With("sharedFn").Parse(sharedFunctionTemplate).Execute(map[string]interface{}{
"rpcServiceName": service.Name.Title(),
"method": method.Name.Title(),
"package": pkgName,
"pbRequest": method.InType,
"pbResponse": method.OutType,
"hasResponse": found,
"hasComment": len(method.Document) > 0,
"comment": comment,
})
if err != nil {
return nil, err
}
functions = append(functions, buffer.String())
}
return functions, nil
}
func (g *defaultRpcGenerator) getInterfaceFuncs(service *parser.RpcService) ([]string, error) {
file := g.ast
functions := make([]string, 0)
for _, method := range service.Funcs {
data, found := file.Strcuts[strings.ToLower(method.OutType)]
if found {
found = len(data.Field) > 0
}
var comment string
if len(method.Document) > 0 {
comment = method.Document[0]
}
buffer, err := util.With("interfaceFn").Parse(sharedInterfaceFunctionTemplate).Execute(map[string]interface{}{
"hasComment": len(method.Document) > 0,
"comment": comment,
"method": method.Name.Title(),
"pbRequest": method.InType,
"pbResponse": method.OutType,
"hasResponse": found,
})
if err != nil {
return nil, err
}
functions = append(functions, buffer.String())
}
return functions, nil
}

@ -0,0 +1,34 @@
package gogen
import (
"fmt"
"path/filepath"
"github.com/tal-tech/go-zero/tools/goctl/util"
)
var svcTemplate = `package svc
import {{.imports}}
type (
ServiceContext struct {
c config.Config
// todo: add your logic here and delete this line
}
)
func NewServiceContext(c config.Config) *ServiceContext {
return &ServiceContext{
c:c,
}
}
`
func (g *defaultRpcGenerator) genSvc() error {
svcPath := g.dirM[dirSvc]
fileName := filepath.Join(svcPath, fileServiceContext)
return util.With("svc").GoFmt(true).Parse(svcTemplate).SaveTo(map[string]interface{}{
"imports": fmt.Sprintf(`"%v"`, g.mustGetPackage(dirConfig)),
}, fileName, false)
}

@ -0,0 +1,44 @@
package gogen
import (
"github.com/tal-tech/go-zero/tools/goctl/util"
"github.com/tal-tech/go-zero/tools/goctl/util/console"
)
var rpcTemplateText = `syntax = "proto3";
package remoteuser;
message Request {
string username = 1;
string password = 2;
}
message Response {
string name = 1;
string gender = 2;
}
service User{
rpc Login(Request)returns(Response);
}`
type (
rpcTemplate struct {
out string
console.Console
}
)
func NewRpcTemplate(out string, idea bool) *rpcTemplate {
return &rpcTemplate{
out: out,
Console: console.NewConsole(idea),
}
}
func (r *rpcTemplate) MustGenerate() {
err := util.With("t").Parse(rpcTemplateText).SaveTo(nil, r.out, false)
r.Must(err)
r.Success("Done.")
}

@ -0,0 +1,483 @@
package parser
import (
"errors"
"fmt"
"go/ast"
"go/parser"
"go/token"
"sort"
"strings"
"github.com/tal-tech/go-zero/core/lang"
sx "github.com/tal-tech/go-zero/core/stringx"
"github.com/tal-tech/go-zero/tools/goctl/util"
"github.com/tal-tech/go-zero/tools/goctl/util/console"
"github.com/tal-tech/go-zero/tools/goctl/util/stringx"
)
const (
flagStar = "*"
suffixServer = "Server"
referenceContext = "context."
unknownPrefix = "XXX_"
ignoreJsonTagExpression = `json:"-"`
)
var (
errorParseError = errors.New("pb parse error")
typeTemplate = `type (
{{.types}}
)`
structTemplate = `{{if .type}}type {{end}}{{.name}} struct {
{{.fields}}
}`
fieldTemplate = `{{if .hasDoc}}{{.doc}}
{{end}}{{.name}} {{.type}} {{.tag}}{{if .hasComment}}{{.comment}}{{end}}`
objectM = make(map[string]*Struct)
)
type (
astParser struct {
golang []byte
filterStruct map[string]lang.PlaceholderType
console.Console
fileSet *token.FileSet
}
Field struct {
Name stringx.String
TypeName string
JsonTag string
Document []string
Comment []string
}
Struct struct {
Name stringx.String
Document []string
Comment []string
Field []*Field
}
Func struct {
Name stringx.String
InType string
InTypeName string // remove *Context,such as LoginRequest、UserRequest
OutTypeName string // remove *Context
OutType string
Document []string
}
RpcService struct {
Name stringx.String
Funcs []*Func
}
// parsing for rpc
PbAst struct {
Package string
// external reference
Imports map[string]string
Strcuts map[string]*Struct
// rpc server's functions,not all functions
Service []*RpcService
}
)
func NewAstParser(golang []byte, filterStruct map[string]lang.PlaceholderType, log console.Console) *astParser {
return &astParser{
golang: golang,
filterStruct: filterStruct,
Console: log,
fileSet: token.NewFileSet(),
}
}
func (a *astParser) Parse() (*PbAst, error) {
fSet := a.fileSet
f, err := parser.ParseFile(fSet, "", a.golang, parser.ParseComments)
if err != nil {
return nil, err
}
commentMap := ast.NewCommentMap(fSet, f, f.Comments)
f.Comments = commentMap.Filter(f).Comments()
var pbAst PbAst
pbAst.Package = a.mustGetIndentName(f.Name)
imports := make(map[string]string)
for _, item := range f.Imports {
if item == nil {
continue
}
if item.Path == nil {
continue
}
key := a.mustGetIndentName(item.Name)
value := item.Path.Value
imports[key] = value
}
structs, funcs := a.mustScope(f.Scope)
pbAst.Imports = imports
pbAst.Strcuts = structs
pbAst.Service = funcs
return &pbAst, nil
}
func (a *astParser) mustScope(scope *ast.Scope) (map[string]*Struct, []*RpcService) {
if scope == nil {
return nil, nil
}
objects := scope.Objects
structs := make(map[string]*Struct)
serviceList := make([]*RpcService, 0)
for name, obj := range objects {
decl := obj.Decl
if decl == nil {
continue
}
typeSpec, ok := decl.(*ast.TypeSpec)
if !ok {
continue
}
tp := typeSpec.Type
switch v := tp.(type) {
case *ast.StructType:
st, err := a.parseObject(name, v)
a.Must(err)
structs[st.Name.Lower()] = st
case *ast.InterfaceType:
if !strings.HasSuffix(name, suffixServer) {
continue
}
list := a.mustServerFunctions(v)
serviceList = append(serviceList, &RpcService{
Name: stringx.From(strings.TrimSuffix(name, suffixServer)),
Funcs: list,
})
}
}
targetStruct := make(map[string]*Struct)
for st := range a.filterStruct {
lower := strings.ToLower(st)
targetStruct[lower] = structs[lower]
}
return targetStruct, serviceList
}
func (a *astParser) mustServerFunctions(v *ast.InterfaceType) []*Func {
funcs := make([]*Func, 0)
methodObject := v.Methods
if methodObject == nil {
return nil
}
for _, method := range methodObject.List {
var item Func
name := a.mustGetIndentName(method.Names[0])
doc := a.parseCommentOrDoc(method.Doc)
item.Name = stringx.From(name)
item.Document = doc
types := method.Type
if types == nil {
funcs = append(funcs, &item)
continue
}
v, ok := types.(*ast.FuncType)
if !ok {
continue
}
params := v.Params
if params != nil {
inList, err := a.parseFields(params.List, true)
a.Must(err)
for _, data := range inList {
if strings.HasPrefix(data.TypeName, referenceContext) {
continue
}
// currently,does not support external references
item.InTypeName = data.TypeName
item.InType = strings.TrimPrefix(data.TypeName, flagStar)
break
}
}
results := v.Results
if results != nil {
outList, err := a.parseFields(results.List, true)
a.Must(err)
for _, data := range outList {
if strings.HasPrefix(data.TypeName, referenceContext) {
continue
}
// currently,does not support external references
item.OutTypeName = data.TypeName
item.OutType = strings.TrimPrefix(data.TypeName, flagStar)
break
}
}
funcs = append(funcs, &item)
}
return funcs
}
func (a *astParser) parseObject(structName string, tp *ast.StructType) (*Struct, error) {
if data, ok := objectM[structName]; ok {
return data, nil
}
var st Struct
st.Name = stringx.From(structName)
if tp == nil {
return &st, nil
}
fields := tp.Fields
if fields == nil {
objectM[structName] = &st
return &st, nil
}
fieldList := fields.List
members, err := a.parseFields(fieldList, false)
if err != nil {
return nil, err
}
for _, m := range members {
var field Field
field.Name = m.Name
field.TypeName = m.TypeName
field.JsonTag = m.JsonTag
field.Document = m.Document
field.Comment = m.Comment
st.Field = append(st.Field, &field)
}
objectM[structName] = &st
return &st, nil
}
func (a *astParser) parseFields(fields []*ast.Field, onlyType bool) ([]*Field, error) {
ret := make([]*Field, 0)
for _, field := range fields {
var item Field
tag := a.parseTag(field.Tag)
if tag == "" && !onlyType {
continue
}
if tag == ignoreJsonTagExpression {
continue
}
item.JsonTag = tag
name := a.parseName(field.Names)
if strings.HasPrefix(name, unknownPrefix) {
continue
}
item.Name = stringx.From(name)
typeName, err := a.parseType(field.Type)
if err != nil {
return nil, err
}
item.TypeName = typeName
if onlyType {
ret = append(ret, &item)
continue
}
docs := a.parseCommentOrDoc(field.Doc)
comments := a.parseCommentOrDoc(field.Comment)
item.Document = docs
item.Comment = comments
isInline := name == ""
if isInline {
return nil, a.wrapError(field.Pos(), "unexpected inline type:%s", name)
}
ret = append(ret, &item)
}
return ret, nil
}
func (a *astParser) parseTag(basicLit *ast.BasicLit) string {
if basicLit == nil {
return ""
}
value := basicLit.Value
splits := strings.Split(value, " ")
if len(splits) == 1 {
return fmt.Sprintf("`%s`", strings.ReplaceAll(splits[0], "`", ""))
} else {
return fmt.Sprintf("`%s`", strings.ReplaceAll(splits[1], "`", ""))
}
}
// returns
// resp1:type's string expression,like int、string、[]int64、map[string]User、*User
// resp2:error
func (a *astParser) parseType(expr ast.Expr) (string, error) {
if expr == nil {
return "", errorParseError
}
switch v := expr.(type) {
case *ast.StarExpr:
stringExpr, err := a.parseType(v.X)
if err != nil {
return "", err
}
e := fmt.Sprintf("*%s", stringExpr)
return e, nil
case *ast.Ident:
return a.mustGetIndentName(v), nil
case *ast.MapType:
keyStringExpr, err := a.parseType(v.Key)
if err != nil {
return "", err
}
valueStringExpr, err := a.parseType(v.Value)
if err != nil {
return "", err
}
e := fmt.Sprintf("map[%s]%s", keyStringExpr, valueStringExpr)
return e, nil
case *ast.ArrayType:
stringExpr, err := a.parseType(v.Elt)
if err != nil {
return "", err
}
e := fmt.Sprintf("[]%s", stringExpr)
return e, nil
case *ast.InterfaceType:
return "interface{}", nil
case *ast.SelectorExpr:
join := make([]string, 0)
xIdent, ok := v.X.(*ast.Ident)
xIndentName := a.mustGetIndentName(xIdent)
if ok {
join = append(join, xIndentName)
}
sel := v.Sel
join = append(join, a.mustGetIndentName(sel))
return strings.Join(join, "."), nil
case *ast.ChanType:
return "", a.wrapError(v.Pos(), "unexpected type 'chan'")
case *ast.FuncType:
return "", a.wrapError(v.Pos(), "unexpected type 'func'")
case *ast.StructType:
return "", a.wrapError(v.Pos(), "unexpected inline struct type")
default:
return "", a.wrapError(v.Pos(), "unexpected type '%v'", v)
}
}
func (a *astParser) parseName(names []*ast.Ident) string {
if len(names) == 0 {
return ""
}
name := names[0]
return a.mustGetIndentName(name)
}
func (a *astParser) parseCommentOrDoc(cg *ast.CommentGroup) []string {
if cg == nil {
return nil
}
comments := make([]string, 0)
for _, comment := range cg.List {
if comment == nil {
continue
}
text := strings.TrimSpace(comment.Text)
if text == "" {
continue
}
comments = append(comments, text)
}
return comments
}
func (a *astParser) mustGetIndentName(ident *ast.Ident) string {
if ident == nil {
return ""
}
return ident.Name
}
func (a *astParser) wrapError(pos token.Pos, format string, arg ...interface{}) error {
file := a.fileSet.Position(pos)
return fmt.Errorf("line %v: %s", file.Line, fmt.Sprintf(format, arg...))
}
func (a *PbAst) GenTypesCode() (string, error) {
types := make([]string, 0)
sts := make([]*Struct, 0)
for _, item := range a.Strcuts {
sts = append(sts, item)
}
sort.Slice(sts, func(i, j int) bool {
return sts[i].Name.Source() < sts[j].Name.Source()
})
for _, s := range sts {
structCode, err := s.genCode(false)
if err != nil {
return "", err
}
if structCode == "" {
continue
}
types = append(types, structCode)
}
buffer, err := util.With("type").Parse(typeTemplate).Execute(map[string]interface{}{
"types": strings.Join(types, "\n"),
})
if err != nil {
return "", err
}
return buffer.String(), nil
}
func (s *Struct) genCode(containsTypeStatement bool) (string, error) {
if len(s.Field) == 0 {
return "", nil
}
fields := make([]string, 0)
for _, f := range s.Field {
var comment, doc string
if len(f.Comment) > 0 {
comment = f.Comment[0]
}
doc = strings.Join(f.Document, "\n")
buffer, err := util.With(sx.Rand()).Parse(fieldTemplate).Execute(map[string]interface{}{
"name": f.Name.Title(),
"type": f.TypeName,
"tag": f.JsonTag,
"hasDoc": len(f.Document) > 0,
"doc": doc,
"hasComment": len(f.Comment) > 0,
"comment": comment,
})
if err != nil {
return "", err
}
fields = append(fields, buffer.String())
}
buffer, err := util.With("struct").Parse(structTemplate).Execute(map[string]interface{}{
"type": containsTypeStatement,
"name": s.Name.Title(),
"fields": strings.Join(fields, "\n"),
})
if err != nil {
return "", err
}
return buffer.String(), nil
}

@ -2,6 +2,7 @@ package console
import (
"fmt"
"os"
"github.com/logrusorgru/aurora"
)
@ -9,8 +10,11 @@ import (
type (
Console interface {
Success(format string, a ...interface{})
Info(format string, a ...interface{})
Warning(format string, a ...interface{})
Error(format string, a ...interface{})
Fatalln(format string, a ...interface{})
Must(err error)
}
colorConsole struct {
}
@ -30,6 +34,11 @@ func NewColorConsole() *colorConsole {
return &colorConsole{}
}
func (c *colorConsole) Info(format string, a ...interface{}) {
msg := fmt.Sprintf(format, a...)
fmt.Println(msg)
}
func (c *colorConsole) Success(format string, a ...interface{}) {
msg := fmt.Sprintf(format, a...)
fmt.Println(aurora.Green(msg))
@ -45,10 +54,26 @@ func (c *colorConsole) Error(format string, a ...interface{}) {
fmt.Println(aurora.Red(msg))
}
func (c *colorConsole) Fatalln(format string, a ...interface{}) {
c.Error(format, a...)
os.Exit(1)
}
func (c *colorConsole) Must(err error) {
if err != nil {
c.Fatalln("%+v", err)
}
}
func NewIdeaConsole() *ideaConsole {
return &ideaConsole{}
}
func (i *ideaConsole) Info(format string, a ...interface{}) {
msg := fmt.Sprintf(format, a...)
fmt.Println(msg)
}
func (i *ideaConsole) Success(format string, a ...interface{}) {
msg := fmt.Sprintf(format, a...)
fmt.Println("[SUCCESS]: ", msg)
@ -63,3 +88,14 @@ func (i *ideaConsole) Error(format string, a ...interface{}) {
msg := fmt.Sprintf(format, a...)
fmt.Println("[ERROR]: ", msg)
}
func (i *ideaConsole) Fatalln(format string, a ...interface{}) {
i.Error(format, a...)
os.Exit(1)
}
func (i *ideaConsole) Must(err error) {
if err != nil {
i.Fatalln("%+v", err)
}
}

@ -2,18 +2,12 @@ package util
import (
"bufio"
"bytes"
"fmt"
"go/format"
"io/ioutil"
"os"
"path/filepath"
"strings"
"text/template"
"time"
"github.com/logrusorgru/aurora"
"github.com/tal-tech/go-zero/core/logx"
)
func CreateIfNotExist(file string) (*os.File, error) {
@ -53,44 +47,3 @@ func FileExists(file string) bool {
func FileNameWithoutExt(file string) string {
return strings.TrimSuffix(file, filepath.Ext(file))
}
func CreateTemplateAndExecute(filename, text string, arg map[string]interface{}, forceUpdate bool, disableFormatCodeArgs ...bool) error {
if FileExists(filename) && !forceUpdate {
return nil
}
var buffer = new(bytes.Buffer)
templateName := fmt.Sprintf("%d", time.Now().UnixNano())
t, err := template.New(templateName).Parse(text)
if err != nil {
return err
}
err = t.Execute(buffer, arg)
if err != nil {
return err
}
var disableFormatCode bool
for _, f := range disableFormatCodeArgs {
disableFormatCode = f
}
var bts = buffer.Bytes()
s := buffer.String()
logx.Info(s)
if !disableFormatCode {
bts, err = format.Source(buffer.Bytes())
if err != nil {
return err
}
}
return ioutil.WriteFile(filename, bts, os.ModePerm)
}
func FormatCodeAndWrite(filename string, code []byte) error {
if FileExists(filename) {
return nil
}
bts, err := format.Source(code)
if err != nil {
return err
}
return ioutil.WriteFile(filename, bts, os.ModePerm)
}

@ -0,0 +1,11 @@
package util
var headTemplate = `// Code generated by goctl. DO NOT EDIT.
// Source: {{.source}}`
func GetHead(source string) string {
buffer, _ := With("head").Parse(headTemplate).Execute(map[string]interface{}{
"source": source,
})
return buffer.String()
}

@ -1,4 +1,4 @@
package templatex
package util
import (
"bytes"
@ -32,7 +32,10 @@ func (t *defaultTemplate) GoFmt(format bool) *defaultTemplate {
return t
}
func (t *defaultTemplate) SaveTo(data interface{}, path string) error {
func (t *defaultTemplate) SaveTo(data interface{}, path string, forceUpdate bool) error {
if FileExists(path) && !forceUpdate {
return nil
}
output, err := t.execute(data)
if err != nil {
return err
Loading…
Cancel
Save