Repository: zeromicro/go-zero
Pull request #1607 (2022-03-03 06:29:41)

fix: fix(gctl): apiparser_parser auto format
Each time test `TestFileSplitor` is executed, an extra newline will be generated at the end of the file `apiparser_parser.go`. 


Patch:
diff --git a/readme-cn.md b/readme-cn.md
index 6b385602..e8bae5ca 100644
--- a/readme-cn.md
+++ b/readme-cn.md
@@ -19,9 +19,9 @@
 >
 > 从 v1.3.0 之前版本升级请执行以下命令：
 > 
-> GOPROXY=https://goproxy.cn/,direct go install github.com/zeromicro/go-zero/tools/goctl@latest
+> `GOPROXY=https://goproxy.cn/,direct go install github.com/zeromicro/go-zero/tools/goctl@latest`
 > 
-> goctl migrate —verbose —version v1.3.0
+> `goctl migrate —verbose —version v1.3.1`
 
 ## 0. go-zero 介绍
 
diff --git a/readme.md b/readme.md
index 3aff815d..0b9a8bff 100644
--- a/readme.md
+++ b/readme.md
@@ -16,11 +16,11 @@ English | [简体中文](readme-cn.md)
 
 > ***Important!***
 > 
-> To upgrade from previous versions, run the following commands.
+> To upgrade from versions eariler than v1.3.0, run the following commands.
 > 
 > `go install github.com/zeromicro/go-zero/tools/goctl@latest`
 > 
-> `goctl migrate —verbose —version v1.3.0`
+> `goctl migrate —verbose —version v1.3.1`
 
 ## 0. what is go-zero
 
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,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<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)
 	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<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)
 	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<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 {
   /// 用于访问的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<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,
+    };
+  }
+}
 `
 )
diff --git a/tools/goctl/api/parser/g4/gen/api/apiparser_parser.go b/tools/goctl/api/parser/g4/gen/api/apiparser_parser.go
index 9b602a9f..8599337c 100755
--- a/tools/goctl/api/parser/g4/gen/api/apiparser_parser.go
+++ b/tools/goctl/api/parser/g4/gen/api/apiparser_parser.go
@@ -634,6 +634,3 @@ func NewSyntaxLitContext(parser antlr.Parser, parent antlr.ParserRuleContext, in
 
 	return p
 }
-
-
-
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,
 			},


Test patch:
diff --git a/tools/goctl/api/parser/g4/gen/api/file_splitor_test.go b/tools/goctl/api/parser/g4/gen/api/file_splitor_test.go
index ab7f0682..541d6cbe 100644
--- a/tools/goctl/api/parser/g4/gen/api/file_splitor_test.go
+++ b/tools/goctl/api/parser/g4/gen/api/file_splitor_test.go
@@ -8,6 +8,7 @@ import (
 	"bufio"
 	"bytes"
 	"fmt"
+	"go/format"
 	"io/ioutil"
 	"log"
 	"os"
@@ -61,7 +62,12 @@ import "github.com/zeromicro/antlr"
 			}
 		}
 
-		err = ioutil.WriteFile(fp, buffer.Bytes(), os.ModePerm)
+		src, err := format.Source(buffer.Bytes())
+		if err != nil {
+			fmt.Printf("%+v\n", err)
+			break
+		}
+		err = ioutil.WriteFile(fp, src, os.ModePerm)
 		if err != nil {
 			fmt.Printf("%+v\n", err)
 		}