From db161150377af090dac7b3d1812bc0681a4a88ee Mon Sep 17 00:00:00 2001 From: Keson Date: Fri, 28 Aug 2020 19:24:58 +0800 Subject: [PATCH] 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 --- go.mod | 4 +- go.sum | 4 +- tools/goctl/goctl.go | 58 ++- tools/goctl/model/sql/gen/delete.go | 4 +- tools/goctl/model/sql/gen/field.go | 4 +- tools/goctl/model/sql/gen/findone.go | 4 +- tools/goctl/model/sql/gen/fineonebyfield.go | 4 +- tools/goctl/model/sql/gen/gen.go | 3 +- tools/goctl/model/sql/gen/insert.go | 4 +- tools/goctl/model/sql/gen/new.go | 4 +- tools/goctl/model/sql/gen/tag.go | 4 +- tools/goctl/model/sql/gen/types.go | 4 +- tools/goctl/model/sql/gen/update.go | 4 +- tools/goctl/model/sql/gen/vars.go | 4 +- tools/goctl/rpc/CHANGELOG.md | 6 + tools/goctl/rpc/README.md | 161 ++++++ tools/goctl/rpc/command/command.go | 23 + tools/goctl/rpc/ctx/ctx.go | 111 ++++ tools/goctl/rpc/ctx/project.go | 208 ++++++++ tools/goctl/rpc/execx/execx.go | 34 ++ tools/goctl/rpc/goen/gen.go | 89 ++++ tools/goctl/rpc/goen/genconfig.go | 29 ++ tools/goctl/rpc/goen/gendir.go | 45 ++ tools/goctl/rpc/goen/genetc.go | 34 ++ tools/goctl/rpc/goen/genhandler.go | 109 ++++ tools/goctl/rpc/goen/genlogic.go | 95 ++++ tools/goctl/rpc/goen/genmain.go | 84 +++ tools/goctl/rpc/goen/genpb.go | 85 +++ tools/goctl/rpc/goen/genshared.go | 216 ++++++++ tools/goctl/rpc/goen/gensvc.go | 34 ++ tools/goctl/rpc/goen/template.go | 44 ++ tools/goctl/rpc/parser/pbast.go | 483 ++++++++++++++++++ tools/goctl/util/console/console.go | 36 ++ tools/goctl/util/file.go | 47 -- tools/goctl/util/head.go | 11 + tools/goctl/util/{templatex => }/templatex.go | 7 +- 36 files changed, 2021 insertions(+), 79 deletions(-) create mode 100644 tools/goctl/rpc/CHANGELOG.md create mode 100644 tools/goctl/rpc/README.md create mode 100644 tools/goctl/rpc/command/command.go create mode 100644 tools/goctl/rpc/ctx/ctx.go create mode 100644 tools/goctl/rpc/ctx/project.go create mode 100644 tools/goctl/rpc/execx/execx.go create mode 100644 tools/goctl/rpc/goen/gen.go create mode 100644 tools/goctl/rpc/goen/genconfig.go create mode 100644 tools/goctl/rpc/goen/gendir.go create mode 100644 tools/goctl/rpc/goen/genetc.go create mode 100644 tools/goctl/rpc/goen/genhandler.go create mode 100644 tools/goctl/rpc/goen/genlogic.go create mode 100644 tools/goctl/rpc/goen/genmain.go create mode 100644 tools/goctl/rpc/goen/genpb.go create mode 100644 tools/goctl/rpc/goen/genshared.go create mode 100644 tools/goctl/rpc/goen/gensvc.go create mode 100644 tools/goctl/rpc/goen/template.go create mode 100644 tools/goctl/rpc/parser/pbast.go create mode 100644 tools/goctl/util/head.go rename tools/goctl/util/{templatex => }/templatex.go (87%) diff --git a/go.mod b/go.mod index 4e008493..2bcf52af 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index 0e11eb70..5eaf60b9 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/tools/goctl/goctl.go b/tools/goctl/goctl.go index 287b35da..63fceb34 100644 --- a/tools/goctl/goctl.go +++ b/tools/goctl/goctl.go @@ -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, }, }, }, diff --git a/tools/goctl/model/sql/gen/delete.go b/tools/goctl/model/sql/gen/delete.go index 0d89d606..29b4fd26 100644 --- a/tools/goctl/model/sql/gen/delete.go +++ b/tools/goctl/model/sql/gen/delete.go @@ -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, diff --git a/tools/goctl/model/sql/gen/field.go b/tools/goctl/model/sql/gen/field.go index 817a5ebd..6c45c2c4 100644 --- a/tools/goctl/model/sql/gen/field.go +++ b/tools/goctl/model/sql/gen/field.go @@ -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(), diff --git a/tools/goctl/model/sql/gen/findone.go b/tools/goctl/model/sql/gen/findone.go index e89e9277..8f48f873 100644 --- a/tools/goctl/model/sql/gen/findone.go +++ b/tools/goctl/model/sql/gen/findone.go @@ -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, diff --git a/tools/goctl/model/sql/gen/fineonebyfield.go b/tools/goctl/model/sql/gen/fineonebyfield.go index 13cddc7b..796ff372 100644 --- a/tools/goctl/model/sql/gen/fineonebyfield.go +++ b/tools/goctl/model/sql/gen/fineonebyfield.go @@ -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 { diff --git a/tools/goctl/model/sql/gen/gen.go b/tools/goctl/model/sql/gen/gen.go index e103ee78..e410ae2e 100644 --- a/tools/goctl/model/sql/gen/gen.go +++ b/tools/goctl/model/sql/gen/gen.go @@ -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) diff --git a/tools/goctl/model/sql/gen/insert.go b/tools/goctl/model/sql/gen/insert.go index a44a9439..bdf055c8 100644 --- a/tools/goctl/model/sql/gen/insert.go +++ b/tools/goctl/model/sql/gen/insert.go @@ -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, diff --git a/tools/goctl/model/sql/gen/new.go b/tools/goctl/model/sql/gen/new.go index b346068f..e5afe0ea 100644 --- a/tools/goctl/model/sql/gen/new.go +++ b/tools/goctl/model/sql/gen/new.go @@ -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, diff --git a/tools/goctl/model/sql/gen/tag.go b/tools/goctl/model/sql/gen/tag.go index 8414a102..86f15c19 100644 --- a/tools/goctl/model/sql/gen/tag.go +++ b/tools/goctl/model/sql/gen/tag.go @@ -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, diff --git a/tools/goctl/model/sql/gen/types.go b/tools/goctl/model/sql/gen/types.go index fc29241f..d6801e6b 100644 --- a/tools/goctl/model/sql/gen/types.go +++ b/tools/goctl/model/sql/gen/types.go @@ -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, diff --git a/tools/goctl/model/sql/gen/update.go b/tools/goctl/model/sql/gen/update.go index 2a8a0a4d..426f8fa8 100644 --- a/tools/goctl/model/sql/gen/update.go +++ b/tools/goctl/model/sql/gen/update.go @@ -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, diff --git a/tools/goctl/model/sql/gen/vars.go b/tools/goctl/model/sql/gen/vars.go index 3d7d200d..3729446e 100644 --- a/tools/goctl/model/sql/gen/vars.go +++ b/tools/goctl/model/sql/gen/vars.go @@ -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{}{ diff --git a/tools/goctl/rpc/CHANGELOG.md b/tools/goctl/rpc/CHANGELOG.md new file mode 100644 index 00000000..4579048b --- /dev/null +++ b/tools/goctl/rpc/CHANGELOG.md @@ -0,0 +1,6 @@ +# Change log + +# 2020-08-27 +* 新增支持rpc模板生成 +* 新增支持rpc服务生成 + diff --git a/tools/goctl/rpc/README.md b/tools/goctl/rpc/README.md new file mode 100644 index 00000000..0d7b1309 --- /dev/null +++ b/tools/goctl/rpc/README.md @@ -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)/shared,rpc 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生成 + + + + + diff --git a/tools/goctl/rpc/command/command.go b/tools/goctl/rpc/command/command.go new file mode 100644 index 00000000..c52cf799 --- /dev/null +++ b/tools/goctl/rpc/command/command.go @@ -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 +} diff --git a/tools/goctl/rpc/ctx/ctx.go b/tools/goctl/rpc/ctx/ctx.go new file mode 100644 index 00000000..f6de0c28 --- /dev/null +++ b/tools/goctl/rpc/ctx/ctx.go @@ -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)) +} diff --git a/tools/goctl/rpc/ctx/project.go b/tools/goctl/rpc/ctx/project.go new file mode 100644 index 00000000..b9620bc6 --- /dev/null +++ b/tools/goctl/rpc/ctx/project.go @@ -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 +} diff --git a/tools/goctl/rpc/execx/execx.go b/tools/goctl/rpc/execx/execx.go new file mode 100644 index 00000000..9d385612 --- /dev/null +++ b/tools/goctl/rpc/execx/execx.go @@ -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 +} diff --git a/tools/goctl/rpc/goen/gen.go b/tools/goctl/rpc/goen/gen.go new file mode 100644 index 00000000..23e60c7d --- /dev/null +++ b/tools/goctl/rpc/goen/gen.go @@ -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 +} diff --git a/tools/goctl/rpc/goen/genconfig.go b/tools/goctl/rpc/goen/genconfig.go new file mode 100644 index 00000000..f80951c7 --- /dev/null +++ b/tools/goctl/rpc/goen/genconfig.go @@ -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) +} diff --git a/tools/goctl/rpc/goen/gendir.go b/tools/goctl/rpc/goen/gendir.go new file mode 100644 index 00000000..cfbeba46 --- /dev/null +++ b/tools/goctl/rpc/goen/gendir.go @@ -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 +} diff --git a/tools/goctl/rpc/goen/genetc.go b/tools/goctl/rpc/goen/genetc.go new file mode 100644 index 00000000..37628068 --- /dev/null +++ b/tools/goctl/rpc/goen/genetc.go @@ -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) +} diff --git a/tools/goctl/rpc/goen/genhandler.go b/tools/goctl/rpc/goen/genhandler.go new file mode 100644 index 00000000..6d8f3055 --- /dev/null +++ b/tools/goctl/rpc/goen/genhandler.go @@ -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 +} diff --git a/tools/goctl/rpc/goen/genlogic.go b/tools/goctl/rpc/goen/genlogic.go new file mode 100644 index 00000000..4bce9b7a --- /dev/null +++ b/tools/goctl/rpc/goen/genlogic.go @@ -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 +} diff --git a/tools/goctl/rpc/goen/genmain.go b/tools/goctl/rpc/goen/genmain.go new file mode 100644 index 00000000..56d774ca --- /dev/null +++ b/tools/goctl/rpc/goen/genmain.go @@ -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") +} diff --git a/tools/goctl/rpc/goen/genpb.go b/tools/goctl/rpc/goen/genpb.go new file mode 100644 index 00000000..5e2bac6b --- /dev/null +++ b/tools/goctl/rpc/goen/genpb.go @@ -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 +} diff --git a/tools/goctl/rpc/goen/genshared.go b/tools/goctl/rpc/goen/genshared.go new file mode 100644 index 00000000..55bd7e01 --- /dev/null +++ b/tools/goctl/rpc/goen/genshared.go @@ -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 +} diff --git a/tools/goctl/rpc/goen/gensvc.go b/tools/goctl/rpc/goen/gensvc.go new file mode 100644 index 00000000..fa94c2ef --- /dev/null +++ b/tools/goctl/rpc/goen/gensvc.go @@ -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) +} diff --git a/tools/goctl/rpc/goen/template.go b/tools/goctl/rpc/goen/template.go new file mode 100644 index 00000000..cefb38ff --- /dev/null +++ b/tools/goctl/rpc/goen/template.go @@ -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.") +} diff --git a/tools/goctl/rpc/parser/pbast.go b/tools/goctl/rpc/parser/pbast.go new file mode 100644 index 00000000..f8e59d4d --- /dev/null +++ b/tools/goctl/rpc/parser/pbast.go @@ -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 +} diff --git a/tools/goctl/util/console/console.go b/tools/goctl/util/console/console.go index fb7b72de..73bd212f 100644 --- a/tools/goctl/util/console/console.go +++ b/tools/goctl/util/console/console.go @@ -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) + } +} diff --git a/tools/goctl/util/file.go b/tools/goctl/util/file.go index 5fdee0c9..3cfe604c 100644 --- a/tools/goctl/util/file.go +++ b/tools/goctl/util/file.go @@ -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) -} diff --git a/tools/goctl/util/head.go b/tools/goctl/util/head.go new file mode 100644 index 00000000..4fc142c2 --- /dev/null +++ b/tools/goctl/util/head.go @@ -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() +} diff --git a/tools/goctl/util/templatex/templatex.go b/tools/goctl/util/templatex.go similarity index 87% rename from tools/goctl/util/templatex/templatex.go rename to tools/goctl/util/templatex.go index e8b448db..0e623f80 100644 --- a/tools/goctl/util/templatex/templatex.go +++ b/tools/goctl/util/templatex.go @@ -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