Skip to content

Commit

Permalink
feat:goctl support generate api client code zeromicro#4625
Browse files Browse the repository at this point in the history
  • Loading branch information
studyzy committed Feb 7, 2025
1 parent 77fb271 commit 1ff79bd
Show file tree
Hide file tree
Showing 10 changed files with 354 additions and 6 deletions.
1 change: 1 addition & 0 deletions tools/goctl/api/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ func init() {
goCmdFlags.StringVar(&gogen.VarStringRemote, "remote")
goCmdFlags.StringVar(&gogen.VarStringBranch, "branch")
goCmdFlags.BoolVar(&gogen.VarBoolWithTest, "test")
goCmdFlags.BoolVarP(&gogen.VarBoolWithClient, "client", "c")
goCmdFlags.StringVarWithDefaultValue(&gogen.VarStringStyle, "style", config.DefaultFormat)

javaCmdFlags.StringVar(&javagen.VarStringDir, "dir")
Expand Down
5 changes: 5 additions & 0 deletions tools/goctl/api/gogen/client-method.tpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{{if .hasDoc}}// {{.function}} {{.doc}}{{end}}
func (c *ApiClient) {{.function}}(ctx context.Context, {{.request}}) {{.responseString}} {
const {{.function}}Url= "{{.url}}"
return call[{{.responseType}}](ctx, c, {{.httpMethod}}, {{.function}}Url{{if .hasRequest}}, req{{else}}, nil{{end}})
}
106 changes: 106 additions & 0 deletions tools/goctl/api/gogen/client.tpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package client

import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"

{{.imports}}
"github.com/zeromicro/go-zero/core/logx"
"github.com/zeromicro/go-zero/rest/httpc"
)

type ApiClient struct {
url string
cs httpc.Service
}

// NewApiClientWithClient returns a http-api client with the given url.
// opts are used to customize the *http.Client.
func NewApiClientWithClient(url string, c *http.Client, opts ...httpc.Option) *ApiClient {
return &ApiClient{
url: url,
cs: httpc.NewServiceWithClient("{{.client}}", c, opts...),
}
}

// NewApiClient returns a http-api client with the given url.
// opts are used to customize the *http.Client.
func NewApiClient(url string, opts ...httpc.Option) *ApiClient {
return &ApiClient{
url: url,
cs: httpc.NewService("{{.client}}", opts...),
}
}

// Do calls the URL with the given requestBody
func (cli *ApiClient) Do(ctx context.Context, method, url string, requestBody any) ([]byte, error) {
return doRequest(ctx, method, cli.url+url, requestBody, cli.cs.Do)
}

// call makes an HTTP request and unmarshals the response into the specified type
func call[T any](ctx context.Context, c *ApiClient, httpMethod string, url string, req any) (resp T, err error) {
result, err := c.Do(ctx, httpMethod, url, req)
if err != nil {
logx.Error(err)
return resp, err
}
if err = json.Unmarshal(result, &resp); err != nil {
return resp, fmt.Errorf("json unmarshal failed. error: %v", err)
}
return resp, nil
}

// doRequest performs the HTTP request and handles the response
func doRequest(
ctx context.Context, method, url string, requestBody any,
do func(ctx context.Context, method, url string, data any) (*http.Response, error)) ([]byte, error) {
res, err := do(ctx, method, url, requestBody)
if err != nil {
return nil, err
}
defer res.Body.Close()

// Log the request
logx.Debugfn(func() any {
param, _ := json.Marshal(requestBody)
return fmt.Sprintf("call http api %s [%s] request: %s", method, url, string(param))
})

// Check the response status
if res.StatusCode != http.StatusOK {
return handleErrorResponse(res)
}

return handleSuccessResponse(res)
}

// handleErrorResponse handles non-OK HTTP responses
func handleErrorResponse(res *http.Response) ([]byte, error) {
var bz []byte
var err error
if res.Body != nil {
bz, err = io.ReadAll(res.Body)
logx.Error(string(bz), err)
} else {
logx.Error("server request failed", res.StatusCode, res.Status)
}
return nil, errors.New("server request failed, status: " + res.Status)
}

// handleSuccessResponse handles OK HTTP responses
func handleSuccessResponse(res *http.Response) ([]byte, error) {
responseData, err := io.ReadAll(res.Body)
if err != nil {
logx.Error(err)
return nil, errors.New("server request failed, status: " + res.Status)
}
logx.Debugf("call http api %s [%s] response: %s", res.Request.Method, res.Request.URL, string(responseData))
return responseData, nil
}

