goctl added

This commit is contained in:
kim
2020-07-29 17:11:41 +08:00
parent b1975d29a7
commit 121323b8c3
142 changed files with 10690 additions and 0 deletions

4
tools/goctl/Makefile Normal file
View 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

View 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
}

View 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))
}

View 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
}

View 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
}

View 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
}

View 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
}

View 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)
}

View 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,
};
}
}
`

View File

@@ -0,0 +1,7 @@
package config
import "zero/ngin"
type Config struct {
ngin.NgConf
}

View 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()
}

View File

@@ -0,0 +1,8 @@
{
"Name": "user",
"Host": "127.0.0.1",
"Port": 3333,
"Log": {
"Mode": "console"
}
}

View 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",
})
}

View 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,
},
})
}

View File

@@ -0,0 +1,4 @@
package svc
type ServiceContext struct {
}

View 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(), "&#34;", `"`, -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
}

View 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
}

View 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
}

View 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
})
}

View 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
}

View 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
}

View 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"
}

View 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")
}

View 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")
}

View 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
}

View 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
}

View 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
}

View 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)
}

View 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"
)

View 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
}

View 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
}

View 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
}

View 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
}

View File

@@ -0,0 +1,3 @@
package javagen
const modelDir = "model"

21
tools/goctl/api/main.go Normal file
View 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)
}

View 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)
}

View 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"])
}

View 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
}

View 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
}

View 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)
}
}
}

View 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)
}
}

View 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)
}

View File

@@ -0,0 +1,7 @@
package parser
import "zero/tools/goctl/api/spec"
type state interface {
process(api *spec.ApiSpec) (state, error)
}

View 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
}

View 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)
}

View 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
}

View 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, ""
}

View 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
View 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
}

View 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
}

View 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
}

View 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
}

View 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
}

View 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
}

View File

@@ -0,0 +1,5 @@
package tsgen
const (
packagePrefix = "components."
)

View 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
}

View 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:]
}

View 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
}

View 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
}

View 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"
}

View 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
}

View 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
}

View 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)
}

View 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);
}

View 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
}

View 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(),
})
}

View 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
}
}

View 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
View 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
View 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基本信息比如Authapi是哪个用途。
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, redisrpc等
* 在定义的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
View 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
View 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
View 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
}

View 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}}`

View 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
}

View 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
}

View 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
}

View 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
}`
)

View 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
}

View 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:]
}

View 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>
- 无

View 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
}

View 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
}

View File

@@ -0,0 +1,7 @@
package gen
import "errors"
var (
ErrCircleQuery = errors.New("circle query with other fields")
)

View 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
}

View 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
}

View 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
}

View 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
}

View 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
}

View 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
}

View 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
}

View 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
}

View 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)
}

View 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
}

View 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
}

View 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"`
}
)

View 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
}

View 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
}

View 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
}

View 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