From d142ccff6bb0a57d4515ce815af4f43ab54d410f Mon Sep 17 00:00:00 2001 From: chaosannals Date: Thu, 5 Dec 2024 16:26:22 +0800 Subject: [PATCH 01/25] tsgen adjust: field name '-' add " and change request and response 'interface' to 'type'. change 'unknown' to some ts types. --- tools/goctl/api/tsgen/genpacket.go | 13 ++- tools/goctl/api/tsgen/genrequest.go | 5 -- tools/goctl/api/tsgen/request.ts | 134 +++++++++++++++------------- tools/goctl/api/tsgen/util.go | 17 ++-- 4 files changed, 93 insertions(+), 76 deletions(-) diff --git a/tools/goctl/api/tsgen/genpacket.go b/tools/goctl/api/tsgen/genpacket.go index 6635220a3c56..152c15c2f769 100644 --- a/tools/goctl/api/tsgen/genpacket.go +++ b/tools/goctl/api/tsgen/genpacket.go @@ -173,14 +173,21 @@ func callParamsForRoute(route spec.Route, group spec.Group) string { var params = []string{pathForRoute(route, group)} if hasParams { params = append(params, "params") + } else { + params = append(params, "null") } + + configParams := []string{} + if hasBody { - params = append(params, "req") + configParams = append(configParams, "body: JSON.stringify(req)") } if hasHeader { - params = append(params, "headers") + configParams = append(configParams, "headers: headers") } + params = append(params, fmt.Sprintf("{%s}", strings.Join(configParams, ", "))) + return strings.Join(params, ", ") } @@ -212,7 +219,7 @@ func pathHasParams(route spec.Route) bool { return false } - return len(ds.Members) != len(ds.GetBodyMembers()) + return len(ds.Members) != (len(ds.GetBodyMembers()) + len(ds.GetTagMembers(headerTagKey)) + len(ds.GetTagMembers(pathTagKey))) } func hasRequestBody(route spec.Route) bool { diff --git a/tools/goctl/api/tsgen/genrequest.go b/tools/goctl/api/tsgen/genrequest.go index 440ae4cb8be0..0218aa3045a4 100644 --- a/tools/goctl/api/tsgen/genrequest.go +++ b/tools/goctl/api/tsgen/genrequest.go @@ -4,8 +4,6 @@ import ( _ "embed" "os" "path/filepath" - - "github.com/zeromicro/go-zero/tools/goctl/util/pathx" ) //go:embed request.ts @@ -18,9 +16,6 @@ func genRequest(dir string) error { } filename := filepath.Join(abs, "gocliRequest.ts") - if pathx.FileExists(filename) { - return nil - } return os.WriteFile(filename, []byte(requestTemplate), 0644) } diff --git a/tools/goctl/api/tsgen/request.ts b/tools/goctl/api/tsgen/request.ts index a0a2845836e0..287f18297e9b 100644 --- a/tools/goctl/api/tsgen/request.ts +++ b/tools/goctl/api/tsgen/request.ts @@ -1,18 +1,22 @@ export type Method = - | 'get' - | 'GET' - | 'delete' - | 'DELETE' - | 'head' - | 'HEAD' - | 'options' - | 'OPTIONS' - | 'post' - | 'POST' - | 'put' - | 'PUT' - | 'patch' - | 'PATCH'; + | "get" + | "GET" + | "delete" + | "DELETE" + | "head" + | "HEAD" + | "options" + | "OPTIONS" + | "post" + | "POST" + | "put" + | "PUT" + | "patch" + | "PATCH"; + +export type QueryParams = { + [key: string | symbol | number]: string|number; +} | null; /** * Parse route parameters for responseType @@ -24,7 +28,7 @@ export function parseParams(url: string): Array { if (!ps) { return []; } - return ps.map((k) => k.replace(/:/, '')); + return ps.map((k) => k.replace(/:/, "")); } /** @@ -32,7 +36,7 @@ export function parseParams(url: string): Array { * @param url * @param params */ -export function genUrl(url: string, params: any) { +export function genUrl(url: string, params: QueryParams) { if (!params) { return url; } @@ -40,7 +44,7 @@ export function genUrl(url: string, params: any) { const ps = parseParams(url); ps.forEach((k) => { const reg = new RegExp(`:${k}`); - url = url.replace(reg, params[k]); + url = url.replace(reg, params[k].toString()); }); const path: Array = []; @@ -50,77 +54,83 @@ export function genUrl(url: string, params: any) { } } - return url + (path.length > 0 ? `?${path.join('&')}` : ''); + return url + (path.length > 0 ? `?${path.join("&")}` : ""); } -export async function request({ - method, - url, - data, - config = {} -}: { - method: Method; - url: string; - data?: unknown; - config?: unknown; -}) { +export async function request( + method: Method, + url: string, + config?: RequestInit +) { + if (config?.body && /get|head/i.test(method)) { + throw new Error( + "Request with GET/HEAD method cannot have body. *.api service use other method, example: POST or PUT." + ); + } const response = await fetch(url, { method: method.toLocaleUpperCase(), - credentials: 'include', + credentials: "include", + ...config, headers: { - 'Content-Type': 'application/json' + "Content-Type": "application/json", + ...config?.headers, }, - body: data ? JSON.stringify(data) : undefined, - // @ts-ignore - ...config }); return response.json(); } function api( - method: Method = 'get', + method: Method = "get", url: string, - req: any, - config?: unknown + params?: QueryParams, + config?: RequestInit ): Promise { - if (url.match(/:/) || method.match(/get|delete/i)) { - url = genUrl(url, req.params || req.forms); + if (params) { + url = genUrl(url, params); } method = method.toLocaleLowerCase() as Method; switch (method) { - case 'get': - return request({method: 'get', url, data: req, config}); - case 'delete': - return request({method: 'delete', url, data: req, config}); - case 'put': - return request({method: 'put', url, data: req, config}); - case 'post': - return request({method: 'post', url, data: req, config}); - case 'patch': - return request({method: 'patch', url, data: req, config}); + case "get": + return request("get", url, config); + case "delete": + return request("delete", url, config); + case "put": + return request("put", url, config); + case "post": + return request("post", url, config); + case "patch": + return request("patch", url, config); default: - return request({method: 'post', url, data: req, config}); + return request("post", url, config); } } export const webapi = { - get(url: string, req: unknown, config?: unknown): Promise { - return api('get', url, req, config); + get(url: string, params?: QueryParams, config?: RequestInit): Promise { + return api("get", url, params, config); }, - delete(url: string, req: unknown, config?: unknown): Promise { - return api('delete', url, req, config); + delete( + url: string, + params?: QueryParams, + config?: RequestInit + ): Promise { + return api("delete", url, params, config); }, - put(url: string, req: unknown, config?: unknown): Promise { - return api('put', url, req, config); + put(url: string, params?: QueryParams, config?: RequestInit): Promise { + return api("put", url, params, config); }, - post(url: string, req: unknown, config?: unknown): Promise { - return api('post', url, req, config); + post(url: string, params?: QueryParams, config?: RequestInit): Promise { + return api("post", url, params, config); + }, + patch( + url: string, + params?: QueryParams, + config?: RequestInit + ): Promise { + return api("patch", url, params, config); }, - patch(url: string, req: unknown, config?: unknown): Promise { - return api('patch', url, req, config); - } }; -export default webapi +export default webapi; diff --git a/tools/goctl/api/tsgen/util.go b/tools/goctl/api/tsgen/util.go index d105eea70b5f..4d0e32acea79 100644 --- a/tools/goctl/api/tsgen/util.go +++ b/tools/goctl/api/tsgen/util.go @@ -33,6 +33,9 @@ func writeProperty(writer io.Writer, member spec.Member, indent int) error { if err != nil { return err } + if strings.Contains(name, "-") { + name = fmt.Sprintf("\"%s\"", name) + } comment := member.GetComment() if len(comment) > 0 { @@ -150,7 +153,7 @@ func primitiveType(tp string) (string, bool) { } func writeType(writer io.Writer, tp spec.Type) error { - fmt.Fprintf(writer, "export interface %s {\n", util.Title(tp.Name())) + fmt.Fprintf(writer, "export type %s = {\n", util.Title(tp.Name())) if err := writeMembers(writer, tp, false, 1); err != nil { return err } @@ -170,14 +173,16 @@ func genParamsTypesIfNeed(writer io.Writer, tp spec.Type) error { return nil } - fmt.Fprintf(writer, "export interface %sParams {\n", util.Title(tp.Name())) - if err := writeTagMembers(writer, tp, formTagKey); err != nil { - return err + if len(definedType.GetTagMembers(formTagKey)) > 0 { + fmt.Fprintf(writer, "export type %sParams = {\n", util.Title(tp.Name())) + if err := writeTagMembers(writer, tp, formTagKey); err != nil { + return err + } + fmt.Fprintf(writer, "}\n") } - fmt.Fprintf(writer, "}\n") if len(definedType.GetTagMembers(headerTagKey)) > 0 { - fmt.Fprintf(writer, "export interface %sHeaders {\n", util.Title(tp.Name())) + fmt.Fprintf(writer, "export type %sHeaders = {\n", util.Title(tp.Name())) if err := writeTagMembers(writer, tp, headerTagKey); err != nil { return err } From 3f26ff83e0cfd88fca0d74a00ce155fc8e2772db Mon Sep 17 00:00:00 2001 From: chaosannals Date: Thu, 5 Dec 2024 17:59:49 +0800 Subject: [PATCH 02/25] tsgen: request(header,params,path) optional or omit. --- tools/goctl/api/spec/fn.go | 10 ++++++++++ tools/goctl/api/tsgen/util.go | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/tools/goctl/api/spec/fn.go b/tools/goctl/api/spec/fn.go index d208a8c20da8..464395cc1f7c 100644 --- a/tools/goctl/api/spec/fn.go +++ b/tools/goctl/api/spec/fn.go @@ -55,6 +55,16 @@ func (m Member) Tags() []*Tag { return tags.Tags() } +func (m Member) IsOptionalOrOmitEmpty() bool { + tag := m.Tags() + for _, item := range tag { + if stringx.Contains(item.Options, "optional") || stringx.Contains(item.Options, "omitempty") { + return true + } + } + return false +} + // IsOptional returns true if tag is optional func (m Member) IsOptional() bool { if !m.IsBodyMember() { diff --git a/tools/goctl/api/tsgen/util.go b/tools/goctl/api/tsgen/util.go index 4d0e32acea79..db4ee94831b4 100644 --- a/tools/goctl/api/tsgen/util.go +++ b/tools/goctl/api/tsgen/util.go @@ -26,7 +26,7 @@ func writeProperty(writer io.Writer, member spec.Member, indent int) error { } optionalTag := "" - if member.IsOptional() || member.IsOmitEmpty() { + if member.IsOptionalOrOmitEmpty() { optionalTag = "?" } name, err := member.GetPropertyName() From b6ee69c000affc5e5061c7102bf4e47d88d89bd6 Mon Sep 17 00:00:00 2001 From: chaosannals Date: Tue, 10 Dec 2024 15:33:17 +0800 Subject: [PATCH 03/25] phpgen. --- tools/goctl/api/cmd.go | 9 +- tools/goctl/api/phpgen/ApiBaseClient.php | 144 +++++++++++++++ tools/goctl/api/phpgen/ApiException.php | 16 ++ tools/goctl/api/phpgen/client.go | 163 +++++++++++++++++ tools/goctl/api/phpgen/cmd.go | 53 ++++++ tools/goctl/api/phpgen/msg.go | 214 +++++++++++++++++++++++ tools/goctl/api/phpgen/util.go | 48 +++++ 7 files changed, 646 insertions(+), 1 deletion(-) create mode 100644 tools/goctl/api/phpgen/ApiBaseClient.php create mode 100644 tools/goctl/api/phpgen/ApiException.php create mode 100644 tools/goctl/api/phpgen/client.go create mode 100644 tools/goctl/api/phpgen/cmd.go create mode 100644 tools/goctl/api/phpgen/msg.go create mode 100644 tools/goctl/api/phpgen/util.go diff --git a/tools/goctl/api/cmd.go b/tools/goctl/api/cmd.go index 0863805285eb..d513ffdd862a 100644 --- a/tools/goctl/api/cmd.go +++ b/tools/goctl/api/cmd.go @@ -10,6 +10,7 @@ import ( "github.com/zeromicro/go-zero/tools/goctl/api/javagen" "github.com/zeromicro/go-zero/tools/goctl/api/ktgen" "github.com/zeromicro/go-zero/tools/goctl/api/new" + "github.com/zeromicro/go-zero/tools/goctl/api/phpgen" "github.com/zeromicro/go-zero/tools/goctl/api/tsgen" "github.com/zeromicro/go-zero/tools/goctl/api/validate" "github.com/zeromicro/go-zero/tools/goctl/config" @@ -31,6 +32,7 @@ var ( ktCmd = cobrax.NewCommand("kt", cobrax.WithRunE(ktgen.KtCommand)) pluginCmd = cobrax.NewCommand("plugin", cobrax.WithRunE(plugin.PluginCommand)) tsCmd = cobrax.NewCommand("ts", cobrax.WithRunE(tsgen.TsCommand)) + phpCmd = cobrax.NewCommand("php", cobrax.WithRunE(phpgen.PhpCommand)) ) func init() { @@ -46,6 +48,7 @@ func init() { pluginCmdFlags = pluginCmd.Flags() tsCmdFlags = tsCmd.Flags() validateCmdFlags = validateCmd.Flags() + phpCmdFlags = phpCmd.Flags() ) apiCmdFlags.StringVar(&apigen.VarStringOutput, "o") @@ -98,6 +101,10 @@ func init() { validateCmdFlags.StringVar(&validate.VarStringAPI, "api") + phpCmdFlags.StringVar(&phpgen.VarStringDir, "dir") + phpCmdFlags.StringVar(&phpgen.VarStringAPI, "api") + phpCmdFlags.StringVar(&phpgen.VarStringNS, "ns") + // Add sub-commands - Cmd.AddCommand(dartCmd, docCmd, formatCmd, goCmd, javaCmd, ktCmd, newCmd, pluginCmd, tsCmd, validateCmd) + Cmd.AddCommand(dartCmd, docCmd, formatCmd, goCmd, javaCmd, ktCmd, newCmd, pluginCmd, tsCmd, validateCmd, phpCmd) } diff --git a/tools/goctl/api/phpgen/ApiBaseClient.php b/tools/goctl/api/phpgen/ApiBaseClient.php new file mode 100644 index 000000000000..7afaa69d6421 --- /dev/null +++ b/tools/goctl/api/phpgen/ApiBaseClient.php @@ -0,0 +1,144 @@ +host = $host; + $this->port = $port; + } + + public function getHost() + { + return $this->host; + } + public function getPort() + { + return $this->port; + } + + public function getAddress() + { + return "$this->host:$this->port"; + } + + protected function request( + $path, // 请求路径 + $method, // 请求方法 get post delete put + $params, // 请求路径参数 + $query, // 请求字符串 + $headers, // 头部字段 + $body // 内容体 + ) { + $address = $this->getAddress(); + if (!$headers) { + $headers = []; + } else { + $headers = $headers->toAssocArray(); + } + + // path + if ($params) { + $path = self::replacePathParams($path, $params->toAssocArray()); + } + $url = "$address$path"; + + // query + if ($query) { + $queryString = $query->toQueryString(); + $url = "$url?$queryString"; + } + + $ch = curl_init(); + + // 2. 设置请求选项, 包括具体的url + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1); // HTTP/1.1 + curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 4); /* 在发起连接前等待的时间,如果设置为0,则无限等待 */ + curl_setopt($ch, CURLOPT_TIMEOUT, 20); /* 设置cURL允许执行的最长秒数 */ + curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($ch, CURLOPT_HEADER, 1); + + // POST + if ($method == 'post') { + curl_setopt($ch, CURLOPT_POST, true); + } else { + curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method); + } + + // body + if ($body) { + $headers['Content-Type'] = 'application/json'; + curl_setopt($ch, CURLOPT_POSTFIELDS, $body->toJsonString()); + } + + // header + if (!empty($headers)) { + $header = []; + foreach ($headers as $k => $v) { + $header[] = "$k: $v"; + } + curl_setopt($ch, CURLOPT_HTTPHEADER, $header); + } + + // 3. 执行一个cURL会话并且获取相关回复 + curl_setopt($ch, CURLINFO_HEADER_OUT, true); + $response = curl_exec($ch); + // $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); + + // 4. 释放cURL句柄,关闭一个cURL会话 + curl_close($ch); + + return self::parseResponse($response); + } + + private static function parseResponse($response) + { + if ($response === false) { + throw new ApiException("curl exec failed.", -1); + } + + $statusEnd = strpos($response, "\r\n"); + $status = substr($response, 0, $statusEnd); + $status = explode(' ', $status, 3); + $statusCode = intval($status[1] ?? 0); + + if ($status < 200 && $status > 299) { + throw new ApiException("response failed.", -2, null, $response); + } + + $headerEnd = strpos($response, "\r\n\r\n"); + $header = substr($response, $statusEnd + 2, $headerEnd - ($statusEnd + 2)); + $header = explode("\r\n", $header); + $headers = []; + foreach ($header as $row) { + $kw = explode(':', $row, 2); + $headers[strtolower($kw[0])] = $kw[1]; + } + + $body = json_decode(substr($response, $headerEnd + 4), true); + + return [ + 'status' => $status[2], + 'statusCode' => $statusCode, + 'headers' => $headers, + 'body' => $body, + ]; + } + + // 填入路径参数 + private static function replacePathParams($path, $kw) + { + $map = []; + foreach ($kw as $k => $v) { + $map[":$k"] = $v; + } + $path = str_replace(array_keys($map), $map, $path); + return $path; + } +} diff --git a/tools/goctl/api/phpgen/ApiException.php b/tools/goctl/api/phpgen/ApiException.php new file mode 100644 index 000000000000..b89dd8e3a2f1 --- /dev/null +++ b/tools/goctl/api/phpgen/ApiException.php @@ -0,0 +1,16 @@ +responseContent = $responseContent; + parent::__construct($message, $code, $previous); + } + + public function getResponseContent() { + return $this->responseContent; + } +} diff --git a/tools/goctl/api/phpgen/client.go b/tools/goctl/api/phpgen/client.go new file mode 100644 index 000000000000..47174c66a7cc --- /dev/null +++ b/tools/goctl/api/phpgen/client.go @@ -0,0 +1,163 @@ +package phpgen + +import ( + _ "embed" + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/zeromicro/go-zero/tools/goctl/api/spec" +) + +//go:embed ApiBaseClient.php +var apiBaseClientTemplate string + +//go:embed ApiException.php +var apiExceptionTemplate string + +func genClient(dir string, ns string, api *spec.ApiSpec) error { + if err := writeBaseClient(dir, ns); err != nil { + return err + } + if err := writeException(dir, ns); err != nil { + return err + } + return writeClient(dir, ns, api) +} + +func writeBaseClient(dir string, ns string) error { + bPath := filepath.Join(dir, "ApiBaseClient.php") + bHead := fmt.Sprintf("request('%s', '%s',", r.Path, strings.ToLower(r.Method)) + + if r.RequestType != nil { + params := []string{} + for _, tagKey := range tagKeys { + if hasTagMembers(r.RequestType, tagKey) { + sn := camelCase(fmt.Sprintf("get-%s", tagToSubName(tagKey)), false) + params = append(params, fmt.Sprintf("$request->%s()", sn)) + } else { + params = append(params, "null") + } + } + fmt.Fprint(f, strings.Join(params, ",")) + } else { + fmt.Fprint(f, "null, null, null, null") + } + + fmt.Fprintln(f, ");") + + writeIndent(f, 8) + if r.ResponseType != nil { + n := camelCase(r.ResponseType.Name(), true) + fmt.Fprintf(f, "$response = new %s();\n", n) + definedType, ok := r.ResponseType.(spec.DefineStruct) + if !ok { + return fmt.Errorf("type %s not supported", n) + } + if err := writeResponseHeader(f, &definedType); err != nil { + return err + } + if err := writeResponseBody(f, &definedType); err != nil { + return err + } + writeIndent(f, 8) + fmt.Fprint(f, "return $response;\n") + } else { + fmt.Fprint(f, "return null;\n") + } + + writeIndent(f, 4) + fmt.Fprintln(f, "}") + } + } + + fmt.Fprintln(f, "}") + + return nil +} + +func writeResponseBody(f *os.File, definedType *spec.DefineStruct) error { + // 获取字段 + ms := definedType.GetTagMembers(bodyTagKey) + if len(ms) <= 0 { + return nil + } + writeIndent(f, 8) + fmt.Fprint(f, "$response->getBody()") + for _, m := range ms { + tags := m.Tags() + k := "" + if len(tags) > 0 { + k = tags[0].Name + } else { + k = m.Name + } + fmt.Fprintf(f, "\n ->set%s($result['body']['%s'])", camelCase(m.Name, true), k) + } + fmt.Fprintln(f, ";") + return nil +} + +func writeResponseHeader(f *os.File, definedType *spec.DefineStruct) error { + // 获取字段 + ms := definedType.GetTagMembers(headerTagKey) + if len(ms) <= 0 { + return nil + } + writeIndent(f, 8) + fmt.Fprint(f, "$response->getHeader()") + for _, m := range ms { + tags := m.Tags() + k := "" + if len(tags) > 0 { + k = tags[0].Name + } else { + k = m.Name + } + fmt.Fprintf(f, "\n ->set%s($result['header']['%s'])", camelCase(m.Name, true), k) + } + fmt.Fprintln(f, ";") + return nil +} diff --git a/tools/goctl/api/phpgen/cmd.go b/tools/goctl/api/phpgen/cmd.go new file mode 100644 index 000000000000..bb36359af964 --- /dev/null +++ b/tools/goctl/api/phpgen/cmd.go @@ -0,0 +1,53 @@ +package phpgen + +import ( + "errors" + "fmt" + + "github.com/gookit/color" + "github.com/spf13/cobra" + "github.com/zeromicro/go-zero/core/logx" + "github.com/zeromicro/go-zero/tools/goctl/api/parser" + "github.com/zeromicro/go-zero/tools/goctl/util/pathx" +) + +var ( + // VarStringDir describes a directory. + VarStringDir string + // VarStringAPI describes an API file. + VarStringAPI string + // VarStringAPI describes an PHP namespace. + VarStringNS string +) + +func PhpCommand(_ *cobra.Command, _ []string) error { + apiFile := VarStringAPI + if apiFile == "" { + return errors.New("missing -api") + } + dir := VarStringDir + if dir == "" { + return errors.New("missing -dir") + } + + ns := VarStringNS + if ns == "" { + return errors.New("missing -ns") + } + + api, e := parser.Parse(apiFile) + if e != nil { + return e + } + + if err := api.Validate(); err != nil { + return err + } + + logx.Must(pathx.MkdirIfNotExist(dir)) + logx.Must(genMessages(dir, ns, api)) + logx.Must(genClient(dir, ns, api)) + + fmt.Println(color.Green.Render("Done.")) + return nil +} diff --git a/tools/goctl/api/phpgen/msg.go b/tools/goctl/api/phpgen/msg.go new file mode 100644 index 000000000000..029db699a2f0 --- /dev/null +++ b/tools/goctl/api/phpgen/msg.go @@ -0,0 +1,214 @@ +package phpgen + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/zeromicro/go-zero/tools/goctl/api/spec" +) + +const ( + formTagKey = "form" + pathTagKey = "path" + headerTagKey = "header" + bodyTagKey = "json" +) + +var ( + // 这个顺序与 PHP ApiBaseClient request 参数相关,改动时要注意 PHP 那边的代码。 + tagKeys = []string{pathTagKey, formTagKey, headerTagKey, bodyTagKey} +) + +func tagToSubName(tagKey string) string { + suffix := tagKey + switch tagKey { + case "json": + suffix = "body" + case "form": + suffix = "query" + } + return suffix +} + +func getMessageName(tn string, tagKey string, isPascal bool) string { + suffix := tagToSubName(tagKey) + return camelCase(fmt.Sprintf("%s-%s", tn, suffix), isPascal) +} + +func hasTagMembers(t spec.Type, tagKey string) bool { + definedType, ok := t.(spec.DefineStruct) + if !ok { + return false + } + ms := definedType.GetTagMembers(tagKey) + return len(ms) > 0 +} + +func genMessages(dir string, ns string, api *spec.ApiSpec) error { + for _, t := range api.Types { + tn := t.Name() + definedType, ok := t.(spec.DefineStruct) + if !ok { + return fmt.Errorf("type %s not supported", tn) + } + + // 子类型 + tags := []string{} + for _, tagKey := range tagKeys { + // 获取字段 + ms := definedType.GetTagMembers(tagKey) + if len(ms) <= 0 { + continue + } + + // 打开文件 + cn := getMessageName(tn, tagKey, true) + tags = append(tags, tagKey) + fp := filepath.Join(dir, fmt.Sprintf("%s.php", cn)) + f, err := os.OpenFile(fp, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o644) + if err != nil { + return err + } + defer f.Close() + + // 写入 + if err := writeSubMessage(f, ns, cn, ms); err != nil { + return err + } + } + + // 主类型 + rn := camelCase(tn, true) + fp := filepath.Join(dir, fmt.Sprintf("%s.php", rn)) + f, err := os.OpenFile(fp, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o644) + if err != nil { + return err + } + defer f.Close() + + if err := writeMessage(f, ns, rn, tags); err != nil { + return nil + } + } + + return nil +} + +func writeMessage(f *os.File, ns string, rn string, tags []string) error { + // 文件头 + fmt.Fprintln(f, "%s = new %s();\n", sn, cn) + } + writeIndent(f, 4) + fmt.Fprintln(f, "}") + + // getter + for _, tag := range tags { + sn := tagToSubName(tag) + pn := camelCase(fmt.Sprintf("get-%s", sn), false) + writeIndent(f, 4) + fmt.Fprintf(f, "public function %s() { return $this->%s; }\n", pn, sn) + } + + fmt.Fprintln(f, "}") + + return nil +} + +func writeSubMessage(f *os.File, ns string, cn string, ms []spec.Member) error { + // 文件头 + fmt.Fprintln(f, " 0 { + k = tags[0].Name + } else { + k = n + } + writeIndent(f, indent) + fmt.Fprintf(f, "'%s' => $this->%s,\n", k, n) + } +} + +func writeField(f *os.File, m spec.Member) { + writeIndent(f, 4) + fmt.Fprintf(f, "private $%s;\n", camelCase(m.Name, false)) +} + +func writeProperty(f *os.File, m spec.Member) { + pName := camelCase(m.Name, true) + cName := camelCase(m.Name, false) + writeIndent(f, 4) + fmt.Fprintf(f, "public function get%s() { return $this->%s; }\n\n", pName, cName) + writeIndent(f, 4) + fmt.Fprintf(f, "public function set%s($v) { $this->%s = $v; return $this; }\n\n", pName, cName) +} + +func writeIndent(f *os.File, n int) { + for i := 0; i < n; i++ { + fmt.Fprint(f, " ") + } +} diff --git a/tools/goctl/api/phpgen/util.go b/tools/goctl/api/phpgen/util.go new file mode 100644 index 000000000000..955c7954d8f8 --- /dev/null +++ b/tools/goctl/api/phpgen/util.go @@ -0,0 +1,48 @@ +package phpgen + +import ( + "regexp" + "strings" + + "golang.org/x/text/cases" + "golang.org/x/text/language" +) + +func camelCase(raw string, isPascal bool) string { + re := regexp.MustCompile("[A-Z_/: -]") + vs := re.FindAllStringIndex(raw, -1) + + // 全小写 + if len(vs) == 0 { + return raw + } + + // 小写开头 + if vs[0][0] > 0 { + vs = append([][]int{{0, vs[0][0]}}, vs...) + } + + // 满 + vc := len(vs) + for i := 0; i < vc; i++ { + if (i + 1) < len(vs) { + vs[i][1] = vs[i+1][0] + } else { + vs[i][1] = len(raw) + } + } + + // 驼峰 + ss := make([]string, len(vs)) + c := cases.Title(language.English) + for i, v := range vs { + s := strings.Trim(raw[v[0]:v[1]], "/:_ -") + if i == 0 && !isPascal { + ss[i] = strings.ToLower(s) + } else { + ss[i] = c.String(s) + } + } + + return strings.Join(ss, "") +} From 38727324a77b5c3259eecd2352208acf7d0178bf Mon Sep 17 00:00:00 2001 From: chaosannals Date: Wed, 11 Dec 2024 09:17:47 +0800 Subject: [PATCH 04/25] fix: php gen request. --- tools/goctl/api/phpgen/ApiBaseClient.php | 11 +++++------ tools/goctl/api/phpgen/client.go | 8 ++++++-- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/tools/goctl/api/phpgen/ApiBaseClient.php b/tools/goctl/api/phpgen/ApiBaseClient.php index 7afaa69d6421..c71618abc9bf 100644 --- a/tools/goctl/api/phpgen/ApiBaseClient.php +++ b/tools/goctl/api/phpgen/ApiBaseClient.php @@ -89,7 +89,10 @@ protected function request( // 3. 执行一个cURL会话并且获取相关回复 curl_setopt($ch, CURLINFO_HEADER_OUT, true); $response = curl_exec($ch); - // $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); + if ($response === false) { + $error = curl_error($ch); + throw new ApiException("curl exec failed.".var_export($error, true), -1); + } // 4. 释放cURL句柄,关闭一个cURL会话 curl_close($ch); @@ -99,10 +102,6 @@ protected function request( private static function parseResponse($response) { - if ($response === false) { - throw new ApiException("curl exec failed.", -1); - } - $statusEnd = strpos($response, "\r\n"); $status = substr($response, 0, $statusEnd); $status = explode(' ', $status, 3); @@ -118,7 +117,7 @@ private static function parseResponse($response) $headers = []; foreach ($header as $row) { $kw = explode(':', $row, 2); - $headers[strtolower($kw[0])] = $kw[1]; + $headers[strtolower($kw[0])] = $kw[1] ?? null; } $body = json_decode(substr($response, $headerEnd + 4), true); diff --git a/tools/goctl/api/phpgen/client.go b/tools/goctl/api/phpgen/client.go index 47174c66a7cc..b45e6370169a 100644 --- a/tools/goctl/api/phpgen/client.go +++ b/tools/goctl/api/phpgen/client.go @@ -58,18 +58,22 @@ func writeClient(dir string, ns string, api *spec.ApiSpec) error { fmt.Fprintf(f, "class %sClient extends ApiBaseClient {\n", name) for _, g := range api.Service.Groups { + prefix := g.GetAnnotation("prefix") + p := camelCase(prefix, true) + // 路由 for _, r := range g.Routes { an := camelCase(r.Path, true) + writeIndent(f, 4) - fmt.Fprintf(f, "public function %s%s(", strings.ToLower(r.Method), an) + fmt.Fprintf(f, "public function %s%s%s(", strings.ToLower(r.Method), p, an) if r.RequestType != nil { fmt.Fprint(f, "$request") } fmt.Fprintln(f, ") {") writeIndent(f, 8) - fmt.Fprintf(f, "$result = $this->request('%s', '%s',", r.Path, strings.ToLower(r.Method)) + fmt.Fprintf(f, "$result = $this->request('%s%s', '%s',", prefix, r.Path, strings.ToLower(r.Method)) if r.RequestType != nil { params := []string{} From 67d09f28095a2faa0e573d30399de5295f678941 Mon Sep 17 00:00:00 2001 From: chaosannals Date: Wed, 11 Dec 2024 09:59:06 +0800 Subject: [PATCH 05/25] php gen custom body. --- tools/goctl/api/phpgen/ApiBody.php | 27 ++++++++++++++++ tools/goctl/api/phpgen/client.go | 51 +++++++++++++++++------------- 2 files changed, 56 insertions(+), 22 deletions(-) create mode 100644 tools/goctl/api/phpgen/ApiBody.php diff --git a/tools/goctl/api/phpgen/ApiBody.php b/tools/goctl/api/phpgen/ApiBody.php new file mode 100644 index 000000000000..f2606eeedf9c --- /dev/null +++ b/tools/goctl/api/phpgen/ApiBody.php @@ -0,0 +1,27 @@ +data = $data; + } + + public function toJsonString() + { + return json_encode($this->data, JSON_UNESCAPED_UNICODE); + } + + public function toAssocArray() + { + return $this->data; + } +} diff --git a/tools/goctl/api/phpgen/client.go b/tools/goctl/api/phpgen/client.go index b45e6370169a..232c4ea0da16 100644 --- a/tools/goctl/api/phpgen/client.go +++ b/tools/goctl/api/phpgen/client.go @@ -16,30 +16,28 @@ var apiBaseClientTemplate string //go:embed ApiException.php var apiExceptionTemplate string +//go:embed ApiBody.php +var apiBodyTemplate string + func genClient(dir string, ns string, api *spec.ApiSpec) error { - if err := writeBaseClient(dir, ns); err != nil { + if err := writeTemplate(dir, ns, "ApiBaseClient", apiBaseClientTemplate); err != nil { + return err + } + if err := writeTemplate(dir, ns, "ApiException", apiExceptionTemplate); err != nil { return err } - if err := writeException(dir, ns); err != nil { + if err := writeTemplate(dir, ns, "ApiBody", apiBodyTemplate); err != nil { return err } return writeClient(dir, ns, api) } -func writeBaseClient(dir string, ns string) error { - bPath := filepath.Join(dir, "ApiBaseClient.php") - bHead := fmt.Sprintf("request('%s%s', '%s',", prefix, r.Path, strings.ToLower(r.Method)) + fmt.Fprintf(f, "$result = $this->request('%s%s', '%s',", prefix, r.Path, method) if r.RequestType != nil { params := []string{} for _, tagKey := range tagKeys { if hasTagMembers(r.RequestType, tagKey) { sn := camelCase(fmt.Sprintf("get-%s", tagToSubName(tagKey)), false) - params = append(params, fmt.Sprintf("$request->%s()", sn)) + if tagKey == bodyTagKey { + params = append(params, fmt.Sprintf("$body ?? $request->%s()", sn)) + } else { + params = append(params, fmt.Sprintf("$request->%s()", sn)) + } } else { - params = append(params, "null") + if tagKey == bodyTagKey { + params = append(params, "$body") + } else { + params = append(params, "null") + } } } fmt.Fprint(f, strings.Join(params, ",")) } else { - fmt.Fprint(f, "null, null, null, null") + fmt.Fprint(f, "null, null, null, $body") } fmt.Fprintln(f, ");") From c429c6705c591e4e827d4498611143a37b7cdef8 Mon Sep 17 00:00:00 2001 From: chaosannals Date: Wed, 11 Dec 2024 10:08:54 +0800 Subject: [PATCH 06/25] fix: php gen exception. --- tools/goctl/api/phpgen/ApiBaseClient.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/goctl/api/phpgen/ApiBaseClient.php b/tools/goctl/api/phpgen/ApiBaseClient.php index c71618abc9bf..01efab650394 100644 --- a/tools/goctl/api/phpgen/ApiBaseClient.php +++ b/tools/goctl/api/phpgen/ApiBaseClient.php @@ -91,7 +91,7 @@ protected function request( $response = curl_exec($ch); if ($response === false) { $error = curl_error($ch); - throw new ApiException("curl exec failed.".var_export($error, true), -1); + throw new ApiException("curl exec failed." . var_export($error, true), -1); } // 4. 释放cURL句柄,关闭一个cURL会话 @@ -107,7 +107,7 @@ private static function parseResponse($response) $status = explode(' ', $status, 3); $statusCode = intval($status[1] ?? 0); - if ($status < 200 && $status > 299) { + if ($statusCode < 200 || $statusCode > 299) { throw new ApiException("response failed.", -2, null, $response); } From 4cee12c59e35632e41addf80ea63426f3162a501 Mon Sep 17 00:00:00 2001 From: chaosannals Date: Wed, 11 Dec 2024 10:43:59 +0800 Subject: [PATCH 07/25] fix: curl method upper. --- tools/goctl/api/phpgen/ApiBaseClient.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/goctl/api/phpgen/ApiBaseClient.php b/tools/goctl/api/phpgen/ApiBaseClient.php index 01efab650394..5ea7f15c9e5c 100644 --- a/tools/goctl/api/phpgen/ApiBaseClient.php +++ b/tools/goctl/api/phpgen/ApiBaseClient.php @@ -68,7 +68,7 @@ protected function request( if ($method == 'post') { curl_setopt($ch, CURLOPT_POST, true); } else { - curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method); + curl_setopt($ch, CURLOPT_CUSTOMREQUEST, strtoupper($method)); } // body From c6a56871cf96930754e5dfdd98de30d2b69aa3c6 Mon Sep 17 00:00:00 2001 From: chaosannals Date: Wed, 11 Dec 2024 16:07:34 +0800 Subject: [PATCH 08/25] ts gen request custom url prefix and body. --- tools/goctl/api/cmd.go | 2 ++ tools/goctl/api/tsgen/gen.go | 4 ++++ tools/goctl/api/tsgen/genpacket.go | 25 +++++++++++++++++++++---- tools/goctl/api/tsgen/request.ts | 6 +++++- 4 files changed, 32 insertions(+), 5 deletions(-) diff --git a/tools/goctl/api/cmd.go b/tools/goctl/api/cmd.go index d513ffdd862a..f4e82f646d5b 100644 --- a/tools/goctl/api/cmd.go +++ b/tools/goctl/api/cmd.go @@ -98,6 +98,8 @@ func init() { tsCmdFlags.StringVar(&tsgen.VarStringAPI, "api") tsCmdFlags.StringVar(&tsgen.VarStringCaller, "caller") tsCmdFlags.BoolVar(&tsgen.VarBoolUnWrap, "unwrap") + tsCmdFlags.StringVar(&tsgen.VarStringUrlPrefix, "url") + tsCmdFlags.BoolVar(&tsgen.VarBoolCustomBody, "body") validateCmdFlags.StringVar(&validate.VarStringAPI, "api") diff --git a/tools/goctl/api/tsgen/gen.go b/tools/goctl/api/tsgen/gen.go index 34732b67622d..3714658f1907 100644 --- a/tools/goctl/api/tsgen/gen.go +++ b/tools/goctl/api/tsgen/gen.go @@ -22,6 +22,10 @@ var ( VarStringCaller string // VarBoolUnWrap describes whether wrap or not. VarBoolUnWrap bool + // VarStringUrlPrefix request url prefix + VarStringUrlPrefix string + // VarBoolCustomBody request custom body + VarBoolCustomBody bool ) // TsCommand provides the entry to generate typescript codes diff --git a/tools/goctl/api/tsgen/genpacket.go b/tools/goctl/api/tsgen/genpacket.go index 152c15c2f769..f809813d392b 100644 --- a/tools/goctl/api/tsgen/genpacket.go +++ b/tools/goctl/api/tsgen/genpacket.go @@ -78,7 +78,11 @@ func genAPI(api *spec.ApiSpec, caller string) (string, error) { if len(comment) > 0 { fmt.Fprintf(&builder, "%s\n", comment) } - fmt.Fprintf(&builder, "export function %s(%s) {\n", handler, paramsForRoute(route)) + genericsType := "" + if VarBoolCustomBody { + genericsType = "" + } + fmt.Fprintf(&builder, "export function %s%s(%s) {\n", handler, genericsType, paramsForRoute(route)) writeIndent(&builder, 1) responseGeneric := "" if len(route.ResponseTypeName()) > 0 { @@ -101,6 +105,9 @@ func genAPI(api *spec.ApiSpec, caller string) (string, error) { func paramsForRoute(route spec.Route) string { if route.RequestType == nil { + if VarBoolCustomBody { + return "body?: T" + } return "" } hasParams := pathHasParams(route) @@ -141,6 +148,10 @@ func paramsForRoute(route spec.Route) string { } } } + + if VarBoolCustomBody { + params = append(params, "body?: T") + } return strings.Join(params, ", ") } @@ -180,7 +191,13 @@ func callParamsForRoute(route spec.Route, group spec.Group) string { configParams := []string{} if hasBody { - configParams = append(configParams, "body: JSON.stringify(req)") + if VarBoolCustomBody { + configParams = append(configParams, "body: JSON.stringify(body ?? req)") + } else { + configParams = append(configParams, "body: JSON.stringify(req)") + } + } else if VarBoolCustomBody { + configParams = append(configParams, "body: body ? JSON.stringify(body): null") } if hasHeader { configParams = append(configParams, "headers: headers") @@ -205,12 +222,12 @@ func pathForRoute(route spec.Route, group spec.Group) string { routePath = strings.Join(pathSlice, "/") } if len(prefix) == 0 { - return "`" + routePath + "`" + return "`" + VarStringUrlPrefix + routePath + "`" } prefix = strings.TrimPrefix(prefix, `"`) prefix = strings.TrimSuffix(prefix, `"`) - return fmt.Sprintf("`%s/%s`", prefix, strings.TrimPrefix(routePath, "/")) + return fmt.Sprintf("`%s%s/%s`", VarStringUrlPrefix, prefix, strings.TrimPrefix(routePath, "/")) } func pathHasParams(route spec.Route) bool { diff --git a/tools/goctl/api/tsgen/request.ts b/tools/goctl/api/tsgen/request.ts index 287f18297e9b..1f39967f39bb 100644 --- a/tools/goctl/api/tsgen/request.ts +++ b/tools/goctl/api/tsgen/request.ts @@ -77,7 +77,11 @@ export async function request( }, }); - return response.json(); + if (response.headers.get('Content-Type') == 'application/json') { + return response.json(); + } else { + return response.text(); + } } function api( From ac394ebd639e096e2953d527ae8b2e25818fe057 Mon Sep 17 00:00:00 2001 From: chaosannals Date: Wed, 11 Dec 2024 17:30:19 +0800 Subject: [PATCH 09/25] php gen return $result when not Response Type. --- tools/goctl/api/phpgen/client.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/goctl/api/phpgen/client.go b/tools/goctl/api/phpgen/client.go index 232c4ea0da16..8f363a1bd1be 100644 --- a/tools/goctl/api/phpgen/client.go +++ b/tools/goctl/api/phpgen/client.go @@ -116,7 +116,7 @@ func writeClient(dir string, ns string, api *spec.ApiSpec) error { writeIndent(f, 8) fmt.Fprint(f, "return $response;\n") } else { - fmt.Fprint(f, "return null;\n") + fmt.Fprint(f, "return $result;\n") } writeIndent(f, 4) From 7090dc1825715b46c8c11e62c55be0438cc6f85d Mon Sep 17 00:00:00 2001 From: chaosannals Date: Thu, 5 Dec 2024 16:26:22 +0800 Subject: [PATCH 10/25] tsgen adjust: field name '-' add " and change request and response 'interface' to 'type'. change 'unknown' to some ts types. --- tools/goctl/api/tsgen/genpacket.go | 13 ++- tools/goctl/api/tsgen/genrequest.go | 5 -- tools/goctl/api/tsgen/request.ts | 134 +++++++++++++++------------- tools/goctl/api/tsgen/util.go | 17 ++-- 4 files changed, 93 insertions(+), 76 deletions(-) diff --git a/tools/goctl/api/tsgen/genpacket.go b/tools/goctl/api/tsgen/genpacket.go index 6635220a3c56..152c15c2f769 100644 --- a/tools/goctl/api/tsgen/genpacket.go +++ b/tools/goctl/api/tsgen/genpacket.go @@ -173,14 +173,21 @@ func callParamsForRoute(route spec.Route, group spec.Group) string { var params = []string{pathForRoute(route, group)} if hasParams { params = append(params, "params") + } else { + params = append(params, "null") } + + configParams := []string{} + if hasBody { - params = append(params, "req") + configParams = append(configParams, "body: JSON.stringify(req)") } if hasHeader { - params = append(params, "headers") + configParams = append(configParams, "headers: headers") } + params = append(params, fmt.Sprintf("{%s}", strings.Join(configParams, ", "))) + return strings.Join(params, ", ") } @@ -212,7 +219,7 @@ func pathHasParams(route spec.Route) bool { return false } - return len(ds.Members) != len(ds.GetBodyMembers()) + return len(ds.Members) != (len(ds.GetBodyMembers()) + len(ds.GetTagMembers(headerTagKey)) + len(ds.GetTagMembers(pathTagKey))) } func hasRequestBody(route spec.Route) bool { diff --git a/tools/goctl/api/tsgen/genrequest.go b/tools/goctl/api/tsgen/genrequest.go index 440ae4cb8be0..0218aa3045a4 100644 --- a/tools/goctl/api/tsgen/genrequest.go +++ b/tools/goctl/api/tsgen/genrequest.go @@ -4,8 +4,6 @@ import ( _ "embed" "os" "path/filepath" - - "github.com/zeromicro/go-zero/tools/goctl/util/pathx" ) //go:embed request.ts @@ -18,9 +16,6 @@ func genRequest(dir string) error { } filename := filepath.Join(abs, "gocliRequest.ts") - if pathx.FileExists(filename) { - return nil - } return os.WriteFile(filename, []byte(requestTemplate), 0644) } diff --git a/tools/goctl/api/tsgen/request.ts b/tools/goctl/api/tsgen/request.ts index a0a2845836e0..287f18297e9b 100644 --- a/tools/goctl/api/tsgen/request.ts +++ b/tools/goctl/api/tsgen/request.ts @@ -1,18 +1,22 @@ export type Method = - | 'get' - | 'GET' - | 'delete' - | 'DELETE' - | 'head' - | 'HEAD' - | 'options' - | 'OPTIONS' - | 'post' - | 'POST' - | 'put' - | 'PUT' - | 'patch' - | 'PATCH'; + | "get" + | "GET" + | "delete" + | "DELETE" + | "head" + | "HEAD" + | "options" + | "OPTIONS" + | "post" + | "POST" + | "put" + | "PUT" + | "patch" + | "PATCH"; + +export type QueryParams = { + [key: string | symbol | number]: string|number; +} | null; /** * Parse route parameters for responseType @@ -24,7 +28,7 @@ export function parseParams(url: string): Array { if (!ps) { return []; } - return ps.map((k) => k.replace(/:/, '')); + return ps.map((k) => k.replace(/:/, "")); } /** @@ -32,7 +36,7 @@ export function parseParams(url: string): Array { * @param url * @param params */ -export function genUrl(url: string, params: any) { +export function genUrl(url: string, params: QueryParams) { if (!params) { return url; } @@ -40,7 +44,7 @@ export function genUrl(url: string, params: any) { const ps = parseParams(url); ps.forEach((k) => { const reg = new RegExp(`:${k}`); - url = url.replace(reg, params[k]); + url = url.replace(reg, params[k].toString()); }); const path: Array = []; @@ -50,77 +54,83 @@ export function genUrl(url: string, params: any) { } } - return url + (path.length > 0 ? `?${path.join('&')}` : ''); + return url + (path.length > 0 ? `?${path.join("&")}` : ""); } -export async function request({ - method, - url, - data, - config = {} -}: { - method: Method; - url: string; - data?: unknown; - config?: unknown; -}) { +export async function request( + method: Method, + url: string, + config?: RequestInit +) { + if (config?.body && /get|head/i.test(method)) { + throw new Error( + "Request with GET/HEAD method cannot have body. *.api service use other method, example: POST or PUT." + ); + } const response = await fetch(url, { method: method.toLocaleUpperCase(), - credentials: 'include', + credentials: "include", + ...config, headers: { - 'Content-Type': 'application/json' + "Content-Type": "application/json", + ...config?.headers, }, - body: data ? JSON.stringify(data) : undefined, - // @ts-ignore - ...config }); return response.json(); } function api( - method: Method = 'get', + method: Method = "get", url: string, - req: any, - config?: unknown + params?: QueryParams, + config?: RequestInit ): Promise { - if (url.match(/:/) || method.match(/get|delete/i)) { - url = genUrl(url, req.params || req.forms); + if (params) { + url = genUrl(url, params); } method = method.toLocaleLowerCase() as Method; switch (method) { - case 'get': - return request({method: 'get', url, data: req, config}); - case 'delete': - return request({method: 'delete', url, data: req, config}); - case 'put': - return request({method: 'put', url, data: req, config}); - case 'post': - return request({method: 'post', url, data: req, config}); - case 'patch': - return request({method: 'patch', url, data: req, config}); + case "get": + return request("get", url, config); + case "delete": + return request("delete", url, config); + case "put": + return request("put", url, config); + case "post": + return request("post", url, config); + case "patch": + return request("patch", url, config); default: - return request({method: 'post', url, data: req, config}); + return request("post", url, config); } } export const webapi = { - get(url: string, req: unknown, config?: unknown): Promise { - return api('get', url, req, config); + get(url: string, params?: QueryParams, config?: RequestInit): Promise { + return api("get", url, params, config); }, - delete(url: string, req: unknown, config?: unknown): Promise { - return api('delete', url, req, config); + delete( + url: string, + params?: QueryParams, + config?: RequestInit + ): Promise { + return api("delete", url, params, config); }, - put(url: string, req: unknown, config?: unknown): Promise { - return api('put', url, req, config); + put(url: string, params?: QueryParams, config?: RequestInit): Promise { + return api("put", url, params, config); }, - post(url: string, req: unknown, config?: unknown): Promise { - return api('post', url, req, config); + post(url: string, params?: QueryParams, config?: RequestInit): Promise { + return api("post", url, params, config); + }, + patch( + url: string, + params?: QueryParams, + config?: RequestInit + ): Promise { + return api("patch", url, params, config); }, - patch(url: string, req: unknown, config?: unknown): Promise { - return api('patch', url, req, config); - } }; -export default webapi +export default webapi; diff --git a/tools/goctl/api/tsgen/util.go b/tools/goctl/api/tsgen/util.go index d105eea70b5f..4d0e32acea79 100644 --- a/tools/goctl/api/tsgen/util.go +++ b/tools/goctl/api/tsgen/util.go @@ -33,6 +33,9 @@ func writeProperty(writer io.Writer, member spec.Member, indent int) error { if err != nil { return err } + if strings.Contains(name, "-") { + name = fmt.Sprintf("\"%s\"", name) + } comment := member.GetComment() if len(comment) > 0 { @@ -150,7 +153,7 @@ func primitiveType(tp string) (string, bool) { } func writeType(writer io.Writer, tp spec.Type) error { - fmt.Fprintf(writer, "export interface %s {\n", util.Title(tp.Name())) + fmt.Fprintf(writer, "export type %s = {\n", util.Title(tp.Name())) if err := writeMembers(writer, tp, false, 1); err != nil { return err } @@ -170,14 +173,16 @@ func genParamsTypesIfNeed(writer io.Writer, tp spec.Type) error { return nil } - fmt.Fprintf(writer, "export interface %sParams {\n", util.Title(tp.Name())) - if err := writeTagMembers(writer, tp, formTagKey); err != nil { - return err + if len(definedType.GetTagMembers(formTagKey)) > 0 { + fmt.Fprintf(writer, "export type %sParams = {\n", util.Title(tp.Name())) + if err := writeTagMembers(writer, tp, formTagKey); err != nil { + return err + } + fmt.Fprintf(writer, "}\n") } - fmt.Fprintf(writer, "}\n") if len(definedType.GetTagMembers(headerTagKey)) > 0 { - fmt.Fprintf(writer, "export interface %sHeaders {\n", util.Title(tp.Name())) + fmt.Fprintf(writer, "export type %sHeaders = {\n", util.Title(tp.Name())) if err := writeTagMembers(writer, tp, headerTagKey); err != nil { return err } From 83102c5ae6121084c70a0cc910a14c36be6c4fdc Mon Sep 17 00:00:00 2001 From: chaosannals Date: Thu, 5 Dec 2024 17:59:49 +0800 Subject: [PATCH 11/25] tsgen: request(header,params,path) optional or omit. --- tools/goctl/api/spec/fn.go | 10 ++++++++++ tools/goctl/api/tsgen/util.go | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/tools/goctl/api/spec/fn.go b/tools/goctl/api/spec/fn.go index d208a8c20da8..464395cc1f7c 100644 --- a/tools/goctl/api/spec/fn.go +++ b/tools/goctl/api/spec/fn.go @@ -55,6 +55,16 @@ func (m Member) Tags() []*Tag { return tags.Tags() } +func (m Member) IsOptionalOrOmitEmpty() bool { + tag := m.Tags() + for _, item := range tag { + if stringx.Contains(item.Options, "optional") || stringx.Contains(item.Options, "omitempty") { + return true + } + } + return false +} + // IsOptional returns true if tag is optional func (m Member) IsOptional() bool { if !m.IsBodyMember() { diff --git a/tools/goctl/api/tsgen/util.go b/tools/goctl/api/tsgen/util.go index 4d0e32acea79..db4ee94831b4 100644 --- a/tools/goctl/api/tsgen/util.go +++ b/tools/goctl/api/tsgen/util.go @@ -26,7 +26,7 @@ func writeProperty(writer io.Writer, member spec.Member, indent int) error { } optionalTag := "" - if member.IsOptional() || member.IsOmitEmpty() { + if member.IsOptionalOrOmitEmpty() { optionalTag = "?" } name, err := member.GetPropertyName() From 4e0ecaf85596fef1d2e87c02ba25af4821460f06 Mon Sep 17 00:00:00 2001 From: chaosannals Date: Tue, 10 Dec 2024 15:33:17 +0800 Subject: [PATCH 12/25] phpgen. --- tools/goctl/api/cmd.go | 9 +- tools/goctl/api/phpgen/ApiBaseClient.php | 144 +++++++++++++++ tools/goctl/api/phpgen/ApiException.php | 16 ++ tools/goctl/api/phpgen/client.go | 163 +++++++++++++++++ tools/goctl/api/phpgen/cmd.go | 53 ++++++ tools/goctl/api/phpgen/msg.go | 214 +++++++++++++++++++++++ tools/goctl/api/phpgen/util.go | 48 +++++ 7 files changed, 646 insertions(+), 1 deletion(-) create mode 100644 tools/goctl/api/phpgen/ApiBaseClient.php create mode 100644 tools/goctl/api/phpgen/ApiException.php create mode 100644 tools/goctl/api/phpgen/client.go create mode 100644 tools/goctl/api/phpgen/cmd.go create mode 100644 tools/goctl/api/phpgen/msg.go create mode 100644 tools/goctl/api/phpgen/util.go diff --git a/tools/goctl/api/cmd.go b/tools/goctl/api/cmd.go index 0863805285eb..d513ffdd862a 100644 --- a/tools/goctl/api/cmd.go +++ b/tools/goctl/api/cmd.go @@ -10,6 +10,7 @@ import ( "github.com/zeromicro/go-zero/tools/goctl/api/javagen" "github.com/zeromicro/go-zero/tools/goctl/api/ktgen" "github.com/zeromicro/go-zero/tools/goctl/api/new" + "github.com/zeromicro/go-zero/tools/goctl/api/phpgen" "github.com/zeromicro/go-zero/tools/goctl/api/tsgen" "github.com/zeromicro/go-zero/tools/goctl/api/validate" "github.com/zeromicro/go-zero/tools/goctl/config" @@ -31,6 +32,7 @@ var ( ktCmd = cobrax.NewCommand("kt", cobrax.WithRunE(ktgen.KtCommand)) pluginCmd = cobrax.NewCommand("plugin", cobrax.WithRunE(plugin.PluginCommand)) tsCmd = cobrax.NewCommand("ts", cobrax.WithRunE(tsgen.TsCommand)) + phpCmd = cobrax.NewCommand("php", cobrax.WithRunE(phpgen.PhpCommand)) ) func init() { @@ -46,6 +48,7 @@ func init() { pluginCmdFlags = pluginCmd.Flags() tsCmdFlags = tsCmd.Flags() validateCmdFlags = validateCmd.Flags() + phpCmdFlags = phpCmd.Flags() ) apiCmdFlags.StringVar(&apigen.VarStringOutput, "o") @@ -98,6 +101,10 @@ func init() { validateCmdFlags.StringVar(&validate.VarStringAPI, "api") + phpCmdFlags.StringVar(&phpgen.VarStringDir, "dir") + phpCmdFlags.StringVar(&phpgen.VarStringAPI, "api") + phpCmdFlags.StringVar(&phpgen.VarStringNS, "ns") + // Add sub-commands - Cmd.AddCommand(dartCmd, docCmd, formatCmd, goCmd, javaCmd, ktCmd, newCmd, pluginCmd, tsCmd, validateCmd) + Cmd.AddCommand(dartCmd, docCmd, formatCmd, goCmd, javaCmd, ktCmd, newCmd, pluginCmd, tsCmd, validateCmd, phpCmd) } diff --git a/tools/goctl/api/phpgen/ApiBaseClient.php b/tools/goctl/api/phpgen/ApiBaseClient.php new file mode 100644 index 000000000000..7afaa69d6421 --- /dev/null +++ b/tools/goctl/api/phpgen/ApiBaseClient.php @@ -0,0 +1,144 @@ +host = $host; + $this->port = $port; + } + + public function getHost() + { + return $this->host; + } + public function getPort() + { + return $this->port; + } + + public function getAddress() + { + return "$this->host:$this->port"; + } + + protected function request( + $path, // 请求路径 + $method, // 请求方法 get post delete put + $params, // 请求路径参数 + $query, // 请求字符串 + $headers, // 头部字段 + $body // 内容体 + ) { + $address = $this->getAddress(); + if (!$headers) { + $headers = []; + } else { + $headers = $headers->toAssocArray(); + } + + // path + if ($params) { + $path = self::replacePathParams($path, $params->toAssocArray()); + } + $url = "$address$path"; + + // query + if ($query) { + $queryString = $query->toQueryString(); + $url = "$url?$queryString"; + } + + $ch = curl_init(); + + // 2. 设置请求选项, 包括具体的url + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1); // HTTP/1.1 + curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 4); /* 在发起连接前等待的时间,如果设置为0,则无限等待 */ + curl_setopt($ch, CURLOPT_TIMEOUT, 20); /* 设置cURL允许执行的最长秒数 */ + curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($ch, CURLOPT_HEADER, 1); + + // POST + if ($method == 'post') { + curl_setopt($ch, CURLOPT_POST, true); + } else { + curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method); + } + + // body + if ($body) { + $headers['Content-Type'] = 'application/json'; + curl_setopt($ch, CURLOPT_POSTFIELDS, $body->toJsonString()); + } + + // header + if (!empty($headers)) { + $header = []; + foreach ($headers as $k => $v) { + $header[] = "$k: $v"; + } + curl_setopt($ch, CURLOPT_HTTPHEADER, $header); + } + + // 3. 执行一个cURL会话并且获取相关回复 + curl_setopt($ch, CURLINFO_HEADER_OUT, true); + $response = curl_exec($ch); + // $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); + + // 4. 释放cURL句柄,关闭一个cURL会话 + curl_close($ch); + + return self::parseResponse($response); + } + + private static function parseResponse($response) + { + if ($response === false) { + throw new ApiException("curl exec failed.", -1); + } + + $statusEnd = strpos($response, "\r\n"); + $status = substr($response, 0, $statusEnd); + $status = explode(' ', $status, 3); + $statusCode = intval($status[1] ?? 0); + + if ($status < 200 && $status > 299) { + throw new ApiException("response failed.", -2, null, $response); + } + + $headerEnd = strpos($response, "\r\n\r\n"); + $header = substr($response, $statusEnd + 2, $headerEnd - ($statusEnd + 2)); + $header = explode("\r\n", $header); + $headers = []; + foreach ($header as $row) { + $kw = explode(':', $row, 2); + $headers[strtolower($kw[0])] = $kw[1]; + } + + $body = json_decode(substr($response, $headerEnd + 4), true); + + return [ + 'status' => $status[2], + 'statusCode' => $statusCode, + 'headers' => $headers, + 'body' => $body, + ]; + } + + // 填入路径参数 + private static function replacePathParams($path, $kw) + { + $map = []; + foreach ($kw as $k => $v) { + $map[":$k"] = $v; + } + $path = str_replace(array_keys($map), $map, $path); + return $path; + } +} diff --git a/tools/goctl/api/phpgen/ApiException.php b/tools/goctl/api/phpgen/ApiException.php new file mode 100644 index 000000000000..b89dd8e3a2f1 --- /dev/null +++ b/tools/goctl/api/phpgen/ApiException.php @@ -0,0 +1,16 @@ +responseContent = $responseContent; + parent::__construct($message, $code, $previous); + } + + public function getResponseContent() { + return $this->responseContent; + } +} diff --git a/tools/goctl/api/phpgen/client.go b/tools/goctl/api/phpgen/client.go new file mode 100644 index 000000000000..47174c66a7cc --- /dev/null +++ b/tools/goctl/api/phpgen/client.go @@ -0,0 +1,163 @@ +package phpgen + +import ( + _ "embed" + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/zeromicro/go-zero/tools/goctl/api/spec" +) + +//go:embed ApiBaseClient.php +var apiBaseClientTemplate string + +//go:embed ApiException.php +var apiExceptionTemplate string + +func genClient(dir string, ns string, api *spec.ApiSpec) error { + if err := writeBaseClient(dir, ns); err != nil { + return err + } + if err := writeException(dir, ns); err != nil { + return err + } + return writeClient(dir, ns, api) +} + +func writeBaseClient(dir string, ns string) error { + bPath := filepath.Join(dir, "ApiBaseClient.php") + bHead := fmt.Sprintf("request('%s', '%s',", r.Path, strings.ToLower(r.Method)) + + if r.RequestType != nil { + params := []string{} + for _, tagKey := range tagKeys { + if hasTagMembers(r.RequestType, tagKey) { + sn := camelCase(fmt.Sprintf("get-%s", tagToSubName(tagKey)), false) + params = append(params, fmt.Sprintf("$request->%s()", sn)) + } else { + params = append(params, "null") + } + } + fmt.Fprint(f, strings.Join(params, ",")) + } else { + fmt.Fprint(f, "null, null, null, null") + } + + fmt.Fprintln(f, ");") + + writeIndent(f, 8) + if r.ResponseType != nil { + n := camelCase(r.ResponseType.Name(), true) + fmt.Fprintf(f, "$response = new %s();\n", n) + definedType, ok := r.ResponseType.(spec.DefineStruct) + if !ok { + return fmt.Errorf("type %s not supported", n) + } + if err := writeResponseHeader(f, &definedType); err != nil { + return err + } + if err := writeResponseBody(f, &definedType); err != nil { + return err + } + writeIndent(f, 8) + fmt.Fprint(f, "return $response;\n") + } else { + fmt.Fprint(f, "return null;\n") + } + + writeIndent(f, 4) + fmt.Fprintln(f, "}") + } + } + + fmt.Fprintln(f, "}") + + return nil +} + +func writeResponseBody(f *os.File, definedType *spec.DefineStruct) error { + // 获取字段 + ms := definedType.GetTagMembers(bodyTagKey) + if len(ms) <= 0 { + return nil + } + writeIndent(f, 8) + fmt.Fprint(f, "$response->getBody()") + for _, m := range ms { + tags := m.Tags() + k := "" + if len(tags) > 0 { + k = tags[0].Name + } else { + k = m.Name + } + fmt.Fprintf(f, "\n ->set%s($result['body']['%s'])", camelCase(m.Name, true), k) + } + fmt.Fprintln(f, ";") + return nil +} + +func writeResponseHeader(f *os.File, definedType *spec.DefineStruct) error { + // 获取字段 + ms := definedType.GetTagMembers(headerTagKey) + if len(ms) <= 0 { + return nil + } + writeIndent(f, 8) + fmt.Fprint(f, "$response->getHeader()") + for _, m := range ms { + tags := m.Tags() + k := "" + if len(tags) > 0 { + k = tags[0].Name + } else { + k = m.Name + } + fmt.Fprintf(f, "\n ->set%s($result['header']['%s'])", camelCase(m.Name, true), k) + } + fmt.Fprintln(f, ";") + return nil +} diff --git a/tools/goctl/api/phpgen/cmd.go b/tools/goctl/api/phpgen/cmd.go new file mode 100644 index 000000000000..bb36359af964 --- /dev/null +++ b/tools/goctl/api/phpgen/cmd.go @@ -0,0 +1,53 @@ +package phpgen + +import ( + "errors" + "fmt" + + "github.com/gookit/color" + "github.com/spf13/cobra" + "github.com/zeromicro/go-zero/core/logx" + "github.com/zeromicro/go-zero/tools/goctl/api/parser" + "github.com/zeromicro/go-zero/tools/goctl/util/pathx" +) + +var ( + // VarStringDir describes a directory. + VarStringDir string + // VarStringAPI describes an API file. + VarStringAPI string + // VarStringAPI describes an PHP namespace. + VarStringNS string +) + +func PhpCommand(_ *cobra.Command, _ []string) error { + apiFile := VarStringAPI + if apiFile == "" { + return errors.New("missing -api") + } + dir := VarStringDir + if dir == "" { + return errors.New("missing -dir") + } + + ns := VarStringNS + if ns == "" { + return errors.New("missing -ns") + } + + api, e := parser.Parse(apiFile) + if e != nil { + return e + } + + if err := api.Validate(); err != nil { + return err + } + + logx.Must(pathx.MkdirIfNotExist(dir)) + logx.Must(genMessages(dir, ns, api)) + logx.Must(genClient(dir, ns, api)) + + fmt.Println(color.Green.Render("Done.")) + return nil +} diff --git a/tools/goctl/api/phpgen/msg.go b/tools/goctl/api/phpgen/msg.go new file mode 100644 index 000000000000..029db699a2f0 --- /dev/null +++ b/tools/goctl/api/phpgen/msg.go @@ -0,0 +1,214 @@ +package phpgen + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/zeromicro/go-zero/tools/goctl/api/spec" +) + +const ( + formTagKey = "form" + pathTagKey = "path" + headerTagKey = "header" + bodyTagKey = "json" +) + +var ( + // 这个顺序与 PHP ApiBaseClient request 参数相关,改动时要注意 PHP 那边的代码。 + tagKeys = []string{pathTagKey, formTagKey, headerTagKey, bodyTagKey} +) + +func tagToSubName(tagKey string) string { + suffix := tagKey + switch tagKey { + case "json": + suffix = "body" + case "form": + suffix = "query" + } + return suffix +} + +func getMessageName(tn string, tagKey string, isPascal bool) string { + suffix := tagToSubName(tagKey) + return camelCase(fmt.Sprintf("%s-%s", tn, suffix), isPascal) +} + +func hasTagMembers(t spec.Type, tagKey string) bool { + definedType, ok := t.(spec.DefineStruct) + if !ok { + return false + } + ms := definedType.GetTagMembers(tagKey) + return len(ms) > 0 +} + +func genMessages(dir string, ns string, api *spec.ApiSpec) error { + for _, t := range api.Types { + tn := t.Name() + definedType, ok := t.(spec.DefineStruct) + if !ok { + return fmt.Errorf("type %s not supported", tn) + } + + // 子类型 + tags := []string{} + for _, tagKey := range tagKeys { + // 获取字段 + ms := definedType.GetTagMembers(tagKey) + if len(ms) <= 0 { + continue + } + + // 打开文件 + cn := getMessageName(tn, tagKey, true) + tags = append(tags, tagKey) + fp := filepath.Join(dir, fmt.Sprintf("%s.php", cn)) + f, err := os.OpenFile(fp, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o644) + if err != nil { + return err + } + defer f.Close() + + // 写入 + if err := writeSubMessage(f, ns, cn, ms); err != nil { + return err + } + } + + // 主类型 + rn := camelCase(tn, true) + fp := filepath.Join(dir, fmt.Sprintf("%s.php", rn)) + f, err := os.OpenFile(fp, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o644) + if err != nil { + return err + } + defer f.Close() + + if err := writeMessage(f, ns, rn, tags); err != nil { + return nil + } + } + + return nil +} + +func writeMessage(f *os.File, ns string, rn string, tags []string) error { + // 文件头 + fmt.Fprintln(f, "%s = new %s();\n", sn, cn) + } + writeIndent(f, 4) + fmt.Fprintln(f, "}") + + // getter + for _, tag := range tags { + sn := tagToSubName(tag) + pn := camelCase(fmt.Sprintf("get-%s", sn), false) + writeIndent(f, 4) + fmt.Fprintf(f, "public function %s() { return $this->%s; }\n", pn, sn) + } + + fmt.Fprintln(f, "}") + + return nil +} + +func writeSubMessage(f *os.File, ns string, cn string, ms []spec.Member) error { + // 文件头 + fmt.Fprintln(f, " 0 { + k = tags[0].Name + } else { + k = n + } + writeIndent(f, indent) + fmt.Fprintf(f, "'%s' => $this->%s,\n", k, n) + } +} + +func writeField(f *os.File, m spec.Member) { + writeIndent(f, 4) + fmt.Fprintf(f, "private $%s;\n", camelCase(m.Name, false)) +} + +func writeProperty(f *os.File, m spec.Member) { + pName := camelCase(m.Name, true) + cName := camelCase(m.Name, false) + writeIndent(f, 4) + fmt.Fprintf(f, "public function get%s() { return $this->%s; }\n\n", pName, cName) + writeIndent(f, 4) + fmt.Fprintf(f, "public function set%s($v) { $this->%s = $v; return $this; }\n\n", pName, cName) +} + +func writeIndent(f *os.File, n int) { + for i := 0; i < n; i++ { + fmt.Fprint(f, " ") + } +} diff --git a/tools/goctl/api/phpgen/util.go b/tools/goctl/api/phpgen/util.go new file mode 100644 index 000000000000..955c7954d8f8 --- /dev/null +++ b/tools/goctl/api/phpgen/util.go @@ -0,0 +1,48 @@ +package phpgen + +import ( + "regexp" + "strings" + + "golang.org/x/text/cases" + "golang.org/x/text/language" +) + +func camelCase(raw string, isPascal bool) string { + re := regexp.MustCompile("[A-Z_/: -]") + vs := re.FindAllStringIndex(raw, -1) + + // 全小写 + if len(vs) == 0 { + return raw + } + + // 小写开头 + if vs[0][0] > 0 { + vs = append([][]int{{0, vs[0][0]}}, vs...) + } + + // 满 + vc := len(vs) + for i := 0; i < vc; i++ { + if (i + 1) < len(vs) { + vs[i][1] = vs[i+1][0] + } else { + vs[i][1] = len(raw) + } + } + + // 驼峰 + ss := make([]string, len(vs)) + c := cases.Title(language.English) + for i, v := range vs { + s := strings.Trim(raw[v[0]:v[1]], "/:_ -") + if i == 0 && !isPascal { + ss[i] = strings.ToLower(s) + } else { + ss[i] = c.String(s) + } + } + + return strings.Join(ss, "") +} From a3c95442402f3446486cb39b5113ae03b4a38685 Mon Sep 17 00:00:00 2001 From: chaosannals Date: Wed, 11 Dec 2024 09:17:47 +0800 Subject: [PATCH 13/25] fix: php gen request. --- tools/goctl/api/phpgen/ApiBaseClient.php | 11 +++++------ tools/goctl/api/phpgen/client.go | 8 ++++++-- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/tools/goctl/api/phpgen/ApiBaseClient.php b/tools/goctl/api/phpgen/ApiBaseClient.php index 7afaa69d6421..c71618abc9bf 100644 --- a/tools/goctl/api/phpgen/ApiBaseClient.php +++ b/tools/goctl/api/phpgen/ApiBaseClient.php @@ -89,7 +89,10 @@ protected function request( // 3. 执行一个cURL会话并且获取相关回复 curl_setopt($ch, CURLINFO_HEADER_OUT, true); $response = curl_exec($ch); - // $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); + if ($response === false) { + $error = curl_error($ch); + throw new ApiException("curl exec failed.".var_export($error, true), -1); + } // 4. 释放cURL句柄,关闭一个cURL会话 curl_close($ch); @@ -99,10 +102,6 @@ protected function request( private static function parseResponse($response) { - if ($response === false) { - throw new ApiException("curl exec failed.", -1); - } - $statusEnd = strpos($response, "\r\n"); $status = substr($response, 0, $statusEnd); $status = explode(' ', $status, 3); @@ -118,7 +117,7 @@ private static function parseResponse($response) $headers = []; foreach ($header as $row) { $kw = explode(':', $row, 2); - $headers[strtolower($kw[0])] = $kw[1]; + $headers[strtolower($kw[0])] = $kw[1] ?? null; } $body = json_decode(substr($response, $headerEnd + 4), true); diff --git a/tools/goctl/api/phpgen/client.go b/tools/goctl/api/phpgen/client.go index 47174c66a7cc..b45e6370169a 100644 --- a/tools/goctl/api/phpgen/client.go +++ b/tools/goctl/api/phpgen/client.go @@ -58,18 +58,22 @@ func writeClient(dir string, ns string, api *spec.ApiSpec) error { fmt.Fprintf(f, "class %sClient extends ApiBaseClient {\n", name) for _, g := range api.Service.Groups { + prefix := g.GetAnnotation("prefix") + p := camelCase(prefix, true) + // 路由 for _, r := range g.Routes { an := camelCase(r.Path, true) + writeIndent(f, 4) - fmt.Fprintf(f, "public function %s%s(", strings.ToLower(r.Method), an) + fmt.Fprintf(f, "public function %s%s%s(", strings.ToLower(r.Method), p, an) if r.RequestType != nil { fmt.Fprint(f, "$request") } fmt.Fprintln(f, ") {") writeIndent(f, 8) - fmt.Fprintf(f, "$result = $this->request('%s', '%s',", r.Path, strings.ToLower(r.Method)) + fmt.Fprintf(f, "$result = $this->request('%s%s', '%s',", prefix, r.Path, strings.ToLower(r.Method)) if r.RequestType != nil { params := []string{} From 759c713d10c926c8b48e69f0e03c37be23dff619 Mon Sep 17 00:00:00 2001 From: chaosannals Date: Wed, 11 Dec 2024 09:59:06 +0800 Subject: [PATCH 14/25] php gen custom body. --- tools/goctl/api/phpgen/ApiBody.php | 27 ++++++++++++++++ tools/goctl/api/phpgen/client.go | 51 +++++++++++++++++------------- 2 files changed, 56 insertions(+), 22 deletions(-) create mode 100644 tools/goctl/api/phpgen/ApiBody.php diff --git a/tools/goctl/api/phpgen/ApiBody.php b/tools/goctl/api/phpgen/ApiBody.php new file mode 100644 index 000000000000..f2606eeedf9c --- /dev/null +++ b/tools/goctl/api/phpgen/ApiBody.php @@ -0,0 +1,27 @@ +data = $data; + } + + public function toJsonString() + { + return json_encode($this->data, JSON_UNESCAPED_UNICODE); + } + + public function toAssocArray() + { + return $this->data; + } +} diff --git a/tools/goctl/api/phpgen/client.go b/tools/goctl/api/phpgen/client.go index b45e6370169a..232c4ea0da16 100644 --- a/tools/goctl/api/phpgen/client.go +++ b/tools/goctl/api/phpgen/client.go @@ -16,30 +16,28 @@ var apiBaseClientTemplate string //go:embed ApiException.php var apiExceptionTemplate string +//go:embed ApiBody.php +var apiBodyTemplate string + func genClient(dir string, ns string, api *spec.ApiSpec) error { - if err := writeBaseClient(dir, ns); err != nil { + if err := writeTemplate(dir, ns, "ApiBaseClient", apiBaseClientTemplate); err != nil { + return err + } + if err := writeTemplate(dir, ns, "ApiException", apiExceptionTemplate); err != nil { return err } - if err := writeException(dir, ns); err != nil { + if err := writeTemplate(dir, ns, "ApiBody", apiBodyTemplate); err != nil { return err } return writeClient(dir, ns, api) } -func writeBaseClient(dir string, ns string) error { - bPath := filepath.Join(dir, "ApiBaseClient.php") - bHead := fmt.Sprintf("request('%s%s', '%s',", prefix, r.Path, strings.ToLower(r.Method)) + fmt.Fprintf(f, "$result = $this->request('%s%s', '%s',", prefix, r.Path, method) if r.RequestType != nil { params := []string{} for _, tagKey := range tagKeys { if hasTagMembers(r.RequestType, tagKey) { sn := camelCase(fmt.Sprintf("get-%s", tagToSubName(tagKey)), false) - params = append(params, fmt.Sprintf("$request->%s()", sn)) + if tagKey == bodyTagKey { + params = append(params, fmt.Sprintf("$body ?? $request->%s()", sn)) + } else { + params = append(params, fmt.Sprintf("$request->%s()", sn)) + } } else { - params = append(params, "null") + if tagKey == bodyTagKey { + params = append(params, "$body") + } else { + params = append(params, "null") + } } } fmt.Fprint(f, strings.Join(params, ",")) } else { - fmt.Fprint(f, "null, null, null, null") + fmt.Fprint(f, "null, null, null, $body") } fmt.Fprintln(f, ");") From 02b9bf2a053b21ed6799badca0d615aa3b99d68f Mon Sep 17 00:00:00 2001 From: chaosannals Date: Wed, 11 Dec 2024 10:08:54 +0800 Subject: [PATCH 15/25] fix: php gen exception. --- tools/goctl/api/phpgen/ApiBaseClient.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/goctl/api/phpgen/ApiBaseClient.php b/tools/goctl/api/phpgen/ApiBaseClient.php index c71618abc9bf..01efab650394 100644 --- a/tools/goctl/api/phpgen/ApiBaseClient.php +++ b/tools/goctl/api/phpgen/ApiBaseClient.php @@ -91,7 +91,7 @@ protected function request( $response = curl_exec($ch); if ($response === false) { $error = curl_error($ch); - throw new ApiException("curl exec failed.".var_export($error, true), -1); + throw new ApiException("curl exec failed." . var_export($error, true), -1); } // 4. 释放cURL句柄,关闭一个cURL会话 @@ -107,7 +107,7 @@ private static function parseResponse($response) $status = explode(' ', $status, 3); $statusCode = intval($status[1] ?? 0); - if ($status < 200 && $status > 299) { + if ($statusCode < 200 || $statusCode > 299) { throw new ApiException("response failed.", -2, null, $response); } From 805e568f3fca68bff1d0dac92c8ba6b0e53722a4 Mon Sep 17 00:00:00 2001 From: chaosannals Date: Wed, 11 Dec 2024 10:43:59 +0800 Subject: [PATCH 16/25] fix: curl method upper. --- tools/goctl/api/phpgen/ApiBaseClient.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/goctl/api/phpgen/ApiBaseClient.php b/tools/goctl/api/phpgen/ApiBaseClient.php index 01efab650394..5ea7f15c9e5c 100644 --- a/tools/goctl/api/phpgen/ApiBaseClient.php +++ b/tools/goctl/api/phpgen/ApiBaseClient.php @@ -68,7 +68,7 @@ protected function request( if ($method == 'post') { curl_setopt($ch, CURLOPT_POST, true); } else { - curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method); + curl_setopt($ch, CURLOPT_CUSTOMREQUEST, strtoupper($method)); } // body From 57eb0030da76477abfd7852fcf9c4943bacceec3 Mon Sep 17 00:00:00 2001 From: chaosannals Date: Wed, 11 Dec 2024 16:07:34 +0800 Subject: [PATCH 17/25] ts gen request custom url prefix and body. --- tools/goctl/api/cmd.go | 2 ++ tools/goctl/api/tsgen/gen.go | 4 ++++ tools/goctl/api/tsgen/genpacket.go | 25 +++++++++++++++++++++---- tools/goctl/api/tsgen/request.ts | 6 +++++- 4 files changed, 32 insertions(+), 5 deletions(-) diff --git a/tools/goctl/api/cmd.go b/tools/goctl/api/cmd.go index d513ffdd862a..f4e82f646d5b 100644 --- a/tools/goctl/api/cmd.go +++ b/tools/goctl/api/cmd.go @@ -98,6 +98,8 @@ func init() { tsCmdFlags.StringVar(&tsgen.VarStringAPI, "api") tsCmdFlags.StringVar(&tsgen.VarStringCaller, "caller") tsCmdFlags.BoolVar(&tsgen.VarBoolUnWrap, "unwrap") + tsCmdFlags.StringVar(&tsgen.VarStringUrlPrefix, "url") + tsCmdFlags.BoolVar(&tsgen.VarBoolCustomBody, "body") validateCmdFlags.StringVar(&validate.VarStringAPI, "api") diff --git a/tools/goctl/api/tsgen/gen.go b/tools/goctl/api/tsgen/gen.go index 34732b67622d..3714658f1907 100644 --- a/tools/goctl/api/tsgen/gen.go +++ b/tools/goctl/api/tsgen/gen.go @@ -22,6 +22,10 @@ var ( VarStringCaller string // VarBoolUnWrap describes whether wrap or not. VarBoolUnWrap bool + // VarStringUrlPrefix request url prefix + VarStringUrlPrefix string + // VarBoolCustomBody request custom body + VarBoolCustomBody bool ) // TsCommand provides the entry to generate typescript codes diff --git a/tools/goctl/api/tsgen/genpacket.go b/tools/goctl/api/tsgen/genpacket.go index 152c15c2f769..f809813d392b 100644 --- a/tools/goctl/api/tsgen/genpacket.go +++ b/tools/goctl/api/tsgen/genpacket.go @@ -78,7 +78,11 @@ func genAPI(api *spec.ApiSpec, caller string) (string, error) { if len(comment) > 0 { fmt.Fprintf(&builder, "%s\n", comment) } - fmt.Fprintf(&builder, "export function %s(%s) {\n", handler, paramsForRoute(route)) + genericsType := "" + if VarBoolCustomBody { + genericsType = "" + } + fmt.Fprintf(&builder, "export function %s%s(%s) {\n", handler, genericsType, paramsForRoute(route)) writeIndent(&builder, 1) responseGeneric := "" if len(route.ResponseTypeName()) > 0 { @@ -101,6 +105,9 @@ func genAPI(api *spec.ApiSpec, caller string) (string, error) { func paramsForRoute(route spec.Route) string { if route.RequestType == nil { + if VarBoolCustomBody { + return "body?: T" + } return "" } hasParams := pathHasParams(route) @@ -141,6 +148,10 @@ func paramsForRoute(route spec.Route) string { } } } + + if VarBoolCustomBody { + params = append(params, "body?: T") + } return strings.Join(params, ", ") } @@ -180,7 +191,13 @@ func callParamsForRoute(route spec.Route, group spec.Group) string { configParams := []string{} if hasBody { - configParams = append(configParams, "body: JSON.stringify(req)") + if VarBoolCustomBody { + configParams = append(configParams, "body: JSON.stringify(body ?? req)") + } else { + configParams = append(configParams, "body: JSON.stringify(req)") + } + } else if VarBoolCustomBody { + configParams = append(configParams, "body: body ? JSON.stringify(body): null") } if hasHeader { configParams = append(configParams, "headers: headers") @@ -205,12 +222,12 @@ func pathForRoute(route spec.Route, group spec.Group) string { routePath = strings.Join(pathSlice, "/") } if len(prefix) == 0 { - return "`" + routePath + "`" + return "`" + VarStringUrlPrefix + routePath + "`" } prefix = strings.TrimPrefix(prefix, `"`) prefix = strings.TrimSuffix(prefix, `"`) - return fmt.Sprintf("`%s/%s`", prefix, strings.TrimPrefix(routePath, "/")) + return fmt.Sprintf("`%s%s/%s`", VarStringUrlPrefix, prefix, strings.TrimPrefix(routePath, "/")) } func pathHasParams(route spec.Route) bool { diff --git a/tools/goctl/api/tsgen/request.ts b/tools/goctl/api/tsgen/request.ts index 287f18297e9b..1f39967f39bb 100644 --- a/tools/goctl/api/tsgen/request.ts +++ b/tools/goctl/api/tsgen/request.ts @@ -77,7 +77,11 @@ export async function request( }, }); - return response.json(); + if (response.headers.get('Content-Type') == 'application/json') { + return response.json(); + } else { + return response.text(); + } } function api( From c904c6994930b4d69fc9feffbe949cd4e1080d11 Mon Sep 17 00:00:00 2001 From: chaosannals Date: Wed, 11 Dec 2024 17:30:19 +0800 Subject: [PATCH 18/25] php gen return $result when not Response Type. --- tools/goctl/api/phpgen/client.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/goctl/api/phpgen/client.go b/tools/goctl/api/phpgen/client.go index 232c4ea0da16..8f363a1bd1be 100644 --- a/tools/goctl/api/phpgen/client.go +++ b/tools/goctl/api/phpgen/client.go @@ -116,7 +116,7 @@ func writeClient(dir string, ns string, api *spec.ApiSpec) error { writeIndent(f, 8) fmt.Fprint(f, "return $response;\n") } else { - fmt.Fprint(f, "return null;\n") + fmt.Fprint(f, "return $result;\n") } writeIndent(f, 4) From 7fe982e5f57a9ead19574b08b2c1d6630112e470 Mon Sep 17 00:00:00 2001 From: chaosannals Date: Sat, 28 Dec 2024 17:13:10 +0800 Subject: [PATCH 19/25] c# gen. --- tools/goctl/api/cmd.go | 9 +- tools/goctl/api/csgen/ApiAttribute.cs | 43 ++++++++ tools/goctl/api/csgen/ApiBaseClient.cs | 101 +++++++++++++++++ tools/goctl/api/csgen/client.go | 105 ++++++++++++++++++ tools/goctl/api/csgen/cmd.go | 53 +++++++++ tools/goctl/api/csgen/msg.go | 146 +++++++++++++++++++++++++ tools/goctl/api/csgen/util.go | 64 +++++++++++ 7 files changed, 520 insertions(+), 1 deletion(-) create mode 100644 tools/goctl/api/csgen/ApiAttribute.cs create mode 100644 tools/goctl/api/csgen/ApiBaseClient.cs create mode 100644 tools/goctl/api/csgen/client.go create mode 100644 tools/goctl/api/csgen/cmd.go create mode 100644 tools/goctl/api/csgen/msg.go create mode 100644 tools/goctl/api/csgen/util.go diff --git a/tools/goctl/api/cmd.go b/tools/goctl/api/cmd.go index f4e82f646d5b..3d050ab8d7bf 100644 --- a/tools/goctl/api/cmd.go +++ b/tools/goctl/api/cmd.go @@ -3,6 +3,7 @@ package api import ( "github.com/spf13/cobra" "github.com/zeromicro/go-zero/tools/goctl/api/apigen" + "github.com/zeromicro/go-zero/tools/goctl/api/csgen" "github.com/zeromicro/go-zero/tools/goctl/api/dartgen" "github.com/zeromicro/go-zero/tools/goctl/api/docgen" "github.com/zeromicro/go-zero/tools/goctl/api/format" @@ -33,6 +34,7 @@ var ( pluginCmd = cobrax.NewCommand("plugin", cobrax.WithRunE(plugin.PluginCommand)) tsCmd = cobrax.NewCommand("ts", cobrax.WithRunE(tsgen.TsCommand)) phpCmd = cobrax.NewCommand("php", cobrax.WithRunE(phpgen.PhpCommand)) + csCmd = cobrax.NewCommand("cs", cobrax.WithRunE(csgen.CSharpCommand)) ) func init() { @@ -49,6 +51,7 @@ func init() { tsCmdFlags = tsCmd.Flags() validateCmdFlags = validateCmd.Flags() phpCmdFlags = phpCmd.Flags() + csCmdFlags = csCmd.Flags() ) apiCmdFlags.StringVar(&apigen.VarStringOutput, "o") @@ -107,6 +110,10 @@ func init() { phpCmdFlags.StringVar(&phpgen.VarStringAPI, "api") phpCmdFlags.StringVar(&phpgen.VarStringNS, "ns") + csCmdFlags.StringVar(&csgen.VarStringDir, "dir") + csCmdFlags.StringVar(&csgen.VarStringAPI, "api") + csCmdFlags.StringVar(&csgen.VarStringNS, "ns") + // Add sub-commands - Cmd.AddCommand(dartCmd, docCmd, formatCmd, goCmd, javaCmd, ktCmd, newCmd, pluginCmd, tsCmd, validateCmd, phpCmd) + Cmd.AddCommand(dartCmd, docCmd, formatCmd, goCmd, javaCmd, ktCmd, newCmd, pluginCmd, tsCmd, validateCmd, phpCmd, csCmd) } diff --git a/tools/goctl/api/csgen/ApiAttribute.cs b/tools/goctl/api/csgen/ApiAttribute.cs new file mode 100644 index 000000000000..8075b3635c38 --- /dev/null +++ b/tools/goctl/api/csgen/ApiAttribute.cs @@ -0,0 +1,43 @@ +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false)] +public sealed class HeaderPropertyName : Attribute +{ + public HeaderPropertyName(string name) + { + Name = name; + } + + /// + /// The name of the property. + /// + public string Name { get; } +} + + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false)] +public sealed class PathPropertyName : Attribute +{ + public PathPropertyName(string name) + { + Name = name; + } + + /// + /// The name of the property. + /// + public string Name { get; } +} + + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false)] +public sealed class FormPropertyName : Attribute +{ + public FormPropertyName(string name) + { + Name = name; + } + + /// + /// The name of the property. + /// + public string Name { get; } +} \ No newline at end of file diff --git a/tools/goctl/api/csgen/ApiBaseClient.cs b/tools/goctl/api/csgen/ApiBaseClient.cs new file mode 100644 index 000000000000..3ac44e07ad5d --- /dev/null +++ b/tools/goctl/api/csgen/ApiBaseClient.cs @@ -0,0 +1,101 @@ +using System.Net.Http.Json; +using System.Reflection; +using System.Web; + +public abstract class ApiBaseClient +{ + protected readonly HttpClient httpClient = new HttpClient(); + + + public ApiBaseClient(string host = "localhost", short port = 8080, string scheme = "http") + { + httpClient.BaseAddress = new Uri($"{scheme}://{host}:{port}"); + } + + public async Task CallResultAsync(HttpMethod method, string path, CancellationToken cancellationToken) where R : new() + { + var response = await CallAsync(HttpMethod.Post, path, cancellationToken); + return await ParseResponseAsync(response); + } + + public async Task CallAsync(HttpMethod method, string path, CancellationToken cancellationToken) + { + using var request = new HttpRequestMessage(method, path); + return await httpClient.SendAsync(request, cancellationToken); + } + + + public async Task RequestResultAsync(HttpMethod method, string path, T? param, CancellationToken cancellationToken) where R : new() + { + var response = await RequestAsync(HttpMethod.Post, path, param, cancellationToken); + return await ParseResponseAsync(response); + } + + public async Task RequestAsync(HttpMethod method, string path, T? param, CancellationToken cancellationToken) + { + using var request = new HttpRequestMessage(); + request.Method = method; + var query = HttpUtility.ParseQueryString(string.Empty); + + if (param != null) + { + foreach (var p in param.GetType().GetProperties()) + { + var pv = p.GetValue(param); + + // 头部 + var hpn = p.GetCustomAttribute(); + if (hpn != null) + { + request.Headers.Add(hpn.Name, pv?.ToString() ?? ""); + continue; + } + + // 请求参数 + var fpn = p.GetCustomAttribute(); + if (fpn != null) + { + query.Add(fpn.Name, pv?.ToString() ?? ""); + continue; + } + + // 路径参数 + var ppn = p.GetCustomAttribute(); + if (ppn != null) + { + path.Replace($":{ppn.Name}", pv?.ToString() ?? ""); + continue; + } + } + + // 请求链接 + request.RequestUri = new Uri(httpClient.BaseAddress!, query.Count > 0 ? $"{path}?{query}" : path); + + // JSON 内容 + if (HttpMethod.Get != method) + { + request.Content = JsonContent.Create(param); + } + } + + return await httpClient.SendAsync(request, cancellationToken); + } + + public static async Task ParseResponseAsync(HttpResponseMessage response) where R : new() + { + R result = await response.Content.ReadFromJsonAsync() ?? new R(); + foreach (var p in typeof(R).GetProperties()) + { + // 头部 + var hpn = p.GetCustomAttribute(); + if (hpn != null && response.Headers.Contains(hpn.Name)) + { + // TODO 这里应该有类型问题 + var v = response.Headers.GetValues(hpn.Name); + p.SetValue(result, v); + continue; + } + } + return result; + } +} diff --git a/tools/goctl/api/csgen/client.go b/tools/goctl/api/csgen/client.go new file mode 100644 index 000000000000..d9a2986ce5ee --- /dev/null +++ b/tools/goctl/api/csgen/client.go @@ -0,0 +1,105 @@ +package csgen + +import ( + "fmt" + "os" + "path/filepath" + "strings" + + _ "embed" + + "github.com/zeromicro/go-zero/tools/goctl/api/spec" +) + +//go:embed ApiAttribute.cs +var apiAttributeTemplate string + +//go:embed ApiBaseClient.cs +var apiApiBaseClientTemplate string + +func genClient(dir string, ns string, api *spec.ApiSpec) error { + if err := writeTemplate(dir, ns, "ApiAttribute", apiAttributeTemplate); err != nil { + return err + } + if err := writeTemplate(dir, ns, "ApiBaseClient", apiApiBaseClientTemplate); err != nil { + return err + } + + return writeClient(dir, ns, api) +} + +func writeTemplate(dir string, ns string, name string, template string) error { + fp := filepath.Join(dir, fmt.Sprintf("%s.cs", name)) + f, err := os.OpenFile(fp, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o644) + if err != nil { + return err + } + defer f.Close() + fmt.Fprintf(f, "namespace %s;\r\n\r\n", ns) + fmt.Fprint(f, template) + return nil +} + +func writeClient(dir string, ns string, api *spec.ApiSpec) error { + name := camelCase(api.Service.Name, true) + fp := filepath.Join(dir, fmt.Sprintf("%sClient.cs", name)) + f, err := os.OpenFile(fp, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o644) + if err != nil { + return err + } + defer f.Close() + + fmt.Fprintf(f, "namespace %s;\r\n\r\n", ns) + + // 类 + fmt.Fprintf(f, "public sealed class %sClient : ApiBaseClient\r\n{\r\n", name) + + // 组 + for _, g := range api.Service.Groups { + prefix := g.GetAnnotation("prefix") + p := camelCase(prefix, true) + + // 路由 + for _, r := range g.Routes { + an := camelCase(r.Path, true) + method := upperHead(strings.ToLower(r.Method), 1) + + writeIndent(f, 4) + fmt.Fprint(f, "public async ") + if r.ResponseType != nil { + fmt.Fprintf(f, "Task<%s>", r.ResponseType.Name()) + } else { + fmt.Fprint(f, "Task") + } + fmt.Fprintf(f, " %s%s%sAsync(", method, p, an) + if r.RequestType != nil { + fmt.Fprintf(f, "%s request,", r.RequestType.Name()) + } + fmt.Fprint(f, "CancellationToken cancellationToken)\r\n {\r\n") + + writeIndent(f, 8) + fmt.Fprint(f, "return await ") + + if r.RequestType != nil { + if r.ResponseType != nil { + fmt.Fprintf(f, "RequestResultAsync<%s,%s>(HttpMethod.%s, \"%s\", request, cancellationToken);\r\n", r.RequestType.Name(), r.ResponseType.Name(), method, r.Path) + } else { + fmt.Fprintf(f, "RequestAsync(HttpMethod.%s, \"%s\", request, cancellationToken);\r\n", method, r.Path) + } + } else { + if r.ResponseType != nil { + fmt.Fprintf(f, "CallResultAsync<%s>(HttpMethod.%s, \"%s\", cancellationToken);\r\n", r.ResponseType.Name(), method, r.Path) + } else { + fmt.Fprintf(f, "CallAsync(HttpMethod.%s, \"%s\", cancellationToken);\r\n", method, r.Path) + } + } + + writeIndent(f, 4) + fmt.Fprint(f, "}\r\n") + } + } + + fmt.Fprint(f, "}\r\n") + + return nil +} diff --git a/tools/goctl/api/csgen/cmd.go b/tools/goctl/api/csgen/cmd.go new file mode 100644 index 000000000000..1d44b41660d8 --- /dev/null +++ b/tools/goctl/api/csgen/cmd.go @@ -0,0 +1,53 @@ +package csgen + +import ( + "errors" + "fmt" + + "github.com/gookit/color" + "github.com/spf13/cobra" + "github.com/zeromicro/go-zero/core/logx" + "github.com/zeromicro/go-zero/tools/goctl/api/parser" + "github.com/zeromicro/go-zero/tools/goctl/util/pathx" +) + +var ( + // VarStringDir describes a directory. + VarStringDir string + // VarStringAPI describes an API file. + VarStringAPI string + // VarStringAPI describes an C# namespace. + VarStringNS string +) + +func CSharpCommand(_ *cobra.Command, _ []string) error { + apiFile := VarStringAPI + if apiFile == "" { + return errors.New("missing -api") + } + dir := VarStringDir + if dir == "" { + return errors.New("missing -dir") + } + + ns := VarStringNS + if ns == "" { + return errors.New("missing -ns") + } + + api, e := parser.Parse(apiFile) + if e != nil { + return e + } + + if err := api.Validate(); err != nil { + return err + } + + logx.Must(pathx.MkdirIfNotExist(dir)) + logx.Must(genMessages(dir, ns, api)) + logx.Must(genClient(dir, ns, api)) + + fmt.Println(color.Green.Render("Done.")) + return nil +} diff --git a/tools/goctl/api/csgen/msg.go b/tools/goctl/api/csgen/msg.go new file mode 100644 index 000000000000..494a156c10da --- /dev/null +++ b/tools/goctl/api/csgen/msg.go @@ -0,0 +1,146 @@ +package csgen + +import ( + "errors" + "fmt" + "os" + "path/filepath" + + "github.com/zeromicro/go-zero/tools/goctl/api/spec" + "github.com/zeromicro/go-zero/tools/goctl/api/util" +) + +const ( + formTagKey = "form" + pathTagKey = "path" + headerTagKey = "header" + bodyTagKey = "json" +) + +var ( + tagKeys = []string{pathTagKey, formTagKey, headerTagKey, bodyTagKey} +) + +func genMessages(dir string, ns string, api *spec.ApiSpec) error { + for _, t := range api.Types { + tn := t.Name() + definedType, ok := t.(spec.DefineStruct) + if !ok { + return fmt.Errorf("type %s not supported", tn) + } + + cn := camelCase(tn, true) + fp := filepath.Join(dir, fmt.Sprintf("%s.cs", cn)) + f, err := os.OpenFile(fp, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o644) + if err != nil { + return err + } + defer f.Close() + + // 引入依赖 + fmt.Fprintln(f, "using System.Text.Json.Serialization;") + fmt.Fprintln(f) + + // 名字空间 + fmt.Fprintf(f, "namespace %s;\n\n", ns) + + // 类 + fmt.Fprintf(f, "public class %s\n{\n", cn) + + for _, tagKey := range tagKeys { + // 获取字段 + ms := definedType.GetTagMembers(tagKey) + if len(ms) <= 0 { + continue + } + + for _, m := range ms { + tags := m.Tags() + k := "" + if len(tags) > 0 { + k = tags[0].Name + } else { + k = m.Name + } + + writeIndent(f, 4) + switch tagKey { + case bodyTagKey: + fmt.Fprintf(f, "[JsonPropertyName(\"%s\")]\n", k) + case headerTagKey: + fmt.Fprintln(f, "[JsonIgnore]") + writeIndent(f, 4) + fmt.Fprintf(f, "[HeaderPropertyName(\"%s\")]\n", k) + case formTagKey: + fmt.Fprintln(f, "[JsonIgnore]") + writeIndent(f, 4) + fmt.Fprintf(f, "[FormPropertyName(\"%s\")]\n", k) + case pathTagKey: + fmt.Fprintln(f, "[JsonIgnore]") + writeIndent(f, 4) + fmt.Fprintf(f, "[PathPropertyName(\"%s\")]\n", k) + } + + writeIndent(f, 4) + tn, err := apiTypeToCsTypeName(m.Type) + if err != nil { + return err + } + fmt.Fprintf(f, "public %s %s { get; set; }\n\n", tn, camelCase(m.Name, true)) + } + } + + fmt.Fprintln(f, "}") + } + return nil +} + +func apiTypeToCsTypeName(t spec.Type) (string, error) { + switch tt := t.(type) { + case spec.PrimitiveType: + r, ok := primitiveType(t.Name()) + if !ok { + return "", errors.New("unsupported primitive type " + t.Name()) + } + + return r, nil + case spec.ArrayType: + et, err := apiTypeToCsTypeName(tt.Value) + if err != nil { + return "", err + } + return fmt.Sprintf("List<%s>", et), nil + case spec.MapType: + vt, err := apiTypeToCsTypeName(tt.Value) + if err != nil { + return "", err + } + kt, ok := primitiveType(tt.Key) + if !ok { + return "", errors.New("unsupported key is not primitive type " + t.Name()) + } + return fmt.Sprintf("Dictionary<%s,%s>", kt, vt), nil + } + + return "", errors.New("unsupported type " + t.Name()) +} + +func primitiveType(tp string) (string, bool) { + switch tp { + case "string", "int", "uint", "bool", "byte": + return tp, true + case "int8": + return "SByte", true + case "uint8": + return "byte", true + case "int16", "int32", "int64": + return util.UpperFirst(tp), true + case "uint16", "uint32", "uint64": + return upperHead(tp, 2), true + case "float", "float32": + return "float", true + case "float64": + return "double", true + } + return "", false +} diff --git a/tools/goctl/api/csgen/util.go b/tools/goctl/api/csgen/util.go new file mode 100644 index 000000000000..373a920bfeee --- /dev/null +++ b/tools/goctl/api/csgen/util.go @@ -0,0 +1,64 @@ +package csgen + +import ( + "fmt" + "io" + "regexp" + "strings" + + "github.com/zeromicro/go-zero/tools/goctl/api/util" + "golang.org/x/text/cases" + "golang.org/x/text/language" +) + +func writeIndent(f io.Writer, n int) { + for i := 0; i < n; i++ { + fmt.Fprint(f, " ") + } +} + +func upperHead(s string, n int) string { + if len(s) == 0 { + return s + } + return util.ToUpper(s[:n]) + s[n:] +} + +func camelCase(raw string, isPascal bool) string { + re := regexp.MustCompile("[A-Z_/: -]") + vs := re.FindAllStringIndex(raw, -1) + + // 全小写 + if len(vs) == 0 { + return raw + } + + // 小写开头 + if vs[0][0] > 0 { + vs = append([][]int{{0, vs[0][0]}}, vs...) + } + + // 满 + vc := len(vs) + for i := 0; i < vc; i++ { + if (i + 1) < len(vs) { + vs[i][1] = vs[i+1][0] + } else { + vs[i][1] = len(raw) + } + } + + // 驼峰 + ss := make([]string, len(vs)) + c := cases.Title(language.English) + for i, v := range vs { + s := strings.Trim(raw[v[0]:v[1]], "/:_ -") + if i == 0 && !isPascal { + ss[i] = strings.ToLower(s) + } else { + ss[i] = c.String(s) + } + } + + return strings.Join(ss, "") +} From 821d5a842b79073b646e6f25777536fe97e46089 Mon Sep 17 00:00:00 2001 From: chaosannals Date: Sat, 28 Dec 2024 17:13:19 +0800 Subject: [PATCH 20/25] fix file close. --- tools/goctl/api/phpgen/client.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/goctl/api/phpgen/client.go b/tools/goctl/api/phpgen/client.go index 8f363a1bd1be..7f2142660347 100644 --- a/tools/goctl/api/phpgen/client.go +++ b/tools/goctl/api/phpgen/client.go @@ -47,12 +47,12 @@ func writeClient(dir string, ns string, api *spec.ApiSpec) error { if err != nil { return err } + defer f.Close() // 头部 fmt.Fprintf(f, " Date: Mon, 30 Dec 2024 11:21:33 +0800 Subject: [PATCH 21/25] fix: cs gen client. --- tools/goctl/api/csgen/ApiBaseClient.cs | 18 +++++++++------- tools/goctl/api/csgen/client.go | 3 +++ tools/goctl/api/csgen/msg.go | 30 +++++++++++++++----------- 3 files changed, 30 insertions(+), 21 deletions(-) diff --git a/tools/goctl/api/csgen/ApiBaseClient.cs b/tools/goctl/api/csgen/ApiBaseClient.cs index 3ac44e07ad5d..22aa34894fb3 100644 --- a/tools/goctl/api/csgen/ApiBaseClient.cs +++ b/tools/goctl/api/csgen/ApiBaseClient.cs @@ -7,31 +7,31 @@ public abstract class ApiBaseClient protected readonly HttpClient httpClient = new HttpClient(); - public ApiBaseClient(string host = "localhost", short port = 8080, string scheme = "http") + public ApiBaseClient(string host, short port, string scheme) { httpClient.BaseAddress = new Uri($"{scheme}://{host}:{port}"); } - public async Task CallResultAsync(HttpMethod method, string path, CancellationToken cancellationToken) where R : new() + protected async Task CallResultAsync(HttpMethod method, string path, CancellationToken cancellationToken) where R : new() { var response = await CallAsync(HttpMethod.Post, path, cancellationToken); return await ParseResponseAsync(response); } - public async Task CallAsync(HttpMethod method, string path, CancellationToken cancellationToken) + protected async Task CallAsync(HttpMethod method, string path, CancellationToken cancellationToken) { using var request = new HttpRequestMessage(method, path); return await httpClient.SendAsync(request, cancellationToken); } - public async Task RequestResultAsync(HttpMethod method, string path, T? param, CancellationToken cancellationToken) where R : new() + protected async Task RequestResultAsync(HttpMethod method, string path, T? param, CancellationToken cancellationToken) where R : new() { var response = await RequestAsync(HttpMethod.Post, path, param, cancellationToken); return await ParseResponseAsync(response); } - public async Task RequestAsync(HttpMethod method, string path, T? param, CancellationToken cancellationToken) + protected async Task RequestAsync(HttpMethod method, string path, T? param, CancellationToken cancellationToken) { using var request = new HttpRequestMessage(); request.Method = method; @@ -81,7 +81,7 @@ public async Task RequestAsync(HttpMethod method, string return await httpClient.SendAsync(request, cancellationToken); } - public static async Task ParseResponseAsync(HttpResponseMessage response) where R : new() + protected static async Task ParseResponseAsync(HttpResponseMessage response) where R : new() { R result = await response.Content.ReadFromJsonAsync() ?? new R(); foreach (var p in typeof(R).GetProperties()) @@ -90,9 +90,11 @@ public async Task RequestAsync(HttpMethod method, string var hpn = p.GetCustomAttribute(); if (hpn != null && response.Headers.Contains(hpn.Name)) { - // TODO 这里应该有类型问题 var v = response.Headers.GetValues(hpn.Name); - p.SetValue(result, v); + if (v != null && v.Count() > 0) + { + p.SetValue(result, v.First()); + } continue; } } diff --git a/tools/goctl/api/csgen/client.go b/tools/goctl/api/csgen/client.go index d9a2986ce5ee..3fe3839aaa48 100644 --- a/tools/goctl/api/csgen/client.go +++ b/tools/goctl/api/csgen/client.go @@ -54,6 +54,9 @@ func writeClient(dir string, ns string, api *spec.ApiSpec) error { // 类 fmt.Fprintf(f, "public sealed class %sClient : ApiBaseClient\r\n{\r\n", name) + // 构造函数 + fmt.Fprintf(f, " public %sClient(string host, short port, string scheme = \"http\") : base(host, port, scheme){}\r\n", name) + // 组 for _, g := range api.Service.Groups { prefix := g.GetAnnotation("prefix") diff --git a/tools/goctl/api/csgen/msg.go b/tools/goctl/api/csgen/msg.go index 494a156c10da..60f946f58ae2 100644 --- a/tools/goctl/api/csgen/msg.go +++ b/tools/goctl/api/csgen/msg.go @@ -38,14 +38,13 @@ func genMessages(dir string, ns string, api *spec.ApiSpec) error { defer f.Close() // 引入依赖 - fmt.Fprintln(f, "using System.Text.Json.Serialization;") - fmt.Fprintln(f) + fmt.Fprint(f, "using System.Text.Json.Serialization;\r\n\r\n") // 名字空间 - fmt.Fprintf(f, "namespace %s;\n\n", ns) + fmt.Fprintf(f, "namespace %s;\r\n\r\n", ns) // 类 - fmt.Fprintf(f, "public class %s\n{\n", cn) + fmt.Fprintf(f, "public class %s\r\n{\r\n", cn) for _, tagKey := range tagKeys { // 获取字段 @@ -66,19 +65,19 @@ func genMessages(dir string, ns string, api *spec.ApiSpec) error { writeIndent(f, 4) switch tagKey { case bodyTagKey: - fmt.Fprintf(f, "[JsonPropertyName(\"%s\")]\n", k) + fmt.Fprintf(f, "[JsonPropertyName(\"%s\")]\r\n", k) case headerTagKey: - fmt.Fprintln(f, "[JsonIgnore]") + fmt.Fprint(f, "[JsonIgnore]\r\n") writeIndent(f, 4) - fmt.Fprintf(f, "[HeaderPropertyName(\"%s\")]\n", k) + fmt.Fprintf(f, "[HeaderPropertyName(\"%s\")]\r\n", k) case formTagKey: - fmt.Fprintln(f, "[JsonIgnore]") + fmt.Fprint(f, "[JsonIgnore]\r\n") writeIndent(f, 4) - fmt.Fprintf(f, "[FormPropertyName(\"%s\")]\n", k) + fmt.Fprintf(f, "[FormPropertyName(\"%s\")]\r\n", k) case pathTagKey: - fmt.Fprintln(f, "[JsonIgnore]") + fmt.Fprint(f, "[JsonIgnore]\r\n") writeIndent(f, 4) - fmt.Fprintf(f, "[PathPropertyName(\"%s\")]\n", k) + fmt.Fprintf(f, "[PathPropertyName(\"%s\")]\r\n", k) } writeIndent(f, 4) @@ -86,11 +85,16 @@ func genMessages(dir string, ns string, api *spec.ApiSpec) error { if err != nil { return err } - fmt.Fprintf(f, "public %s %s { get; set; }\n\n", tn, camelCase(m.Name, true)) + + optionalTag := "" + if m.IsOptionalOrOmitEmpty() { + optionalTag = "?" + } + fmt.Fprintf(f, "public %s%s %s { get; set; }\r\n\r\n", tn, optionalTag, camelCase(m.Name, true)) } } - fmt.Fprintln(f, "}") + fmt.Fprint(f, "}\r\n") } return nil } From 88e43b26fcd4e07e070a80f3482270ef72c2d5fe Mon Sep 17 00:00:00 2001 From: chaosannals Date: Mon, 30 Dec 2024 17:22:48 +0800 Subject: [PATCH 22/25] fix: http message parse. --- tools/goctl/api/csgen/ApiBaseClient.cs | 41 +++++++++++++++------ tools/goctl/api/csgen/ApiBodyJsonContent.cs | 33 +++++++++++++++++ tools/goctl/api/csgen/client.go | 20 ++++++---- 3 files changed, 75 insertions(+), 19 deletions(-) create mode 100644 tools/goctl/api/csgen/ApiBodyJsonContent.cs diff --git a/tools/goctl/api/csgen/ApiBaseClient.cs b/tools/goctl/api/csgen/ApiBaseClient.cs index 22aa34894fb3..0ddcbe160b43 100644 --- a/tools/goctl/api/csgen/ApiBaseClient.cs +++ b/tools/goctl/api/csgen/ApiBaseClient.cs @@ -1,3 +1,4 @@ +using System.Net.Http.Headers; using System.Net.Http.Json; using System.Reflection; using System.Web; @@ -12,26 +13,30 @@ public ApiBaseClient(string host, short port, string scheme) httpClient.BaseAddress = new Uri($"{scheme}://{host}:{port}"); } - protected async Task CallResultAsync(HttpMethod method, string path, CancellationToken cancellationToken) where R : new() + protected async Task CallResultAsync(HttpMethod method, string path, CancellationToken cancellationToken, HttpContent? body) where R : new() { - var response = await CallAsync(HttpMethod.Post, path, cancellationToken); - return await ParseResponseAsync(response); + var response = await CallAsync(HttpMethod.Post, path, cancellationToken, body); + return await ParseResponseAsync(response, cancellationToken); } - protected async Task CallAsync(HttpMethod method, string path, CancellationToken cancellationToken) + protected async Task CallAsync(HttpMethod method, string path, CancellationToken cancellationToken, HttpContent? body) { using var request = new HttpRequestMessage(method, path); + if (body != null) + { + request.Content = body; + } return await httpClient.SendAsync(request, cancellationToken); } - protected async Task RequestResultAsync(HttpMethod method, string path, T? param, CancellationToken cancellationToken) where R : new() + protected async Task RequestResultAsync(HttpMethod method, string path, T? param, CancellationToken cancellationToken, HttpContent? body) where R : new() { - var response = await RequestAsync(HttpMethod.Post, path, param, cancellationToken); - return await ParseResponseAsync(response); + var response = await RequestAsync(HttpMethod.Post, path, param, cancellationToken, body); + return await ParseResponseAsync(response, cancellationToken); } - protected async Task RequestAsync(HttpMethod method, string path, T? param, CancellationToken cancellationToken) + protected async Task RequestAsync(HttpMethod method, string path, T? param, CancellationToken cancellationToken, HttpContent? body) { using var request = new HttpRequestMessage(); request.Method = method; @@ -47,7 +52,19 @@ protected async Task RequestAsync(HttpMethod method, str var hpn = p.GetCustomAttribute(); if (hpn != null) { - request.Headers.Add(hpn.Name, pv?.ToString() ?? ""); + var hv = pv?.ToString() ?? ""; + if (hpn.Name.ToLower() == "user-agent") + { + request.Headers.UserAgent.Clear(); + if (!string.IsNullOrEmpty(hv)) + { + request.Headers.UserAgent.Add(new ProductInfoHeaderValue(hv)); + } + } + else + { + request.Headers.Add(hpn.Name, hv); + } continue; } @@ -74,16 +91,16 @@ protected async Task RequestAsync(HttpMethod method, str // JSON 内容 if (HttpMethod.Get != method) { - request.Content = JsonContent.Create(param); + request.Content = body ?? ApiBodyJsonContent.Create(param); } } return await httpClient.SendAsync(request, cancellationToken); } - protected static async Task ParseResponseAsync(HttpResponseMessage response) where R : new() + protected static async Task ParseResponseAsync(HttpResponseMessage response, CancellationToken cancellationToken) where R : new() { - R result = await response.Content.ReadFromJsonAsync() ?? new R(); + R result = await response.Content.ReadFromJsonAsync(cancellationToken) ?? new R(); foreach (var p in typeof(R).GetProperties()) { // 头部 diff --git a/tools/goctl/api/csgen/ApiBodyJsonContent.cs b/tools/goctl/api/csgen/ApiBodyJsonContent.cs new file mode 100644 index 000000000000..8adc8d1e4741 --- /dev/null +++ b/tools/goctl/api/csgen/ApiBodyJsonContent.cs @@ -0,0 +1,33 @@ +using System.Net; +using System.Text.Json; + +public sealed class ApiBodyJsonContent : HttpContent +{ + public MemoryStream Stream { get; private set; } + + private ApiBodyJsonContent() + { + Stream = new MemoryStream(); + Headers.Add("Content-Type", "application/json"); + } + + protected override Task SerializeToStreamAsync(Stream stream, TransportContext? context) + { + Stream.CopyTo(stream); + return Task.CompletedTask; + } + + protected override bool TryComputeLength(out long length) + { + length = Stream.Length; + return true; + } + + public static ApiBodyJsonContent Create(T t, JsonSerializerOptions? settings=null) + { + var content = new ApiBodyJsonContent(); + JsonSerializer.Serialize(content.Stream, t, settings); + content.Stream.Position = 0; + return content; + } +} diff --git a/tools/goctl/api/csgen/client.go b/tools/goctl/api/csgen/client.go index 3fe3839aaa48..aa8ae35c0ff2 100644 --- a/tools/goctl/api/csgen/client.go +++ b/tools/goctl/api/csgen/client.go @@ -14,14 +14,20 @@ import ( //go:embed ApiAttribute.cs var apiAttributeTemplate string +//go:embed ApiBodyJsonContent.cs +var apiBodyJsonContentTemplate string + //go:embed ApiBaseClient.cs -var apiApiBaseClientTemplate string +var apiBaseClientTemplate string func genClient(dir string, ns string, api *spec.ApiSpec) error { if err := writeTemplate(dir, ns, "ApiAttribute", apiAttributeTemplate); err != nil { return err } - if err := writeTemplate(dir, ns, "ApiBaseClient", apiApiBaseClientTemplate); err != nil { + if err := writeTemplate(dir, ns, "ApiBodyJsonContent", apiBodyJsonContentTemplate); err != nil { + return err + } + if err := writeTemplate(dir, ns, "ApiBaseClient", apiBaseClientTemplate); err != nil { return err } @@ -78,22 +84,22 @@ func writeClient(dir string, ns string, api *spec.ApiSpec) error { if r.RequestType != nil { fmt.Fprintf(f, "%s request,", r.RequestType.Name()) } - fmt.Fprint(f, "CancellationToken cancellationToken)\r\n {\r\n") + fmt.Fprint(f, "CancellationToken cancellationToken, HttpContent? body=null)\r\n {\r\n") writeIndent(f, 8) fmt.Fprint(f, "return await ") if r.RequestType != nil { if r.ResponseType != nil { - fmt.Fprintf(f, "RequestResultAsync<%s,%s>(HttpMethod.%s, \"%s\", request, cancellationToken);\r\n", r.RequestType.Name(), r.ResponseType.Name(), method, r.Path) + fmt.Fprintf(f, "RequestResultAsync<%s,%s>(HttpMethod.%s, \"%s%s\", request, cancellationToken, body);\r\n", r.RequestType.Name(), r.ResponseType.Name(), method, prefix, r.Path) } else { - fmt.Fprintf(f, "RequestAsync(HttpMethod.%s, \"%s\", request, cancellationToken);\r\n", method, r.Path) + fmt.Fprintf(f, "RequestAsync(HttpMethod.%s, \"%s%s\", request, cancellationToken, body);\r\n", method, prefix, r.Path) } } else { if r.ResponseType != nil { - fmt.Fprintf(f, "CallResultAsync<%s>(HttpMethod.%s, \"%s\", cancellationToken);\r\n", r.ResponseType.Name(), method, r.Path) + fmt.Fprintf(f, "CallResultAsync<%s>(HttpMethod.%s, \"%s%s\", cancellationToken, body);\r\n", r.ResponseType.Name(), method, prefix, r.Path) } else { - fmt.Fprintf(f, "CallAsync(HttpMethod.%s, \"%s\", cancellationToken);\r\n", method, r.Path) + fmt.Fprintf(f, "CallAsync(HttpMethod.%s, \"%s%s\", cancellationToken, body);\r\n", method, prefix, r.Path) } } From 1e1691ba657d40699f97e40b49da01f845e1ba5a Mon Sep 17 00:00:00 2001 From: chaosannals Date: Thu, 2 Jan 2025 13:29:54 +0800 Subject: [PATCH 23/25] fix: client options. --- tools/goctl/api/csgen/ApiBaseClient.cs | 9 +++++---- tools/goctl/api/csgen/ApiException.cs | 7 +++++++ tools/goctl/api/csgen/client.go | 6 ++++++ 3 files changed, 18 insertions(+), 4 deletions(-) create mode 100644 tools/goctl/api/csgen/ApiException.cs diff --git a/tools/goctl/api/csgen/ApiBaseClient.cs b/tools/goctl/api/csgen/ApiBaseClient.cs index 0ddcbe160b43..edf76294134a 100644 --- a/tools/goctl/api/csgen/ApiBaseClient.cs +++ b/tools/goctl/api/csgen/ApiBaseClient.cs @@ -1,5 +1,6 @@ using System.Net.Http.Headers; using System.Net.Http.Json; +using System.Text.Json; using System.Reflection; using System.Web; @@ -16,7 +17,7 @@ public ApiBaseClient(string host, short port, string scheme) protected async Task CallResultAsync(HttpMethod method, string path, CancellationToken cancellationToken, HttpContent? body) where R : new() { var response = await CallAsync(HttpMethod.Post, path, cancellationToken, body); - return await ParseResponseAsync(response, cancellationToken); + return await ParseResponseAsync(response, null, cancellationToken); } protected async Task CallAsync(HttpMethod method, string path, CancellationToken cancellationToken, HttpContent? body) @@ -33,7 +34,7 @@ protected async Task CallAsync(HttpMethod method, string pa protected async Task RequestResultAsync(HttpMethod method, string path, T? param, CancellationToken cancellationToken, HttpContent? body) where R : new() { var response = await RequestAsync(HttpMethod.Post, path, param, cancellationToken, body); - return await ParseResponseAsync(response, cancellationToken); + return await ParseResponseAsync(response, null, cancellationToken); } protected async Task RequestAsync(HttpMethod method, string path, T? param, CancellationToken cancellationToken, HttpContent? body) @@ -98,9 +99,9 @@ protected async Task RequestAsync(HttpMethod method, str return await httpClient.SendAsync(request, cancellationToken); } - protected static async Task ParseResponseAsync(HttpResponseMessage response, CancellationToken cancellationToken) where R : new() + protected static async Task ParseResponseAsync(HttpResponseMessage response, JsonSerializerOptions? options, CancellationToken cancellationToken) where R : new() { - R result = await response.Content.ReadFromJsonAsync(cancellationToken) ?? new R(); + R result = await response.Content.ReadFromJsonAsync(options, cancellationToken) ?? new R(); foreach (var p in typeof(R).GetProperties()) { // 头部 diff --git a/tools/goctl/api/csgen/ApiException.cs b/tools/goctl/api/csgen/ApiException.cs new file mode 100644 index 000000000000..ff8e09cadbb8 --- /dev/null +++ b/tools/goctl/api/csgen/ApiException.cs @@ -0,0 +1,7 @@ +public class ApiException : Exception +{ + public ApiException(string message, Exception? inner=null): base(message, inner) + { + + } +} diff --git a/tools/goctl/api/csgen/client.go b/tools/goctl/api/csgen/client.go index aa8ae35c0ff2..97412b26fe5b 100644 --- a/tools/goctl/api/csgen/client.go +++ b/tools/goctl/api/csgen/client.go @@ -17,6 +17,9 @@ var apiAttributeTemplate string //go:embed ApiBodyJsonContent.cs var apiBodyJsonContentTemplate string +//go:embed ApiException.cs +var apiExceptionTemplate string + //go:embed ApiBaseClient.cs var apiBaseClientTemplate string @@ -27,6 +30,9 @@ func genClient(dir string, ns string, api *spec.ApiSpec) error { if err := writeTemplate(dir, ns, "ApiBodyJsonContent", apiBodyJsonContentTemplate); err != nil { return err } + if err := writeTemplate(dir, ns, "ApiException", apiExceptionTemplate); err != nil { + return err + } if err := writeTemplate(dir, ns, "ApiBaseClient", apiBaseClientTemplate); err != nil { return err } From 50d4ada409c9293e88516035850765ccb0cce8ec Mon Sep 17 00:00:00 2001 From: chaosannals Date: Fri, 10 Jan 2025 16:24:42 +0800 Subject: [PATCH 24/25] php gen. scheme. --- tools/goctl/api/phpgen/ApiBaseClient.php | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/tools/goctl/api/phpgen/ApiBaseClient.php b/tools/goctl/api/phpgen/ApiBaseClient.php index 5ea7f15c9e5c..1898b3e227a1 100644 --- a/tools/goctl/api/phpgen/ApiBaseClient.php +++ b/tools/goctl/api/phpgen/ApiBaseClient.php @@ -4,13 +4,16 @@ class ApiBaseClient { private $host; private $port; + private $scheme; public function __construct( $host, - $port + $port, + $scheme ) { $this->host = $host; $this->port = $port; + $this->scheme = $scheme; } public function getHost() @@ -24,7 +27,7 @@ public function getPort() public function getAddress() { - return "$this->host:$this->port"; + return "$this->scheme://$this->host:$this->port"; } protected function request( @@ -59,8 +62,8 @@ protected function request( // 2. 设置请求选项, 包括具体的url curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1); // HTTP/1.1 - curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 4); /* 在发起连接前等待的时间,如果设置为0,则无限等待 */ - curl_setopt($ch, CURLOPT_TIMEOUT, 20); /* 设置cURL允许执行的最长秒数 */ + curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 4); // 在发起连接前等待的时间,如果设置为0,则无限等待。 + curl_setopt($ch, CURLOPT_TIMEOUT, 20); // 设置cURL允许执行的最长秒数 curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); curl_setopt($ch, CURLOPT_HEADER, 1); @@ -86,7 +89,7 @@ protected function request( curl_setopt($ch, CURLOPT_HTTPHEADER, $header); } - // 3. 执行一个cURL会话并且获取相关回复 + // 3. 执行一个cURL会话并且获取响应消息。 curl_setopt($ch, CURLINFO_HEADER_OUT, true); $response = curl_exec($ch); if ($response === false) { @@ -94,7 +97,7 @@ protected function request( throw new ApiException("curl exec failed." . var_export($error, true), -1); } - // 4. 释放cURL句柄,关闭一个cURL会话 + // 4. 释放cURL句柄,关闭一个cURL会话。 curl_close($ch); return self::parseResponse($response); From 9de232eb2b8516d04db44cef25812340858d549e Mon Sep 17 00:00:00 2001 From: chaosannals Date: Mon, 13 Jan 2025 10:30:37 +0800 Subject: [PATCH 25/25] kt gen optional. --- tools/goctl/api/ktgen/api.tpl | 2 +- tools/goctl/api/ktgen/funcs.go | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/tools/goctl/api/ktgen/api.tpl b/tools/goctl/api/ktgen/api.tpl index 41ee484d2b96..ce6b2b0969da 100644 --- a/tools/goctl/api/ktgen/api.tpl +++ b/tools/goctl/api/ktgen/api.tpl @@ -5,7 +5,7 @@ import com.google.gson.Gson object {{with .Info}}{{.Title}}{{end}}{ {{range .Types}} data class {{.Name}}({{$length := (len .Members)}}{{range $i,$item := .Members}} - val {{with $item}}{{lowCamelCase .Name}}: {{parseType .Type.Name}}{{end}}{{if ne $i (add $length -1)}},{{end}}{{end}} + val {{with $item}}{{lowCamelCase .Name}}: {{parseType .Type.Name}}{{parseOptional $item}}{{end}}{{if ne $i (add $length -1)}},{{end}}{{end}} ){{end}} {{with .Service}} {{range .Routes}}suspend fun {{routeToFuncName .Method .Path}}({{with .RequestType}}{{if ne .Name ""}} diff --git a/tools/goctl/api/ktgen/funcs.go b/tools/goctl/api/ktgen/funcs.go index b8085b7b8645..bb97e88828d3 100644 --- a/tools/goctl/api/ktgen/funcs.go +++ b/tools/goctl/api/ktgen/funcs.go @@ -7,6 +7,7 @@ import ( "text/template" "github.com/iancoleman/strcase" + "github.com/zeromicro/go-zero/tools/goctl/api/spec" "github.com/zeromicro/go-zero/tools/goctl/api/util" ) @@ -16,6 +17,7 @@ var funcsMap = template.FuncMap{ "parseType": parseType, "add": add, "upperCase": upperCase, + "parseOptional": parseOptional, } func lowCamelCase(s string) string { @@ -117,3 +119,10 @@ func add(a, i int) int { func upperCase(s string) string { return strings.ToUpper(s) } + +func parseOptional(m spec.Member) string { + if m.IsOptionalOrOmitEmpty() { + return "?" + } + return "" +}