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

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
}