mirror of
https://github.com/zeromicro/go-zero.git
synced 2026-05-07 06:59:59 +08:00
goctl added
This commit is contained in:
4
tools/goctl/Makefile
Normal file
4
tools/goctl/Makefile
Normal file
@@ -0,0 +1,4 @@
|
||||
version := $(shell /bin/date "+%Y-%m-%d %H:%M")
|
||||
|
||||
build:
|
||||
go build -ldflags="-s -w" -ldflags="-X 'main.BuildTime=$(version)'" goctl.go && upx goctl
|
||||
78
tools/goctl/api/apigen/gen.go
Normal file
78
tools/goctl/api/apigen/gen.go
Normal file
@@ -0,0 +1,78 @@
|
||||
package apigen
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"zero/tools/goctl/util"
|
||||
|
||||
"github.com/logrusorgru/aurora"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
const apiTemplate = `info(
|
||||
title: // TODO: add title
|
||||
desc: // TODO: add description
|
||||
author: {{.gitUser}}
|
||||
email: {{.gitEmail}}
|
||||
)
|
||||
|
||||
type request struct{
|
||||
// TODO: add members here and delete this comment
|
||||
}
|
||||
|
||||
type response struct{
|
||||
// TODO: add members here and delete this comment
|
||||
}
|
||||
|
||||
@server(
|
||||
port: // TODO: add port here and delete this comment
|
||||
)
|
||||
service {{.serviceName}} {
|
||||
@server(
|
||||
handler: // TODO: set handler name and delete this comment
|
||||
)
|
||||
// TODO: edit the below line
|
||||
// get /users/id/:userId(request) returns(response)
|
||||
|
||||
@server(
|
||||
handler: // TODO: set handler name and delete this comment
|
||||
)
|
||||
// TODO: edit the below line
|
||||
// post /users/create(request)
|
||||
}
|
||||
`
|
||||
|
||||
func ApiCommand(c *cli.Context) error {
|
||||
apiFile := c.String("o")
|
||||
if len(apiFile) == 0 {
|
||||
return errors.New("missing -o")
|
||||
}
|
||||
|
||||
fp, err := util.CreateIfNotExist(apiFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer fp.Close()
|
||||
|
||||
baseName := util.FileNameWithoutExt(filepath.Base(apiFile))
|
||||
if strings.HasSuffix(strings.ToLower(baseName), "-api") {
|
||||
baseName = baseName[:len(baseName)-4]
|
||||
} else if strings.HasSuffix(strings.ToLower(baseName), "api") {
|
||||
baseName = baseName[:len(baseName)-3]
|
||||
}
|
||||
t := template.Must(template.New("etcTemplate").Parse(apiTemplate))
|
||||
if err := t.Execute(fp, map[string]string{
|
||||
"gitUser": getGitName(),
|
||||
"gitEmail": getGitEmail(),
|
||||
"serviceName": baseName + "-api",
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println(aurora.Green("Done."))
|
||||
return nil
|
||||
}
|
||||
26
tools/goctl/api/apigen/util.go
Normal file
26
tools/goctl/api/apigen/util.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package apigen
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func getGitName() string {
|
||||
cmd := exec.Command("git", "config", "user.name")
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return strings.TrimSpace(string(out))
|
||||
}
|
||||
|
||||
func getGitEmail() string {
|
||||
cmd := exec.Command("git", "config", "user.email")
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return strings.TrimSpace(string(out))
|
||||
}
|
||||
40
tools/goctl/api/dartgen/gen.go
Normal file
40
tools/goctl/api/dartgen/gen.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package dartgen
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"zero/core/lang"
|
||||
"zero/tools/goctl/api/parser"
|
||||
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
func DartCommand(c *cli.Context) error {
|
||||
apiFile := c.String("api")
|
||||
dir := c.String("dir")
|
||||
if len(apiFile) == 0 {
|
||||
return errors.New("missing -api")
|
||||
}
|
||||
if len(dir) == 0 {
|
||||
return errors.New("missing -dir")
|
||||
}
|
||||
|
||||
p, err := parser.NewParser(apiFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
api, err := p.Parse()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !strings.HasSuffix(dir, "/") {
|
||||
dir = dir + "/"
|
||||
}
|
||||
api.Info.Title = strings.Replace(apiFile, ".api", "", -1)
|
||||
lang.Must(genData(dir+"data/", api))
|
||||
lang.Must(genApi(dir+"api/", api))
|
||||
lang.Must(genVars(dir + "vars/"))
|
||||
return nil
|
||||
}
|
||||
75
tools/goctl/api/dartgen/genapi.go
Normal file
75
tools/goctl/api/dartgen/genapi.go
Normal file
@@ -0,0 +1,75 @@
|
||||
package dartgen
|
||||
|
||||
import (
|
||||
"os"
|
||||
"text/template"
|
||||
|
||||
"zero/core/logx"
|
||||
"zero/tools/goctl/api/spec"
|
||||
)
|
||||
|
||||
const apiTemplate = `import 'api.dart';
|
||||
import '../data/{{with .Info}}{{.Title}}{{end}}.dart';
|
||||
{{with .Service}}
|
||||
/// {{.Name}}
|
||||
{{range .Routes}}
|
||||
/// --{{.Path}}--
|
||||
///
|
||||
/// 请求: {{with .RequestType}}{{.Name}}{{end}}
|
||||
/// 返回: {{with .ResponseType}}{{.Name}}{{end}}
|
||||
Future {{pathToFuncName .Path}}( {{if ne .Method "get"}}{{with .RequestType}}{{.Name}} request,{{end}}{{end}}
|
||||
{Function({{with .ResponseType}}{{.Name}}{{end}}) ok,
|
||||
Function(String) fail,
|
||||
Function eventually}) async {
|
||||
await api{{if eq .Method "get"}}Get{{else}}Post{{end}}('{{.Path}}',{{if ne .Method "get"}}request,{{end}}
|
||||
ok: (data) {
|
||||
if (ok != null) ok({{with .ResponseType}}{{.Name}}{{end}}.fromJson(data));
|
||||
}, fail: fail, eventually: eventually);
|
||||
}
|
||||
{{end}}
|
||||
{{end}}`
|
||||
|
||||
func genApi(dir string, api *spec.ApiSpec) error {
|
||||
e := os.MkdirAll(dir, 0755)
|
||||
if e != nil {
|
||||
logx.Error(e)
|
||||
return e
|
||||
}
|
||||
e = genApiFile(dir)
|
||||
if e != nil {
|
||||
logx.Error(e)
|
||||
return e
|
||||
}
|
||||
|
||||
file, e := os.OpenFile(dir+api.Info.Title+".dart", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
|
||||
if e != nil {
|
||||
logx.Error(e)
|
||||
return e
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
t := template.New("apiTemplate")
|
||||
t = t.Funcs(funcMap)
|
||||
t, e = t.Parse(apiTemplate)
|
||||
if e != nil {
|
||||
logx.Error(e)
|
||||
return e
|
||||
}
|
||||
t.Execute(file, api)
|
||||
return nil
|
||||
}
|
||||
|
||||
func genApiFile(dir string) error {
|
||||
path := dir + "api.dart"
|
||||
if fileExists(path) {
|
||||
return nil
|
||||
}
|
||||
apiFile, e := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
|
||||
if e != nil {
|
||||
logx.Error(e)
|
||||
return e
|
||||
}
|
||||
defer apiFile.Close()
|
||||
apiFile.WriteString(apiFileContent)
|
||||
return nil
|
||||
}
|
||||
79
tools/goctl/api/dartgen/gendata.go
Normal file
79
tools/goctl/api/dartgen/gendata.go
Normal file
@@ -0,0 +1,79 @@
|
||||
package dartgen
|
||||
|
||||
import (
|
||||
"os"
|
||||
"text/template"
|
||||
|
||||
"zero/core/logx"
|
||||
"zero/tools/goctl/api/spec"
|
||||
)
|
||||
|
||||
const dataTemplate = `// --{{with .Info}}{{.Title}}{{end}}--
|
||||
{{ range .Types}}
|
||||
class {{.Name}}{
|
||||
{{range .Members}}
|
||||
/// {{.Comment}}
|
||||
final {{.Type}} {{lowCamelCase .Name}};
|
||||
{{end}}
|
||||
{{.Name}}({ {{range .Members}}
|
||||
this.{{lowCamelCase .Name}},{{end}}
|
||||
});
|
||||
factory {{.Name}}.fromJson(Map<String,dynamic> m) {
|
||||
return {{.Name}}({{range .Members}}
|
||||
{{lowCamelCase .Name}}: {{if isDirectType .Type}}m['{{tagGet .Tag "json"}}']{{else if isClassListType .Type}}(m['{{tagGet .Tag "json"}}'] as List<dynamic>).map((i) => {{getCoreType .Type}}.fromJson(i)){{else}}{{.Type}}.fromJson(m['{{tagGet .Tag "json"}}']){{end}},{{end}}
|
||||
);
|
||||
}
|
||||
Map<String,dynamic> toJson() {
|
||||
return { {{range .Members}}
|
||||
'{{tagGet .Tag "json"}}': {{if isDirectType .Type}}{{lowCamelCase .Name}}{{else if isClassListType .Type}}{{lowCamelCase .Name}}.map((i) => i.toJson()){{else}}{{lowCamelCase .Name}}.toJson(){{end}},{{end}}
|
||||
};
|
||||
}
|
||||
}
|
||||
{{end}}
|
||||
`
|
||||
|
||||
func genData(dir string, api *spec.ApiSpec) error {
|
||||
e := os.MkdirAll(dir, 0755)
|
||||
if e != nil {
|
||||
logx.Error(e)
|
||||
return e
|
||||
}
|
||||
e = genTokens(dir)
|
||||
if e != nil {
|
||||
logx.Error(e)
|
||||
return e
|
||||
}
|
||||
|
||||
file, e := os.OpenFile(dir+api.Info.Title+".dart", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
|
||||
if e != nil {
|
||||
logx.Error(e)
|
||||
return e
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
t := template.New("dataTemplate")
|
||||
t = t.Funcs(funcMap)
|
||||
t, e = t.Parse(dataTemplate)
|
||||
if e != nil {
|
||||
logx.Error(e)
|
||||
return e
|
||||
}
|
||||
|
||||
convertMemberType(api)
|
||||
return t.Execute(file, api)
|
||||
}
|
||||
|
||||
func genTokens(dir string) error {
|
||||
path := dir + "tokens.dart"
|
||||
if fileExists(path) {
|
||||
return nil
|
||||
}
|
||||
tokensFile, e := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
|
||||
if e != nil {
|
||||
logx.Error(e)
|
||||
return e
|
||||
}
|
||||
defer tokensFile.Close()
|
||||
tokensFile.WriteString(tokensFileContent)
|
||||
return nil
|
||||
}
|
||||
66
tools/goctl/api/dartgen/genvars.go
Normal file
66
tools/goctl/api/dartgen/genvars.go
Normal file
@@ -0,0 +1,66 @@
|
||||
package dartgen
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
|
||||
"zero/core/logx"
|
||||
)
|
||||
|
||||
func genVars(dir string) error {
|
||||
e := os.MkdirAll(dir, 0755)
|
||||
if e != nil {
|
||||
logx.Error(e)
|
||||
return e
|
||||
}
|
||||
|
||||
if !fileExists(dir + "vars.dart") {
|
||||
e = ioutil.WriteFile(dir+"vars.dart", []byte(`const serverHost='demo-crm.xiaoheiban.cn';`), 0644)
|
||||
if e != nil {
|
||||
logx.Error(e)
|
||||
return e
|
||||
}
|
||||
}
|
||||
|
||||
if !fileExists(dir + "kv.dart") {
|
||||
e = ioutil.WriteFile(dir+"kv.dart", []byte(`import 'dart:convert';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import '../data/tokens.dart';
|
||||
|
||||
/// 保存tokens到本地
|
||||
///
|
||||
/// 传入null则删除本地tokens
|
||||
/// 返回:true:设置成功 false:设置失败
|
||||
Future<bool> setTokens(Tokens tokens) async {
|
||||
var sp = await SharedPreferences.getInstance();
|
||||
if (tokens == null) {
|
||||
sp.remove('tokens');
|
||||
return true;
|
||||
}
|
||||
return await sp.setString('tokens', jsonEncode(tokens.toJson()));
|
||||
}
|
||||
|
||||
/// 获取本地存储的tokens
|
||||
///
|
||||
/// 如果没有,则返回null
|
||||
Future<Tokens> getTokens() async {
|
||||
try {
|
||||
var sp = await SharedPreferences.getInstance();
|
||||
var str = sp.getString('tokens');
|
||||
if (str.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
return Tokens.fromJson(jsonDecode(str));
|
||||
} catch (e) {
|
||||
print(e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
`), 0644)
|
||||
if e != nil {
|
||||
logx.Error(e)
|
||||
return e
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
118
tools/goctl/api/dartgen/util.go
Normal file
118
tools/goctl/api/dartgen/util.go
Normal file
@@ -0,0 +1,118 @@
|
||||
package dartgen
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"zero/tools/goctl/api/spec"
|
||||
"zero/tools/goctl/api/util"
|
||||
)
|
||||
|
||||
func lowCamelCase(s string) string {
|
||||
if len(s) < 1 {
|
||||
return ""
|
||||
}
|
||||
s = util.ToCamelCase(util.ToSnakeCase(s))
|
||||
return util.ToLower(s[:1]) + s[1:]
|
||||
}
|
||||
|
||||
func pathToFuncName(path string) string {
|
||||
if !strings.HasPrefix(path, "/") {
|
||||
path = "/" + path
|
||||
}
|
||||
if !strings.HasPrefix(path, "/api") {
|
||||
path = "/api" + path
|
||||
}
|
||||
|
||||
path = strings.Replace(path, "/", "_", -1)
|
||||
path = strings.Replace(path, "-", "_", -1)
|
||||
|
||||
camel := util.ToCamelCase(path)
|
||||
return util.ToLower(camel[:1]) + camel[1:]
|
||||
}
|
||||
|
||||
func tagGet(tag, k string) (reflect.Value, error) {
|
||||
v, _ := util.TagLookup(tag, k)
|
||||
out := strings.Split(v, ",")[0]
|
||||
return reflect.ValueOf(out), nil
|
||||
}
|
||||
|
||||
func convertMemberType(api *spec.ApiSpec) {
|
||||
for i, t := range api.Types {
|
||||
for j, mem := range t.Members {
|
||||
api.Types[i].Members[j].Type = goTypeToDart(mem.Type)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func goTypeToDart(t string) string {
|
||||
t = strings.Replace(t, "*", "", -1)
|
||||
if strings.HasPrefix(t, "[]") {
|
||||
return "List<" + goTypeToDart(t[2:]) + ">"
|
||||
}
|
||||
|
||||
if strings.HasPrefix(t, "map") {
|
||||
tys, e := util.DecomposeType(t)
|
||||
if e != nil {
|
||||
log.Fatal(e)
|
||||
}
|
||||
|
||||
if len(tys) != 2 {
|
||||
log.Fatal("Map type number !=2")
|
||||
}
|
||||
|
||||
return "Map<String," + goTypeToDart(tys[1]) + ">"
|
||||
}
|
||||
|
||||
switch t {
|
||||
case "string":
|
||||
return "String"
|
||||
case "int", "int32", "int64":
|
||||
return "int"
|
||||
case "float", "float32", "float64":
|
||||
return "double"
|
||||
case "bool":
|
||||
return "bool"
|
||||
default:
|
||||
return t
|
||||
}
|
||||
}
|
||||
|
||||
func isDirectType(s string) bool {
|
||||
return isAtomicType(s) || isListType(s) && isAtomicType(getCoreType(s))
|
||||
}
|
||||
|
||||
func isAtomicType(s string) bool {
|
||||
switch s {
|
||||
case "String", "int", "double", "bool":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func isListType(s string) bool {
|
||||
return strings.HasPrefix(s, "List<")
|
||||
}
|
||||
|
||||
func isClassListType(s string) bool {
|
||||
return strings.HasPrefix(s, "List<") && !isAtomicType(getCoreType(s))
|
||||
}
|
||||
|
||||
func getCoreType(s string) string {
|
||||
if isAtomicType(s) {
|
||||
return s
|
||||
}
|
||||
if isListType(s) {
|
||||
s = strings.Replace(s, "List<", "", -1)
|
||||
return strings.Replace(s, ">", "", -1)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func fileExists(path string) bool {
|
||||
_, err := os.Stat(path)
|
||||
return !os.IsNotExist(err)
|
||||
}
|
||||
133
tools/goctl/api/dartgen/vars.go
Normal file
133
tools/goctl/api/dartgen/vars.go
Normal file
@@ -0,0 +1,133 @@
|
||||
package dartgen
|
||||
|
||||
import "text/template"
|
||||
|
||||
var funcMap = template.FuncMap{
|
||||
"tagGet": tagGet,
|
||||
"isDirectType": isDirectType,
|
||||
"isClassListType": isClassListType,
|
||||
"getCoreType": getCoreType,
|
||||
"pathToFuncName": pathToFuncName,
|
||||
"lowCamelCase": lowCamelCase,
|
||||
}
|
||||
|
||||
const apiFileContent = `import 'dart:io';
|
||||
import 'dart:convert';
|
||||
import '../vars/kv.dart';
|
||||
import '../vars/vars.dart';
|
||||
|
||||
/// 发送POST请求.
|
||||
///
|
||||
/// data:为你要post的结构体,我们会帮你转换成json字符串;
|
||||
/// ok函数:请求成功的时候调用,fail函数:请求失败的时候会调用,eventually函数:无论成功失败都会调用
|
||||
Future apiPost(String path, dynamic data,
|
||||
{Map<String, String> header,
|
||||
Function(Map<String, dynamic>) ok,
|
||||
Function(String) fail,
|
||||
Function eventually}) async {
|
||||
await _apiRequest('POST', path, data,
|
||||
header: header, ok: ok, fail: fail, eventually: eventually);
|
||||
}
|
||||
|
||||
/// 发送GET请求.
|
||||
///
|
||||
/// ok函数:请求成功的时候调用,fail函数:请求失败的时候会调用,eventually函数:无论成功失败都会调用
|
||||
Future apiGet(String path,
|
||||
{Map<String, String> header,
|
||||
Function(Map<String, dynamic>) ok,
|
||||
Function(String) fail,
|
||||
Function eventually}) async {
|
||||
await _apiRequest('GET', path, null,
|
||||
header: header, ok: ok, fail: fail, eventually: eventually);
|
||||
}
|
||||
|
||||
Future _apiRequest(String method, String path, dynamic data,
|
||||
{Map<String, String> header,
|
||||
Function(Map<String, dynamic>) ok,
|
||||
Function(String) fail,
|
||||
Function eventually}) async {
|
||||
var tokens = await getTokens();
|
||||
try {
|
||||
var client = HttpClient();
|
||||
HttpClientRequest r;
|
||||
if (method == 'POST') {
|
||||
r = await client.postUrl(Uri.parse('https://' + serverHost + path));
|
||||
} else {
|
||||
r = await client.getUrl(Uri.parse('https://' + serverHost + path));
|
||||
}
|
||||
|
||||
r.headers.set('Content-Type', 'application/json');
|
||||
if (tokens != null) {
|
||||
r.headers.set('Authorization', tokens.accessToken);
|
||||
}
|
||||
if (header != null) {
|
||||
header.forEach((k, v) {
|
||||
r.headers.set(k, v);
|
||||
});
|
||||
}
|
||||
var strData = '';
|
||||
if (data != null) {
|
||||
strData = jsonEncode(data);
|
||||
}
|
||||
r.write(strData);
|
||||
var rp = await r.close();
|
||||
var body = await rp.transform(utf8.decoder).join();
|
||||
print('${rp.statusCode} - $path');
|
||||
print('-- request --');
|
||||
print(strData);
|
||||
print('-- response --');
|
||||
print('$body \n');
|
||||
if (rp.statusCode == 404) {
|
||||
if (fail != null) fail('404 not found');
|
||||
} else {
|
||||
Map<String, dynamic> base = jsonDecode(body);
|
||||
if (rp.statusCode == 200) {
|
||||
if (base['code'] != 0) {
|
||||
if (fail != null) fail(base['desc']);
|
||||
} else {
|
||||
if (ok != null) ok(base['data']);
|
||||
}
|
||||
} else if (base['code'] != 0) {
|
||||
if (fail != null) fail(base['desc']);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
if (fail != null) fail(e.toString());
|
||||
}
|
||||
if (eventually != null) eventually();
|
||||
}
|
||||
`
|
||||
const tokensFileContent = `class Tokens {
|
||||
/// 用于访问的token, 每次请求都必须带在Header里面
|
||||
final String accessToken;
|
||||
final int accessExpire;
|
||||
|
||||
/// 用于刷新token
|
||||
final String refreshToken;
|
||||
final int refreshExpire;
|
||||
final int refreshAfter;
|
||||
Tokens(
|
||||
{this.accessToken,
|
||||
this.accessExpire,
|
||||
this.refreshToken,
|
||||
this.refreshExpire,
|
||||
this.refreshAfter});
|
||||
factory Tokens.fromJson(Map<String, dynamic> m) {
|
||||
return Tokens(
|
||||
accessToken: m['access_token'],
|
||||
accessExpire: m['access_expire'],
|
||||
refreshToken: m['refresh_token'],
|
||||
refreshExpire: m['refresh_expire'],
|
||||
refreshAfter: m['refresh_after']);
|
||||
}
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'access_token': accessToken,
|
||||
'access_expire': accessExpire,
|
||||
'refresh_token': refreshToken,
|
||||
'refresh_expire': refreshExpire,
|
||||
'refresh_after': refreshAfter,
|
||||
};
|
||||
}
|
||||
}
|
||||
`
|
||||
7
tools/goctl/api/demo/config/config.go
Normal file
7
tools/goctl/api/demo/config/config.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package config
|
||||
|
||||
import "zero/ngin"
|
||||
|
||||
type Config struct {
|
||||
ngin.NgConf
|
||||
}
|
||||
25
tools/goctl/api/demo/demo.go
Normal file
25
tools/goctl/api/demo/demo.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
|
||||
"zero/core/conf"
|
||||
"zero/ngin"
|
||||
"zero/tools/goctl/api/demo/config"
|
||||
"zero/tools/goctl/api/demo/handler"
|
||||
)
|
||||
|
||||
var configFile = flag.String("f", "etc/user.json", "the config file")
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
|
||||
var c config.Config
|
||||
conf.MustLoad(*configFile, &c)
|
||||
|
||||
engine := ngin.MustNewEngine(c.NgConf)
|
||||
defer engine.Stop()
|
||||
|
||||
handler.RegisterHandlers(engine)
|
||||
engine.Start()
|
||||
}
|
||||
8
tools/goctl/api/demo/etc/user.json
Normal file
8
tools/goctl/api/demo/etc/user.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"Name": "user",
|
||||
"Host": "127.0.0.1",
|
||||
"Port": 3333,
|
||||
"Log": {
|
||||
"Mode": "console"
|
||||
}
|
||||
}
|
||||
33
tools/goctl/api/demo/handler/getuserhandler.go
Normal file
33
tools/goctl/api/demo/handler/getuserhandler.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"zero/core/httpx"
|
||||
)
|
||||
|
||||
type (
|
||||
request struct {
|
||||
User string `form:"user,optional"`
|
||||
}
|
||||
|
||||
response struct {
|
||||
Code int `json:"code"`
|
||||
Greet string `json:"greet"`
|
||||
From string `json:"from,omitempty"`
|
||||
}
|
||||
)
|
||||
|
||||
func GreetHandler(w http.ResponseWriter, r *http.Request) {
|
||||
var req request
|
||||
err := httpx.Parse(r, &req)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
httpx.OkJson(w, response{
|
||||
Code: 0,
|
||||
Greet: "hello",
|
||||
})
|
||||
}
|
||||
17
tools/goctl/api/demo/handler/handlers.go
Normal file
17
tools/goctl/api/demo/handler/handlers.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"zero/ngin"
|
||||
)
|
||||
|
||||
func RegisterHandlers(engine *ngin.Engine) {
|
||||
engine.AddRoutes([]ngin.Route{
|
||||
{
|
||||
Method: http.MethodGet,
|
||||
Path: "/",
|
||||
Handler: GreetHandler,
|
||||
},
|
||||
})
|
||||
}
|
||||
4
tools/goctl/api/demo/svc/servicecontext.go
Normal file
4
tools/goctl/api/demo/svc/servicecontext.go
Normal file
@@ -0,0 +1,4 @@
|
||||
package svc
|
||||
|
||||
type ServiceContext struct {
|
||||
}
|
||||
82
tools/goctl/api/docgen/doc.go
Normal file
82
tools/goctl/api/docgen/doc.go
Normal file
@@ -0,0 +1,82 @@
|
||||
package docgen
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"zero/core/stringx"
|
||||
"zero/tools/goctl/api/gogen"
|
||||
"zero/tools/goctl/api/spec"
|
||||
"zero/tools/goctl/api/util"
|
||||
)
|
||||
|
||||
const (
|
||||
markdownTemplate = `
|
||||
### {{.index}}. {{.routeComment}}
|
||||
|
||||
1. 路由定义
|
||||
|
||||
- Url: {{.uri}}
|
||||
- Method: {{.method}}
|
||||
- Request: {{.requestType}}
|
||||
- Response: {{.responseType}}
|
||||
|
||||
|
||||
2. 类型定义
|
||||
|
||||
{{.responseContent}}
|
||||
|
||||
`
|
||||
)
|
||||
|
||||
func genDoc(api *spec.ApiSpec, dir string, filename string) error {
|
||||
fp, _, err := util.MaybeCreateFile(dir, "", filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer fp.Close()
|
||||
|
||||
var builder strings.Builder
|
||||
for index, route := range api.Service.Routes {
|
||||
routeComment, _ := util.GetAnnotationValue(route.Annotations, "doc", "summary")
|
||||
if len(routeComment) == 0 {
|
||||
routeComment = "N/A"
|
||||
}
|
||||
|
||||
responseContent, err := responseBody(api, route)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
t := template.Must(template.New("markdownTemplate").Parse(markdownTemplate))
|
||||
var tmplBytes bytes.Buffer
|
||||
err = t.Execute(&tmplBytes, map[string]string{
|
||||
"index": strconv.Itoa(index + 1),
|
||||
"routeComment": routeComment,
|
||||
"method": strings.ToUpper(route.Method),
|
||||
"uri": route.Path,
|
||||
"requestType": "`" + stringx.TakeOne(route.RequestType.Name, "-") + "`",
|
||||
"responseType": "`" + stringx.TakeOne(route.ResponseType.Name, "-") + "`",
|
||||
"responseContent": responseContent,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
builder.Write(tmplBytes.Bytes())
|
||||
}
|
||||
_, err = fp.WriteString(strings.Replace(builder.String(), """, `"`, -1))
|
||||
return err
|
||||
}
|
||||
|
||||
func responseBody(api *spec.ApiSpec, route spec.Route) (string, error) {
|
||||
tps := util.GetLocalTypes(api, route)
|
||||
value, err := gogen.BuildTypes(tps)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return fmt.Sprintf("\n\n```golang\n%s\n```\n", value), nil
|
||||
}
|
||||
65
tools/goctl/api/docgen/gen.go
Normal file
65
tools/goctl/api/docgen/gen.go
Normal file
@@ -0,0 +1,65 @@
|
||||
package docgen
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"zero/tools/goctl/api/parser"
|
||||
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
var docDir = "doc"
|
||||
|
||||
func DocCommand(c *cli.Context) error {
|
||||
dir := c.String("dir")
|
||||
if len(dir) == 0 {
|
||||
return errors.New("missing -dir")
|
||||
}
|
||||
|
||||
files, err := filePathWalkDir(dir)
|
||||
if err != nil {
|
||||
return errors.New(fmt.Sprintf("dir %s not exist", dir))
|
||||
}
|
||||
|
||||
err = os.RemoveAll(dir + "/" + docDir + "/")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, f := range files {
|
||||
p, err := parser.NewParser(f)
|
||||
if err != nil {
|
||||
return errors.New(fmt.Sprintf("parse file: %s, err: %s", f, err.Error()))
|
||||
}
|
||||
api, err := p.Parse()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
index := strings.Index(f, dir)
|
||||
if index < 0 {
|
||||
continue
|
||||
}
|
||||
dst := dir + "/" + docDir + f[index+len(dir):]
|
||||
index = strings.LastIndex(dst, "/")
|
||||
if index < 0 {
|
||||
continue
|
||||
}
|
||||
dir := dst[:index]
|
||||
genDoc(api, dir, strings.Replace(dst[index+1:], ".api", ".md", 1))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func filePathWalkDir(root string) ([]string, error) {
|
||||
var files []string
|
||||
err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
|
||||
if !info.IsDir() && strings.HasSuffix(path, ".api") {
|
||||
files = append(files, path)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return files, err
|
||||
}
|
||||
114
tools/goctl/api/format/format.go
Normal file
114
tools/goctl/api/format/format.go
Normal file
@@ -0,0 +1,114 @@
|
||||
package format
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"go/format"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"zero/tools/goctl/api/util"
|
||||
|
||||
"zero/core/errorx"
|
||||
"zero/tools/goctl/api/parser"
|
||||
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
var (
|
||||
reg = regexp.MustCompile("type (?P<name>.*)[\\s]+{")
|
||||
)
|
||||
|
||||
func GoFormatApi(c *cli.Context) error {
|
||||
dir := c.String("dir")
|
||||
if len(dir) == 0 {
|
||||
return errors.New("missing -dir")
|
||||
}
|
||||
|
||||
printToConsole := c.Bool("p")
|
||||
|
||||
var be errorx.BatchError
|
||||
err := filepath.Walk(dir, func(path string, fi os.FileInfo, errBack error) (err error) {
|
||||
if strings.HasSuffix(path, ".api") {
|
||||
err := ApiFormat(path, printToConsole)
|
||||
if err != nil {
|
||||
be.Add(util.WrapErr(err, fi.Name()))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
be.Add(err)
|
||||
if be.NotNil() {
|
||||
errs := be.Err().Error()
|
||||
fmt.Println(errs)
|
||||
os.Exit(1)
|
||||
}
|
||||
return be.Err()
|
||||
}
|
||||
|
||||
func ApiFormat(path string, printToConsole bool) error {
|
||||
data, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
r := reg.ReplaceAllStringFunc(string(data), func(m string) string {
|
||||
parts := reg.FindStringSubmatch(m)
|
||||
if len(parts) < 2 {
|
||||
return m
|
||||
}
|
||||
if !strings.Contains(m, "struct") {
|
||||
return "type " + parts[1] + " struct {"
|
||||
}
|
||||
return m
|
||||
})
|
||||
|
||||
info, st, service, err := parser.MatchStruct(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
info = strings.TrimSpace(info)
|
||||
if len(service) == 0 || len(st) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
fs, err := format.Source([]byte(strings.TrimSpace(st)))
|
||||
if err != nil {
|
||||
str := err.Error()
|
||||
lineNumber := strings.Index(str, ":")
|
||||
if lineNumber > 0 {
|
||||
ln, err := strconv.ParseInt(str[:lineNumber], 10, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pn := 0
|
||||
if len(info) > 0 {
|
||||
pn = countRune(info, '\n') + 1
|
||||
}
|
||||
number := int(ln) + pn + 1
|
||||
return errors.New(fmt.Sprintf("line: %d, %s", number, str[lineNumber+1:]))
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
result := strings.Join([]string{info, string(fs), service}, "\n\n")
|
||||
if printToConsole {
|
||||
_, err := fmt.Print(result)
|
||||
return err
|
||||
}
|
||||
return ioutil.WriteFile(path, []byte(result), os.ModePerm)
|
||||
}
|
||||
|
||||
func countRune(s string, r rune) int {
|
||||
count := 0
|
||||
for _, c := range s {
|
||||
if c == r {
|
||||
count++
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
134
tools/goctl/api/gogen/gen.go
Normal file
134
tools/goctl/api/gogen/gen.go
Normal file
@@ -0,0 +1,134 @@
|
||||
package gogen
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"zero/core/lang"
|
||||
apiformat "zero/tools/goctl/api/format"
|
||||
"zero/tools/goctl/api/parser"
|
||||
apiutil "zero/tools/goctl/api/util"
|
||||
"zero/tools/goctl/util"
|
||||
|
||||
"github.com/logrusorgru/aurora"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
const tmpFile = "%s-%d"
|
||||
|
||||
var tmpDir = path.Join(os.TempDir(), "goctl")
|
||||
|
||||
func GoCommand(c *cli.Context) error {
|
||||
apiFile := c.String("api")
|
||||
dir := c.String("dir")
|
||||
if len(apiFile) == 0 {
|
||||
return errors.New("missing -api")
|
||||
}
|
||||
if len(dir) == 0 {
|
||||
return errors.New("missing -dir")
|
||||
}
|
||||
|
||||
p, err := parser.NewParser(apiFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
api, err := p.Parse()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
lang.Must(util.MkdirIfNotExist(dir))
|
||||
lang.Must(genEtc(dir, api))
|
||||
lang.Must(genConfig(dir, api))
|
||||
lang.Must(genMain(dir, api))
|
||||
lang.Must(genServiceContext(dir, api))
|
||||
lang.Must(genTypes(dir, api))
|
||||
lang.Must(genHandlers(dir, api))
|
||||
lang.Must(genRoutes(dir, api))
|
||||
lang.Must(genLogic(dir, api))
|
||||
// it does not work
|
||||
format(dir)
|
||||
|
||||
if err := backupAndSweep(apiFile); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = apiformat.ApiFormat(apiFile, false); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println(aurora.Green("Done."))
|
||||
return nil
|
||||
}
|
||||
|
||||
func backupAndSweep(apiFile string) error {
|
||||
var err error
|
||||
var wg sync.WaitGroup
|
||||
|
||||
wg.Add(2)
|
||||
_ = os.MkdirAll(tmpDir, os.ModePerm)
|
||||
|
||||
go func() {
|
||||
_, fileName := filepath.Split(apiFile)
|
||||
_, e := apiutil.Copy(apiFile, fmt.Sprintf(path.Join(tmpDir, tmpFile), fileName, time.Now().Unix()))
|
||||
if e != nil {
|
||||
err = e
|
||||
}
|
||||
wg.Done()
|
||||
}()
|
||||
go func() {
|
||||
if e := sweep(); e != nil {
|
||||
err = e
|
||||
}
|
||||
wg.Done()
|
||||
}()
|
||||
wg.Wait()
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func format(dir string) {
|
||||
cmd := exec.Command("go", "fmt", "./"+dir+"...")
|
||||
_, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
print(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func sweep() error {
|
||||
keepTime := time.Now().AddDate(0, 0, -7)
|
||||
return filepath.Walk(tmpDir, func(fpath string, info os.FileInfo, err error) error {
|
||||
if info.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
pos := strings.LastIndexByte(info.Name(), '-')
|
||||
if pos > 0 {
|
||||
timestamp := info.Name()[pos+1:]
|
||||
seconds, err := strconv.ParseInt(timestamp, 10, 64)
|
||||
if err != nil {
|
||||
// print error and ignore
|
||||
fmt.Println(aurora.Red(fmt.Sprintf("sweep ignored file: %s", fpath)))
|
||||
return nil
|
||||
}
|
||||
|
||||
tm := time.Unix(seconds, 0)
|
||||
if tm.Before(keepTime) {
|
||||
if err := os.Remove(fpath); err != nil {
|
||||
fmt.Println(aurora.Red(fmt.Sprintf("failed to remove file: %s", fpath)))
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
48
tools/goctl/api/gogen/genconfig.go
Normal file
48
tools/goctl/api/gogen/genconfig.go
Normal file
@@ -0,0 +1,48 @@
|
||||
package gogen
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"text/template"
|
||||
|
||||
"zero/tools/goctl/api/spec"
|
||||
"zero/tools/goctl/api/util"
|
||||
)
|
||||
|
||||
const (
|
||||
configFile = "config.go"
|
||||
configTemplate = `package config
|
||||
|
||||
import (
|
||||
"zero/ngin"
|
||||
{{.authImport}}
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
ngin.NgConf
|
||||
}
|
||||
`
|
||||
)
|
||||
|
||||
func genConfig(dir string, api *spec.ApiSpec) error {
|
||||
fp, created, err := util.MaybeCreateFile(dir, configDir, configFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !created {
|
||||
return nil
|
||||
}
|
||||
defer fp.Close()
|
||||
|
||||
var authImportStr = ""
|
||||
t := template.Must(template.New("configTemplate").Parse(configTemplate))
|
||||
buffer := new(bytes.Buffer)
|
||||
err = t.Execute(buffer, map[string]string{
|
||||
"authImport": authImportStr,
|
||||
})
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
formatCode := formatCode(buffer.String())
|
||||
_, err = fp.WriteString(formatCode)
|
||||
return err
|
||||
}
|
||||
56
tools/goctl/api/gogen/genetc.go
Normal file
56
tools/goctl/api/gogen/genetc.go
Normal file
@@ -0,0 +1,56 @@
|
||||
package gogen
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"text/template"
|
||||
|
||||
"zero/tools/goctl/api/spec"
|
||||
"zero/tools/goctl/api/util"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultPort = 8888
|
||||
etcDir = "etc"
|
||||
etcTemplate = `{
|
||||
"Name": "{{.serviceName}}",
|
||||
"Host": "{{.host}}",
|
||||
"Port": {{.port}}
|
||||
}`
|
||||
)
|
||||
|
||||
func genEtc(dir string, api *spec.ApiSpec) error {
|
||||
fp, created, err := util.MaybeCreateFile(dir, etcDir, fmt.Sprintf("%s.json", api.Service.Name))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !created {
|
||||
return nil
|
||||
}
|
||||
defer fp.Close()
|
||||
|
||||
service := api.Service
|
||||
host, ok := util.GetAnnotationValue(service.Annotations, "server", "host")
|
||||
if !ok {
|
||||
host = "0.0.0.0"
|
||||
}
|
||||
port, ok := util.GetAnnotationValue(service.Annotations, "server", "port")
|
||||
if !ok {
|
||||
port = strconv.Itoa(defaultPort)
|
||||
}
|
||||
|
||||
t := template.Must(template.New("etcTemplate").Parse(etcTemplate))
|
||||
buffer := new(bytes.Buffer)
|
||||
err = t.Execute(buffer, map[string]string{
|
||||
"serviceName": service.Name,
|
||||
"host": host,
|
||||
"port": port,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
formatCode := formatCode(buffer.String())
|
||||
_, err = fp.WriteString(formatCode)
|
||||
return err
|
||||
}
|
||||
199
tools/goctl/api/gogen/genhandlers.go
Normal file
199
tools/goctl/api/gogen/genhandlers.go
Normal file
@@ -0,0 +1,199 @@
|
||||
package gogen
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"path"
|
||||
"sort"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"zero/tools/goctl/api/spec"
|
||||
apiutil "zero/tools/goctl/api/util"
|
||||
"zero/tools/goctl/util"
|
||||
)
|
||||
|
||||
const (
|
||||
handlerTemplate = `package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
{{.importPackages}}
|
||||
)
|
||||
|
||||
func {{.handlerName}}(ctx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
l := logic.{{.logic}}(r.Context(), ctx)
|
||||
{{.handlerBody}}
|
||||
}
|
||||
}
|
||||
`
|
||||
handlerBodyTemplate = `{{.parseRequest}}
|
||||
{{.processBody}}
|
||||
`
|
||||
parseRequestTemplate = `var req {{.requestType}}
|
||||
if err := httpx.Parse(r, &req); err != nil {
|
||||
logx.Error(err)
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
`
|
||||
hasRespTemplate = `
|
||||
{{.logicResponse}} l.{{.callee}}({{.req}})
|
||||
// TODO write data to response
|
||||
`
|
||||
)
|
||||
|
||||
func genHandler(dir string, group spec.Group, route spec.Route) error {
|
||||
handler, ok := apiutil.GetAnnotationValue(route.Annotations, "server", "handler")
|
||||
if !ok {
|
||||
return fmt.Errorf("missing handler annotation for %q", route.Path)
|
||||
}
|
||||
handler = getHandlerName(handler)
|
||||
var reqBody string
|
||||
if len(route.RequestType.Name) > 0 {
|
||||
var bodyBuilder strings.Builder
|
||||
t := template.Must(template.New("parseRequest").Parse(parseRequestTemplate))
|
||||
if err := t.Execute(&bodyBuilder, map[string]string{
|
||||
"requestType": typesPacket + "." + util.Title(route.RequestType.Name),
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
reqBody = bodyBuilder.String()
|
||||
}
|
||||
|
||||
var req = "req"
|
||||
if len(route.RequestType.Name) == 0 {
|
||||
req = ""
|
||||
}
|
||||
var logicResponse = ""
|
||||
var writeResponse = "nil, nil"
|
||||
if len(route.ResponseType.Name) > 0 {
|
||||
logicResponse = "resp, err :="
|
||||
writeResponse = "resp, err"
|
||||
} else {
|
||||
logicResponse = "err :="
|
||||
writeResponse = "nil, err"
|
||||
}
|
||||
var logicBodyBuilder strings.Builder
|
||||
t := template.Must(template.New("hasRespTemplate").Parse(hasRespTemplate))
|
||||
if err := t.Execute(&logicBodyBuilder, map[string]string{
|
||||
"callee": strings.Title(strings.TrimSuffix(handler, "Handler")),
|
||||
"req": req,
|
||||
"logicResponse": logicResponse,
|
||||
"writeResponse": writeResponse,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
respBody := logicBodyBuilder.String()
|
||||
|
||||
if !strings.HasSuffix(handler, "Handler") {
|
||||
handler = handler + "Handler"
|
||||
}
|
||||
|
||||
var bodyBuilder strings.Builder
|
||||
bodyTemplate := template.Must(template.New("handlerBodyTemplate").Parse(handlerBodyTemplate))
|
||||
if err := bodyTemplate.Execute(&bodyBuilder, map[string]string{
|
||||
"parseRequest": reqBody,
|
||||
"processBody": respBody,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
return doGenToFile(dir, handler, group, route, bodyBuilder)
|
||||
}
|
||||
|
||||
func doGenToFile(dir, handler string, group spec.Group, route spec.Route, bodyBuilder strings.Builder) error {
|
||||
if getHandlerFolderPath(group, route) != handlerDir {
|
||||
handler = strings.Title(handler)
|
||||
}
|
||||
parentPkg, err := getParentPackage(dir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
filename := strings.ToLower(handler)
|
||||
if strings.HasSuffix(filename, "handler") {
|
||||
filename = filename + ".go"
|
||||
} else {
|
||||
filename = filename + "handler.go"
|
||||
}
|
||||
fp, created, err := apiutil.MaybeCreateFile(dir, getHandlerFolderPath(group, route), filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !created {
|
||||
return nil
|
||||
}
|
||||
defer fp.Close()
|
||||
t := template.Must(template.New("handlerTemplate").Parse(handlerTemplate))
|
||||
buffer := new(bytes.Buffer)
|
||||
err = t.Execute(buffer, map[string]string{
|
||||
"logic": "New" + strings.TrimSuffix(strings.Title(handler), "Handler") + "Logic",
|
||||
"importPackages": genHandlerImports(group, route, parentPkg),
|
||||
"handlerName": handler,
|
||||
"handlerBody": strings.TrimSpace(bodyBuilder.String()),
|
||||
})
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
formatCode := formatCode(buffer.String())
|
||||
_, err = fp.WriteString(formatCode)
|
||||
return err
|
||||
}
|
||||
|
||||
func genHandlers(dir string, api *spec.ApiSpec) error {
|
||||
for _, group := range api.Service.Groups {
|
||||
for _, route := range group.Routes {
|
||||
if err := genHandler(dir, group, route); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func genHandlerImports(group spec.Group, route spec.Route, parentPkg string) string {
|
||||
var imports []string
|
||||
if len(route.RequestType.Name) > 0 || len(route.ResponseType.Name) > 0 {
|
||||
imports = append(imports, "\"zero/core/httpx\"")
|
||||
}
|
||||
if len(route.RequestType.Name) > 0 {
|
||||
imports = append(imports, "\"zero/core/logx\"")
|
||||
}
|
||||
imports = append(imports, fmt.Sprintf("\"%s\"", path.Join(parentPkg, contextDir)))
|
||||
if len(route.RequestType.Name) > 0 || len(route.ResponseType.Name) > 0 {
|
||||
imports = append(imports, fmt.Sprintf("\"%s\"", path.Join(parentPkg, typesDir)))
|
||||
}
|
||||
imports = append(imports, fmt.Sprintf("\"%s\"", path.Join(parentPkg, getLogicFolderPath(group, route))))
|
||||
sort.Strings(imports)
|
||||
|
||||
return strings.Join(imports, "\n\t")
|
||||
}
|
||||
|
||||
func getHandlerBaseName(handler string) string {
|
||||
handlerName := util.Untitle(handler)
|
||||
if strings.HasSuffix(handlerName, "handler") {
|
||||
handlerName = strings.ReplaceAll(handlerName, "handler", "")
|
||||
} else if strings.HasSuffix(handlerName, "Handler") {
|
||||
handlerName = strings.ReplaceAll(handlerName, "Handler", "")
|
||||
}
|
||||
return handlerName
|
||||
}
|
||||
|
||||
func getHandlerFolderPath(group spec.Group, route spec.Route) string {
|
||||
folder, ok := apiutil.GetAnnotationValue(route.Annotations, "server", folderProperty)
|
||||
if !ok {
|
||||
folder, ok = apiutil.GetAnnotationValue(group.Annotations, "server", folderProperty)
|
||||
if !ok {
|
||||
return handlerDir
|
||||
}
|
||||
}
|
||||
folder = strings.TrimPrefix(folder, "/")
|
||||
folder = strings.TrimSuffix(folder, "/")
|
||||
return path.Join(handlerDir, folder)
|
||||
}
|
||||
|
||||
func getHandlerName(handler string) string {
|
||||
return getHandlerBaseName(handler) + "Handler"
|
||||
}
|
||||
130
tools/goctl/api/gogen/genlogic.go
Normal file
130
tools/goctl/api/gogen/genlogic.go
Normal file
@@ -0,0 +1,130 @@
|
||||
package gogen
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"path"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"zero/tools/goctl/api/spec"
|
||||
"zero/tools/goctl/api/util"
|
||||
)
|
||||
|
||||
const logicTemplate = `package logic
|
||||
|
||||
import (
|
||||
{{.imports}}
|
||||
)
|
||||
|
||||
type {{.logic}} struct {
|
||||
ctx context.Context
|
||||
logx.Logger
|
||||
}
|
||||
|
||||
func New{{.logic}}(ctx context.Context, svcCtx *svc.ServiceContext) {{.logic}} {
|
||||
return {{.logic}}{
|
||||
ctx: ctx,
|
||||
Logger: logx.WithContext(ctx),
|
||||
}
|
||||
// TODO need set model here from svc
|
||||
}
|
||||
|
||||
func (l *{{.logic}}) {{.function}}({{.request}}) {{.responseType}} {
|
||||
{{.returnString}}
|
||||
}
|
||||
|
||||
`
|
||||
|
||||
func genLogic(dir string, api *spec.ApiSpec) error {
|
||||
for _, g := range api.Service.Groups {
|
||||
for _, r := range g.Routes {
|
||||
err := genLogicByRoute(dir, g, r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func genLogicByRoute(dir string, group spec.Group, route spec.Route) error {
|
||||
handler, ok := util.GetAnnotationValue(route.Annotations, "server", "handler")
|
||||
if !ok {
|
||||
return fmt.Errorf("missing handler annotation for %q", route.Path)
|
||||
}
|
||||
handler = strings.TrimSuffix(handler, "handler")
|
||||
handler = strings.TrimSuffix(handler, "Handler")
|
||||
filename := strings.ToLower(handler)
|
||||
goFile := filename + "logic.go"
|
||||
fp, created, err := util.MaybeCreateFile(dir, getLogicFolderPath(group, route), goFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !created {
|
||||
return nil
|
||||
}
|
||||
defer fp.Close()
|
||||
|
||||
parentPkg, err := getParentPackage(dir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
imports := genLogicImports(route, parentPkg)
|
||||
|
||||
responseString := ""
|
||||
returnString := ""
|
||||
requestString := ""
|
||||
if len(route.ResponseType.Name) > 0 {
|
||||
responseString = "(*types." + strings.Title(route.ResponseType.Name) + ", error)"
|
||||
returnString = "return nil, nil"
|
||||
} else {
|
||||
responseString = "error"
|
||||
returnString = "return nil"
|
||||
}
|
||||
if len(route.RequestType.Name) > 0 {
|
||||
requestString = "req " + "types." + strings.Title(route.RequestType.Name)
|
||||
}
|
||||
|
||||
t := template.Must(template.New("logicTemplate").Parse(logicTemplate))
|
||||
buffer := new(bytes.Buffer)
|
||||
err = t.Execute(fp, map[string]string{
|
||||
"imports": imports,
|
||||
"logic": strings.Title(handler) + "Logic",
|
||||
"function": strings.Title(strings.TrimSuffix(handler, "Handler")),
|
||||
"responseType": responseString,
|
||||
"returnString": returnString,
|
||||
"request": requestString,
|
||||
})
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
formatCode := formatCode(buffer.String())
|
||||
_, err = fp.WriteString(formatCode)
|
||||
return err
|
||||
}
|
||||
|
||||
func getLogicFolderPath(group spec.Group, route spec.Route) string {
|
||||
folder, ok := util.GetAnnotationValue(route.Annotations, "server", folderProperty)
|
||||
if !ok {
|
||||
folder, ok = util.GetAnnotationValue(group.Annotations, "server", folderProperty)
|
||||
if !ok {
|
||||
return logicDir
|
||||
}
|
||||
}
|
||||
folder = strings.TrimPrefix(folder, "/")
|
||||
folder = strings.TrimSuffix(folder, "/")
|
||||
return path.Join(logicDir, folder)
|
||||
}
|
||||
|
||||
func genLogicImports(route spec.Route, parentPkg string) string {
|
||||
var imports []string
|
||||
imports = append(imports, `"context"`)
|
||||
imports = append(imports, "\n")
|
||||
imports = append(imports, `"zero/core/logx"`)
|
||||
if len(route.ResponseType.Name) > 0 || len(route.RequestType.Name) > 0 {
|
||||
imports = append(imports, fmt.Sprintf("\"%s\"", path.Join(parentPkg, typesDir)))
|
||||
}
|
||||
imports = append(imports, fmt.Sprintf("\"%s\"", path.Join(parentPkg, contextDir)))
|
||||
return strings.Join(imports, "\n\t")
|
||||
}
|
||||
85
tools/goctl/api/gogen/genmain.go
Normal file
85
tools/goctl/api/gogen/genmain.go
Normal file
@@ -0,0 +1,85 @@
|
||||
package gogen
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"path"
|
||||
"sort"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"zero/tools/goctl/api/spec"
|
||||
"zero/tools/goctl/api/util"
|
||||
)
|
||||
|
||||
const mainTemplate = `package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
|
||||
{{.importPackages}}
|
||||
)
|
||||
|
||||
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)
|
||||
|
||||
engine := ngin.MustNewEngine(c.NgConf)
|
||||
defer engine.Stop()
|
||||
|
||||
handler.RegisterHandlers(engine, ctx)
|
||||
engine.Start()
|
||||
}
|
||||
`
|
||||
|
||||
func genMain(dir string, api *spec.ApiSpec) error {
|
||||
name := strings.ToLower(api.Service.Name)
|
||||
if strings.HasSuffix(name, "-api") {
|
||||
name = strings.ReplaceAll(name, "-api", "")
|
||||
}
|
||||
goFile := name + ".go"
|
||||
fp, created, err := util.MaybeCreateFile(dir, "", goFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !created {
|
||||
return nil
|
||||
}
|
||||
defer fp.Close()
|
||||
|
||||
parentPkg, err := getParentPackage(dir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
t := template.Must(template.New("mainTemplate").Parse(mainTemplate))
|
||||
buffer := new(bytes.Buffer)
|
||||
err = t.Execute(buffer, map[string]string{
|
||||
"importPackages": genMainImports(parentPkg),
|
||||
"serviceName": api.Service.Name,
|
||||
})
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
formatCode := formatCode(buffer.String())
|
||||
_, err = fp.WriteString(formatCode)
|
||||
return err
|
||||
}
|
||||
|
||||
func genMainImports(parentPkg string) string {
|
||||
imports := []string{
|
||||
`"zero/core/conf"`,
|
||||
`"zero/ngin"`,
|
||||
}
|
||||
imports = append(imports, fmt.Sprintf("\"%s\"", path.Join(parentPkg, configDir)))
|
||||
imports = append(imports, fmt.Sprintf("\"%s\"", path.Join(parentPkg, handlerDir)))
|
||||
imports = append(imports, fmt.Sprintf("\"%s\"", path.Join(parentPkg, contextDir)))
|
||||
sort.Strings(imports)
|
||||
return strings.Join(imports, "\n\t")
|
||||
}
|
||||
193
tools/goctl/api/gogen/genroutes.go
Normal file
193
tools/goctl/api/gogen/genroutes.go
Normal file
@@ -0,0 +1,193 @@
|
||||
package gogen
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"path"
|
||||
"sort"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"zero/core/collection"
|
||||
"zero/tools/goctl/api/spec"
|
||||
apiutil "zero/tools/goctl/api/util"
|
||||
"zero/tools/goctl/util"
|
||||
)
|
||||
|
||||
const (
|
||||
routesFilename = "routes.go"
|
||||
routesTemplate = `// DO NOT EDIT, generated by goctl
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
{{.importPackages}}
|
||||
)
|
||||
|
||||
func RegisterHandlers(engine *ngin.Engine, serverCtx *svc.ServiceContext) {
|
||||
{{.routesAdditions}}
|
||||
}
|
||||
`
|
||||
routesAdditionTemplate = `
|
||||
engine.AddRoutes([]ngin.Route{
|
||||
{{.routes}}
|
||||
}{{.jwt}}{{.signature}})
|
||||
`
|
||||
)
|
||||
|
||||
var mapping = map[string]string{
|
||||
"delete": "http.MethodDelete",
|
||||
"get": "http.MethodGet",
|
||||
"head": "http.MethodHead",
|
||||
"post": "http.MethodPost",
|
||||
"put": "http.MethodPut",
|
||||
}
|
||||
|
||||
type (
|
||||
group struct {
|
||||
routes []route
|
||||
jwtEnabled bool
|
||||
signatureEnabled bool
|
||||
authName string
|
||||
}
|
||||
route struct {
|
||||
method string
|
||||
path string
|
||||
handler string
|
||||
}
|
||||
)
|
||||
|
||||
func genRoutes(dir string, api *spec.ApiSpec) error {
|
||||
var builder strings.Builder
|
||||
groups, err := getRoutes(api)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
gt := template.Must(template.New("groupTemplate").Parse(routesAdditionTemplate))
|
||||
for _, g := range groups {
|
||||
var gbuilder strings.Builder
|
||||
for _, r := range g.routes {
|
||||
fmt.Fprintf(&gbuilder, `
|
||||
{
|
||||
Method: %s,
|
||||
Path: "%s",
|
||||
Handler: %s,
|
||||
},`,
|
||||
r.method, r.path, r.handler)
|
||||
}
|
||||
jwt := ""
|
||||
if g.jwtEnabled {
|
||||
jwt = fmt.Sprintf(", ngin.WithJwt(serverCtx.Config.%s.AccessSecret)", g.authName)
|
||||
}
|
||||
signature := ""
|
||||
if g.signatureEnabled {
|
||||
signature = fmt.Sprintf(", ngin.WithSignature(serverCtx.Config.%s.Signature)", g.authName)
|
||||
}
|
||||
if err := gt.Execute(&builder, map[string]string{
|
||||
"routes": strings.TrimSpace(gbuilder.String()),
|
||||
"jwt": jwt,
|
||||
"signature": signature,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
parentPkg, err := getParentPackage(dir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
filename := path.Join(dir, handlerDir, routesFilename)
|
||||
if err := util.RemoveOrQuit(filename); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fp, created, err := apiutil.MaybeCreateFile(dir, handlerDir, routesFilename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !created {
|
||||
return nil
|
||||
}
|
||||
defer fp.Close()
|
||||
|
||||
t := template.Must(template.New("routesTemplate").Parse(routesTemplate))
|
||||
buffer := new(bytes.Buffer)
|
||||
err = t.Execute(buffer, map[string]string{
|
||||
"importPackages": genRouteImports(parentPkg, api),
|
||||
"routesAdditions": strings.TrimSpace(builder.String()),
|
||||
})
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
formatCode := formatCode(buffer.String())
|
||||
_, err = fp.WriteString(formatCode)
|
||||
return err
|
||||
}
|
||||
|
||||
func genRouteImports(parentPkg string, api *spec.ApiSpec) string {
|
||||
var importSet = collection.NewSet()
|
||||
importSet.AddStr(`"zero/ngin"`)
|
||||
importSet.AddStr(fmt.Sprintf("\"%s\"", path.Join(parentPkg, contextDir)))
|
||||
for _, group := range api.Service.Groups {
|
||||
for _, route := range group.Routes {
|
||||
folder, ok := apiutil.GetAnnotationValue(route.Annotations, "server", folderProperty)
|
||||
if !ok {
|
||||
folder, ok = apiutil.GetAnnotationValue(group.Annotations, "server", folderProperty)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
}
|
||||
importSet.AddStr(fmt.Sprintf("%s \"%s\"", folder, path.Join(parentPkg, handlerDir, folder)))
|
||||
}
|
||||
}
|
||||
imports := importSet.KeysStr()
|
||||
sort.Strings(imports)
|
||||
return strings.Join(imports, "\n\t")
|
||||
}
|
||||
|
||||
func getRoutes(api *spec.ApiSpec) ([]group, error) {
|
||||
var routes []group
|
||||
|
||||
for _, g := range api.Service.Groups {
|
||||
var groupedRoutes group
|
||||
for _, r := range g.Routes {
|
||||
handler, ok := apiutil.GetAnnotationValue(r.Annotations, "server", "handler")
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("missing handler annotation for route %q", r.Path)
|
||||
}
|
||||
handler = getHandlerBaseName(handler) + "Handler(serverCtx)"
|
||||
folder, ok := apiutil.GetAnnotationValue(r.Annotations, "server", folderProperty)
|
||||
if ok {
|
||||
handler = folder + "." + strings.ToUpper(handler[:1]) + handler[1:]
|
||||
} else {
|
||||
folder, ok = apiutil.GetAnnotationValue(g.Annotations, "server", folderProperty)
|
||||
if ok {
|
||||
handler = folder + "." + strings.ToUpper(handler[:1]) + handler[1:]
|
||||
}
|
||||
}
|
||||
groupedRoutes.routes = append(groupedRoutes.routes, route{
|
||||
method: mapping[r.Method],
|
||||
path: r.Path,
|
||||
handler: handler,
|
||||
})
|
||||
}
|
||||
|
||||
if value, ok := apiutil.GetAnnotationValue(g.Annotations, "server", "jwt"); ok {
|
||||
groupedRoutes.authName = value
|
||||
groupedRoutes.jwtEnabled = true
|
||||
}
|
||||
if value, ok := apiutil.GetAnnotationValue(g.Annotations, "server", "signature"); ok {
|
||||
if groupedRoutes.authName != "" && groupedRoutes.authName != value {
|
||||
return nil, errors.New("auth signature should config same")
|
||||
}
|
||||
groupedRoutes.signatureEnabled = true
|
||||
}
|
||||
routes = append(routes, groupedRoutes)
|
||||
}
|
||||
|
||||
return routes, nil
|
||||
}
|
||||
63
tools/goctl/api/gogen/genservicecontext.go
Normal file
63
tools/goctl/api/gogen/genservicecontext.go
Normal file
@@ -0,0 +1,63 @@
|
||||
package gogen
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"path"
|
||||
"text/template"
|
||||
|
||||
"zero/tools/goctl/api/spec"
|
||||
"zero/tools/goctl/api/util"
|
||||
)
|
||||
|
||||
const (
|
||||
contextFilename = "servicecontext.go"
|
||||
contextTemplate = `package svc
|
||||
|
||||
import {{.configImport}}
|
||||
|
||||
type ServiceContext struct {
|
||||
Config {{.config}}
|
||||
}
|
||||
|
||||
func NewServiceContext(config {{.config}}) *ServiceContext {
|
||||
return &ServiceContext{Config: config}
|
||||
}
|
||||
|
||||
`
|
||||
)
|
||||
|
||||
func genServiceContext(dir string, api *spec.ApiSpec) error {
|
||||
fp, created, err := util.MaybeCreateFile(dir, contextDir, contextFilename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !created {
|
||||
return nil
|
||||
}
|
||||
defer fp.Close()
|
||||
|
||||
var authNames = getAuths(api)
|
||||
var auths []string
|
||||
for _, item := range authNames {
|
||||
auths = append(auths, fmt.Sprintf("%s config.AuthConfig", item))
|
||||
}
|
||||
|
||||
parentPkg, err := getParentPackage(dir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var configImport = "\"" + path.Join(parentPkg, configDir) + "\""
|
||||
t := template.Must(template.New("contextTemplate").Parse(contextTemplate))
|
||||
buffer := new(bytes.Buffer)
|
||||
err = t.Execute(buffer, map[string]string{
|
||||
"configImport": configImport,
|
||||
"config": "config.Config",
|
||||
})
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
formatCode := formatCode(buffer.String())
|
||||
_, err = fp.WriteString(formatCode)
|
||||
return err
|
||||
}
|
||||
146
tools/goctl/api/gogen/gentypes.go
Normal file
146
tools/goctl/api/gogen/gentypes.go
Normal file
@@ -0,0 +1,146 @@
|
||||
package gogen
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"path"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"zero/tools/goctl/api/spec"
|
||||
apiutil "zero/tools/goctl/api/util"
|
||||
"zero/tools/goctl/util"
|
||||
)
|
||||
|
||||
const (
|
||||
typesFile = "types.go"
|
||||
typesTemplate = `// DO NOT EDIT, generated by goctl
|
||||
package types{{if .containsTime}}
|
||||
import (
|
||||
"time"
|
||||
){{end}}
|
||||
{{.types}}
|
||||
`
|
||||
)
|
||||
|
||||
func BuildTypes(types []spec.Type) (string, error) {
|
||||
var builder strings.Builder
|
||||
first := true
|
||||
for _, tp := range types {
|
||||
if first {
|
||||
first = false
|
||||
} else {
|
||||
builder.WriteString("\n\n")
|
||||
}
|
||||
if err := writeType(&builder, tp, types); err != nil {
|
||||
return "", apiutil.WrapErr(err, "Type "+tp.Name+" generate error")
|
||||
}
|
||||
}
|
||||
|
||||
return builder.String(), nil
|
||||
}
|
||||
|
||||
func genTypes(dir string, api *spec.ApiSpec) error {
|
||||
val, err := BuildTypes(api.Types)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
filename := path.Join(dir, typesDir, typesFile)
|
||||
if err := util.RemoveOrQuit(filename); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fp, created, err := apiutil.MaybeCreateFile(dir, typesDir, typesFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !created {
|
||||
return nil
|
||||
}
|
||||
defer fp.Close()
|
||||
|
||||
t := template.Must(template.New("typesTemplate").Parse(typesTemplate))
|
||||
buffer := new(bytes.Buffer)
|
||||
err = t.Execute(buffer, map[string]interface{}{
|
||||
"types": val,
|
||||
"containsTime": api.ContainsTime(),
|
||||
})
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
formatCode := formatCode(buffer.String())
|
||||
_, err = fp.WriteString(formatCode)
|
||||
return err
|
||||
}
|
||||
|
||||
func convertTypeCase(types []spec.Type, t string) (string, error) {
|
||||
ts, err := apiutil.DecomposeType(t)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var defTypes []string
|
||||
for _, tp := range ts {
|
||||
for _, typ := range types {
|
||||
if typ.Name == tp {
|
||||
defTypes = append(defTypes, tp)
|
||||
}
|
||||
|
||||
if len(typ.Annotations) > 0 {
|
||||
if value, ok := apiutil.GetAnnotationValue(typ.Annotations, "serverReplacer", tp); ok {
|
||||
t = strings.ReplaceAll(t, tp, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, tp := range defTypes {
|
||||
t = strings.ReplaceAll(t, tp, util.Title(tp))
|
||||
}
|
||||
|
||||
return t, nil
|
||||
}
|
||||
|
||||
func writeType(writer io.Writer, tp spec.Type, types []spec.Type) error {
|
||||
fmt.Fprintf(writer, "type %s struct {\n", util.Title(tp.Name))
|
||||
for _, member := range tp.Members {
|
||||
if member.IsInline {
|
||||
var found = false
|
||||
for _, ty := range types {
|
||||
if strings.ToLower(ty.Name) == strings.ToLower(member.Name) {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return errors.New("inline type " + member.Name + " not exist, please correct api file")
|
||||
}
|
||||
if _, err := fmt.Fprintf(writer, "%s\n", strings.Title(member.Type)); err != nil {
|
||||
return err
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
}
|
||||
tpString, err := convertTypeCase(types, member.Type)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pm, err := member.GetPropertyName()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !strings.Contains(pm, "_") {
|
||||
if strings.Title(member.Name) != strings.Title(pm) {
|
||||
fmt.Printf("type: %s, property name %s json tag illegal, "+
|
||||
"should set json tag as `json:\"%s\"` \n", tp.Name, member.Name, util.Untitle(member.Name))
|
||||
}
|
||||
}
|
||||
if err := writeProperty(writer, member.Name, tpString, member.Tag, member.GetComment(), 1); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
fmt.Fprintf(writer, "}")
|
||||
return nil
|
||||
}
|
||||
67
tools/goctl/api/gogen/util.go
Normal file
67
tools/goctl/api/gogen/util.go
Normal file
@@ -0,0 +1,67 @@
|
||||
package gogen
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
goformat "go/format"
|
||||
"io"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"zero/core/collection"
|
||||
"zero/tools/goctl/api/spec"
|
||||
"zero/tools/goctl/api/util"
|
||||
"zero/tools/goctl/vars"
|
||||
)
|
||||
|
||||
func getParentPackage(dir string) (string, error) {
|
||||
absDir, err := filepath.Abs(dir)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
pos := strings.Index(absDir, vars.ProjectName)
|
||||
if pos < 0 {
|
||||
return "", fmt.Errorf("%s not in project directory", dir)
|
||||
}
|
||||
|
||||
return absDir[pos:], nil
|
||||
}
|
||||
|
||||
func writeIndent(writer io.Writer, indent int) {
|
||||
for i := 0; i < indent; i++ {
|
||||
fmt.Fprint(writer, "\t")
|
||||
}
|
||||
}
|
||||
|
||||
func writeProperty(writer io.Writer, name, tp, tag, comment string, indent int) error {
|
||||
writeIndent(writer, indent)
|
||||
var err error
|
||||
if len(comment) > 0 {
|
||||
comment = strings.TrimPrefix(comment, "//")
|
||||
comment = "//" + comment
|
||||
_, err = fmt.Fprintf(writer, "%s %s %s %s\n", strings.Title(name), tp, tag, comment)
|
||||
} else {
|
||||
_, err = fmt.Fprintf(writer, "%s %s %s\n", strings.Title(name), tp, tag)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func getAuths(api *spec.ApiSpec) []string {
|
||||
var authNames = collection.NewSet()
|
||||
for _, g := range api.Service.Groups {
|
||||
if value, ok := util.GetAnnotationValue(g.Annotations, "server", "jwt"); ok {
|
||||
authNames.Add(value)
|
||||
}
|
||||
if value, ok := util.GetAnnotationValue(g.Annotations, "server", "signature"); ok {
|
||||
authNames.Add(value)
|
||||
}
|
||||
}
|
||||
return authNames.KeysStr()
|
||||
}
|
||||
|
||||
func formatCode(code string) string {
|
||||
ret, err := goformat.Source([]byte(code))
|
||||
if err != nil {
|
||||
return code
|
||||
}
|
||||
return string(ret)
|
||||
}
|
||||
12
tools/goctl/api/gogen/vars.go
Normal file
12
tools/goctl/api/gogen/vars.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package gogen
|
||||
|
||||
const (
|
||||
interval = "internal/"
|
||||
typesPacket = "types"
|
||||
configDir = interval + "config"
|
||||
contextDir = interval + "svc"
|
||||
handlerDir = interval + "handler"
|
||||
logicDir = interval + "logic"
|
||||
typesDir = interval + typesPacket
|
||||
folderProperty = "folder"
|
||||
)
|
||||
46
tools/goctl/api/javagen/gen.go
Normal file
46
tools/goctl/api/javagen/gen.go
Normal file
@@ -0,0 +1,46 @@
|
||||
package javagen
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"zero/core/lang"
|
||||
"zero/tools/goctl/api/parser"
|
||||
"zero/tools/goctl/util"
|
||||
|
||||
"github.com/logrusorgru/aurora"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
func JavaCommand(c *cli.Context) error {
|
||||
apiFile := c.String("api")
|
||||
dir := c.String("dir")
|
||||
if len(apiFile) == 0 {
|
||||
return errors.New("missing -api")
|
||||
}
|
||||
if len(dir) == 0 {
|
||||
return errors.New("missing -dir")
|
||||
}
|
||||
|
||||
p, err := parser.NewParser(apiFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
api, err := p.Parse()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
packetName := api.Service.Name
|
||||
if strings.HasSuffix(packetName, "-api") {
|
||||
packetName = packetName[:len(packetName)-4]
|
||||
}
|
||||
|
||||
lang.Must(util.MkdirIfNotExist(dir))
|
||||
lang.Must(genPacket(dir, packetName, api))
|
||||
lang.Must(genComponents(dir, packetName, api))
|
||||
|
||||
fmt.Println(aurora.Green("Done."))
|
||||
return nil
|
||||
}
|
||||
85
tools/goctl/api/javagen/gencomponents.go
Normal file
85
tools/goctl/api/javagen/gencomponents.go
Normal file
@@ -0,0 +1,85 @@
|
||||
package javagen
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"path"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"zero/tools/goctl/api/spec"
|
||||
apiutil "zero/tools/goctl/api/util"
|
||||
"zero/tools/goctl/util"
|
||||
)
|
||||
|
||||
const (
|
||||
componentTemplate = `// DO NOT EDIT, generated by goctl
|
||||
package com.xhb.logic.http.packet.{{.packet}}.model;
|
||||
|
||||
import com.xhb.logic.http.DeProguardable;
|
||||
|
||||
{{.componentType}}
|
||||
`
|
||||
)
|
||||
|
||||
func genComponents(dir, packetName string, api *spec.ApiSpec) error {
|
||||
types := apiutil.GetSharedTypes(api)
|
||||
if len(types) == 0 {
|
||||
return nil
|
||||
}
|
||||
for _, ty := range types {
|
||||
if err := createComponent(dir, packetName, ty); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func createComponent(dir, packetName string, ty spec.Type) error {
|
||||
modelFile := util.Title(ty.Name) + ".java"
|
||||
filename := path.Join(dir, modelDir, modelFile)
|
||||
if err := util.RemoveOrQuit(filename); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fp, created, err := apiutil.MaybeCreateFile(dir, modelDir, modelFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !created {
|
||||
return nil
|
||||
}
|
||||
defer fp.Close()
|
||||
|
||||
tys, err := buildType(ty)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
t := template.Must(template.New("componentType").Parse(componentTemplate))
|
||||
return t.Execute(fp, map[string]string{
|
||||
"componentType": tys,
|
||||
"packet": packetName,
|
||||
})
|
||||
}
|
||||
|
||||
func buildType(ty spec.Type) (string, error) {
|
||||
var builder strings.Builder
|
||||
if err := writeType(&builder, ty); err != nil {
|
||||
return "", apiutil.WrapErr(err, "Type "+ty.Name+" generate error")
|
||||
}
|
||||
return builder.String(), nil
|
||||
}
|
||||
|
||||
func writeType(writer io.Writer, tp spec.Type) error {
|
||||
fmt.Fprintf(writer, "public class %s implements DeProguardable {\n", util.Title(tp.Name))
|
||||
for _, member := range tp.Members {
|
||||
if err := writeProperty(writer, member, 1); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
genGetSet(writer, tp, 1)
|
||||
fmt.Fprintf(writer, "}\n")
|
||||
return nil
|
||||
}
|
||||
277
tools/goctl/api/javagen/genpacket.go
Normal file
277
tools/goctl/api/javagen/genpacket.go
Normal file
@@ -0,0 +1,277 @@
|
||||
package javagen
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"zero/core/stringx"
|
||||
"zero/tools/goctl/api/spec"
|
||||
apiutil "zero/tools/goctl/api/util"
|
||||
"zero/tools/goctl/util"
|
||||
)
|
||||
|
||||
const packetTemplate = `package com.xhb.logic.http.packet.{{.packet}};
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.xhb.commons.JSON;
|
||||
import com.xhb.commons.JsonParser;
|
||||
import com.xhb.core.network.HttpRequestClient;
|
||||
import com.xhb.core.packet.HttpRequestPacket;
|
||||
import com.xhb.core.response.HttpResponseData;
|
||||
import com.xhb.logic.http.DeProguardable;
|
||||
{{.import}}
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.json.JSONObject;
|
||||
|
||||
public class {{.packetName}} extends HttpRequestPacket<{{.packetName}}.{{.packetName}}Response> {
|
||||
|
||||
{{.paramsDeclaration}}
|
||||
|
||||
public {{.packetName}}({{.params}}{{.requestType}} request) {
|
||||
super(request);
|
||||
this.request = request;{{.paramsSet}}
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpRequestClient.Method requestMethod() {
|
||||
return HttpRequestClient.Method.{{.method}};
|
||||
}
|
||||
|
||||
@Override
|
||||
public String requestUri() {
|
||||
return {{.uri}};
|
||||
}
|
||||
|
||||
@Override
|
||||
public {{.packetName}}Response newInstanceFrom(JSON json) {
|
||||
return new {{.packetName}}Response(json);
|
||||
}
|
||||
|
||||
public static class {{.packetName}}Response extends HttpResponseData {
|
||||
|
||||
private {{.responseType}} responseData;
|
||||
|
||||
{{.packetName}}Response(@NotNull JSON json) {
|
||||
super(json);
|
||||
JSONObject jsonObject = json.asObject();
|
||||
if (JsonParser.hasKey(jsonObject, "data")) {
|
||||
Gson gson = new Gson();
|
||||
JSONObject dataJson = JsonParser.getJSONObject(jsonObject, "data");
|
||||
responseData = gson.fromJson(dataJson.toString(), {{.responseType}}.class);
|
||||
}
|
||||
}
|
||||
|
||||
public {{.responseType}} get{{.responseType}} () {
|
||||
return responseData;
|
||||
}
|
||||
}
|
||||
|
||||
{{.types}}
|
||||
}
|
||||
`
|
||||
|
||||
func genPacket(dir, packetName string, api *spec.ApiSpec) error {
|
||||
for _, route := range api.Service.Routes {
|
||||
if err := createWith(dir, api, route, packetName); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func createWith(dir string, api *spec.ApiSpec, route spec.Route, packetName string) error {
|
||||
packet, ok := apiutil.GetAnnotationValue(route.Annotations, "server", "handler")
|
||||
packet = strings.Replace(packet, "Handler", "Packet", 1)
|
||||
if !ok {
|
||||
return fmt.Errorf("missing packet annotation for %q", route.Path)
|
||||
}
|
||||
|
||||
javaFile := packet + ".java"
|
||||
fp, created, err := apiutil.MaybeCreateFile(dir, "", javaFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !created {
|
||||
return nil
|
||||
}
|
||||
defer fp.Close()
|
||||
|
||||
var builder strings.Builder
|
||||
var first bool
|
||||
tps := apiutil.GetLocalTypes(api, route)
|
||||
|
||||
for _, tp := range tps {
|
||||
if first {
|
||||
first = false
|
||||
} else {
|
||||
fmt.Fprintln(&builder)
|
||||
}
|
||||
if err := genType(&builder, tp); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
types := builder.String()
|
||||
writeIndent(&builder, 1)
|
||||
|
||||
params := paramsForRoute(route)
|
||||
paramsDeclaration := declarationForRoute(route)
|
||||
paramsSet := paramsSet(route)
|
||||
|
||||
t := template.Must(template.New("packetTemplate").Parse(packetTemplate))
|
||||
var tmplBytes bytes.Buffer
|
||||
err = t.Execute(&tmplBytes, map[string]string{
|
||||
"packetName": packet,
|
||||
"method": strings.ToUpper(route.Method),
|
||||
"uri": processUri(route),
|
||||
"types": strings.TrimSpace(types),
|
||||
"responseType": stringx.TakeOne(util.Title(route.ResponseType.Name), "Object"),
|
||||
"params": params,
|
||||
"paramsDeclaration": strings.TrimSpace(paramsDeclaration),
|
||||
"paramsSet": paramsSet,
|
||||
"packet": packetName,
|
||||
"requestType": util.Title(route.RequestType.Name),
|
||||
"import": getImports(api, route, packetName),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
formatFile(&tmplBytes, fp)
|
||||
return nil
|
||||
}
|
||||
|
||||
func getImports(api *spec.ApiSpec, route spec.Route, packetName string) string {
|
||||
var builder strings.Builder
|
||||
allTypes := apiutil.GetAllTypes(api, route)
|
||||
sharedTypes := apiutil.GetSharedTypes(api)
|
||||
for _, at := range allTypes {
|
||||
for _, item := range sharedTypes {
|
||||
if item.Name == at.Name {
|
||||
fmt.Fprintf(&builder, "import com.xhb.logic.http.packet.%s.model.%s;\n", packetName, item.Name)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return builder.String()
|
||||
}
|
||||
|
||||
func formatFile(tmplBytes *bytes.Buffer, file *os.File) {
|
||||
scanner := bufio.NewScanner(tmplBytes)
|
||||
builder := bufio.NewWriter(file)
|
||||
defer builder.Flush()
|
||||
preIsBreakLine := false
|
||||
for scanner.Scan() {
|
||||
text := strings.TrimSpace(scanner.Text())
|
||||
if text == "" && preIsBreakLine {
|
||||
continue
|
||||
}
|
||||
preIsBreakLine = text == ""
|
||||
builder.WriteString(scanner.Text() + "\n")
|
||||
}
|
||||
if err := scanner.Err(); err != nil {
|
||||
println(err)
|
||||
}
|
||||
}
|
||||
|
||||
func paramsSet(route spec.Route) string {
|
||||
path := route.Path
|
||||
cops := strings.Split(path, "/")
|
||||
var builder strings.Builder
|
||||
for _, cop := range cops {
|
||||
if len(cop) == 0 {
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(cop, ":") {
|
||||
param := cop[1:]
|
||||
builder.WriteString("\n")
|
||||
builder.WriteString(fmt.Sprintf("\t\tthis.%s = %s;", param, param))
|
||||
}
|
||||
}
|
||||
result := builder.String()
|
||||
return result
|
||||
}
|
||||
|
||||
func paramsForRoute(route spec.Route) string {
|
||||
path := route.Path
|
||||
cops := strings.Split(path, "/")
|
||||
var builder strings.Builder
|
||||
for _, cop := range cops {
|
||||
if len(cop) == 0 {
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(cop, ":") {
|
||||
builder.WriteString(fmt.Sprintf("String %s, ", cop[1:]))
|
||||
}
|
||||
}
|
||||
return builder.String()
|
||||
}
|
||||
|
||||
func declarationForRoute(route spec.Route) string {
|
||||
path := route.Path
|
||||
cops := strings.Split(path, "/")
|
||||
var builder strings.Builder
|
||||
writeIndent(&builder, 1)
|
||||
for _, cop := range cops {
|
||||
if len(cop) == 0 {
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(cop, ":") {
|
||||
writeIndent(&builder, 1)
|
||||
builder.WriteString(fmt.Sprintf("private String %s;\n", cop[1:]))
|
||||
}
|
||||
}
|
||||
result := strings.TrimSpace(builder.String())
|
||||
if len(result) > 0 {
|
||||
result = "\n" + result
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func processUri(route spec.Route) string {
|
||||
path := route.Path
|
||||
var builder strings.Builder
|
||||
cops := strings.Split(path, "/")
|
||||
for index, cop := range cops {
|
||||
if len(cop) == 0 {
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(cop, ":") {
|
||||
builder.WriteString("/\" + " + cop[1:] + " + \"")
|
||||
} else {
|
||||
builder.WriteString("/" + cop)
|
||||
if index == len(cops)-1 {
|
||||
builder.WriteString("\"")
|
||||
}
|
||||
}
|
||||
}
|
||||
result := builder.String()
|
||||
if strings.HasSuffix(result, " + \"") {
|
||||
result = result[:len(result)-4]
|
||||
}
|
||||
if strings.HasPrefix(result, "/") {
|
||||
result = "\"" + result
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func genType(writer io.Writer, tp spec.Type) error {
|
||||
writeIndent(writer, 1)
|
||||
fmt.Fprintf(writer, "static class %s implements DeProguardable {\n", util.Title(tp.Name))
|
||||
for _, member := range tp.Members {
|
||||
if err := writeProperty(writer, member, 2); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
writeBreakline(writer)
|
||||
writeIndent(writer, 1)
|
||||
genGetSet(writer, tp, 2)
|
||||
writeIndent(writer, 1)
|
||||
fmt.Fprintln(writer, "}")
|
||||
return nil
|
||||
}
|
||||
163
tools/goctl/api/javagen/util.go
Normal file
163
tools/goctl/api/javagen/util.go
Normal file
@@ -0,0 +1,163 @@
|
||||
package javagen
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"zero/tools/goctl/api/spec"
|
||||
apiutil "zero/tools/goctl/api/util"
|
||||
"zero/tools/goctl/util"
|
||||
)
|
||||
|
||||
const getSetTemplate = `
|
||||
{{.indent}}{{.decorator}}
|
||||
{{.indent}}public {{.returnType}} get{{.property}}() {
|
||||
{{.indent}} return this.{{.propertyValue}};
|
||||
{{.indent}}}
|
||||
|
||||
{{.indent}}public void set{{.property}}({{.type}} {{.propertyValue}}) {
|
||||
{{.indent}} this.{{.propertyValue}} = {{.propertyValue}};
|
||||
{{.indent}}}
|
||||
`
|
||||
|
||||
func writeProperty(writer io.Writer, member spec.Member, indent int) error {
|
||||
writeIndent(writer, indent)
|
||||
ty, err := goTypeToJava(member.Type)
|
||||
ty = strings.Replace(ty, "*", "", 1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
name, err := member.GetPropertyName()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = fmt.Fprintf(writer, "private %s %s", ty, name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
writeDefaultValue(writer, member)
|
||||
fmt.Fprint(writer, ";\n")
|
||||
return err
|
||||
}
|
||||
|
||||
func writeDefaultValue(writer io.Writer, member spec.Member) error {
|
||||
switch member.Type {
|
||||
case "string":
|
||||
_, err := fmt.Fprintf(writer, " = \"\"")
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeIndent(writer io.Writer, indent int) {
|
||||
for i := 0; i < indent; i++ {
|
||||
fmt.Fprint(writer, "\t")
|
||||
}
|
||||
}
|
||||
|
||||
func indentString(indent int) string {
|
||||
var result = ""
|
||||
for i := 0; i < indent; i++ {
|
||||
result += "\t"
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func writeBreakline(writer io.Writer) {
|
||||
fmt.Fprint(writer, "\n")
|
||||
}
|
||||
|
||||
func isPrimitiveType(tp string) bool {
|
||||
switch tp {
|
||||
case "int", "int32", "int64":
|
||||
return true
|
||||
case "float", "float32", "float64":
|
||||
return true
|
||||
case "bool":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func goTypeToJava(tp string) (string, error) {
|
||||
if len(tp) == 0 {
|
||||
return "", errors.New("property type empty")
|
||||
}
|
||||
if strings.HasPrefix(tp, "*") {
|
||||
tp = tp[1:]
|
||||
}
|
||||
switch tp {
|
||||
case "string":
|
||||
return "String", nil
|
||||
case "int64":
|
||||
return "long", nil
|
||||
case "int", "int8", "int32":
|
||||
return "int", nil
|
||||
case "float", "float32", "float64":
|
||||
return "double", nil
|
||||
case "bool":
|
||||
return "boolean", nil
|
||||
}
|
||||
if strings.HasPrefix(tp, "[]") {
|
||||
tys, err := apiutil.DecomposeType(tp)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if len(tys) == 0 {
|
||||
return "", fmt.Errorf("%s tp parse error", tp)
|
||||
}
|
||||
return fmt.Sprintf("java.util.ArrayList<%s>", util.Title(tys[0])), nil
|
||||
} else if strings.HasPrefix(tp, "map") {
|
||||
tys, err := apiutil.DecomposeType(tp)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if len(tys) == 2 {
|
||||
return "", fmt.Errorf("%s tp parse error", tp)
|
||||
}
|
||||
return fmt.Sprintf("java.util.HashMap<String, %s>", util.Title(tys[1])), nil
|
||||
}
|
||||
return util.Title(tp), nil
|
||||
}
|
||||
|
||||
func genGetSet(writer io.Writer, tp spec.Type, indent int) error {
|
||||
t := template.Must(template.New("getSetTemplate").Parse(getSetTemplate))
|
||||
for _, member := range tp.Members {
|
||||
var tmplBytes bytes.Buffer
|
||||
|
||||
oty, err := goTypeToJava(member.Type)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tyString := oty
|
||||
decorator := ""
|
||||
if !isPrimitiveType(member.Type) {
|
||||
if member.IsOptional() {
|
||||
decorator = "@org.jetbrains.annotations.Nullable "
|
||||
} else {
|
||||
decorator = "@org.jetbrains.annotations.NotNull "
|
||||
}
|
||||
tyString = decorator + tyString
|
||||
}
|
||||
|
||||
err = t.Execute(&tmplBytes, map[string]string{
|
||||
"property": util.Title(member.Name),
|
||||
"propertyValue": util.Untitle(member.Name),
|
||||
"type": tyString,
|
||||
"decorator": decorator,
|
||||
"returnType": oty,
|
||||
"indent": indentString(indent),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r := tmplBytes.String()
|
||||
r = strings.Replace(r, " boolean get", " boolean is", 1)
|
||||
writer.Write([]byte(r))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
3
tools/goctl/api/javagen/vars.go
Normal file
3
tools/goctl/api/javagen/vars.go
Normal file
@@ -0,0 +1,3 @@
|
||||
package javagen
|
||||
|
||||
const modelDir = "model"
|
||||
21
tools/goctl/api/main.go
Normal file
21
tools/goctl/api/main.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"zero/core/lang"
|
||||
"zero/tools/goctl/api/parser"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if len(os.Args) <= 1 {
|
||||
return
|
||||
}
|
||||
|
||||
p, err := parser.NewParser(os.Args[1])
|
||||
lang.Must(err)
|
||||
api, err := p.Parse()
|
||||
lang.Must(err)
|
||||
fmt.Println(api)
|
||||
}
|
||||
182
tools/goctl/api/parser/basestate.go
Normal file
182
tools/goctl/api/parser/basestate.go
Normal file
@@ -0,0 +1,182 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
startState = iota
|
||||
attrNameState
|
||||
attrValueState
|
||||
attrColonState
|
||||
multilineState
|
||||
)
|
||||
|
||||
type baseState struct {
|
||||
r *bufio.Reader
|
||||
lineNumber *int
|
||||
}
|
||||
|
||||
func newBaseState(r *bufio.Reader, lineNumber *int) *baseState {
|
||||
return &baseState{
|
||||
r: r,
|
||||
lineNumber: lineNumber,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *baseState) parseProperties() (map[string]string, error) {
|
||||
var r = s.r
|
||||
var attributes = make(map[string]string)
|
||||
var builder strings.Builder
|
||||
var key string
|
||||
var st = startState
|
||||
|
||||
for {
|
||||
ch, err := s.read()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch st {
|
||||
case startState:
|
||||
switch {
|
||||
case isNewline(ch):
|
||||
return nil, fmt.Errorf("%q should be on the same line with %q", leftParenthesis, infoDirective)
|
||||
case isSpace(ch):
|
||||
continue
|
||||
case ch == leftParenthesis:
|
||||
st = attrNameState
|
||||
default:
|
||||
return nil, fmt.Errorf("unexpected char %q after %q", ch, infoDirective)
|
||||
}
|
||||
case attrNameState:
|
||||
switch {
|
||||
case isNewline(ch):
|
||||
if builder.Len() > 0 {
|
||||
return nil, fmt.Errorf("unexpected newline after %q", builder.String())
|
||||
}
|
||||
case isLetterDigit(ch):
|
||||
builder.WriteRune(ch)
|
||||
case isSpace(ch):
|
||||
if builder.Len() > 0 {
|
||||
key = builder.String()
|
||||
builder.Reset()
|
||||
st = attrColonState
|
||||
}
|
||||
case ch == colon:
|
||||
if builder.Len() == 0 {
|
||||
return nil, fmt.Errorf("unexpected leading %q", ch)
|
||||
}
|
||||
key = builder.String()
|
||||
builder.Reset()
|
||||
st = attrValueState
|
||||
case ch == rightParenthesis:
|
||||
return attributes, nil
|
||||
}
|
||||
case attrColonState:
|
||||
switch {
|
||||
case isSpace(ch):
|
||||
continue
|
||||
case ch == colon:
|
||||
st = attrValueState
|
||||
default:
|
||||
return nil, fmt.Errorf("bad char %q after %q in %q", ch, key, infoDirective)
|
||||
}
|
||||
case attrValueState:
|
||||
switch {
|
||||
case ch == multilineBeginTag:
|
||||
if builder.Len() > 0 {
|
||||
return nil, fmt.Errorf("%q before %q", builder.String(), multilineBeginTag)
|
||||
} else {
|
||||
st = multilineState
|
||||
}
|
||||
case isSpace(ch):
|
||||
if builder.Len() > 0 {
|
||||
builder.WriteRune(ch)
|
||||
}
|
||||
case isNewline(ch):
|
||||
attributes[key] = builder.String()
|
||||
builder.Reset()
|
||||
st = attrNameState
|
||||
case ch == rightParenthesis:
|
||||
attributes[key] = builder.String()
|
||||
builder.Reset()
|
||||
return attributes, nil
|
||||
default:
|
||||
builder.WriteRune(ch)
|
||||
}
|
||||
case multilineState:
|
||||
switch {
|
||||
case ch == multilineEndTag:
|
||||
attributes[key] = builder.String()
|
||||
builder.Reset()
|
||||
st = attrNameState
|
||||
case isNewline(ch):
|
||||
var multipleNewlines bool
|
||||
loopAfterNewline:
|
||||
for {
|
||||
next, err := read(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch {
|
||||
case isSpace(next):
|
||||
continue
|
||||
case isNewline(next):
|
||||
multipleNewlines = true
|
||||
default:
|
||||
if err := unread(r); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
break loopAfterNewline
|
||||
}
|
||||
}
|
||||
|
||||
if multipleNewlines {
|
||||
fmt.Fprintln(&builder)
|
||||
} else {
|
||||
builder.WriteByte(' ')
|
||||
}
|
||||
case ch == rightParenthesis:
|
||||
if builder.Len() > 0 {
|
||||
attributes[key] = builder.String()
|
||||
builder.Reset()
|
||||
}
|
||||
return attributes, nil
|
||||
default:
|
||||
builder.WriteRune(ch)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *baseState) read() (rune, error) {
|
||||
value, err := read(s.r)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if isNewline(value) {
|
||||
*s.lineNumber++
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
|
||||
func (s *baseState) readLine() (string, error) {
|
||||
line, _, err := s.r.ReadLine()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
*s.lineNumber++
|
||||
return string(line), nil
|
||||
}
|
||||
|
||||
func (s *baseState) skipSpaces() error {
|
||||
return skipSpaces(s.r)
|
||||
}
|
||||
|
||||
func (s *baseState) unread() error {
|
||||
return unread(s.r)
|
||||
}
|
||||
20
tools/goctl/api/parser/basestate_test.go
Normal file
20
tools/goctl/api/parser/basestate_test.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestProperties(t *testing.T) {
|
||||
const text = `(summary: hello world)`
|
||||
var builder bytes.Buffer
|
||||
builder.WriteString(text)
|
||||
var lineNumber = 1
|
||||
var state = newBaseState(bufio.NewReader(&builder), &lineNumber)
|
||||
m, err := state.parseProperties()
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, "hello world", m["summary"])
|
||||
}
|
||||
132
tools/goctl/api/parser/entity.go
Normal file
132
tools/goctl/api/parser/entity.go
Normal file
@@ -0,0 +1,132 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"zero/tools/goctl/api/spec"
|
||||
)
|
||||
|
||||
type (
|
||||
entity struct {
|
||||
state *baseState
|
||||
api *spec.ApiSpec
|
||||
parser entityParser
|
||||
}
|
||||
|
||||
entityParser interface {
|
||||
parseLine(line string, api *spec.ApiSpec, annos []spec.Annotation) error
|
||||
setEntityName(name string)
|
||||
}
|
||||
)
|
||||
|
||||
func newEntity(state *baseState, api *spec.ApiSpec, parser entityParser) entity {
|
||||
return entity{
|
||||
state: state,
|
||||
api: api,
|
||||
parser: parser,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *entity) process() error {
|
||||
line, err := s.state.readLine()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) < 2 {
|
||||
return fmt.Errorf("invalid type definition for %q",
|
||||
strings.TrimSpace(strings.Trim(string(line), "{")))
|
||||
}
|
||||
|
||||
if len(fields) == 2 {
|
||||
if fields[1] != leftBrace {
|
||||
return fmt.Errorf("bad string %q after type", fields[1])
|
||||
}
|
||||
} else if len(fields) == 3 {
|
||||
if fields[1] != typeStruct {
|
||||
return fmt.Errorf("bad string %q after type", fields[1])
|
||||
}
|
||||
if fields[2] != leftBrace {
|
||||
return fmt.Errorf("bad string %q after type", fields[2])
|
||||
}
|
||||
}
|
||||
|
||||
s.parser.setEntityName(fields[0])
|
||||
|
||||
var annos []spec.Annotation
|
||||
memberLoop:
|
||||
for {
|
||||
ch, err := s.state.read()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var annoName string
|
||||
var builder strings.Builder
|
||||
switch {
|
||||
case ch == at:
|
||||
annotationLoop:
|
||||
for {
|
||||
next, err := s.state.read()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
switch {
|
||||
case isSpace(next):
|
||||
if builder.Len() > 0 {
|
||||
annoName = builder.String()
|
||||
builder.Reset()
|
||||
}
|
||||
case isNewline(next):
|
||||
if builder.Len() == 0 {
|
||||
return errors.New("invalid annotation format")
|
||||
}
|
||||
case next == leftParenthesis:
|
||||
if builder.Len() == 0 {
|
||||
return errors.New("invalid annotation format")
|
||||
}
|
||||
annoName = builder.String()
|
||||
builder.Reset()
|
||||
if err := s.state.unread(); err != nil {
|
||||
return err
|
||||
}
|
||||
attrs, err := s.state.parseProperties()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
annos = append(annos, spec.Annotation{
|
||||
Name: annoName,
|
||||
Properties: attrs,
|
||||
})
|
||||
break annotationLoop
|
||||
default:
|
||||
builder.WriteRune(next)
|
||||
}
|
||||
}
|
||||
case ch == rightBrace:
|
||||
break memberLoop
|
||||
case isLetterDigit(ch):
|
||||
if err := s.state.unread(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var line string
|
||||
line, err = s.state.readLine()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
line = strings.TrimSpace(line)
|
||||
if err := s.parser.parseLine(line, s.api, annos); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
annos = nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
62
tools/goctl/api/parser/infostate.go
Normal file
62
tools/goctl/api/parser/infostate.go
Normal file
@@ -0,0 +1,62 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"zero/tools/goctl/api/spec"
|
||||
)
|
||||
|
||||
const (
|
||||
titleTag = "title"
|
||||
descTag = "desc"
|
||||
versionTag = "version"
|
||||
authorTag = "author"
|
||||
emailTag = "email"
|
||||
)
|
||||
|
||||
type infoState struct {
|
||||
*baseState
|
||||
innerState int
|
||||
}
|
||||
|
||||
func newInfoState(st *baseState) state {
|
||||
return &infoState{
|
||||
baseState: st,
|
||||
innerState: startState,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *infoState) process(api *spec.ApiSpec) (state, error) {
|
||||
attrs, err := s.parseProperties()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := s.writeInfo(api, attrs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return newRootState(s.r, s.lineNumber), nil
|
||||
}
|
||||
|
||||
func (s *infoState) writeInfo(api *spec.ApiSpec, attrs map[string]string) error {
|
||||
for k, v := range attrs {
|
||||
switch k {
|
||||
case titleTag:
|
||||
api.Info.Title = strings.TrimSpace(v)
|
||||
case descTag:
|
||||
api.Info.Desc = strings.TrimSpace(v)
|
||||
case versionTag:
|
||||
api.Info.Version = strings.TrimSpace(v)
|
||||
case authorTag:
|
||||
api.Info.Author = strings.TrimSpace(v)
|
||||
case emailTag:
|
||||
api.Info.Email = strings.TrimSpace(v)
|
||||
default:
|
||||
return fmt.Errorf("unknown directive %q in %q section", k, infoDirective)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
57
tools/goctl/api/parser/parser.go
Normal file
57
tools/goctl/api/parser/parser.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
|
||||
"zero/tools/goctl/api/spec"
|
||||
)
|
||||
|
||||
type Parser struct {
|
||||
r *bufio.Reader
|
||||
st string
|
||||
}
|
||||
|
||||
func NewParser(filename string) (*Parser, error) {
|
||||
api, err := ioutil.ReadFile(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
info, body, service, err := MatchStruct(string(api))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var buffer = new(bytes.Buffer)
|
||||
buffer.WriteString(info)
|
||||
buffer.WriteString(service)
|
||||
return &Parser{
|
||||
r: bufio.NewReader(buffer),
|
||||
st: body,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (p *Parser) Parse() (api *spec.ApiSpec, err error) {
|
||||
api = new(spec.ApiSpec)
|
||||
types, err := parseStructAst(p.st)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
api.Types = types
|
||||
var lineNumber = 1
|
||||
st := newRootState(p.r, &lineNumber)
|
||||
for {
|
||||
st, err = st.process(api)
|
||||
if err == io.EOF {
|
||||
return api, p.validate(api)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("near line: %d, %s", lineNumber, err.Error())
|
||||
}
|
||||
if st == nil {
|
||||
return api, p.validate(api)
|
||||
}
|
||||
}
|
||||
}
|
||||
109
tools/goctl/api/parser/rootstate.go
Normal file
109
tools/goctl/api/parser/rootstate.go
Normal file
@@ -0,0 +1,109 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"zero/tools/goctl/api/spec"
|
||||
)
|
||||
|
||||
type rootState struct {
|
||||
*baseState
|
||||
}
|
||||
|
||||
func newRootState(r *bufio.Reader, lineNumber *int) state {
|
||||
var state = newBaseState(r, lineNumber)
|
||||
return rootState{
|
||||
baseState: state,
|
||||
}
|
||||
}
|
||||
|
||||
func (s rootState) process(api *spec.ApiSpec) (state, error) {
|
||||
var annos []spec.Annotation
|
||||
var builder strings.Builder
|
||||
for {
|
||||
ch, err := s.read()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch {
|
||||
case isSpace(ch):
|
||||
if builder.Len() == 0 {
|
||||
continue
|
||||
}
|
||||
token := builder.String()
|
||||
builder.Reset()
|
||||
return s.processToken(token, annos)
|
||||
case ch == at:
|
||||
if builder.Len() > 0 {
|
||||
return nil, fmt.Errorf("%q before %q", builder.String(), at)
|
||||
}
|
||||
|
||||
var annoName string
|
||||
annoLoop:
|
||||
for {
|
||||
next, err := s.read()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
switch {
|
||||
case isSpace(next):
|
||||
if builder.Len() > 0 {
|
||||
annoName = builder.String()
|
||||
builder.Reset()
|
||||
}
|
||||
case next == leftParenthesis:
|
||||
if err := s.unread(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if builder.Len() > 0 {
|
||||
annoName = builder.String()
|
||||
builder.Reset()
|
||||
}
|
||||
attrs, err := s.parseProperties()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
annos = append(annos, spec.Annotation{
|
||||
Name: annoName,
|
||||
Properties: attrs,
|
||||
})
|
||||
break annoLoop
|
||||
default:
|
||||
builder.WriteRune(next)
|
||||
}
|
||||
}
|
||||
case ch == leftParenthesis:
|
||||
if builder.Len() == 0 {
|
||||
return nil, fmt.Errorf("incorrect %q at the beginning of the line", leftParenthesis)
|
||||
}
|
||||
if err := s.unread(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
token := builder.String()
|
||||
builder.Reset()
|
||||
return s.processToken(token, annos)
|
||||
case isLetterDigit(ch):
|
||||
builder.WriteRune(ch)
|
||||
case isNewline(ch):
|
||||
if builder.Len() > 0 {
|
||||
return nil, fmt.Errorf("incorrect newline after %q", builder.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s rootState) processToken(token string, annos []spec.Annotation) (state, error) {
|
||||
switch token {
|
||||
case infoDirective:
|
||||
return newInfoState(s.baseState), nil
|
||||
//case typeDirective:
|
||||
//return newTypeState(s.baseState, annos), nil
|
||||
case serviceDirective:
|
||||
return newServiceState(s.baseState, annos), nil
|
||||
default:
|
||||
return nil, fmt.Errorf("wrong directive %q", token)
|
||||
}
|
||||
}
|
||||
97
tools/goctl/api/parser/servicestate.go
Normal file
97
tools/goctl/api/parser/servicestate.go
Normal file
@@ -0,0 +1,97 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"zero/tools/goctl/api/spec"
|
||||
)
|
||||
|
||||
type serviceState struct {
|
||||
*baseState
|
||||
annos []spec.Annotation
|
||||
}
|
||||
|
||||
func newServiceState(state *baseState, annos []spec.Annotation) state {
|
||||
return &serviceState{
|
||||
baseState: state,
|
||||
annos: annos,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *serviceState) process(api *spec.ApiSpec) (state, error) {
|
||||
var name string
|
||||
var routes []spec.Route
|
||||
parser := &serviceEntityParser{
|
||||
acceptName: func(n string) {
|
||||
name = n
|
||||
},
|
||||
acceptRoute: func(route spec.Route) {
|
||||
routes = append(routes, route)
|
||||
},
|
||||
}
|
||||
ent := newEntity(s.baseState, api, parser)
|
||||
if err := ent.process(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
api.Service = spec.Service{
|
||||
Name: name,
|
||||
Annotations: append(api.Service.Annotations, s.annos...),
|
||||
Routes: append(api.Service.Routes, routes...),
|
||||
Groups: append(api.Service.Groups, spec.Group{
|
||||
Annotations: s.annos,
|
||||
Routes: routes,
|
||||
}),
|
||||
}
|
||||
|
||||
return newRootState(s.r, s.lineNumber), nil
|
||||
}
|
||||
|
||||
type serviceEntityParser struct {
|
||||
acceptName func(name string)
|
||||
acceptRoute func(route spec.Route)
|
||||
}
|
||||
|
||||
func (p *serviceEntityParser) parseLine(line string, api *spec.ApiSpec, annos []spec.Annotation) error {
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) < 2 {
|
||||
return fmt.Errorf("wrong line %q", line)
|
||||
}
|
||||
|
||||
method := fields[0]
|
||||
pathAndRequest := fields[1]
|
||||
pos := strings.Index(pathAndRequest, "(")
|
||||
if pos < 0 {
|
||||
return fmt.Errorf("wrong line %q", line)
|
||||
}
|
||||
path := strings.TrimSpace(pathAndRequest[:pos])
|
||||
pathAndRequest = pathAndRequest[pos+1:]
|
||||
pos = strings.Index(pathAndRequest, ")")
|
||||
if pos < 0 {
|
||||
return fmt.Errorf("wrong line %q", line)
|
||||
}
|
||||
req := pathAndRequest[:pos]
|
||||
var returns string
|
||||
if len(fields) > 2 {
|
||||
returns = fields[2]
|
||||
}
|
||||
returns = strings.ReplaceAll(returns, "returns", "")
|
||||
returns = strings.ReplaceAll(returns, "(", "")
|
||||
returns = strings.ReplaceAll(returns, ")", "")
|
||||
returns = strings.TrimSpace(returns)
|
||||
|
||||
p.acceptRoute(spec.Route{
|
||||
Annotations: annos,
|
||||
Method: method,
|
||||
Path: path,
|
||||
RequestType: GetType(api, req),
|
||||
ResponseType: GetType(api, returns),
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *serviceEntityParser) setEntityName(name string) {
|
||||
p.acceptName(name)
|
||||
}
|
||||
7
tools/goctl/api/parser/state.go
Normal file
7
tools/goctl/api/parser/state.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package parser
|
||||
|
||||
import "zero/tools/goctl/api/spec"
|
||||
|
||||
type state interface {
|
||||
process(api *spec.ApiSpec) (state, error)
|
||||
}
|
||||
329
tools/goctl/api/parser/typeparser.go
Normal file
329
tools/goctl/api/parser/typeparser.go
Normal file
@@ -0,0 +1,329 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"zero/tools/goctl/api/spec"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrStructNotFound = errors.New("struct not found")
|
||||
ErrUnSupportType = errors.New("unsupport type")
|
||||
ErrUnSupportInlineType = errors.New("unsupport inline type")
|
||||
interfaceExpr = `interface{}`
|
||||
objectM = make(map[string]*spec.Type)
|
||||
)
|
||||
|
||||
const (
|
||||
golangF = `package ast
|
||||
%s
|
||||
`
|
||||
pkgPrefix = "package"
|
||||
)
|
||||
|
||||
func parseStructAst(golang string) ([]spec.Type, error) {
|
||||
if !strings.HasPrefix(golang, pkgPrefix) {
|
||||
golang = fmt.Sprintf(golangF, golang)
|
||||
}
|
||||
fSet := token.NewFileSet()
|
||||
f, err := parser.ParseFile(fSet, "", golang, parser.ParseComments)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
commentMap := ast.NewCommentMap(fSet, f, f.Comments)
|
||||
f.Comments = commentMap.Filter(f).Comments()
|
||||
scope := f.Scope
|
||||
if scope == nil {
|
||||
return nil, ErrStructNotFound
|
||||
}
|
||||
objects := scope.Objects
|
||||
structs := make([]*spec.Type, 0)
|
||||
for structName, obj := range objects {
|
||||
st, err := parseObject(structName, obj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
structs = append(structs, st)
|
||||
}
|
||||
sort.Slice(structs, func(i, j int) bool {
|
||||
return structs[i].Name < structs[j].Name
|
||||
})
|
||||
resp := make([]spec.Type, 0)
|
||||
for _, item := range structs {
|
||||
resp = append(resp, *item)
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func parseObject(structName string, obj *ast.Object) (*spec.Type, error) {
|
||||
if data, ok := objectM[structName]; ok {
|
||||
return data, nil
|
||||
}
|
||||
var st spec.Type
|
||||
st.Name = structName
|
||||
if obj.Decl == nil {
|
||||
objectM[structName] = &st
|
||||
return &st, nil
|
||||
}
|
||||
decl, ok := obj.Decl.(*ast.TypeSpec)
|
||||
if !ok {
|
||||
objectM[structName] = &st
|
||||
return &st, nil
|
||||
}
|
||||
if decl.Type == nil {
|
||||
objectM[structName] = &st
|
||||
return &st, nil
|
||||
}
|
||||
tp, ok := decl.Type.(*ast.StructType)
|
||||
if !ok {
|
||||
objectM[structName] = &st
|
||||
return &st, nil
|
||||
}
|
||||
fields := tp.Fields
|
||||
if fields == nil {
|
||||
objectM[structName] = &st
|
||||
return &st, nil
|
||||
}
|
||||
fieldList := fields.List
|
||||
members, err := parseFields(fieldList)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
st.Members = members
|
||||
objectM[structName] = &st
|
||||
return &st, nil
|
||||
}
|
||||
|
||||
func parseFields(fields []*ast.Field) ([]spec.Member, error) {
|
||||
members := make([]spec.Member, 0)
|
||||
for _, field := range fields {
|
||||
docs := parseCommentOrDoc(field.Doc)
|
||||
comments := parseCommentOrDoc(field.Comment)
|
||||
name := parseName(field.Names)
|
||||
tp, stringExpr, err := parseType(field.Type)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tag := parseTag(field.Tag)
|
||||
isInline := name == ""
|
||||
if isInline {
|
||||
var err error
|
||||
name, err = getInlineName(tp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
members = append(members, spec.Member{
|
||||
Name: name,
|
||||
Type: stringExpr,
|
||||
Expr: tp,
|
||||
Tag: tag,
|
||||
Comments: comments,
|
||||
Docs: docs,
|
||||
IsInline: isInline,
|
||||
})
|
||||
|
||||
}
|
||||
return members, nil
|
||||
}
|
||||
|
||||
func getInlineName(tp interface{}) (string, error) {
|
||||
switch v := tp.(type) {
|
||||
case *spec.Type:
|
||||
return v.Name, nil
|
||||
case *spec.PointerType:
|
||||
return getInlineName(v.Star)
|
||||
case *spec.StructType:
|
||||
return v.StringExpr, nil
|
||||
default:
|
||||
return "", ErrUnSupportInlineType
|
||||
}
|
||||
}
|
||||
|
||||
func getInlineTypePrefix(tp interface{}) (string, error) {
|
||||
if tp == nil {
|
||||
return "", nil
|
||||
}
|
||||
switch tp.(type) {
|
||||
case *ast.Ident:
|
||||
return "", nil
|
||||
case *ast.StarExpr:
|
||||
return "*", nil
|
||||
case *ast.TypeSpec:
|
||||
return "", nil
|
||||
default:
|
||||
return "", ErrUnSupportInlineType
|
||||
}
|
||||
}
|
||||
|
||||
func parseTag(basicLit *ast.BasicLit) string {
|
||||
if basicLit == nil {
|
||||
return ""
|
||||
}
|
||||
return basicLit.Value
|
||||
}
|
||||
|
||||
// returns
|
||||
// resp1:type can convert to *spec.PointerType|*spec.BasicType|*spec.MapType|*spec.ArrayType|*spec.InterfaceType
|
||||
// resp2:type's string expression,like int、string、[]int64、map[string]User、*User
|
||||
// resp3:error
|
||||
func parseType(expr ast.Expr) (interface{}, string, error) {
|
||||
if expr == nil {
|
||||
return nil, "", ErrUnSupportType
|
||||
}
|
||||
switch v := expr.(type) {
|
||||
case *ast.StarExpr:
|
||||
star, stringExpr, err := parseType(v.X)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
e := fmt.Sprintf("*%s", stringExpr)
|
||||
return &spec.PointerType{Star: star, StringExpr: e}, e, nil
|
||||
case *ast.Ident:
|
||||
if isBasicType(v.Name) {
|
||||
return &spec.BasicType{Name: v.Name, StringExpr: v.Name}, v.Name, nil
|
||||
} else if v.Obj != nil {
|
||||
obj := v.Obj
|
||||
if obj.Name != v.Name { // 防止引用自己而无限递归
|
||||
specType, err := parseObject(v.Name, v.Obj)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
} else {
|
||||
return specType, v.Obj.Name, nil
|
||||
}
|
||||
} else {
|
||||
inlineType, err := getInlineTypePrefix(obj.Decl)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
return &spec.StructType{
|
||||
StringExpr: fmt.Sprintf("%s%s", inlineType, v.Name),
|
||||
}, v.Name, nil
|
||||
}
|
||||
} else {
|
||||
return nil, "", fmt.Errorf(" [%s] - member is not exist", v.Name)
|
||||
}
|
||||
case *ast.MapType:
|
||||
key, keyStringExpr, err := parseType(v.Key)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
value, valueStringExpr, err := parseType(v.Value)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
keyType, ok := key.(*spec.BasicType)
|
||||
if !ok {
|
||||
return nil, "", fmt.Errorf("[%+v] - unsupport type of map key", v.Key)
|
||||
}
|
||||
e := fmt.Sprintf("map[%s]%s", keyStringExpr, valueStringExpr)
|
||||
return &spec.MapType{
|
||||
Key: keyType.Name,
|
||||
Value: value,
|
||||
StringExpr: e,
|
||||
}, e, nil
|
||||
case *ast.ArrayType:
|
||||
arrayType, stringExpr, err := parseType(v.Elt)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
e := fmt.Sprintf("[]%s", stringExpr)
|
||||
return &spec.ArrayType{ArrayType: arrayType, StringExpr: e}, e, nil
|
||||
case *ast.InterfaceType:
|
||||
return &spec.InterfaceType{StringExpr: interfaceExpr}, interfaceExpr, nil
|
||||
case *ast.ChanType:
|
||||
return nil, "", errors.New("[chan] - unsupport type")
|
||||
case *ast.FuncType:
|
||||
return nil, "", errors.New("[func] - unsupport type")
|
||||
case *ast.StructType: // todo can optimize
|
||||
return nil, "", errors.New("[struct] - unsupport inline struct type")
|
||||
case *ast.SelectorExpr:
|
||||
x := v.X
|
||||
sel := v.Sel
|
||||
xIdent, ok := x.(*ast.Ident)
|
||||
if ok {
|
||||
name := xIdent.Name
|
||||
if name != "time" && sel.Name != "Time" {
|
||||
return nil, "", fmt.Errorf("[outter package] - package:%s, unsupport type", name)
|
||||
}
|
||||
tm := fmt.Sprintf("time.Time")
|
||||
return &spec.TimeType{
|
||||
StringExpr: tm,
|
||||
}, tm, nil
|
||||
}
|
||||
return nil, "", ErrUnSupportType
|
||||
default:
|
||||
return nil, "", ErrUnSupportType
|
||||
}
|
||||
}
|
||||
|
||||
func isBasicType(tp string) bool {
|
||||
switch tp {
|
||||
case
|
||||
"bool",
|
||||
"uint8",
|
||||
"uint16",
|
||||
"uint32",
|
||||
"uint64",
|
||||
"int8",
|
||||
"int16",
|
||||
"int32",
|
||||
"int64",
|
||||
"float32",
|
||||
"float64",
|
||||
"complex64",
|
||||
"complex128",
|
||||
"string",
|
||||
"int",
|
||||
"uint",
|
||||
"uintptr",
|
||||
"byte",
|
||||
"rune",
|
||||
"Type",
|
||||
"Type1",
|
||||
"IntegerType",
|
||||
"FloatType",
|
||||
"ComplexType":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
func parseName(names []*ast.Ident) string {
|
||||
if len(names) == 0 {
|
||||
return ""
|
||||
}
|
||||
name := names[0]
|
||||
return parseIdent(name)
|
||||
}
|
||||
|
||||
func parseIdent(ident *ast.Ident) string {
|
||||
if ident == nil {
|
||||
return ""
|
||||
}
|
||||
return ident.Name
|
||||
}
|
||||
|
||||
func 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
|
||||
}
|
||||
95
tools/goctl/api/parser/typestate.go
Normal file
95
tools/goctl/api/parser/typestate.go
Normal file
@@ -0,0 +1,95 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"zero/tools/goctl/api/spec"
|
||||
"zero/tools/goctl/util"
|
||||
)
|
||||
|
||||
type typeState struct {
|
||||
*baseState
|
||||
annos []spec.Annotation
|
||||
}
|
||||
|
||||
func newTypeState(state *baseState, annos []spec.Annotation) state {
|
||||
return &typeState{
|
||||
baseState: state,
|
||||
annos: annos,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *typeState) process(api *spec.ApiSpec) (state, error) {
|
||||
var name string
|
||||
var members []spec.Member
|
||||
parser := &typeEntityParser{
|
||||
acceptName: func(n string) {
|
||||
name = n
|
||||
},
|
||||
acceptMember: func(member spec.Member) {
|
||||
members = append(members, member)
|
||||
},
|
||||
}
|
||||
ent := newEntity(s.baseState, api, parser)
|
||||
if err := ent.process(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
api.Types = append(api.Types, spec.Type{
|
||||
Name: name,
|
||||
Annotations: s.annos,
|
||||
Members: members,
|
||||
})
|
||||
|
||||
return newRootState(s.r, s.lineNumber), nil
|
||||
}
|
||||
|
||||
type typeEntityParser struct {
|
||||
acceptName func(name string)
|
||||
acceptMember func(member spec.Member)
|
||||
}
|
||||
|
||||
func (p *typeEntityParser) parseLine(line string, api *spec.ApiSpec, annos []spec.Annotation) error {
|
||||
index := strings.Index(line, "//")
|
||||
comment := ""
|
||||
if index >= 0 {
|
||||
comment = line[index+2:]
|
||||
line = strings.TrimSpace(line[:index])
|
||||
}
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) == 0 {
|
||||
return nil
|
||||
}
|
||||
if len(fields) == 1 {
|
||||
p.acceptMember(spec.Member{
|
||||
Annotations: annos,
|
||||
Name: fields[0],
|
||||
Type: fields[0],
|
||||
IsInline: true,
|
||||
})
|
||||
return nil
|
||||
}
|
||||
name := fields[0]
|
||||
tp := fields[1]
|
||||
var tag string
|
||||
if len(fields) > 2 {
|
||||
tag = fields[2]
|
||||
} else {
|
||||
tag = fmt.Sprintf("`json:\"%s\"`", util.Untitle(name))
|
||||
}
|
||||
|
||||
p.acceptMember(spec.Member{
|
||||
Annotations: annos,
|
||||
Name: name,
|
||||
Type: tp,
|
||||
Tag: tag,
|
||||
Comment: comment,
|
||||
IsInline: false,
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *typeEntityParser) setEntityName(name string) {
|
||||
p.acceptName(name)
|
||||
}
|
||||
103
tools/goctl/api/parser/util.go
Normal file
103
tools/goctl/api/parser/util.go
Normal file
@@ -0,0 +1,103 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"zero/tools/goctl/api/spec"
|
||||
)
|
||||
|
||||
const (
|
||||
// struct匹配
|
||||
typeRegex = `(?m)(?m)(^ *type\s+[a-zA-Z][a-zA-Z0-9_-]+\s+(((struct)\s*?\{[\w\W]*?[^\{]\})|([a-zA-Z][a-zA-Z0-9_-]+)))|(^ *type\s*?\([\w\W]+\}\s*\))`
|
||||
)
|
||||
|
||||
var (
|
||||
emptyStrcut = errors.New("struct body not found")
|
||||
)
|
||||
|
||||
var emptyType spec.Type
|
||||
|
||||
func GetType(api *spec.ApiSpec, t string) spec.Type {
|
||||
for _, tp := range api.Types {
|
||||
if tp.Name == t {
|
||||
return tp
|
||||
}
|
||||
}
|
||||
|
||||
return emptyType
|
||||
}
|
||||
|
||||
func isLetterDigit(r rune) bool {
|
||||
return (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || ('0' <= r && r <= '9')
|
||||
}
|
||||
|
||||
func isSpace(r rune) bool {
|
||||
return r == ' ' || r == '\t'
|
||||
}
|
||||
|
||||
func isNewline(r rune) bool {
|
||||
return r == '\n' || r == '\r'
|
||||
}
|
||||
|
||||
func read(r *bufio.Reader) (rune, error) {
|
||||
ch, _, err := r.ReadRune()
|
||||
return ch, err
|
||||
}
|
||||
|
||||
func readLine(r *bufio.Reader) (string, error) {
|
||||
line, _, err := r.ReadLine()
|
||||
if err != nil {
|
||||
return "", err
|
||||
} else {
|
||||
return string(line), nil
|
||||
}
|
||||
}
|
||||
|
||||
func skipSpaces(r *bufio.Reader) error {
|
||||
for {
|
||||
next, err := read(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !isSpace(next) {
|
||||
return unread(r)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func unread(r *bufio.Reader) error {
|
||||
return r.UnreadRune()
|
||||
}
|
||||
|
||||
func MatchStruct(api string) (info, structBody, service string, err error) {
|
||||
r := regexp.MustCompile(typeRegex)
|
||||
indexes := r.FindAllStringIndex(api, -1)
|
||||
if len(indexes) == 0 {
|
||||
return "", "", "", emptyStrcut
|
||||
}
|
||||
startIndexes := indexes[0]
|
||||
endIndexes := indexes[len(indexes)-1]
|
||||
|
||||
info = api[:startIndexes[0]]
|
||||
structBody = api[startIndexes[0]:endIndexes[len(endIndexes)-1]]
|
||||
service = api[endIndexes[len(endIndexes)-1]:]
|
||||
|
||||
firstIIndex := strings.Index(info, "i")
|
||||
if firstIIndex > 0 {
|
||||
info = info[firstIIndex:]
|
||||
}
|
||||
|
||||
lastServiceRightBraceIndex := strings.LastIndex(service, "}") + 1
|
||||
var firstServiceIndex int
|
||||
for index, char := range service {
|
||||
if !isSpace(char) && !isNewline(char) {
|
||||
firstServiceIndex = index
|
||||
break
|
||||
}
|
||||
}
|
||||
service = service[firstServiceIndex:lastServiceRightBraceIndex]
|
||||
return
|
||||
}
|
||||
54
tools/goctl/api/parser/validator.go
Normal file
54
tools/goctl/api/parser/validator.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"zero/core/stringx"
|
||||
"zero/tools/goctl/api/spec"
|
||||
"zero/tools/goctl/api/util"
|
||||
)
|
||||
|
||||
func (p *Parser) validate(api *spec.ApiSpec) (err error) {
|
||||
var builder strings.Builder
|
||||
for _, tp := range api.Types {
|
||||
if ok, name := p.validateDuplicateProperty(tp); !ok {
|
||||
fmt.Fprintf(&builder, `duplicate property "%s" of type "%s"`+"\n", name, tp.Name)
|
||||
}
|
||||
}
|
||||
if ok, info := p.validateDuplicateRouteHandler(api); !ok {
|
||||
fmt.Fprintf(&builder, info)
|
||||
}
|
||||
if len(builder.String()) > 0 {
|
||||
return errors.New(builder.String())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Parser) validateDuplicateProperty(tp spec.Type) (bool, string) {
|
||||
var names []string
|
||||
for _, member := range tp.Members {
|
||||
if stringx.Contains(names, member.Name) {
|
||||
return false, member.Name
|
||||
} else {
|
||||
names = append(names, member.Name)
|
||||
}
|
||||
}
|
||||
return true, ""
|
||||
}
|
||||
|
||||
func (p *Parser) validateDuplicateRouteHandler(api *spec.ApiSpec) (bool, string) {
|
||||
var names []string
|
||||
for _, r := range api.Service.Routes {
|
||||
handler, ok := util.GetAnnotationValue(r.Annotations, "server", "handler")
|
||||
if !ok {
|
||||
return false, fmt.Sprintf("missing handler annotation for %s", r.Path)
|
||||
}
|
||||
if stringx.Contains(names, handler) {
|
||||
return false, fmt.Sprintf(`duplicated handler for name "%s"`, handler)
|
||||
} else {
|
||||
names = append(names, handler)
|
||||
}
|
||||
}
|
||||
return true, ""
|
||||
}
|
||||
16
tools/goctl/api/parser/vars.go
Normal file
16
tools/goctl/api/parser/vars.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package parser
|
||||
|
||||
const (
|
||||
infoDirective = "info"
|
||||
serviceDirective = "service"
|
||||
typeDirective = "type"
|
||||
typeStruct = "struct"
|
||||
at = '@'
|
||||
colon = ':'
|
||||
leftParenthesis = '('
|
||||
rightParenthesis = ')'
|
||||
leftBrace = "{"
|
||||
rightBrace = '}'
|
||||
multilineBeginTag = '>'
|
||||
multilineEndTag = '<'
|
||||
)
|
||||
143
tools/goctl/api/spec/fn.go
Normal file
143
tools/goctl/api/spec/fn.go
Normal file
@@ -0,0 +1,143 @@
|
||||
package spec
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"zero/core/stringx"
|
||||
"zero/tools/goctl/util"
|
||||
)
|
||||
|
||||
const (
|
||||
TagKey = "tag"
|
||||
NameKey = "name"
|
||||
OptionKey = "option"
|
||||
BodyTag = "json"
|
||||
)
|
||||
|
||||
var (
|
||||
TagRe = regexp.MustCompile(`(?P<tag>\w+):"(?P<name>[^,"]+)[,]?(?P<option>[^"]*)"`)
|
||||
TagSubNames = TagRe.SubexpNames()
|
||||
definedTags = []string{TagKey, NameKey, OptionKey}
|
||||
)
|
||||
|
||||
type Attribute struct {
|
||||
Key string
|
||||
value string
|
||||
}
|
||||
|
||||
func (m Member) IsOptional() bool {
|
||||
var option string
|
||||
|
||||
matches := TagRe.FindStringSubmatch(m.Tag)
|
||||
for i := range matches {
|
||||
name := TagSubNames[i]
|
||||
if name == OptionKey {
|
||||
option = matches[i]
|
||||
}
|
||||
}
|
||||
|
||||
if len(option) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
fields := strings.Split(option, ",")
|
||||
for _, field := range fields {
|
||||
if field == "optional" || strings.HasPrefix(field, "default=") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (m Member) IsOmitempty() bool {
|
||||
var option string
|
||||
|
||||
matches := TagRe.FindStringSubmatch(m.Tag)
|
||||
for i := range matches {
|
||||
name := TagSubNames[i]
|
||||
if name == OptionKey {
|
||||
option = matches[i]
|
||||
}
|
||||
}
|
||||
|
||||
if len(option) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
fields := strings.Split(option, ",")
|
||||
for _, field := range fields {
|
||||
if field == "omitempty" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (m Member) GetAttributes() []Attribute {
|
||||
matches := TagRe.FindStringSubmatch(m.Tag)
|
||||
var result []Attribute
|
||||
for i := range matches {
|
||||
name := TagSubNames[i]
|
||||
if stringx.Contains(definedTags, name) {
|
||||
result = append(result, Attribute{
|
||||
Key: name,
|
||||
value: matches[i],
|
||||
})
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (m Member) GetPropertyName() (string, error) {
|
||||
attrs := m.GetAttributes()
|
||||
for _, attr := range attrs {
|
||||
if attr.Key == NameKey && len(attr.value) > 0 {
|
||||
if attr.value == "-" {
|
||||
return util.Untitle(m.Name), nil
|
||||
}
|
||||
return attr.value, nil
|
||||
}
|
||||
}
|
||||
return "", errors.New("json property name not exist, member: " + m.Name)
|
||||
}
|
||||
|
||||
func (m Member) GetComment() string {
|
||||
return strings.TrimSpace(strings.Join(m.Comments, "; "))
|
||||
}
|
||||
|
||||
func (m Member) IsBodyMember() bool {
|
||||
if m.IsInline {
|
||||
return true
|
||||
}
|
||||
attrs := m.GetAttributes()
|
||||
for _, attr := range attrs {
|
||||
if attr.value == BodyTag {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (t Type) GetBodyMembers() []Member {
|
||||
var result []Member
|
||||
for _, member := range t.Members {
|
||||
if member.IsBodyMember() {
|
||||
result = append(result, member)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (t Type) GetNonBodyMembers() []Member {
|
||||
var result []Member
|
||||
for _, member := range t.Members {
|
||||
if !member.IsBodyMember() {
|
||||
result = append(result, member)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
131
tools/goctl/api/spec/spec.go
Normal file
131
tools/goctl/api/spec/spec.go
Normal file
@@ -0,0 +1,131 @@
|
||||
package spec
|
||||
|
||||
type (
|
||||
Annotation struct {
|
||||
Name string
|
||||
Properties map[string]string
|
||||
}
|
||||
|
||||
ApiSpec struct {
|
||||
Info Info
|
||||
Types []Type
|
||||
Service Service
|
||||
}
|
||||
|
||||
Group struct {
|
||||
Annotations []Annotation
|
||||
Routes []Route
|
||||
}
|
||||
|
||||
Info struct {
|
||||
Title string
|
||||
Desc string
|
||||
Version string
|
||||
Author string
|
||||
Email string
|
||||
}
|
||||
|
||||
Member struct {
|
||||
Annotations []Annotation
|
||||
Name string
|
||||
// 数据类型字面值,如:string、map[int]string、[]int64、[]*User
|
||||
Type string
|
||||
// it can be asserted as BasicType: int、bool、
|
||||
// PointerType: *string、*User、
|
||||
// MapType: map[${BasicType}]interface、
|
||||
// ArrayType:[]int、[]User、[]*User
|
||||
// InterfaceType: interface{}
|
||||
// Type
|
||||
Expr interface{}
|
||||
Tag string
|
||||
// Deprecated
|
||||
Comment string // 换成标准struct中将废弃
|
||||
// 成员尾部注释说明
|
||||
Comments []string
|
||||
// 成员头顶注释说明
|
||||
Docs []string
|
||||
IsInline bool
|
||||
}
|
||||
|
||||
Route struct {
|
||||
Annotations []Annotation
|
||||
Method string
|
||||
Path string
|
||||
RequestType Type
|
||||
ResponseType Type
|
||||
}
|
||||
|
||||
Service struct {
|
||||
Name string
|
||||
Annotations []Annotation
|
||||
Routes []Route
|
||||
Groups []Group
|
||||
}
|
||||
|
||||
Type struct {
|
||||
Name string
|
||||
Annotations []Annotation
|
||||
Members []Member
|
||||
}
|
||||
|
||||
// 系统预设基本数据类型
|
||||
BasicType struct {
|
||||
StringExpr string
|
||||
Name string
|
||||
}
|
||||
PointerType struct {
|
||||
StringExpr string
|
||||
// it can be asserted as BasicType: int、bool、
|
||||
// PointerType: *string、*User、
|
||||
// MapType: map[${BasicType}]interface、
|
||||
// ArrayType:[]int、[]User、[]*User
|
||||
// InterfaceType: interface{}
|
||||
// Type
|
||||
Star interface{}
|
||||
}
|
||||
|
||||
MapType struct {
|
||||
StringExpr string
|
||||
// only support the BasicType
|
||||
Key string
|
||||
// it can be asserted as BasicType: int、bool、
|
||||
// PointerType: *string、*User、
|
||||
// MapType: map[${BasicType}]interface、
|
||||
// ArrayType:[]int、[]User、[]*User
|
||||
// InterfaceType: interface{}
|
||||
// Type
|
||||
Value interface{}
|
||||
}
|
||||
ArrayType struct {
|
||||
StringExpr string
|
||||
// it can be asserted as BasicType: int、bool、
|
||||
// PointerType: *string、*User、
|
||||
// MapType: map[${BasicType}]interface、
|
||||
// ArrayType:[]int、[]User、[]*User
|
||||
// InterfaceType: interface{}
|
||||
// Type
|
||||
ArrayType interface{}
|
||||
}
|
||||
InterfaceType struct {
|
||||
StringExpr string
|
||||
// do nothing,just for assert
|
||||
}
|
||||
TimeType struct {
|
||||
StringExpr string
|
||||
}
|
||||
StructType struct {
|
||||
StringExpr string
|
||||
}
|
||||
)
|
||||
|
||||
func (spec *ApiSpec) ContainsTime() bool {
|
||||
for _, item := range spec.Types {
|
||||
members := item.Members
|
||||
for _, member := range members {
|
||||
if _, ok := member.Expr.(*TimeType); ok {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
44
tools/goctl/api/tsgen/gen.go
Normal file
44
tools/goctl/api/tsgen/gen.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package tsgen
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"zero/core/lang"
|
||||
"zero/tools/goctl/api/parser"
|
||||
"zero/tools/goctl/util"
|
||||
|
||||
"github.com/logrusorgru/aurora"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
func TsCommand(c *cli.Context) error {
|
||||
apiFile := c.String("api")
|
||||
dir := c.String("dir")
|
||||
webApi := c.String("webapi")
|
||||
caller := c.String("caller")
|
||||
unwrapApi := c.Bool("unwrap")
|
||||
if len(apiFile) == 0 {
|
||||
return errors.New("missing -api")
|
||||
}
|
||||
if len(dir) == 0 {
|
||||
return errors.New("missing -dir")
|
||||
}
|
||||
|
||||
p, err := parser.NewParser(apiFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
api, err := p.Parse()
|
||||
if err != nil {
|
||||
fmt.Println(aurora.Red("Failed"))
|
||||
return err
|
||||
}
|
||||
|
||||
lang.Must(util.MkdirIfNotExist(dir))
|
||||
lang.Must(genHandler(dir, webApi, caller, api, unwrapApi))
|
||||
lang.Must(genComponents(dir, api))
|
||||
|
||||
fmt.Println(aurora.Green("Done."))
|
||||
return nil
|
||||
}
|
||||
79
tools/goctl/api/tsgen/gencomponents.go
Normal file
79
tools/goctl/api/tsgen/gencomponents.go
Normal file
@@ -0,0 +1,79 @@
|
||||
package tsgen
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"path"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"zero/tools/goctl/api/spec"
|
||||
apiutil "zero/tools/goctl/api/util"
|
||||
"zero/tools/goctl/util"
|
||||
)
|
||||
|
||||
const (
|
||||
componentsTemplate = `// DO NOT EDIT, generated by goctl
|
||||
|
||||
{{.componentTypes}}
|
||||
`
|
||||
)
|
||||
|
||||
func genComponents(dir string, api *spec.ApiSpec) error {
|
||||
types := apiutil.GetSharedTypes(api)
|
||||
if len(types) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
val, err := buildTypes(types, func(name string) (*spec.Type, error) {
|
||||
for _, ty := range api.Types {
|
||||
if strings.ToLower(ty.Name) == strings.ToLower(name) {
|
||||
return &ty, nil
|
||||
}
|
||||
}
|
||||
return nil, errors.New("inline type " + name + " not exist, please correct api file")
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
outputFile := apiutil.ComponentName(api) + ".ts"
|
||||
filename := path.Join(dir, outputFile)
|
||||
if err := util.RemoveIfExist(filename); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fp, created, err := apiutil.MaybeCreateFile(dir, ".", outputFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !created {
|
||||
return nil
|
||||
}
|
||||
defer fp.Close()
|
||||
|
||||
t := template.Must(template.New("componentsTemplate").Parse(componentsTemplate))
|
||||
return t.Execute(fp, map[string]string{
|
||||
"componentTypes": val,
|
||||
})
|
||||
}
|
||||
|
||||
func buildTypes(types []spec.Type, inlineType func(string) (*spec.Type, error)) (string, error) {
|
||||
var builder strings.Builder
|
||||
first := true
|
||||
for _, tp := range types {
|
||||
if first {
|
||||
first = false
|
||||
} else {
|
||||
builder.WriteString("\n")
|
||||
}
|
||||
if err := writeType(&builder, tp, func(name string) (*spec.Type, error) {
|
||||
return inlineType(name)
|
||||
}, func(tp string) string {
|
||||
return ""
|
||||
}); err != nil {
|
||||
return "", apiutil.WrapErr(err, "Type "+tp.Name+" generate error")
|
||||
}
|
||||
}
|
||||
|
||||
return builder.String(), nil
|
||||
}
|
||||
214
tools/goctl/api/tsgen/genpacket.go
Normal file
214
tools/goctl/api/tsgen/genpacket.go
Normal file
@@ -0,0 +1,214 @@
|
||||
package tsgen
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"path"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"zero/tools/goctl/api/spec"
|
||||
apiutil "zero/tools/goctl/api/util"
|
||||
"zero/tools/goctl/util"
|
||||
)
|
||||
|
||||
const (
|
||||
handlerTemplate = `{{.imports}}
|
||||
|
||||
{{.types}}
|
||||
|
||||
{{.apis}}
|
||||
`
|
||||
)
|
||||
|
||||
func genHandler(dir, webApi, caller string, api *spec.ApiSpec, unwrapApi bool) error {
|
||||
filename := strings.Replace(api.Service.Name, "-api", "", 1) + ".ts"
|
||||
if err := util.RemoveIfExist(path.Join(dir, filename)); err != nil {
|
||||
return err
|
||||
}
|
||||
fp, created, err := apiutil.MaybeCreateFile(dir, "", filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !created {
|
||||
return nil
|
||||
}
|
||||
defer fp.Close()
|
||||
|
||||
var localTypes []spec.Type
|
||||
for _, route := range api.Service.Routes {
|
||||
rts := apiutil.GetLocalTypes(api, route)
|
||||
localTypes = append(localTypes, rts...)
|
||||
}
|
||||
|
||||
var prefixForType = func(ty string) string {
|
||||
if _, pri := primitiveType(ty); pri {
|
||||
return ""
|
||||
}
|
||||
for _, item := range localTypes {
|
||||
if util.Title(item.Name) == ty {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
return packagePrefix
|
||||
}
|
||||
|
||||
types, err := genTypes(localTypes, func(name string) (*spec.Type, error) {
|
||||
for _, ty := range api.Types {
|
||||
if strings.ToLower(ty.Name) == strings.ToLower(name) {
|
||||
return &ty, nil
|
||||
}
|
||||
}
|
||||
return nil, errors.New("inline type " + name + " not exist, please correct api file")
|
||||
}, prefixForType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
imports := ""
|
||||
if len(caller) == 0 {
|
||||
caller = "webapi"
|
||||
}
|
||||
importCaller := caller
|
||||
if unwrapApi {
|
||||
importCaller = "{ " + importCaller + " }"
|
||||
}
|
||||
if len(webApi) > 0 {
|
||||
imports += `import ` + importCaller + ` from ` + "\"" + webApi + "\""
|
||||
}
|
||||
shardTypes := apiutil.GetSharedTypes(api)
|
||||
if len(shardTypes) != 0 {
|
||||
if len(imports) > 0 {
|
||||
imports += "\n"
|
||||
}
|
||||
outputFile := apiutil.ComponentName(api)
|
||||
imports += fmt.Sprintf(`import * as components from "%s"`, "./"+outputFile)
|
||||
}
|
||||
|
||||
apis, err := genApi(api, localTypes, caller, prefixForType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
t := template.Must(template.New("handlerTemplate").Parse(handlerTemplate))
|
||||
return t.Execute(fp, map[string]string{
|
||||
"webApi": webApi,
|
||||
"types": strings.TrimSpace(types),
|
||||
"imports": imports,
|
||||
"apis": strings.TrimSpace(apis),
|
||||
})
|
||||
}
|
||||
|
||||
func genTypes(localTypes []spec.Type, inlineType func(string) (*spec.Type, error), prefixForType func(string) string) (string, error) {
|
||||
var builder strings.Builder
|
||||
var first bool
|
||||
|
||||
for _, tp := range localTypes {
|
||||
if first {
|
||||
first = false
|
||||
} else {
|
||||
fmt.Fprintln(&builder)
|
||||
}
|
||||
if err := writeType(&builder, tp, func(name string) (s *spec.Type, err error) {
|
||||
return inlineType(name)
|
||||
}, prefixForType); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
types := builder.String()
|
||||
return types, nil
|
||||
}
|
||||
|
||||
func genApi(api *spec.ApiSpec, localTypes []spec.Type, caller string, prefixForType func(string) string) (string, error) {
|
||||
var builder strings.Builder
|
||||
for _, route := range api.Service.Routes {
|
||||
handler, ok := apiutil.GetAnnotationValue(route.Annotations, "server", "handler")
|
||||
if !ok {
|
||||
return "", fmt.Errorf("missing handler annotation for route %q", route.Path)
|
||||
}
|
||||
handler = util.Untitle(handler)
|
||||
handler = strings.Replace(handler, "Handler", "", 1)
|
||||
comment := commentForRoute(route)
|
||||
if len(comment) > 0 {
|
||||
fmt.Fprintf(&builder, "%s\n", comment)
|
||||
}
|
||||
fmt.Fprintf(&builder, "export function %s(%s) {\n", handler, paramsForRoute(route, prefixForType))
|
||||
writeIndent(&builder, 1)
|
||||
responseGeneric := "<null>"
|
||||
if len(route.ResponseType.Name) > 0 {
|
||||
val, err := goTypeToTs(route.ResponseType.Name, prefixForType)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
responseGeneric = fmt.Sprintf("<%s>", val)
|
||||
}
|
||||
fmt.Fprintf(&builder, `return %s.%s%s(%s)`, caller, strings.ToLower(route.Method),
|
||||
util.Title(responseGeneric), callParamsForRoute(route))
|
||||
builder.WriteString("\n}\n\n")
|
||||
}
|
||||
|
||||
apis := builder.String()
|
||||
return apis, nil
|
||||
}
|
||||
|
||||
func paramsForRoute(route spec.Route, prefixForType func(string) string) string {
|
||||
hasParams := pathHasParams(route)
|
||||
hasBody := hasRequestBody(route)
|
||||
rt, err := goTypeToTs(route.RequestType.Name, prefixForType)
|
||||
if err != nil {
|
||||
println(err.Error())
|
||||
return ""
|
||||
}
|
||||
if hasParams && hasBody {
|
||||
return fmt.Sprintf("params: %s, req: %s", rt+"Params", rt)
|
||||
} else if hasParams {
|
||||
return fmt.Sprintf("params: %s", rt+"Params")
|
||||
} else if hasBody {
|
||||
return fmt.Sprintf("req: %s", rt)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func commentForRoute(route spec.Route) string {
|
||||
var builder strings.Builder
|
||||
comment, _ := apiutil.GetAnnotationValue(route.Annotations, "doc", "summary")
|
||||
builder.WriteString("/**")
|
||||
builder.WriteString("\n * @description " + comment)
|
||||
hasParams := pathHasParams(route)
|
||||
hasBody := hasRequestBody(route)
|
||||
if hasParams && hasBody {
|
||||
builder.WriteString("\n * @param params")
|
||||
builder.WriteString("\n * @param req")
|
||||
} else if hasParams {
|
||||
builder.WriteString("\n * @param params")
|
||||
} else if hasBody {
|
||||
builder.WriteString("\n * @param req")
|
||||
}
|
||||
builder.WriteString("\n */")
|
||||
return builder.String()
|
||||
}
|
||||
|
||||
func callParamsForRoute(route spec.Route) string {
|
||||
hasParams := pathHasParams(route)
|
||||
hasBody := hasRequestBody(route)
|
||||
if hasParams && hasBody {
|
||||
return fmt.Sprintf("%s, %s, %s", pathForRoute(route), "params", "req")
|
||||
} else if hasParams {
|
||||
return fmt.Sprintf("%s, %s", pathForRoute(route), "params")
|
||||
} else if hasBody {
|
||||
return fmt.Sprintf("%s, %s", pathForRoute(route), "req")
|
||||
}
|
||||
return pathForRoute(route)
|
||||
}
|
||||
|
||||
func pathForRoute(route spec.Route) string {
|
||||
return "\"" + route.Path + "\""
|
||||
}
|
||||
|
||||
func pathHasParams(route spec.Route) bool {
|
||||
return len(route.RequestType.Members) != len(route.RequestType.GetBodyMembers())
|
||||
}
|
||||
|
||||
func hasRequestBody(route spec.Route) bool {
|
||||
return len(route.RequestType.Name) > 0 && len(route.RequestType.GetBodyMembers()) > 0
|
||||
}
|
||||
167
tools/goctl/api/tsgen/util.go
Normal file
167
tools/goctl/api/tsgen/util.go
Normal file
@@ -0,0 +1,167 @@
|
||||
package tsgen
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"zero/tools/goctl/api/spec"
|
||||
apiutil "zero/tools/goctl/api/util"
|
||||
"zero/tools/goctl/util"
|
||||
)
|
||||
|
||||
func writeProperty(writer io.Writer, member spec.Member, indent int, prefixForType func(string) string) error {
|
||||
writeIndent(writer, indent)
|
||||
ty, err := goTypeToTs(member.Type, prefixForType)
|
||||
optionalTag := ""
|
||||
if member.IsOptional() || member.IsOmitempty() {
|
||||
optionalTag = "?"
|
||||
}
|
||||
name, err := member.GetPropertyName()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
comment := member.GetComment()
|
||||
if len(comment) > 0 {
|
||||
comment = strings.TrimPrefix(comment, "//")
|
||||
comment = " // " + strings.TrimSpace(comment)
|
||||
}
|
||||
if len(member.Docs) > 0 {
|
||||
_, err = fmt.Fprintf(writer, "%s\n", strings.Join(member.Docs, ""))
|
||||
writeIndent(writer, 1)
|
||||
}
|
||||
_, err = fmt.Fprintf(writer, "%s%s: %s%s\n", name, optionalTag, ty, comment)
|
||||
return err
|
||||
}
|
||||
|
||||
func writeIndent(writer io.Writer, indent int) {
|
||||
for i := 0; i < indent; i++ {
|
||||
fmt.Fprint(writer, "\t")
|
||||
}
|
||||
}
|
||||
|
||||
func goTypeToTs(tp string, prefixForType func(string) string) (string, error) {
|
||||
if val, pri := primitiveType(tp); pri {
|
||||
return val, nil
|
||||
}
|
||||
if tp == "[]byte" {
|
||||
return "Blob", nil
|
||||
} else if strings.HasPrefix(tp, "[][]") {
|
||||
tys, err := apiutil.DecomposeType(tp)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if len(tys) == 0 {
|
||||
return "", fmt.Errorf("%s tp parse error", tp)
|
||||
}
|
||||
innerType, err := goTypeToTs(tys[0], prefixForType)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return fmt.Sprintf("Array<Array<%s>>", innerType), nil
|
||||
} else if strings.HasPrefix(tp, "[]") {
|
||||
tys, err := apiutil.DecomposeType(tp)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if len(tys) == 0 {
|
||||
return "", fmt.Errorf("%s tp parse error", tp)
|
||||
}
|
||||
innerType, err := goTypeToTs(tys[0], prefixForType)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return fmt.Sprintf("Array<%s>", innerType), nil
|
||||
} else if strings.HasPrefix(tp, "map") {
|
||||
tys, err := apiutil.DecomposeType(tp)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if len(tys) != 2 {
|
||||
return "", fmt.Errorf("%s tp parse error", tp)
|
||||
}
|
||||
innerType, err := goTypeToTs(tys[1], prefixForType)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return fmt.Sprintf("{ [key: string]: %s }", innerType), nil
|
||||
}
|
||||
return addPrefixIfNeed(util.Title(tp), prefixForType), nil
|
||||
}
|
||||
|
||||
func addPrefixIfNeed(tp string, prefixForType func(string) string) string {
|
||||
if val, pri := primitiveType(tp); pri {
|
||||
return val
|
||||
}
|
||||
tp = strings.Replace(tp, "*", "", 1)
|
||||
return prefixForType(tp) + util.Title(tp)
|
||||
}
|
||||
|
||||
func primitiveType(tp string) (string, bool) {
|
||||
switch tp {
|
||||
case "string":
|
||||
return "string", true
|
||||
case "int", "int8", "int32", "int64":
|
||||
return "number", true
|
||||
case "float", "float32", "float64":
|
||||
return "number", true
|
||||
case "bool":
|
||||
return "boolean", true
|
||||
case "[]byte":
|
||||
return "Blob", true
|
||||
case "interface{}":
|
||||
return "any", true
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
func writeType(writer io.Writer, tp spec.Type, inlineType func(string) (*spec.Type, error), prefixForType func(string) string) error {
|
||||
fmt.Fprintf(writer, "export interface %s {\n", util.Title(tp.Name))
|
||||
if err := genMembers(writer, tp, false, inlineType, prefixForType); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintf(writer, "}\n")
|
||||
err := genParamsTypesIfNeed(writer, tp, inlineType, prefixForType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func genParamsTypesIfNeed(writer io.Writer, tp spec.Type, inlineType func(string) (*spec.Type, error), prefixForType func(string) string) error {
|
||||
members := tp.GetNonBodyMembers()
|
||||
if len(members) == 0 {
|
||||
return nil
|
||||
}
|
||||
fmt.Fprintf(writer, "\n")
|
||||
fmt.Fprintf(writer, "export interface %sParams {\n", util.Title(tp.Name))
|
||||
if err := genMembers(writer, tp, true, inlineType, prefixForType); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintf(writer, "}\n")
|
||||
return nil
|
||||
}
|
||||
|
||||
func genMembers(writer io.Writer, tp spec.Type, isParam bool, inlineType func(string) (*spec.Type, error), prefixForType func(string) string) error {
|
||||
members := tp.GetBodyMembers()
|
||||
if isParam {
|
||||
members = tp.GetNonBodyMembers()
|
||||
}
|
||||
for _, member := range members {
|
||||
if member.IsInline {
|
||||
// 获取inline类型的成员然后添加到type中
|
||||
it, err := inlineType(strings.TrimPrefix(member.Type, "*"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := genMembers(writer, *it, isParam, inlineType, prefixForType); err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
if err := writeProperty(writer, member, 1, prefixForType); err != nil {
|
||||
return apiutil.WrapErr(err, " type "+tp.Name)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
5
tools/goctl/api/tsgen/vars.go
Normal file
5
tools/goctl/api/tsgen/vars.go
Normal file
@@ -0,0 +1,5 @@
|
||||
package tsgen
|
||||
|
||||
const (
|
||||
packagePrefix = "components."
|
||||
)
|
||||
13
tools/goctl/api/util/annotation.go
Normal file
13
tools/goctl/api/util/annotation.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package util
|
||||
|
||||
import "zero/tools/goctl/api/spec"
|
||||
|
||||
func GetAnnotationValue(annos []spec.Annotation, key, field string) (string, bool) {
|
||||
for _, anno := range annos {
|
||||
if anno.Name == key {
|
||||
value, ok := anno.Properties[field]
|
||||
return value, ok
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
107
tools/goctl/api/util/case.go
Normal file
107
tools/goctl/api/util/case.go
Normal file
@@ -0,0 +1,107 @@
|
||||
package util
|
||||
|
||||
func IsUpperCase(r rune) bool {
|
||||
if r >= 'A' && r <= 'Z' {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func IsLowerCase(r rune) bool {
|
||||
if r >= 'a' && r <= 'z' {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func ToSnakeCase(s string) string {
|
||||
out := []rune{}
|
||||
for index, r := range s {
|
||||
if index == 0 {
|
||||
out = append(out, ToLowerCase(r))
|
||||
continue
|
||||
}
|
||||
|
||||
if IsUpperCase(r) && index != 0 {
|
||||
if IsLowerCase(rune(s[index-1])) {
|
||||
out = append(out, '_', ToLowerCase(r))
|
||||
continue
|
||||
}
|
||||
if index < len(s)-1 && IsLowerCase(rune(s[index+1])) {
|
||||
out = append(out, '_', ToLowerCase(r))
|
||||
continue
|
||||
}
|
||||
out = append(out, ToLowerCase(r))
|
||||
continue
|
||||
}
|
||||
out = append(out, r)
|
||||
}
|
||||
return string(out)
|
||||
}
|
||||
|
||||
func ToCamelCase(s string) string {
|
||||
s = ToLower(s)
|
||||
out := []rune{}
|
||||
for index, r := range s {
|
||||
if r == '_' {
|
||||
continue
|
||||
}
|
||||
if index == 0 {
|
||||
out = append(out, ToUpperCase(r))
|
||||
continue
|
||||
}
|
||||
|
||||
if index > 0 && s[index-1] == '_' {
|
||||
out = append(out, ToUpperCase(r))
|
||||
continue
|
||||
}
|
||||
|
||||
out = append(out, r)
|
||||
}
|
||||
return string(out)
|
||||
}
|
||||
|
||||
func ToLowerCase(r rune) rune {
|
||||
dx := 'A' - 'a'
|
||||
if IsUpperCase(r) {
|
||||
return r - dx
|
||||
}
|
||||
return r
|
||||
}
|
||||
func ToUpperCase(r rune) rune {
|
||||
dx := 'A' - 'a'
|
||||
if IsLowerCase(r) {
|
||||
return r + dx
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
func ToLower(s string) string {
|
||||
out := []rune{}
|
||||
for _, r := range s {
|
||||
out = append(out, ToLowerCase(r))
|
||||
}
|
||||
return string(out)
|
||||
}
|
||||
|
||||
func ToUpper(s string) string {
|
||||
out := []rune{}
|
||||
for _, r := range s {
|
||||
out = append(out, ToUpperCase(r))
|
||||
}
|
||||
return string(out)
|
||||
}
|
||||
|
||||
func LowerFirst(s string) string {
|
||||
if len(s) == 0 {
|
||||
return s
|
||||
}
|
||||
return ToLower(s[:1]) + s[1:]
|
||||
}
|
||||
|
||||
func UpperFirst(s string) string {
|
||||
if len(s) == 0 {
|
||||
return s
|
||||
}
|
||||
return ToUpper(s[:1]) + s[1:]
|
||||
}
|
||||
58
tools/goctl/api/util/tag.go
Normal file
58
tools/goctl/api/util/tag.go
Normal file
@@ -0,0 +1,58 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func TagLookup(tag, key string) (value string, ok bool) {
|
||||
tag = strings.Replace(tag, "`", "", -1)
|
||||
for tag != "" {
|
||||
// Skip leading space.
|
||||
i := 0
|
||||
for i < len(tag) && tag[i] == ' ' {
|
||||
i++
|
||||
}
|
||||
tag = tag[i:]
|
||||
if tag == "" {
|
||||
break
|
||||
}
|
||||
|
||||
// Scan to colon. A space, a quote or a control character is a syntax error.
|
||||
// Strictly speaking, control chars include the range [0x7f, 0x9f], not just
|
||||
// [0x00, 0x1f], but in practice, we ignore the multi-byte control characters
|
||||
// as it is simpler to inspect the tag's bytes than the tag's runes.
|
||||
i = 0
|
||||
for i < len(tag) && tag[i] > ' ' && tag[i] != ':' && tag[i] != '"' && tag[i] != 0x7f {
|
||||
i++
|
||||
}
|
||||
if i == 0 || i+1 >= len(tag) || tag[i] != ':' || tag[i+1] != '"' {
|
||||
break
|
||||
}
|
||||
name := string(tag[:i])
|
||||
tag = tag[i+1:]
|
||||
|
||||
// Scan quoted string to find value.
|
||||
i = 1
|
||||
for i < len(tag) && tag[i] != '"' {
|
||||
if tag[i] == '\\' {
|
||||
i++
|
||||
}
|
||||
i++
|
||||
}
|
||||
if i >= len(tag) {
|
||||
break
|
||||
}
|
||||
qvalue := string(tag[:i+1])
|
||||
tag = tag[i+1:]
|
||||
|
||||
if key == name {
|
||||
value, err := strconv.Unquote(qvalue)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
return value, true
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
159
tools/goctl/api/util/types.go
Normal file
159
tools/goctl/api/util/types.go
Normal file
@@ -0,0 +1,159 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"zero/tools/goctl/api/spec"
|
||||
)
|
||||
|
||||
func DecomposeType(t string) (result []string, err error) {
|
||||
add := func(tp string) error {
|
||||
ret, err := DecomposeType(tp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
result = append(result, ret...)
|
||||
return nil
|
||||
}
|
||||
if strings.HasPrefix(t, "map") {
|
||||
t = strings.ReplaceAll(t, "map", "")
|
||||
if t[0] == '[' {
|
||||
pos := strings.Index(t, "]")
|
||||
if pos > 1 {
|
||||
if err = add(t[1:pos]); err != nil {
|
||||
return
|
||||
}
|
||||
if len(t) > pos+1 {
|
||||
err = add(t[pos+1:])
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if strings.HasPrefix(t, "[]") {
|
||||
if len(t) > 2 {
|
||||
err = add(t[2:])
|
||||
return
|
||||
}
|
||||
} else if strings.HasPrefix(t, "*") {
|
||||
err = add(t[1:])
|
||||
return
|
||||
} else {
|
||||
result = append(result, t)
|
||||
return
|
||||
}
|
||||
|
||||
err = fmt.Errorf("bad type %q", t)
|
||||
return
|
||||
}
|
||||
|
||||
func GetAllTypes(api *spec.ApiSpec, route spec.Route) []spec.Type {
|
||||
var rts []spec.Type
|
||||
types := api.Types
|
||||
getTypeRecursive(route.RequestType, types, &rts)
|
||||
getTypeRecursive(route.ResponseType, types, &rts)
|
||||
return rts
|
||||
}
|
||||
|
||||
func GetLocalTypes(api *spec.ApiSpec, route spec.Route) []spec.Type {
|
||||
sharedTypes := GetSharedTypes(api)
|
||||
isSharedType := func(ty spec.Type) bool {
|
||||
for _, item := range sharedTypes {
|
||||
if item.Name == ty.Name {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
var rts = GetAllTypes(api, route)
|
||||
|
||||
var result []spec.Type
|
||||
for _, item := range rts {
|
||||
if !isSharedType(item) {
|
||||
result = append(result, item)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func getTypeRecursive(ty spec.Type, allTypes []spec.Type, result *[]spec.Type) {
|
||||
isCustomType := func(name string) (*spec.Type, bool) {
|
||||
for _, item := range allTypes {
|
||||
if item.Name == name {
|
||||
return &item, true
|
||||
}
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
if len(ty.Name) > 0 {
|
||||
*result = append(*result, ty)
|
||||
}
|
||||
for _, member := range ty.Members {
|
||||
decomposedItems, _ := DecomposeType(member.Type)
|
||||
if len(decomposedItems) == 0 {
|
||||
continue
|
||||
}
|
||||
var customTypes []spec.Type
|
||||
for _, item := range decomposedItems {
|
||||
c, e := isCustomType(item)
|
||||
if e {
|
||||
customTypes = append(customTypes, *c)
|
||||
}
|
||||
}
|
||||
for _, ty := range customTypes {
|
||||
hasAppend := false
|
||||
for _, item := range *result {
|
||||
if ty.Name == item.Name {
|
||||
hasAppend = true
|
||||
break
|
||||
}
|
||||
|
||||
}
|
||||
if !hasAppend {
|
||||
getTypeRecursive(ty, allTypes, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func GetSharedTypes(api *spec.ApiSpec) []spec.Type {
|
||||
types := api.Types
|
||||
var result []spec.Type
|
||||
var container []spec.Type
|
||||
hasInclude := func(all []spec.Type, ty spec.Type) bool {
|
||||
for _, item := range all {
|
||||
if item.Name == ty.Name {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
for _, route := range api.Service.Routes {
|
||||
var rts []spec.Type
|
||||
getTypeRecursive(route.RequestType, types, &rts)
|
||||
getTypeRecursive(route.ResponseType, types, &rts)
|
||||
for _, item := range rts {
|
||||
if len(item.Name) == 0 {
|
||||
continue
|
||||
}
|
||||
if hasInclude(container, item) {
|
||||
hasAppend := false
|
||||
for _, r := range result {
|
||||
if item.Name == r.Name {
|
||||
hasAppend = true
|
||||
break
|
||||
}
|
||||
|
||||
}
|
||||
if !hasAppend {
|
||||
result = append(result, item)
|
||||
}
|
||||
} else {
|
||||
container = append(container, item)
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
74
tools/goctl/api/util/util.go
Normal file
74
tools/goctl/api/util/util.go
Normal file
@@ -0,0 +1,74 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"zero/tools/goctl/api/spec"
|
||||
|
||||
"zero/core/lang"
|
||||
"zero/tools/goctl/util"
|
||||
)
|
||||
|
||||
func MaybeCreateFile(dir, subdir, file string) (fp *os.File, created bool, err error) {
|
||||
lang.Must(util.MkdirIfNotExist(path.Join(dir, subdir)))
|
||||
fpath := path.Join(dir, subdir, file)
|
||||
if util.FileExists(fpath) {
|
||||
fmt.Printf("%s exists, ignored generation\n", fpath)
|
||||
return nil, false, nil
|
||||
}
|
||||
|
||||
fp, err = util.CreateIfNotExist(fpath)
|
||||
created = err == nil
|
||||
return
|
||||
}
|
||||
|
||||
func ClearAndOpenFile(fpath string) (*os.File, error) {
|
||||
f, err := os.OpenFile(fpath, os.O_WRONLY|os.O_TRUNC, 0600)
|
||||
|
||||
_, err = f.WriteString("")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return f, nil
|
||||
}
|
||||
|
||||
func WrapErr(err error, message string) error {
|
||||
return errors.New(message + ", " + err.Error())
|
||||
}
|
||||
|
||||
func Copy(src, dst string) (int64, error) {
|
||||
sourceFileStat, err := os.Stat(src)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if !sourceFileStat.Mode().IsRegular() {
|
||||
return 0, fmt.Errorf("%s is not a regular file", src)
|
||||
}
|
||||
|
||||
source, err := os.Open(src)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer source.Close()
|
||||
|
||||
destination, err := os.Create(dst)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer destination.Close()
|
||||
nBytes, err := io.Copy(destination, source)
|
||||
return nBytes, err
|
||||
}
|
||||
|
||||
func ComponentName(api *spec.ApiSpec) string {
|
||||
name := api.Service.Name
|
||||
if strings.HasSuffix(name, "-api") {
|
||||
return name[:len(name)-4] + "Components"
|
||||
}
|
||||
return name + "Components"
|
||||
}
|
||||
29
tools/goctl/api/validate/validate.go
Normal file
29
tools/goctl/api/validate/validate.go
Normal file
@@ -0,0 +1,29 @@
|
||||
package validate
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"zero/tools/goctl/api/parser"
|
||||
|
||||
"github.com/logrusorgru/aurora"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
func GoValidateApi(c *cli.Context) error {
|
||||
apiFile := c.String("api")
|
||||
|
||||
if len(apiFile) == 0 {
|
||||
return errors.New("missing -api")
|
||||
}
|
||||
|
||||
p, err := parser.NewParser(apiFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = p.Parse()
|
||||
if err == nil {
|
||||
fmt.Println(aurora.Green("api format ok"))
|
||||
}
|
||||
return err
|
||||
}
|
||||
77
tools/goctl/configgen/genconfigjson.go
Normal file
77
tools/goctl/configgen/genconfigjson.go
Normal file
@@ -0,0 +1,77 @@
|
||||
package configgen
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"github.com/logrusorgru/aurora"
|
||||
"github.com/urfave/cli"
|
||||
"zero/tools/goctl/vars"
|
||||
)
|
||||
|
||||
const configTemplate = `package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"{{.import}}"
|
||||
)
|
||||
|
||||
func main() {
|
||||
var c config.Config
|
||||
template, err := json.MarshalIndent(c, "", " ")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
err = ioutil.WriteFile("config.json", template, os.ModePerm)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
func GenConfigCommand(c *cli.Context) error {
|
||||
path, err := filepath.Abs(c.String("path"))
|
||||
if err != nil {
|
||||
return errors.New("abs failed: " + c.String("path"))
|
||||
}
|
||||
xi := strings.Index(path, vars.ProjectName)
|
||||
if xi <= 0 {
|
||||
return errors.New("path should the absolute path of config go file")
|
||||
}
|
||||
path = strings.TrimSuffix(path, "/config.go")
|
||||
location := path + "/tmp"
|
||||
err = os.MkdirAll(location, os.ModePerm)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
goPath := filepath.Join(location, "config.go")
|
||||
fp, err := os.Create(goPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer fp.Close()
|
||||
defer os.RemoveAll(location)
|
||||
|
||||
t := template.Must(template.New("template").Parse(configTemplate))
|
||||
if err := t.Execute(fp, map[string]string{
|
||||
"import": path[xi:],
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd := exec.Command("go", "run", goPath)
|
||||
_, err = cmd.Output()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println(aurora.Green("Done."))
|
||||
return nil
|
||||
}
|
||||
23
tools/goctl/docker/docker.go
Normal file
23
tools/goctl/docker/docker.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package docker
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"zero/tools/goctl/gen"
|
||||
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
func DockerCommand(c *cli.Context) error {
|
||||
goFile := c.String("go")
|
||||
namespace := c.String("namespace")
|
||||
if len(goFile) == 0 || len(namespace) == 0 {
|
||||
return errors.New("-go and -namespace can't be empty")
|
||||
}
|
||||
|
||||
if err := gen.GenerateDockerfile(goFile, "-f", "etc/config.json"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return gen.GenerateMakefile(goFile, namespace)
|
||||
}
|
||||
30
tools/goctl/example/rec.proto
Normal file
30
tools/goctl/example/rec.proto
Normal file
@@ -0,0 +1,30 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package recommendservice;
|
||||
|
||||
message RecArticle {
|
||||
int64 id = 1;
|
||||
}
|
||||
|
||||
message RecommendRequest {
|
||||
// the id of the request user.
|
||||
int64 uid = 1;
|
||||
// how many top ranked article for this user.
|
||||
int32 topk = 2;
|
||||
// current hour
|
||||
int32 hour = 3;
|
||||
// current minute
|
||||
int32 minute = 4;
|
||||
// the article list.
|
||||
repeated RecArticle articles = 5;
|
||||
}
|
||||
|
||||
message RecommendResponse {
|
||||
repeated int64 articles = 1;
|
||||
}
|
||||
|
||||
service RecommendService {
|
||||
// the method to get the topk performers for this user.
|
||||
rpc recommend1(RecommendRequest) returns (RecommendResponse);
|
||||
rpc recommend2(RecommendRequest) returns (RecommendResponse);
|
||||
}
|
||||
20
tools/goctl/feature/feature.go
Normal file
20
tools/goctl/feature/feature.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package feature
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/logrusorgru/aurora"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
var feature = `
|
||||
1、新增对rpc错误转换处理
|
||||
1.1、目前暂时仅处理not found 和 unknown错误
|
||||
2、增加feature命令支持,详细使用请通过命令[goctl -feature]查看
|
||||
`
|
||||
|
||||
func Feature(c *cli.Context) error {
|
||||
fmt.Println(aurora.Blue("\nFEATURE:"))
|
||||
fmt.Println(aurora.Blue(feature))
|
||||
return nil
|
||||
}
|
||||
36
tools/goctl/gen/dockerfile.go
Normal file
36
tools/goctl/gen/dockerfile.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package gen
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"zero/tools/goctl/util"
|
||||
"zero/tools/goctl/vars"
|
||||
)
|
||||
|
||||
func GenerateDockerfile(goFile string, args ...string) error {
|
||||
relPath, err := util.PathFromGoSrc()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
out, err := util.CreateIfNotExist("Dockerfile")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer out.Close()
|
||||
|
||||
var builder strings.Builder
|
||||
for _, arg := range args {
|
||||
builder.WriteString(`, "` + arg + `"`)
|
||||
}
|
||||
|
||||
t := template.Must(template.New("dockerfile").Parse(dockerTemplate))
|
||||
return t.Execute(out, map[string]string{
|
||||
"projectName": vars.ProjectName,
|
||||
"goRelPath": relPath,
|
||||
"goFile": goFile,
|
||||
"exeFile": util.FileNameWithoutExt(goFile),
|
||||
"argument": builder.String(),
|
||||
})
|
||||
}
|
||||
52
tools/goctl/gen/makefile.go
Normal file
52
tools/goctl/gen/makefile.go
Normal file
@@ -0,0 +1,52 @@
|
||||
package gen
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"zero/tools/goctl/util"
|
||||
)
|
||||
|
||||
func GenerateMakefile(goFile, namespace string) error {
|
||||
relPath, err := util.PathFromGoSrc()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
movePath, err := getMovePath()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
out, err := util.CreateIfNotExist("Makefile")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer out.Close()
|
||||
|
||||
t := template.Must(template.New("makefile").Parse(makefileTemplate))
|
||||
return t.Execute(out, map[string]string{
|
||||
"rootRelPath": movePath,
|
||||
"relPath": relPath,
|
||||
"exeFile": util.FileNameWithoutExt(goFile),
|
||||
"namespace": namespace,
|
||||
})
|
||||
}
|
||||
|
||||
func getMovePath() (string, error) {
|
||||
relPath, err := util.PathFromGoSrc()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var builder strings.Builder
|
||||
for range strings.Split(relPath, "/") {
|
||||
builder.WriteString("../")
|
||||
}
|
||||
|
||||
if move := builder.String(); len(move) == 0 {
|
||||
return ".", nil
|
||||
} else {
|
||||
return move, nil
|
||||
}
|
||||
}
|
||||
44
tools/goctl/gen/template.go
Normal file
44
tools/goctl/gen/template.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package gen
|
||||
|
||||
const (
|
||||
dockerTemplate = `FROM golang:alpine AS builder
|
||||
|
||||
LABEL stage=gobuilder
|
||||
|
||||
ENV CGO_ENABLED 0
|
||||
ENV GOOS linux
|
||||
ENV GOPROXY https://goproxy.cn,direct
|
||||
|
||||
WORKDIR $GOPATH/src/{{.projectName}}
|
||||
COPY . .
|
||||
RUN go build -ldflags="-s -w" -o /app/{{.exeFile}} {{.goRelPath}}/{{.goFile}}
|
||||
|
||||
|
||||
FROM alpine
|
||||
|
||||
RUN apk update --no-cache
|
||||
RUN apk add --no-cache ca-certificates
|
||||
RUN apk add --no-cache tzdata
|
||||
ENV TZ Asia/Shanghai
|
||||
|
||||
WORKDIR /app
|
||||
COPY --from=builder /app/{{.exeFile}} /app/{{.exeFile}}
|
||||
|
||||
CMD ["./{{.exeFile}}"{{.argument}}]
|
||||
`
|
||||
|
||||
makefileTemplate = `version := v$(shell /bin/date "+%y%m%d%H%M%S")
|
||||
|
||||
build:
|
||||
docker pull alpine
|
||||
docker pull golang:alpine
|
||||
cd $(GOPATH)/src/xiao && docker build -t registry.cn-hangzhou.aliyuncs.com/xapp/{{.exeFile}}:$(version) . -f {{.relPath}}/Dockerfile
|
||||
docker image prune --filter label=stage=gobuilder -f
|
||||
|
||||
push: build
|
||||
docker push registry.cn-hangzhou.aliyuncs.com/xapp/{{.exeFile}}:$(version)
|
||||
|
||||
deploy: push
|
||||
kubectl -n {{.namespace}} set image deployment/{{.exeFile}}-deployment {{.exeFile}}=registry-vpc.cn-hangzhou.aliyuncs.com/xapp/{{.exeFile}}:$(version)
|
||||
`
|
||||
)
|
||||
369
tools/goctl/goctl.go
Normal file
369
tools/goctl/goctl.go
Normal file
@@ -0,0 +1,369 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/user"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"zero/core/conf"
|
||||
"zero/core/hash"
|
||||
"zero/core/lang"
|
||||
"zero/core/logx"
|
||||
"zero/core/mapreduce"
|
||||
"zero/core/stringx"
|
||||
"zero/tools/goctl/api/apigen"
|
||||
"zero/tools/goctl/api/dartgen"
|
||||
"zero/tools/goctl/api/docgen"
|
||||
"zero/tools/goctl/api/format"
|
||||
"zero/tools/goctl/api/gogen"
|
||||
"zero/tools/goctl/api/javagen"
|
||||
"zero/tools/goctl/api/tsgen"
|
||||
"zero/tools/goctl/api/validate"
|
||||
"zero/tools/goctl/configgen"
|
||||
"zero/tools/goctl/docker"
|
||||
"zero/tools/goctl/feature"
|
||||
"zero/tools/goctl/model/mongomodel"
|
||||
"zero/tools/goctl/util"
|
||||
|
||||
"github.com/logrusorgru/aurora"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
const (
|
||||
autoUpdate = "GOCTL_AUTO_UPDATE"
|
||||
configFile = ".goctl"
|
||||
configTemplate = `url = http://47.97.184.41:7777/`
|
||||
toolName = "goctl"
|
||||
)
|
||||
|
||||
var (
|
||||
BuildTime = "not set"
|
||||
commands = []cli.Command{
|
||||
{
|
||||
Name: "api",
|
||||
Usage: "generate api related files",
|
||||
Flags: []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "o",
|
||||
Usage: "the output api file",
|
||||
},
|
||||
},
|
||||
Action: apigen.ApiCommand,
|
||||
Subcommands: []cli.Command{
|
||||
{
|
||||
Name: "format",
|
||||
Usage: "format api files",
|
||||
Flags: []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "dir",
|
||||
Usage: "the format target dir",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "p",
|
||||
Usage: "print result to console",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "iu",
|
||||
Usage: "ignore update",
|
||||
Required: false,
|
||||
},
|
||||
},
|
||||
Action: format.GoFormatApi,
|
||||
},
|
||||
{
|
||||
Name: "validate",
|
||||
Usage: "validate api file",
|
||||
Flags: []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "api",
|
||||
Usage: "validate target api file",
|
||||
},
|
||||
},
|
||||
Action: validate.GoValidateApi,
|
||||
},
|
||||
{
|
||||
Name: "doc",
|
||||
Usage: "generate doc files",
|
||||
Flags: []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "dir",
|
||||
Usage: "the target dir",
|
||||
},
|
||||
},
|
||||
Action: docgen.DocCommand,
|
||||
},
|
||||
{
|
||||
Name: "go",
|
||||
Usage: "generate go files for provided api in yaml file",
|
||||
Flags: []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "dir",
|
||||
Usage: "the target dir",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "api",
|
||||
Usage: "the api file",
|
||||
},
|
||||
},
|
||||
Action: gogen.GoCommand,
|
||||
},
|
||||
{
|
||||
Name: "java",
|
||||
Usage: "generate java files for provided api in api file",
|
||||
Flags: []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "dir",
|
||||
Usage: "the target dir",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "api",
|
||||
Usage: "the api file",
|
||||
},
|
||||
},
|
||||
Action: javagen.JavaCommand,
|
||||
},
|
||||
{
|
||||
Name: "ts",
|
||||
Usage: "generate ts files for provided api in api file",
|
||||
Flags: []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "dir",
|
||||
Usage: "the target dir",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "api",
|
||||
Usage: "the api file",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "webapi",
|
||||
Usage: "the web api file path",
|
||||
Required: false,
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "caller",
|
||||
Usage: "the web api caller",
|
||||
Required: false,
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "unwrap",
|
||||
Usage: "unwrap the webapi caller for import",
|
||||
Required: false,
|
||||
},
|
||||
},
|
||||
Action: tsgen.TsCommand,
|
||||
},
|
||||
{
|
||||
Name: "dart",
|
||||
Usage: "generate dart files for provided api in api file",
|
||||
Flags: []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "dir",
|
||||
Usage: "the target dir",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "api",
|
||||
Usage: "the api file",
|
||||
},
|
||||
},
|
||||
Action: dartgen.DartCommand,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "docker",
|
||||
Usage: "generate Dockerfile and Makefile",
|
||||
Flags: []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "go",
|
||||
Usage: "the file that contains main function",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "namespace, n",
|
||||
Usage: "which namespace of kubernetes to deploy the service",
|
||||
},
|
||||
},
|
||||
Action: docker.DockerCommand,
|
||||
},
|
||||
{
|
||||
Name: "model",
|
||||
Usage: "generate sql model",
|
||||
Flags: []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "config, c",
|
||||
Usage: "the file that contains main function",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "dir, d",
|
||||
Usage: "the target dir",
|
||||
},
|
||||
},
|
||||
Subcommands: []cli.Command{
|
||||
{
|
||||
Name: "mongo",
|
||||
Usage: "generate mongoModel files for provided somemongo.go in go file",
|
||||
Flags: []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "src, s",
|
||||
Usage: "the src file",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "cache",
|
||||
Usage: "need cache code([yes/no] default value is no)",
|
||||
},
|
||||
},
|
||||
Action: mongomodel.ModelCommond,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "config",
|
||||
Usage: "generate config json",
|
||||
Flags: []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "path, p",
|
||||
Usage: "the target config go file",
|
||||
},
|
||||
},
|
||||
Action: configgen.GenConfigCommand,
|
||||
},
|
||||
{
|
||||
Name: "feature",
|
||||
Usage: "the features of the latest version",
|
||||
Action: feature.Feature,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
func genConfigFile(file string) error {
|
||||
return ioutil.WriteFile(file, []byte(configTemplate), 0600)
|
||||
}
|
||||
|
||||
func getAbsFile() (string, error) {
|
||||
exe, err := os.Executable()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
dir, err := filepath.Abs(filepath.Dir(exe))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return path.Join(dir, filepath.Base(os.Args[0])), nil
|
||||
}
|
||||
|
||||
func getFilePerm(file string) (os.FileMode, error) {
|
||||
info, err := os.Stat(file)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return info.Mode(), nil
|
||||
}
|
||||
|
||||
func update() {
|
||||
usr, err := user.Current()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
absConfigFile := path.Join(usr.HomeDir, configFile)
|
||||
if !util.FileExists(absConfigFile) {
|
||||
if err := genConfigFile(absConfigFile); err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
props, err := conf.LoadProperties(absConfigFile)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
u, err := url.Parse(props.GetString("url"))
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
u.Path = path.Join(u.Path, toolName)
|
||||
req, err := http.NewRequest(http.MethodGet, u.String(), nil)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
file, err := getAbsFile()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
content, err := ioutil.ReadFile(file)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Md5", hash.Md5Hex(content))
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
mode, err := getFilePerm(file)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
content, err = ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
switch resp.StatusCode {
|
||||
case http.StatusOK:
|
||||
if err := ioutil.WriteFile(file, content, mode); err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
logx.Disable()
|
||||
|
||||
done := make(chan lang.PlaceholderType)
|
||||
mapreduce.FinishVoid(func() {
|
||||
if os.Getenv(autoUpdate) != "off" && !stringx.Contains(os.Args, "-iu") {
|
||||
update()
|
||||
}
|
||||
close(done)
|
||||
}, func() {
|
||||
app := cli.NewApp()
|
||||
app.Usage = "a cli tool to generate code"
|
||||
app.Version = BuildTime
|
||||
app.Commands = commands
|
||||
// cli already print error messages
|
||||
if err := app.Run(os.Args); err != nil {
|
||||
fmt.Println("error:", err)
|
||||
}
|
||||
}, func() {
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(time.Second):
|
||||
fmt.Println(aurora.Yellow("Updating goctl, please wait..."))
|
||||
}
|
||||
})
|
||||
}
|
||||
266
tools/goctl/goctl.md
Normal file
266
tools/goctl/goctl.md
Normal file
@@ -0,0 +1,266 @@
|
||||
# goctl使用说明
|
||||
|
||||
## goctl用途
|
||||
* 定义api请求
|
||||
* 根据定义的api自动生成golang(后端), java(iOS & Android), typescript(web & 晓程序),dart(flutter)
|
||||
* 生成MySQL CURD (https://goctl.xiaoheiban.cn)
|
||||
* 生成MongoDB CURD (https://goctl.xiaoheiban.cn)
|
||||
|
||||
## goctl使用说明
|
||||
#### goctl参数说明
|
||||
|
||||
`goctl api [go/java/ts] [-api user/user.api] [-dir ./src]`
|
||||
|
||||
> api 后面接生成的语言,现支持go/java/typescript
|
||||
|
||||
> -api 自定义api所在路径
|
||||
|
||||
> -dir 自定义生成目录
|
||||
|
||||
#### 保持goctl总是最新版
|
||||
|
||||
第一次运行会在~/.goctl里增加下面两行:
|
||||
|
||||
```
|
||||
url = http://47.97.184.41:7777/
|
||||
```
|
||||
|
||||
#### API 语法说明
|
||||
|
||||
```
|
||||
info(
|
||||
title: doc title
|
||||
desc: >
|
||||
doc description first part,
|
||||
doc description second part<
|
||||
version: 1.0
|
||||
)
|
||||
|
||||
type int userType
|
||||
|
||||
type user struct {
|
||||
name string `json:"user"` // 用户姓名
|
||||
}
|
||||
|
||||
type student struct {
|
||||
name string `json:"name"` // 学生姓名
|
||||
}
|
||||
|
||||
type teacher struct {
|
||||
}
|
||||
|
||||
type (
|
||||
address struct {
|
||||
city string `json:"city"` // 城市
|
||||
}
|
||||
|
||||
innerType struct {
|
||||
image string `json:"image"`
|
||||
}
|
||||
|
||||
createRequest struct {
|
||||
innerType
|
||||
name string `form:"name"` // niha
|
||||
age int `form:"age,optional"` // nihaod
|
||||
address []address `json:"address,optional"`
|
||||
}
|
||||
|
||||
getRequest struct {
|
||||
name string `path:"name"`
|
||||
age int `form:"age,optional"`
|
||||
}
|
||||
|
||||
getResponse struct {
|
||||
code int `json:"code"`
|
||||
desc string `json:"desc,omitempty"`
|
||||
address address `json:"address"`
|
||||
service int `json:"service"`
|
||||
}
|
||||
)
|
||||
|
||||
service user-api {
|
||||
@doc(
|
||||
summary: user title
|
||||
desc: >
|
||||
user description first part,
|
||||
user description second part,
|
||||
user description second line
|
||||
)
|
||||
@server(
|
||||
handler: GetUserHandler
|
||||
folder: user
|
||||
)
|
||||
get /api/user/:name(getRequest) returns(getResponse)
|
||||
|
||||
@server(
|
||||
handler: CreateUserHandler
|
||||
folder: user
|
||||
)
|
||||
post /api/users/create(createRequest)
|
||||
}
|
||||
|
||||
@server(
|
||||
jwt: Auth
|
||||
folder: profile
|
||||
)
|
||||
service user-api {
|
||||
@doc(summary: user title)
|
||||
@server(
|
||||
handler: GetProfileHandler
|
||||
)
|
||||
get /api/profile/:name(getRequest) returns(getResponse)
|
||||
|
||||
@server(
|
||||
handler: CreateProfileHandler
|
||||
)
|
||||
post /api/profile/create(createRequest)
|
||||
}
|
||||
|
||||
service user-api {
|
||||
@doc(summary: desc in one line)
|
||||
@server(
|
||||
handler: PingHandler
|
||||
)
|
||||
head /api/ping()
|
||||
}
|
||||
```
|
||||
1. info部分:描述了api基本信息,比如Auth,api是哪个用途。
|
||||
2. type部分:type类型声明和golang语法兼容。
|
||||
3. service部分:service代表一组服务,一个服务可以由多组名称相同的service组成,可以针对每一组service配置jwt和auth认证,另外通过folder属性可以指定service生成所在子目录。
|
||||
service里面包含api路由,比如上面第一组service的第一个路由,doc用来描述此路由的用途,GetProfileHandler表示处理这个路由的handler,
|
||||
`get /api/profile/:name(getRequest) returns(getResponse)` 中get代表api的请求方式(get/post/put/delete), `/api/profile/:name` 描述了路由path,`:name`通过
|
||||
请求getRequest里面的属性赋值,getResponse为返回的结构体,这两个类型都定义在2描述的类型中。
|
||||
|
||||
#### api vscode插件
|
||||
开发者可以在vscode中搜索goctl的api插件,它提供了api语法高亮,语法检测和格式化相关功能。
|
||||
|
||||
1. 支持语法高亮和类型导航。
|
||||
2. 语法检测,格式化api会自动检测api编写错误地方,用vscode默认的格式化快捷键(option+command+F)或者自定义的也可以。
|
||||
3. 格式化(option+command+F),类似代码格式化,统一样式支持。
|
||||
|
||||
#### 根据定义好的api文件生成golang代码
|
||||
|
||||
命令如下:
|
||||
`goctl api go -api user/user.api -dir user`
|
||||
|
||||
```
|
||||
|
||||
.
|
||||
├── internal
|
||||
│ ├── config
|
||||
│ │ └── config.go
|
||||
│ ├── handler
|
||||
│ │ ├── pinghandler.go
|
||||
│ │ ├── profile
|
||||
│ │ │ ├── createprofilehandler.go
|
||||
│ │ │ └── getprofilehandler.go
|
||||
│ │ ├── routes.go
|
||||
│ │ └── user
|
||||
│ │ ├── createuserhandler.go
|
||||
│ │ └── getuserhandler.go
|
||||
│ ├── logic
|
||||
│ │ ├── pinglogic.go
|
||||
│ │ ├── profile
|
||||
│ │ │ ├── createprofilelogic.go
|
||||
│ │ │ └── getprofilelogic.go
|
||||
│ │ └── user
|
||||
│ │ ├── createuserlogic.go
|
||||
│ │ └── getuserlogic.go
|
||||
│ ├── svc
|
||||
│ │ └── servicecontext.go
|
||||
│ └── types
|
||||
│ └── types.go
|
||||
└── user.go
|
||||
|
||||
```
|
||||
生成的代码可以直接跑,有几个地方需要改:
|
||||
|
||||
* 在`servicecontext.go`里面增加需要传递给logic的一些资源,比如mysql, redis,rpc等
|
||||
* 在定义的get/post/put/delete等请求的handler和logic里增加处理业务逻辑的代码
|
||||
|
||||
#### 根据定义好的api文件生成java代码
|
||||
`goctl api java -api user/user.api -dir ./src`
|
||||
|
||||
#### 根据定义好的api文件生成typescript代码
|
||||
`goctl api ts -api user/user.api -dir ./src -webapi ***`
|
||||
|
||||
ts需要指定webapi所在目录
|
||||
|
||||
#### 根据定义好的api文件生成Dart代码
|
||||
`goctl api dart -api user/user.api -dir ./src`
|
||||
|
||||
## 根据定义好的简单go文件生成mongo代码文件(仅限golang使用)
|
||||
`goctl model mongo -src {{yourDir}}/xiao/service/xhb/user/model/usermodel.go -cache yes`
|
||||
|
||||
-src需要提供简单的usermodel.go文件,里面只需要提供一个结构体即可
|
||||
-cache 控制是否需要缓存 yes=需要 no=不需要
|
||||
src 示例代码如下
|
||||
```
|
||||
package model
|
||||
|
||||
type User struct {
|
||||
Name string `o:"find,get,set" c:"姓名"`
|
||||
Age int `o:"find,get,set" c:"年纪"`
|
||||
School string `c:"学校"`
|
||||
}
|
||||
|
||||
```
|
||||
结构体中不需要提供Id,CreateTime,UpdateTime三个字段,会自动生成
|
||||
结构体中每个tag有两个可选标签 c 和 o
|
||||
c是改字段的注释
|
||||
o是改字段需要生产的操作函数 可以取得get,find,set 分别表示生成返回单个对象的查询方法,返回多个对象的查询方法,设置该字段方法
|
||||
生成的目标文件会覆盖该简单go文件
|
||||
|
||||
## goctl rpc生成
|
||||
|
||||
命令 `goctl rpc proto -proto ${proto} -service ${serviceName} -project ${projectName} -dir ${directory} -shared ${shared}`
|
||||
如: `goctl rpc proto -proto test.proto -service test -project xjy -dir .`
|
||||
|
||||
参数说明:
|
||||
|
||||
- ${proto}: proto文件
|
||||
- ${serviceName}: rpc服务名称
|
||||
- ${projectName}: 所属项目,如xjy,xhb,crm,hera,具体查看help,主要为了根据不同项目服务往redis注册key,可选
|
||||
- ${directory}: 输出目录
|
||||
- ${shared}: shared文件生成目录,可选,默认为${pwd}/shared
|
||||
|
||||
生成目录结构示例:
|
||||
|
||||
``` go
|
||||
.
|
||||
├── shared [示例目录,可自己指定,强制覆盖更新]
|
||||
│ └── contentservicemodel.go
|
||||
├── test
|
||||
│ ├── etc
|
||||
│ │ └── test.json
|
||||
│ ├── internal
|
||||
│ │ ├── config
|
||||
│ │ │ └── config.go
|
||||
│ │ ├── handler [强制覆盖更新]
|
||||
│ │ │ ├── changeavatarhandler.go
|
||||
│ │ │ ├── changebirthdayhandler.go
|
||||
│ │ │ ├── changenamehandler.go
|
||||
│ │ │ ├── changepasswordhandler.go
|
||||
│ │ │ ├── changeuserinfohandler.go
|
||||
│ │ │ ├── getuserinfohandler.go
|
||||
│ │ │ ├── loginhandler.go
|
||||
│ │ │ ├── logouthandler.go
|
||||
│ │ │ └── testhandler.go
|
||||
│ │ ├── logic
|
||||
│ │ │ ├── changeavatarlogic.go
|
||||
│ │ │ ├── changebirthdaylogic.go
|
||||
│ │ │ ├── changenamelogic.go
|
||||
│ │ │ ├── changepasswordlogic.go
|
||||
│ │ │ ├── changeuserinfologic.go
|
||||
│ │ │ ├── getuserinfologic.go
|
||||
│ │ │ ├── loginlogic.go
|
||||
│ │ │ └── logoutlogic.go
|
||||
│ │ └── svc
|
||||
│ │ └── servicecontext.go
|
||||
│ ├── pb
|
||||
│ │ └── test.pb.go
|
||||
│ └── test.go [强制覆盖更新]
|
||||
└── test.proto
|
||||
```
|
||||
- 注意 :目前rpc目录生成的proto文件暂不支持import外部proto文件
|
||||
* 如有不理解的地方,随时问Kim/Kevin
|
||||
126
tools/goctl/k8s/apirpc.go
Normal file
126
tools/goctl/k8s/apirpc.go
Normal file
@@ -0,0 +1,126 @@
|
||||
package k8s
|
||||
|
||||
var apiRpcTmeplate = `apiVersion: apps/v1beta2
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{.name}}
|
||||
namespace: {{.namespace}}
|
||||
labels:
|
||||
app: {{.name}}
|
||||
spec:
|
||||
replicas: {{.replicas}}
|
||||
revisionHistoryLimit: {{.revisionHistoryLimit}}
|
||||
selector:
|
||||
matchLabels:
|
||||
app: {{.name}}
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: {{.name}}
|
||||
spec:{{if .envIsDev}}
|
||||
terminationGracePeriodSeconds: 60{{end}}
|
||||
containers:
|
||||
- name: {{.name}}
|
||||
image: registry-vpc.cn-hangzhou.aliyuncs.com/{{.namespace}}/
|
||||
lifecycle:
|
||||
preStop:
|
||||
exec:
|
||||
command: ["sh","-c","sleep 5"]
|
||||
ports:
|
||||
- containerPort: {{.port}}
|
||||
readinessProbe:
|
||||
tcpSocket:
|
||||
port: {{.port}}
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 10
|
||||
livenessProbe:
|
||||
tcpSocket:
|
||||
port: {{.port}}
|
||||
initialDelaySeconds: 15
|
||||
periodSeconds: 20
|
||||
env:
|
||||
- name: aliyun_logs_k8slog
|
||||
value: "stdout"
|
||||
- name: aliyun_logs_k8slog_tags
|
||||
value: "stage={{.env}}"
|
||||
- name: aliyun_logs_k8slog_format
|
||||
value: "json"
|
||||
resources:
|
||||
limits:
|
||||
cpu: {{.limitCpu}}m
|
||||
memory: {{.limitMem}}Mi
|
||||
requests:
|
||||
cpu: {{.requestCpu}}m
|
||||
memory: {{.requestMem}}Mi
|
||||
command:
|
||||
- ./{{.serviceName}}
|
||||
- -f
|
||||
- ./{{.name}}.json
|
||||
volumeMounts:
|
||||
- name: timezone
|
||||
mountPath: /etc/localtime
|
||||
imagePullSecrets:
|
||||
- name: {{.namespace}}
|
||||
volumes:
|
||||
- name: timezone
|
||||
hostPath:
|
||||
path: /usr/share/zoneinfo/Asia/Shanghai
|
||||
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{.name}}-svc
|
||||
namespace: {{.namespace}}
|
||||
spec:
|
||||
ports:
|
||||
- nodePort: 3{{.port}}
|
||||
port: {{.port}}
|
||||
protocol: TCP
|
||||
targetPort: {{.port}}
|
||||
selector:
|
||||
app: {{.name}}
|
||||
sessionAffinity: None
|
||||
type: NodePort{{if .envIsPreOrPro}}
|
||||
|
||||
---
|
||||
apiVersion: autoscaling/v2beta1
|
||||
kind: HorizontalPodAutoscaler
|
||||
metadata:
|
||||
name: {{.name}}-hpa-c
|
||||
namespace: {{.namespace}}
|
||||
labels:
|
||||
app: {{.name}}-hpa-c
|
||||
spec:
|
||||
scaleTargetRef:
|
||||
apiVersion: apps/v1beta1
|
||||
kind: Deployment
|
||||
name: di-api
|
||||
minReplicas: {{.minReplicas}}
|
||||
maxReplicas: {{.maxReplicas}}
|
||||
metrics:
|
||||
- type: Resource
|
||||
resource:
|
||||
name: cpu
|
||||
targetAverageUtilization: 80
|
||||
|
||||
---
|
||||
apiVersion: autoscaling/v2beta1
|
||||
kind: HorizontalPodAutoscaler
|
||||
metadata:
|
||||
name: {{.name}}-hpa-m
|
||||
namespace: {{.namespace}}
|
||||
labels:
|
||||
app: {{.name}}-hpa-m
|
||||
spec:
|
||||
scaleTargetRef:
|
||||
apiVersion: apps/v1beta1
|
||||
kind: Deployment
|
||||
name: {{.name}}
|
||||
minReplicas: {{.minReplicas}}
|
||||
maxReplicas: {{.maxReplicas}}
|
||||
metrics:
|
||||
- type: Resource
|
||||
resource:
|
||||
name: memory
|
||||
targetAverageUtilization: 80{{end}}`
|
||||
46
tools/goctl/k8s/job.go
Normal file
46
tools/goctl/k8s/job.go
Normal file
@@ -0,0 +1,46 @@
|
||||
package k8s
|
||||
|
||||
// 无环境区分
|
||||
var jobTmeplate = `apiVersion: batch/v1beta1
|
||||
kind: CronJob
|
||||
metadata:
|
||||
name: {{.name}}
|
||||
namespace: {{.namespace}}
|
||||
spec:
|
||||
successfulJobsHistoryLimit: {{.successfulJobsHistoryLimit}}
|
||||
schedule: "{{.schedule}}"
|
||||
jobTemplate:
|
||||
spec:
|
||||
template:
|
||||
spec:
|
||||
containers:
|
||||
- name: {{.name}}
|
||||
image: registry-vpc.cn-hangzhou.aliyuncs.com/{{.namespace}}/
|
||||
env:
|
||||
- name: aliyun_logs_k8slog
|
||||
value: "stdout"
|
||||
- name: aliyun_logs_k8slog_tags
|
||||
value: "stage={{.env}}"
|
||||
- name: aliyun_logs_k8slog_format
|
||||
value: "json"
|
||||
resources:
|
||||
limits:
|
||||
cpu: {{.limitCpu}}m
|
||||
memory: {{.limitMem}}Mi
|
||||
requests:
|
||||
cpu: {{.requestCpu}}m
|
||||
memory: {{.requestMem}}Mi
|
||||
command:
|
||||
- ./{{.serviceName}}
|
||||
- -f
|
||||
- ./{{.name}}.json
|
||||
volumeMounts:
|
||||
- name: timezone
|
||||
mountPath: /etc/localtime
|
||||
imagePullSecrets:
|
||||
- name: {{.namespace}}
|
||||
restartPolicy: OnFailure
|
||||
volumes:
|
||||
- name: timezone
|
||||
hostPath:
|
||||
path: /usr/share/zoneinfo/Asia/Shanghai`
|
||||
136
tools/goctl/k8s/k8s.go
Normal file
136
tools/goctl/k8s/k8s.go
Normal file
@@ -0,0 +1,136 @@
|
||||
package k8s
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
var (
|
||||
errUnknownServiceType = errors.New("unknown service type")
|
||||
)
|
||||
|
||||
const (
|
||||
ServiceTypeApi ServiceType = "api"
|
||||
ServiceTypeRpc ServiceType = "rpc"
|
||||
ServiceTypeJob ServiceType = "job"
|
||||
ServiceTypeRmq ServiceType = "rmq"
|
||||
ServiceTypeSync ServiceType = "sync"
|
||||
envDev = "dev"
|
||||
envPre = "pre"
|
||||
envPro = "pro"
|
||||
)
|
||||
|
||||
type (
|
||||
ServiceType string
|
||||
K8sRequest struct {
|
||||
Env string
|
||||
ServiceName string
|
||||
ServiceType ServiceType
|
||||
Namespace string
|
||||
Schedule string
|
||||
Replicas int
|
||||
RevisionHistoryLimit int
|
||||
Port int
|
||||
LimitCpu int
|
||||
LimitMem int
|
||||
RequestCpu int
|
||||
RequestMem int
|
||||
SuccessfulJobsHistoryLimit int
|
||||
HpaMinReplicas int
|
||||
HpaMaxReplicas int
|
||||
}
|
||||
)
|
||||
|
||||
func Gen(req K8sRequest) (string, error) {
|
||||
switch req.ServiceType {
|
||||
case ServiceTypeApi, ServiceTypeRpc:
|
||||
return genApiRpc(req)
|
||||
case ServiceTypeJob:
|
||||
return genJob(req)
|
||||
case ServiceTypeRmq, ServiceTypeSync:
|
||||
return genRmqSync(req)
|
||||
default:
|
||||
return "", errUnknownServiceType
|
||||
}
|
||||
}
|
||||
|
||||
func genApiRpc(req K8sRequest) (string, error) {
|
||||
t, err := template.New("api_rpc").Parse(apiRpcTmeplate)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
buffer := new(bytes.Buffer)
|
||||
err = t.Execute(buffer, map[string]interface{}{
|
||||
"name": fmt.Sprintf("%s-%s", req.ServiceName, req.ServiceType),
|
||||
"namespace": req.Namespace,
|
||||
"replicas": req.Replicas,
|
||||
"revisionHistoryLimit": req.RevisionHistoryLimit,
|
||||
"port": req.Port,
|
||||
"limitCpu": req.LimitCpu,
|
||||
"limitMem": req.LimitMem,
|
||||
"requestCpu": req.RequestCpu,
|
||||
"requestMem": req.RequestMem,
|
||||
"serviceName": req.ServiceName,
|
||||
"env": req.Env,
|
||||
"envIsPreOrPro": req.Env != envDev,
|
||||
"envIsDev": req.Env == envDev,
|
||||
"minReplicas": req.HpaMinReplicas,
|
||||
"maxReplicas": req.HpaMaxReplicas,
|
||||
})
|
||||
if err != nil {
|
||||
return "", nil
|
||||
}
|
||||
return buffer.String(), nil
|
||||
}
|
||||
|
||||
func genRmqSync(req K8sRequest) (string, error) {
|
||||
t, err := template.New("rmq_sync").Parse(rmqSyncTmeplate)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
buffer := new(bytes.Buffer)
|
||||
err = t.Execute(buffer, map[string]interface{}{
|
||||
"name": fmt.Sprintf("%s-%s", req.ServiceName, req.ServiceType),
|
||||
"namespace": req.Namespace,
|
||||
"replicas": req.Replicas,
|
||||
"revisionHistoryLimit": req.RevisionHistoryLimit,
|
||||
"limitCpu": req.LimitCpu,
|
||||
"limitMem": req.LimitMem,
|
||||
"requestCpu": req.RequestCpu,
|
||||
"requestMem": req.RequestMem,
|
||||
"serviceName": req.ServiceName,
|
||||
"env": req.Env,
|
||||
"envIsPreOrPro": req.Env != envDev,
|
||||
"envIsDev": req.Env == envDev,
|
||||
})
|
||||
if err != nil {
|
||||
return "", nil
|
||||
}
|
||||
return buffer.String(), nil
|
||||
}
|
||||
|
||||
func genJob(req K8sRequest) (string, error) {
|
||||
t, err := template.New("job").Parse(jobTmeplate)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
buffer := new(bytes.Buffer)
|
||||
err = t.Execute(buffer, map[string]interface{}{
|
||||
"name": fmt.Sprintf("%s-%s", req.ServiceName, req.ServiceType),
|
||||
"namespace": req.Namespace,
|
||||
"schedule": req.Schedule,
|
||||
"successfulJobsHistoryLimit": req.SuccessfulJobsHistoryLimit,
|
||||
"limitCpu": req.LimitCpu,
|
||||
"limitMem": req.LimitMem,
|
||||
"requestCpu": req.RequestCpu,
|
||||
"requestMem": req.RequestMem,
|
||||
"serviceName": req.ServiceName,
|
||||
"env": req.Env,
|
||||
})
|
||||
if err != nil {
|
||||
return "", nil
|
||||
}
|
||||
return buffer.String(), nil
|
||||
}
|
||||
68
tools/goctl/k8s/rmqsync.go
Normal file
68
tools/goctl/k8s/rmqsync.go
Normal file
@@ -0,0 +1,68 @@
|
||||
package k8s
|
||||
|
||||
var rmqSyncTmeplate = `apiVersion: apps/v1beta2
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{.name}}
|
||||
namespace: {{.namespace}}
|
||||
labels:
|
||||
app: {{.name}}
|
||||
spec:
|
||||
replicas: {{.replicas}}
|
||||
revisionHistoryLimit: {{.revisionHistoryLimit}}
|
||||
selector:
|
||||
matchLabels:
|
||||
app: {{.name}}
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: {{.name}}
|
||||
spec:{{if .envIsDev}}
|
||||
terminationGracePeriodSeconds: 60{{end}}
|
||||
containers:
|
||||
- name: {{.name}}
|
||||
image: registry-vpc.cn-hangzhou.aliyuncs.com/{{.namespace}}/
|
||||
lifecycle:
|
||||
preStop:
|
||||
exec:
|
||||
command: ["sh","-c","sleep 5"]
|
||||
env:
|
||||
- name: aliyun_logs_k8slog
|
||||
value: "stdout"
|
||||
- name: aliyun_logs_k8slog_tags
|
||||
value: "stage={{.env}}"
|
||||
- name: aliyun_logs_k8slog_format
|
||||
value: "json"
|
||||
resources:
|
||||
limits:
|
||||
cpu: {{.limitCpu}}m
|
||||
memory: {{.limitMem}}Mi
|
||||
requests:
|
||||
cpu: {{.requestCpu}}m
|
||||
memory: {{.requestMem}}Mi
|
||||
command:
|
||||
- ./{{.serviceName}}
|
||||
- -f
|
||||
- ./{{.name}}.json
|
||||
volumeMounts:
|
||||
- name: timezone
|
||||
mountPath: /etc/localtime
|
||||
imagePullSecrets:
|
||||
- name: {{.namespace}}
|
||||
volumes:
|
||||
- name: timezone
|
||||
hostPath:
|
||||
path: /usr/share/zoneinfo/Asia/Shanghai{{if .envIsPreOrPro}}
|
||||
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{.name}}-svc
|
||||
namespace: {{.namespace}}
|
||||
spec:
|
||||
selector:
|
||||
app: {{.name}}
|
||||
sessionAffinity: None
|
||||
type: ClusterIP
|
||||
clusterIP: None{{end}}`
|
||||
33
tools/goctl/model/mongomodel/gen/genmethod.go
Normal file
33
tools/goctl/model/mongomodel/gen/genmethod.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package gen
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"zero/tools/goctl/model/mongomodel/utils"
|
||||
)
|
||||
|
||||
func genMethodTemplate(funcDesc FunctionDesc, needCache bool) (template string) {
|
||||
var tmp string
|
||||
switch funcDesc.Type {
|
||||
case functionTypeGet:
|
||||
if needCache {
|
||||
tmp = getTemplate
|
||||
} else {
|
||||
tmp = noCacheGetTemplate
|
||||
}
|
||||
case functionTypeFind:
|
||||
tmp = findTemplate
|
||||
case functionTypeSet:
|
||||
if needCache {
|
||||
tmp = ""
|
||||
} else {
|
||||
tmp = noCacheSetFieldtemplate
|
||||
}
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
tmp = strings.ReplaceAll(tmp, "{{.Name}}", funcDesc.FieldName)
|
||||
tmp = strings.ReplaceAll(tmp, "{{.name}}", utils.UpperCamelToLower(funcDesc.FieldName))
|
||||
tmp = strings.ReplaceAll(tmp, "{{.type}}", funcDesc.FieldType)
|
||||
return tmp
|
||||
}
|
||||
156
tools/goctl/model/mongomodel/gen/genmongomodel.go
Normal file
156
tools/goctl/model/mongomodel/gen/genmongomodel.go
Normal file
@@ -0,0 +1,156 @@
|
||||
package gen
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"zero/tools/goctl/api/spec"
|
||||
"zero/tools/goctl/api/util"
|
||||
"zero/tools/goctl/model/mongomodel/utils"
|
||||
)
|
||||
|
||||
const (
|
||||
functionTypeGet = "get" //GetByField return single model
|
||||
functionTypeFind = "find" // findByField return many model
|
||||
functionTypeSet = "set" // SetField only set specified field
|
||||
|
||||
TagOperate = "o" //字段函数的tag
|
||||
TagComment = "c" //字段注释的tag
|
||||
)
|
||||
|
||||
type (
|
||||
FunctionDesc struct {
|
||||
Type string // get,find,set
|
||||
FieldName string // 字段名字 eg:Age
|
||||
FieldType string // 字段类型 eg: string,int64 等
|
||||
}
|
||||
)
|
||||
|
||||
func GenMongoModel(goFilePath string, needCache bool) error {
|
||||
structs, imports, err := utils.ParseGoFile(goFilePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(structs) != 1 {
|
||||
return fmt.Errorf("only 1 struct should be provided")
|
||||
}
|
||||
structStr, err := genStructs(structs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fp, err := util.ClearAndOpenFile(goFilePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer fp.Close()
|
||||
|
||||
var myTemplate string
|
||||
if needCache {
|
||||
myTemplate = cacheTemplate
|
||||
} else {
|
||||
myTemplate = noCacheTemplate
|
||||
}
|
||||
structName := getStructName(structs)
|
||||
functionList := getFunctionList(structs)
|
||||
|
||||
for _, fun := range functionList {
|
||||
funTmp := genMethodTemplate(fun, needCache)
|
||||
if funTmp == "" {
|
||||
continue
|
||||
}
|
||||
myTemplate += "\n"
|
||||
myTemplate += funTmp
|
||||
myTemplate += "\n"
|
||||
}
|
||||
|
||||
t := template.Must(template.New("mongoTemplate").Parse(myTemplate))
|
||||
return t.Execute(fp, map[string]string{
|
||||
"modelName": structName,
|
||||
"importArray": getImports(imports, needCache),
|
||||
"modelFields": structStr,
|
||||
})
|
||||
}
|
||||
|
||||
func getFunctionList(structs []utils.Struct) []FunctionDesc {
|
||||
var list []FunctionDesc
|
||||
for _, field := range structs[0].Fields {
|
||||
tagMap := parseTag(field.Tag)
|
||||
if fun, ok := tagMap[TagOperate]; ok {
|
||||
funList := strings.Split(fun, ",")
|
||||
for _, o := range funList {
|
||||
var f FunctionDesc
|
||||
f.FieldType = field.Type
|
||||
f.FieldName = field.Name
|
||||
f.Type = o
|
||||
list = append(list, f)
|
||||
}
|
||||
}
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
func getStructName(structs []utils.Struct) string {
|
||||
for _, structItem := range structs {
|
||||
return structItem.Name
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func genStructs(structs []utils.Struct) (string, error) {
|
||||
if len(structs) > 1 {
|
||||
return "", fmt.Errorf("input .go file must only one struct")
|
||||
}
|
||||
|
||||
modelFields := `Id bson.ObjectId ` + quotationMark + `bson:"_id" json:"id,omitempty"` + quotationMark + "\n\t"
|
||||
for _, structItem := range structs {
|
||||
for _, field := range structItem.Fields {
|
||||
modelFields += getFieldLine(field)
|
||||
}
|
||||
}
|
||||
modelFields += "\t" + `CreateTime time.Time ` + quotationMark + `json:"createTime,omitempty" bson:"createTime"` + quotationMark + "\n\t"
|
||||
modelFields += "\t" + `UpdateTime time.Time ` + quotationMark + `json:"updateTime,omitempty" bson:"updateTime"` + quotationMark
|
||||
return modelFields, nil
|
||||
}
|
||||
|
||||
func getFieldLine(member spec.Member) string {
|
||||
if member.Name == "CreateTime" || member.Name == "UpdateTime" || member.Name == "Id" {
|
||||
return ""
|
||||
}
|
||||
jsonName := utils.UpperCamelToLower(member.Name)
|
||||
result := "\t" + member.Name + ` ` + member.Type + ` ` + quotationMark + `json:"` + jsonName + `,omitempty"` + ` bson:"` + jsonName + `"` + quotationMark
|
||||
tagMap := parseTag(member.Tag)
|
||||
if comment, ok := tagMap[TagComment]; ok {
|
||||
result += ` //` + comment + "\n\t"
|
||||
} else {
|
||||
result += "\n\t"
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// tag like `o:"find,get,update" c:"姓名"`
|
||||
func parseTag(tag string) map[string]string {
|
||||
var result = make(map[string]string, 0)
|
||||
tags := strings.Split(tag, " ")
|
||||
for _, kv := range tags {
|
||||
temp := strings.Split(kv, ":")
|
||||
if len(temp) > 1 {
|
||||
key := strings.ReplaceAll(strings.ReplaceAll(temp[0], "\"", ""), "`", "")
|
||||
value := strings.ReplaceAll(strings.ReplaceAll(temp[1], "\"", ""), "`", "")
|
||||
result[key] = value
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func getImports(imports []string, needCache bool) string {
|
||||
|
||||
importStr := strings.Join(imports, "\n\t")
|
||||
importStr += "\"errors\"\n\t"
|
||||
importStr += "\"time\"\n\t"
|
||||
importStr += "\n\t\"zero/core/stores/cache\"\n\t"
|
||||
importStr += "\"zero/core/stores/mongoc\"\n\t"
|
||||
importStr += "\n\t\"github.com/globalsign/mgo/bson\""
|
||||
return importStr
|
||||
}
|
||||
67
tools/goctl/model/mongomodel/gen/genmongomodelbynetwork.go
Normal file
67
tools/goctl/model/mongomodel/gen/genmongomodelbynetwork.go
Normal file
@@ -0,0 +1,67 @@
|
||||
package gen
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"zero/tools/goctl/model/mongomodel/utils"
|
||||
)
|
||||
|
||||
func GenMongoModelByNetwork(input string, needCache bool) (string, error) {
|
||||
if strings.TrimSpace(input) == "" {
|
||||
return "", errors.New("struct不能为空")
|
||||
}
|
||||
if strings.Index(strings.TrimSpace(input), "type") != 0 {
|
||||
input = "type " + input
|
||||
}
|
||||
|
||||
if strings.Index(strings.TrimSpace(input), "package") != 0 {
|
||||
input = "package model\r\n" + input
|
||||
}
|
||||
|
||||
structs, imports, err := utils.ParseGoFileByNetwork(input)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if len(structs) != 1 {
|
||||
return "", fmt.Errorf("only 1 struct should be provided")
|
||||
}
|
||||
structStr, err := genStructs(structs)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var myTemplate string
|
||||
if needCache {
|
||||
myTemplate = cacheTemplate
|
||||
} else {
|
||||
myTemplate = noCacheTemplate
|
||||
}
|
||||
structName := getStructName(structs)
|
||||
functionList := getFunctionList(structs)
|
||||
|
||||
for _, fun := range functionList {
|
||||
funTmp := genMethodTemplate(fun, needCache)
|
||||
if funTmp == "" {
|
||||
continue
|
||||
}
|
||||
myTemplate += "\n"
|
||||
myTemplate += funTmp
|
||||
myTemplate += "\n"
|
||||
}
|
||||
|
||||
t := template.Must(template.New("mongoTemplate").Parse(myTemplate))
|
||||
var result bytes.Buffer
|
||||
err = t.Execute(&result, map[string]string{
|
||||
"modelName": structName,
|
||||
"importArray": getImports(imports, needCache),
|
||||
"modelFields": structStr,
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return result.String(), nil
|
||||
}
|
||||
238
tools/goctl/model/mongomodel/gen/templatemodel.go
Normal file
238
tools/goctl/model/mongomodel/gen/templatemodel.go
Normal file
@@ -0,0 +1,238 @@
|
||||
package gen
|
||||
|
||||
const (
|
||||
quotationMark = "`"
|
||||
//templates that do not use caching
|
||||
noCacheTemplate = `package model
|
||||
|
||||
import (
|
||||
{{.importArray}}
|
||||
)
|
||||
|
||||
var ErrNotFound = mongoc.ErrNotFound
|
||||
|
||||
type (
|
||||
{{.modelName}}Model struct {
|
||||
*mongoc.Model
|
||||
}
|
||||
|
||||
{{.modelName}} struct {
|
||||
{{.modelFields}}
|
||||
}
|
||||
)
|
||||
|
||||
func New{{.modelName}}Model(url, database, collection string, c cache.CacheConf, opts ...cache.Option) *{{.modelName}}Model {
|
||||
return &{{.modelName}}Model{mongoc.MustNewModel(url, database, collection, c, opts...)}
|
||||
}
|
||||
|
||||
func (m *{{.modelName}}Model) FindOne(id string) (*{{.modelName}}, error) {
|
||||
session, err := m.Model.TakeSession()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer m.Model.PutSession(session)
|
||||
|
||||
var result {{.modelName}}
|
||||
err = m.GetCollection(session).FindOneIdNoCache(&result,bson.ObjectIdHex(id))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
func (m *{{.modelName}}Model) Delete(id string) error {
|
||||
session, err := m.TakeSession()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer m.PutSession(session)
|
||||
return m.GetCollection(session).RemoveIdNoCache(bson.ObjectIdHex(id))
|
||||
}
|
||||
|
||||
func (m *{{.modelName}}Model) Insert(data *{{.modelName}}) error {
|
||||
session, err := m.TakeSession()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer m.PutSession(session)
|
||||
|
||||
return m.GetCollection(session).Insert(data)
|
||||
}
|
||||
|
||||
func (m *{{.modelName}}Model) Update(data *{{.modelName}}) error {
|
||||
session, err := m.TakeSession()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer m.PutSession(session)
|
||||
|
||||
data.UpdateTime = time.Now()
|
||||
return m.GetCollection(session).UpdateIdNoCache(data.Id, data)
|
||||
}
|
||||
`
|
||||
|
||||
//use cache template
|
||||
cacheTemplate = `package model
|
||||
|
||||
import (
|
||||
{{.importArray}}
|
||||
)
|
||||
|
||||
var ErrNotFound = errors.New("not found")
|
||||
|
||||
const (
|
||||
Prefix{{.modelName}}CacheKey = "#{{.modelName}}#cache" //todo please modify this prefix
|
||||
)
|
||||
|
||||
type (
|
||||
{{.modelName}}Model struct {
|
||||
*mongoc.Model
|
||||
}
|
||||
|
||||
{{.modelName}} struct {
|
||||
{{.modelFields}}
|
||||
}
|
||||
)
|
||||
|
||||
func New{{.modelName}}Model(url, database, collection string, c cache.CacheConf, opts ...cache.Option) *{{.modelName}}Model {
|
||||
return &{{.modelName}}Model{mongoc.MustNewModel(url, database, collection, c, opts...)}
|
||||
}
|
||||
|
||||
func (m *{{.modelName}}Model) FindOne(id string) (*{{.modelName}}, error) {
|
||||
key := Prefix{{.modelName}}CacheKey + id
|
||||
session, err := m.Model.TakeSession()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer m.Model.PutSession(session)
|
||||
|
||||
var result {{.modelName}}
|
||||
err = m.GetCollection(session).FindOneId(&result, key, bson.ObjectIdHex(id))
|
||||
switch err {
|
||||
case nil:
|
||||
return &result, nil
|
||||
case mongoc.ErrNotFound:
|
||||
return nil, ErrNotFound
|
||||
default:
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
func (m *{{.modelName}}Model) Delete(id string) error {
|
||||
session, err := m.TakeSession()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer m.PutSession(session)
|
||||
|
||||
key := Prefix{{.modelName}}CacheKey + id
|
||||
return m.GetCollection(session).RemoveId(bson.ObjectIdHex(id), key)
|
||||
}
|
||||
|
||||
func (m *{{.modelName}}Model) Insert(data *{{.modelName}}) error {
|
||||
session, err := m.TakeSession()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer m.PutSession(session)
|
||||
|
||||
return m.GetCollection(session).Insert(data)
|
||||
}
|
||||
|
||||
func (m *{{.modelName}}Model) Update(data *{{.modelName}}) error {
|
||||
session, err := m.TakeSession()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer m.PutSession(session)
|
||||
|
||||
data.UpdateTime = time.Now()
|
||||
key := Prefix{{.modelName}}CacheKey + data.Id.Hex()
|
||||
return m.GetCollection(session).UpdateId(data.Id, data, key)
|
||||
}
|
||||
`
|
||||
cacheSetFieldtemplate = `func (m *{{.modelName}}Model) Set{{.Name}}(id string, {{.name}} {{.type}}) error {
|
||||
_, err := m.cache.Del(Prefix{{.modelName}}CacheKey + id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
session, err := m.TakeSession()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer m.PutSession(session)
|
||||
|
||||
update := bson.M{"$set": bson.M{"{{.name}}": {{.name}}, "updateTime": time.Now()}}
|
||||
return m.GetCollection(session).UpdateId(bson.ObjectIdHex(id), update)
|
||||
}`
|
||||
|
||||
noCacheSetFieldtemplate = `func (m *{{.modelName}}Model) Set{{.Name}}(id string, {{.name}} {{.type}}) error {
|
||||
session, err := m.TakeSession()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer m.PutSession(session)
|
||||
|
||||
update := bson.M{"$set": bson.M{"{{.name}}": {{.name}}, "updateTime": time.Now()}}
|
||||
return m.GetCollection(session).UpdateId(bson.ObjectIdHex(id), update)
|
||||
}`
|
||||
|
||||
noCacheGetTemplate = `func (m *{{.modelName}}Model) GetBy{{.Name}}({{.name}} {{.type}}) (*{{.modelName}},error) {
|
||||
session, err := m.TakeSession()
|
||||
if err != nil {
|
||||
return nil,err
|
||||
}
|
||||
defer m.PutSession(session)
|
||||
var result {{.modelName}}
|
||||
query := bson.M{"{{.name}}":{{.name}}}
|
||||
err = m.GetCollection(session).FindOneNoCache(&result, query)
|
||||
if err != nil {
|
||||
if err == mgo.ErrNotFound {
|
||||
return nil,ErrNotFound
|
||||
}
|
||||
return nil,err
|
||||
}
|
||||
return &result,nil
|
||||
}`
|
||||
// GetByField return single model
|
||||
getTemplate = `func (m *{{.modelName}}Model) GetBy{{.Name}}({{.name}} {{.type}}) (*{{.modelName}},error) {
|
||||
session, err := m.TakeSession()
|
||||
if err != nil {
|
||||
return nil,err
|
||||
}
|
||||
defer m.PutSession(session)
|
||||
var result {{.modelName}}
|
||||
query := bson.M{"{{.name}}":{{.name}}}
|
||||
key := getCachePrimaryKeyBy{{.Name}}({{.name}})
|
||||
err = m.GetCollection(session).FindOne(&result,key,query)
|
||||
if err != nil {
|
||||
if err == mgo.ErrNotFound {
|
||||
return nil,ErrNotFound
|
||||
}
|
||||
return nil,err
|
||||
}
|
||||
return &result,nil
|
||||
}
|
||||
|
||||
func getCachePrimaryKeyBy{{.Name}}({{.name}} {{.type}}) string {
|
||||
return "" //todo 请补全这里
|
||||
}
|
||||
`
|
||||
|
||||
findTemplate = `func (m *{{.modelName}}Model) FindBy{{.Name}}({{.name}} string) ([]{{.modelName}},error) {
|
||||
session, err := m.TakeSession()
|
||||
if err != nil {
|
||||
return nil,err
|
||||
}
|
||||
defer m.PutSession(session)
|
||||
|
||||
query := bson.M{"{{.name}}":{{.name}}}
|
||||
var result []{{.modelName}}
|
||||
err = m.GetCollection(session).FindAllNoCache(&result,query)
|
||||
if err != nil {
|
||||
return nil,err
|
||||
}
|
||||
return result,nil
|
||||
}`
|
||||
)
|
||||
30
tools/goctl/model/mongomodel/genmongocmd.go
Normal file
30
tools/goctl/model/mongomodel/genmongocmd.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package mongomodel
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"zero/core/lang"
|
||||
"zero/tools/goctl/model/mongomodel/gen"
|
||||
|
||||
"github.com/logrusorgru/aurora"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
func ModelCommond(c *cli.Context) error {
|
||||
src := c.String("src")
|
||||
cache := c.String("cache")
|
||||
|
||||
if len(src) == 0 {
|
||||
return errors.New("missing -src")
|
||||
}
|
||||
var needCache bool
|
||||
if cache == "yes" {
|
||||
needCache = true
|
||||
}
|
||||
|
||||
lang.Must(gen.GenMongoModel(src, needCache))
|
||||
|
||||
fmt.Println(aurora.Green("Done."))
|
||||
return nil
|
||||
}
|
||||
167
tools/goctl/model/mongomodel/utils/parsesimplegofile.go
Normal file
167
tools/goctl/model/mongomodel/utils/parsesimplegofile.go
Normal file
@@ -0,0 +1,167 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
|
||||
"zero/tools/goctl/api/spec"
|
||||
)
|
||||
|
||||
const (
|
||||
StructArr = "struct"
|
||||
ImportArr = "import"
|
||||
Unknown = "unknown"
|
||||
)
|
||||
|
||||
type Struct struct {
|
||||
Name string
|
||||
Fields []spec.Member
|
||||
}
|
||||
|
||||
func readFile(filePath string) (string, error) {
|
||||
b, err := ioutil.ReadFile(filePath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(b), nil
|
||||
}
|
||||
|
||||
func ParseNetworkGoFile(io string) ([]Struct, []string, error) {
|
||||
fset := token.NewFileSet() // 位置是相对于节点
|
||||
|
||||
f, err := parser.ParseFile(fset, "", io, 0)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return parse(f)
|
||||
}
|
||||
|
||||
func ParseGoFile(pathOrStr string) ([]Struct, []string, error) {
|
||||
var goFileStr string
|
||||
var err error
|
||||
goFileStr, err = readFile(pathOrStr)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
fset := token.NewFileSet() // 位置是相对于节点
|
||||
|
||||
f, err := parser.ParseFile(fset, "", goFileStr, 0)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return parse(f)
|
||||
}
|
||||
|
||||
func ParseGoFileByNetwork(io string) ([]Struct, []string, error) {
|
||||
fset := token.NewFileSet() // 位置是相对于节点
|
||||
|
||||
f, err := parser.ParseFile(fset, "", io, 0)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return parse(f)
|
||||
}
|
||||
|
||||
//使用ast包解析golang文件
|
||||
func parse(f *ast.File) ([]Struct, []string, error) {
|
||||
if len(f.Decls) == 0 {
|
||||
return nil, nil, fmt.Errorf("you should provide as least 1 struct")
|
||||
}
|
||||
var structList []Struct
|
||||
var importList []string
|
||||
for _, decl := range f.Decls {
|
||||
structs, imports, err := getStructAndImportInfo(decl)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
structList = append(structList, structs...)
|
||||
importList = append(importList, imports...)
|
||||
}
|
||||
return structList, importList, nil
|
||||
}
|
||||
|
||||
func getStructAndImportInfo(decl ast.Decl) (structs []Struct, imports []string, err error) {
|
||||
var structList []Struct
|
||||
var importList []string
|
||||
genDecl, ok := decl.(*ast.GenDecl)
|
||||
if !ok {
|
||||
return nil, nil, fmt.Errorf("please input right file")
|
||||
}
|
||||
for _, tpyObj := range genDecl.Specs {
|
||||
switch tpyObj.(type) {
|
||||
case *ast.ImportSpec: // import
|
||||
importSpec := tpyObj.(*ast.ImportSpec)
|
||||
s := importSpec.Path.Value
|
||||
importList = append(importList, s)
|
||||
case *ast.TypeSpec: //type
|
||||
typeSpec := tpyObj.(*ast.TypeSpec)
|
||||
switch typeSpec.Type.(type) {
|
||||
case *ast.StructType: // struct
|
||||
struct1, err := parseStruct(typeSpec)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
structList = append(structList, *struct1)
|
||||
}
|
||||
default:
|
||||
|
||||
}
|
||||
}
|
||||
return structList, importList, nil
|
||||
}
|
||||
|
||||
func parseStruct(typeSpec *ast.TypeSpec) (*Struct, error) {
|
||||
var result Struct
|
||||
structName := typeSpec.Name.Name
|
||||
result.Name = structName
|
||||
structType, ok := typeSpec.Type.(*ast.StructType)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("not struct")
|
||||
}
|
||||
|
||||
for _, item := range structType.Fields.List {
|
||||
var member spec.Member
|
||||
var err error
|
||||
member.Name = parseFiledName(item.Names)
|
||||
member.Type, err = parseFiledType(item.Type)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if item.Tag != nil {
|
||||
member.Tag = item.Tag.Value
|
||||
}
|
||||
result.Fields = append(result.Fields, member)
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
func parseFiledType(expr ast.Expr) (string, error) {
|
||||
switch expr.(type) {
|
||||
case *ast.Ident:
|
||||
return expr.(*ast.Ident).Name, nil
|
||||
case *ast.SelectorExpr:
|
||||
selectorExpr := expr.(*ast.SelectorExpr)
|
||||
return selectorExpr.X.(*ast.Ident).Name + "." + selectorExpr.Sel.Name, nil
|
||||
default:
|
||||
return "", fmt.Errorf("can't parse type")
|
||||
}
|
||||
}
|
||||
|
||||
func parseFiledName(idents []*ast.Ident) string {
|
||||
for _, name := range idents {
|
||||
return name.Name
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func UpperCamelToLower(name string) string {
|
||||
if len(name) == 0 {
|
||||
return ""
|
||||
}
|
||||
return strings.ToLower(name[:1]) + name[1:]
|
||||
}
|
||||
430
tools/goctl/model/sql/README.MD
Normal file
430
tools/goctl/model/sql/README.MD
Normal file
@@ -0,0 +1,430 @@
|
||||
<div style="text-align: center;"><h1>Sql生成工具说明文档</h1></div>
|
||||
|
||||
<h2>前言</h2>
|
||||
在当前Sql代码生成工具是基于sqlc生成的逻辑。
|
||||
|
||||
<h2>关键字</h2>
|
||||
|
||||
+ 查询类型(前暂不支持同一字段多种类型混合生成,如按照campus_id查询单结果又查询All或者Limit)
|
||||
- 单结果查询
|
||||
- FindOne(主键特有)
|
||||
- FindOneByXxx
|
||||
- 多结果查询
|
||||
- FindAllByXxx
|
||||
- FindLimitByXxx
|
||||
- withCache
|
||||
- withoutCache
|
||||
|
||||
<h2>准备工作</h2>
|
||||
|
||||
- table
|
||||
|
||||
```
|
||||
CREATE TABLE `user_info` (
|
||||
`id` bigint(20) NOT NULL COMMENT '主键',
|
||||
`campus_id` bigint(20) DEFAULT NULL COMMENT '整校id',
|
||||
`name` varchar(255) DEFAULT NULL COMMENT '用户姓名',
|
||||
`id_number` varchar(255) DEFAULT NULL COMMENT '身份证',
|
||||
`age` int(10) DEFAULT NULL COMMENT '年龄',
|
||||
`gender` tinyint(1) DEFAULT NULL COMMENT '性别,0-男,1-女,2-不限',
|
||||
`mobile` varchar(20) DEFAULT NULL COMMENT '手机号',
|
||||
`create_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`update_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
```
|
||||
|
||||
<h2>imports生成</h2>
|
||||
imports代码生成对应model中包的引入管理,仅使用于晓黑板项目中(非相对路径动态生成),目前受`withCache`参数的影响,除此之外其实为固定代码。
|
||||
|
||||
- withCache
|
||||
|
||||
```
|
||||
import (
|
||||
"database/sql""fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"zero/core/stores/sqlc"
|
||||
"zero/core/stores/sqlx"
|
||||
"zero/core/stringx"
|
||||
"xiao/service/shared/builderx"
|
||||
)
|
||||
```
|
||||
|
||||
- withoutCache
|
||||
|
||||
```
|
||||
import (
|
||||
"database/sql""fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"zero/core/stores/sqlx"
|
||||
"zero/core/stringx"
|
||||
"xiao/service/shared/builderx"
|
||||
)
|
||||
```
|
||||
|
||||
<h2>vars生成</h2>
|
||||
|
||||
vars部分对应model中var声明的包含的代码块,由`table`名和`withCache`来决定其中的代码生成内容,`withCache`决定是否要生成缓存key变量的声明。
|
||||
|
||||
- withCache
|
||||
|
||||
```
|
||||
var (
|
||||
UserInfoFieldNames = builderx.FieldNames(&UserInfo{})
|
||||
UserInfoRows = strings.Join(UserInfoFieldNames, ",")
|
||||
UserInfoRowsExpectAutoSet = strings.Join(stringx.Remove(UserInfoFieldNames, "id", "create_time", "update_time"), ",")
|
||||
UserInfoRowsWithPlaceHolder = strings.Join(stringx.Remove(UserInfoFieldNames, "id", "create_time", "update_time"), "=?,") + "=?"
|
||||
|
||||
cacheUserInfoIdPrefix = "cache#userInfo#id#"
|
||||
cacheUserInfoCampusIdPrefix = "cache#userInfo#campusId#"
|
||||
cacheUserInfoNamePrefix = "cache#userInfo#name#"
|
||||
cacheUserInfoMobilePrefix = "cache#userInfo#mobile#"
|
||||
)
|
||||
```
|
||||
|
||||
- withoutCache
|
||||
|
||||
```
|
||||
var (
|
||||
UserInfoFieldNames = builderx.FieldNames(&UserInfo{})
|
||||
UserInfoRows = strings.Join(UserInfoFieldNames, ",")
|
||||
UserInfoRowsExpectAutoSet = strings.Join(stringx.Remove(UserInfoFieldNames, "id", "create_time", "update_time"), ",")
|
||||
UserInfoRowsWithPlaceHolder = strings.Join(stringx.Remove(UserInfoFieldNames, "id", "create_time", "update_time"), "=?,") + "=?"
|
||||
)
|
||||
```
|
||||
|
||||
<h2>types生成</h2>
|
||||
|
||||
ypes部分对应model中type声明的包含的代码块,由`table`名和`withCache`来决定其中的代码生成内容,`withCache`决定引入sqlc还是sqlx。
|
||||
|
||||
- withCache
|
||||
```
|
||||
type (
|
||||
UserInfoModel struct {
|
||||
conn sqlc.CachedConn
|
||||
table string
|
||||
}
|
||||
|
||||
UserInfo struct {
|
||||
Id int64 `db:"id"` // 主键id
|
||||
CampusId int64 `db:"campus_id"` // 整校id
|
||||
Name string `db:"name"` // 用户姓名
|
||||
IdNumber string `db:"id_number"` // 身份证
|
||||
Age int64 `db:"age"` // 年龄
|
||||
Gender int64 `db:"gender"` // 性别,0-男,1-女,2-不限
|
||||
Mobile string `db:"mobile"` // 手机号
|
||||
CreateTime time.Time `db:"create_time"` // 创建时间
|
||||
UpdateTime time.Time `db:"update_time"` // 更新时间
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
- withoutCache
|
||||
```
|
||||
type (
|
||||
UserInfoModel struct {
|
||||
conn sqlx.SqlConn
|
||||
table string
|
||||
}
|
||||
|
||||
UserInfo struct {
|
||||
Id int64 `db:"id"` // 主键id
|
||||
CampusId int64 `db:"campus_id"` // 整校id
|
||||
Name string `db:"name"` // 用户姓名
|
||||
IdNumber string `db:"id_number"` // 身份证
|
||||
Age int64 `db:"age"` // 年龄
|
||||
Gender int64 `db:"gender"` // 性别,0-男,1-女,2-不限
|
||||
Mobile string `db:"mobile"` // 手机号
|
||||
CreateTime time.Time `db:"create_time"` // 创建时间
|
||||
UpdateTime time.Time `db:"update_time"` // 更新时间
|
||||
}
|
||||
)
|
||||
```
|
||||
<h2>New生成</h2>
|
||||
new生成对应model中struct的New函数,受`withCache`影响决定是否要引入cacheRedis
|
||||
|
||||
- withCache
|
||||
```
|
||||
func NewUserInfoModel(conn sqlx.SqlConn, c cache.CacheConf, table string) *UserInfoModel {
|
||||
return &UserInfoModel{
|
||||
CachedConn: sqlc.NewConn(conn, c),
|
||||
table: table,
|
||||
}
|
||||
}
|
||||
```
|
||||
- withoutCache
|
||||
```
|
||||
func NewUserInfoModel(conn sqlx.SqlConn, table string) *UserInfoModel {
|
||||
return &UserInfoModel{conn: conn, table: table}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
<h2>FindOne查询生成</h2>
|
||||
FindOne查询代码生成仅对主键有效。如`user_info`中生成的FindOne如下:
|
||||
|
||||
- withCache
|
||||
|
||||
```
|
||||
func (m *UserInfoModel) FindOne(id int64) (*UserInfo, error) {
|
||||
idKey := fmt.Sprintf("%s%v", cacheUserInfoIdPrefix, id)
|
||||
var resp UserInfo
|
||||
err := m.QueryRow(&resp, idKey, func(conn sqlx.SqlConn, v interface{}) error {
|
||||
query := `select ` + userInfoRows + ` from ` + m.table + `where id = ? limit 1`
|
||||
return conn.QueryRow(v, query, id)
|
||||
})
|
||||
switch err {
|
||||
case nil:
|
||||
return &resp, nil
|
||||
case sqlc.ErrNotFound:
|
||||
return nil, ErrNotFound
|
||||
default:
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- withoutCache
|
||||
|
||||
```
|
||||
func (m *UserInfoModel) FindOne(id int64) (*UserInfo, error) {
|
||||
|
||||
query := `select ` + userInfoRows + ` from ` + m.table + `where id = ? limit 1`
|
||||
var resp UserInfo
|
||||
err := m.conn.QueryRow(&resp, query, id)
|
||||
switch err {
|
||||
case nil:
|
||||
return &resp, nil
|
||||
case sqlx.ErrNotFound:
|
||||
return nil, ErrNotFound
|
||||
default:
|
||||
return nil, err
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
<h2>FindOneByXxx查询生成</h2>
|
||||
|
||||
FindOneByXxx查询生成可以按照单个字段查询、多个字段以AND关系且表达式符号为`=`的查询(下称:组合查询),对除主键之外的字段有效,对于单个字段可以用`withCache`来控制是否需要缓存,这里的缓存只缓存主键,并不缓存整个struct,注意:这里有一个隐藏的规则,如果单个字段查询需要cache,那么主键一定有cache;多个字段组成的`组合查询`一律没有缓存处理,<strong><i>且组合查询不能相互嵌套</i></strong>,否则会报`circle query with other fields`错误,下面我们按场景来依次查看对应代码生成后的示例。
|
||||
|
||||
>注:目前暂不支持除equals之外的条件查询。
|
||||
|
||||
+ 单字段查询
|
||||
以name查询为例
|
||||
- withCache
|
||||
```
|
||||
func (m *UserInfoModel) FindOneByName(name string) (*UserInfo, error) {
|
||||
nameKey := fmt.Sprintf("%s%v", cacheUserInfoNamePrefix, name)
|
||||
var id string
|
||||
err := m.GetCache(key, &id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if id != "" {
|
||||
return m.FindOne(id)
|
||||
}
|
||||
var resp UserInfo
|
||||
query := `select ` + userInfoRows + ` from ` + m.table + `where name = ? limit 1`
|
||||
err = m.QueryRowNoCache(&resp, query, name)
|
||||
switch err {
|
||||
case nil:
|
||||
err = m.SetCache(nameKey, resp.Id)
|
||||
if err != nil {
|
||||
logx.Error(err)
|
||||
}
|
||||
return &resp, nil
|
||||
case sqlc.ErrNotFound:
|
||||
return nil, ErrNotFound
|
||||
default:
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
```
|
||||
- withoutCache
|
||||
|
||||
```
|
||||
func (m *UserInfoModel) FindOneByName(name string) (*UserInfo, error) {
|
||||
var resp UserInfo
|
||||
query := `select ` + userInfoRows + ` from ` + m.table + `where name = ? limit 1`
|
||||
err = m.conn.QueryRow(&resp, query, name)
|
||||
switch err {
|
||||
case nil:
|
||||
return &resp, nil
|
||||
case sqlx.ErrNotFound:
|
||||
return nil, ErrNotFound
|
||||
default:
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- 组合查询
|
||||
以`campus_id`和`id_number`查询为例。
|
||||
|
||||
```
|
||||
func (m *UserInfoModel) FindOneByCampusIdAndIdNumber(campusId int64,idNumber string) (*UserInfo, error) {
|
||||
var resp UserInfo
|
||||
query := `select ` + userInfoRows + ` from ` + m.table + `where campus_id = ? AND id_number = ? limit 1`
|
||||
err = m.QueryRowNoCache(&resp, query, campusId, idNumber)
|
||||
// err = m.conn.QueryRows(&resp, query, campusId, idNumber)
|
||||
switch err {
|
||||
case nil:
|
||||
return &resp, nil
|
||||
case sqlx.ErrNotFound:
|
||||
return nil, ErrNotFound
|
||||
default:
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
```
|
||||
<h2>FindAllByXxx生成</h2>
|
||||
FindAllByXxx查询和FindOneByXxx功能相似,只是FindOneByXxx限制了limit等于1,而FindAllByXxx是查询所有,以两个例子来说明
|
||||
|
||||
- 查询单个字段`name`等于某值的所有数据
|
||||
```
|
||||
func (m *UserInfoModel) FindAllByName(name string) ([]*UserInfo, error) {
|
||||
var resp []*UserInfo
|
||||
query := `select ` + userInfoRows + ` from ` + m.table + `where name = ?`
|
||||
err := m.QueryRowsNoCache(&resp, query, name)
|
||||
// err := m.conn.QueryRows(&resp, query, name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
```
|
||||
- 查询多个组合字段`campus_id`等于某值且`gender`等于某值的所有数据
|
||||
```
|
||||
func (m *UserInfoModel) FindAllByCampusIdAndGender(campusId int64,gender int64) ([]*UserInfo, error) {
|
||||
var resp []*UserInfo
|
||||
query := `select ` + userInfoRows + ` from ` + m.table + `where campus_id = ? AND gender = ?`
|
||||
err := m.QueryRowsNoCache(&resp, query, campusId, gender)
|
||||
// err := m.conn.QueryRows(&resp, query, campusId, gender)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
```
|
||||
|
||||
<h2>FindLimitByXxx生成</h2>
|
||||
FindLimitByXxx查询和FindAllByXxx功能相似,只是FindAllByXxx限制了limit,除此之外还会生成查询对应Count总数的代码,而FindAllByXxx是查询所有数据,以几个例子来说明
|
||||
|
||||
- 查询`gender`等于某值的分页数据,按照`create_time`降序
|
||||
```
|
||||
func (m *UserInfoModel) FindLimitByGender(gender int64, page, limit int) ([]*UserInfo, error) {
|
||||
var resp []*UserInfo
|
||||
query := `select ` + userInfoRows + `from ` + m.table + `where gender = ? order by create_time DESC limit ?,?`
|
||||
err := m.QueryRowsNoCache(&resp, query, gender, (page-1)*limit, limit)
|
||||
// err := m.conn.QueryRows(&resp, query, gender, (page-1)*limit, limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (m *UserInfoModel) FindAllCountByGender(gender int64) (int64, error) {
|
||||
var count int64
|
||||
query := `select count(1) from ` + m.table + `where gender = ? `
|
||||
err := m.QueryRowsNoCache(&count, query, gender)
|
||||
// err := m.conn.QueryRow(&count, query, gender)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return count, nil
|
||||
}
|
||||
```
|
||||
- 查询`gender`等于某值的分页数据,按照`create_time`降序、`update_time`生序排序
|
||||
```
|
||||
func (m *UserInfoModel) FindLimitByGender(gender int64, page, limit int) ([]*UserInfo, error) {
|
||||
var resp []*UserInfo
|
||||
query := `select ` + userInfoRows + `from ` + m.table + `where gender = ? order by create_time DESC,update_time ASC limit ?,?`
|
||||
err := m.QueryRowsNoCache(&resp, query, gender, (page-1)*limit, limit)
|
||||
// err := m.conn.QueryRows(&resp, query, gender, (page-1)*limit, limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (m *UserInfoModel) FindAllCountByGender(gender int64) (int64, error) {
|
||||
var count int64
|
||||
query := `select count(1) from ` + m.table + `where gender = ? `
|
||||
err := m.QueryRowNoCache(&count, query, gender)
|
||||
// err := m.conn.QueryRow(&count, query, gender)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return count, nil
|
||||
}
|
||||
```
|
||||
- 查询`gender`等于某值且`campus_id`为某值按照`create_time`降序的分页数据
|
||||
```
|
||||
func (m *UserInfoModel) FindLimitByGenderAndCampusId(gender int64,campusId int64, page, limit int) ([]*UserInfo, error) {
|
||||
var resp []*UserInfo
|
||||
query := `select ` + userInfoRows + `from ` + m.table + `where gender = ? AND campus_id = ? order by create_time DESC limit ?,?`
|
||||
err := m.QueryRowsNoCache(&resp, query, gender, campusId, (page-1)*limit, limit)
|
||||
// err := m.conn.QueryRows(&resp, query, gender, campusId, (page-1)*limit, limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (m *UserInfoModel) FindAllCountByGenderAndCampusId(gender int64,campusId int64) (int64, error) {
|
||||
var count int64
|
||||
query := `select count(1) from ` + m.table + `where gender = ? AND campus_id = ? `
|
||||
err := m.QueryRowsNoCache(&count, query, gender, campusId)
|
||||
// err := m.conn.QueryRow(&count, query, gender, campusId)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return count, nil
|
||||
}
|
||||
```
|
||||
|
||||
<h2>Delete生成</h2>
|
||||
Delete代码根据`withCache`的不同可以生成带缓存逻辑代码和不带缓存逻辑代码,<strong><i>Delete代码生成仅按照主键删除</i></strong>。从FindOneByXxx方法描述得知,非主键`withCache`了那么主键会强制被cache,因此在delete时也会删除主键cache。
|
||||
|
||||
- withCache
|
||||
根据`mobile`查询用户信息
|
||||
|
||||
```
|
||||
func (m *UserInfoModel) Delete(userId int64) error {
|
||||
userIdKey := fmt.Sprintf("%s%v", cacheUserInfoUserIdPrefix, userId)
|
||||
mobileKey := fmt.Sprintf("%s%v", cacheUserInfoMobilePrefix, mobile)
|
||||
_, err := m.Exec(func(conn sqlx.SqlConn) (result sql.Result, err error) {
|
||||
query := `delete from ` + m.table + + `where user_id = ?`
|
||||
return conn.Exec(query, userId)
|
||||
}, userIdKey, mobileKey)
|
||||
return err
|
||||
}
|
||||
```
|
||||
- withoutCache
|
||||
```
|
||||
func (m *UserInfoModel) Delete(userId int64) error {
|
||||
_, err := m.Exec(func(conn sqlx.SqlConn) (result sql.Result, err error) {
|
||||
query := `delete from ` + m.table + + `where user_id = ?`
|
||||
return conn.Exec(query, userId)
|
||||
}, )
|
||||
return err
|
||||
}
|
||||
```
|
||||
<h2>Insert生成</h2>
|
||||
|
||||
<h2>Update生成</h2>
|
||||
|
||||
<h2>待完善(TODO)</h2>
|
||||
|
||||
- 同一字段多种查询方式代码生成(优先级较高)
|
||||
- 条件查询
|
||||
- 范围查询
|
||||
- ...
|
||||
|
||||
<h2>反馈与建议</h2>
|
||||
|
||||
- 无
|
||||
108
tools/goctl/model/sql/gen/convert.go
Normal file
108
tools/goctl/model/sql/gen/convert.go
Normal file
@@ -0,0 +1,108 @@
|
||||
package gen
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"zero/tools/goctl/model/sql/util"
|
||||
)
|
||||
|
||||
func TableConvert(outerTable OuterTable) (*InnerTable, error) {
|
||||
var table InnerTable
|
||||
table.CreateNotFound = outerTable.CreateNotFound
|
||||
tableSnakeCase, tableUpperCamelCase, tableLowerCamelCase := util.FormatField(outerTable.Table)
|
||||
table.SnakeCase = tableSnakeCase
|
||||
table.UpperCamelCase = tableUpperCamelCase
|
||||
table.LowerCamelCase = tableLowerCamelCase
|
||||
fields := make([]*InnerField, 0)
|
||||
var primaryField *InnerField
|
||||
conflict := make(map[string]struct{})
|
||||
var containsCache bool
|
||||
for _, field := range outerTable.Fields {
|
||||
if field.Cache && !containsCache {
|
||||
containsCache = true
|
||||
}
|
||||
fieldSnakeCase, fieldUpperCamelCase, fieldLowerCamelCase := util.FormatField(field.Name)
|
||||
tag, err := genTag(fieldSnakeCase)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var comment string
|
||||
if field.Comment != "" {
|
||||
comment = fmt.Sprintf("// %s", field.Comment)
|
||||
}
|
||||
withFields := make([]InnerWithField, 0)
|
||||
unique := make([]string, 0)
|
||||
unique = append(unique, fmt.Sprintf("%v", field.QueryType))
|
||||
unique = append(unique, field.Name)
|
||||
|
||||
for _, item := range field.WithFields {
|
||||
unique = append(unique, item.Name)
|
||||
withFieldSnakeCase, withFieldUpperCamelCase, withFieldLowerCamelCase := util.FormatField(item.Name)
|
||||
withFields = append(withFields, InnerWithField{
|
||||
Case: Case{
|
||||
SnakeCase: withFieldSnakeCase,
|
||||
LowerCamelCase: withFieldLowerCamelCase,
|
||||
UpperCamelCase: withFieldUpperCamelCase,
|
||||
},
|
||||
DataType: commonMysqlDataTypeMap[item.DataBaseType],
|
||||
})
|
||||
}
|
||||
sort.Strings(unique)
|
||||
uniqueKey := strings.Join(unique, "#")
|
||||
if _, ok := conflict[uniqueKey]; ok {
|
||||
return nil, ErrCircleQuery
|
||||
} else {
|
||||
conflict[uniqueKey] = struct{}{}
|
||||
}
|
||||
sortFields := make([]InnerSort, 0)
|
||||
for _, sortField := range field.OuterSort {
|
||||
sortSnake, sortUpperCamelCase, sortLowerCamelCase := util.FormatField(sortField.Field)
|
||||
sortFields = append(sortFields, InnerSort{
|
||||
Field: Case{
|
||||
SnakeCase: sortSnake,
|
||||
LowerCamelCase: sortUpperCamelCase,
|
||||
UpperCamelCase: sortLowerCamelCase,
|
||||
},
|
||||
Asc: sortField.Asc,
|
||||
})
|
||||
}
|
||||
innerField := &InnerField{
|
||||
IsPrimaryKey: field.IsPrimaryKey,
|
||||
InnerWithField: InnerWithField{
|
||||
Case: Case{
|
||||
SnakeCase: fieldSnakeCase,
|
||||
LowerCamelCase: fieldLowerCamelCase,
|
||||
UpperCamelCase: fieldUpperCamelCase,
|
||||
},
|
||||
DataType: commonMysqlDataTypeMap[field.DataBaseType],
|
||||
},
|
||||
DataBaseType: field.DataBaseType,
|
||||
Tag: tag,
|
||||
Comment: comment,
|
||||
Cache: field.Cache,
|
||||
QueryType: field.QueryType,
|
||||
WithFields: withFields,
|
||||
Sort: sortFields,
|
||||
}
|
||||
if field.IsPrimaryKey {
|
||||
primaryField = innerField
|
||||
}
|
||||
fields = append(fields, innerField)
|
||||
}
|
||||
if primaryField == nil {
|
||||
return nil, errors.New("please ensure that primary exists")
|
||||
}
|
||||
table.ContainsCache = containsCache
|
||||
primaryField.Cache = containsCache
|
||||
table.PrimaryField = primaryField
|
||||
table.Fields = fields
|
||||
cacheKey, err := genCacheKeys(&table)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
table.CacheKey = cacheKey
|
||||
return &table, nil
|
||||
}
|
||||
51
tools/goctl/model/sql/gen/delete.go
Normal file
51
tools/goctl/model/sql/gen/delete.go
Normal file
@@ -0,0 +1,51 @@
|
||||
package gen
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
sqltemplate "zero/tools/goctl/model/sql/template"
|
||||
)
|
||||
|
||||
func genDelete(table *InnerTable) (string, error) {
|
||||
t, err := template.New("delete").Parse(sqltemplate.Delete)
|
||||
if err != nil {
|
||||
return "", nil
|
||||
}
|
||||
deleteBuffer := new(bytes.Buffer)
|
||||
keys := make([]string, 0)
|
||||
keyValues := make([]string, 0)
|
||||
for snake, key := range table.CacheKey {
|
||||
if snake == table.PrimaryField.SnakeCase {
|
||||
keys = append(keys, key.Key)
|
||||
} else {
|
||||
keys = append(keys, key.DataKey)
|
||||
}
|
||||
keyValues = append(keyValues, key.KeyVariable)
|
||||
}
|
||||
var isOnlyPrimaryKeyCache = true
|
||||
for _, item := range table.Fields {
|
||||
if item.IsPrimaryKey {
|
||||
continue
|
||||
}
|
||||
if item.Cache {
|
||||
isOnlyPrimaryKeyCache = false
|
||||
break
|
||||
}
|
||||
}
|
||||
err = t.Execute(deleteBuffer, map[string]interface{}{
|
||||
"upperObject": table.UpperCamelCase,
|
||||
"containsCache": table.ContainsCache,
|
||||
"isNotPrimaryKey": !isOnlyPrimaryKeyCache,
|
||||
"lowerPrimaryKey": table.PrimaryField.LowerCamelCase,
|
||||
"dataType": table.PrimaryField.DataType,
|
||||
"keys": strings.Join(keys, "\r\n"),
|
||||
"snakePrimaryKey": table.PrimaryField.SnakeCase,
|
||||
"keyValues": strings.Join(keyValues, ", "),
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return deleteBuffer.String(), nil
|
||||
}
|
||||
7
tools/goctl/model/sql/gen/error.go
Normal file
7
tools/goctl/model/sql/gen/error.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package gen
|
||||
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
ErrCircleQuery = errors.New("circle query with other fields")
|
||||
)
|
||||
39
tools/goctl/model/sql/gen/field.go
Normal file
39
tools/goctl/model/sql/gen/field.go
Normal file
@@ -0,0 +1,39 @@
|
||||
package gen
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
sqltemplate "zero/tools/goctl/model/sql/template"
|
||||
)
|
||||
|
||||
func genFields(fields []*InnerField) (string, error) {
|
||||
list := make([]string, 0)
|
||||
for _, field := range fields {
|
||||
result, err := genField(field)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
list = append(list, result)
|
||||
}
|
||||
return strings.Join(list, "\r\n"), nil
|
||||
}
|
||||
|
||||
func genField(field *InnerField) (string, error) {
|
||||
t, err := template.New("types").Parse(sqltemplate.Field)
|
||||
if err != nil {
|
||||
return "", nil
|
||||
}
|
||||
var typeBuffer = new(bytes.Buffer)
|
||||
err = t.Execute(typeBuffer, map[string]string{
|
||||
"name": field.UpperCamelCase,
|
||||
"type": field.DataType,
|
||||
"tag": field.Tag,
|
||||
"comment": field.Comment,
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return typeBuffer.String(), nil
|
||||
}
|
||||
55
tools/goctl/model/sql/gen/findallbyfield.go
Normal file
55
tools/goctl/model/sql/gen/findallbyfield.go
Normal file
@@ -0,0 +1,55 @@
|
||||
package gen
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
sqltemplate "zero/tools/goctl/model/sql/template"
|
||||
)
|
||||
|
||||
func genFindAllByField(table *InnerTable) (string, error) {
|
||||
t, err := template.New("findAllByField").Parse(sqltemplate.FindAllByField)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
list := make([]string, 0)
|
||||
for _, field := range table.Fields {
|
||||
if field.IsPrimaryKey {
|
||||
continue
|
||||
}
|
||||
if field.QueryType != QueryAll {
|
||||
continue
|
||||
}
|
||||
fineOneByFieldBuffer := new(bytes.Buffer)
|
||||
upperFields := make([]string, 0)
|
||||
in := make([]string, 0)
|
||||
expressionFields := make([]string, 0)
|
||||
expressionValuesFields := make([]string, 0)
|
||||
upperFields = append(upperFields, field.UpperCamelCase)
|
||||
in = append(in, field.LowerCamelCase+" "+field.DataType)
|
||||
expressionFields = append(expressionFields, field.SnakeCase+" = ?")
|
||||
expressionValuesFields = append(expressionValuesFields, field.LowerCamelCase)
|
||||
for _, withField := range field.WithFields {
|
||||
upperFields = append(upperFields, withField.UpperCamelCase)
|
||||
in = append(in, withField.LowerCamelCase+" "+withField.DataType)
|
||||
expressionFields = append(expressionFields, withField.SnakeCase+" = ?")
|
||||
expressionValuesFields = append(expressionValuesFields, withField.LowerCamelCase)
|
||||
}
|
||||
err = t.Execute(fineOneByFieldBuffer, map[string]interface{}{
|
||||
"in": strings.Join(in, ","),
|
||||
"upperObject": table.UpperCamelCase,
|
||||
"upperFields": strings.Join(upperFields, "And"),
|
||||
"lowerObject": table.LowerCamelCase,
|
||||
"snakePrimaryKey": field.SnakeCase,
|
||||
"expression": strings.Join(expressionFields, " AND "),
|
||||
"expressionValues": strings.Join(expressionValuesFields, ", "),
|
||||
"containsCache": table.ContainsCache,
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
list = append(list, fineOneByFieldBuffer.String())
|
||||
}
|
||||
return strings.Join(list, ""), nil
|
||||
}
|
||||
63
tools/goctl/model/sql/gen/findallbylimit.go
Normal file
63
tools/goctl/model/sql/gen/findallbylimit.go
Normal file
@@ -0,0 +1,63 @@
|
||||
package gen
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
sqltemplate "zero/tools/goctl/model/sql/template"
|
||||
)
|
||||
|
||||
func genFindLimitByField(table *InnerTable) (string, error) {
|
||||
t, err := template.New("findLimitByField").Parse(sqltemplate.FindLimitByField)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
list := make([]string, 0)
|
||||
for _, field := range table.Fields {
|
||||
if field.IsPrimaryKey {
|
||||
continue
|
||||
}
|
||||
if field.QueryType != QueryLimit {
|
||||
continue
|
||||
}
|
||||
fineOneByFieldBuffer := new(bytes.Buffer)
|
||||
upperFields := make([]string, 0)
|
||||
in := make([]string, 0)
|
||||
expressionFields := make([]string, 0)
|
||||
expressionValuesFields := make([]string, 0)
|
||||
upperFields = append(upperFields, field.UpperCamelCase)
|
||||
in = append(in, field.LowerCamelCase+" "+field.DataType)
|
||||
expressionFields = append(expressionFields, field.SnakeCase+" = ?")
|
||||
expressionValuesFields = append(expressionValuesFields, field.LowerCamelCase)
|
||||
for _, withField := range field.WithFields {
|
||||
upperFields = append(upperFields, withField.UpperCamelCase)
|
||||
in = append(in, withField.LowerCamelCase+" "+withField.DataType)
|
||||
expressionFields = append(expressionFields, withField.SnakeCase+" = ?")
|
||||
expressionValuesFields = append(expressionValuesFields, withField.LowerCamelCase)
|
||||
}
|
||||
sortList := make([]string, 0)
|
||||
for _, item := range field.Sort {
|
||||
var sort = "ASC"
|
||||
if !item.Asc {
|
||||
sort = "DESC"
|
||||
}
|
||||
sortList = append(sortList, item.Field.SnakeCase+" "+sort)
|
||||
}
|
||||
err = t.Execute(fineOneByFieldBuffer, map[string]interface{}{
|
||||
"in": strings.Join(in, ","),
|
||||
"upperObject": table.UpperCamelCase,
|
||||
"upperFields": strings.Join(upperFields, "And"),
|
||||
"lowerObject": table.LowerCamelCase,
|
||||
"expression": strings.Join(expressionFields, " AND "),
|
||||
"expressionValues": strings.Join(expressionValuesFields, ", "),
|
||||
"sortExpression": strings.Join(sortList, ","),
|
||||
"containsCache": table.ContainsCache,
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
list = append(list, fineOneByFieldBuffer.String())
|
||||
}
|
||||
return strings.Join(list, ""), nil
|
||||
}
|
||||
30
tools/goctl/model/sql/gen/findone.go
Normal file
30
tools/goctl/model/sql/gen/findone.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package gen
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"text/template"
|
||||
|
||||
sqltemplate "zero/tools/goctl/model/sql/template"
|
||||
)
|
||||
|
||||
func genFindOne(table *InnerTable) (string, error) {
|
||||
t, err := template.New("findOne").Parse(sqltemplate.FindOne)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
fineOneBuffer := new(bytes.Buffer)
|
||||
err = t.Execute(fineOneBuffer, map[string]interface{}{
|
||||
"withCache": table.PrimaryField.Cache,
|
||||
"upperObject": table.UpperCamelCase,
|
||||
"lowerObject": table.LowerCamelCase,
|
||||
"snakePrimaryKey": table.PrimaryField.SnakeCase,
|
||||
"lowerPrimaryKey": table.PrimaryField.LowerCamelCase,
|
||||
"dataType": table.PrimaryField.DataType,
|
||||
"cacheKey": table.CacheKey[table.PrimaryField.SnakeCase].Key,
|
||||
"cacheKeyVariable": table.CacheKey[table.PrimaryField.SnakeCase].KeyVariable,
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return fineOneBuffer.String(), nil
|
||||
}
|
||||
67
tools/goctl/model/sql/gen/fineonebyfield.go
Normal file
67
tools/goctl/model/sql/gen/fineonebyfield.go
Normal file
@@ -0,0 +1,67 @@
|
||||
package gen
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
sqltemplate "zero/tools/goctl/model/sql/template"
|
||||
)
|
||||
|
||||
func genFineOneByField(table *InnerTable) (string, error) {
|
||||
t, err := template.New("findOneByField").Parse(sqltemplate.FindOneByField)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
list := make([]string, 0)
|
||||
for _, field := range table.Fields {
|
||||
if field.IsPrimaryKey {
|
||||
continue
|
||||
}
|
||||
if field.QueryType != QueryOne {
|
||||
continue
|
||||
}
|
||||
fineOneByFieldBuffer := new(bytes.Buffer)
|
||||
upperFields := make([]string, 0)
|
||||
in := make([]string, 0)
|
||||
expressionFields := make([]string, 0)
|
||||
expressionValuesFields := make([]string, 0)
|
||||
upperFields = append(upperFields, field.UpperCamelCase)
|
||||
in = append(in, field.LowerCamelCase+" "+field.DataType)
|
||||
expressionFields = append(expressionFields, field.SnakeCase+" = ?")
|
||||
expressionValuesFields = append(expressionValuesFields, field.LowerCamelCase)
|
||||
for _, withField := range field.WithFields {
|
||||
upperFields = append(upperFields, withField.UpperCamelCase)
|
||||
in = append(in, withField.LowerCamelCase+" "+withField.DataType)
|
||||
expressionFields = append(expressionFields, withField.SnakeCase+" = ?")
|
||||
expressionValuesFields = append(expressionValuesFields, withField.LowerCamelCase)
|
||||
}
|
||||
err = t.Execute(fineOneByFieldBuffer, map[string]interface{}{
|
||||
"in": strings.Join(in, ","),
|
||||
"upperObject": table.UpperCamelCase,
|
||||
"upperFields": strings.Join(upperFields, "And"),
|
||||
"onlyOneFiled": len(field.WithFields) == 0,
|
||||
"withCache": field.Cache,
|
||||
"containsCache": table.ContainsCache,
|
||||
"lowerObject": table.LowerCamelCase,
|
||||
"lowerField": field.LowerCamelCase,
|
||||
"snakeField": field.SnakeCase,
|
||||
"lowerPrimaryKey": table.PrimaryField.LowerCamelCase,
|
||||
"UpperPrimaryKey": table.PrimaryField.UpperCamelCase,
|
||||
"primaryKeyDefine": table.CacheKey[table.PrimaryField.SnakeCase].Define,
|
||||
"primarySnakeField": table.PrimaryField.SnakeCase,
|
||||
"primaryDataType": table.PrimaryField.DataType,
|
||||
"primaryDataTypeString": table.PrimaryField.DataType == "string",
|
||||
"upperObjectKey": table.PrimaryField.UpperCamelCase,
|
||||
"cacheKey": table.CacheKey[field.SnakeCase].Key,
|
||||
"cacheKeyVariable": table.CacheKey[field.SnakeCase].KeyVariable,
|
||||
"expression": strings.Join(expressionFields, " AND "),
|
||||
"expressionValues": strings.Join(expressionValuesFields, ", "),
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
list = append(list, fineOneByFieldBuffer.String())
|
||||
}
|
||||
return strings.Join(list, ""), nil
|
||||
}
|
||||
23
tools/goctl/model/sql/gen/imports.go
Normal file
23
tools/goctl/model/sql/gen/imports.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package gen
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"text/template"
|
||||
|
||||
sqltemplate "zero/tools/goctl/model/sql/template"
|
||||
)
|
||||
|
||||
func genImports(table *InnerTable) (string, error) {
|
||||
t, err := template.New("imports").Parse(sqltemplate.Imports)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
importBuffer := new(bytes.Buffer)
|
||||
err = t.Execute(importBuffer, map[string]interface{}{
|
||||
"containsCache": table.ContainsCache,
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return importBuffer.String(), nil
|
||||
}
|
||||
37
tools/goctl/model/sql/gen/insert.go
Normal file
37
tools/goctl/model/sql/gen/insert.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package gen
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
sqltemplate "zero/tools/goctl/model/sql/template"
|
||||
)
|
||||
|
||||
func genInsert(table *InnerTable) (string, error) {
|
||||
t, err := template.New("insert").Parse(sqltemplate.Insert)
|
||||
if err != nil {
|
||||
return "", nil
|
||||
}
|
||||
insertBuffer := new(bytes.Buffer)
|
||||
expressions := make([]string, 0)
|
||||
expressionValues := make([]string, 0)
|
||||
for _, filed := range table.Fields {
|
||||
if filed.SnakeCase == "create_time" || filed.SnakeCase == "update_time" || filed.IsPrimaryKey {
|
||||
continue
|
||||
}
|
||||
expressions = append(expressions, "?")
|
||||
expressionValues = append(expressionValues, "data."+filed.UpperCamelCase)
|
||||
}
|
||||
err = t.Execute(insertBuffer, map[string]interface{}{
|
||||
"upperObject": table.UpperCamelCase,
|
||||
"lowerObject": table.LowerCamelCase,
|
||||
"expression": strings.Join(expressions, ", "),
|
||||
"expressionValues": strings.Join(expressionValues, ", "),
|
||||
"containsCache": table.ContainsCache,
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return insertBuffer.String(), nil
|
||||
}
|
||||
106
tools/goctl/model/sql/gen/keys.go
Normal file
106
tools/goctl/model/sql/gen/keys.go
Normal file
@@ -0,0 +1,106 @@
|
||||
package gen
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strings"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
var (
|
||||
cacheKeyExpressionTemplate = `cache{{.upperCamelTable}}{{.upperCamelField}}Prefix = "cache#{{.lowerCamelTable}}#{{.lowerCamelField}}#"`
|
||||
keyTemplate = `{{.lowerCamelField}}Key := fmt.Sprintf("%s%v", {{.define}}, {{.lowerCamelField}})`
|
||||
keyRespTemplate = `{{.lowerCamelField}}Key := fmt.Sprintf("%s%v", {{.define}}, resp.{{.upperCamelField}})`
|
||||
keyDataTemplate = `{{.lowerCamelField}}Key := fmt.Sprintf("%s%v", {{.define}}, data.{{.upperCamelField}})`
|
||||
)
|
||||
|
||||
type (
|
||||
Key struct {
|
||||
Define string // cacheKey define,如:cacheUserUserIdPrefix
|
||||
Value string // cacheKey value expression,如:cache#user#userId#
|
||||
Expression string // cacheKey expression,如:cacheUserUserIdPrefix="cache#user#userId#"
|
||||
KeyVariable string // cacheKey 声明变量,如:userIdKey
|
||||
Key string // 缓存key的代码,如 userIdKey:=fmt.Sprintf("%s%v", cacheUserUserIdPrefix, userId)
|
||||
DataKey string // 缓存key的代码,如 userIdKey:=fmt.Sprintf("%s%v", cacheUserUserIdPrefix, data.userId)
|
||||
RespKey string // 缓存key的代码,如 userIdKey:=fmt.Sprintf("%s%v", cacheUserUserIdPrefix, resp.userId)
|
||||
}
|
||||
)
|
||||
|
||||
// key-数据库原始字段名,value-缓存key对象
|
||||
func genCacheKeys(table *InnerTable) (map[string]Key, error) {
|
||||
fields := table.Fields
|
||||
var m = make(map[string]Key)
|
||||
if !table.ContainsCache {
|
||||
return m, nil
|
||||
}
|
||||
for _, field := range fields {
|
||||
if !field.Cache && !field.IsPrimaryKey {
|
||||
continue
|
||||
}
|
||||
t, err := template.New("keyExpression").Parse(cacheKeyExpressionTemplate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var expressionBuffer = new(bytes.Buffer)
|
||||
err = t.Execute(expressionBuffer, map[string]string{
|
||||
"upperCamelTable": table.UpperCamelCase,
|
||||
"lowerCamelTable": table.LowerCamelCase,
|
||||
"upperCamelField": field.UpperCamelCase,
|
||||
"lowerCamelField": field.LowerCamelCase,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
expression := expressionBuffer.String()
|
||||
expressionAr := strings.Split(expression, "=")
|
||||
define := strings.TrimSpace(expressionAr[0])
|
||||
value := strings.TrimSpace(expressionAr[1])
|
||||
t, err = template.New("key").Parse(keyTemplate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var keyBuffer = new(bytes.Buffer)
|
||||
err = t.Execute(keyBuffer, map[string]string{
|
||||
"lowerCamelField": field.LowerCamelCase,
|
||||
"define": define,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
t, err = template.New("keyData").Parse(keyDataTemplate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var keyDataBuffer = new(bytes.Buffer)
|
||||
err = t.Execute(keyDataBuffer, map[string]string{
|
||||
"lowerCamelField": field.LowerCamelCase,
|
||||
"upperCamelField": field.UpperCamelCase,
|
||||
"define": define,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
t, err = template.New("keyResp").Parse(keyRespTemplate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var keyRespBuffer = new(bytes.Buffer)
|
||||
err = t.Execute(keyRespBuffer, map[string]string{
|
||||
"lowerCamelField": field.LowerCamelCase,
|
||||
"upperCamelField": field.UpperCamelCase,
|
||||
"define": define,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m[field.SnakeCase] = Key{
|
||||
Define: define,
|
||||
Value: value,
|
||||
Expression: expression,
|
||||
KeyVariable: field.LowerCamelCase + "Key",
|
||||
Key: keyBuffer.String(),
|
||||
DataKey: keyDataBuffer.String(),
|
||||
RespKey: keyRespBuffer.String(),
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
100
tools/goctl/model/sql/gen/keys_test.go
Normal file
100
tools/goctl/model/sql/gen/keys_test.go
Normal file
@@ -0,0 +1,100 @@
|
||||
package gen
|
||||
|
||||
import (
|
||||
"log"
|
||||
"testing"
|
||||
|
||||
"zero/core/logx"
|
||||
)
|
||||
|
||||
func TestKeys(t *testing.T) {
|
||||
var table = OuterTable{
|
||||
Table: "user_info",
|
||||
CreateNotFound: true,
|
||||
Fields: []*OuterFiled{
|
||||
{
|
||||
IsPrimaryKey: true,
|
||||
Name: "user_id",
|
||||
DataBaseType: "bigint",
|
||||
Comment: "主键id",
|
||||
},
|
||||
{
|
||||
Name: "campus_id",
|
||||
DataBaseType: "bigint",
|
||||
Comment: "整校id",
|
||||
QueryType: QueryAll,
|
||||
Cache: false,
|
||||
},
|
||||
{
|
||||
Name: "name",
|
||||
DataBaseType: "varchar",
|
||||
Comment: "用户姓名",
|
||||
QueryType: QueryOne,
|
||||
},
|
||||
{
|
||||
Name: "id_number",
|
||||
DataBaseType: "varchar",
|
||||
Comment: "身份证",
|
||||
Cache: false,
|
||||
QueryType: QueryNone,
|
||||
WithFields: []OuterWithField{
|
||||
{
|
||||
Name: "name",
|
||||
DataBaseType: "varchar",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "age",
|
||||
DataBaseType: "int",
|
||||
Comment: "年龄",
|
||||
Cache: false,
|
||||
QueryType: QueryNone,
|
||||
},
|
||||
{
|
||||
Name: "gender",
|
||||
DataBaseType: "tinyint",
|
||||
Comment: "性别,0-男,1-女,2-不限",
|
||||
QueryType: QueryLimit,
|
||||
WithFields: []OuterWithField{
|
||||
{
|
||||
Name: "campus_id",
|
||||
DataBaseType: "bigint",
|
||||
},
|
||||
},
|
||||
OuterSort: []OuterSort{
|
||||
{
|
||||
Field: "create_time",
|
||||
Asc: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "mobile",
|
||||
DataBaseType: "varchar",
|
||||
Comment: "手机号",
|
||||
QueryType: QueryOne,
|
||||
Cache: true,
|
||||
},
|
||||
{
|
||||
Name: "create_time",
|
||||
DataBaseType: "timestamp",
|
||||
Comment: "创建时间",
|
||||
},
|
||||
{
|
||||
Name: "update_time",
|
||||
DataBaseType: "timestamp",
|
||||
Comment: "更新时间",
|
||||
},
|
||||
},
|
||||
}
|
||||
innerTable, err := TableConvert(table)
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
tp, err := GenModel(innerTable)
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
logx.Info(tp)
|
||||
}
|
||||
86
tools/goctl/model/sql/gen/model.go
Normal file
86
tools/goctl/model/sql/gen/model.go
Normal file
@@ -0,0 +1,86 @@
|
||||
package gen
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"go/format"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"zero/core/logx"
|
||||
sqltemplate "zero/tools/goctl/model/sql/template"
|
||||
)
|
||||
|
||||
func GenModel(table *InnerTable) (string, error) {
|
||||
t, err := template.New("model").Parse(sqltemplate.Model)
|
||||
if err != nil {
|
||||
return "", nil
|
||||
}
|
||||
modelBuffer := new(bytes.Buffer)
|
||||
importsCode, err := genImports(table)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
varsCode, err := genVars(table)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
typesCode, err := genTypes(table)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
newCode, err := genNew(table)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
insertCode, err := genInsert(table)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
var findCode = make([]string, 0)
|
||||
findOneCode, err := genFindOne(table)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
findOneByFieldCode, err := genFineOneByField(table)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
findAllCode, err := genFindAllByField(table)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
findLimitCode, err := genFindLimitByField(table)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
findCode = append(findCode, findOneCode, findOneByFieldCode, findAllCode, findLimitCode)
|
||||
updateCode, err := genUpdate(table)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
deleteCode, err := genDelete(table)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
err = t.Execute(modelBuffer, map[string]interface{}{
|
||||
"imports": importsCode,
|
||||
"vars": varsCode,
|
||||
"types": typesCode,
|
||||
"new": newCode,
|
||||
"insert": insertCode,
|
||||
"find": strings.Join(findCode, "\r\n"),
|
||||
"update": updateCode,
|
||||
"delete": deleteCode,
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
result := modelBuffer.String()
|
||||
bts, err := format.Source([]byte(result))
|
||||
if err != nil {
|
||||
logx.Errorf("%+v", err)
|
||||
return "", err
|
||||
}
|
||||
return string(bts), nil
|
||||
}
|
||||
24
tools/goctl/model/sql/gen/new.go
Normal file
24
tools/goctl/model/sql/gen/new.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package gen
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"text/template"
|
||||
|
||||
sqltemplate "zero/tools/goctl/model/sql/template"
|
||||
)
|
||||
|
||||
func genNew(table *InnerTable) (string, error) {
|
||||
t, err := template.New("new").Parse(sqltemplate.New)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
newBuffer := new(bytes.Buffer)
|
||||
err = t.Execute(newBuffer, map[string]interface{}{
|
||||
"containsCache": table.ContainsCache,
|
||||
"upperObject": table.UpperCamelCase,
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return newBuffer.String(), nil
|
||||
}
|
||||
99
tools/goctl/model/sql/gen/shared.go
Normal file
99
tools/goctl/model/sql/gen/shared.go
Normal file
@@ -0,0 +1,99 @@
|
||||
package gen
|
||||
|
||||
var (
|
||||
commonMysqlDataTypeMap = map[string]string{
|
||||
"tinyint": "int64",
|
||||
"smallint": "int64",
|
||||
"mediumint": "int64",
|
||||
"int": "int64",
|
||||
"integer": "int64",
|
||||
"bigint": "int64",
|
||||
"float": "float64",
|
||||
"double": "float64",
|
||||
"decimal": "float64",
|
||||
"date": "time.Time",
|
||||
"time": "string",
|
||||
"year": "int64",
|
||||
"datetime": "time.Time",
|
||||
"timestamp": "time.Time",
|
||||
"char": "string",
|
||||
"varchar": "string",
|
||||
"tinyblob": "string",
|
||||
"tinytext": "string",
|
||||
"blob": "string",
|
||||
"text": "string",
|
||||
"mediumblob": "string",
|
||||
"mediumtext": "string",
|
||||
"longblob": "string",
|
||||
"longtext": "string",
|
||||
}
|
||||
)
|
||||
|
||||
const (
|
||||
QueryNone QueryType = 0
|
||||
QueryOne QueryType = 1 // 仅支持单个字段为查询条件
|
||||
QueryAll QueryType = 2 // 可支持多个字段为查询条件,且关系均为and
|
||||
QueryLimit QueryType = 3 // 可支持多个字段为查询条件,且关系均为and
|
||||
)
|
||||
|
||||
type (
|
||||
QueryType int
|
||||
|
||||
Case struct {
|
||||
SnakeCase string
|
||||
LowerCamelCase string
|
||||
UpperCamelCase string
|
||||
}
|
||||
InnerWithField struct {
|
||||
Case
|
||||
DataType string
|
||||
}
|
||||
InnerTable struct {
|
||||
Case
|
||||
ContainsCache bool
|
||||
CreateNotFound bool
|
||||
PrimaryField *InnerField
|
||||
Fields []*InnerField
|
||||
CacheKey map[string]Key // key-数据库字段
|
||||
}
|
||||
InnerField struct {
|
||||
IsPrimaryKey bool
|
||||
InnerWithField
|
||||
DataBaseType string // 数据库中字段类型
|
||||
Tag string // 标签,格式:`db:"xxx"`
|
||||
Comment string // 注释,以"// 开头"
|
||||
Cache bool // 是否缓存模式
|
||||
QueryType QueryType
|
||||
WithFields []InnerWithField
|
||||
Sort []InnerSort
|
||||
}
|
||||
InnerSort struct {
|
||||
Field Case
|
||||
Asc bool
|
||||
}
|
||||
|
||||
OuterTable struct {
|
||||
Table string `json:"table"`
|
||||
CreateNotFound bool `json:"createNotFound,optional"`
|
||||
Fields []*OuterFiled `json:"fields"`
|
||||
}
|
||||
OuterWithField struct {
|
||||
Name string `json:"name"`
|
||||
DataBaseType string `json:"dataBaseType"`
|
||||
}
|
||||
OuterSort struct {
|
||||
Field string `json:"fields"`
|
||||
Asc bool `json:"asc,optional"`
|
||||
}
|
||||
OuterFiled struct {
|
||||
IsPrimaryKey bool `json:"isPrimaryKey,optional"`
|
||||
Name string `json:"name"`
|
||||
DataBaseType string `json:"dataBaseType"`
|
||||
Comment string `json:"comment"`
|
||||
Cache bool `json:"cache,optional"`
|
||||
// if IsPrimaryKey==false下面字段有效
|
||||
QueryType QueryType `json:"queryType"` // 查找类型
|
||||
WithFields []OuterWithField `json:"withFields,optional"` // 其他字段联合组成条件的字段列表
|
||||
OuterSort []OuterSort `json:"sort,optional"`
|
||||
}
|
||||
)
|
||||
26
tools/goctl/model/sql/gen/tag.go
Normal file
26
tools/goctl/model/sql/gen/tag.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package gen
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"text/template"
|
||||
|
||||
sqltemplate "zero/tools/goctl/model/sql/template"
|
||||
)
|
||||
|
||||
func genTag(in string) (string, error) {
|
||||
if in == "" {
|
||||
return in, nil
|
||||
}
|
||||
t, err := template.New("tag").Parse(sqltemplate.Tag)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
var tagBuffer = new(bytes.Buffer)
|
||||
err = t.Execute(tagBuffer, map[string]interface{}{
|
||||
"field": in,
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return tagBuffer.String(), nil
|
||||
}
|
||||
30
tools/goctl/model/sql/gen/types.go
Normal file
30
tools/goctl/model/sql/gen/types.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package gen
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"text/template"
|
||||
|
||||
sqltemplate "zero/tools/goctl/model/sql/template"
|
||||
)
|
||||
|
||||
func genTypes(table *InnerTable) (string, error) {
|
||||
fields := table.Fields
|
||||
t, err := template.New("types").Parse(sqltemplate.Types)
|
||||
if err != nil {
|
||||
return "", nil
|
||||
}
|
||||
var typeBuffer = new(bytes.Buffer)
|
||||
fieldsString, err := genFields(fields)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
err = t.Execute(typeBuffer, map[string]interface{}{
|
||||
"upperObject": table.UpperCamelCase,
|
||||
"containsCache": table.ContainsCache,
|
||||
"fields": fieldsString,
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return typeBuffer.String(), nil
|
||||
}
|
||||
38
tools/goctl/model/sql/gen/update.go
Normal file
38
tools/goctl/model/sql/gen/update.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package gen
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
sqltemplate "zero/tools/goctl/model/sql/template"
|
||||
)
|
||||
|
||||
func genUpdate(table *InnerTable) (string, error) {
|
||||
t, err := template.New("update").Parse(sqltemplate.Update)
|
||||
if err != nil {
|
||||
return "", nil
|
||||
}
|
||||
updateBuffer := new(bytes.Buffer)
|
||||
expressionValues := make([]string, 0)
|
||||
for _, filed := range table.Fields {
|
||||
if filed.SnakeCase == "create_time" || filed.SnakeCase == "update_time" || filed.IsPrimaryKey {
|
||||
continue
|
||||
}
|
||||
expressionValues = append(expressionValues, "data."+filed.UpperCamelCase)
|
||||
}
|
||||
expressionValues = append(expressionValues, "data."+table.PrimaryField.UpperCamelCase)
|
||||
err = t.Execute(updateBuffer, map[string]interface{}{
|
||||
"containsCache": table.ContainsCache,
|
||||
"upperObject": table.UpperCamelCase,
|
||||
"primaryCacheKey": table.CacheKey[table.PrimaryField.SnakeCase].DataKey,
|
||||
"primaryKeyVariable": table.CacheKey[table.PrimaryField.SnakeCase].KeyVariable,
|
||||
"lowerObject": table.LowerCamelCase,
|
||||
"primarySnakeCase": table.PrimaryField.SnakeCase,
|
||||
"expressionValues": strings.Join(expressionValues, ", "),
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return updateBuffer.String(), nil
|
||||
}
|
||||
36
tools/goctl/model/sql/gen/vars.go
Normal file
36
tools/goctl/model/sql/gen/vars.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package gen
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
sqltemplate "zero/tools/goctl/model/sql/template"
|
||||
)
|
||||
|
||||
func genVars(table *InnerTable) (string, error) {
|
||||
t, err := template.New("vars").Parse(sqltemplate.Vars)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
varBuffer := new(bytes.Buffer)
|
||||
m, err := genCacheKeys(table)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
keys := make([]string, 0)
|
||||
for _, v := range m {
|
||||
keys = append(keys, v.Expression)
|
||||
}
|
||||
err = t.Execute(varBuffer, map[string]interface{}{
|
||||
"lowerObject": table.LowerCamelCase,
|
||||
"upperObject": table.UpperCamelCase,
|
||||
"createNotFound": table.CreateNotFound,
|
||||
"keysDefine": strings.Join(keys, "\r\n"),
|
||||
"snakePrimaryKey": table.PrimaryField.SnakeCase,
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return varBuffer.String(), nil
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user