{{.clientMethods}}
15 changes: 11 additions & 4 deletions tools/goctl/api/gogen/gen.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,11 @@ var (
// VarStringBranch describes the branch.
VarStringBranch string
// VarStringStyle describes the style of output files.
VarStringStyle string
VarStringStyle string
// VarBoolWithTest describes whether to generate unit tests.
VarBoolWithTest bool
// VarBoolWithClient describes whether to generate http-api client.
VarBoolWithClient bool
)

// GoCommand gen go project files from command line
Expand All @@ -51,6 +54,7 @@ func GoCommand(_ *cobra.Command, _ []string) error {
remote := VarStringRemote
branch := VarStringBranch
withTest := VarBoolWithTest
withClient := VarBoolWithClient
if len(remote) > 0 {
repo, _ := util.CloneIntoGitHome(remote, branch)
if len(repo) > 0 {
Expand All @@ -68,11 +72,11 @@ func GoCommand(_ *cobra.Command, _ []string) error {
return errors.New("missing -dir")
}

return DoGenProject(apiFile, dir, namingStyle, withTest)
return DoGenProject(apiFile, dir, namingStyle, withTest, withClient)
}

// DoGenProject gen go project files with api file
func DoGenProject(apiFile, dir, style string, withTest bool) error {
func DoGenProject(apiFile, dir, style string, withTest, withClient bool) error {
api, err := parser.Parse(apiFile)
if err != nil {
return err
Expand Down Expand Up @@ -106,7 +110,10 @@ func DoGenProject(apiFile, dir, style string, withTest bool) error {
logx.Must(genHandlersTest(dir, rootPkg, cfg, api))
logx.Must(genLogicTest(dir, rootPkg, cfg, api))
}

if withClient {
logx.Must(genInterface(dir, rootPkg, cfg, api))
logx.Must(genClient(dir, rootPkg, cfg, api))
}
if err := backupAndSweep(apiFile); err != nil {
return err
}
Expand Down
106 changes: 106 additions & 0 deletions tools/goctl/api/gogen/genclient.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package gogen

import (
_ "embed"
"fmt"
"strings"
"text/template"

"github.com/zeromicro/go-zero/tools/goctl/api/spec"
"github.com/zeromicro/go-zero/tools/goctl/config"
"github.com/zeromicro/go-zero/tools/goctl/util/pathx"
"github.com/zeromicro/go-zero/tools/goctl/vars"
)

//go:embed client.tpl
var clientTemplate string

//go:embed client-method.tpl
var clientMethodTemplate string

func genClient(dir, rootPkg string, cfg *config.Config, api *spec.ApiSpec) error {
// load client method template
templateText, err := pathx.LoadTemplate(category, clientMethodTemplateFile, clientMethodTemplate)
if err != nil {
return err
}
gt := template.Must(template.New("groupTemplate").Parse(templateText))
// generate client method
var builder strings.Builder
for _, g := range api.Service.Groups {
for _, r := range g.Routes {
if err := generateClientMethod(gt, &builder, g, r); err != nil {
return err
}
}
}
// generate client file
return genClientFile(&builder, dir, rootPkg, api.Service.Name)
}

func generateClientMethod(gt *template.Template, builder *strings.Builder, g spec.Group, r spec.Route) error {
client := getClientName(r)
var responseString, responseType, returnString, requestString string
if len(r.ResponseTypeName()) > 0 {
responseType = responseGoTypeName(r, typesPacket)
responseString = "(resp " + responseType + ", err error)"
returnString = "return"
} else {
responseString = "error"
returnString = "return nil"
}
if len(r.RequestTypeName()) > 0 {
requestString = "req *" + requestGoTypeName(r, typesPacket)
}

data := map[string]any{
"client": strings.Title(client),
"function": strings.Title(strings.TrimSuffix(client, "Client")),
"responseString": responseString,
"responseType": responseType,
"httpMethod": mapping[r.Method],
"hasRequest": len(r.RequestTypeName()) > 0,
"returnString": returnString,
"request": requestString,
"hasDoc": len(r.JoinedDoc()) > 0,
"doc": strings.Trim(r.JoinedDoc(), "\""),
"url": g.Annotation.Properties["prefix"] + r.Path,
}

return gt.Execute(builder, data)
}

func genClientFile(builder *strings.Builder, dir, rootPkg string, name string) error {
imports := genClientImports(rootPkg)
subDir := clientDir
return genFile(fileGenConfig{
dir: dir,
subdir: subDir,
filename: "client.go",
templateName: "clientTemplate",
category: category,
templateFile: clientTemplateFile,
builtinTemplate: clientTemplate,
data: map[string]any{
"imports": imports,
"client": strings.Title(name),
"clientMethods": builder.String(),
},
})
}

func genClientImports(parentPkg string) string {
var imports []string
imports = append(imports, fmt.Sprintf("\"%s\"\n", pathx.JoinPackages(parentPkg, typesDir)))
imports = append(imports, fmt.Sprintf("\"%s/core/logx\"", vars.ProjectOpenSourceURL))
return strings.Join(imports, "\n\t")
}

func getClientName(route spec.Route) string {
handler, err := getHandlerBaseName(route)
if err != nil {
panic(err)
}

return handler + "Client"
}
113 changes: 113 additions & 0 deletions tools/goctl/api/gogen/geninterface.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package gogen

import (
"fmt"
"os"
"path"
"strings"
"text/template"

"github.com/zeromicro/go-zero/tools/goctl/api/spec"
"github.com/zeromicro/go-zero/tools/goctl/config"
"github.com/zeromicro/go-zero/tools/goctl/internal/version"
"github.com/zeromicro/go-zero/tools/goctl/util/format"
"github.com/zeromicro/go-zero/tools/goctl/util/pathx"
)

const (
interfaceFilename = "interface"
interfaceTemplate = `package client
import (
{{.importPackages}}
)
type Client interface {
{{.methods}}
}
`
interfaceMethodTemplate = `
{{if .hasDoc}}// {{.function}} {{.doc}}{{end}}
{{.function}}(ctx context.Context,{{.request}}) {{.responseType}}
`
)

func genMethod(gt *template.Template, builder *strings.Builder, route spec.Route) error {
//{{.function}}({{.request}}) {{.responseType}}
client := getClientName(route)
request := ""
requestType := requestGoTypeName(route, typesPacket)
if len(requestType) > 0 {
request = "req *" + requestType
}
var responseString string
if len(route.ResponseTypeName()) > 0 {
resp := responseGoTypeName(route, typesPacket)
responseString = "(" + resp + ", error)"
} else {
responseString = "error"
}
data := map[string]any{
"client": strings.Title(client),
"function": strings.Title(strings.TrimSuffix(client, "Client")),
"responseType": responseString,
"hasRequest": len(route.RequestTypeName()) > 0,
"request": request,
"hasDoc": len(route.JoinedDoc()) > 0,
"doc": strings.Trim(route.JoinedDoc(), "\""),
}
return gt.Execute(builder, data)
}

func genInterface(dir, rootPkg string, cfg *config.Config, api *spec.ApiSpec) error {
var builder strings.Builder
templateText, err := pathx.LoadTemplate(category, interfaceMethodTemplateFile, interfaceMethodTemplate)
if err != nil {
return err
}
gt := template.Must(template.New("groupTemplate").Parse(templateText))

for _, g := range api.Service.Groups {
for _, r := range g.Routes {
err = genMethod(gt, &builder, r)
if err != nil {
return err
}
}
}

var hasTimeout bool

interfaceFilename, err := format.FileNamingFormat(cfg.NamingFormat, interfaceFilename)
if err != nil {
return err
}

interfaceFilename = interfaceFilename + ".go"
filename := path.Join(dir, clientDir, interfaceFilename)
os.Remove(filename)

return genFile(fileGenConfig{
dir: dir,
subdir: clientDir,
filename: interfaceFilename,
templateName: "interfaceTemplate",
category: category,
templateFile: interfaceTemplateFile,
builtinTemplate: interfaceTemplate,
data: map[string]any{
"hasTimeout": hasTimeout,
"importPackages": genInterfaceImports(rootPkg, api),
"methods": strings.TrimSpace(builder.String()),
"version": version.BuildVersion,
},
})
}

func genInterfaceImports(parentPkg string, api *spec.ApiSpec) string {
var imports []string
imports = append(imports, "\"context\"")
imports = append(imports, "")
imports = append(imports, fmt.Sprintf("\"%s\"\n", pathx.JoinPackages(parentPkg, typesDir)))
return strings.Join(imports, "\n\t")
}
Loading

0 comments on commit 1ff79bd

Please sign in to comment.