diff --git a/tools/goctl/Makefile b/tools/goctl/Makefile new file mode 100644 index 00000000..c42e1a97 --- /dev/null +++ b/tools/goctl/Makefile @@ -0,0 +1,4 @@ +version := $(shell /bin/date "+%Y-%m-%d %H:%M") + +build: + go build -ldflags="-s -w" -ldflags="-X 'main.BuildTime=$(version)'" goctl.go && upx goctl diff --git a/tools/goctl/api/apigen/gen.go b/tools/goctl/api/apigen/gen.go new file mode 100644 index 00000000..b3a5cce8 --- /dev/null +++ b/tools/goctl/api/apigen/gen.go @@ -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 +} diff --git a/tools/goctl/api/apigen/util.go b/tools/goctl/api/apigen/util.go new file mode 100644 index 00000000..700b1d0d --- /dev/null +++ b/tools/goctl/api/apigen/util.go @@ -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)) +} diff --git a/tools/goctl/api/dartgen/gen.go b/tools/goctl/api/dartgen/gen.go new file mode 100644 index 00000000..3ce37a89 --- /dev/null +++ b/tools/goctl/api/dartgen/gen.go @@ -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 +} diff --git a/tools/goctl/api/dartgen/genapi.go b/tools/goctl/api/dartgen/genapi.go new file mode 100644 index 00000000..8c1d5573 --- /dev/null +++ b/tools/goctl/api/dartgen/genapi.go @@ -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 +} diff --git a/tools/goctl/api/dartgen/gendata.go b/tools/goctl/api/dartgen/gendata.go new file mode 100644 index 00000000..738576f4 --- /dev/null +++ b/tools/goctl/api/dartgen/gendata.go @@ -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 m) { + return {{.Name}}({{range .Members}} + {{lowCamelCase .Name}}: {{if isDirectType .Type}}m['{{tagGet .Tag "json"}}']{{else if isClassListType .Type}}(m['{{tagGet .Tag "json"}}'] as List).map((i) => {{getCoreType .Type}}.fromJson(i)){{else}}{{.Type}}.fromJson(m['{{tagGet .Tag "json"}}']){{end}},{{end}} + ); + } + Map 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 +} diff --git a/tools/goctl/api/dartgen/genvars.go b/tools/goctl/api/dartgen/genvars.go new file mode 100644 index 00000000..974701f2 --- /dev/null +++ b/tools/goctl/api/dartgen/genvars.go @@ -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 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 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 +} diff --git a/tools/goctl/api/dartgen/util.go b/tools/goctl/api/dartgen/util.go new file mode 100644 index 00000000..720dd3d7 --- /dev/null +++ b/tools/goctl/api/dartgen/util.go @@ -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" + } + + 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) +} diff --git a/tools/goctl/api/dartgen/vars.go b/tools/goctl/api/dartgen/vars.go new file mode 100644 index 00000000..864126cb --- /dev/null +++ b/tools/goctl/api/dartgen/vars.go @@ -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 header, + Function(Map) 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 header, + Function(Map) 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 header, + Function(Map) 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 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 m) { + return Tokens( + accessToken: m['access_token'], + accessExpire: m['access_expire'], + refreshToken: m['refresh_token'], + refreshExpire: m['refresh_expire'], + refreshAfter: m['refresh_after']); + } + Map toJson() { + return { + 'access_token': accessToken, + 'access_expire': accessExpire, + 'refresh_token': refreshToken, + 'refresh_expire': refreshExpire, + 'refresh_after': refreshAfter, + }; + } +} +` diff --git a/tools/goctl/api/demo/config/config.go b/tools/goctl/api/demo/config/config.go new file mode 100644 index 00000000..5beaa249 --- /dev/null +++ b/tools/goctl/api/demo/config/config.go @@ -0,0 +1,7 @@ +package config + +import "zero/ngin" + +type Config struct { + ngin.NgConf +} diff --git a/tools/goctl/api/demo/demo.go b/tools/goctl/api/demo/demo.go new file mode 100644 index 00000000..3c203a08 --- /dev/null +++ b/tools/goctl/api/demo/demo.go @@ -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() +} diff --git a/tools/goctl/api/demo/etc/user.json b/tools/goctl/api/demo/etc/user.json new file mode 100644 index 00000000..cd52be6f --- /dev/null +++ b/tools/goctl/api/demo/etc/user.json @@ -0,0 +1,8 @@ +{ + "Name": "user", + "Host": "127.0.0.1", + "Port": 3333, + "Log": { + "Mode": "console" + } +} diff --git a/tools/goctl/api/demo/handler/getuserhandler.go b/tools/goctl/api/demo/handler/getuserhandler.go new file mode 100644 index 00000000..d4cbda21 --- /dev/null +++ b/tools/goctl/api/demo/handler/getuserhandler.go @@ -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", + }) +} diff --git a/tools/goctl/api/demo/handler/handlers.go b/tools/goctl/api/demo/handler/handlers.go new file mode 100644 index 00000000..24e71299 --- /dev/null +++ b/tools/goctl/api/demo/handler/handlers.go @@ -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, + }, + }) +} diff --git a/tools/goctl/api/demo/svc/servicecontext.go b/tools/goctl/api/demo/svc/servicecontext.go new file mode 100644 index 00000000..51137699 --- /dev/null +++ b/tools/goctl/api/demo/svc/servicecontext.go @@ -0,0 +1,4 @@ +package svc + +type ServiceContext struct { +} diff --git a/tools/goctl/api/docgen/doc.go b/tools/goctl/api/docgen/doc.go new file mode 100644 index 00000000..31ed2fb7 --- /dev/null +++ b/tools/goctl/api/docgen/doc.go @@ -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 +} diff --git a/tools/goctl/api/docgen/gen.go b/tools/goctl/api/docgen/gen.go new file mode 100644 index 00000000..4e83962e --- /dev/null +++ b/tools/goctl/api/docgen/gen.go @@ -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 +} diff --git a/tools/goctl/api/format/format.go b/tools/goctl/api/format/format.go new file mode 100644 index 00000000..17e2bdd0 --- /dev/null +++ b/tools/goctl/api/format/format.go @@ -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.*)[\\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 +} diff --git a/tools/goctl/api/gogen/gen.go b/tools/goctl/api/gogen/gen.go new file mode 100644 index 00000000..14adbf29 --- /dev/null +++ b/tools/goctl/api/gogen/gen.go @@ -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 + }) +} diff --git a/tools/goctl/api/gogen/genconfig.go b/tools/goctl/api/gogen/genconfig.go new file mode 100644 index 00000000..1bc808d0 --- /dev/null +++ b/tools/goctl/api/gogen/genconfig.go @@ -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 +} diff --git a/tools/goctl/api/gogen/genetc.go b/tools/goctl/api/gogen/genetc.go new file mode 100644 index 00000000..d514b553 --- /dev/null +++ b/tools/goctl/api/gogen/genetc.go @@ -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 +} diff --git a/tools/goctl/api/gogen/genhandlers.go b/tools/goctl/api/gogen/genhandlers.go new file mode 100644 index 00000000..128dbfbd --- /dev/null +++ b/tools/goctl/api/gogen/genhandlers.go @@ -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" +} diff --git a/tools/goctl/api/gogen/genlogic.go b/tools/goctl/api/gogen/genlogic.go new file mode 100644 index 00000000..c2b54315 --- /dev/null +++ b/tools/goctl/api/gogen/genlogic.go @@ -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") +} diff --git a/tools/goctl/api/gogen/genmain.go b/tools/goctl/api/gogen/genmain.go new file mode 100644 index 00000000..f03b95ab --- /dev/null +++ b/tools/goctl/api/gogen/genmain.go @@ -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") +} diff --git a/tools/goctl/api/gogen/genroutes.go b/tools/goctl/api/gogen/genroutes.go new file mode 100644 index 00000000..cc848334 --- /dev/null +++ b/tools/goctl/api/gogen/genroutes.go @@ -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 +} diff --git a/tools/goctl/api/gogen/genservicecontext.go b/tools/goctl/api/gogen/genservicecontext.go new file mode 100644 index 00000000..979a7927 --- /dev/null +++ b/tools/goctl/api/gogen/genservicecontext.go @@ -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 +} diff --git a/tools/goctl/api/gogen/gentypes.go b/tools/goctl/api/gogen/gentypes.go new file mode 100644 index 00000000..c8e2aab7 --- /dev/null +++ b/tools/goctl/api/gogen/gentypes.go @@ -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 +} diff --git a/tools/goctl/api/gogen/util.go b/tools/goctl/api/gogen/util.go new file mode 100644 index 00000000..89c90081 --- /dev/null +++ b/tools/goctl/api/gogen/util.go @@ -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) +} diff --git a/tools/goctl/api/gogen/vars.go b/tools/goctl/api/gogen/vars.go new file mode 100644 index 00000000..17656f1b --- /dev/null +++ b/tools/goctl/api/gogen/vars.go @@ -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" +) diff --git a/tools/goctl/api/javagen/gen.go b/tools/goctl/api/javagen/gen.go new file mode 100644 index 00000000..34a6bf79 --- /dev/null +++ b/tools/goctl/api/javagen/gen.go @@ -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 +} diff --git a/tools/goctl/api/javagen/gencomponents.go b/tools/goctl/api/javagen/gencomponents.go new file mode 100644 index 00000000..5f116225 --- /dev/null +++ b/tools/goctl/api/javagen/gencomponents.go @@ -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 +} diff --git a/tools/goctl/api/javagen/genpacket.go b/tools/goctl/api/javagen/genpacket.go new file mode 100644 index 00000000..dbf7d6cf --- /dev/null +++ b/tools/goctl/api/javagen/genpacket.go @@ -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 +} diff --git a/tools/goctl/api/javagen/util.go b/tools/goctl/api/javagen/util.go new file mode 100644 index 00000000..d51a39a1 --- /dev/null +++ b/tools/goctl/api/javagen/util.go @@ -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", 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 +} diff --git a/tools/goctl/api/javagen/vars.go b/tools/goctl/api/javagen/vars.go new file mode 100644 index 00000000..3eec6cd3 --- /dev/null +++ b/tools/goctl/api/javagen/vars.go @@ -0,0 +1,3 @@ +package javagen + +const modelDir = "model" diff --git a/tools/goctl/api/main.go b/tools/goctl/api/main.go new file mode 100644 index 00000000..5f0e4c25 --- /dev/null +++ b/tools/goctl/api/main.go @@ -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) +} diff --git a/tools/goctl/api/parser/basestate.go b/tools/goctl/api/parser/basestate.go new file mode 100644 index 00000000..f5a083da --- /dev/null +++ b/tools/goctl/api/parser/basestate.go @@ -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) +} diff --git a/tools/goctl/api/parser/basestate_test.go b/tools/goctl/api/parser/basestate_test.go new file mode 100644 index 00000000..51a0b20b --- /dev/null +++ b/tools/goctl/api/parser/basestate_test.go @@ -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"]) +} diff --git a/tools/goctl/api/parser/entity.go b/tools/goctl/api/parser/entity.go new file mode 100644 index 00000000..af535203 --- /dev/null +++ b/tools/goctl/api/parser/entity.go @@ -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 +} diff --git a/tools/goctl/api/parser/infostate.go b/tools/goctl/api/parser/infostate.go new file mode 100644 index 00000000..5d461476 --- /dev/null +++ b/tools/goctl/api/parser/infostate.go @@ -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 +} diff --git a/tools/goctl/api/parser/parser.go b/tools/goctl/api/parser/parser.go new file mode 100644 index 00000000..79cbb301 --- /dev/null +++ b/tools/goctl/api/parser/parser.go @@ -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) + } + } +} diff --git a/tools/goctl/api/parser/rootstate.go b/tools/goctl/api/parser/rootstate.go new file mode 100644 index 00000000..715e00b6 --- /dev/null +++ b/tools/goctl/api/parser/rootstate.go @@ -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) + } +} diff --git a/tools/goctl/api/parser/servicestate.go b/tools/goctl/api/parser/servicestate.go new file mode 100644 index 00000000..cbbe792a --- /dev/null +++ b/tools/goctl/api/parser/servicestate.go @@ -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) +} diff --git a/tools/goctl/api/parser/state.go b/tools/goctl/api/parser/state.go new file mode 100644 index 00000000..b167696f --- /dev/null +++ b/tools/goctl/api/parser/state.go @@ -0,0 +1,7 @@ +package parser + +import "zero/tools/goctl/api/spec" + +type state interface { + process(api *spec.ApiSpec) (state, error) +} diff --git a/tools/goctl/api/parser/typeparser.go b/tools/goctl/api/parser/typeparser.go new file mode 100644 index 00000000..804d9c4c --- /dev/null +++ b/tools/goctl/api/parser/typeparser.go @@ -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 +} diff --git a/tools/goctl/api/parser/typestate.go b/tools/goctl/api/parser/typestate.go new file mode 100644 index 00000000..8ea015ee --- /dev/null +++ b/tools/goctl/api/parser/typestate.go @@ -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) +} diff --git a/tools/goctl/api/parser/util.go b/tools/goctl/api/parser/util.go new file mode 100644 index 00000000..d70ac60f --- /dev/null +++ b/tools/goctl/api/parser/util.go @@ -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 +} diff --git a/tools/goctl/api/parser/validator.go b/tools/goctl/api/parser/validator.go new file mode 100644 index 00000000..0bf6b491 --- /dev/null +++ b/tools/goctl/api/parser/validator.go @@ -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, "" +} diff --git a/tools/goctl/api/parser/vars.go b/tools/goctl/api/parser/vars.go new file mode 100644 index 00000000..f1793137 --- /dev/null +++ b/tools/goctl/api/parser/vars.go @@ -0,0 +1,16 @@ +package parser + +const ( + infoDirective = "info" + serviceDirective = "service" + typeDirective = "type" + typeStruct = "struct" + at = '@' + colon = ':' + leftParenthesis = '(' + rightParenthesis = ')' + leftBrace = "{" + rightBrace = '}' + multilineBeginTag = '>' + multilineEndTag = '<' +) diff --git a/tools/goctl/api/spec/fn.go b/tools/goctl/api/spec/fn.go new file mode 100644 index 00000000..5dfa32ad --- /dev/null +++ b/tools/goctl/api/spec/fn.go @@ -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\w+):"(?P[^,"]+)[,]?(?P