goctl added
This commit is contained in:
78
tools/goctl/api/apigen/gen.go
Normal file
78
tools/goctl/api/apigen/gen.go
Normal file
@@ -0,0 +1,78 @@
|
||||
package apigen
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"zero/tools/goctl/util"
|
||||
|
||||
"github.com/logrusorgru/aurora"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
const apiTemplate = `info(
|
||||
title: // TODO: add title
|
||||
desc: // TODO: add description
|
||||
author: {{.gitUser}}
|
||||
email: {{.gitEmail}}
|
||||
)
|
||||
|
||||
type request struct{
|
||||
// TODO: add members here and delete this comment
|
||||
}
|
||||
|
||||
type response struct{
|
||||
// TODO: add members here and delete this comment
|
||||
}
|
||||
|
||||
@server(
|
||||
port: // TODO: add port here and delete this comment
|
||||
)
|
||||
service {{.serviceName}} {
|
||||
@server(
|
||||
handler: // TODO: set handler name and delete this comment
|
||||
)
|
||||
// TODO: edit the below line
|
||||
// get /users/id/:userId(request) returns(response)
|
||||
|
||||
@server(
|
||||
handler: // TODO: set handler name and delete this comment
|
||||
)
|
||||
// TODO: edit the below line
|
||||
// post /users/create(request)
|
||||
}
|
||||
`
|
||||
|
||||
func ApiCommand(c *cli.Context) error {
|
||||
apiFile := c.String("o")
|
||||
if len(apiFile) == 0 {
|
||||
return errors.New("missing -o")
|
||||
}
|
||||
|
||||
fp, err := util.CreateIfNotExist(apiFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer fp.Close()
|
||||
|
||||
baseName := util.FileNameWithoutExt(filepath.Base(apiFile))
|
||||
if strings.HasSuffix(strings.ToLower(baseName), "-api") {
|
||||
baseName = baseName[:len(baseName)-4]
|
||||
} else if strings.HasSuffix(strings.ToLower(baseName), "api") {
|
||||
baseName = baseName[:len(baseName)-3]
|
||||
}
|
||||
t := template.Must(template.New("etcTemplate").Parse(apiTemplate))
|
||||
if err := t.Execute(fp, map[string]string{
|
||||
"gitUser": getGitName(),
|
||||
"gitEmail": getGitEmail(),
|
||||
"serviceName": baseName + "-api",
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println(aurora.Green("Done."))
|
||||
return nil
|
||||
}
|
||||
26
tools/goctl/api/apigen/util.go
Normal file
26
tools/goctl/api/apigen/util.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package apigen
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func getGitName() string {
|
||||
cmd := exec.Command("git", "config", "user.name")
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return strings.TrimSpace(string(out))
|
||||
}
|
||||
|
||||
func getGitEmail() string {
|
||||
cmd := exec.Command("git", "config", "user.email")
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return strings.TrimSpace(string(out))
|
||||
}
|
||||
40
tools/goctl/api/dartgen/gen.go
Normal file
40
tools/goctl/api/dartgen/gen.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package dartgen
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"zero/core/lang"
|
||||
"zero/tools/goctl/api/parser"
|
||||
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
func DartCommand(c *cli.Context) error {
|
||||
apiFile := c.String("api")
|
||||
dir := c.String("dir")
|
||||
if len(apiFile) == 0 {
|
||||
return errors.New("missing -api")
|
||||
}
|
||||
if len(dir) == 0 {
|
||||
return errors.New("missing -dir")
|
||||
}
|
||||
|
||||
p, err := parser.NewParser(apiFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
api, err := p.Parse()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !strings.HasSuffix(dir, "/") {
|
||||
dir = dir + "/"
|
||||
}
|
||||
api.Info.Title = strings.Replace(apiFile, ".api", "", -1)
|
||||
lang.Must(genData(dir+"data/", api))
|
||||
lang.Must(genApi(dir+"api/", api))
|
||||
lang.Must(genVars(dir + "vars/"))
|
||||
return nil
|
||||
}
|
||||
75
tools/goctl/api/dartgen/genapi.go
Normal file
75
tools/goctl/api/dartgen/genapi.go
Normal file
@@ -0,0 +1,75 @@
|
||||
package dartgen
|
||||
|
||||
import (
|
||||
"os"
|
||||
"text/template"
|
||||
|
||||
"zero/core/logx"
|
||||
"zero/tools/goctl/api/spec"
|
||||
)
|
||||
|
||||
const apiTemplate = `import 'api.dart';
|
||||
import '../data/{{with .Info}}{{.Title}}{{end}}.dart';
|
||||
{{with .Service}}
|
||||
/// {{.Name}}
|
||||
{{range .Routes}}
|
||||
/// --{{.Path}}--
|
||||
///
|
||||
/// 请求: {{with .RequestType}}{{.Name}}{{end}}
|
||||
/// 返回: {{with .ResponseType}}{{.Name}}{{end}}
|
||||
Future {{pathToFuncName .Path}}( {{if ne .Method "get"}}{{with .RequestType}}{{.Name}} request,{{end}}{{end}}
|
||||
{Function({{with .ResponseType}}{{.Name}}{{end}}) ok,
|
||||
Function(String) fail,
|
||||
Function eventually}) async {
|
||||
await api{{if eq .Method "get"}}Get{{else}}Post{{end}}('{{.Path}}',{{if ne .Method "get"}}request,{{end}}
|
||||
ok: (data) {
|
||||
if (ok != null) ok({{with .ResponseType}}{{.Name}}{{end}}.fromJson(data));
|
||||
}, fail: fail, eventually: eventually);
|
||||
}
|
||||
{{end}}
|
||||
{{end}}`
|
||||
|
||||
func genApi(dir string, api *spec.ApiSpec) error {
|
||||
e := os.MkdirAll(dir, 0755)
|
||||
if e != nil {
|
||||
logx.Error(e)
|
||||
return e
|
||||
}
|
||||
e = genApiFile(dir)
|
||||
if e != nil {
|
||||
logx.Error(e)
|
||||
return e
|
||||
}
|
||||
|
||||
file, e := os.OpenFile(dir+api.Info.Title+".dart", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
|
||||
if e != nil {
|
||||
logx.Error(e)
|
||||
return e
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
t := template.New("apiTemplate")
|
||||
t = t.Funcs(funcMap)
|
||||
t, e = t.Parse(apiTemplate)
|
||||
if e != nil {
|
||||
logx.Error(e)
|
||||
return e
|
||||
}
|
||||
t.Execute(file, api)
|
||||
return nil
|
||||
}
|
||||
|
||||
func genApiFile(dir string) error {
|
||||
path := dir + "api.dart"
|
||||
if fileExists(path) {
|
||||
return nil
|
||||
}
|
||||
apiFile, e := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
|
||||
if e != nil {
|
||||
logx.Error(e)
|
||||
return e
|
||||
}
|
||||
defer apiFile.Close()
|
||||
apiFile.WriteString(apiFileContent)
|
||||
return nil
|
||||
}
|
||||
79
tools/goctl/api/dartgen/gendata.go
Normal file
79
tools/goctl/api/dartgen/gendata.go
Normal file
@@ -0,0 +1,79 @@
|
||||
package dartgen
|
||||
|
||||
import (
|
||||
"os"
|
||||
"text/template"
|
||||
|
||||
"zero/core/logx"
|
||||
"zero/tools/goctl/api/spec"
|
||||
)
|
||||
|
||||
const dataTemplate = `// --{{with .Info}}{{.Title}}{{end}}--
|
||||
{{ range .Types}}
|
||||
class {{.Name}}{
|
||||
{{range .Members}}
|
||||
/// {{.Comment}}
|
||||
final {{.Type}} {{lowCamelCase .Name}};
|
||||
{{end}}
|
||||
{{.Name}}({ {{range .Members}}
|
||||
this.{{lowCamelCase .Name}},{{end}}
|
||||
});
|
||||
factory {{.Name}}.fromJson(Map<String,dynamic> m) {
|
||||
return {{.Name}}({{range .Members}}
|
||||
{{lowCamelCase .Name}}: {{if isDirectType .Type}}m['{{tagGet .Tag "json"}}']{{else if isClassListType .Type}}(m['{{tagGet .Tag "json"}}'] as List<dynamic>).map((i) => {{getCoreType .Type}}.fromJson(i)){{else}}{{.Type}}.fromJson(m['{{tagGet .Tag "json"}}']){{end}},{{end}}
|
||||
);
|
||||
}
|
||||
Map<String,dynamic> toJson() {
|
||||
return { {{range .Members}}
|
||||
'{{tagGet .Tag "json"}}': {{if isDirectType .Type}}{{lowCamelCase .Name}}{{else if isClassListType .Type}}{{lowCamelCase .Name}}.map((i) => i.toJson()){{else}}{{lowCamelCase .Name}}.toJson(){{end}},{{end}}
|
||||
};
|
||||
}
|
||||
}
|
||||
{{end}}
|
||||
`
|
||||
|
||||
func genData(dir string, api *spec.ApiSpec) error {
|
||||
e := os.MkdirAll(dir, 0755)
|
||||
if e != nil {
|
||||
logx.Error(e)
|
||||
return e
|
||||
}
|
||||
e = genTokens(dir)
|
||||
if e != nil {
|
||||
logx.Error(e)
|
||||
return e
|
||||
}
|
||||
|
||||
file, e := os.OpenFile(dir+api.Info.Title+".dart", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
|
||||
if e != nil {
|
||||
logx.Error(e)
|
||||
return e
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
t := template.New("dataTemplate")
|
||||
t = t.Funcs(funcMap)
|
||||
t, e = t.Parse(dataTemplate)
|
||||
if e != nil {
|
||||
logx.Error(e)
|
||||
return e
|
||||
}
|
||||
|
||||
convertMemberType(api)
|
||||
return t.Execute(file, api)
|
||||
}
|
||||
|
||||
func genTokens(dir string) error {
|
||||
path := dir + "tokens.dart"
|
||||
if fileExists(path) {
|
||||
return nil
|
||||
}
|
||||
tokensFile, e := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
|
||||
if e != nil {
|
||||
logx.Error(e)
|
||||
return e
|
||||
}
|
||||
defer tokensFile.Close()
|
||||
tokensFile.WriteString(tokensFileContent)
|
||||
return nil
|
||||
}
|
||||
66
tools/goctl/api/dartgen/genvars.go
Normal file
66
tools/goctl/api/dartgen/genvars.go
Normal file
@@ -0,0 +1,66 @@
|
||||
package dartgen
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
|
||||
"zero/core/logx"
|
||||
)
|
||||
|
||||
func genVars(dir string) error {
|
||||
e := os.MkdirAll(dir, 0755)
|
||||
if e != nil {
|
||||
logx.Error(e)
|
||||
return e
|
||||
}
|
||||
|
||||
if !fileExists(dir + "vars.dart") {
|
||||
e = ioutil.WriteFile(dir+"vars.dart", []byte(`const serverHost='demo-crm.xiaoheiban.cn';`), 0644)
|
||||
if e != nil {
|
||||
logx.Error(e)
|
||||
return e
|
||||
}
|
||||
}
|
||||
|
||||
if !fileExists(dir + "kv.dart") {
|
||||
e = ioutil.WriteFile(dir+"kv.dart", []byte(`import 'dart:convert';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import '../data/tokens.dart';
|
||||
|
||||
/// 保存tokens到本地
|
||||
///
|
||||
/// 传入null则删除本地tokens
|
||||
/// 返回:true:设置成功 false:设置失败
|
||||
Future<bool> setTokens(Tokens tokens) async {
|
||||
var sp = await SharedPreferences.getInstance();
|
||||
if (tokens == null) {
|
||||
sp.remove('tokens');
|
||||
return true;
|
||||
}
|
||||
return await sp.setString('tokens', jsonEncode(tokens.toJson()));
|
||||
}
|
||||
|
||||
/// 获取本地存储的tokens
|
||||
///
|
||||
/// 如果没有,则返回null
|
||||
Future<Tokens> getTokens() async {
|
||||
try {
|
||||
var sp = await SharedPreferences.getInstance();
|
||||
var str = sp.getString('tokens');
|
||||
if (str.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
return Tokens.fromJson(jsonDecode(str));
|
||||
} catch (e) {
|
||||
print(e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
`), 0644)
|
||||
if e != nil {
|
||||
logx.Error(e)
|
||||
return e
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
118
tools/goctl/api/dartgen/util.go
Normal file
118
tools/goctl/api/dartgen/util.go
Normal file
@@ -0,0 +1,118 @@
|
||||
package dartgen
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"zero/tools/goctl/api/spec"
|
||||
"zero/tools/goctl/api/util"
|
||||
)
|
||||
|
||||
func lowCamelCase(s string) string {
|
||||
if len(s) < 1 {
|
||||
return ""
|
||||
}
|
||||
s = util.ToCamelCase(util.ToSnakeCase(s))
|
||||
return util.ToLower(s[:1]) + s[1:]
|
||||
}
|
||||
|
||||
func pathToFuncName(path string) string {
|
||||
if !strings.HasPrefix(path, "/") {
|
||||
path = "/" + path
|
||||
}
|
||||
if !strings.HasPrefix(path, "/api") {
|
||||
path = "/api" + path
|
||||
}
|
||||
|
||||
path = strings.Replace(path, "/", "_", -1)
|
||||
path = strings.Replace(path, "-", "_", -1)
|
||||
|
||||
camel := util.ToCamelCase(path)
|
||||
return util.ToLower(camel[:1]) + camel[1:]
|
||||
}
|
||||
|
||||
func tagGet(tag, k string) (reflect.Value, error) {
|
||||
v, _ := util.TagLookup(tag, k)
|
||||
out := strings.Split(v, ",")[0]
|
||||
return reflect.ValueOf(out), nil
|
||||
}
|
||||
|
||||
func convertMemberType(api *spec.ApiSpec) {
|
||||
for i, t := range api.Types {
|
||||
for j, mem := range t.Members {
|
||||
api.Types[i].Members[j].Type = goTypeToDart(mem.Type)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func goTypeToDart(t string) string {
|
||||
t = strings.Replace(t, "*", "", -1)
|
||||
if strings.HasPrefix(t, "[]") {
|
||||
return "List<" + goTypeToDart(t[2:]) + ">"
|
||||
}
|
||||
|
||||
if strings.HasPrefix(t, "map") {
|
||||
tys, e := util.DecomposeType(t)
|
||||
if e != nil {
|
||||
log.Fatal(e)
|
||||
}
|
||||
|
||||
if len(tys) != 2 {
|
||||
log.Fatal("Map type number !=2")
|
||||
}
|
||||
|
||||
return "Map<String," + goTypeToDart(tys[1]) + ">"
|
||||
}
|
||||
|
||||
switch t {
|
||||
case "string":
|
||||
return "String"
|
||||
case "int", "int32", "int64":
|
||||
return "int"
|
||||
case "float", "float32", "float64":
|
||||
return "double"
|
||||
case "bool":
|
||||
return "bool"
|
||||
default:
|
||||
return t
|
||||
}
|
||||
}
|
||||
|
||||
func isDirectType(s string) bool {
|
||||
return isAtomicType(s) || isListType(s) && isAtomicType(getCoreType(s))
|
||||
}
|
||||
|
||||
func isAtomicType(s string) bool {
|
||||
switch s {
|
||||
case "String", "int", "double", "bool":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func isListType(s string) bool {
|
||||
return strings.HasPrefix(s, "List<")
|
||||
}
|
||||
|
||||
func isClassListType(s string) bool {
|
||||
return strings.HasPrefix(s, "List<") && !isAtomicType(getCoreType(s))
|
||||
}
|
||||
|
||||
func getCoreType(s string) string {
|
||||
if isAtomicType(s) {
|
||||
return s
|
||||
}
|
||||
if isListType(s) {
|
||||
s = strings.Replace(s, "List<", "", -1)
|
||||
return strings.Replace(s, ">", "", -1)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func fileExists(path string) bool {
|
||||
_, err := os.Stat(path)
|
||||
return !os.IsNotExist(err)
|
||||
}
|
||||
133
tools/goctl/api/dartgen/vars.go
Normal file
133
tools/goctl/api/dartgen/vars.go
Normal file
@@ -0,0 +1,133 @@
|
||||
package dartgen
|
||||
|
||||
import "text/template"
|
||||
|
||||
var funcMap = template.FuncMap{
|
||||
"tagGet": tagGet,
|
||||
"isDirectType": isDirectType,
|
||||
"isClassListType": isClassListType,
|
||||
"getCoreType": getCoreType,
|
||||
"pathToFuncName": pathToFuncName,
|
||||
"lowCamelCase": lowCamelCase,
|
||||
}
|
||||
|
||||
const apiFileContent = `import 'dart:io';
|
||||
import 'dart:convert';
|
||||
import '../vars/kv.dart';
|
||||
import '../vars/vars.dart';
|
||||
|
||||
/// 发送POST请求.
|
||||
///
|
||||
/// data:为你要post的结构体,我们会帮你转换成json字符串;
|
||||
/// ok函数:请求成功的时候调用,fail函数:请求失败的时候会调用,eventually函数:无论成功失败都会调用
|
||||
Future apiPost(String path, dynamic data,
|
||||
{Map<String, String> header,
|
||||
Function(Map<String, dynamic>) ok,
|
||||
Function(String) fail,
|
||||
Function eventually}) async {
|
||||
await _apiRequest('POST', path, data,
|
||||
header: header, ok: ok, fail: fail, eventually: eventually);
|
||||
}
|
||||
|
||||
/// 发送GET请求.
|
||||
///
|
||||
/// ok函数:请求成功的时候调用,fail函数:请求失败的时候会调用,eventually函数:无论成功失败都会调用
|
||||
Future apiGet(String path,
|
||||
{Map<String, String> header,
|
||||
Function(Map<String, dynamic>) ok,
|
||||
Function(String) fail,
|
||||
Function eventually}) async {
|
||||
await _apiRequest('GET', path, null,
|
||||
header: header, ok: ok, fail: fail, eventually: eventually);
|
||||
}
|
||||
|
||||
Future _apiRequest(String method, String path, dynamic data,
|
||||
{Map<String, String> header,
|
||||
Function(Map<String, dynamic>) ok,
|
||||
Function(String) fail,
|
||||
Function eventually}) async {
|
||||
var tokens = await getTokens();
|
||||
try {
|
||||
var client = HttpClient();
|
||||
HttpClientRequest r;
|
||||
if (method == 'POST') {
|
||||
r = await client.postUrl(Uri.parse('https://' + serverHost + path));
|
||||
} else {
|
||||
r = await client.getUrl(Uri.parse('https://' + serverHost + path));
|
||||
}
|
||||
|
||||
r.headers.set('Content-Type', 'application/json');
|
||||
if (tokens != null) {
|
||||
r.headers.set('Authorization', tokens.accessToken);
|
||||
}
|
||||
if (header != null) {
|
||||
header.forEach((k, v) {
|
||||
r.headers.set(k, v);
|
||||
});
|
||||
}
|
||||
var strData = '';
|
||||
if (data != null) {
|
||||
strData = jsonEncode(data);
|
||||
}
|
||||
r.write(strData);
|
||||
var rp = await r.close();
|
||||
var body = await rp.transform(utf8.decoder).join();
|
||||
print('${rp.statusCode} - $path');
|
||||
print('-- request --');
|
||||
print(strData);
|
||||
print('-- response --');
|
||||
print('$body \n');
|
||||
if (rp.statusCode == 404) {
|
||||
if (fail != null) fail('404 not found');
|
||||
} else {
|
||||
Map<String, dynamic> base = jsonDecode(body);
|
||||
if (rp.statusCode == 200) {
|
||||
if (base['code'] != 0) {
|
||||
if (fail != null) fail(base['desc']);
|
||||
} else {
|
||||
if (ok != null) ok(base['data']);
|
||||
}
|
||||
} else if (base['code'] != 0) {
|
||||
if (fail != null) fail(base['desc']);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
if (fail != null) fail(e.toString());
|
||||
}
|
||||
if (eventually != null) eventually();
|
||||
}
|
||||
`
|
||||
const tokensFileContent = `class Tokens {
|
||||
/// 用于访问的token, 每次请求都必须带在Header里面
|
||||
final String accessToken;
|
||||
final int accessExpire;
|
||||
|
||||
/// 用于刷新token
|
||||
final String refreshToken;
|
||||
final int refreshExpire;
|
||||
final int refreshAfter;
|
||||
Tokens(
|
||||
{this.accessToken,
|
||||
this.accessExpire,
|
||||
this.refreshToken,
|
||||
this.refreshExpire,
|
||||
this.refreshAfter});
|
||||
factory Tokens.fromJson(Map<String, dynamic> m) {
|
||||
return Tokens(
|
||||
accessToken: m['access_token'],
|
||||
accessExpire: m['access_expire'],
|
||||
refreshToken: m['refresh_token'],
|
||||
refreshExpire: m['refresh_expire'],
|
||||
refreshAfter: m['refresh_after']);
|
||||
}
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'access_token': accessToken,
|
||||
'access_expire': accessExpire,
|
||||
'refresh_token': refreshToken,
|
||||
'refresh_expire': refreshExpire,
|
||||
'refresh_after': refreshAfter,
|
||||
};
|
||||
}
|
||||
}
|
||||
`
|
||||
7
tools/goctl/api/demo/config/config.go
Normal file
7
tools/goctl/api/demo/config/config.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package config
|
||||
|
||||
import "zero/ngin"
|
||||
|
||||
type Config struct {
|
||||
ngin.NgConf
|
||||
}
|
||||
25
tools/goctl/api/demo/demo.go
Normal file
25
tools/goctl/api/demo/demo.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
|
||||
"zero/core/conf"
|
||||
"zero/ngin"
|
||||
"zero/tools/goctl/api/demo/config"
|
||||
"zero/tools/goctl/api/demo/handler"
|
||||
)
|
||||
|
||||
var configFile = flag.String("f", "etc/user.json", "the config file")
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
|
||||
var c config.Config
|
||||
conf.MustLoad(*configFile, &c)
|
||||
|
||||
engine := ngin.MustNewEngine(c.NgConf)
|
||||
defer engine.Stop()
|
||||
|
||||
handler.RegisterHandlers(engine)
|
||||
engine.Start()
|
||||
}
|
||||
8
tools/goctl/api/demo/etc/user.json
Normal file
8
tools/goctl/api/demo/etc/user.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"Name": "user",
|
||||
"Host": "127.0.0.1",
|
||||
"Port": 3333,
|
||||
"Log": {
|
||||
"Mode": "console"
|
||||
}
|
||||
}
|
||||
33
tools/goctl/api/demo/handler/getuserhandler.go
Normal file
33
tools/goctl/api/demo/handler/getuserhandler.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"zero/core/httpx"
|
||||
)
|
||||
|
||||
type (
|
||||
request struct {
|
||||
User string `form:"user,optional"`
|
||||
}
|
||||
|
||||
response struct {
|
||||
Code int `json:"code"`
|
||||
Greet string `json:"greet"`
|
||||
From string `json:"from,omitempty"`
|
||||
}
|
||||
)
|
||||
|
||||
func GreetHandler(w http.ResponseWriter, r *http.Request) {
|
||||
var req request
|
||||
err := httpx.Parse(r, &req)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
httpx.OkJson(w, response{
|
||||
Code: 0,
|
||||
Greet: "hello",
|
||||
})
|
||||
}
|
||||
17
tools/goctl/api/demo/handler/handlers.go
Normal file
17
tools/goctl/api/demo/handler/handlers.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"zero/ngin"
|
||||
)
|
||||
|
||||
func RegisterHandlers(engine *ngin.Engine) {
|
||||
engine.AddRoutes([]ngin.Route{
|
||||
{
|
||||
Method: http.MethodGet,
|
||||
Path: "/",
|
||||
Handler: GreetHandler,
|
||||
},
|
||||
})
|
||||
}
|
||||
4
tools/goctl/api/demo/svc/servicecontext.go
Normal file
4
tools/goctl/api/demo/svc/servicecontext.go
Normal file
@@ -0,0 +1,4 @@
|
||||
package svc
|
||||
|
||||
type ServiceContext struct {
|
||||
}
|
||||
82
tools/goctl/api/docgen/doc.go
Normal file
82
tools/goctl/api/docgen/doc.go
Normal file
@@ -0,0 +1,82 @@
|
||||
package docgen
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"zero/core/stringx"
|
||||
"zero/tools/goctl/api/gogen"
|
||||
"zero/tools/goctl/api/spec"
|
||||
"zero/tools/goctl/api/util"
|
||||
)
|
||||
|
||||
const (
|
||||
markdownTemplate = `
|
||||
### {{.index}}. {{.routeComment}}
|
||||
|
||||
1. 路由定义
|
||||
|
||||
- Url: {{.uri}}
|
||||
- Method: {{.method}}
|
||||
- Request: {{.requestType}}
|
||||
- Response: {{.responseType}}
|
||||
|
||||
|
||||
2. 类型定义
|
||||
|
||||
{{.responseContent}}
|
||||
|
||||
`
|
||||
)
|
||||
|
||||
func genDoc(api *spec.ApiSpec, dir string, filename string) error {
|
||||
fp, _, err := util.MaybeCreateFile(dir, "", filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer fp.Close()
|
||||
|
||||
var builder strings.Builder
|
||||
for index, route := range api.Service.Routes {
|
||||
routeComment, _ := util.GetAnnotationValue(route.Annotations, "doc", "summary")
|
||||
if len(routeComment) == 0 {
|
||||
routeComment = "N/A"
|
||||
}
|
||||
|
||||
responseContent, err := responseBody(api, route)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
t := template.Must(template.New("markdownTemplate").Parse(markdownTemplate))
|
||||
var tmplBytes bytes.Buffer
|
||||
err = t.Execute(&tmplBytes, map[string]string{
|
||||
"index": strconv.Itoa(index + 1),
|
||||
"routeComment": routeComment,
|
||||
"method": strings.ToUpper(route.Method),
|
||||
"uri": route.Path,
|
||||
"requestType": "`" + stringx.TakeOne(route.RequestType.Name, "-") + "`",
|
||||
"responseType": "`" + stringx.TakeOne(route.ResponseType.Name, "-") + "`",
|
||||
"responseContent": responseContent,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
builder.Write(tmplBytes.Bytes())
|
||||
}
|
||||
_, err = fp.WriteString(strings.Replace(builder.String(), """, `"`, -1))
|
||||
return err
|
||||
}
|
||||
|
||||
func responseBody(api *spec.ApiSpec, route spec.Route) (string, error) {
|
||||
tps := util.GetLocalTypes(api, route)
|
||||
value, err := gogen.BuildTypes(tps)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return fmt.Sprintf("\n\n```golang\n%s\n```\n", value), nil
|
||||
}
|
||||
65
tools/goctl/api/docgen/gen.go
Normal file
65
tools/goctl/api/docgen/gen.go
Normal file
@@ -0,0 +1,65 @@
|
||||
package docgen
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"zero/tools/goctl/api/parser"
|
||||
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
var docDir = "doc"
|
||||
|
||||
func DocCommand(c *cli.Context) error {
|
||||
dir := c.String("dir")
|
||||
if len(dir) == 0 {
|
||||
return errors.New("missing -dir")
|
||||
}
|
||||
|
||||
files, err := filePathWalkDir(dir)
|
||||
if err != nil {
|
||||
return errors.New(fmt.Sprintf("dir %s not exist", dir))
|
||||
}
|
||||
|
||||
err = os.RemoveAll(dir + "/" + docDir + "/")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, f := range files {
|
||||
p, err := parser.NewParser(f)
|
||||
if err != nil {
|
||||
return errors.New(fmt.Sprintf("parse file: %s, err: %s", f, err.Error()))
|
||||
}
|
||||
api, err := p.Parse()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
index := strings.Index(f, dir)
|
||||
if index < 0 {
|
||||
continue
|
||||
}
|
||||
dst := dir + "/" + docDir + f[index+len(dir):]
|
||||
index = strings.LastIndex(dst, "/")
|
||||
if index < 0 {
|
||||
continue
|
||||
}
|
||||
dir := dst[:index]
|
||||
genDoc(api, dir, strings.Replace(dst[index+1:], ".api", ".md", 1))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func filePathWalkDir(root string) ([]string, error) {
|
||||
var files []string
|
||||
err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
|
||||
if !info.IsDir() && strings.HasSuffix(path, ".api") {
|
||||
files = append(files, path)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return files, err
|
||||
}
|
||||
114
tools/goctl/api/format/format.go
Normal file
114
tools/goctl/api/format/format.go
Normal file
@@ -0,0 +1,114 @@
|
||||
package format
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"go/format"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"zero/tools/goctl/api/util"
|
||||
|
||||
"zero/core/errorx"
|
||||
"zero/tools/goctl/api/parser"
|
||||
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
var (
|
||||
reg = regexp.MustCompile("type (?P<name>.*)[\\s]+{")
|
||||
)
|
||||
|
||||
func GoFormatApi(c *cli.Context) error {
|
||||
dir := c.String("dir")
|
||||
if len(dir) == 0 {
|
||||
return errors.New("missing -dir")
|
||||
}
|
||||
|
||||
printToConsole := c.Bool("p")
|
||||
|
||||
var be errorx.BatchError
|
||||
err := filepath.Walk(dir, func(path string, fi os.FileInfo, errBack error) (err error) {
|
||||
if strings.HasSuffix(path, ".api") {
|
||||
err := ApiFormat(path, printToConsole)
|
||||
if err != nil {
|
||||
be.Add(util.WrapErr(err, fi.Name()))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
be.Add(err)
|
||||
if be.NotNil() {
|
||||
errs := be.Err().Error()
|
||||
fmt.Println(errs)
|
||||
os.Exit(1)
|
||||
}
|
||||
return be.Err()
|
||||
}
|
||||
|
||||
func ApiFormat(path string, printToConsole bool) error {
|
||||
data, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
r := reg.ReplaceAllStringFunc(string(data), func(m string) string {
|
||||
parts := reg.FindStringSubmatch(m)
|
||||
if len(parts) < 2 {
|
||||
return m
|
||||
}
|
||||
if !strings.Contains(m, "struct") {
|
||||
return "type " + parts[1] + " struct {"
|
||||
}
|
||||
return m
|
||||
})
|
||||
|
||||
info, st, service, err := parser.MatchStruct(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
info = strings.TrimSpace(info)
|
||||
if len(service) == 0 || len(st) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
fs, err := format.Source([]byte(strings.TrimSpace(st)))
|
||||
if err != nil {
|
||||
str := err.Error()
|
||||
lineNumber := strings.Index(str, ":")
|
||||
if lineNumber > 0 {
|
||||
ln, err := strconv.ParseInt(str[:lineNumber], 10, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pn := 0
|
||||
if len(info) > 0 {
|
||||
pn = countRune(info, '\n') + 1
|
||||
}
|
||||
number := int(ln) + pn + 1
|
||||
return errors.New(fmt.Sprintf("line: %d, %s", number, str[lineNumber+1:]))
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
result := strings.Join([]string{info, string(fs), service}, "\n\n")
|
||||
if printToConsole {
|
||||
_, err := fmt.Print(result)
|
||||
return err
|
||||
}
|
||||
return ioutil.WriteFile(path, []byte(result), os.ModePerm)
|
||||
}
|
||||
|
||||
func countRune(s string, r rune) int {
|
||||
count := 0
|
||||
for _, c := range s {
|
||||
if c == r {
|
||||
count++
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
134
tools/goctl/api/gogen/gen.go
Normal file
134
tools/goctl/api/gogen/gen.go
Normal file
@@ -0,0 +1,134 @@
|
||||
package gogen
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"zero/core/lang"
|
||||
apiformat "zero/tools/goctl/api/format"
|
||||
"zero/tools/goctl/api/parser"
|
||||
apiutil "zero/tools/goctl/api/util"
|
||||
"zero/tools/goctl/util"
|
||||
|
||||
"github.com/logrusorgru/aurora"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
const tmpFile = "%s-%d"
|
||||
|
||||
var tmpDir = path.Join(os.TempDir(), "goctl")
|
||||
|
||||
func GoCommand(c *cli.Context) error {
|
||||
apiFile := c.String("api")
|
||||
dir := c.String("dir")
|
||||
if len(apiFile) == 0 {
|
||||
return errors.New("missing -api")
|
||||
}
|
||||
if len(dir) == 0 {
|
||||
return errors.New("missing -dir")
|
||||
}
|
||||
|
||||
p, err := parser.NewParser(apiFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
api, err := p.Parse()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
lang.Must(util.MkdirIfNotExist(dir))
|
||||
lang.Must(genEtc(dir, api))
|
||||
lang.Must(genConfig(dir, api))
|
||||
lang.Must(genMain(dir, api))
|
||||
lang.Must(genServiceContext(dir, api))
|
||||
lang.Must(genTypes(dir, api))
|
||||
lang.Must(genHandlers(dir, api))
|
||||
lang.Must(genRoutes(dir, api))
|
||||
lang.Must(genLogic(dir, api))
|
||||
// it does not work
|
||||
format(dir)
|
||||
|
||||
if err := backupAndSweep(apiFile); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = apiformat.ApiFormat(apiFile, false); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println(aurora.Green("Done."))
|
||||
return nil
|
||||
}
|
||||
|
||||
func backupAndSweep(apiFile string) error {
|
||||
var err error
|
||||
var wg sync.WaitGroup
|
||||
|
||||
wg.Add(2)
|
||||
_ = os.MkdirAll(tmpDir, os.ModePerm)
|
||||
|
||||
go func() {
|
||||
_, fileName := filepath.Split(apiFile)
|
||||
_, e := apiutil.Copy(apiFile, fmt.Sprintf(path.Join(tmpDir, tmpFile), fileName, time.Now().Unix()))
|
||||
if e != nil {
|
||||
err = e
|
||||
}
|
||||
wg.Done()
|
||||
}()
|
||||
go func() {
|
||||
if e := sweep(); e != nil {
|
||||
err = e
|
||||
}
|
||||
wg.Done()
|
||||
}()
|
||||
wg.Wait()
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func format(dir string) {
|
||||
cmd := exec.Command("go", "fmt", "./"+dir+"...")
|
||||
_, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
print(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func sweep() error {
|
||||
keepTime := time.Now().AddDate(0, 0, -7)
|
||||
return filepath.Walk(tmpDir, func(fpath string, info os.FileInfo, err error) error {
|
||||
if info.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
pos := strings.LastIndexByte(info.Name(), '-')
|
||||
if pos > 0 {
|
||||
timestamp := info.Name()[pos+1:]
|
||||
seconds, err := strconv.ParseInt(timestamp, 10, 64)
|
||||
if err != nil {
|
||||
// print error and ignore
|
||||
fmt.Println(aurora.Red(fmt.Sprintf("sweep ignored file: %s", fpath)))
|
||||
return nil
|
||||
}
|
||||
|
||||
tm := time.Unix(seconds, 0)
|
||||
if tm.Before(keepTime) {
|
||||
if err := os.Remove(fpath); err != nil {
|
||||
fmt.Println(aurora.Red(fmt.Sprintf("failed to remove file: %s", fpath)))
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
48
tools/goctl/api/gogen/genconfig.go
Normal file
48
tools/goctl/api/gogen/genconfig.go
Normal file
@@ -0,0 +1,48 @@
|
||||
package gogen
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"text/template"
|
||||
|
||||
"zero/tools/goctl/api/spec"
|
||||
"zero/tools/goctl/api/util"
|
||||
)
|
||||
|
||||
const (
|
||||
configFile = "config.go"
|
||||
configTemplate = `package config
|
||||
|
||||
import (
|
||||
"zero/ngin"
|
||||
{{.authImport}}
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
ngin.NgConf
|
||||
}
|
||||
`
|
||||
)
|
||||
|
||||
func genConfig(dir string, api *spec.ApiSpec) error {
|
||||
fp, created, err := util.MaybeCreateFile(dir, configDir, configFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !created {
|
||||
return nil
|
||||
}
|
||||
defer fp.Close()
|
||||
|
||||
var authImportStr = ""
|
||||
t := template.Must(template.New("configTemplate").Parse(configTemplate))
|
||||
buffer := new(bytes.Buffer)
|
||||
err = t.Execute(buffer, map[string]string{
|
||||
"authImport": authImportStr,
|
||||
})
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
formatCode := formatCode(buffer.String())
|
||||
_, err = fp.WriteString(formatCode)
|
||||
return err
|
||||
}
|
||||
56
tools/goctl/api/gogen/genetc.go
Normal file
56
tools/goctl/api/gogen/genetc.go
Normal file
@@ -0,0 +1,56 @@
|
||||
package gogen
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"text/template"
|
||||
|
||||
"zero/tools/goctl/api/spec"
|
||||
"zero/tools/goctl/api/util"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultPort = 8888
|
||||
etcDir = "etc"
|
||||
etcTemplate = `{
|
||||
"Name": "{{.serviceName}}",
|
||||
"Host": "{{.host}}",
|
||||
"Port": {{.port}}
|
||||
}`
|
||||
)
|
||||
|
||||
func genEtc(dir string, api *spec.ApiSpec) error {
|
||||
fp, created, err := util.MaybeCreateFile(dir, etcDir, fmt.Sprintf("%s.json", api.Service.Name))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !created {
|
||||
return nil
|
||||
}
|
||||
defer fp.Close()
|
||||
|
||||
service := api.Service
|
||||
host, ok := util.GetAnnotationValue(service.Annotations, "server", "host")
|
||||
if !ok {
|
||||
host = "0.0.0.0"
|
||||
}
|
||||
port, ok := util.GetAnnotationValue(service.Annotations, "server", "port")
|
||||
if !ok {
|
||||
port = strconv.Itoa(defaultPort)
|
||||
}
|
||||
|
||||
t := template.Must(template.New("etcTemplate").Parse(etcTemplate))
|
||||
buffer := new(bytes.Buffer)
|
||||
err = t.Execute(buffer, map[string]string{
|
||||
"serviceName": service.Name,
|
||||
"host": host,
|
||||
"port": port,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
formatCode := formatCode(buffer.String())
|
||||
_, err = fp.WriteString(formatCode)
|
||||
return err
|
||||
}
|
||||
199
tools/goctl/api/gogen/genhandlers.go
Normal file
199
tools/goctl/api/gogen/genhandlers.go
Normal file
@@ -0,0 +1,199 @@
|
||||
package gogen
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"path"
|
||||
"sort"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"zero/tools/goctl/api/spec"
|
||||
apiutil "zero/tools/goctl/api/util"
|
||||
"zero/tools/goctl/util"
|
||||
)
|
||||
|
||||
const (
|
||||
handlerTemplate = `package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
{{.importPackages}}
|
||||
)
|
||||
|
||||
func {{.handlerName}}(ctx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
l := logic.{{.logic}}(r.Context(), ctx)
|
||||
{{.handlerBody}}
|
||||
}
|
||||
}
|
||||
`
|
||||
handlerBodyTemplate = `{{.parseRequest}}
|
||||
{{.processBody}}
|
||||
`
|
||||
parseRequestTemplate = `var req {{.requestType}}
|
||||
if err := httpx.Parse(r, &req); err != nil {
|
||||
logx.Error(err)
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
`
|
||||
hasRespTemplate = `
|
||||
{{.logicResponse}} l.{{.callee}}({{.req}})
|
||||
// TODO write data to response
|
||||
`
|
||||
)
|
||||
|
||||
func genHandler(dir string, group spec.Group, route spec.Route) error {
|
||||
handler, ok := apiutil.GetAnnotationValue(route.Annotations, "server", "handler")
|
||||
if !ok {
|
||||
return fmt.Errorf("missing handler annotation for %q", route.Path)
|
||||
}
|
||||
handler = getHandlerName(handler)
|
||||
var reqBody string
|
||||
if len(route.RequestType.Name) > 0 {
|
||||
var bodyBuilder strings.Builder
|
||||
t := template.Must(template.New("parseRequest").Parse(parseRequestTemplate))
|
||||
if err := t.Execute(&bodyBuilder, map[string]string{
|
||||
"requestType": typesPacket + "." + util.Title(route.RequestType.Name),
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
reqBody = bodyBuilder.String()
|
||||
}
|
||||
|
||||
var req = "req"
|
||||
if len(route.RequestType.Name) == 0 {
|
||||
req = ""
|
||||
}
|
||||
var logicResponse = ""
|
||||
var writeResponse = "nil, nil"
|
||||
if len(route.ResponseType.Name) > 0 {
|
||||
logicResponse = "resp, err :="
|
||||
writeResponse = "resp, err"
|
||||
} else {
|
||||
logicResponse = "err :="
|
||||
writeResponse = "nil, err"
|
||||
}
|
||||
var logicBodyBuilder strings.Builder
|
||||
t := template.Must(template.New("hasRespTemplate").Parse(hasRespTemplate))
|
||||
if err := t.Execute(&logicBodyBuilder, map[string]string{
|
||||
"callee": strings.Title(strings.TrimSuffix(handler, "Handler")),
|
||||
"req": req,
|
||||
"logicResponse": logicResponse,
|
||||
"writeResponse": writeResponse,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
respBody := logicBodyBuilder.String()
|
||||
|
||||
if !strings.HasSuffix(handler, "Handler") {
|
||||
handler = handler + "Handler"
|
||||
}
|
||||
|
||||
var bodyBuilder strings.Builder
|
||||
bodyTemplate := template.Must(template.New("handlerBodyTemplate").Parse(handlerBodyTemplate))
|
||||
if err := bodyTemplate.Execute(&bodyBuilder, map[string]string{
|
||||
"parseRequest": reqBody,
|
||||
"processBody": respBody,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
return doGenToFile(dir, handler, group, route, bodyBuilder)
|
||||
}
|
||||
|
||||
func doGenToFile(dir, handler string, group spec.Group, route spec.Route, bodyBuilder strings.Builder) error {
|
||||
if getHandlerFolderPath(group, route) != handlerDir {
|
||||
handler = strings.Title(handler)
|
||||
}
|
||||
parentPkg, err := getParentPackage(dir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
filename := strings.ToLower(handler)
|
||||
if strings.HasSuffix(filename, "handler") {
|
||||
filename = filename + ".go"
|
||||
} else {
|
||||
filename = filename + "handler.go"
|
||||
}
|
||||
fp, created, err := apiutil.MaybeCreateFile(dir, getHandlerFolderPath(group, route), filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !created {
|
||||
return nil
|
||||
}
|
||||
defer fp.Close()
|
||||
t := template.Must(template.New("handlerTemplate").Parse(handlerTemplate))
|
||||
buffer := new(bytes.Buffer)
|
||||
err = t.Execute(buffer, map[string]string{
|
||||
"logic": "New" + strings.TrimSuffix(strings.Title(handler), "Handler") + "Logic",
|
||||
"importPackages": genHandlerImports(group, route, parentPkg),
|
||||
"handlerName": handler,
|
||||
"handlerBody": strings.TrimSpace(bodyBuilder.String()),
|
||||
})
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
formatCode := formatCode(buffer.String())
|
||||
_, err = fp.WriteString(formatCode)
|
||||
return err
|
||||
}
|
||||
|
||||
func genHandlers(dir string, api *spec.ApiSpec) error {
|
||||
for _, group := range api.Service.Groups {
|
||||
for _, route := range group.Routes {
|
||||
if err := genHandler(dir, group, route); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func genHandlerImports(group spec.Group, route spec.Route, parentPkg string) string {
|
||||
var imports []string
|
||||
if len(route.RequestType.Name) > 0 || len(route.ResponseType.Name) > 0 {
|
||||
imports = append(imports, "\"zero/core/httpx\"")
|
||||
}
|
||||
if len(route.RequestType.Name) > 0 {
|
||||
imports = append(imports, "\"zero/core/logx\"")
|
||||
}
|
||||
imports = append(imports, fmt.Sprintf("\"%s\"", path.Join(parentPkg, contextDir)))
|
||||
if len(route.RequestType.Name) > 0 || len(route.ResponseType.Name) > 0 {
|
||||
imports = append(imports, fmt.Sprintf("\"%s\"", path.Join(parentPkg, typesDir)))
|
||||
}
|
||||
imports = append(imports, fmt.Sprintf("\"%s\"", path.Join(parentPkg, getLogicFolderPath(group, route))))
|
||||
sort.Strings(imports)
|
||||
|
||||
return strings.Join(imports, "\n\t")
|
||||
}
|
||||
|
||||
func getHandlerBaseName(handler string) string {
|
||||
handlerName := util.Untitle(handler)
|
||||
if strings.HasSuffix(handlerName, "handler") {
|
||||
handlerName = strings.ReplaceAll(handlerName, "handler", "")
|
||||
} else if strings.HasSuffix(handlerName, "Handler") {
|
||||
handlerName = strings.ReplaceAll(handlerName, "Handler", "")
|
||||
}
|
||||
return handlerName
|
||||
}
|
||||
|
||||
func getHandlerFolderPath(group spec.Group, route spec.Route) string {
|
||||
folder, ok := apiutil.GetAnnotationValue(route.Annotations, "server", folderProperty)
|
||||
if !ok {
|
||||
folder, ok = apiutil.GetAnnotationValue(group.Annotations, "server", folderProperty)
|
||||
if !ok {
|
||||
return handlerDir
|
||||
}
|
||||
}
|
||||
folder = strings.TrimPrefix(folder, "/")
|
||||
folder = strings.TrimSuffix(folder, "/")
|
||||
return path.Join(handlerDir, folder)
|
||||
}
|
||||
|
||||
func getHandlerName(handler string) string {
|
||||
return getHandlerBaseName(handler) + "Handler"
|
||||
}
|
||||
130
tools/goctl/api/gogen/genlogic.go
Normal file
130
tools/goctl/api/gogen/genlogic.go
Normal file
@@ -0,0 +1,130 @@
|
||||
package gogen
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"path"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"zero/tools/goctl/api/spec"
|
||||
"zero/tools/goctl/api/util"
|
||||
)
|
||||
|
||||
const logicTemplate = `package logic
|
||||
|
||||
import (
|
||||
{{.imports}}
|
||||
)
|
||||
|
||||
type {{.logic}} struct {
|
||||
ctx context.Context
|
||||
logx.Logger
|
||||
}
|
||||
|
||||
func New{{.logic}}(ctx context.Context, svcCtx *svc.ServiceContext) {{.logic}} {
|
||||
return {{.logic}}{
|
||||
ctx: ctx,
|
||||
Logger: logx.WithContext(ctx),
|
||||
}
|
||||
// TODO need set model here from svc
|
||||
}
|
||||
|
||||
func (l *{{.logic}}) {{.function}}({{.request}}) {{.responseType}} {
|
||||
{{.returnString}}
|
||||
}
|
||||
|
||||
`
|
||||
|
||||
func genLogic(dir string, api *spec.ApiSpec) error {
|
||||
for _, g := range api.Service.Groups {
|
||||
for _, r := range g.Routes {
|
||||
err := genLogicByRoute(dir, g, r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func genLogicByRoute(dir string, group spec.Group, route spec.Route) error {
|
||||
handler, ok := util.GetAnnotationValue(route.Annotations, "server", "handler")
|
||||
if !ok {
|
||||
return fmt.Errorf("missing handler annotation for %q", route.Path)
|
||||
}
|
||||
handler = strings.TrimSuffix(handler, "handler")
|
||||
handler = strings.TrimSuffix(handler, "Handler")
|
||||
filename := strings.ToLower(handler)
|
||||
goFile := filename + "logic.go"
|
||||
fp, created, err := util.MaybeCreateFile(dir, getLogicFolderPath(group, route), goFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !created {
|
||||
return nil
|
||||
}
|
||||
defer fp.Close()
|
||||
|
||||
parentPkg, err := getParentPackage(dir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
imports := genLogicImports(route, parentPkg)
|
||||
|
||||
responseString := ""
|
||||
returnString := ""
|
||||
requestString := ""
|
||||
if len(route.ResponseType.Name) > 0 {
|
||||
responseString = "(*types." + strings.Title(route.ResponseType.Name) + ", error)"
|
||||
returnString = "return nil, nil"
|
||||
} else {
|
||||
responseString = "error"
|
||||
returnString = "return nil"
|
||||
}
|
||||
if len(route.RequestType.Name) > 0 {
|
||||
requestString = "req " + "types." + strings.Title(route.RequestType.Name)
|
||||
}
|
||||
|
||||
t := template.Must(template.New("logicTemplate").Parse(logicTemplate))
|
||||
buffer := new(bytes.Buffer)
|
||||
err = t.Execute(fp, map[string]string{
|
||||
"imports": imports,
|
||||
"logic": strings.Title(handler) + "Logic",
|
||||
"function": strings.Title(strings.TrimSuffix(handler, "Handler")),
|
||||
"responseType": responseString,
|
||||
"returnString": returnString,
|
||||
"request": requestString,
|
||||
})
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
formatCode := formatCode(buffer.String())
|
||||
_, err = fp.WriteString(formatCode)
|
||||
return err
|
||||
}
|
||||
|
||||
func getLogicFolderPath(group spec.Group, route spec.Route) string {
|
||||
folder, ok := util.GetAnnotationValue(route.Annotations, "server", folderProperty)
|
||||
if !ok {
|
||||
folder, ok = util.GetAnnotationValue(group.Annotations, "server", folderProperty)
|
||||
if !ok {
|
||||
return logicDir
|
||||
}
|
||||
}
|
||||
folder = strings.TrimPrefix(folder, "/")
|
||||
folder = strings.TrimSuffix(folder, "/")
|
||||
return path.Join(logicDir, folder)
|
||||
}
|
||||
|
||||
func genLogicImports(route spec.Route, parentPkg string) string {
|
||||
var imports []string
|
||||
imports = append(imports, `"context"`)
|
||||
imports = append(imports, "\n")
|
||||
imports = append(imports, `"zero/core/logx"`)
|
||||
if len(route.ResponseType.Name) > 0 || len(route.RequestType.Name) > 0 {
|
||||
imports = append(imports, fmt.Sprintf("\"%s\"", path.Join(parentPkg, typesDir)))
|
||||
}
|
||||
imports = append(imports, fmt.Sprintf("\"%s\"", path.Join(parentPkg, contextDir)))
|
||||
return strings.Join(imports, "\n\t")
|
||||
}
|
||||
85
tools/goctl/api/gogen/genmain.go
Normal file
85
tools/goctl/api/gogen/genmain.go
Normal file
@@ -0,0 +1,85 @@
|
||||
package gogen
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"path"
|
||||
"sort"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"zero/tools/goctl/api/spec"
|
||||
"zero/tools/goctl/api/util"
|
||||
)
|
||||
|
||||
const mainTemplate = `package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
|
||||
{{.importPackages}}
|
||||
)
|
||||
|
||||
var configFile = flag.String("f", "etc/{{.serviceName}}.json", "the config file")
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
|
||||
var c config.Config
|
||||
conf.MustLoad(*configFile, &c)
|
||||
|
||||
ctx := svc.NewServiceContext(c)
|
||||
|
||||
engine := ngin.MustNewEngine(c.NgConf)
|
||||
defer engine.Stop()
|
||||
|
||||
handler.RegisterHandlers(engine, ctx)
|
||||
engine.Start()
|
||||
}
|
||||
`
|
||||
|
||||
func genMain(dir string, api *spec.ApiSpec) error {
|
||||
name := strings.ToLower(api.Service.Name)
|
||||
if strings.HasSuffix(name, "-api") {
|
||||
name = strings.ReplaceAll(name, "-api", "")
|
||||
}
|
||||
goFile := name + ".go"
|
||||
fp, created, err := util.MaybeCreateFile(dir, "", goFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !created {
|
||||
return nil
|
||||
}
|
||||
defer fp.Close()
|
||||
|
||||
parentPkg, err := getParentPackage(dir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
t := template.Must(template.New("mainTemplate").Parse(mainTemplate))
|
||||
buffer := new(bytes.Buffer)
|
||||
err = t.Execute(buffer, map[string]string{
|
||||
"importPackages": genMainImports(parentPkg),
|
||||
"serviceName": api.Service.Name,
|
||||
})
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
formatCode := formatCode(buffer.String())
|
||||
_, err = fp.WriteString(formatCode)
|
||||
return err
|
||||
}
|
||||
|
||||
func genMainImports(parentPkg string) string {
|
||||
imports := []string{
|
||||
`"zero/core/conf"`,
|
||||
`"zero/ngin"`,
|
||||
}
|
||||
imports = append(imports, fmt.Sprintf("\"%s\"", path.Join(parentPkg, configDir)))
|
||||
imports = append(imports, fmt.Sprintf("\"%s\"", path.Join(parentPkg, handlerDir)))
|
||||
imports = append(imports, fmt.Sprintf("\"%s\"", path.Join(parentPkg, contextDir)))
|
||||
sort.Strings(imports)
|
||||
return strings.Join(imports, "\n\t")
|
||||
}
|
||||
193
tools/goctl/api/gogen/genroutes.go
Normal file
193
tools/goctl/api/gogen/genroutes.go
Normal file
@@ -0,0 +1,193 @@
|
||||
package gogen
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"path"
|
||||
"sort"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"zero/core/collection"
|
||||
"zero/tools/goctl/api/spec"
|
||||
apiutil "zero/tools/goctl/api/util"
|
||||
"zero/tools/goctl/util"
|
||||
)
|
||||
|
||||
const (
|
||||
routesFilename = "routes.go"
|
||||
routesTemplate = `// DO NOT EDIT, generated by goctl
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
{{.importPackages}}
|
||||
)
|
||||
|
||||
func RegisterHandlers(engine *ngin.Engine, serverCtx *svc.ServiceContext) {
|
||||
{{.routesAdditions}}
|
||||
}
|
||||
`
|
||||
routesAdditionTemplate = `
|
||||
engine.AddRoutes([]ngin.Route{
|
||||
{{.routes}}
|
||||
}{{.jwt}}{{.signature}})
|
||||
`
|
||||
)
|
||||
|
||||
var mapping = map[string]string{
|
||||
"delete": "http.MethodDelete",
|
||||
"get": "http.MethodGet",
|
||||
"head": "http.MethodHead",
|
||||
"post": "http.MethodPost",
|
||||
"put": "http.MethodPut",
|
||||
}
|
||||
|
||||
type (
|
||||
group struct {
|
||||
routes []route
|
||||
jwtEnabled bool
|
||||
signatureEnabled bool
|
||||
authName string
|
||||
}
|
||||
route struct {
|
||||
method string
|
||||
path string
|
||||
handler string
|
||||
}
|
||||
)
|
||||
|
||||
func genRoutes(dir string, api *spec.ApiSpec) error {
|
||||
var builder strings.Builder
|
||||
groups, err := getRoutes(api)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
gt := template.Must(template.New("groupTemplate").Parse(routesAdditionTemplate))
|
||||
for _, g := range groups {
|
||||
var gbuilder strings.Builder
|
||||
for _, r := range g.routes {
|
||||
fmt.Fprintf(&gbuilder, `
|
||||
{
|
||||
Method: %s,
|
||||
Path: "%s",
|
||||
Handler: %s,
|
||||
},`,
|
||||
r.method, r.path, r.handler)
|
||||
}
|
||||
jwt := ""
|
||||
if g.jwtEnabled {
|
||||
jwt = fmt.Sprintf(", ngin.WithJwt(serverCtx.Config.%s.AccessSecret)", g.authName)
|
||||
}
|
||||
signature := ""
|
||||
if g.signatureEnabled {
|
||||
signature = fmt.Sprintf(", ngin.WithSignature(serverCtx.Config.%s.Signature)", g.authName)
|
||||
}
|
||||
if err := gt.Execute(&builder, map[string]string{
|
||||
"routes": strings.TrimSpace(gbuilder.String()),
|
||||
"jwt": jwt,
|
||||
"signature": signature,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
parentPkg, err := getParentPackage(dir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
filename := path.Join(dir, handlerDir, routesFilename)
|
||||
if err := util.RemoveOrQuit(filename); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fp, created, err := apiutil.MaybeCreateFile(dir, handlerDir, routesFilename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !created {
|
||||
return nil
|
||||
}
|
||||
defer fp.Close()
|
||||
|
||||
t := template.Must(template.New("routesTemplate").Parse(routesTemplate))
|
||||
buffer := new(bytes.Buffer)
|
||||
err = t.Execute(buffer, map[string]string{
|
||||
"importPackages": genRouteImports(parentPkg, api),
|
||||
"routesAdditions": strings.TrimSpace(builder.String()),
|
||||
})
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
formatCode := formatCode(buffer.String())
|
||||
_, err = fp.WriteString(formatCode)
|
||||
return err
|
||||
}
|
||||
|
||||
func genRouteImports(parentPkg string, api *spec.ApiSpec) string {
|
||||
var importSet = collection.NewSet()
|
||||
importSet.AddStr(`"zero/ngin"`)
|
||||
importSet.AddStr(fmt.Sprintf("\"%s\"", path.Join(parentPkg, contextDir)))
|
||||
for _, group := range api.Service.Groups {
|
||||
for _, route := range group.Routes {
|
||||
folder, ok := apiutil.GetAnnotationValue(route.Annotations, "server", folderProperty)
|
||||
if !ok {
|
||||
folder, ok = apiutil.GetAnnotationValue(group.Annotations, "server", folderProperty)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
}
|
||||
importSet.AddStr(fmt.Sprintf("%s \"%s\"", folder, path.Join(parentPkg, handlerDir, folder)))
|
||||
}
|
||||
}
|
||||
imports := importSet.KeysStr()
|
||||
sort.Strings(imports)
|
||||
return strings.Join(imports, "\n\t")
|
||||
}
|
||||
|
||||
func getRoutes(api *spec.ApiSpec) ([]group, error) {
|
||||
var routes []group
|
||||
|
||||
for _, g := range api.Service.Groups {
|
||||
var groupedRoutes group
|
||||
for _, r := range g.Routes {
|
||||
handler, ok := apiutil.GetAnnotationValue(r.Annotations, "server", "handler")
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("missing handler annotation for route %q", r.Path)
|
||||
}
|
||||
handler = getHandlerBaseName(handler) + "Handler(serverCtx)"
|
||||
folder, ok := apiutil.GetAnnotationValue(r.Annotations, "server", folderProperty)
|
||||
if ok {
|
||||
handler = folder + "." + strings.ToUpper(handler[:1]) + handler[1:]
|
||||
} else {
|
||||
folder, ok = apiutil.GetAnnotationValue(g.Annotations, "server", folderProperty)
|
||||
if ok {
|
||||
handler = folder + "." + strings.ToUpper(handler[:1]) + handler[1:]
|
||||
}
|
||||
}
|
||||
groupedRoutes.routes = append(groupedRoutes.routes, route{
|
||||
method: mapping[r.Method],
|
||||
path: r.Path,
|
||||
handler: handler,
|
||||
})
|
||||
}
|
||||
|
||||
if value, ok := apiutil.GetAnnotationValue(g.Annotations, "server", "jwt"); ok {
|
||||
groupedRoutes.authName = value
|
||||
groupedRoutes.jwtEnabled = true
|
||||
}
|
||||
if value, ok := apiutil.GetAnnotationValue(g.Annotations, "server", "signature"); ok {
|
||||
if groupedRoutes.authName != "" && groupedRoutes.authName != value {
|
||||
return nil, errors.New("auth signature should config same")
|
||||
}
|
||||
groupedRoutes.signatureEnabled = true
|
||||
}
|
||||
routes = append(routes, groupedRoutes)
|
||||
}
|
||||
|
||||
return routes, nil
|
||||
}
|
||||
63
tools/goctl/api/gogen/genservicecontext.go
Normal file
63
tools/goctl/api/gogen/genservicecontext.go
Normal file
@@ -0,0 +1,63 @@
|
||||
package gogen
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"path"
|
||||
"text/template"
|
||||
|
||||
"zero/tools/goctl/api/spec"
|
||||
"zero/tools/goctl/api/util"
|
||||
)
|
||||
|
||||
const (
|
||||
contextFilename = "servicecontext.go"
|
||||
contextTemplate = `package svc
|
||||
|
||||
import {{.configImport}}
|
||||
|
||||
type ServiceContext struct {
|
||||
Config {{.config}}
|
||||
}
|
||||
|
||||
func NewServiceContext(config {{.config}}) *ServiceContext {
|
||||
return &ServiceContext{Config: config}
|
||||
}
|
||||
|
||||
`
|
||||
)
|
||||
|
||||
func genServiceContext(dir string, api *spec.ApiSpec) error {
|
||||
fp, created, err := util.MaybeCreateFile(dir, contextDir, contextFilename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !created {
|
||||
return nil
|
||||
}
|
||||
defer fp.Close()
|
||||
|
||||
var authNames = getAuths(api)
|
||||
var auths []string
|
||||
for _, item := range authNames {
|
||||
auths = append(auths, fmt.Sprintf("%s config.AuthConfig", item))
|
||||
}
|
||||
|
||||
parentPkg, err := getParentPackage(dir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var configImport = "\"" + path.Join(parentPkg, configDir) + "\""
|
||||
t := template.Must(template.New("contextTemplate").Parse(contextTemplate))
|
||||
buffer := new(bytes.Buffer)
|
||||
err = t.Execute(buffer, map[string]string{
|
||||
"configImport": configImport,
|
||||
"config": "config.Config",
|
||||
})
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
formatCode := formatCode(buffer.String())
|
||||
_, err = fp.WriteString(formatCode)
|
||||
return err
|
||||
}
|
||||
146
tools/goctl/api/gogen/gentypes.go
Normal file
146
tools/goctl/api/gogen/gentypes.go
Normal file
@@ -0,0 +1,146 @@
|
||||
package gogen
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"path"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"zero/tools/goctl/api/spec"
|
||||
apiutil "zero/tools/goctl/api/util"
|
||||
"zero/tools/goctl/util"
|
||||
)
|
||||
|
||||
const (
|
||||
typesFile = "types.go"
|
||||
typesTemplate = `// DO NOT EDIT, generated by goctl
|
||||
package types{{if .containsTime}}
|
||||
import (
|
||||
"time"
|
||||
){{end}}
|
||||
{{.types}}
|
||||
`
|
||||
)
|
||||
|
||||
func BuildTypes(types []spec.Type) (string, error) {
|
||||
var builder strings.Builder
|
||||
first := true
|
||||
for _, tp := range types {
|
||||
if first {
|
||||
first = false
|
||||
} else {
|
||||
builder.WriteString("\n\n")
|
||||
}
|
||||
if err := writeType(&builder, tp, types); err != nil {
|
||||
return "", apiutil.WrapErr(err, "Type "+tp.Name+" generate error")
|
||||
}
|
||||
}
|
||||
|
||||
return builder.String(), nil
|
||||
}
|
||||
|
||||
func genTypes(dir string, api *spec.ApiSpec) error {
|
||||
val, err := BuildTypes(api.Types)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
filename := path.Join(dir, typesDir, typesFile)
|
||||
if err := util.RemoveOrQuit(filename); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fp, created, err := apiutil.MaybeCreateFile(dir, typesDir, typesFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !created {
|
||||
return nil
|
||||
}
|
||||
defer fp.Close()
|
||||
|
||||
t := template.Must(template.New("typesTemplate").Parse(typesTemplate))
|
||||
buffer := new(bytes.Buffer)
|
||||
err = t.Execute(buffer, map[string]interface{}{
|
||||
"types": val,
|
||||
"containsTime": api.ContainsTime(),
|
||||
})
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
formatCode := formatCode(buffer.String())
|
||||
_, err = fp.WriteString(formatCode)
|
||||
return err
|
||||
}
|
||||
|
||||
func convertTypeCase(types []spec.Type, t string) (string, error) {
|
||||
ts, err := apiutil.DecomposeType(t)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var defTypes []string
|
||||
for _, tp := range ts {
|
||||
for _, typ := range types {
|
||||
if typ.Name == tp {
|
||||
defTypes = append(defTypes, tp)
|
||||
}
|
||||
|
||||
if len(typ.Annotations) > 0 {
|
||||
if value, ok := apiutil.GetAnnotationValue(typ.Annotations, "serverReplacer", tp); ok {
|
||||
t = strings.ReplaceAll(t, tp, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, tp := range defTypes {
|
||||
t = strings.ReplaceAll(t, tp, util.Title(tp))
|
||||
}
|
||||
|
||||
return t, nil
|
||||
}
|
||||
|
||||
func writeType(writer io.Writer, tp spec.Type, types []spec.Type) error {
|
||||
fmt.Fprintf(writer, "type %s struct {\n", util.Title(tp.Name))
|
||||
for _, member := range tp.Members {
|
||||
if member.IsInline {
|
||||
var found = false
|
||||
for _, ty := range types {
|
||||
if strings.ToLower(ty.Name) == strings.ToLower(member.Name) {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return errors.New("inline type " + member.Name + " not exist, please correct api file")
|
||||
}
|
||||
if _, err := fmt.Fprintf(writer, "%s\n", strings.Title(member.Type)); err != nil {
|
||||
return err
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
}
|
||||
tpString, err := convertTypeCase(types, member.Type)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pm, err := member.GetPropertyName()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !strings.Contains(pm, "_") {
|
||||
if strings.Title(member.Name) != strings.Title(pm) {
|
||||
fmt.Printf("type: %s, property name %s json tag illegal, "+
|
||||
"should set json tag as `json:\"%s\"` \n", tp.Name, member.Name, util.Untitle(member.Name))
|
||||
}
|
||||
}
|
||||
if err := writeProperty(writer, member.Name, tpString, member.Tag, member.GetComment(), 1); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
fmt.Fprintf(writer, "}")
|
||||
return nil
|
||||
}
|
||||
67
tools/goctl/api/gogen/util.go
Normal file
67
tools/goctl/api/gogen/util.go
Normal file
@@ -0,0 +1,67 @@
|
||||
package gogen
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
goformat "go/format"
|
||||
"io"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"zero/core/collection"
|
||||
"zero/tools/goctl/api/spec"
|
||||
"zero/tools/goctl/api/util"
|
||||
"zero/tools/goctl/vars"
|
||||
)
|
||||
|
||||
func getParentPackage(dir string) (string, error) {
|
||||
absDir, err := filepath.Abs(dir)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
pos := strings.Index(absDir, vars.ProjectName)
|
||||
if pos < 0 {
|
||||
return "", fmt.Errorf("%s not in project directory", dir)
|
||||
}
|
||||
|
||||
return absDir[pos:], nil
|
||||
}
|
||||
|
||||
func writeIndent(writer io.Writer, indent int) {
|
||||
for i := 0; i < indent; i++ {
|
||||
fmt.Fprint(writer, "\t")
|
||||
}
|
||||
}
|
||||
|
||||
func writeProperty(writer io.Writer, name, tp, tag, comment string, indent int) error {
|
||||
writeIndent(writer, indent)
|
||||
var err error
|
||||
if len(comment) > 0 {
|
||||
comment = strings.TrimPrefix(comment, "//")
|
||||
comment = "//" + comment
|
||||
_, err = fmt.Fprintf(writer, "%s %s %s %s\n", strings.Title(name), tp, tag, comment)
|
||||
} else {
|
||||
_, err = fmt.Fprintf(writer, "%s %s %s\n", strings.Title(name), tp, tag)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func getAuths(api *spec.ApiSpec) []string {
|
||||
var authNames = collection.NewSet()
|
||||
for _, g := range api.Service.Groups {
|
||||
if value, ok := util.GetAnnotationValue(g.Annotations, "server", "jwt"); ok {
|
||||
authNames.Add(value)
|
||||
}
|
||||
if value, ok := util.GetAnnotationValue(g.Annotations, "server", "signature"); ok {
|
||||
authNames.Add(value)
|
||||
}
|
||||
}
|
||||
return authNames.KeysStr()
|
||||
}
|
||||
|
||||
func formatCode(code string) string {
|
||||
ret, err := goformat.Source([]byte(code))
|
||||
if err != nil {
|
||||
return code
|
||||
}
|
||||
return string(ret)
|
||||
}
|
||||
12
tools/goctl/api/gogen/vars.go
Normal file
12
tools/goctl/api/gogen/vars.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package gogen
|
||||
|
||||
const (
|
||||
interval = "internal/"
|
||||
typesPacket = "types"
|
||||
configDir = interval + "config"
|
||||
contextDir = interval + "svc"
|
||||
handlerDir = interval + "handler"
|
||||
logicDir = interval + "logic"
|
||||
typesDir = interval + typesPacket
|
||||
folderProperty = "folder"
|
||||
)
|
||||
46
tools/goctl/api/javagen/gen.go
Normal file
46
tools/goctl/api/javagen/gen.go
Normal file
@@ -0,0 +1,46 @@
|
||||
package javagen
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"zero/core/lang"
|
||||
"zero/tools/goctl/api/parser"
|
||||
"zero/tools/goctl/util"
|
||||
|
||||
"github.com/logrusorgru/aurora"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
func JavaCommand(c *cli.Context) error {
|
||||
apiFile := c.String("api")
|
||||
dir := c.String("dir")
|
||||
if len(apiFile) == 0 {
|
||||
return errors.New("missing -api")
|
||||
}
|
||||
if len(dir) == 0 {
|
||||
return errors.New("missing -dir")
|
||||
}
|
||||
|
||||
p, err := parser.NewParser(apiFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
api, err := p.Parse()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
packetName := api.Service.Name
|
||||
if strings.HasSuffix(packetName, "-api") {
|
||||
packetName = packetName[:len(packetName)-4]
|
||||
}
|
||||
|
||||
lang.Must(util.MkdirIfNotExist(dir))
|
||||
lang.Must(genPacket(dir, packetName, api))
|
||||
lang.Must(genComponents(dir, packetName, api))
|
||||
|
||||
fmt.Println(aurora.Green("Done."))
|
||||
return nil
|
||||
}
|
||||
85
tools/goctl/api/javagen/gencomponents.go
Normal file
85
tools/goctl/api/javagen/gencomponents.go
Normal file
@@ -0,0 +1,85 @@
|
||||
package javagen
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"path"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"zero/tools/goctl/api/spec"
|
||||
apiutil "zero/tools/goctl/api/util"
|
||||
"zero/tools/goctl/util"
|
||||
)
|
||||
|
||||
const (
|
||||
componentTemplate = `// DO NOT EDIT, generated by goctl
|
||||
package com.xhb.logic.http.packet.{{.packet}}.model;
|
||||
|
||||
import com.xhb.logic.http.DeProguardable;
|
||||
|
||||
{{.componentType}}
|
||||
`
|
||||
)
|
||||
|
||||
func genComponents(dir, packetName string, api *spec.ApiSpec) error {
|
||||
types := apiutil.GetSharedTypes(api)
|
||||
if len(types) == 0 {
|
||||
return nil
|
||||
}
|
||||
for _, ty := range types {
|
||||
if err := createComponent(dir, packetName, ty); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func createComponent(dir, packetName string, ty spec.Type) error {
|
||||
modelFile := util.Title(ty.Name) + ".java"
|
||||
filename := path.Join(dir, modelDir, modelFile)
|
||||
if err := util.RemoveOrQuit(filename); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fp, created, err := apiutil.MaybeCreateFile(dir, modelDir, modelFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !created {
|
||||
return nil
|
||||
}
|
||||
defer fp.Close()
|
||||
|
||||
tys, err := buildType(ty)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
t := template.Must(template.New("componentType").Parse(componentTemplate))
|
||||
return t.Execute(fp, map[string]string{
|
||||
"componentType": tys,
|
||||
"packet": packetName,
|
||||
})
|
||||
}
|
||||
|
||||
func buildType(ty spec.Type) (string, error) {
|
||||
var builder strings.Builder
|
||||
if err := writeType(&builder, ty); err != nil {
|
||||
return "", apiutil.WrapErr(err, "Type "+ty.Name+" generate error")
|
||||
}
|
||||
return builder.String(), nil
|
||||
}
|
||||
|
||||
func writeType(writer io.Writer, tp spec.Type) error {
|
||||
fmt.Fprintf(writer, "public class %s implements DeProguardable {\n", util.Title(tp.Name))
|
||||
for _, member := range tp.Members {
|
||||
if err := writeProperty(writer, member, 1); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
genGetSet(writer, tp, 1)
|
||||
fmt.Fprintf(writer, "}\n")
|
||||
return nil
|
||||
}
|
||||
277
tools/goctl/api/javagen/genpacket.go
Normal file
277
tools/goctl/api/javagen/genpacket.go
Normal file
@@ -0,0 +1,277 @@
|
||||
package javagen
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"zero/core/stringx"
|
||||
"zero/tools/goctl/api/spec"
|
||||
apiutil "zero/tools/goctl/api/util"
|
||||
"zero/tools/goctl/util"
|
||||
)
|
||||
|
||||
const packetTemplate = `package com.xhb.logic.http.packet.{{.packet}};
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.xhb.commons.JSON;
|
||||
import com.xhb.commons.JsonParser;
|
||||
import com.xhb.core.network.HttpRequestClient;
|
||||
import com.xhb.core.packet.HttpRequestPacket;
|
||||
import com.xhb.core.response.HttpResponseData;
|
||||
import com.xhb.logic.http.DeProguardable;
|
||||
{{.import}}
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.json.JSONObject;
|
||||
|
||||
public class {{.packetName}} extends HttpRequestPacket<{{.packetName}}.{{.packetName}}Response> {
|
||||
|
||||
{{.paramsDeclaration}}
|
||||
|
||||
public {{.packetName}}({{.params}}{{.requestType}} request) {
|
||||
super(request);
|
||||
this.request = request;{{.paramsSet}}
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpRequestClient.Method requestMethod() {
|
||||
return HttpRequestClient.Method.{{.method}};
|
||||
}
|
||||
|
||||
@Override
|
||||
public String requestUri() {
|
||||
return {{.uri}};
|
||||
}
|
||||
|
||||
@Override
|
||||
public {{.packetName}}Response newInstanceFrom(JSON json) {
|
||||
return new {{.packetName}}Response(json);
|
||||
}
|
||||
|
||||
public static class {{.packetName}}Response extends HttpResponseData {
|
||||
|
||||
private {{.responseType}} responseData;
|
||||
|
||||
{{.packetName}}Response(@NotNull JSON json) {
|
||||
super(json);
|
||||
JSONObject jsonObject = json.asObject();
|
||||
if (JsonParser.hasKey(jsonObject, "data")) {
|
||||
Gson gson = new Gson();
|
||||
JSONObject dataJson = JsonParser.getJSONObject(jsonObject, "data");
|
||||
responseData = gson.fromJson(dataJson.toString(), {{.responseType}}.class);
|
||||
}
|
||||
}
|
||||
|
||||
public {{.responseType}} get{{.responseType}} () {
|
||||
return responseData;
|
||||
}
|
||||
}
|
||||
|
||||
{{.types}}
|
||||
}
|
||||
`
|
||||
|
||||
func genPacket(dir, packetName string, api *spec.ApiSpec) error {
|
||||
for _, route := range api.Service.Routes {
|
||||
if err := createWith(dir, api, route, packetName); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func createWith(dir string, api *spec.ApiSpec, route spec.Route, packetName string) error {
|
||||
packet, ok := apiutil.GetAnnotationValue(route.Annotations, "server", "handler")
|
||||
packet = strings.Replace(packet, "Handler", "Packet", 1)
|
||||
if !ok {
|
||||
return fmt.Errorf("missing packet annotation for %q", route.Path)
|
||||
}
|
||||
|
||||
javaFile := packet + ".java"
|
||||
fp, created, err := apiutil.MaybeCreateFile(dir, "", javaFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !created {
|
||||
return nil
|
||||
}
|
||||
defer fp.Close()
|
||||
|
||||
var builder strings.Builder
|
||||
var first bool
|
||||
tps := apiutil.GetLocalTypes(api, route)
|
||||
|
||||
for _, tp := range tps {
|
||||
if first {
|
||||
first = false
|
||||
} else {
|
||||
fmt.Fprintln(&builder)
|
||||
}
|
||||
if err := genType(&builder, tp); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
types := builder.String()
|
||||
writeIndent(&builder, 1)
|
||||
|
||||
params := paramsForRoute(route)
|
||||
paramsDeclaration := declarationForRoute(route)
|
||||
paramsSet := paramsSet(route)
|
||||
|
||||
t := template.Must(template.New("packetTemplate").Parse(packetTemplate))
|
||||
var tmplBytes bytes.Buffer
|
||||
err = t.Execute(&tmplBytes, map[string]string{
|
||||
"packetName": packet,
|
||||
"method": strings.ToUpper(route.Method),
|
||||
"uri": processUri(route),
|
||||
"types": strings.TrimSpace(types),
|
||||
"responseType": stringx.TakeOne(util.Title(route.ResponseType.Name), "Object"),
|
||||
"params": params,
|
||||
"paramsDeclaration": strings.TrimSpace(paramsDeclaration),
|
||||
"paramsSet": paramsSet,
|
||||
"packet": packetName,
|
||||
"requestType": util.Title(route.RequestType.Name),
|
||||
"import": getImports(api, route, packetName),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
formatFile(&tmplBytes, fp)
|
||||
return nil
|
||||
}
|
||||
|
||||
func getImports(api *spec.ApiSpec, route spec.Route, packetName string) string {
|
||||
var builder strings.Builder
|
||||
allTypes := apiutil.GetAllTypes(api, route)
|
||||
sharedTypes := apiutil.GetSharedTypes(api)
|
||||
for _, at := range allTypes {
|
||||
for _, item := range sharedTypes {
|
||||
if item.Name == at.Name {
|
||||
fmt.Fprintf(&builder, "import com.xhb.logic.http.packet.%s.model.%s;\n", packetName, item.Name)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return builder.String()
|
||||
}
|
||||
|
||||
func formatFile(tmplBytes *bytes.Buffer, file *os.File) {
|
||||
scanner := bufio.NewScanner(tmplBytes)
|
||||
builder := bufio.NewWriter(file)
|
||||
defer builder.Flush()
|
||||
preIsBreakLine := false
|
||||
for scanner.Scan() {
|
||||
text := strings.TrimSpace(scanner.Text())
|
||||
if text == "" && preIsBreakLine {
|
||||
continue
|
||||
}
|
||||
preIsBreakLine = text == ""
|
||||
builder.WriteString(scanner.Text() + "\n")
|
||||
}
|
||||
if err := scanner.Err(); err != nil {
|
||||
println(err)
|
||||
}
|
||||
}
|
||||
|
||||
func paramsSet(route spec.Route) string {
|
||||
path := route.Path
|
||||
cops := strings.Split(path, "/")
|
||||
var builder strings.Builder
|
||||
for _, cop := range cops {
|
||||
if len(cop) == 0 {
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(cop, ":") {
|
||||
param := cop[1:]
|
||||
builder.WriteString("\n")
|
||||
builder.WriteString(fmt.Sprintf("\t\tthis.%s = %s;", param, param))
|
||||
}
|
||||
}
|
||||
result := builder.String()
|
||||
return result
|
||||
}
|
||||
|
||||
func paramsForRoute(route spec.Route) string {
|
||||
path := route.Path
|
||||
cops := strings.Split(path, "/")
|
||||
var builder strings.Builder
|
||||
for _, cop := range cops {
|
||||
if len(cop) == 0 {
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(cop, ":") {
|
||||
builder.WriteString(fmt.Sprintf("String %s, ", cop[1:]))
|
||||
}
|
||||
}
|
||||
return builder.String()
|
||||
}
|
||||
|
||||
func declarationForRoute(route spec.Route) string {
|
||||
path := route.Path
|
||||
cops := strings.Split(path, "/")
|
||||
var builder strings.Builder
|
||||
writeIndent(&builder, 1)
|
||||
for _, cop := range cops {
|
||||
if len(cop) == 0 {
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(cop, ":") {
|
||||
writeIndent(&builder, 1)
|
||||
builder.WriteString(fmt.Sprintf("private String %s;\n", cop[1:]))
|
||||
}
|
||||
}
|
||||
result := strings.TrimSpace(builder.String())
|
||||
if len(result) > 0 {
|
||||
result = "\n" + result
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func processUri(route spec.Route) string {
|
||||
path := route.Path
|
||||
var builder strings.Builder
|
||||
cops := strings.Split(path, "/")
|
||||
for index, cop := range cops {
|
||||
if len(cop) == 0 {
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(cop, ":") {
|
||||
builder.WriteString("/\" + " + cop[1:] + " + \"")
|
||||
} else {
|
||||
builder.WriteString("/" + cop)
|
||||
if index == len(cops)-1 {
|
||||
builder.WriteString("\"")
|
||||
}
|
||||
}
|
||||
}
|
||||
result := builder.String()
|
||||
if strings.HasSuffix(result, " + \"") {
|
||||
result = result[:len(result)-4]
|
||||
}
|
||||
if strings.HasPrefix(result, "/") {
|
||||
result = "\"" + result
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func genType(writer io.Writer, tp spec.Type) error {
|
||||
writeIndent(writer, 1)
|
||||
fmt.Fprintf(writer, "static class %s implements DeProguardable {\n", util.Title(tp.Name))
|
||||
for _, member := range tp.Members {
|
||||
if err := writeProperty(writer, member, 2); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
writeBreakline(writer)
|
||||
writeIndent(writer, 1)
|
||||
genGetSet(writer, tp, 2)
|
||||
writeIndent(writer, 1)
|
||||
fmt.Fprintln(writer, "}")
|
||||
return nil
|
||||
}
|
||||
163
tools/goctl/api/javagen/util.go
Normal file
163
tools/goctl/api/javagen/util.go
Normal file
@@ -0,0 +1,163 @@
|
||||
package javagen
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"zero/tools/goctl/api/spec"
|
||||
apiutil "zero/tools/goctl/api/util"
|
||||
"zero/tools/goctl/util"
|
||||
)
|
||||
|
||||
const getSetTemplate = `
|
||||
{{.indent}}{{.decorator}}
|
||||
{{.indent}}public {{.returnType}} get{{.property}}() {
|
||||
{{.indent}} return this.{{.propertyValue}};
|
||||
{{.indent}}}
|
||||
|
||||
{{.indent}}public void set{{.property}}({{.type}} {{.propertyValue}}) {
|
||||
{{.indent}} this.{{.propertyValue}} = {{.propertyValue}};
|
||||
{{.indent}}}
|
||||
`
|
||||
|
||||
func writeProperty(writer io.Writer, member spec.Member, indent int) error {
|
||||
writeIndent(writer, indent)
|
||||
ty, err := goTypeToJava(member.Type)
|
||||
ty = strings.Replace(ty, "*", "", 1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
name, err := member.GetPropertyName()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = fmt.Fprintf(writer, "private %s %s", ty, name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
writeDefaultValue(writer, member)
|
||||
fmt.Fprint(writer, ";\n")
|
||||
return err
|
||||
}
|
||||
|
||||
func writeDefaultValue(writer io.Writer, member spec.Member) error {
|
||||
switch member.Type {
|
||||
case "string":
|
||||
_, err := fmt.Fprintf(writer, " = \"\"")
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeIndent(writer io.Writer, indent int) {
|
||||
for i := 0; i < indent; i++ {
|
||||
fmt.Fprint(writer, "\t")
|
||||
}
|
||||
}
|
||||
|
||||
func indentString(indent int) string {
|
||||
var result = ""
|
||||
for i := 0; i < indent; i++ {
|
||||
result += "\t"
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func writeBreakline(writer io.Writer) {
|
||||
fmt.Fprint(writer, "\n")
|
||||
}
|
||||
|
||||
func isPrimitiveType(tp string) bool {
|
||||
switch tp {
|
||||
case "int", "int32", "int64":
|
||||
return true
|
||||
case "float", "float32", "float64":
|
||||
return true
|
||||
case "bool":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func goTypeToJava(tp string) (string, error) {
|
||||
if len(tp) == 0 {
|
||||
return "", errors.New("property type empty")
|
||||
}
|
||||
if strings.HasPrefix(tp, "*") {
|
||||
tp = tp[1:]
|
||||
}
|
||||
switch tp {
|
||||
case "string":
|
||||
return "String", nil
|
||||
case "int64":
|
||||
return "long", nil
|
||||
case "int", "int8", "int32":
|
||||
return "int", nil
|
||||
case "float", "float32", "float64":
|
||||
return "double", nil
|
||||
case "bool":
|
||||
return "boolean", nil
|
||||
}
|
||||
if strings.HasPrefix(tp, "[]") {
|
||||
tys, err := apiutil.DecomposeType(tp)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if len(tys) == 0 {
|
||||
return "", fmt.Errorf("%s tp parse error", tp)
|
||||
}
|
||||
return fmt.Sprintf("java.util.ArrayList<%s>", util.Title(tys[0])), nil
|
||||
} else if strings.HasPrefix(tp, "map") {
|
||||
tys, err := apiutil.DecomposeType(tp)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if len(tys) == 2 {
|
||||
return "", fmt.Errorf("%s tp parse error", tp)
|
||||
}
|
||||
return fmt.Sprintf("java.util.HashMap<String, %s>", util.Title(tys[1])), nil
|
||||
}
|
||||
return util.Title(tp), nil
|
||||
}
|
||||
|
||||
func genGetSet(writer io.Writer, tp spec.Type, indent int) error {
|
||||
t := template.Must(template.New("getSetTemplate").Parse(getSetTemplate))
|
||||
for _, member := range tp.Members {
|
||||
var tmplBytes bytes.Buffer
|
||||
|
||||
oty, err := goTypeToJava(member.Type)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tyString := oty
|
||||
decorator := ""
|
||||
if !isPrimitiveType(member.Type) {
|
||||
if member.IsOptional() {
|
||||
decorator = "@org.jetbrains.annotations.Nullable "
|
||||
} else {
|
||||
decorator = "@org.jetbrains.annotations.NotNull "
|
||||
}
|
||||
tyString = decorator + tyString
|
||||
}
|
||||
|
||||
err = t.Execute(&tmplBytes, map[string]string{
|
||||
"property": util.Title(member.Name),
|
||||
"propertyValue": util.Untitle(member.Name),
|
||||
"type": tyString,
|
||||
"decorator": decorator,
|
||||
"returnType": oty,
|
||||
"indent": indentString(indent),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r := tmplBytes.String()
|
||||
r = strings.Replace(r, " boolean get", " boolean is", 1)
|
||||
writer.Write([]byte(r))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
3
tools/goctl/api/javagen/vars.go
Normal file
3
tools/goctl/api/javagen/vars.go
Normal file
@@ -0,0 +1,3 @@
|
||||
package javagen
|
||||
|
||||
const modelDir = "model"
|
||||
21
tools/goctl/api/main.go
Normal file
21
tools/goctl/api/main.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"zero/core/lang"
|
||||
"zero/tools/goctl/api/parser"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if len(os.Args) <= 1 {
|
||||
return
|
||||
}
|
||||
|
||||
p, err := parser.NewParser(os.Args[1])
|
||||
lang.Must(err)
|
||||
api, err := p.Parse()
|
||||
lang.Must(err)
|
||||
fmt.Println(api)
|
||||
}
|
||||
182
tools/goctl/api/parser/basestate.go
Normal file
182
tools/goctl/api/parser/basestate.go
Normal file
@@ -0,0 +1,182 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
startState = iota
|
||||
attrNameState
|
||||
attrValueState
|
||||
attrColonState
|
||||
multilineState
|
||||
)
|
||||
|
||||
type baseState struct {
|
||||
r *bufio.Reader
|
||||
lineNumber *int
|
||||
}
|
||||
|
||||
func newBaseState(r *bufio.Reader, lineNumber *int) *baseState {
|
||||
return &baseState{
|
||||
r: r,
|
||||
lineNumber: lineNumber,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *baseState) parseProperties() (map[string]string, error) {
|
||||
var r = s.r
|
||||
var attributes = make(map[string]string)
|
||||
var builder strings.Builder
|
||||
var key string
|
||||
var st = startState
|
||||
|
||||
for {
|
||||
ch, err := s.read()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch st {
|
||||
case startState:
|
||||
switch {
|
||||
case isNewline(ch):
|
||||
return nil, fmt.Errorf("%q should be on the same line with %q", leftParenthesis, infoDirective)
|
||||
case isSpace(ch):
|
||||
continue
|
||||
case ch == leftParenthesis:
|
||||
st = attrNameState
|
||||
default:
|
||||
return nil, fmt.Errorf("unexpected char %q after %q", ch, infoDirective)
|
||||
}
|
||||
case attrNameState:
|
||||
switch {
|
||||
case isNewline(ch):
|
||||
if builder.Len() > 0 {
|
||||
return nil, fmt.Errorf("unexpected newline after %q", builder.String())
|
||||
}
|
||||
case isLetterDigit(ch):
|
||||
builder.WriteRune(ch)
|
||||
case isSpace(ch):
|
||||
if builder.Len() > 0 {
|
||||
key = builder.String()
|
||||
builder.Reset()
|
||||
st = attrColonState
|
||||
}
|
||||
case ch == colon:
|
||||
if builder.Len() == 0 {
|
||||
return nil, fmt.Errorf("unexpected leading %q", ch)
|
||||
}
|
||||
key = builder.String()
|
||||
builder.Reset()
|
||||
st = attrValueState
|
||||
case ch == rightParenthesis:
|
||||
return attributes, nil
|
||||
}
|
||||
case attrColonState:
|
||||
switch {
|
||||
case isSpace(ch):
|
||||
continue
|
||||
case ch == colon:
|
||||
st = attrValueState
|
||||
default:
|
||||
return nil, fmt.Errorf("bad char %q after %q in %q", ch, key, infoDirective)
|
||||
}
|
||||
case attrValueState:
|
||||
switch {
|
||||
case ch == multilineBeginTag:
|
||||
if builder.Len() > 0 {
|
||||
return nil, fmt.Errorf("%q before %q", builder.String(), multilineBeginTag)
|
||||
} else {
|
||||
st = multilineState
|
||||
}
|
||||
case isSpace(ch):
|
||||
if builder.Len() > 0 {
|
||||
builder.WriteRune(ch)
|
||||
}
|
||||
case isNewline(ch):
|
||||
attributes[key] = builder.String()
|
||||
builder.Reset()
|
||||
st = attrNameState
|
||||
case ch == rightParenthesis:
|
||||
attributes[key] = builder.String()
|
||||
builder.Reset()
|
||||
return attributes, nil
|
||||
default:
|
||||
builder.WriteRune(ch)
|
||||
}
|
||||
case multilineState:
|
||||
switch {
|
||||
case ch == multilineEndTag:
|
||||
attributes[key] = builder.String()
|
||||
builder.Reset()
|
||||
st = attrNameState
|
||||
case isNewline(ch):
|
||||
var multipleNewlines bool
|
||||
loopAfterNewline:
|
||||
for {
|
||||
next, err := read(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch {
|
||||
case isSpace(next):
|
||||
continue
|
||||
case isNewline(next):
|
||||
multipleNewlines = true
|
||||
default:
|
||||
if err := unread(r); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
break loopAfterNewline
|
||||
}
|
||||
}
|
||||
|
||||
if multipleNewlines {
|
||||
fmt.Fprintln(&builder)
|
||||
} else {
|
||||
builder.WriteByte(' ')
|
||||
}
|
||||
case ch == rightParenthesis:
|
||||
if builder.Len() > 0 {
|
||||
attributes[key] = builder.String()
|
||||
builder.Reset()
|
||||
}
|
||||
return attributes, nil
|
||||
default:
|
||||
builder.WriteRune(ch)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *baseState) read() (rune, error) {
|
||||
value, err := read(s.r)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if isNewline(value) {
|
||||
*s.lineNumber++
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
|
||||
func (s *baseState) readLine() (string, error) {
|
||||
line, _, err := s.r.ReadLine()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
*s.lineNumber++
|
||||
return string(line), nil
|
||||
}
|
||||
|
||||
func (s *baseState) skipSpaces() error {
|
||||
return skipSpaces(s.r)
|
||||
}
|
||||
|
||||
func (s *baseState) unread() error {
|
||||
return unread(s.r)
|
||||
}
|
||||
20
tools/goctl/api/parser/basestate_test.go
Normal file
20
tools/goctl/api/parser/basestate_test.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestProperties(t *testing.T) {
|
||||
const text = `(summary: hello world)`
|
||||
var builder bytes.Buffer
|
||||
builder.WriteString(text)
|
||||
var lineNumber = 1
|
||||
var state = newBaseState(bufio.NewReader(&builder), &lineNumber)
|
||||
m, err := state.parseProperties()
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, "hello world", m["summary"])
|
||||
}
|
||||
132
tools/goctl/api/parser/entity.go
Normal file
132
tools/goctl/api/parser/entity.go
Normal file
@@ -0,0 +1,132 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"zero/tools/goctl/api/spec"
|
||||
)
|
||||
|
||||
type (
|
||||
entity struct {
|
||||
state *baseState
|
||||
api *spec.ApiSpec
|
||||
parser entityParser
|
||||
}
|
||||
|
||||
entityParser interface {
|
||||
parseLine(line string, api *spec.ApiSpec, annos []spec.Annotation) error
|
||||
setEntityName(name string)
|
||||
}
|
||||
)
|
||||
|
||||
func newEntity(state *baseState, api *spec.ApiSpec, parser entityParser) entity {
|
||||
return entity{
|
||||
state: state,
|
||||
api: api,
|
||||
parser: parser,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *entity) process() error {
|
||||
line, err := s.state.readLine()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) < 2 {
|
||||
return fmt.Errorf("invalid type definition for %q",
|
||||
strings.TrimSpace(strings.Trim(string(line), "{")))
|
||||
}
|
||||
|
||||
if len(fields) == 2 {
|
||||
if fields[1] != leftBrace {
|
||||
return fmt.Errorf("bad string %q after type", fields[1])
|
||||
}
|
||||
} else if len(fields) == 3 {
|
||||
if fields[1] != typeStruct {
|
||||
return fmt.Errorf("bad string %q after type", fields[1])
|
||||
}
|
||||
if fields[2] != leftBrace {
|
||||
return fmt.Errorf("bad string %q after type", fields[2])
|
||||
}
|
||||
}
|
||||
|
||||
s.parser.setEntityName(fields[0])
|
||||
|
||||
var annos []spec.Annotation
|
||||
memberLoop:
|
||||
for {
|
||||
ch, err := s.state.read()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var annoName string
|
||||
var builder strings.Builder
|
||||
switch {
|
||||
case ch == at:
|
||||
annotationLoop:
|
||||
for {
|
||||
next, err := s.state.read()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
switch {
|
||||
case isSpace(next):
|
||||
if builder.Len() > 0 {
|
||||
annoName = builder.String()
|
||||
builder.Reset()
|
||||
}
|
||||
case isNewline(next):
|
||||
if builder.Len() == 0 {
|
||||
return errors.New("invalid annotation format")
|
||||
}
|
||||
case next == leftParenthesis:
|
||||
if builder.Len() == 0 {
|
||||
return errors.New("invalid annotation format")
|
||||
}
|
||||
annoName = builder.String()
|
||||
builder.Reset()
|
||||
if err := s.state.unread(); err != nil {
|
||||
return err
|
||||
}
|
||||
attrs, err := s.state.parseProperties()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
annos = append(annos, spec.Annotation{
|
||||
Name: annoName,
|
||||
Properties: attrs,
|
||||
})
|
||||
break annotationLoop
|
||||
default:
|
||||
builder.WriteRune(next)
|
||||
}
|
||||
}
|
||||
case ch == rightBrace:
|
||||
break memberLoop
|
||||
case isLetterDigit(ch):
|
||||
if err := s.state.unread(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var line string
|
||||
line, err = s.state.readLine()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
line = strings.TrimSpace(line)
|
||||
if err := s.parser.parseLine(line, s.api, annos); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
annos = nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
62
tools/goctl/api/parser/infostate.go
Normal file
62
tools/goctl/api/parser/infostate.go
Normal file
@@ -0,0 +1,62 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"zero/tools/goctl/api/spec"
|
||||
)
|
||||
|
||||
const (
|
||||
titleTag = "title"
|
||||
descTag = "desc"
|
||||
versionTag = "version"
|
||||
authorTag = "author"
|
||||
emailTag = "email"
|
||||
)
|
||||
|
||||
type infoState struct {
|
||||
*baseState
|
||||
innerState int
|
||||
}
|
||||
|
||||
func newInfoState(st *baseState) state {
|
||||
return &infoState{
|
||||
baseState: st,
|
||||
innerState: startState,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *infoState) process(api *spec.ApiSpec) (state, error) {
|
||||
attrs, err := s.parseProperties()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := s.writeInfo(api, attrs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return newRootState(s.r, s.lineNumber), nil
|
||||
}
|
||||
|
||||
func (s *infoState) writeInfo(api *spec.ApiSpec, attrs map[string]string) error {
|
||||
for k, v := range attrs {
|
||||
switch k {
|
||||
case titleTag:
|
||||
api.Info.Title = strings.TrimSpace(v)
|
||||
case descTag:
|
||||
api.Info.Desc = strings.TrimSpace(v)
|
||||
case versionTag:
|
||||
api.Info.Version = strings.TrimSpace(v)
|
||||
case authorTag:
|
||||
api.Info.Author = strings.TrimSpace(v)
|
||||
case emailTag:
|
||||
api.Info.Email = strings.TrimSpace(v)
|
||||
default:
|
||||
return fmt.Errorf("unknown directive %q in %q section", k, infoDirective)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
57
tools/goctl/api/parser/parser.go
Normal file
57
tools/goctl/api/parser/parser.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
|
||||
"zero/tools/goctl/api/spec"
|
||||
)
|
||||
|
||||
type Parser struct {
|
||||
r *bufio.Reader
|
||||
st string
|
||||
}
|
||||
|
||||
func NewParser(filename string) (*Parser, error) {
|
||||
api, err := ioutil.ReadFile(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
info, body, service, err := MatchStruct(string(api))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var buffer = new(bytes.Buffer)
|
||||
buffer.WriteString(info)
|
||||
buffer.WriteString(service)
|
||||
return &Parser{
|
||||
r: bufio.NewReader(buffer),
|
||||
st: body,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (p *Parser) Parse() (api *spec.ApiSpec, err error) {
|
||||
api = new(spec.ApiSpec)
|
||||
types, err := parseStructAst(p.st)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
api.Types = types
|
||||
var lineNumber = 1
|
||||
st := newRootState(p.r, &lineNumber)
|
||||
for {
|
||||
st, err = st.process(api)
|
||||
if err == io.EOF {
|
||||
return api, p.validate(api)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("near line: %d, %s", lineNumber, err.Error())
|
||||
}
|
||||
if st == nil {
|
||||
return api, p.validate(api)
|
||||
}
|
||||
}
|
||||
}
|
||||
109
tools/goctl/api/parser/rootstate.go
Normal file
109
tools/goctl/api/parser/rootstate.go
Normal file
@@ -0,0 +1,109 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"zero/tools/goctl/api/spec"
|
||||
)
|
||||
|
||||
type rootState struct {
|
||||
*baseState
|
||||
}
|
||||
|
||||
func newRootState(r *bufio.Reader, lineNumber *int) state {
|
||||
var state = newBaseState(r, lineNumber)
|
||||
return rootState{
|
||||
baseState: state,
|
||||
}
|
||||
}
|
||||
|
||||
func (s rootState) process(api *spec.ApiSpec) (state, error) {
|
||||
var annos []spec.Annotation
|
||||
var builder strings.Builder
|
||||
for {
|
||||
ch, err := s.read()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch {
|
||||
case isSpace(ch):
|
||||
if builder.Len() == 0 {
|
||||
continue
|
||||
}
|
||||
token := builder.String()
|
||||
builder.Reset()
|
||||
return s.processToken(token, annos)
|
||||
case ch == at:
|
||||
if builder.Len() > 0 {
|
||||
return nil, fmt.Errorf("%q before %q", builder.String(), at)
|
||||
}
|
||||
|
||||
var annoName string
|
||||
annoLoop:
|
||||
for {
|
||||
next, err := s.read()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
switch {
|
||||
case isSpace(next):
|
||||
if builder.Len() > 0 {
|
||||
annoName = builder.String()
|
||||
builder.Reset()
|
||||
}
|
||||
case next == leftParenthesis:
|
||||
if err := s.unread(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if builder.Len() > 0 {
|
||||
annoName = builder.String()
|
||||
builder.Reset()
|
||||
}
|
||||
attrs, err := s.parseProperties()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
annos = append(annos, spec.Annotation{
|
||||
Name: annoName,
|
||||
Properties: attrs,
|
||||
})
|
||||
break annoLoop
|
||||
default:
|
||||
builder.WriteRune(next)
|
||||
}
|
||||
}
|
||||
case ch == leftParenthesis:
|
||||
if builder.Len() == 0 {
|
||||
return nil, fmt.Errorf("incorrect %q at the beginning of the line", leftParenthesis)
|
||||
}
|
||||
if err := s.unread(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
token := builder.String()
|
||||
builder.Reset()
|
||||
return s.processToken(token, annos)
|
||||
case isLetterDigit(ch):
|
||||
builder.WriteRune(ch)
|
||||
case isNewline(ch):
|
||||
if builder.Len() > 0 {
|
||||
return nil, fmt.Errorf("incorrect newline after %q", builder.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s rootState) processToken(token string, annos []spec.Annotation) (state, error) {
|
||||
switch token {
|
||||
case infoDirective:
|
||||
return newInfoState(s.baseState), nil
|
||||
//case typeDirective:
|
||||
//return newTypeState(s.baseState, annos), nil
|
||||
case serviceDirective:
|
||||
return newServiceState(s.baseState, annos), nil
|
||||
default:
|
||||
return nil, fmt.Errorf("wrong directive %q", token)
|
||||
}
|
||||
}
|
||||
97
tools/goctl/api/parser/servicestate.go
Normal file
97
tools/goctl/api/parser/servicestate.go
Normal file
@@ -0,0 +1,97 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"zero/tools/goctl/api/spec"
|
||||
)
|
||||
|
||||
type serviceState struct {
|
||||
*baseState
|
||||
annos []spec.Annotation
|
||||
}
|
||||
|
||||
func newServiceState(state *baseState, annos []spec.Annotation) state {
|
||||
return &serviceState{
|
||||
baseState: state,
|
||||
annos: annos,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *serviceState) process(api *spec.ApiSpec) (state, error) {
|
||||
var name string
|
||||
var routes []spec.Route
|
||||
parser := &serviceEntityParser{
|
||||
acceptName: func(n string) {
|
||||
name = n
|
||||
},
|
||||
acceptRoute: func(route spec.Route) {
|
||||
routes = append(routes, route)
|
||||
},
|
||||
}
|
||||
ent := newEntity(s.baseState, api, parser)
|
||||
if err := ent.process(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
api.Service = spec.Service{
|
||||
Name: name,
|
||||
Annotations: append(api.Service.Annotations, s.annos...),
|
||||
Routes: append(api.Service.Routes, routes...),
|
||||
Groups: append(api.Service.Groups, spec.Group{
|
||||
Annotations: s.annos,
|
||||
Routes: routes,
|
||||
}),
|
||||
}
|
||||
|
||||
return newRootState(s.r, s.lineNumber), nil
|
||||
}
|
||||
|
||||
type serviceEntityParser struct {
|
||||
acceptName func(name string)
|
||||
acceptRoute func(route spec.Route)
|
||||
}
|
||||
|
||||
func (p *serviceEntityParser) parseLine(line string, api *spec.ApiSpec, annos []spec.Annotation) error {
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) < 2 {
|
||||
return fmt.Errorf("wrong line %q", line)
|
||||
}
|
||||
|
||||
method := fields[0]
|
||||
pathAndRequest := fields[1]
|
||||
pos := strings.Index(pathAndRequest, "(")
|
||||
if pos < 0 {
|
||||
return fmt.Errorf("wrong line %q", line)
|
||||
}
|
||||
path := strings.TrimSpace(pathAndRequest[:pos])
|
||||
pathAndRequest = pathAndRequest[pos+1:]
|
||||
pos = strings.Index(pathAndRequest, ")")
|
||||
if pos < 0 {
|
||||
return fmt.Errorf("wrong line %q", line)
|
||||
}
|
||||
req := pathAndRequest[:pos]
|
||||
var returns string
|
||||
if len(fields) > 2 {
|
||||
returns = fields[2]
|
||||
}
|
||||
returns = strings.ReplaceAll(returns, "returns", "")
|
||||
returns = strings.ReplaceAll(returns, "(", "")
|
||||
returns = strings.ReplaceAll(returns, ")", "")
|
||||
returns = strings.TrimSpace(returns)
|
||||
|
||||
p.acceptRoute(spec.Route{
|
||||
Annotations: annos,
|
||||
Method: method,
|
||||
Path: path,
|
||||
RequestType: GetType(api, req),
|
||||
ResponseType: GetType(api, returns),
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *serviceEntityParser) setEntityName(name string) {
|
||||
p.acceptName(name)
|
||||
}
|
||||
7
tools/goctl/api/parser/state.go
Normal file
7
tools/goctl/api/parser/state.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package parser
|
||||
|
||||
import "zero/tools/goctl/api/spec"
|
||||
|
||||
type state interface {
|
||||
process(api *spec.ApiSpec) (state, error)
|
||||
}
|
||||
329
tools/goctl/api/parser/typeparser.go
Normal file
329
tools/goctl/api/parser/typeparser.go
Normal file
@@ -0,0 +1,329 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"zero/tools/goctl/api/spec"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrStructNotFound = errors.New("struct not found")
|
||||
ErrUnSupportType = errors.New("unsupport type")
|
||||
ErrUnSupportInlineType = errors.New("unsupport inline type")
|
||||
interfaceExpr = `interface{}`
|
||||
objectM = make(map[string]*spec.Type)
|
||||
)
|
||||
|
||||
const (
|
||||
golangF = `package ast
|
||||
%s
|
||||
`
|
||||
pkgPrefix = "package"
|
||||
)
|
||||
|
||||
func parseStructAst(golang string) ([]spec.Type, error) {
|
||||
if !strings.HasPrefix(golang, pkgPrefix) {
|
||||
golang = fmt.Sprintf(golangF, golang)
|
||||
}
|
||||
fSet := token.NewFileSet()
|
||||
f, err := parser.ParseFile(fSet, "", golang, parser.ParseComments)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
commentMap := ast.NewCommentMap(fSet, f, f.Comments)
|
||||
f.Comments = commentMap.Filter(f).Comments()
|
||||
scope := f.Scope
|
||||
if scope == nil {
|
||||
return nil, ErrStructNotFound
|
||||
}
|
||||
objects := scope.Objects
|
||||
structs := make([]*spec.Type, 0)
|
||||
for structName, obj := range objects {
|
||||
st, err := parseObject(structName, obj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
structs = append(structs, st)
|
||||
}
|
||||
sort.Slice(structs, func(i, j int) bool {
|
||||
return structs[i].Name < structs[j].Name
|
||||
})
|
||||
resp := make([]spec.Type, 0)
|
||||
for _, item := range structs {
|
||||
resp = append(resp, *item)
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func parseObject(structName string, obj *ast.Object) (*spec.Type, error) {
|
||||
if data, ok := objectM[structName]; ok {
|
||||
return data, nil
|
||||
}
|
||||
var st spec.Type
|
||||
st.Name = structName
|
||||
if obj.Decl == nil {
|
||||
objectM[structName] = &st
|
||||
return &st, nil
|
||||
}
|
||||
decl, ok := obj.Decl.(*ast.TypeSpec)
|
||||
if !ok {
|
||||
objectM[structName] = &st
|
||||
return &st, nil
|
||||
}
|
||||
if decl.Type == nil {
|
||||
objectM[structName] = &st
|
||||
return &st, nil
|
||||
}
|
||||
tp, ok := decl.Type.(*ast.StructType)
|
||||
if !ok {
|
||||
objectM[structName] = &st
|
||||
return &st, nil
|
||||
}
|
||||
fields := tp.Fields
|
||||
if fields == nil {
|
||||
objectM[structName] = &st
|
||||
return &st, nil
|
||||
}
|
||||
fieldList := fields.List
|
||||
members, err := parseFields(fieldList)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
st.Members = members
|
||||
objectM[structName] = &st
|
||||
return &st, nil
|
||||
}
|
||||
|
||||
func parseFields(fields []*ast.Field) ([]spec.Member, error) {
|
||||
members := make([]spec.Member, 0)
|
||||
for _, field := range fields {
|
||||
docs := parseCommentOrDoc(field.Doc)
|
||||
comments := parseCommentOrDoc(field.Comment)
|
||||
name := parseName(field.Names)
|
||||
tp, stringExpr, err := parseType(field.Type)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tag := parseTag(field.Tag)
|
||||
isInline := name == ""
|
||||
if isInline {
|
||||
var err error
|
||||
name, err = getInlineName(tp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
members = append(members, spec.Member{
|
||||
Name: name,
|
||||
Type: stringExpr,
|
||||
Expr: tp,
|
||||
Tag: tag,
|
||||
Comments: comments,
|
||||
Docs: docs,
|
||||
IsInline: isInline,
|
||||
})
|
||||
|
||||
}
|
||||
return members, nil
|
||||
}
|
||||
|
||||
func getInlineName(tp interface{}) (string, error) {
|
||||
switch v := tp.(type) {
|
||||
case *spec.Type:
|
||||
return v.Name, nil
|
||||
case *spec.PointerType:
|
||||
return getInlineName(v.Star)
|
||||
case *spec.StructType:
|
||||
return v.StringExpr, nil
|
||||
default:
|
||||
return "", ErrUnSupportInlineType
|
||||
}
|
||||
}
|
||||
|
||||
func getInlineTypePrefix(tp interface{}) (string, error) {
|
||||
if tp == nil {
|
||||
return "", nil
|
||||
}
|
||||
switch tp.(type) {
|
||||
case *ast.Ident:
|
||||
return "", nil
|
||||
case *ast.StarExpr:
|
||||
return "*", nil
|
||||
case *ast.TypeSpec:
|
||||
return "", nil
|
||||
default:
|
||||
return "", ErrUnSupportInlineType
|
||||
}
|
||||
}
|
||||
|
||||
func parseTag(basicLit *ast.BasicLit) string {
|
||||
if basicLit == nil {
|
||||
return ""
|
||||
}
|
||||
return basicLit.Value
|
||||
}
|
||||
|
||||
// returns
|
||||
// resp1:type can convert to *spec.PointerType|*spec.BasicType|*spec.MapType|*spec.ArrayType|*spec.InterfaceType
|
||||
// resp2:type's string expression,like int、string、[]int64、map[string]User、*User
|
||||
// resp3:error
|
||||
func parseType(expr ast.Expr) (interface{}, string, error) {
|
||||
if expr == nil {
|
||||
return nil, "", ErrUnSupportType
|
||||
}
|
||||
switch v := expr.(type) {
|
||||
case *ast.StarExpr:
|
||||
star, stringExpr, err := parseType(v.X)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
e := fmt.Sprintf("*%s", stringExpr)
|
||||
return &spec.PointerType{Star: star, StringExpr: e}, e, nil
|
||||
case *ast.Ident:
|
||||
if isBasicType(v.Name) {
|
||||
return &spec.BasicType{Name: v.Name, StringExpr: v.Name}, v.Name, nil
|
||||
} else if v.Obj != nil {
|
||||
obj := v.Obj
|
||||
if obj.Name != v.Name { // 防止引用自己而无限递归
|
||||
specType, err := parseObject(v.Name, v.Obj)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
} else {
|
||||
return specType, v.Obj.Name, nil
|
||||
}
|
||||
} else {
|
||||
inlineType, err := getInlineTypePrefix(obj.Decl)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
return &spec.StructType{
|
||||
StringExpr: fmt.Sprintf("%s%s", inlineType, v.Name),
|
||||
}, v.Name, nil
|
||||
}
|
||||
} else {
|
||||
return nil, "", fmt.Errorf(" [%s] - member is not exist", v.Name)
|
||||
}
|
||||
case *ast.MapType:
|
||||
key, keyStringExpr, err := parseType(v.Key)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
value, valueStringExpr, err := parseType(v.Value)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
keyType, ok := key.(*spec.BasicType)
|
||||
if !ok {
|
||||
return nil, "", fmt.Errorf("[%+v] - unsupport type of map key", v.Key)
|
||||
}
|
||||
e := fmt.Sprintf("map[%s]%s", keyStringExpr, valueStringExpr)
|
||||
return &spec.MapType{
|
||||
Key: keyType.Name,
|
||||
Value: value,
|
||||
StringExpr: e,
|
||||
}, e, nil
|
||||
case *ast.ArrayType:
|
||||
arrayType, stringExpr, err := parseType(v.Elt)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
e := fmt.Sprintf("[]%s", stringExpr)
|
||||
return &spec.ArrayType{ArrayType: arrayType, StringExpr: e}, e, nil
|
||||
case *ast.InterfaceType:
|
||||
return &spec.InterfaceType{StringExpr: interfaceExpr}, interfaceExpr, nil
|
||||
case *ast.ChanType:
|
||||
return nil, "", errors.New("[chan] - unsupport type")
|
||||
case *ast.FuncType:
|
||||
return nil, "", errors.New("[func] - unsupport type")
|
||||
case *ast.StructType: // todo can optimize
|
||||
return nil, "", errors.New("[struct] - unsupport inline struct type")
|
||||
case *ast.SelectorExpr:
|
||||
x := v.X
|
||||
sel := v.Sel
|
||||
xIdent, ok := x.(*ast.Ident)
|
||||
if ok {
|
||||
name := xIdent.Name
|
||||
if name != "time" && sel.Name != "Time" {
|
||||
return nil, "", fmt.Errorf("[outter package] - package:%s, unsupport type", name)
|
||||
}
|
||||
tm := fmt.Sprintf("time.Time")
|
||||
return &spec.TimeType{
|
||||
StringExpr: tm,
|
||||
}, tm, nil
|
||||
}
|
||||
return nil, "", ErrUnSupportType
|
||||
default:
|
||||
return nil, "", ErrUnSupportType
|
||||
}
|
||||
}
|
||||
|
||||
func isBasicType(tp string) bool {
|
||||
switch tp {
|
||||
case
|
||||
"bool",
|
||||
"uint8",
|
||||
"uint16",
|
||||
"uint32",
|
||||
"uint64",
|
||||
"int8",
|
||||
"int16",
|
||||
"int32",
|
||||
"int64",
|
||||
"float32",
|
||||
"float64",
|
||||
"complex64",
|
||||
"complex128",
|
||||
"string",
|
||||
"int",
|
||||
"uint",
|
||||
"uintptr",
|
||||
"byte",
|
||||
"rune",
|
||||
"Type",
|
||||
"Type1",
|
||||
"IntegerType",
|
||||
"FloatType",
|
||||
"ComplexType":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
func parseName(names []*ast.Ident) string {
|
||||
if len(names) == 0 {
|
||||
return ""
|
||||
}
|
||||
name := names[0]
|
||||
return parseIdent(name)
|
||||
}
|
||||
|
||||
func parseIdent(ident *ast.Ident) string {
|
||||
if ident == nil {
|
||||
return ""
|
||||
}
|
||||
return ident.Name
|
||||
}
|
||||
|
||||
func parseCommentOrDoc(cg *ast.CommentGroup) []string {
|
||||
if cg == nil {
|
||||
return nil
|
||||
}
|
||||
comments := make([]string, 0)
|
||||
for _, comment := range cg.List {
|
||||
if comment == nil {
|
||||
continue
|
||||
}
|
||||
text := strings.TrimSpace(comment.Text)
|
||||
if text == "" {
|
||||
continue
|
||||
}
|
||||
comments = append(comments, text)
|
||||
}
|
||||
return comments
|
||||
}
|
||||
95
tools/goctl/api/parser/typestate.go
Normal file
95
tools/goctl/api/parser/typestate.go
Normal file
@@ -0,0 +1,95 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"zero/tools/goctl/api/spec"
|
||||
"zero/tools/goctl/util"
|
||||
)
|
||||
|
||||
type typeState struct {
|
||||
*baseState
|
||||
annos []spec.Annotation
|
||||
}
|
||||
|
||||
func newTypeState(state *baseState, annos []spec.Annotation) state {
|
||||
return &typeState{
|
||||
baseState: state,
|
||||
annos: annos,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *typeState) process(api *spec.ApiSpec) (state, error) {
|
||||
var name string
|
||||
var members []spec.Member
|
||||
parser := &typeEntityParser{
|
||||
acceptName: func(n string) {
|
||||
name = n
|
||||
},
|
||||
acceptMember: func(member spec.Member) {
|
||||
members = append(members, member)
|
||||
},
|
||||
}
|
||||
ent := newEntity(s.baseState, api, parser)
|
||||
if err := ent.process(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
api.Types = append(api.Types, spec.Type{
|
||||
Name: name,
|
||||
Annotations: s.annos,
|
||||
Members: members,
|
||||
})
|
||||
|
||||
return newRootState(s.r, s.lineNumber), nil
|
||||
}
|
||||
|
||||
type typeEntityParser struct {
|
||||
acceptName func(name string)
|
||||
acceptMember func(member spec.Member)
|
||||
}
|
||||
|
||||
func (p *typeEntityParser) parseLine(line string, api *spec.ApiSpec, annos []spec.Annotation) error {
|
||||
index := strings.Index(line, "//")
|
||||
comment := ""
|
||||
if index >= 0 {
|
||||
comment = line[index+2:]
|
||||
line = strings.TrimSpace(line[:index])
|
||||
}
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) == 0 {
|
||||
return nil
|
||||
}
|
||||
if len(fields) == 1 {
|
||||
p.acceptMember(spec.Member{
|
||||
Annotations: annos,
|
||||
Name: fields[0],
|
||||
Type: fields[0],
|
||||
IsInline: true,
|
||||
})
|
||||
return nil
|
||||
}
|
||||
name := fields[0]
|
||||
tp := fields[1]
|
||||
var tag string
|
||||
if len(fields) > 2 {
|
||||
tag = fields[2]
|
||||
} else {
|
||||
tag = fmt.Sprintf("`json:\"%s\"`", util.Untitle(name))
|
||||
}
|
||||
|
||||
p.acceptMember(spec.Member{
|
||||
Annotations: annos,
|
||||
Name: name,
|
||||
Type: tp,
|
||||
Tag: tag,
|
||||
Comment: comment,
|
||||
IsInline: false,
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *typeEntityParser) setEntityName(name string) {
|
||||
p.acceptName(name)
|
||||
}
|
||||
103
tools/goctl/api/parser/util.go
Normal file
103
tools/goctl/api/parser/util.go
Normal file
@@ -0,0 +1,103 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"zero/tools/goctl/api/spec"
|
||||
)
|
||||
|
||||
const (
|
||||
// struct匹配
|
||||
typeRegex = `(?m)(?m)(^ *type\s+[a-zA-Z][a-zA-Z0-9_-]+\s+(((struct)\s*?\{[\w\W]*?[^\{]\})|([a-zA-Z][a-zA-Z0-9_-]+)))|(^ *type\s*?\([\w\W]+\}\s*\))`
|
||||
)
|
||||
|
||||
var (
|
||||
emptyStrcut = errors.New("struct body not found")
|
||||
)
|
||||
|
||||
var emptyType spec.Type
|
||||
|
||||
func GetType(api *spec.ApiSpec, t string) spec.Type {
|
||||
for _, tp := range api.Types {
|
||||
if tp.Name == t {
|
||||
return tp
|
||||
}
|
||||
}
|
||||
|
||||
return emptyType
|
||||
}
|
||||
|
||||
func isLetterDigit(r rune) bool {
|
||||
return (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || ('0' <= r && r <= '9')
|
||||
}
|
||||
|
||||
func isSpace(r rune) bool {
|
||||
return r == ' ' || r == '\t'
|
||||
}
|
||||
|
||||
func isNewline(r rune) bool {
|
||||
return r == '\n' || r == '\r'
|
||||
}
|
||||
|
||||
func read(r *bufio.Reader) (rune, error) {
|
||||
ch, _, err := r.ReadRune()
|
||||
return ch, err
|
||||
}
|
||||
|
||||
func readLine(r *bufio.Reader) (string, error) {
|
||||
line, _, err := r.ReadLine()
|
||||
if err != nil {
|
||||
return "", err
|
||||
} else {
|
||||
return string(line), nil
|
||||
}
|
||||
}
|
||||
|
||||
func skipSpaces(r *bufio.Reader) error {
|
||||
for {
|
||||
next, err := read(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !isSpace(next) {
|
||||
return unread(r)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func unread(r *bufio.Reader) error {
|
||||
return r.UnreadRune()
|
||||
}
|
||||
|
||||
func MatchStruct(api string) (info, structBody, service string, err error) {
|
||||
r := regexp.MustCompile(typeRegex)
|
||||
indexes := r.FindAllStringIndex(api, -1)
|
||||
if len(indexes) == 0 {
|
||||
return "", "", "", emptyStrcut
|
||||
}
|
||||
startIndexes := indexes[0]
|
||||
endIndexes := indexes[len(indexes)-1]
|
||||
|
||||
info = api[:startIndexes[0]]
|
||||
structBody = api[startIndexes[0]:endIndexes[len(endIndexes)-1]]
|
||||
service = api[endIndexes[len(endIndexes)-1]:]
|
||||
|
||||
firstIIndex := strings.Index(info, "i")
|
||||
if firstIIndex > 0 {
|
||||
info = info[firstIIndex:]
|
||||
}
|
||||
|
||||
lastServiceRightBraceIndex := strings.LastIndex(service, "}") + 1
|
||||
var firstServiceIndex int
|
||||
for index, char := range service {
|
||||
if !isSpace(char) && !isNewline(char) {
|
||||
firstServiceIndex = index
|
||||
break
|
||||
}
|
||||
}
|
||||
service = service[firstServiceIndex:lastServiceRightBraceIndex]
|
||||
return
|
||||
}
|
||||
54
tools/goctl/api/parser/validator.go
Normal file
54
tools/goctl/api/parser/validator.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"zero/core/stringx"
|
||||
"zero/tools/goctl/api/spec"
|
||||
"zero/tools/goctl/api/util"
|
||||
)
|
||||
|
||||
func (p *Parser) validate(api *spec.ApiSpec) (err error) {
|
||||
var builder strings.Builder
|
||||
for _, tp := range api.Types {
|
||||
if ok, name := p.validateDuplicateProperty(tp); !ok {
|
||||
fmt.Fprintf(&builder, `duplicate property "%s" of type "%s"`+"\n", name, tp.Name)
|
||||
}
|
||||
}
|
||||
if ok, info := p.validateDuplicateRouteHandler(api); !ok {
|
||||
fmt.Fprintf(&builder, info)
|
||||
}
|
||||
if len(builder.String()) > 0 {
|
||||
return errors.New(builder.String())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Parser) validateDuplicateProperty(tp spec.Type) (bool, string) {
|
||||
var names []string
|
||||
for _, member := range tp.Members {
|
||||
if stringx.Contains(names, member.Name) {
|
||||
return false, member.Name
|
||||
} else {
|
||||
names = append(names, member.Name)
|
||||
}
|
||||
}
|
||||
return true, ""
|
||||
}
|
||||
|
||||
func (p *Parser) validateDuplicateRouteHandler(api *spec.ApiSpec) (bool, string) {
|
||||
var names []string
|
||||
for _, r := range api.Service.Routes {
|
||||
handler, ok := util.GetAnnotationValue(r.Annotations, "server", "handler")
|
||||
if !ok {
|
||||
return false, fmt.Sprintf("missing handler annotation for %s", r.Path)
|
||||
}
|
||||
if stringx.Contains(names, handler) {
|
||||
return false, fmt.Sprintf(`duplicated handler for name "%s"`, handler)
|
||||
} else {
|
||||
names = append(names, handler)
|
||||
}
|
||||
}
|
||||
return true, ""
|
||||
}
|
||||
16
tools/goctl/api/parser/vars.go
Normal file
16
tools/goctl/api/parser/vars.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package parser
|
||||
|
||||
const (
|
||||
infoDirective = "info"
|
||||
serviceDirective = "service"
|
||||
typeDirective = "type"
|
||||
typeStruct = "struct"
|
||||
at = '@'
|
||||
colon = ':'
|
||||
leftParenthesis = '('
|
||||
rightParenthesis = ')'
|
||||
leftBrace = "{"
|
||||
rightBrace = '}'
|
||||
multilineBeginTag = '>'
|
||||
multilineEndTag = '<'
|
||||
)
|
||||
143
tools/goctl/api/spec/fn.go
Normal file
143
tools/goctl/api/spec/fn.go
Normal file
@@ -0,0 +1,143 @@
|
||||
package spec
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"zero/core/stringx"
|
||||
"zero/tools/goctl/util"
|
||||
)
|
||||
|
||||
const (
|
||||
TagKey = "tag"
|
||||
NameKey = "name"
|
||||
OptionKey = "option"
|
||||
BodyTag = "json"
|
||||
)
|
||||
|
||||
var (
|
||||
TagRe = regexp.MustCompile(`(?P<tag>\w+):"(?P<name>[^,"]+)[,]?(?P<option>[^"]*)"`)
|
||||
TagSubNames = TagRe.SubexpNames()
|
||||
definedTags = []string{TagKey, NameKey, OptionKey}
|
||||
)
|
||||
|
||||
type Attribute struct {
|
||||
Key string
|
||||
value string
|
||||
}
|
||||
|
||||
func (m Member) IsOptional() bool {
|
||||
var option string
|
||||
|
||||
matches := TagRe.FindStringSubmatch(m.Tag)
|
||||
for i := range matches {
|
||||
name := TagSubNames[i]
|
||||
if name == OptionKey {
|
||||
option = matches[i]
|
||||
}
|
||||
}
|
||||
|
||||
if len(option) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
fields := strings.Split(option, ",")
|
||||
for _, field := range fields {
|
||||
if field == "optional" || strings.HasPrefix(field, "default=") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (m Member) IsOmitempty() bool {
|
||||
var option string
|
||||
|
||||
matches := TagRe.FindStringSubmatch(m.Tag)
|
||||
for i := range matches {
|
||||
name := TagSubNames[i]
|
||||
if name == OptionKey {
|
||||
option = matches[i]
|
||||
}
|
||||
}
|
||||
|
||||
if len(option) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
fields := strings.Split(option, ",")
|
||||
for _, field := range fields {
|
||||
if field == "omitempty" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (m Member) GetAttributes() []Attribute {
|
||||
matches := TagRe.FindStringSubmatch(m.Tag)
|
||||
var result []Attribute
|
||||
for i := range matches {
|
||||
name := TagSubNames[i]
|
||||
if stringx.Contains(definedTags, name) {
|
||||
result = append(result, Attribute{
|
||||
Key: name,
|
||||
value: matches[i],
|
||||
})
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (m Member) GetPropertyName() (string, error) {
|
||||
attrs := m.GetAttributes()
|
||||
for _, attr := range attrs {
|
||||
if attr.Key == NameKey && len(attr.value) > 0 {
|
||||
if attr.value == "-" {
|
||||
return util.Untitle(m.Name), nil
|
||||
}
|
||||
return attr.value, nil
|
||||
}
|
||||
}
|
||||
return "", errors.New("json property name not exist, member: " + m.Name)
|
||||
}
|
||||
|
||||
func (m Member) GetComment() string {
|
||||
return strings.TrimSpace(strings.Join(m.Comments, "; "))
|
||||
}
|
||||
|
||||
func (m Member) IsBodyMember() bool {
|
||||
if m.IsInline {
|
||||
return true
|
||||
}
|
||||
attrs := m.GetAttributes()
|
||||
for _, attr := range attrs {
|
||||
if attr.value == BodyTag {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (t Type) GetBodyMembers() []Member {
|
||||
var result []Member
|
||||
for _, member := range t.Members {
|
||||
if member.IsBodyMember() {
|
||||
result = append(result, member)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (t Type) GetNonBodyMembers() []Member {
|
||||
var result []Member
|
||||
for _, member := range t.Members {
|
||||
if !member.IsBodyMember() {
|
||||
result = append(result, member)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
131
tools/goctl/api/spec/spec.go
Normal file
131
tools/goctl/api/spec/spec.go
Normal file
@@ -0,0 +1,131 @@
|
||||
package spec
|
||||
|
||||
type (
|
||||
Annotation struct {
|
||||
Name string
|
||||
Properties map[string]string
|
||||
}
|
||||
|
||||
ApiSpec struct {
|
||||
Info Info
|
||||
Types []Type
|
||||
Service Service
|
||||
}
|
||||
|
||||
Group struct {
|
||||
Annotations []Annotation
|
||||
Routes []Route
|
||||
}
|
||||
|
||||
Info struct {
|
||||
Title string
|
||||
Desc string
|
||||
Version string
|
||||
Author string
|
||||
Email string
|
||||
}
|
||||
|
||||
Member struct {
|
||||
Annotations []Annotation
|
||||
Name string
|
||||
// 数据类型字面值,如:string、map[int]string、[]int64、[]*User
|
||||
Type string
|
||||
// it can be asserted as BasicType: int、bool、
|
||||
// PointerType: *string、*User、
|
||||
// MapType: map[${BasicType}]interface、
|
||||
// ArrayType:[]int、[]User、[]*User
|
||||
// InterfaceType: interface{}
|
||||
// Type
|
||||
Expr interface{}
|
||||
Tag string
|
||||
// Deprecated
|
||||
Comment string // 换成标准struct中将废弃
|
||||
// 成员尾部注释说明
|
||||
Comments []string
|
||||
// 成员头顶注释说明
|
||||
Docs []string
|
||||
IsInline bool
|
||||
}
|
||||
|
||||
Route struct {
|
||||
Annotations []Annotation
|
||||
Method string
|
||||
Path string
|
||||
RequestType Type
|
||||
ResponseType Type
|
||||
}
|
||||
|
||||
Service struct {
|
||||
Name string
|
||||
Annotations []Annotation
|
||||
Routes []Route
|
||||
Groups []Group
|
||||
}
|
||||
|
||||
Type struct {
|
||||
Name string
|
||||
Annotations []Annotation
|
||||
Members []Member
|
||||
}
|
||||
|
||||
// 系统预设基本数据类型
|
||||
BasicType struct {
|
||||
StringExpr string
|
||||
Name string
|
||||
}
|
||||
PointerType struct {
|
||||
StringExpr string
|
||||
// it can be asserted as BasicType: int、bool、
|
||||
// PointerType: *string、*User、
|
||||
// MapType: map[${BasicType}]interface、
|
||||
// ArrayType:[]int、[]User、[]*User
|
||||
// InterfaceType: interface{}
|
||||
// Type
|
||||
Star interface{}
|
||||
}
|
||||
|
||||
MapType struct {
|
||||
StringExpr string
|
||||
// only support the BasicType
|
||||
Key string
|
||||
// it can be asserted as BasicType: int、bool、
|
||||
// PointerType: *string、*User、
|
||||
// MapType: map[${BasicType}]interface、
|
||||
// ArrayType:[]int、[]User、[]*User
|
||||
// InterfaceType: interface{}
|
||||
// Type
|
||||
Value interface{}
|
||||
}
|
||||
ArrayType struct {
|
||||
StringExpr string
|
||||
// it can be asserted as BasicType: int、bool、
|
||||
// PointerType: *string、*User、
|
||||
// MapType: map[${BasicType}]interface、
|
||||
// ArrayType:[]int、[]User、[]*User
|
||||
// InterfaceType: interface{}
|
||||
// Type
|
||||
ArrayType interface{}
|
||||
}
|
||||
InterfaceType struct {
|
||||
StringExpr string
|
||||
// do nothing,just for assert
|
||||
}
|
||||
TimeType struct {
|
||||
StringExpr string
|
||||
}
|
||||
StructType struct {
|
||||
StringExpr string
|
||||
}
|
||||
)
|
||||
|
||||
func (spec *ApiSpec) ContainsTime() bool {
|
||||
for _, item := range spec.Types {
|
||||
members := item.Members
|
||||
for _, member := range members {
|
||||
if _, ok := member.Expr.(*TimeType); ok {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
44
tools/goctl/api/tsgen/gen.go
Normal file
44
tools/goctl/api/tsgen/gen.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package tsgen
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"zero/core/lang"
|
||||
"zero/tools/goctl/api/parser"
|
||||
"zero/tools/goctl/util"
|
||||
|
||||
"github.com/logrusorgru/aurora"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
func TsCommand(c *cli.Context) error {
|
||||
apiFile := c.String("api")
|
||||
dir := c.String("dir")
|
||||
webApi := c.String("webapi")
|
||||
caller := c.String("caller")
|
||||
unwrapApi := c.Bool("unwrap")
|
||||
if len(apiFile) == 0 {
|
||||
return errors.New("missing -api")
|
||||
}
|
||||
if len(dir) == 0 {
|
||||
return errors.New("missing -dir")
|
||||
}
|
||||
|
||||
p, err := parser.NewParser(apiFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
api, err := p.Parse()
|
||||
if err != nil {
|
||||
fmt.Println(aurora.Red("Failed"))
|
||||
return err
|
||||
}
|
||||
|
||||
lang.Must(util.MkdirIfNotExist(dir))
|
||||
lang.Must(genHandler(dir, webApi, caller, api, unwrapApi))
|
||||
lang.Must(genComponents(dir, api))
|
||||
|
||||
fmt.Println(aurora.Green("Done."))
|
||||
return nil
|
||||
}
|
||||
79
tools/goctl/api/tsgen/gencomponents.go
Normal file
79
tools/goctl/api/tsgen/gencomponents.go
Normal file
@@ -0,0 +1,79 @@
|
||||
package tsgen
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"path"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"zero/tools/goctl/api/spec"
|
||||
apiutil "zero/tools/goctl/api/util"
|
||||
"zero/tools/goctl/util"
|
||||
)
|
||||
|
||||
const (
|
||||
componentsTemplate = `// DO NOT EDIT, generated by goctl
|
||||
|
||||
{{.componentTypes}}
|
||||
`
|
||||
)
|
||||
|
||||
func genComponents(dir string, api *spec.ApiSpec) error {
|
||||
types := apiutil.GetSharedTypes(api)
|
||||
if len(types) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
val, err := buildTypes(types, func(name string) (*spec.Type, error) {
|
||||
for _, ty := range api.Types {
|
||||
if strings.ToLower(ty.Name) == strings.ToLower(name) {
|
||||
return &ty, nil
|
||||
}
|
||||
}
|
||||
return nil, errors.New("inline type " + name + " not exist, please correct api file")
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
outputFile := apiutil.ComponentName(api) + ".ts"
|
||||
filename := path.Join(dir, outputFile)
|
||||
if err := util.RemoveIfExist(filename); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fp, created, err := apiutil.MaybeCreateFile(dir, ".", outputFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !created {
|
||||
return nil
|
||||
}
|
||||
defer fp.Close()
|
||||
|
||||
t := template.Must(template.New("componentsTemplate").Parse(componentsTemplate))
|
||||
return t.Execute(fp, map[string]string{
|
||||
"componentTypes": val,
|
||||
})
|
||||
}
|
||||
|
||||
func buildTypes(types []spec.Type, inlineType func(string) (*spec.Type, error)) (string, error) {
|
||||
var builder strings.Builder
|
||||
first := true
|
||||
for _, tp := range types {
|
||||
if first {
|
||||
first = false
|
||||
} else {
|
||||
builder.WriteString("\n")
|
||||
}
|
||||
if err := writeType(&builder, tp, func(name string) (*spec.Type, error) {
|
||||
return inlineType(name)
|
||||
}, func(tp string) string {
|
||||
return ""
|
||||
}); err != nil {
|
||||
return "", apiutil.WrapErr(err, "Type "+tp.Name+" generate error")
|
||||
}
|
||||
}
|
||||
|
||||
return builder.String(), nil
|
||||
}
|
||||
214
tools/goctl/api/tsgen/genpacket.go
Normal file
214
tools/goctl/api/tsgen/genpacket.go
Normal file
@@ -0,0 +1,214 @@
|
||||
package tsgen
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"path"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"zero/tools/goctl/api/spec"
|
||||
apiutil "zero/tools/goctl/api/util"
|
||||
"zero/tools/goctl/util"
|
||||
)
|
||||
|
||||
const (
|
||||
handlerTemplate = `{{.imports}}
|
||||
|
||||
{{.types}}
|
||||
|
||||
{{.apis}}
|
||||
`
|
||||
)
|
||||
|
||||
func genHandler(dir, webApi, caller string, api *spec.ApiSpec, unwrapApi bool) error {
|
||||
filename := strings.Replace(api.Service.Name, "-api", "", 1) + ".ts"
|
||||
if err := util.RemoveIfExist(path.Join(dir, filename)); err != nil {
|
||||
return err
|
||||
}
|
||||
fp, created, err := apiutil.MaybeCreateFile(dir, "", filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !created {
|
||||
return nil
|
||||
}
|
||||
defer fp.Close()
|
||||
|
||||
var localTypes []spec.Type
|
||||
for _, route := range api.Service.Routes {
|
||||
rts := apiutil.GetLocalTypes(api, route)
|
||||
localTypes = append(localTypes, rts...)
|
||||
}
|
||||
|
||||
var prefixForType = func(ty string) string {
|
||||
if _, pri := primitiveType(ty); pri {
|
||||
return ""
|
||||
}
|
||||
for _, item := range localTypes {
|
||||
if util.Title(item.Name) == ty {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
return packagePrefix
|
||||
}
|
||||
|
||||
types, err := genTypes(localTypes, func(name string) (*spec.Type, error) {
|
||||
for _, ty := range api.Types {
|
||||
if strings.ToLower(ty.Name) == strings.ToLower(name) {
|
||||
return &ty, nil
|
||||
}
|
||||
}
|
||||
return nil, errors.New("inline type " + name + " not exist, please correct api file")
|
||||
}, prefixForType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
imports := ""
|
||||
if len(caller) == 0 {
|
||||
caller = "webapi"
|
||||
}
|
||||
importCaller := caller
|
||||
if unwrapApi {
|
||||
importCaller = "{ " + importCaller + " }"
|
||||
}
|
||||
if len(webApi) > 0 {
|
||||
imports += `import ` + importCaller + ` from ` + "\"" + webApi + "\""
|
||||
}
|
||||
shardTypes := apiutil.GetSharedTypes(api)
|
||||
if len(shardTypes) != 0 {
|
||||
if len(imports) > 0 {
|
||||
imports += "\n"
|
||||
}
|
||||
outputFile := apiutil.ComponentName(api)
|
||||
imports += fmt.Sprintf(`import * as components from "%s"`, "./"+outputFile)
|
||||
}
|
||||
|
||||
apis, err := genApi(api, localTypes, caller, prefixForType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
t := template.Must(template.New("handlerTemplate").Parse(handlerTemplate))
|
||||
return t.Execute(fp, map[string]string{
|
||||
"webApi": webApi,
|
||||
"types": strings.TrimSpace(types),
|
||||
"imports": imports,
|
||||
"apis": strings.TrimSpace(apis),
|
||||
})
|
||||
}
|
||||
|
||||
func genTypes(localTypes []spec.Type, inlineType func(string) (*spec.Type, error), prefixForType func(string) string) (string, error) {
|
||||
var builder strings.Builder
|
||||
var first bool
|
||||
|
||||
for _, tp := range localTypes {
|
||||
if first {
|
||||
first = false
|
||||
} else {
|
||||
fmt.Fprintln(&builder)
|
||||
}
|
||||
if err := writeType(&builder, tp, func(name string) (s *spec.Type, err error) {
|
||||
return inlineType(name)
|
||||
}, prefixForType); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
types := builder.String()
|
||||
return types, nil
|
||||
}
|
||||
|
||||
func genApi(api *spec.ApiSpec, localTypes []spec.Type, caller string, prefixForType func(string) string) (string, error) {
|
||||
var builder strings.Builder
|
||||
for _, route := range api.Service.Routes {
|
||||
handler, ok := apiutil.GetAnnotationValue(route.Annotations, "server", "handler")
|
||||
if !ok {
|
||||
return "", fmt.Errorf("missing handler annotation for route %q", route.Path)
|
||||
}
|
||||
handler = util.Untitle(handler)
|
||||
handler = strings.Replace(handler, "Handler", "", 1)
|
||||
comment := commentForRoute(route)
|
||||
if len(comment) > 0 {
|
||||
fmt.Fprintf(&builder, "%s\n", comment)
|
||||
}
|
||||
fmt.Fprintf(&builder, "export function %s(%s) {\n", handler, paramsForRoute(route, prefixForType))
|
||||
writeIndent(&builder, 1)
|
||||
responseGeneric := "<null>"
|
||||
if len(route.ResponseType.Name) > 0 {
|
||||
val, err := goTypeToTs(route.ResponseType.Name, prefixForType)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
responseGeneric = fmt.Sprintf("<%s>", val)
|
||||
}
|
||||
fmt.Fprintf(&builder, `return %s.%s%s(%s)`, caller, strings.ToLower(route.Method),
|
||||
util.Title(responseGeneric), callParamsForRoute(route))
|
||||
builder.WriteString("\n}\n\n")
|
||||
}
|
||||
|
||||
apis := builder.String()
|
||||
return apis, nil
|
||||
}
|
||||
|
||||
func paramsForRoute(route spec.Route, prefixForType func(string) string) string {
|
||||
hasParams := pathHasParams(route)
|
||||
hasBody := hasRequestBody(route)
|
||||
rt, err := goTypeToTs(route.RequestType.Name, prefixForType)
|
||||
if err != nil {
|
||||
println(err.Error())
|
||||
return ""
|
||||
}
|
||||
if hasParams && hasBody {
|
||||
return fmt.Sprintf("params: %s, req: %s", rt+"Params", rt)
|
||||
} else if hasParams {
|
||||
return fmt.Sprintf("params: %s", rt+"Params")
|
||||
} else if hasBody {
|
||||
return fmt.Sprintf("req: %s", rt)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func commentForRoute(route spec.Route) string {
|
||||
var builder strings.Builder
|
||||
comment, _ := apiutil.GetAnnotationValue(route.Annotations, "doc", "summary")
|
||||
builder.WriteString("/**")
|
||||
builder.WriteString("\n * @description " + comment)
|
||||
hasParams := pathHasParams(route)
|
||||
hasBody := hasRequestBody(route)
|
||||
if hasParams && hasBody {
|
||||
builder.WriteString("\n * @param params")
|
||||
builder.WriteString("\n * @param req")
|
||||
} else if hasParams {
|
||||
builder.WriteString("\n * @param params")
|
||||
} else if hasBody {
|
||||
builder.WriteString("\n * @param req")
|
||||
}
|
||||
builder.WriteString("\n */")
|
||||
return builder.String()
|
||||
}
|
||||
|
||||
func callParamsForRoute(route spec.Route) string {
|
||||
hasParams := pathHasParams(route)
|
||||
hasBody := hasRequestBody(route)
|
||||
if hasParams && hasBody {
|
||||
return fmt.Sprintf("%s, %s, %s", pathForRoute(route), "params", "req")
|
||||
} else if hasParams {
|
||||
return fmt.Sprintf("%s, %s", pathForRoute(route), "params")
|
||||
} else if hasBody {
|
||||
return fmt.Sprintf("%s, %s", pathForRoute(route), "req")
|
||||
}
|
||||
return pathForRoute(route)
|
||||
}
|
||||
|
||||
func pathForRoute(route spec.Route) string {
|
||||
return "\"" + route.Path + "\""
|
||||
}
|
||||
|
||||
func pathHasParams(route spec.Route) bool {
|
||||
return len(route.RequestType.Members) != len(route.RequestType.GetBodyMembers())
|
||||
}
|
||||
|
||||
func hasRequestBody(route spec.Route) bool {
|
||||
return len(route.RequestType.Name) > 0 && len(route.RequestType.GetBodyMembers()) > 0
|
||||
}
|
||||
167
tools/goctl/api/tsgen/util.go
Normal file
167
tools/goctl/api/tsgen/util.go
Normal file
@@ -0,0 +1,167 @@
|
||||
package tsgen
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"zero/tools/goctl/api/spec"
|
||||
apiutil "zero/tools/goctl/api/util"
|
||||
"zero/tools/goctl/util"
|
||||
)
|
||||
|
||||
func writeProperty(writer io.Writer, member spec.Member, indent int, prefixForType func(string) string) error {
|
||||
writeIndent(writer, indent)
|
||||
ty, err := goTypeToTs(member.Type, prefixForType)
|
||||
optionalTag := ""
|
||||
if member.IsOptional() || member.IsOmitempty() {
|
||||
optionalTag = "?"
|
||||
}
|
||||
name, err := member.GetPropertyName()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
comment := member.GetComment()
|
||||
if len(comment) > 0 {
|
||||
comment = strings.TrimPrefix(comment, "//")
|
||||
comment = " // " + strings.TrimSpace(comment)
|
||||
}
|
||||
if len(member.Docs) > 0 {
|
||||
_, err = fmt.Fprintf(writer, "%s\n", strings.Join(member.Docs, ""))
|
||||
writeIndent(writer, 1)
|
||||
}
|
||||
_, err = fmt.Fprintf(writer, "%s%s: %s%s\n", name, optionalTag, ty, comment)
|
||||
return err
|
||||
}
|
||||
|
||||
func writeIndent(writer io.Writer, indent int) {
|
||||
for i := 0; i < indent; i++ {
|
||||
fmt.Fprint(writer, "\t")
|
||||
}
|
||||
}
|
||||
|
||||
func goTypeToTs(tp string, prefixForType func(string) string) (string, error) {
|
||||
if val, pri := primitiveType(tp); pri {
|
||||
return val, nil
|
||||
}
|
||||
if tp == "[]byte" {
|
||||
return "Blob", nil
|
||||
} else if strings.HasPrefix(tp, "[][]") {
|
||||
tys, err := apiutil.DecomposeType(tp)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if len(tys) == 0 {
|
||||
return "", fmt.Errorf("%s tp parse error", tp)
|
||||
}
|
||||
innerType, err := goTypeToTs(tys[0], prefixForType)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return fmt.Sprintf("Array<Array<%s>>", innerType), nil
|
||||
} else if strings.HasPrefix(tp, "[]") {
|
||||
tys, err := apiutil.DecomposeType(tp)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if len(tys) == 0 {
|
||||
return "", fmt.Errorf("%s tp parse error", tp)
|
||||
}
|
||||
innerType, err := goTypeToTs(tys[0], prefixForType)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return fmt.Sprintf("Array<%s>", innerType), nil
|
||||
} else if strings.HasPrefix(tp, "map") {
|
||||
tys, err := apiutil.DecomposeType(tp)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if len(tys) != 2 {
|
||||
return "", fmt.Errorf("%s tp parse error", tp)
|
||||
}
|
||||
innerType, err := goTypeToTs(tys[1], prefixForType)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return fmt.Sprintf("{ [key: string]: %s }", innerType), nil
|
||||
}
|
||||
return addPrefixIfNeed(util.Title(tp), prefixForType), nil
|
||||
}
|
||||
|
||||
func addPrefixIfNeed(tp string, prefixForType func(string) string) string {
|
||||
if val, pri := primitiveType(tp); pri {
|
||||
return val
|
||||
}
|
||||
tp = strings.Replace(tp, "*", "", 1)
|
||||
return prefixForType(tp) + util.Title(tp)
|
||||
}
|
||||
|
||||
func primitiveType(tp string) (string, bool) {
|
||||
switch tp {
|
||||
case "string":
|
||||
return "string", true
|
||||
case "int", "int8", "int32", "int64":
|
||||
return "number", true
|
||||
case "float", "float32", "float64":
|
||||
return "number", true
|
||||
case "bool":
|
||||
return "boolean", true
|
||||
case "[]byte":
|
||||
return "Blob", true
|
||||
case "interface{}":
|
||||
return "any", true
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
func writeType(writer io.Writer, tp spec.Type, inlineType func(string) (*spec.Type, error), prefixForType func(string) string) error {
|
||||
fmt.Fprintf(writer, "export interface %s {\n", util.Title(tp.Name))
|
||||
if err := genMembers(writer, tp, false, inlineType, prefixForType); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintf(writer, "}\n")
|
||||
err := genParamsTypesIfNeed(writer, tp, inlineType, prefixForType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func genParamsTypesIfNeed(writer io.Writer, tp spec.Type, inlineType func(string) (*spec.Type, error), prefixForType func(string) string) error {
|
||||
members := tp.GetNonBodyMembers()
|
||||
if len(members) == 0 {
|
||||
return nil
|
||||
}
|
||||
fmt.Fprintf(writer, "\n")
|
||||
fmt.Fprintf(writer, "export interface %sParams {\n", util.Title(tp.Name))
|
||||
if err := genMembers(writer, tp, true, inlineType, prefixForType); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintf(writer, "}\n")
|
||||
return nil
|
||||
}
|
||||
|
||||
func genMembers(writer io.Writer, tp spec.Type, isParam bool, inlineType func(string) (*spec.Type, error), prefixForType func(string) string) error {
|
||||
members := tp.GetBodyMembers()
|
||||
if isParam {
|
||||
members = tp.GetNonBodyMembers()
|
||||
}
|
||||
for _, member := range members {
|
||||
if member.IsInline {
|
||||
// 获取inline类型的成员然后添加到type中
|
||||
it, err := inlineType(strings.TrimPrefix(member.Type, "*"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := genMembers(writer, *it, isParam, inlineType, prefixForType); err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
if err := writeProperty(writer, member, 1, prefixForType); err != nil {
|
||||
return apiutil.WrapErr(err, " type "+tp.Name)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
5
tools/goctl/api/tsgen/vars.go
Normal file
5
tools/goctl/api/tsgen/vars.go
Normal file
@@ -0,0 +1,5 @@
|
||||
package tsgen
|
||||
|
||||
const (
|
||||
packagePrefix = "components."
|
||||
)
|
||||
13
tools/goctl/api/util/annotation.go
Normal file
13
tools/goctl/api/util/annotation.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package util
|
||||
|
||||
import "zero/tools/goctl/api/spec"
|
||||
|
||||
func GetAnnotationValue(annos []spec.Annotation, key, field string) (string, bool) {
|
||||
for _, anno := range annos {
|
||||
if anno.Name == key {
|
||||
value, ok := anno.Properties[field]
|
||||
return value, ok
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
107
tools/goctl/api/util/case.go
Normal file
107
tools/goctl/api/util/case.go
Normal file
@@ -0,0 +1,107 @@
|
||||
package util
|
||||
|
||||
func IsUpperCase(r rune) bool {
|
||||
if r >= 'A' && r <= 'Z' {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func IsLowerCase(r rune) bool {
|
||||
if r >= 'a' && r <= 'z' {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func ToSnakeCase(s string) string {
|
||||
out := []rune{}
|
||||
for index, r := range s {
|
||||
if index == 0 {
|
||||
out = append(out, ToLowerCase(r))
|
||||
continue
|
||||
}
|
||||
|
||||
if IsUpperCase(r) && index != 0 {
|
||||
if IsLowerCase(rune(s[index-1])) {
|
||||
out = append(out, '_', ToLowerCase(r))
|
||||
continue
|
||||
}
|
||||
if index < len(s)-1 && IsLowerCase(rune(s[index+1])) {
|
||||
out = append(out, '_', ToLowerCase(r))
|
||||
continue
|
||||
}
|
||||
out = append(out, ToLowerCase(r))
|
||||
continue
|
||||
}
|
||||
out = append(out, r)
|
||||
}
|
||||
return string(out)
|
||||
}
|
||||
|
||||
func ToCamelCase(s string) string {
|
||||
s = ToLower(s)
|
||||
out := []rune{}
|
||||
for index, r := range s {
|
||||
if r == '_' {
|
||||
continue
|
||||
}
|
||||
if index == 0 {
|
||||
out = append(out, ToUpperCase(r))
|
||||
continue
|
||||
}
|
||||
|
||||
if index > 0 && s[index-1] == '_' {
|
||||
out = append(out, ToUpperCase(r))
|
||||
continue
|
||||
}
|
||||
|
||||
out = append(out, r)
|
||||
}
|
||||
return string(out)
|
||||
}
|
||||
|
||||
func ToLowerCase(r rune) rune {
|
||||
dx := 'A' - 'a'
|
||||
if IsUpperCase(r) {
|
||||
return r - dx
|
||||
}
|
||||
return r
|
||||
}
|
||||
func ToUpperCase(r rune) rune {
|
||||
dx := 'A' - 'a'
|
||||
if IsLowerCase(r) {
|
||||
return r + dx
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
func ToLower(s string) string {
|
||||
out := []rune{}
|
||||
for _, r := range s {
|
||||
out = append(out, ToLowerCase(r))
|
||||
}
|
||||
return string(out)
|
||||
}
|
||||
|
||||
func ToUpper(s string) string {
|
||||
out := []rune{}
|
||||
for _, r := range s {
|
||||
out = append(out, ToUpperCase(r))
|
||||
}
|
||||
return string(out)
|
||||
}
|
||||
|
||||
func LowerFirst(s string) string {
|
||||
if len(s) == 0 {
|
||||
return s
|
||||
}
|
||||
return ToLower(s[:1]) + s[1:]
|
||||
}
|
||||
|
||||
func UpperFirst(s string) string {
|
||||
if len(s) == 0 {
|
||||
return s
|
||||
}
|
||||
return ToUpper(s[:1]) + s[1:]
|
||||
}
|
||||
58
tools/goctl/api/util/tag.go
Normal file
58
tools/goctl/api/util/tag.go
Normal file
@@ -0,0 +1,58 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func TagLookup(tag, key string) (value string, ok bool) {
|
||||
tag = strings.Replace(tag, "`", "", -1)
|
||||
for tag != "" {
|
||||
// Skip leading space.
|
||||
i := 0
|
||||
for i < len(tag) && tag[i] == ' ' {
|
||||
i++
|
||||
}
|
||||
tag = tag[i:]
|
||||
if tag == "" {
|
||||
break
|
||||
}
|
||||
|
||||
// Scan to colon. A space, a quote or a control character is a syntax error.
|
||||
// Strictly speaking, control chars include the range [0x7f, 0x9f], not just
|
||||
// [0x00, 0x1f], but in practice, we ignore the multi-byte control characters
|
||||
// as it is simpler to inspect the tag's bytes than the tag's runes.
|
||||
i = 0
|
||||
for i < len(tag) && tag[i] > ' ' && tag[i] != ':' && tag[i] != '"' && tag[i] != 0x7f {
|
||||
i++
|
||||
}
|
||||
if i == 0 || i+1 >= len(tag) || tag[i] != ':' || tag[i+1] != '"' {
|
||||
break
|
||||
}
|
||||
name := string(tag[:i])
|
||||
tag = tag[i+1:]
|
||||
|
||||
// Scan quoted string to find value.
|
||||
i = 1
|
||||
for i < len(tag) && tag[i] != '"' {
|
||||
if tag[i] == '\\' {
|
||||
i++
|
||||
}
|
||||
i++
|
||||
}
|
||||
if i >= len(tag) {
|
||||
break
|
||||
}
|
||||
qvalue := string(tag[:i+1])
|
||||
tag = tag[i+1:]
|
||||
|
||||
if key == name {
|
||||
value, err := strconv.Unquote(qvalue)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
return value, true
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
159
tools/goctl/api/util/types.go
Normal file
159
tools/goctl/api/util/types.go
Normal file
@@ -0,0 +1,159 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"zero/tools/goctl/api/spec"
|
||||
)
|
||||
|
||||
func DecomposeType(t string) (result []string, err error) {
|
||||
add := func(tp string) error {
|
||||
ret, err := DecomposeType(tp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
result = append(result, ret...)
|
||||
return nil
|
||||
}
|
||||
if strings.HasPrefix(t, "map") {
|
||||
t = strings.ReplaceAll(t, "map", "")
|
||||
if t[0] == '[' {
|
||||
pos := strings.Index(t, "]")
|
||||
if pos > 1 {
|
||||
if err = add(t[1:pos]); err != nil {
|
||||
return
|
||||
}
|
||||
if len(t) > pos+1 {
|
||||
err = add(t[pos+1:])
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if strings.HasPrefix(t, "[]") {
|
||||
if len(t) > 2 {
|
||||
err = add(t[2:])
|
||||
return
|
||||
}
|
||||
} else if strings.HasPrefix(t, "*") {
|
||||
err = add(t[1:])
|
||||
return
|
||||
} else {
|
||||
result = append(result, t)
|
||||
return
|
||||
}
|
||||
|
||||
err = fmt.Errorf("bad type %q", t)
|
||||
return
|
||||
}
|
||||
|
||||
func GetAllTypes(api *spec.ApiSpec, route spec.Route) []spec.Type {
|
||||
var rts []spec.Type
|
||||
types := api.Types
|
||||
getTypeRecursive(route.RequestType, types, &rts)
|
||||
getTypeRecursive(route.ResponseType, types, &rts)
|
||||
return rts
|
||||
}
|
||||
|
||||
func GetLocalTypes(api *spec.ApiSpec, route spec.Route) []spec.Type {
|
||||
sharedTypes := GetSharedTypes(api)
|
||||
isSharedType := func(ty spec.Type) bool {
|
||||
for _, item := range sharedTypes {
|
||||
if item.Name == ty.Name {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
var rts = GetAllTypes(api, route)
|
||||
|
||||
var result []spec.Type
|
||||
for _, item := range rts {
|
||||
if !isSharedType(item) {
|
||||
result = append(result, item)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func getTypeRecursive(ty spec.Type, allTypes []spec.Type, result *[]spec.Type) {
|
||||
isCustomType := func(name string) (*spec.Type, bool) {
|
||||
for _, item := range allTypes {
|
||||
if item.Name == name {
|
||||
return &item, true
|
||||
}
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
if len(ty.Name) > 0 {
|
||||
*result = append(*result, ty)
|
||||
}
|
||||
for _, member := range ty.Members {
|
||||
decomposedItems, _ := DecomposeType(member.Type)
|
||||
if len(decomposedItems) == 0 {
|
||||
continue
|
||||
}
|
||||
var customTypes []spec.Type
|
||||
for _, item := range decomposedItems {
|
||||
c, e := isCustomType(item)
|
||||
if e {
|
||||
customTypes = append(customTypes, *c)
|
||||
}
|
||||
}
|
||||
for _, ty := range customTypes {
|
||||
hasAppend := false
|
||||
for _, item := range *result {
|
||||
if ty.Name == item.Name {
|
||||
hasAppend = true
|
||||
break
|
||||
}
|
||||
|
||||
}
|
||||
if !hasAppend {
|
||||
getTypeRecursive(ty, allTypes, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func GetSharedTypes(api *spec.ApiSpec) []spec.Type {
|
||||
types := api.Types
|
||||
var result []spec.Type
|
||||
var container []spec.Type
|
||||
hasInclude := func(all []spec.Type, ty spec.Type) bool {
|
||||
for _, item := range all {
|
||||
if item.Name == ty.Name {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
for _, route := range api.Service.Routes {
|
||||
var rts []spec.Type
|
||||
getTypeRecursive(route.RequestType, types, &rts)
|
||||
getTypeRecursive(route.ResponseType, types, &rts)
|
||||
for _, item := range rts {
|
||||
if len(item.Name) == 0 {
|
||||
continue
|
||||
}
|
||||
if hasInclude(container, item) {
|
||||
hasAppend := false
|
||||
for _, r := range result {
|
||||
if item.Name == r.Name {
|
||||
hasAppend = true
|
||||
break
|
||||
}
|
||||
|
||||
}
|
||||
if !hasAppend {
|
||||
result = append(result, item)
|
||||
}
|
||||
} else {
|
||||
container = append(container, item)
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
74
tools/goctl/api/util/util.go
Normal file
74
tools/goctl/api/util/util.go
Normal file
@@ -0,0 +1,74 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"zero/tools/goctl/api/spec"
|
||||
|
||||
"zero/core/lang"
|
||||
"zero/tools/goctl/util"
|
||||
)
|
||||
|
||||
func MaybeCreateFile(dir, subdir, file string) (fp *os.File, created bool, err error) {
|
||||
lang.Must(util.MkdirIfNotExist(path.Join(dir, subdir)))
|
||||
fpath := path.Join(dir, subdir, file)
|
||||
if util.FileExists(fpath) {
|
||||
fmt.Printf("%s exists, ignored generation\n", fpath)
|
||||
return nil, false, nil
|
||||
}
|
||||
|
||||
fp, err = util.CreateIfNotExist(fpath)
|
||||
created = err == nil
|
||||
return
|
||||
}
|
||||
|
||||
func ClearAndOpenFile(fpath string) (*os.File, error) {
|
||||
f, err := os.OpenFile(fpath, os.O_WRONLY|os.O_TRUNC, 0600)
|
||||
|
||||
_, err = f.WriteString("")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return f, nil
|
||||
}
|
||||
|
||||
func WrapErr(err error, message string) error {
|
||||
return errors.New(message + ", " + err.Error())
|
||||
}
|
||||
|
||||
func Copy(src, dst string) (int64, error) {
|
||||
sourceFileStat, err := os.Stat(src)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if !sourceFileStat.Mode().IsRegular() {
|
||||
return 0, fmt.Errorf("%s is not a regular file", src)
|
||||
}
|
||||
|
||||
source, err := os.Open(src)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer source.Close()
|
||||
|
||||
destination, err := os.Create(dst)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer destination.Close()
|
||||
nBytes, err := io.Copy(destination, source)
|
||||
return nBytes, err
|
||||
}
|
||||
|
||||
func ComponentName(api *spec.ApiSpec) string {
|
||||
name := api.Service.Name
|
||||
if strings.HasSuffix(name, "-api") {
|
||||
return name[:len(name)-4] + "Components"
|
||||
}
|
||||
return name + "Components"
|
||||
}
|
||||
29
tools/goctl/api/validate/validate.go
Normal file
29
tools/goctl/api/validate/validate.go
Normal file
@@ -0,0 +1,29 @@
|
||||
package validate
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"zero/tools/goctl/api/parser"
|
||||
|
||||
"github.com/logrusorgru/aurora"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
func GoValidateApi(c *cli.Context) error {
|
||||
apiFile := c.String("api")
|
||||
|
||||
if len(apiFile) == 0 {
|
||||
return errors.New("missing -api")
|
||||
}
|
||||
|
||||
p, err := parser.NewParser(apiFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = p.Parse()
|
||||
if err == nil {
|
||||
fmt.Println(aurora.Green("api format ok"))
|
||||
}
|
||||
return err
|
||||
}
|
||||
Reference in New Issue
Block a user