mirror of
https://github.com/zeromicro/go-zero.git
synced 2025-01-23 00:50:20 +08:00
This commit is contained in:
parent
bf883101d7
commit
6138f85470
@ -72,6 +72,7 @@ func init() {
|
|||||||
goCmdFlags.StringVar(&gogen.VarStringHome, "home")
|
goCmdFlags.StringVar(&gogen.VarStringHome, "home")
|
||||||
goCmdFlags.StringVar(&gogen.VarStringRemote, "remote")
|
goCmdFlags.StringVar(&gogen.VarStringRemote, "remote")
|
||||||
goCmdFlags.StringVar(&gogen.VarStringBranch, "branch")
|
goCmdFlags.StringVar(&gogen.VarStringBranch, "branch")
|
||||||
|
goCmdFlags.BoolVar(&gogen.VarBoolWithTest, "test")
|
||||||
goCmdFlags.StringVarWithDefaultValue(&gogen.VarStringStyle, "style", config.DefaultFormat)
|
goCmdFlags.StringVarWithDefaultValue(&gogen.VarStringStyle, "style", config.DefaultFormat)
|
||||||
|
|
||||||
javaCmdFlags.StringVar(&javagen.VarStringDir, "dir")
|
javaCmdFlags.StringVar(&javagen.VarStringDir, "dir")
|
||||||
|
@ -38,7 +38,8 @@ var (
|
|||||||
// VarStringBranch describes the branch.
|
// VarStringBranch describes the branch.
|
||||||
VarStringBranch string
|
VarStringBranch string
|
||||||
// VarStringStyle describes the style of output files.
|
// VarStringStyle describes the style of output files.
|
||||||
VarStringStyle string
|
VarStringStyle string
|
||||||
|
VarBoolWithTest bool
|
||||||
)
|
)
|
||||||
|
|
||||||
// GoCommand gen go project files from command line
|
// GoCommand gen go project files from command line
|
||||||
@ -49,6 +50,7 @@ func GoCommand(_ *cobra.Command, _ []string) error {
|
|||||||
home := VarStringHome
|
home := VarStringHome
|
||||||
remote := VarStringRemote
|
remote := VarStringRemote
|
||||||
branch := VarStringBranch
|
branch := VarStringBranch
|
||||||
|
withTest := VarBoolWithTest
|
||||||
if len(remote) > 0 {
|
if len(remote) > 0 {
|
||||||
repo, _ := util.CloneIntoGitHome(remote, branch)
|
repo, _ := util.CloneIntoGitHome(remote, branch)
|
||||||
if len(repo) > 0 {
|
if len(repo) > 0 {
|
||||||
@ -66,11 +68,11 @@ func GoCommand(_ *cobra.Command, _ []string) error {
|
|||||||
return errors.New("missing -dir")
|
return errors.New("missing -dir")
|
||||||
}
|
}
|
||||||
|
|
||||||
return DoGenProject(apiFile, dir, namingStyle)
|
return DoGenProject(apiFile, dir, namingStyle, withTest)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DoGenProject gen go project files with api file
|
// DoGenProject gen go project files with api file
|
||||||
func DoGenProject(apiFile, dir, style string) error {
|
func DoGenProject(apiFile, dir, style string, withTest bool) error {
|
||||||
api, err := parser.Parse(apiFile)
|
api, err := parser.Parse(apiFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -100,6 +102,10 @@ func DoGenProject(apiFile, dir, style string) error {
|
|||||||
logx.Must(genHandlers(dir, rootPkg, cfg, api))
|
logx.Must(genHandlers(dir, rootPkg, cfg, api))
|
||||||
logx.Must(genLogic(dir, rootPkg, cfg, api))
|
logx.Must(genLogic(dir, rootPkg, cfg, api))
|
||||||
logx.Must(genMiddleware(dir, cfg, api))
|
logx.Must(genMiddleware(dir, cfg, api))
|
||||||
|
if withTest {
|
||||||
|
logx.Must(genHandlersTest(dir, rootPkg, cfg, api))
|
||||||
|
logx.Must(genLogicTest(dir, rootPkg, cfg, api))
|
||||||
|
}
|
||||||
|
|
||||||
if err := backupAndSweep(apiFile); err != nil {
|
if err := backupAndSweep(apiFile); err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -348,7 +348,7 @@ func validateWithCamel(t *testing.T, api, camel string) {
|
|||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
err = initMod(dir)
|
err = initMod(dir)
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
err = DoGenProject(api, dir, camel)
|
err = DoGenProject(api, dir, camel, true)
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
|
filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
|
||||||
if strings.HasSuffix(path, ".go") {
|
if strings.HasSuffix(path, ".go") {
|
||||||
|
80
tools/goctl/api/gogen/genhandlerstest.go
Normal file
80
tools/goctl/api/gogen/genhandlerstest.go
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
package gogen
|
||||||
|
|
||||||
|
import (
|
||||||
|
_ "embed"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"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"
|
||||||
|
"github.com/zeromicro/go-zero/tools/goctl/util/format"
|
||||||
|
"github.com/zeromicro/go-zero/tools/goctl/util/pathx"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed handler_test.tpl
|
||||||
|
var handlerTestTemplate string
|
||||||
|
|
||||||
|
func genHandlerTest(dir, rootPkg string, cfg *config.Config, group spec.Group, route spec.Route) error {
|
||||||
|
handler := getHandlerName(route)
|
||||||
|
handlerPath := getHandlerFolderPath(group, route)
|
||||||
|
pkgName := handlerPath[strings.LastIndex(handlerPath, "/")+1:]
|
||||||
|
logicName := defaultLogicPackage
|
||||||
|
if handlerPath != handlerDir {
|
||||||
|
handler = strings.Title(handler)
|
||||||
|
logicName = pkgName
|
||||||
|
}
|
||||||
|
filename, err := format.FileNamingFormat(cfg.NamingFormat, handler)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return genFile(fileGenConfig{
|
||||||
|
dir: dir,
|
||||||
|
subdir: getHandlerFolderPath(group, route),
|
||||||
|
filename: filename + "_test.go",
|
||||||
|
templateName: "handlerTestTemplate",
|
||||||
|
category: category,
|
||||||
|
templateFile: handlerTestTemplateFile,
|
||||||
|
builtinTemplate: handlerTestTemplate,
|
||||||
|
data: map[string]any{
|
||||||
|
"PkgName": pkgName,
|
||||||
|
"ImportPackages": genHandlerTestImports(group, route, rootPkg),
|
||||||
|
"HandlerName": handler,
|
||||||
|
"RequestType": util.Title(route.RequestTypeName()),
|
||||||
|
"ResponseType": util.Title(route.ResponseTypeName()),
|
||||||
|
"LogicName": logicName,
|
||||||
|
"LogicType": strings.Title(getLogicName(route)),
|
||||||
|
"Call": strings.Title(strings.TrimSuffix(handler, "Handler")),
|
||||||
|
"HasResp": len(route.ResponseTypeName()) > 0,
|
||||||
|
"HasRequest": len(route.RequestTypeName()) > 0,
|
||||||
|
"HasDoc": len(route.JoinedDoc()) > 0,
|
||||||
|
"Doc": getDoc(route.JoinedDoc()),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func genHandlersTest(dir, rootPkg string, cfg *config.Config, api *spec.ApiSpec) error {
|
||||||
|
for _, group := range api.Service.Groups {
|
||||||
|
for _, route := range group.Routes {
|
||||||
|
if err := genHandlerTest(dir, rootPkg, cfg, group, route); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func genHandlerTestImports(group spec.Group, route spec.Route, parentPkg string) string {
|
||||||
|
imports := []string{
|
||||||
|
//fmt.Sprintf("\"%s\"", pathx.JoinPackages(parentPkg, getLogicFolderPath(group, route))),
|
||||||
|
fmt.Sprintf("\"%s\"", pathx.JoinPackages(parentPkg, contextDir)),
|
||||||
|
fmt.Sprintf("\"%s\"", pathx.JoinPackages(parentPkg, configDir)),
|
||||||
|
}
|
||||||
|
if len(route.RequestTypeName()) > 0 {
|
||||||
|
imports = append(imports, fmt.Sprintf("\"%s\"\n", pathx.JoinPackages(parentPkg, typesDir)))
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Join(imports, "\n\t")
|
||||||
|
}
|
90
tools/goctl/api/gogen/genlogictest.go
Normal file
90
tools/goctl/api/gogen/genlogictest.go
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
package gogen
|
||||||
|
|
||||||
|
import (
|
||||||
|
_ "embed"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"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/format"
|
||||||
|
"github.com/zeromicro/go-zero/tools/goctl/util/pathx"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed logic_test.tpl
|
||||||
|
var logicTestTemplate string
|
||||||
|
|
||||||
|
func genLogicTest(dir, rootPkg string, cfg *config.Config, api *spec.ApiSpec) error {
|
||||||
|
for _, g := range api.Service.Groups {
|
||||||
|
for _, r := range g.Routes {
|
||||||
|
err := genLogicTestByRoute(dir, rootPkg, cfg, g, r)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func genLogicTestByRoute(dir, rootPkg string, cfg *config.Config, group spec.Group, route spec.Route) error {
|
||||||
|
logic := getLogicName(route)
|
||||||
|
goFile, err := format.FileNamingFormat(cfg.NamingFormat, logic)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
imports := genLogicTestImports(route, rootPkg)
|
||||||
|
var responseString string
|
||||||
|
var returnString string
|
||||||
|
var requestString string
|
||||||
|
var requestType string
|
||||||
|
if len(route.ResponseTypeName()) > 0 {
|
||||||
|
resp := responseGoTypeName(route, typesPacket)
|
||||||
|
responseString = "(resp " + resp + ", err error)"
|
||||||
|
returnString = "return"
|
||||||
|
} else {
|
||||||
|
responseString = "error"
|
||||||
|
returnString = "return nil"
|
||||||
|
}
|
||||||
|
if len(route.RequestTypeName()) > 0 {
|
||||||
|
requestString = "req *" + requestGoTypeName(route, typesPacket)
|
||||||
|
requestType = requestGoTypeName(route, typesPacket)
|
||||||
|
}
|
||||||
|
|
||||||
|
subDir := getLogicFolderPath(group, route)
|
||||||
|
return genFile(fileGenConfig{
|
||||||
|
dir: dir,
|
||||||
|
subdir: subDir,
|
||||||
|
filename: goFile + "_test.go",
|
||||||
|
templateName: "logicTestTemplate",
|
||||||
|
category: category,
|
||||||
|
templateFile: logicTestTemplateFile,
|
||||||
|
builtinTemplate: logicTestTemplate,
|
||||||
|
data: map[string]any{
|
||||||
|
"pkgName": subDir[strings.LastIndex(subDir, "/")+1:],
|
||||||
|
"imports": imports,
|
||||||
|
"logic": strings.Title(logic),
|
||||||
|
"function": strings.Title(strings.TrimSuffix(logic, "Logic")),
|
||||||
|
"responseType": responseString,
|
||||||
|
"returnString": returnString,
|
||||||
|
"request": requestString,
|
||||||
|
"hasRequest": len(requestType) > 0,
|
||||||
|
"hasResponse": len(route.ResponseTypeName()) > 0,
|
||||||
|
"requestType": requestType,
|
||||||
|
"hasDoc": len(route.JoinedDoc()) > 0,
|
||||||
|
"doc": getDoc(route.JoinedDoc()),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func genLogicTestImports(route spec.Route, parentPkg string) string {
|
||||||
|
var imports []string
|
||||||
|
//imports = append(imports, `"context"`+"\n")
|
||||||
|
imports = append(imports, fmt.Sprintf("\"%s\"", pathx.JoinPackages(parentPkg, contextDir)))
|
||||||
|
imports = append(imports, fmt.Sprintf("\"%s\"", pathx.JoinPackages(parentPkg, configDir)))
|
||||||
|
if shallImportTypesPackage(route) {
|
||||||
|
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")
|
||||||
|
}
|
81
tools/goctl/api/gogen/handler_test.tpl
Normal file
81
tools/goctl/api/gogen/handler_test.tpl
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
package {{.PkgName}}
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
{{if .HasRequest}}"encoding/json"{{end}}
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
{{.ImportPackages}}
|
||||||
|
)
|
||||||
|
|
||||||
|
{{if .HasDoc}}{{.Doc}}{{end}}
|
||||||
|
func Test{{.HandlerName}}(t *testing.T) {
|
||||||
|
// new service context
|
||||||
|
c := config.Config{}
|
||||||
|
svcCtx := svc.NewServiceContext(c)
|
||||||
|
// init mock service context here
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
reqBody interface{}
|
||||||
|
wantStatus int
|
||||||
|
wantResp string
|
||||||
|
setupMocks func()
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "invalid request body",
|
||||||
|
reqBody: "invalid",
|
||||||
|
wantStatus: http.StatusBadRequest,
|
||||||
|
wantResp: "unsupported type", // Adjust based on actual error response
|
||||||
|
setupMocks: func() {
|
||||||
|
// No setup needed for this test case
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "handler error",
|
||||||
|
{{if .HasRequest}}reqBody: types.{{.RequestType}}{
|
||||||
|
//TODO: add fields here
|
||||||
|
},
|
||||||
|
{{end}}wantStatus: http.StatusBadRequest,
|
||||||
|
wantResp: "error", // Adjust based on actual error response
|
||||||
|
setupMocks: func() {
|
||||||
|
// Mock login logic to return an error
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "handler successful",
|
||||||
|
{{if .HasRequest}}reqBody: types.{{.RequestType}}{
|
||||||
|
//TODO: add fields here
|
||||||
|
},
|
||||||
|
{{end}}wantStatus: http.StatusOK,
|
||||||
|
wantResp: `{"code":0,"msg":"success","data":{}}`, // Adjust based on actual success response
|
||||||
|
setupMocks: func() {
|
||||||
|
// Mock login logic to return success
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
tt.setupMocks()
|
||||||
|
var reqBody []byte
|
||||||
|
{{if .HasRequest}}var err error
|
||||||
|
reqBody, err = json.Marshal(tt.reqBody)
|
||||||
|
require.NoError(t, err){{end}}
|
||||||
|
req, err := http.NewRequest("POST", "/ut", bytes.NewBuffer(reqBody))
|
||||||
|
require.NoError(t, err)
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
rr := httptest.NewRecorder()
|
||||||
|
handler := {{.HandlerName}}(svcCtx)
|
||||||
|
handler.ServeHTTP(rr, req)
|
||||||
|
t.Log(rr.Body.String())
|
||||||
|
assert.Equal(t, tt.wantStatus, rr.Code)
|
||||||
|
assert.Contains(t, rr.Body.String(), tt.wantResp)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
69
tools/goctl/api/gogen/logic_test.tpl
Normal file
69
tools/goctl/api/gogen/logic_test.tpl
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
package {{.pkgName}}
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
{{.imports}}
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test{{.logic}}_{{.function}}(t *testing.T) {
|
||||||
|
c := config.Config{}
|
||||||
|
mockSvcCtx := svc.NewServiceContext(c)
|
||||||
|
// init mock service context here
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
ctx context.Context
|
||||||
|
setupMocks func()
|
||||||
|
{{if .hasRequest}}req *{{.requestType}}{{end}}
|
||||||
|
wantErr bool
|
||||||
|
checkResp func{{if .hasResponse}}{{.responseType}}{{else}}(err error){{end}}
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "response error",
|
||||||
|
ctx: context.Background(),
|
||||||
|
setupMocks: func() {
|
||||||
|
// mock data for this test case
|
||||||
|
},
|
||||||
|
{{if .hasRequest}}req: &{{.requestType}}{
|
||||||
|
// TODO: init your request here
|
||||||
|
},{{end}}
|
||||||
|
wantErr: true,
|
||||||
|
checkResp: func{{if .hasResponse}}{{.responseType}}{{else}}(err error){{end}} {
|
||||||
|
// TODO: Add your check logic here
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "successful",
|
||||||
|
ctx: context.Background(),
|
||||||
|
setupMocks: func() {
|
||||||
|
// Mock data for this test case
|
||||||
|
},
|
||||||
|
{{if .hasRequest}}req: &{{.requestType}}{
|
||||||
|
// TODO: init your request here
|
||||||
|
},{{end}}
|
||||||
|
wantErr: false,
|
||||||
|
checkResp: func{{if .hasResponse}}{{.responseType}}{{else}}(err error){{end}} {
|
||||||
|
// TODO: Add your check logic here
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
tt.setupMocks()
|
||||||
|
l := New{{.logic}}(tt.ctx, mockSvcCtx)
|
||||||
|
{{if .hasResponse}}resp, {{end}}err := l.{{.function}}({{if .hasRequest}}tt.req{{end}})
|
||||||
|
if tt.wantErr {
|
||||||
|
assert.Error(t, err)
|
||||||
|
} else {
|
||||||
|
require.NoError(t, err)
|
||||||
|
{{if .hasResponse}}assert.NotNil(t, resp){{end}}
|
||||||
|
}
|
||||||
|
tt.checkResp({{if .hasResponse}}resp, {{end}}err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -12,7 +12,9 @@ const (
|
|||||||
contextTemplateFile = "context.tpl"
|
contextTemplateFile = "context.tpl"
|
||||||
etcTemplateFile = "etc.tpl"
|
etcTemplateFile = "etc.tpl"
|
||||||
handlerTemplateFile = "handler.tpl"
|
handlerTemplateFile = "handler.tpl"
|
||||||
|
handlerTestTemplateFile = "handler_test.tpl"
|
||||||
logicTemplateFile = "logic.tpl"
|
logicTemplateFile = "logic.tpl"
|
||||||
|
logicTestTemplateFile = "logic_test.tpl"
|
||||||
mainTemplateFile = "main.tpl"
|
mainTemplateFile = "main.tpl"
|
||||||
middlewareImplementCodeFile = "middleware.tpl"
|
middlewareImplementCodeFile = "middleware.tpl"
|
||||||
routesTemplateFile = "routes.tpl"
|
routesTemplateFile = "routes.tpl"
|
||||||
@ -25,7 +27,9 @@ var templates = map[string]string{
|
|||||||
contextTemplateFile: contextTemplate,
|
contextTemplateFile: contextTemplate,
|
||||||
etcTemplateFile: etcTemplate,
|
etcTemplateFile: etcTemplate,
|
||||||
handlerTemplateFile: handlerTemplate,
|
handlerTemplateFile: handlerTemplate,
|
||||||
|
handlerTestTemplateFile: handlerTestTemplate,
|
||||||
logicTemplateFile: logicTemplate,
|
logicTemplateFile: logicTemplate,
|
||||||
|
logicTestTemplateFile: logicTestTemplate,
|
||||||
mainTemplateFile: mainTemplate,
|
mainTemplateFile: mainTemplate,
|
||||||
middlewareImplementCodeFile: middlewareImplementCode,
|
middlewareImplementCodeFile: middlewareImplementCode,
|
||||||
routesTemplateFile: routesTemplate,
|
routesTemplateFile: routesTemplate,
|
||||||
|
@ -83,6 +83,6 @@ func CreateServiceCommand(_ *cobra.Command, args []string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = gogen.DoGenProject(apiFilePath, abs, VarStringStyle)
|
err = gogen.DoGenProject(apiFilePath, abs, VarStringStyle, false)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -37,7 +37,8 @@
|
|||||||
"home": "{{.global.home}}",
|
"home": "{{.global.home}}",
|
||||||
"remote": "{{.global.remote}}",
|
"remote": "{{.global.remote}}",
|
||||||
"branch": "{{.global.branch}}",
|
"branch": "{{.global.branch}}",
|
||||||
"style": "{{.global.style}}"
|
"style": "{{.global.style}}",
|
||||||
|
"test": "Generate test files"
|
||||||
},
|
},
|
||||||
"new": {
|
"new": {
|
||||||
"short": "Fast create api service",
|
"short": "Fast create api service",
|
||||||
|
Loading…
Reference in New Issue
Block a user