diff --git a/tools/goctl/.gitignore b/tools/goctl/.gitignore new file mode 100644 index 00000000..722d5e71 --- /dev/null +++ b/tools/goctl/.gitignore @@ -0,0 +1 @@ +.vscode diff --git a/tools/goctl/api/dartgen/format.go b/tools/goctl/api/dartgen/format.go new file mode 100644 index 00000000..029e3bec --- /dev/null +++ b/tools/goctl/api/dartgen/format.go @@ -0,0 +1,40 @@ +package dartgen + +import ( + "fmt" + "os" + "os/exec" +) + +const dartExec = "dart" + +func formatDir(dir string) error { + ok, err := dirctoryExists(dir) + if err != nil { + return err + } + if !ok { + return fmt.Errorf("format failed, directory %q does not exist", dir) + } + + _, err = exec.LookPath(dartExec) + if err != nil { + return err + } + cmd := exec.Command(dartExec, "format", dir) + cmd.Env = os.Environ() + cmd.Stderr = os.Stderr + + return cmd.Run() +} + +func dirctoryExists(dir string) (bool, error) { + _, err := os.Stat(dir) + if err == nil { + return true, nil + } + if os.IsNotExist(err) { + return false, nil + } + return false, err +} diff --git a/tools/goctl/api/dartgen/gen.go b/tools/goctl/api/dartgen/gen.go index 3bb08a99..c7078bf2 100644 --- a/tools/goctl/api/dartgen/gen.go +++ b/tools/goctl/api/dartgen/gen.go @@ -2,6 +2,7 @@ package dartgen import ( "errors" + "fmt" "strings" "github.com/urfave/cli" @@ -13,12 +14,18 @@ import ( func DartCommand(c *cli.Context) error { apiFile := c.String("api") dir := c.String("dir") + isLegacy := c.Bool("legacy") + hostname := c.String("hostname") if len(apiFile) == 0 { return errors.New("missing -api") } if len(dir) == 0 { return errors.New("missing -dir") } + if len(hostname) == 0 { + fmt.Println("you could use '-hostname' flag to specify your server hostname") + hostname = "go-zero.dev" + } api, err := parser.Parse(apiFile) if err != nil { @@ -30,8 +37,11 @@ func DartCommand(c *cli.Context) error { dir = dir + "/" } api.Info.Title = strings.Replace(apiFile, ".api", "", -1) - logx.Must(genData(dir+"data/", api)) - logx.Must(genApi(dir+"api/", api)) - logx.Must(genVars(dir + "vars/")) + logx.Must(genData(dir+"data/", api, isLegacy)) + logx.Must(genApi(dir+"api/", api, isLegacy)) + logx.Must(genVars(dir+"vars/", isLegacy, hostname)) + if err := formatDir(dir); err != nil { + logx.Errorf("failed to format, %v", err) + } return nil } diff --git a/tools/goctl/api/dartgen/genapi.go b/tools/goctl/api/dartgen/genapi.go index 380b858a..7f735a81 100644 --- a/tools/goctl/api/dartgen/genapi.go +++ b/tools/goctl/api/dartgen/genapi.go @@ -2,13 +2,14 @@ package dartgen import ( "os" + "strings" "text/template" "github.com/zeromicro/go-zero/tools/goctl/api/spec" ) const apiTemplate = `import 'api.dart'; -import '../data/{{with .Info}}{{.Title}}{{end}}.dart'; +import '../data/{{with .Info}}{{getBaseName .Title}}{{end}}.dart'; {{with .Service}} /// {{.Name}} {{range .Routes}} @@ -22,24 +23,45 @@ Future {{pathToFuncName .Path}}( {{if ne .Method "get"}}{{with .RequestType}}{{. 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)); + if (ok != null) ok({{with .ResponseType}}{{.Name}}.fromJson(data){{end}}); }, fail: fail, eventually: eventually); } {{end}} {{end}}` -func genApi(dir string, api *spec.ApiSpec) error { +const apiTemplateV2 = `import 'api.dart'; +import '../data/{{with .Info}}{{getBaseName .Title}}{{end}}.dart'; +{{with .Service}} +/// {{.Name}} +{{range .Routes}} +/// --{{.Path}}-- +/// +/// request: {{with .RequestType}}{{.Name}}{{end}} +/// response: {{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}}.fromJson(data){{end}}); + }, fail: fail, eventually: eventually); +} +{{end}} +{{end}}` + +func genApi(dir string, api *spec.ApiSpec, isLegacy bool) error { err := os.MkdirAll(dir, 0o755) if err != nil { return err } - err = genApiFile(dir) + err = genApiFile(dir, isLegacy) if err != nil { return err } - file, err := os.OpenFile(dir+api.Service.Name+".dart", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o644) + file, err := os.OpenFile(dir+strings.ToLower(api.Service.Name+".dart"), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o644) if err != nil { return err } @@ -47,7 +69,11 @@ func genApi(dir string, api *spec.ApiSpec) error { defer file.Close() t := template.New("apiTemplate") t = t.Funcs(funcMap) - t, err = t.Parse(apiTemplate) + tpl := apiTemplateV2 + if isLegacy { + tpl = apiTemplate + } + t, err = t.Parse(tpl) if err != nil { return err } @@ -55,7 +81,7 @@ func genApi(dir string, api *spec.ApiSpec) error { return t.Execute(file, api) } -func genApiFile(dir string) error { +func genApiFile(dir string, isLegacy bool) error { path := dir + "api.dart" if fileExists(path) { return nil @@ -66,6 +92,10 @@ func genApiFile(dir string) error { } defer apiFile.Close() - _, err = apiFile.WriteString(apiFileContent) + tpl := apiFileContentV2 + if isLegacy { + tpl = apiFileContent + } + _, err = apiFile.WriteString(tpl) return err } diff --git a/tools/goctl/api/dartgen/gendata.go b/tools/goctl/api/dartgen/gendata.go index f8e4d538..49c5f0a6 100644 --- a/tools/goctl/api/dartgen/gendata.go +++ b/tools/goctl/api/dartgen/gendata.go @@ -2,6 +2,7 @@ package dartgen import ( "os" + "strings" "text/template" "github.com/zeromicro/go-zero/tools/goctl/api/spec" @@ -31,18 +32,40 @@ class {{.Name}}{ {{end}} ` -func genData(dir string, api *spec.ApiSpec) error { +const dataTemplateV2 = `// --{{with .Info}}{{.Title}}{{end}}-- +{{ range .Types}} +class {{.Name}} { + {{range .Members}} + {{if .Comment}}{{.Comment}}{{end}} + final {{.Type.Name}} {{lowCamelCase .Name}}; + {{end}}{{.Name}}({{if .Members}}{ + {{range .Members}} required this.{{lowCamelCase .Name}}, + {{end}}}{{end}}); + factory {{.Name}}.fromJson(Map m) { + return {{.Name}}({{range .Members}} + {{lowCamelCase .Name}}: {{if isDirectType .Type.Name}}m['{{getPropertyFromMember .}}']{{else if isClassListType .Type.Name}}(m['{{getPropertyFromMember .}}'] as List).map((i) => {{getCoreType .Type.Name}}.fromJson(i)){{else}}{{.Type.Name}}.fromJson(m['{{getPropertyFromMember .}}']){{end}},{{end}} + ); + } + Map toJson() { + return { {{range .Members}} + '{{getPropertyFromMember .}}': {{if isDirectType .Type.Name}}{{lowCamelCase .Name}}{{else if isClassListType .Type.Name}}{{lowCamelCase .Name}}.map((i) => i.toJson()){{else}}{{lowCamelCase .Name}}.toJson(){{end}},{{end}} + }; + } +} +{{end}}` + +func genData(dir string, api *spec.ApiSpec, isLegacy bool) error { err := os.MkdirAll(dir, 0o755) if err != nil { return err } - err = genTokens(dir) + err = genTokens(dir, isLegacy) if err != nil { return err } - file, err := os.OpenFile(dir+api.Service.Name+".dart", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o644) + file, err := os.OpenFile(dir+strings.ToLower(api.Service.Name+".dart"), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o644) if err != nil { return err } @@ -50,7 +73,11 @@ func genData(dir string, api *spec.ApiSpec) error { t := template.New("dataTemplate") t = t.Funcs(funcMap) - t, err = t.Parse(dataTemplate) + tpl := dataTemplateV2 + if isLegacy { + tpl = dataTemplate + } + t, err = t.Parse(tpl) if err != nil { return err } @@ -63,7 +90,7 @@ func genData(dir string, api *spec.ApiSpec) error { return t.Execute(file, api) } -func genTokens(dir string) error { +func genTokens(dir string, isLeagcy bool) error { path := dir + "tokens.dart" if fileExists(path) { return nil @@ -75,7 +102,11 @@ func genTokens(dir string) error { } defer tokensFile.Close() - _, err = tokensFile.WriteString(tokensFileContent) + tpl := tokensFileContentV2 + if isLeagcy { + tpl = tokensFileContent + } + _, err = tokensFile.WriteString(tpl) return err } diff --git a/tools/goctl/api/dartgen/genvars.go b/tools/goctl/api/dartgen/genvars.go index c6b90a56..51a9fa13 100644 --- a/tools/goctl/api/dartgen/genvars.go +++ b/tools/goctl/api/dartgen/genvars.go @@ -1,11 +1,13 @@ package dartgen import ( + "fmt" "io/ioutil" "os" ) -const varTemplate = `import 'dart:convert'; +const ( + varTemplate = `import 'dart:convert'; import 'package:shared_preferences/shared_preferences.dart'; import '../data/tokens.dart'; @@ -40,21 +42,59 @@ Future getTokens() async { } ` -func genVars(dir string) error { + varTemplateV2 = `import 'dart:convert'; +import 'package:shared_preferences/shared_preferences.dart'; +import '../data/tokens.dart'; + +const String _tokenKey = 'tokens'; + +/// Saves tokens +Future setTokens(Tokens tokens) async { + var sp = await SharedPreferences.getInstance(); + return await sp.setString(_tokenKey, jsonEncode(tokens.toJson())); +} + +/// remove tokens +Future removeTokens() async { + var sp = await SharedPreferences.getInstance(); + return sp.remove(_tokenKey); +} + +/// Reads tokens +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; + } +}` +) + +func genVars(dir string, isLegacy bool, hostname string) error { err := os.MkdirAll(dir, 0o755) if err != nil { return err } if !fileExists(dir + "vars.dart") { - err = ioutil.WriteFile(dir+"vars.dart", []byte(`const serverHost='demo-crm.xiaoheiban.cn';`), 0o644) + err = ioutil.WriteFile(dir+"vars.dart", []byte(fmt.Sprintf(`const serverHost='%s';`, hostname)), 0o644) if err != nil { return err } } if !fileExists(dir + "kv.dart") { - err = ioutil.WriteFile(dir+"kv.dart", []byte(varTemplate), 0o644) + tpl := varTemplateV2 + if isLegacy { + tpl = varTemplate + } + err = ioutil.WriteFile(dir+"kv.dart", []byte(tpl), 0o644) if err != nil { return err } diff --git a/tools/goctl/api/dartgen/util.go b/tools/goctl/api/dartgen/util.go index 92cf2a82..0cccf36d 100644 --- a/tools/goctl/api/dartgen/util.go +++ b/tools/goctl/api/dartgen/util.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" "os" + "path" "strings" "github.com/zeromicro/go-zero/tools/goctl/api/spec" @@ -34,6 +35,10 @@ func pathToFuncName(path string) string { return util.ToLower(camel[:1]) + camel[1:] } +func getBaseName(str string) string { + return path.Base(str) +} + func getPropertyFromMember(member spec.Member) string { name, err := member.GetPropertyName() if err != nil { diff --git a/tools/goctl/api/dartgen/vars.go b/tools/goctl/api/dartgen/vars.go index 67aafde2..621a7c3a 100644 --- a/tools/goctl/api/dartgen/vars.go +++ b/tools/goctl/api/dartgen/vars.go @@ -3,6 +3,7 @@ package dartgen import "text/template" var funcMap = template.FuncMap{ + "getBaseName": getBaseName, "getPropertyFromMember": getPropertyFromMember, "isDirectType": isDirectType, "isClassListType": isClassListType, @@ -99,6 +100,96 @@ Future _apiRequest(String method, String path, dynamic data, } ` + apiFileContentV2 = `import 'dart:io'; + import 'dart:convert'; + import '../vars/kv.dart'; + import '../vars/vars.dart'; + + /// send request with post method + /// + /// data: any request class that will be converted to json automatically + /// ok: is called when request succeeds + /// fail: is called when request fails + /// eventually: is always called until the nearby functions returns + 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); + } + + /// send request with get method + /// + /// ok: is called when request succeeds + /// fail: is called when request fails + /// eventually: is always called until the nearby functions returns + 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(); + }` + tokensFileContent = `class Tokens { /// 用于访问的token, 每次请求都必须带在Header里面 final String accessToken; @@ -132,5 +223,41 @@ Future _apiRequest(String method, String path, dynamic data, }; } } +` + + tokensFileContentV2 = `class Tokens { + /// 用于访问的token, 每次请求都必须带在Header里面 + final String accessToken; + final int accessExpire; + + /// 用于刷新token + final String refreshToken; + final int refreshExpire; + final int refreshAfter; + Tokens({ + required this.accessToken, + required this.accessExpire, + required this.refreshToken, + required this.refreshExpire, + required 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/goctl.go b/tools/goctl/goctl.go index 77c23a28..f9af586a 100644 --- a/tools/goctl/goctl.go +++ b/tools/goctl/goctl.go @@ -266,6 +266,14 @@ var commands = []cli.Command{ Name: "api", Usage: "the api file", }, + cli.BoolFlag{ + Name: "legacy", + Usage: "legacy generator for flutter v1", + }, + cli.StringFlag{ + Name: "hostname", + Usage: "hostname of the server", + }, }, Action: dartgen.DartCommand, },