Browse Source

feat(goctl): api dart support flutter v2 (#1603)

0. support null-safety code gen
1. supports -legacy flag for legacy code gen
2. supports -hostname flag for server hostname
3. use dart official format
4. fix some some bugs

Resolves: #1602
Fyn 3 years ago
parent
commit
6a66dde0a1

+ 1 - 0
tools/goctl/.gitignore

@@ -0,0 +1 @@
+.vscode

+ 40 - 0
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
+}

+ 13 - 3
tools/goctl/api/dartgen/gen.go

@@ -2,6 +2,7 @@ package dartgen
 
 
 import (
 import (
 	"errors"
 	"errors"
+	"fmt"
 	"strings"
 	"strings"
 
 
 	"github.com/urfave/cli"
 	"github.com/urfave/cli"
@@ -13,12 +14,18 @@ import (
 func DartCommand(c *cli.Context) error {
 func DartCommand(c *cli.Context) error {
 	apiFile := c.String("api")
 	apiFile := c.String("api")
 	dir := c.String("dir")
 	dir := c.String("dir")
+	isLegacy := c.Bool("legacy")
+	hostname := c.String("hostname")
 	if len(apiFile) == 0 {
 	if len(apiFile) == 0 {
 		return errors.New("missing -api")
 		return errors.New("missing -api")
 	}
 	}
 	if len(dir) == 0 {
 	if len(dir) == 0 {
 		return errors.New("missing -dir")
 		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)
 	api, err := parser.Parse(apiFile)
 	if err != nil {
 	if err != nil {
@@ -30,8 +37,11 @@ func DartCommand(c *cli.Context) error {
 		dir = dir + "/"
 		dir = dir + "/"
 	}
 	}
 	api.Info.Title = strings.Replace(apiFile, ".api", "", -1)
 	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
 	return nil
 }
 }

+ 38 - 8
tools/goctl/api/dartgen/genapi.go

@@ -2,13 +2,14 @@ package dartgen
 
 
 import (
 import (
 	"os"
 	"os"
+	"strings"
 	"text/template"
 	"text/template"
 
 
 	"github.com/zeromicro/go-zero/tools/goctl/api/spec"
 	"github.com/zeromicro/go-zero/tools/goctl/api/spec"
 )
 )
 
 
 const apiTemplate = `import 'api.dart';
 const apiTemplate = `import 'api.dart';
-import '../data/{{with .Info}}{{.Title}}{{end}}.dart';
+import '../data/{{with .Info}}{{getBaseName .Title}}{{end}}.dart';
 {{with .Service}}
 {{with .Service}}
 /// {{.Name}}
 /// {{.Name}}
 {{range .Routes}}
 {{range .Routes}}
@@ -22,24 +23,45 @@ Future {{pathToFuncName .Path}}( {{if ne .Method "get"}}{{with .RequestType}}{{.
     Function eventually}) async {
     Function eventually}) async {
   await api{{if eq .Method "get"}}Get{{else}}Post{{end}}('{{.Path}}',{{if ne .Method "get"}}request,{{end}}
   await api{{if eq .Method "get"}}Get{{else}}Post{{end}}('{{.Path}}',{{if ne .Method "get"}}request,{{end}}
   	 ok: (data) {
   	 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);
   }, fail: fail, eventually: eventually);
 }
 }
 {{end}}
 {{end}}
 {{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)
 	err := os.MkdirAll(dir, 0o755)
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
 
 
-	err = genApiFile(dir)
+	err = genApiFile(dir, isLegacy)
 	if err != nil {
 	if err != nil {
 		return err
 		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 {
 	if err != nil {
 		return err
 		return err
 	}
 	}
@@ -47,7 +69,11 @@ func genApi(dir string, api *spec.ApiSpec) error {
 	defer file.Close()
 	defer file.Close()
 	t := template.New("apiTemplate")
 	t := template.New("apiTemplate")
 	t = t.Funcs(funcMap)
 	t = t.Funcs(funcMap)
-	t, err = t.Parse(apiTemplate)
+	tpl := apiTemplateV2
+	if isLegacy {
+		tpl = apiTemplate
+	}
+	t, err = t.Parse(tpl)
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
@@ -55,7 +81,7 @@ func genApi(dir string, api *spec.ApiSpec) error {
 	return t.Execute(file, api)
 	return t.Execute(file, api)
 }
 }
 
 
-func genApiFile(dir string) error {
+func genApiFile(dir string, isLegacy bool) error {
 	path := dir + "api.dart"
 	path := dir + "api.dart"
 	if fileExists(path) {
 	if fileExists(path) {
 		return nil
 		return nil
@@ -66,6 +92,10 @@ func genApiFile(dir string) error {
 	}
 	}
 
 
 	defer apiFile.Close()
 	defer apiFile.Close()
-	_, err = apiFile.WriteString(apiFileContent)
+	tpl := apiFileContentV2
+	if isLegacy {
+		tpl = apiFileContent
+	}
+	_, err = apiFile.WriteString(tpl)
 	return err
 	return err
 }
 }

+ 37 - 6
tools/goctl/api/dartgen/gendata.go

@@ -2,6 +2,7 @@ package dartgen
 
 
 import (
 import (
 	"os"
 	"os"
+	"strings"
 	"text/template"
 	"text/template"
 
 
 	"github.com/zeromicro/go-zero/tools/goctl/api/spec"
 	"github.com/zeromicro/go-zero/tools/goctl/api/spec"
@@ -31,18 +32,40 @@ class {{.Name}}{
 {{end}}
 {{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<String,dynamic> m) {
+		return {{.Name}}({{range .Members}}
+			{{lowCamelCase .Name}}: {{if isDirectType .Type.Name}}m['{{getPropertyFromMember .}}']{{else if isClassListType .Type.Name}}(m['{{getPropertyFromMember .}}'] as List<dynamic>).map((i) => {{getCoreType .Type.Name}}.fromJson(i)){{else}}{{.Type.Name}}.fromJson(m['{{getPropertyFromMember .}}']){{end}},{{end}}
+		);
+	}
+	Map<String,dynamic> 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)
 	err := os.MkdirAll(dir, 0o755)
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
 
 
-	err = genTokens(dir)
+	err = genTokens(dir, isLegacy)
 	if err != nil {
 	if err != nil {
 		return err
 		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 {
 	if err != nil {
 		return err
 		return err
 	}
 	}
@@ -50,7 +73,11 @@ func genData(dir string, api *spec.ApiSpec) error {
 
 
 	t := template.New("dataTemplate")
 	t := template.New("dataTemplate")
 	t = t.Funcs(funcMap)
 	t = t.Funcs(funcMap)
-	t, err = t.Parse(dataTemplate)
+	tpl := dataTemplateV2
+	if isLegacy {
+		tpl = dataTemplate
+	}
+	t, err = t.Parse(tpl)
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
@@ -63,7 +90,7 @@ func genData(dir string, api *spec.ApiSpec) error {
 	return t.Execute(file, api)
 	return t.Execute(file, api)
 }
 }
 
 
-func genTokens(dir string) error {
+func genTokens(dir string, isLeagcy bool) error {
 	path := dir + "tokens.dart"
 	path := dir + "tokens.dart"
 	if fileExists(path) {
 	if fileExists(path) {
 		return nil
 		return nil
@@ -75,7 +102,11 @@ func genTokens(dir string) error {
 	}
 	}
 
 
 	defer tokensFile.Close()
 	defer tokensFile.Close()
-	_, err = tokensFile.WriteString(tokensFileContent)
+	tpl := tokensFileContentV2
+	if isLeagcy {
+		tpl = tokensFileContent
+	}
+	_, err = tokensFile.WriteString(tpl)
 	return err
 	return err
 }
 }
 
 

+ 44 - 4
tools/goctl/api/dartgen/genvars.go

@@ -1,11 +1,13 @@
 package dartgen
 package dartgen
 
 
 import (
 import (
+	"fmt"
 	"io/ioutil"
 	"io/ioutil"
 	"os"
 	"os"
 )
 )
 
 
-const varTemplate = `import 'dart:convert';
+const (
+	varTemplate = `import 'dart:convert';
 import 'package:shared_preferences/shared_preferences.dart';
 import 'package:shared_preferences/shared_preferences.dart';
 import '../data/tokens.dart';
 import '../data/tokens.dart';
 
 
@@ -40,21 +42,59 @@ Future<Tokens> 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<bool> setTokens(Tokens tokens) async {
+  var sp = await SharedPreferences.getInstance();
+  return await sp.setString(_tokenKey, jsonEncode(tokens.toJson()));
+}
+
+/// remove tokens
+Future<bool> removeTokens() async {
+  var sp = await SharedPreferences.getInstance();
+  return sp.remove(_tokenKey);
+}
+
+/// Reads tokens
+Future<Tokens?> getTokens() async {
+  try {
+    var sp = await SharedPreferences.getInstance();
+    var str = sp.getString('tokens');
+    if (str.isEmpty) {
+      return null;
+    }
+    return Tokens.fromJson(jsonDecode(str));
+  } catch (e) {
+    print(e);
+    return null;
+  }
+}`
+)
+
+func genVars(dir string, isLegacy bool, hostname string) error {
 	err := os.MkdirAll(dir, 0o755)
 	err := os.MkdirAll(dir, 0o755)
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
 
 
 	if !fileExists(dir + "vars.dart") {
 	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 {
 		if err != nil {
 			return err
 			return err
 		}
 		}
 	}
 	}
 
 
 	if !fileExists(dir + "kv.dart") {
 	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 {
 		if err != nil {
 			return err
 			return err
 		}
 		}

+ 5 - 0
tools/goctl/api/dartgen/util.go

@@ -4,6 +4,7 @@ import (
 	"errors"
 	"errors"
 	"fmt"
 	"fmt"
 	"os"
 	"os"
+	"path"
 	"strings"
 	"strings"
 
 
 	"github.com/zeromicro/go-zero/tools/goctl/api/spec"
 	"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:]
 	return util.ToLower(camel[:1]) + camel[1:]
 }
 }
 
 
+func getBaseName(str string) string {
+	return path.Base(str)
+}
+
 func getPropertyFromMember(member spec.Member) string {
 func getPropertyFromMember(member spec.Member) string {
 	name, err := member.GetPropertyName()
 	name, err := member.GetPropertyName()
 	if err != nil {
 	if err != nil {

+ 127 - 0
tools/goctl/api/dartgen/vars.go

@@ -3,6 +3,7 @@ package dartgen
 import "text/template"
 import "text/template"
 
 
 var funcMap = template.FuncMap{
 var funcMap = template.FuncMap{
+	"getBaseName":           getBaseName,
 	"getPropertyFromMember": getPropertyFromMember,
 	"getPropertyFromMember": getPropertyFromMember,
 	"isDirectType":          isDirectType,
 	"isDirectType":          isDirectType,
 	"isClassListType":       isClassListType,
 	"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<String, String>? header,
+			Function(Map<String, dynamic>)? ok,
+			Function(String)? fail,
+			Function? eventually}) async {
+		await _apiRequest('POST', path, data,
+				header: header, ok: ok, fail: fail, eventually: eventually);
+	}
+
+	/// 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<String, String>? header,
+			Function(Map<String, dynamic>)? ok,
+			Function(String)? fail,
+			Function? eventually}) async {
+		await _apiRequest('GET', path, null,
+				header: header, ok: ok, fail: fail, eventually: eventually);
+	}
+
+	Future _apiRequest(String method, String path, dynamic data,
+			{Map<String, String>? header,
+			Function(Map<String, dynamic>)? ok,
+			Function(String)? fail,
+			Function? eventually}) async {
+		var tokens = await getTokens();
+		try {
+			var client = HttpClient();
+			HttpClientRequest r;
+			if (method == 'POST') {
+				r = await client.postUrl(Uri.parse('https://' + serverHost + path));
+			} else {
+				r = await client.getUrl(Uri.parse('https://' + serverHost + path));
+			}
+
+			r.headers.set('Content-Type', 'application/json');
+			if (tokens != null) {
+				r.headers.set('Authorization', tokens.accessToken);
+			}
+			if (header != null) {
+				header.forEach((k, v) {
+					r.headers.set(k, v);
+				});
+			}
+			var strData = '';
+			if (data != null) {
+				strData = jsonEncode(data);
+			}
+			r.write(strData);
+			var rp = await r.close();
+			var body = await rp.transform(utf8.decoder).join();
+			print('${rp.statusCode} - $path');
+			print('-- request --');
+			print(strData);
+			print('-- response --');
+			print('$body \n');
+			if (rp.statusCode == 404) {
+				if (fail != null) fail('404 not found');
+			} else {
+				Map<String, dynamic> base = jsonDecode(body);
+				if (rp.statusCode == 200) {
+					if (base['code'] != 0) {
+						if (fail != null) fail(base['desc']);
+					} else {
+						if (ok != null) ok(base['data']);
+					}
+				} else if (base['code'] != 0) {
+					if (fail != null) fail(base['desc']);
+				}
+			}
+		} catch (e) {
+			if (fail != null) fail(e.toString());
+		}
+		if (eventually != null) eventually();
+	}`
+
 	tokensFileContent = `class Tokens {
 	tokensFileContent = `class Tokens {
   /// 用于访问的token, 每次请求都必须带在Header里面
   /// 用于访问的token, 每次请求都必须带在Header里面
   final String accessToken;
   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<String, dynamic> m) {
+    return Tokens(
+        accessToken: m['access_token'],
+        accessExpire: m['access_expire'],
+        refreshToken: m['refresh_token'],
+        refreshExpire: m['refresh_expire'],
+        refreshAfter: m['refresh_after']);
+  }
+  Map<String, dynamic> toJson() {
+    return {
+      'access_token': accessToken,
+      'access_expire': accessExpire,
+      'refresh_token': refreshToken,
+      'refresh_expire': refreshExpire,
+      'refresh_after': refreshAfter,
+    };
+  }
+}
 `
 `
 )
 )

+ 8 - 0
tools/goctl/goctl.go

@@ -266,6 +266,14 @@ var commands = []cli.Command{
 						Name:  "api",
 						Name:  "api",
 						Usage: "the api file",
 						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,
 				Action: dartgen.DartCommand,
 			},
 			},