mirror of
https://github.com/zeromicro/go-zero.git
synced 2026-05-07 15:10:01 +08:00
feature/goctl-api-swagger (#4780)
This commit is contained in:
@@ -10,10 +10,12 @@ 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/swagger"
|
||||
"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"
|
||||
"github.com/zeromicro/go-zero/tools/goctl/internal/cobrax"
|
||||
"github.com/zeromicro/go-zero/tools/goctl/pkg/env"
|
||||
"github.com/zeromicro/go-zero/tools/goctl/plugin"
|
||||
)
|
||||
|
||||
@@ -31,6 +33,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))
|
||||
swaggerCmd = cobrax.NewCommand("swagger", cobrax.WithRunE(swagger.Command))
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -46,6 +49,7 @@ func init() {
|
||||
pluginCmdFlags = pluginCmd.Flags()
|
||||
tsCmdFlags = tsCmd.Flags()
|
||||
validateCmdFlags = validateCmd.Flags()
|
||||
swaggerCmdFlags = swaggerCmd.Flags()
|
||||
)
|
||||
|
||||
apiCmdFlags.StringVar(&apigen.VarStringOutput, "o")
|
||||
@@ -97,8 +101,15 @@ func init() {
|
||||
tsCmdFlags.StringVar(&tsgen.VarStringCaller, "caller")
|
||||
tsCmdFlags.BoolVar(&tsgen.VarBoolUnWrap, "unwrap")
|
||||
|
||||
swaggerCmdFlags.StringVar(&swagger.VarStringAPI, "api")
|
||||
swaggerCmdFlags.StringVar(&swagger.VarStringDir, "dir")
|
||||
swaggerCmdFlags.BoolVar(&swagger.VarBoolYaml, "yaml")
|
||||
|
||||
validateCmdFlags.StringVar(&validate.VarStringAPI, "api")
|
||||
|
||||
// Add sub-commands
|
||||
Cmd.AddCommand(dartCmd, docCmd, formatCmd, goCmd, javaCmd, ktCmd, newCmd, pluginCmd, tsCmd, validateCmd)
|
||||
if env.UseExperimental() {
|
||||
Cmd.AddCommand(swaggerCmd)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ type (
|
||||
|
||||
// ApiSpec describes an api file
|
||||
ApiSpec struct {
|
||||
Info Info // Deprecated: useless expression
|
||||
Info Info
|
||||
Syntax ApiSyntax // Deprecated: useless expression
|
||||
Imports []Import // Deprecated: useless expression
|
||||
Types []Type
|
||||
|
||||
62
tools/goctl/api/swagger/annotation.go
Normal file
62
tools/goctl/api/swagger/annotation.go
Normal file
@@ -0,0 +1,62 @@
|
||||
package swagger
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/zeromicro/go-zero/tools/goctl/util"
|
||||
"google.golang.org/grpc/metadata"
|
||||
)
|
||||
|
||||
func getBoolFromKVOrDefault(properties map[string]string, key string, def bool) bool {
|
||||
if len(properties) == 0 {
|
||||
return def
|
||||
}
|
||||
md := metadata.New(properties)
|
||||
val := md.Get(key)
|
||||
if len(val) == 0 {
|
||||
return def
|
||||
}
|
||||
str := util.Unquote(val[0])
|
||||
if len(str) == 0 {
|
||||
return def
|
||||
}
|
||||
res, _ := strconv.ParseBool(str)
|
||||
return res
|
||||
}
|
||||
|
||||
func getStringFromKVOrDefault(properties map[string]string, key string, def string) string {
|
||||
if len(properties) == 0 {
|
||||
return def
|
||||
}
|
||||
md := metadata.New(properties)
|
||||
val := md.Get(key)
|
||||
if len(val) == 0 {
|
||||
return def
|
||||
}
|
||||
str := util.Unquote(val[0])
|
||||
if len(str) == 0 {
|
||||
return def
|
||||
}
|
||||
return str
|
||||
}
|
||||
|
||||
func getListFromInfoOrDefault(properties map[string]string, key string, def []string) []string {
|
||||
if len(properties) == 0 {
|
||||
return def
|
||||
}
|
||||
md := metadata.New(properties)
|
||||
val := md.Get(key)
|
||||
if len(val) == 0 {
|
||||
return def
|
||||
}
|
||||
|
||||
str := util.Unquote(val[0])
|
||||
if len(str) == 0 {
|
||||
return def
|
||||
}
|
||||
resp := util.FieldsAndTrimSpace(str, commaRune)
|
||||
if len(resp) == 0 {
|
||||
return def
|
||||
}
|
||||
return resp
|
||||
}
|
||||
53
tools/goctl/api/swagger/annotation_test.go
Normal file
53
tools/goctl/api/swagger/annotation_test.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package swagger
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_getBoolFromKVOrDefault(t *testing.T) {
|
||||
properties := map[string]string{
|
||||
"enabled": `"true"`,
|
||||
"disabled": `"false"`,
|
||||
"invalid": `"notabool"`,
|
||||
"empty_value": `""`,
|
||||
}
|
||||
|
||||
assert.True(t, getBoolFromKVOrDefault(properties, "enabled", false))
|
||||
assert.False(t, getBoolFromKVOrDefault(properties, "disabled", true))
|
||||
assert.False(t, getBoolFromKVOrDefault(properties, "invalid", false))
|
||||
assert.True(t, getBoolFromKVOrDefault(properties, "missing", true))
|
||||
assert.False(t, getBoolFromKVOrDefault(properties, "empty_value", false))
|
||||
assert.False(t, getBoolFromKVOrDefault(nil, "nil", false))
|
||||
assert.False(t, getBoolFromKVOrDefault(map[string]string{}, "empty", false))
|
||||
}
|
||||
|
||||
func Test_getStringFromKVOrDefault(t *testing.T) {
|
||||
properties := map[string]string{
|
||||
"name": `"example"`,
|
||||
"empty": `""`,
|
||||
}
|
||||
|
||||
assert.Equal(t, "example", getStringFromKVOrDefault(properties, "name", "default"))
|
||||
assert.Equal(t, "default", getStringFromKVOrDefault(properties, "empty", "default"))
|
||||
assert.Equal(t, "default", getStringFromKVOrDefault(properties, "missing", "default"))
|
||||
assert.Equal(t, "default", getStringFromKVOrDefault(nil, "nil", "default"))
|
||||
assert.Equal(t, "default", getStringFromKVOrDefault(map[string]string{}, "empty", "default"))
|
||||
}
|
||||
|
||||
func Test_getListFromInfoOrDefault(t *testing.T) {
|
||||
properties := map[string]string{
|
||||
"list": `"a, b, c"`,
|
||||
"empty": `""`,
|
||||
}
|
||||
|
||||
assert.Equal(t, []string{"a", "b", "c"}, getListFromInfoOrDefault(properties, "list", []string{"default"}))
|
||||
assert.Equal(t, []string{"default"}, getListFromInfoOrDefault(properties, "empty", []string{"default"}))
|
||||
assert.Equal(t, []string{"default"}, getListFromInfoOrDefault(properties, "missing", []string{"default"}))
|
||||
assert.Equal(t, []string{"default"}, getListFromInfoOrDefault(nil, "nil", []string{"default"}))
|
||||
assert.Equal(t, []string{"default"}, getListFromInfoOrDefault(map[string]string{}, "empty", []string{"default"}))
|
||||
assert.Equal(t, []string{"default"}, getListFromInfoOrDefault(map[string]string{
|
||||
"foo": ",,",
|
||||
}, "foo", []string{"default"}))
|
||||
}
|
||||
138
tools/goctl/api/swagger/api.go
Normal file
138
tools/goctl/api/swagger/api.go
Normal file
@@ -0,0 +1,138 @@
|
||||
package swagger
|
||||
|
||||
import "github.com/zeromicro/go-zero/tools/goctl/api/spec"
|
||||
|
||||
func fillAllStructs(api *spec.ApiSpec) {
|
||||
var (
|
||||
tps []spec.Type
|
||||
structTypes = make(map[string]spec.DefineStruct)
|
||||
groups []spec.Group
|
||||
)
|
||||
for _, tp := range api.Types {
|
||||
structTypes[tp.Name()] = tp.(spec.DefineStruct)
|
||||
}
|
||||
|
||||
for _, tp := range api.Types {
|
||||
filledTP := fillStruct("", tp, structTypes)
|
||||
tps = append(tps, filledTP)
|
||||
structTypes[filledTP.Name()] = filledTP.(spec.DefineStruct)
|
||||
}
|
||||
|
||||
for _, group := range api.Service.Groups {
|
||||
var routes []spec.Route
|
||||
for _, route := range group.Routes {
|
||||
route.RequestType = fillStruct("", route.RequestType, structTypes)
|
||||
route.ResponseType = fillStruct("", route.ResponseType, structTypes)
|
||||
routes = append(routes, route)
|
||||
}
|
||||
group.Routes = routes
|
||||
groups = append(groups, group)
|
||||
}
|
||||
api.Service.Groups = groups
|
||||
api.Types = tps
|
||||
}
|
||||
|
||||
func fillStruct(parent string, tp spec.Type, allTypes map[string]spec.DefineStruct) spec.Type {
|
||||
switch val := tp.(type) {
|
||||
case spec.DefineStruct:
|
||||
var members []spec.Member
|
||||
for _, member := range val.Members {
|
||||
switch memberType := member.Type.(type) {
|
||||
case spec.PointerType:
|
||||
member.Type = spec.PointerType{
|
||||
RawName: memberType.RawName,
|
||||
Type: fillStruct(val.Name(), memberType.Type, allTypes),
|
||||
}
|
||||
case spec.ArrayType:
|
||||
member.Type = spec.ArrayType{
|
||||
RawName: memberType.RawName,
|
||||
Value: fillStruct(val.Name(), memberType.Value, allTypes),
|
||||
}
|
||||
case spec.MapType:
|
||||
member.Type = spec.MapType{
|
||||
RawName: memberType.RawName,
|
||||
Key: memberType.Key,
|
||||
Value: fillStruct(val.Name(), memberType.Value, allTypes),
|
||||
}
|
||||
case spec.DefineStruct:
|
||||
if parent != memberType.Name() { // avoid recursive struct
|
||||
if st, ok := allTypes[memberType.Name()]; ok {
|
||||
member.Type = fillStruct("", st, allTypes)
|
||||
}
|
||||
}
|
||||
case spec.NestedStruct:
|
||||
member.Type = fillStruct("", member.Type, allTypes)
|
||||
}
|
||||
members = append(members, member)
|
||||
}
|
||||
if len(members) == 0 {
|
||||
st, ok := allTypes[val.RawName]
|
||||
if ok {
|
||||
members = st.Members
|
||||
}
|
||||
}
|
||||
val.Members = members
|
||||
return val
|
||||
case spec.NestedStruct:
|
||||
var members []spec.Member
|
||||
for _, member := range val.Members {
|
||||
switch memberType := member.Type.(type) {
|
||||
case spec.PointerType:
|
||||
member.Type = spec.PointerType{
|
||||
RawName: memberType.RawName,
|
||||
Type: fillStruct(val.Name(), memberType.Type, allTypes),
|
||||
}
|
||||
case spec.ArrayType:
|
||||
member.Type = spec.ArrayType{
|
||||
RawName: memberType.RawName,
|
||||
Value: fillStruct(val.Name(), memberType.Value, allTypes),
|
||||
}
|
||||
case spec.MapType:
|
||||
member.Type = spec.MapType{
|
||||
RawName: memberType.RawName,
|
||||
Key: memberType.Key,
|
||||
Value: fillStruct(val.Name(), memberType.Value, allTypes),
|
||||
}
|
||||
case spec.DefineStruct:
|
||||
if parent != memberType.Name() { // avoid recursive struct
|
||||
if st, ok := allTypes[memberType.Name()]; ok {
|
||||
member.Type = fillStruct("", st, allTypes)
|
||||
}
|
||||
}
|
||||
case spec.NestedStruct:
|
||||
if parent != memberType.Name() {
|
||||
if st, ok := allTypes[memberType.Name()]; ok {
|
||||
member.Type = fillStruct("", st, allTypes)
|
||||
}
|
||||
}
|
||||
}
|
||||
members = append(members, member)
|
||||
}
|
||||
if len(members) == 0 {
|
||||
st, ok := allTypes[val.RawName]
|
||||
if ok {
|
||||
members = st.Members
|
||||
}
|
||||
}
|
||||
val.Members = members
|
||||
return val
|
||||
case spec.PointerType:
|
||||
return spec.PointerType{
|
||||
RawName: val.RawName,
|
||||
Type: fillStruct(parent, val.Type, allTypes),
|
||||
}
|
||||
case spec.ArrayType:
|
||||
return spec.ArrayType{
|
||||
RawName: val.RawName,
|
||||
Value: fillStruct(parent, val.Value, allTypes),
|
||||
}
|
||||
case spec.MapType:
|
||||
return spec.MapType{
|
||||
RawName: val.RawName,
|
||||
Key: val.Key,
|
||||
Value: fillStruct(parent, val.Value, allTypes),
|
||||
}
|
||||
default:
|
||||
return tp
|
||||
}
|
||||
}
|
||||
79
tools/goctl/api/swagger/command.go
Normal file
79
tools/goctl/api/swagger/command.go
Normal file
@@ -0,0 +1,79 @@
|
||||
package swagger
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"github.com/zeromicro/go-zero/tools/goctl/util/pathx"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/zeromicro/go-zero/tools/goctl/pkg/parser/api/parser"
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
var (
|
||||
// VarStringAPI specifies the API filename.
|
||||
VarStringAPI string
|
||||
|
||||
// VarStringDir specifies the directory to generate swagger file.
|
||||
VarStringDir string
|
||||
|
||||
// VarBoolYaml specifies whether to generate a YAML file.
|
||||
VarBoolYaml bool
|
||||
)
|
||||
|
||||
func Command(_ *cobra.Command, _ []string) error {
|
||||
if len(VarStringAPI) == 0 {
|
||||
return errors.New("missing -api")
|
||||
}
|
||||
|
||||
if len(VarStringDir) == 0 {
|
||||
return errors.New("missing -dir")
|
||||
}
|
||||
|
||||
api, err := parser.Parse(VarStringAPI, "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fillAllStructs(api)
|
||||
|
||||
if err := api.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
swagger, err := spec2Swagger(api)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
data, err := json.MarshalIndent(swagger, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = pathx.MkdirIfNotExist(VarStringDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
base := filepath.Base(VarStringAPI)
|
||||
if VarBoolYaml {
|
||||
filename := filepath.Join(VarStringDir, strings.TrimSuffix(base, filepath.Ext(base))+".yaml")
|
||||
|
||||
var jsonObj interface{}
|
||||
if err := yaml.Unmarshal(data, &jsonObj); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
data, err := yaml.Marshal(jsonObj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(filename, data, 0644)
|
||||
}
|
||||
// generate json swagger file
|
||||
filename := filepath.Join(VarStringDir, strings.TrimSuffix(base, filepath.Ext(base))+".json")
|
||||
|
||||
return os.WriteFile(filename, data, 0644)
|
||||
}
|
||||
32
tools/goctl/api/swagger/const.go
Normal file
32
tools/goctl/api/swagger/const.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package swagger
|
||||
|
||||
const (
|
||||
tagHeader = "header"
|
||||
tagPath = "path"
|
||||
tagForm = "form"
|
||||
tagJson = "json"
|
||||
defFlag = "default="
|
||||
enumFlag = "options="
|
||||
rangeFlag = "range="
|
||||
exampleFlag = "example="
|
||||
|
||||
paramsInHeader = "header"
|
||||
paramsInPath = "path"
|
||||
paramsInQuery = "query"
|
||||
paramsInBody = "body"
|
||||
paramsInForm = "formData"
|
||||
|
||||
swaggerTypeInteger = "integer"
|
||||
swaggerTypeNumber = "number"
|
||||
swaggerTypeString = "string"
|
||||
swaggerTypeBoolean = "boolean"
|
||||
swaggerTypeArray = "array"
|
||||
swaggerTypeObject = "object"
|
||||
|
||||
swaggerVersion = "2.0"
|
||||
applicationJson = "application/json"
|
||||
applicationForm = "application/x-www-form-urlencoded"
|
||||
schemeHttps = "https"
|
||||
defaultHost = "127.0.0.1"
|
||||
defaultBasePath = "/"
|
||||
)
|
||||
25
tools/goctl/api/swagger/contenttype.go
Normal file
25
tools/goctl/api/swagger/contenttype.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package swagger
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/zeromicro/go-zero/tools/goctl/api/spec"
|
||||
)
|
||||
|
||||
func consumesFromTypeOrDef(method string, tp spec.Type) []string {
|
||||
if strings.EqualFold(method, http.MethodGet) {
|
||||
return []string{}
|
||||
}
|
||||
if tp == nil {
|
||||
return []string{}
|
||||
}
|
||||
structType, ok := tp.(spec.DefineStruct)
|
||||
if !ok {
|
||||
return []string{}
|
||||
}
|
||||
if typeContainsTag(structType, tagJson) {
|
||||
return []string{applicationJson}
|
||||
}
|
||||
return []string{applicationForm}
|
||||
}
|
||||
68
tools/goctl/api/swagger/contenttype_test.go
Normal file
68
tools/goctl/api/swagger/contenttype_test.go
Normal file
@@ -0,0 +1,68 @@
|
||||
package swagger
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/zeromicro/go-zero/tools/goctl/api/spec"
|
||||
)
|
||||
|
||||
func TestConsumesFromTypeOrDef(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
method string
|
||||
tp spec.Type
|
||||
expected []string
|
||||
}{
|
||||
{
|
||||
name: "GET method with nil type",
|
||||
method: http.MethodGet,
|
||||
tp: nil,
|
||||
expected: []string{},
|
||||
},
|
||||
{
|
||||
name: "post nil",
|
||||
method: http.MethodPost,
|
||||
tp: nil,
|
||||
expected: []string{},
|
||||
},
|
||||
{
|
||||
name: "json tag",
|
||||
method: http.MethodPost,
|
||||
tp: spec.DefineStruct{
|
||||
Members: []spec.Member{
|
||||
{
|
||||
Tag: `json:"example"`,
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: []string{applicationJson},
|
||||
},
|
||||
{
|
||||
name: "form tag",
|
||||
method: http.MethodPost,
|
||||
tp: spec.DefineStruct{
|
||||
Members: []spec.Member{
|
||||
{
|
||||
Tag: `form:"example"`,
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: []string{applicationForm},
|
||||
},
|
||||
{
|
||||
name: "Non struct type",
|
||||
method: http.MethodPost,
|
||||
tp: spec.ArrayType{},
|
||||
expected: []string{},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := consumesFromTypeOrDef(tt.method, tt.tp)
|
||||
assert.Equal(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
4
tools/goctl/api/swagger/example/.gitignore
vendored
Normal file
4
tools/goctl/api/swagger/example/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
*.json
|
||||
*.yaml
|
||||
bin
|
||||
output
|
||||
234
tools/goctl/api/swagger/example/example.api
Normal file
234
tools/goctl/api/swagger/example/example.api
Normal file
@@ -0,0 +1,234 @@
|
||||
syntax = "v1"
|
||||
|
||||
info (
|
||||
title: "Demo API" // title corresponding to Swagger
|
||||
description: "Generating Swagger files using the API demo." // description corresponding to Swagger
|
||||
version: "v1" // version corresponding to Swagger
|
||||
termsOfService: "https://github.com/zeromicro/go-zero" // termsOfService corresponding to Swagger
|
||||
contactName: "keson.an" // contactName corresponding to Swagger
|
||||
contactURL: "https://github.com/zeromicro/go-zero" // contactURL corresponding to Swagger
|
||||
contactEmail: "example@gmail.com" // contactEmail corresponding to Swagger
|
||||
licenseName: "MIT" // licenseName corresponding to Swagger
|
||||
licenseURL: "https://github.com/zeromicro/go-zero" // licenseURL corresponding to Swagger
|
||||
consumes: "application/json" // consumes corresponding to Swagger,default value is `application/json`
|
||||
produces: "application/json" // produces corresponding to Swagger,default value is `application/json`
|
||||
schemes: "https" // schemes corresponding to Swagger,default value is `https``
|
||||
host: "example.com" // host corresponding to Swagger,default value is `127.0.0.1`
|
||||
basePath: "/v1" // basePath corresponding to Swagger,default value is `/`
|
||||
wrapCodeMsg: "true" // to wrap in the universal code-msg structure, like {"code":0,"msg":"OK","data":$data}
|
||||
bizCodeEnumDescription: "1001-User not login<br>1002-User permission denied" // enums of business error codes, in JSON format, with the key being the business error code and the value being the description of that error code. This only takes effect when wrapCodeMsg is set to true.
|
||||
)
|
||||
|
||||
type (
|
||||
QueryReq {
|
||||
Id int `form:"id,range=[1:10000],example=10"`
|
||||
Name string `form:"name,example=keson.an"`
|
||||
Avatar string `form:"avatar,optional,example=https://example.com/avatar.png"`
|
||||
}
|
||||
QueryResp {
|
||||
Id int `json:"id,example=10"`
|
||||
Name string `json:"name,example=keson.an"`
|
||||
}
|
||||
PathQueryReq {
|
||||
Id int `path:"id,range=[1:10000],example=10"`
|
||||
Name string `form:"name,example=keson.an"`
|
||||
}
|
||||
PathQueryResp {
|
||||
Id int `json:"id,example=10"`
|
||||
Name string `json:"name,example=keson.an"`
|
||||
}
|
||||
)
|
||||
|
||||
@server (
|
||||
tags: "query" // tags corresponding to Swagger
|
||||
summary: "query API set" // summary corresponding to Swagger
|
||||
)
|
||||
service Swagger {
|
||||
@doc (
|
||||
description: "query demo"
|
||||
)
|
||||
@handler query
|
||||
get /query (QueryReq) returns (QueryResp)
|
||||
|
||||
@doc (
|
||||
description: "show path query demo"
|
||||
)
|
||||
@handler queryPath
|
||||
get /query/:id (PathQueryReq) returns (PathQueryResp)
|
||||
}
|
||||
|
||||
type (
|
||||
FormReq {
|
||||
Id int `form:"id,range=[1:10000],example=10"`
|
||||
Name string `form:"name,example=keson.an"`
|
||||
}
|
||||
FormResp {
|
||||
Id int `json:"id,example=10"`
|
||||
Name string `json:"name,example=keson.an"`
|
||||
}
|
||||
)
|
||||
|
||||
@server (
|
||||
tags: "form" // tags corresponding to Swagger
|
||||
summary: "form API set" // summary corresponding to Swagger
|
||||
)
|
||||
service Swagger {
|
||||
@doc (
|
||||
description: "form demo"
|
||||
)
|
||||
@handler form
|
||||
post /form (FormReq) returns (FormResp)
|
||||
}
|
||||
|
||||
type (
|
||||
JsonReq {
|
||||
Id int `json:"id,range=[1:10000],example=10"`
|
||||
Name string `json:"name,example=keson.an"`
|
||||
Avatar string `json:"avatar,optional"`
|
||||
Language string `json:"language,options=golang|java|python|typescript|rust"`
|
||||
Gender string `json:"gender,default=male,options=male|female,example=male"`
|
||||
}
|
||||
JsonResp {
|
||||
Id int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Avatar string `json:"avatar"`
|
||||
Language string `json:"language"`
|
||||
Gender string `json:"gender"`
|
||||
}
|
||||
ComplexJsonLevel2 {
|
||||
// basic
|
||||
Integer int `json:"integer,example=1"`
|
||||
Number float64 `json:"number,example=1.1"`
|
||||
Boolean bool `json:"boolean,options=true|false,example=true"`
|
||||
String string `json:"string,example=some text"`
|
||||
}
|
||||
ComplexJsonLevel1 {
|
||||
// basic
|
||||
Integer int `json:"integer,example=1"`
|
||||
Number float64 `json:"number,example=1.1"`
|
||||
Boolean bool `json:"boolean,options=true|false,example=true"`
|
||||
String string `json:"string,example=some text"`
|
||||
// Object
|
||||
Object ComplexJsonLevel2 `json:"object"`
|
||||
PointerObject *ComplexJsonLevel2 `json:"pointerObject"`
|
||||
}
|
||||
ComplexJsonReq {
|
||||
// basic
|
||||
Integer int `json:"integer,example=1"`
|
||||
Number float64 `json:"number,example=1.1"`
|
||||
Boolean bool `json:"boolean,options=true|false,example=true"`
|
||||
String string `json:"string,example=some text"`
|
||||
// basic array
|
||||
ArrayInteger []int `json:"arrayInteger"`
|
||||
ArrayNumber []float64 `json:"arrayNumber"`
|
||||
ArrayBoolean []bool `json:"arrayBoolean"`
|
||||
ArrayString []string `json:"arrayString"`
|
||||
// basic array array
|
||||
ArrayArrayInteger [][]int `json:"arrayArrayInteger"`
|
||||
ArrayArrayNumber [][]float64 `json:"arrayArrayNumber"`
|
||||
ArrayArrayBoolean [][]bool `json:"arrayArrayBoolean"`
|
||||
ArrayArrayString [][]string `json:"arrayArrayString"`
|
||||
// basic map
|
||||
MapInteger map[string]int `json:"mapInteger"`
|
||||
MapNumber map[string]float64 `json:"mapNumber"`
|
||||
MapBoolean map[string]bool `json:"mapBoolean"`
|
||||
MapString map[string]string `json:"mapString"`
|
||||
// basic map array
|
||||
MapArrayInteger map[string][]int `json:"mapArrayInteger"`
|
||||
MapArrayNumber map[string][]float64 `json:"mapArrayNumber"`
|
||||
MapArrayBoolean map[string][]bool `json:"mapArrayBoolean"`
|
||||
MapArrayString map[string][]string `json:"mapArrayString"`
|
||||
// basic map map
|
||||
MapMapInteger map[string]map[string]int `json:"mapMapInteger"`
|
||||
MapMapNumber map[string]map[string]float64 `json:"mapMapNumber"`
|
||||
MapMapBoolean map[string]map[string]bool `json:"mapMapBoolean"`
|
||||
MapMapString map[string]map[string]string `json:"mapMapString"`
|
||||
// Object
|
||||
Object ComplexJsonLevel1 `json:"object"`
|
||||
PointerObject *ComplexJsonLevel1 `json:"pointerObject"`
|
||||
// Object array
|
||||
ArrayObject []ComplexJsonLevel1 `json:"arrayObject"`
|
||||
ArrayPointerObject []*ComplexJsonLevel1 `json:"arrayPointerObject"`
|
||||
// Object map
|
||||
MapObject map[string]ComplexJsonLevel1 `json:"mapObject"`
|
||||
MapPointerObject map[string]*ComplexJsonLevel1 `json:"mapPointerObject"`
|
||||
// Object array array
|
||||
ArrayArrayObject [][]ComplexJsonLevel1 `json:"arrayArrayObject"`
|
||||
ArrayArrayPointerObject [][]*ComplexJsonLevel1 `json:"arrayArrayPointerObject"`
|
||||
// Object array map
|
||||
ArrayMapObject []map[string]ComplexJsonLevel1 `json:"arrayMapObject"`
|
||||
ArrayMapPointerObject []map[string]*ComplexJsonLevel1 `json:"arrayMapPointerObject"`
|
||||
// Object map array
|
||||
MapArrayObject map[string][]ComplexJsonLevel1 `json:"mapArrayObject"`
|
||||
MapArrayPointerObject map[string][]*ComplexJsonLevel1 `json:"mapArrayPointerObject"`
|
||||
}
|
||||
ComplexJsonResp {
|
||||
// basic
|
||||
Integer int `json:"integer,example=1"`
|
||||
Number float64 `json:"number,example=1.1"`
|
||||
Boolean bool `json:"boolean,options=true|false,example=true"`
|
||||
String string `json:"string,example=some text"`
|
||||
// basic array
|
||||
ArrayInteger []int `json:"arrayInteger"`
|
||||
ArrayNumber []float64 `json:"arrayNumber"`
|
||||
ArrayBoolean []bool `json:"arrayBoolean"`
|
||||
ArrayString []string `json:"arrayString"`
|
||||
// basic array array
|
||||
ArrayArrayInteger [][]int `json:"arrayArrayInteger"`
|
||||
ArrayArrayNumber [][]float64 `json:"arrayArrayNumber"`
|
||||
ArrayArrayBoolean [][]bool `json:"arrayArrayBoolean"`
|
||||
ArrayArrayString [][]string `json:"arrayArrayString"`
|
||||
// basic map
|
||||
MapInteger map[string]int `json:"mapInteger"`
|
||||
MapNumber map[string]float64 `json:"mapNumber"`
|
||||
MapBoolean map[string]bool `json:"mapBoolean"`
|
||||
MapString map[string]string `json:"mapString"`
|
||||
// basic map array
|
||||
MapArrayInteger map[string][]int `json:"mapArrayInteger"`
|
||||
MapArrayNumber map[string][]float64 `json:"mapArrayNumber"`
|
||||
MapArrayBoolean map[string][]bool `json:"mapArrayBoolean"`
|
||||
MapArrayString map[string][]string `json:"mapArrayString"`
|
||||
// basic map map
|
||||
MapMapInteger map[string]map[string]int `json:"mapMapInteger"`
|
||||
MapMapNumber map[string]map[string]float64 `json:"mapMapNumber"`
|
||||
MapMapBoolean map[string]map[string]bool `json:"mapMapBoolean"`
|
||||
MapMapString map[string]map[string]string `json:"mapMapString"`
|
||||
// Object
|
||||
Object ComplexJsonLevel1 `json:"object"`
|
||||
PointerObject *ComplexJsonLevel1 `json:"pointerObject"`
|
||||
// Object array
|
||||
ArrayObject []ComplexJsonLevel1 `json:"arrayObject"`
|
||||
ArrayPointerObject []*ComplexJsonLevel1 `json:"arrayPointerObject"`
|
||||
// Object map
|
||||
MapObject map[string]ComplexJsonLevel1 `json:"mapObject"`
|
||||
MapPointerObject map[string]*ComplexJsonLevel1 `json:"mapPointerObject"`
|
||||
// Object array array
|
||||
ArrayArrayObject [][]ComplexJsonLevel1 `json:"arrayArrayObject"`
|
||||
ArrayArrayPointerObject [][]*ComplexJsonLevel1 `json:"arrayArrayPointerObject"`
|
||||
// Object array map
|
||||
ArrayMapObject []map[string]ComplexJsonLevel1 `json:"arrayMapObject"`
|
||||
ArrayMapPointerObject []map[string]*ComplexJsonLevel1 `json:"arrayMapPointerObject"`
|
||||
// Object map array
|
||||
MapArrayObject map[string][]ComplexJsonLevel1 `json:"mapArrayObject"`
|
||||
MapArrayPointerObject map[string][]*ComplexJsonLevel1 `json:"mapArrayPointerObject"`
|
||||
}
|
||||
)
|
||||
|
||||
@server (
|
||||
tags: "postJson" // tags corresponding to Swagger
|
||||
summary: "json API set" // summary corresponding to Swagger
|
||||
)
|
||||
service Swagger {
|
||||
@doc (
|
||||
description: "simple json request body API"
|
||||
)
|
||||
@handler jsonSimple
|
||||
post /json/simple (JsonReq) returns (JsonResp)
|
||||
|
||||
@doc (
|
||||
description: "complex json request body API"
|
||||
)
|
||||
@handler jsonComplex
|
||||
post /json/complex (ComplexJsonReq) returns (ComplexJsonResp)
|
||||
}
|
||||
|
||||
235
tools/goctl/api/swagger/example/example_cn.api
Normal file
235
tools/goctl/api/swagger/example/example_cn.api
Normal file
@@ -0,0 +1,235 @@
|
||||
syntax = "v1"
|
||||
|
||||
info (
|
||||
title: "演示 API" // 对应 swagger 的 title
|
||||
description: "演示 api 生成 swagger 文件的 api 完整写法" // 对应 swagger 的 description
|
||||
version: "v1" // 对应 swagger 的 version
|
||||
termsOfService: "https://github.com/zeromicro/go-zero" // 对应 swagger 的 termsOfService
|
||||
contactName: "keson.an" // 对应 swagger 的 contactName
|
||||
contactURL: "https://github.com/zeromicro/go-zero" // 对应 swagger 的 contactURL
|
||||
contactEmail: "example@gmail.com" // 对应 swagger 的 contactEmail
|
||||
licenseName: "MIT" // 对应 swagger 的 licenseName
|
||||
licenseURL: "https://github.com/zeromicro/go-zero" // 对应 swagger 的 licenseURL
|
||||
consumes: "application/json" // 对应 swagger 的 consumes,不填默认为 application/json
|
||||
produces: "application/json" // 对应 swagger 的 produces,不填默认为 application/json
|
||||
schemes: "https" // 对应 swagger 的 schemes,不填默认为 https
|
||||
host: "example.com" // 对应 swagger 的 host,不填默认为 127.0.0.1
|
||||
basePath: "/v1" // 对应 swagger 的 basePath,不填默认为 /
|
||||
wrapCodeMsg: "true" // 是否用 code-msg 通用响应体,如果开启,则以格式 {"code":0,"msg":"OK","data":$data} 包括响应体
|
||||
bizCodeEnumDescription: "1001-未登录<br>1002-无权限操作" // 业务错误码枚举描述,json 格式,key 为业务错误码,value 为该错误码的描述,仅当 wrapCodeMsg 为 true 时生效
|
||||
)
|
||||
|
||||
type (
|
||||
QueryReq {
|
||||
Id int `form:"id,range=[1:10000],example=10"`
|
||||
Name string `form:"name,example=keson.an"`
|
||||
Avatar string `form:"avatar,optional,example=https://example.com/avatar.png"`
|
||||
}
|
||||
QueryResp {
|
||||
Id int `json:"id,example=10"`
|
||||
Name string `json:"name,example=keson.an"`
|
||||
}
|
||||
PathQueryReq {
|
||||
Id int `path:"id,range=[1:10000],example=10"`
|
||||
Name string `form:"name,example=keson.an"`
|
||||
}
|
||||
PathQueryResp {
|
||||
Id int `json:"id,example=10"`
|
||||
Name string `json:"name,example=keson.an"`
|
||||
}
|
||||
)
|
||||
|
||||
@server (
|
||||
tags: "query 演示" // 对应 swagger 的 tags,可以对 swagger 中的 api 进行分组
|
||||
summary: "query 类型接口集合" // 对应 swagger 的 summary
|
||||
prefix: v1
|
||||
)
|
||||
service Swagger {
|
||||
@doc (
|
||||
description: "query 接口"
|
||||
)
|
||||
@handler query
|
||||
get /query (QueryReq) returns (QueryResp)
|
||||
|
||||
@doc (
|
||||
description: "query path 中包含 id 字段接口"
|
||||
)
|
||||
@handler queryPath
|
||||
get /query/:id (PathQueryReq) returns (PathQueryResp)
|
||||
}
|
||||
|
||||
type (
|
||||
FormReq {
|
||||
Id int `form:"id,range=[1:10000],example=10"`
|
||||
Name string `form:"name,example=keson.an"`
|
||||
}
|
||||
FormResp {
|
||||
Id int `json:"id,example=10"`
|
||||
Name string `json:"name,example=keson.an"`
|
||||
}
|
||||
)
|
||||
|
||||
@server (
|
||||
tags: "form 表单 api 演示" // 对应 swagger 的 tags,可以对 swagger 中的 api 进行分组
|
||||
summary: "form 表单类型接口集合" // 对应 swagger 的 summary
|
||||
)
|
||||
service Swagger {
|
||||
@doc (
|
||||
description: "form 接口"
|
||||
)
|
||||
@handler form
|
||||
post /form (FormReq) returns (FormResp)
|
||||
}
|
||||
|
||||
type (
|
||||
JsonReq {
|
||||
Id int `json:"id,range=[1:10000],example=10"`
|
||||
Name string `json:"name,example=keson.an"`
|
||||
Avatar string `json:"avatar,optional"`
|
||||
Language string `json:"language,options=golang|java|python|typescript|rust"`
|
||||
Gender string `json:"gender,default=male,options=male|female,example=male"`
|
||||
}
|
||||
JsonResp {
|
||||
Id int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Avatar string `json:"avatar"`
|
||||
Language string `json:"language"`
|
||||
Gender string `json:"gender"`
|
||||
}
|
||||
ComplexJsonLevel2 {
|
||||
// basic
|
||||
Integer int `json:"integer,example=1"`
|
||||
Number float64 `json:"number,example=1.1"`
|
||||
Boolean bool `json:"boolean,options=true|false,example=true"`
|
||||
String string `json:"string,example=some text"`
|
||||
}
|
||||
ComplexJsonLevel1 {
|
||||
// basic
|
||||
Integer int `json:"integer,example=1"`
|
||||
Number float64 `json:"number,example=1.1"`
|
||||
Boolean bool `json:"boolean,options=true|false,example=true"`
|
||||
String string `json:"string,example=some text"`
|
||||
// Object
|
||||
Object ComplexJsonLevel2 `json:"object"`
|
||||
PointerObject *ComplexJsonLevel2 `json:"pointerObject"`
|
||||
}
|
||||
ComplexJsonReq {
|
||||
// basic
|
||||
Integer int `json:"integer,example=1"`
|
||||
Number float64 `json:"number,example=1.1"`
|
||||
Boolean bool `json:"boolean,options=true|false,example=true"`
|
||||
String string `json:"string,example=some text"`
|
||||
// basic array
|
||||
ArrayInteger []int `json:"arrayInteger"`
|
||||
ArrayNumber []float64 `json:"arrayNumber"`
|
||||
ArrayBoolean []bool `json:"arrayBoolean"`
|
||||
ArrayString []string `json:"arrayString"`
|
||||
// basic array array
|
||||
ArrayArrayInteger [][]int `json:"arrayArrayInteger"`
|
||||
ArrayArrayNumber [][]float64 `json:"arrayArrayNumber"`
|
||||
ArrayArrayBoolean [][]bool `json:"arrayArrayBoolean"`
|
||||
ArrayArrayString [][]string `json:"arrayArrayString"`
|
||||
// basic map
|
||||
MapInteger map[string]int `json:"mapInteger"`
|
||||
MapNumber map[string]float64 `json:"mapNumber"`
|
||||
MapBoolean map[string]bool `json:"mapBoolean"`
|
||||
MapString map[string]string `json:"mapString"`
|
||||
// basic map array
|
||||
MapArrayInteger map[string][]int `json:"mapArrayInteger"`
|
||||
MapArrayNumber map[string][]float64 `json:"mapArrayNumber"`
|
||||
MapArrayBoolean map[string][]bool `json:"mapArrayBoolean"`
|
||||
MapArrayString map[string][]string `json:"mapArrayString"`
|
||||
// basic map map
|
||||
MapMapInteger map[string]map[string]int `json:"mapMapInteger"`
|
||||
MapMapNumber map[string]map[string]float64 `json:"mapMapNumber"`
|
||||
MapMapBoolean map[string]map[string]bool `json:"mapMapBoolean"`
|
||||
MapMapString map[string]map[string]string `json:"mapMapString"`
|
||||
// Object
|
||||
Object ComplexJsonLevel1 `json:"object"`
|
||||
PointerObject *ComplexJsonLevel1 `json:"pointerObject"`
|
||||
// Object array
|
||||
ArrayObject []ComplexJsonLevel1 `json:"arrayObject"`
|
||||
ArrayPointerObject []*ComplexJsonLevel1 `json:"arrayPointerObject"`
|
||||
// Object map
|
||||
MapObject map[string]ComplexJsonLevel1 `json:"mapObject"`
|
||||
MapPointerObject map[string]*ComplexJsonLevel1 `json:"mapPointerObject"`
|
||||
// Object array array
|
||||
ArrayArrayObject [][]ComplexJsonLevel1 `json:"arrayArrayObject"`
|
||||
ArrayArrayPointerObject [][]*ComplexJsonLevel1 `json:"arrayArrayPointerObject"`
|
||||
// Object array map
|
||||
ArrayMapObject []map[string]ComplexJsonLevel1 `json:"arrayMapObject"`
|
||||
ArrayMapPointerObject []map[string]*ComplexJsonLevel1 `json:"arrayMapPointerObject"`
|
||||
// Object map array
|
||||
MapArrayObject map[string][]ComplexJsonLevel1 `json:"mapArrayObject"`
|
||||
MapArrayPointerObject map[string][]*ComplexJsonLevel1 `json:"mapArrayPointerObject"`
|
||||
}
|
||||
ComplexJsonResp {
|
||||
// basic
|
||||
Integer int `json:"integer,example=1"`
|
||||
Number float64 `json:"number,example=1.1"`
|
||||
Boolean bool `json:"boolean,options=true|false,example=true"`
|
||||
String string `json:"string,example=some text"`
|
||||
// basic array
|
||||
ArrayInteger []int `json:"arrayInteger"`
|
||||
ArrayNumber []float64 `json:"arrayNumber"`
|
||||
ArrayBoolean []bool `json:"arrayBoolean"`
|
||||
ArrayString []string `json:"arrayString"`
|
||||
// basic array array
|
||||
ArrayArrayInteger [][]int `json:"arrayArrayInteger"`
|
||||
ArrayArrayNumber [][]float64 `json:"arrayArrayNumber"`
|
||||
ArrayArrayBoolean [][]bool `json:"arrayArrayBoolean"`
|
||||
ArrayArrayString [][]string `json:"arrayArrayString"`
|
||||
// basic map
|
||||
MapInteger map[string]int `json:"mapInteger"`
|
||||
MapNumber map[string]float64 `json:"mapNumber"`
|
||||
MapBoolean map[string]bool `json:"mapBoolean"`
|
||||
MapString map[string]string `json:"mapString"`
|
||||
// basic map array
|
||||
MapArrayInteger map[string][]int `json:"mapArrayInteger"`
|
||||
MapArrayNumber map[string][]float64 `json:"mapArrayNumber"`
|
||||
MapArrayBoolean map[string][]bool `json:"mapArrayBoolean"`
|
||||
MapArrayString map[string][]string `json:"mapArrayString"`
|
||||
// basic map map
|
||||
MapMapInteger map[string]map[string]int `json:"mapMapInteger"`
|
||||
MapMapNumber map[string]map[string]float64 `json:"mapMapNumber"`
|
||||
MapMapBoolean map[string]map[string]bool `json:"mapMapBoolean"`
|
||||
MapMapString map[string]map[string]string `json:"mapMapString"`
|
||||
// Object
|
||||
Object ComplexJsonLevel1 `json:"object"`
|
||||
PointerObject *ComplexJsonLevel1 `json:"pointerObject"`
|
||||
// Object array
|
||||
ArrayObject []ComplexJsonLevel1 `json:"arrayObject"`
|
||||
ArrayPointerObject []*ComplexJsonLevel1 `json:"arrayPointerObject"`
|
||||
// Object map
|
||||
MapObject map[string]ComplexJsonLevel1 `json:"mapObject"`
|
||||
MapPointerObject map[string]*ComplexJsonLevel1 `json:"mapPointerObject"`
|
||||
// Object array array
|
||||
ArrayArrayObject [][]ComplexJsonLevel1 `json:"arrayArrayObject"`
|
||||
ArrayArrayPointerObject [][]*ComplexJsonLevel1 `json:"arrayArrayPointerObject"`
|
||||
// Object array map
|
||||
ArrayMapObject []map[string]ComplexJsonLevel1 `json:"arrayMapObject"`
|
||||
ArrayMapPointerObject []map[string]*ComplexJsonLevel1 `json:"arrayMapPointerObject"`
|
||||
// Object map array
|
||||
MapArrayObject map[string][]ComplexJsonLevel1 `json:"mapArrayObject"`
|
||||
MapArrayPointerObject map[string][]*ComplexJsonLevel1 `json:"mapArrayPointerObject"`
|
||||
}
|
||||
)
|
||||
|
||||
@server (
|
||||
tags: "post json api 演示" // 对应 swagger 的 tags,可以对 swagger 中的 api 进行分组
|
||||
summary: "json 请求类型接口集合" // 对应 swagger 的 summary
|
||||
)
|
||||
service Swagger {
|
||||
@doc (
|
||||
description: "简单的 json 请求体接口"
|
||||
)
|
||||
@handler jsonSimple
|
||||
post /json/simple (JsonReq) returns (JsonResp)
|
||||
|
||||
@doc (
|
||||
description: "复杂的 json 请求体接口"
|
||||
)
|
||||
@handler jsonComplex
|
||||
post /json/complex (ComplexJsonReq) returns (ComplexJsonResp)
|
||||
}
|
||||
|
||||
39
tools/goctl/api/swagger/example/go-swagger-cn.sh
Normal file
39
tools/goctl/api/swagger/example/go-swagger-cn.sh
Normal file
@@ -0,0 +1,39 @@
|
||||
#!/bin/bash
|
||||
|
||||
# 1. 检查并安装 swagger
|
||||
if ! command -v swagger &> /dev/null; then
|
||||
echo "swagger 未安装,正在从 GitHub 安装..."
|
||||
# 这里使用 go-swagger 的安装方式
|
||||
go install github.com/go-swagger/go-swagger/cmd/swagger@latest
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "安装 swagger 失败"
|
||||
exit 1
|
||||
fi
|
||||
echo "swagger 安装成功"
|
||||
else
|
||||
echo "swagger 已安装"
|
||||
fi
|
||||
|
||||
mkdir bin output
|
||||
|
||||
export GOBIN=$(pwd)/bin
|
||||
|
||||
# 2. 安装最新版 goctl
|
||||
go install ../../..
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "安装 goctl 失败"
|
||||
exit 1
|
||||
fi
|
||||
echo "goctl 安装成功"
|
||||
|
||||
# 3. 生成 swagger 文件
|
||||
echo "正在生成 swagger 文件..."
|
||||
./bin/goctl api swagger --api example_cn.api --dir output
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "生成 swagger 文件失败"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 4. 启动 swagger 服务
|
||||
echo "启动 swagger 服务..."
|
||||
swagger serve ./output/example_cn.json
|
||||
BIN
tools/goctl/api/swagger/example/go-swagger-ui-cn.png
Normal file
BIN
tools/goctl/api/swagger/example/go-swagger-ui-cn.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.3 MiB |
BIN
tools/goctl/api/swagger/example/go-swagger-ui.png
Normal file
BIN
tools/goctl/api/swagger/example/go-swagger-ui.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.6 MiB |
39
tools/goctl/api/swagger/example/go-swagger.sh
Normal file
39
tools/goctl/api/swagger/example/go-swagger.sh
Normal file
@@ -0,0 +1,39 @@
|
||||
#!/bin/bash
|
||||
|
||||
# 1. Check and install swagger if not exists
|
||||
if ! command -v swagger &> /dev/null; then
|
||||
echo "swagger not found, installing from GitHub..."
|
||||
# Using go-swagger installation method
|
||||
go install github.com/go-swagger/go-swagger/cmd/swagger@latest
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Failed to install swagger"
|
||||
exit 1
|
||||
fi
|
||||
echo "swagger installed successfully"
|
||||
else
|
||||
echo "swagger already installed"
|
||||
fi
|
||||
|
||||
mkdir bin output
|
||||
|
||||
export GOBIN=$(pwd)/bin
|
||||
|
||||
# 2. Install latest goctl version
|
||||
go install ../../..
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Failed to install goctl"
|
||||
exit 1
|
||||
fi
|
||||
echo "goctl installed successfully"
|
||||
|
||||
# 3. Generate swagger files
|
||||
echo "Generating swagger files..."
|
||||
./bin/goctl api swagger --api example.api --dir output
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Failed to generate swagger files"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 4. Start swagger server
|
||||
echo "Starting swagger server..."
|
||||
swagger serve ./output/example.json
|
||||
BIN
tools/goctl/api/swagger/example/swagger-ui-cn-example.png
Normal file
BIN
tools/goctl/api/swagger/example/swagger-ui-cn-example.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.7 MiB |
BIN
tools/goctl/api/swagger/example/swagger-ui-cn-model.png
Normal file
BIN
tools/goctl/api/swagger/example/swagger-ui-cn-model.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.4 MiB |
91
tools/goctl/api/swagger/example/swagger-ui-cn.sh
Normal file
91
tools/goctl/api/swagger/example/swagger-ui-cn.sh
Normal file
@@ -0,0 +1,91 @@
|
||||
#!/bin/bash
|
||||
|
||||
# 检查Docker是否运行的函数
|
||||
is_docker_running() {
|
||||
if ! docker info >/dev/null 2>&1; then
|
||||
return 1 # Docker未运行
|
||||
else
|
||||
return 0 # Docker正在运行
|
||||
fi
|
||||
}
|
||||
|
||||
mkdir bin output
|
||||
|
||||
export GOBIN=$(pwd)/bin
|
||||
|
||||
# 1. 检查并安装Docker(如果不存在)
|
||||
if ! command -v docker &> /dev/null; then
|
||||
echo "未检测到Docker,正在尝试安装..."
|
||||
|
||||
# 使用官方脚本安装Docker
|
||||
curl -fsSL https://get.docker.com -o get-docker.sh
|
||||
sudo sh get-docker.sh
|
||||
rm get-docker.sh
|
||||
|
||||
# 验证安装
|
||||
if ! command -v docker &> /dev/null; then
|
||||
echo "Docker安装失败"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 将当前用户加入docker组(可能需要重新登录)
|
||||
sudo usermod -aG docker $USER
|
||||
echo "Docker安装成功。您可能需要注销并重新登录使更改生效。"
|
||||
else
|
||||
echo "Docker已安装"
|
||||
fi
|
||||
|
||||
# 2. 安装最新版goctl
|
||||
go install ../../..
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "goctl安装失败"
|
||||
exit 1
|
||||
fi
|
||||
echo "goctl 安装成功"
|
||||
|
||||
# 3. 生成swagger文件
|
||||
echo "正在生成swagger文件..."
|
||||
./bin/goctl api swagger --api example_cn.api --dir output
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "swagger文件生成失败"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 检查Docker是否运行
|
||||
if ! is_docker_running; then
|
||||
echo "Docker未运行,请先启动Docker服务"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 4. 清理现有的swagger-ui容器
|
||||
echo "正在清理现有的swagger-ui容器..."
|
||||
docker rm -f swagger-ui 2>/dev/null && echo "已移除现有的swagger-ui容器"
|
||||
|
||||
# 5. 在Docker中运行swagger-ui
|
||||
echo "正在启动swagger-ui容器..."
|
||||
docker run -d --name swagger-ui -p 8080:8080 \
|
||||
-e SWAGGER_JSON=/tmp/example.json \
|
||||
-v $(pwd)/output/example_cn.json:/tmp/example.json \
|
||||
swaggerapi/swagger-ui
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "swagger-ui容器启动失败"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 等待1秒确保服务就绪
|
||||
echo "等待swagger-ui初始化..."
|
||||
sleep 1
|
||||
|
||||
# 显示访问信息并尝试打开浏览器
|
||||
SWAGGER_URL="http://localhost:8080"
|
||||
echo -e "\nSwagger UI 已准备就绪,访问地址: \033[1;34m${SWAGGER_URL}\033[0m"
|
||||
echo "正在尝试在默认浏览器中打开..."
|
||||
|
||||
# 跨平台打开浏览器
|
||||
case "$(uname -s)" in
|
||||
Linux*) xdg-open "$SWAGGER_URL";;
|
||||
Darwin*) open "$SWAGGER_URL";;
|
||||
CYGWIN*|MINGW*|MSYS*) start "$SWAGGER_URL";;
|
||||
*) echo "无法在当前操作系统自动打开浏览器";;
|
||||
esac
|
||||
BIN
tools/goctl/api/swagger/example/swagger-ui-example.png
Normal file
BIN
tools/goctl/api/swagger/example/swagger-ui-example.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.6 MiB |
BIN
tools/goctl/api/swagger/example/swagger-ui-model.png
Normal file
BIN
tools/goctl/api/swagger/example/swagger-ui-model.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.4 MiB |
80
tools/goctl/api/swagger/example/swagger-ui.sh
Normal file
80
tools/goctl/api/swagger/example/swagger-ui.sh
Normal file
@@ -0,0 +1,80 @@
|
||||
#!/bin/bash
|
||||
|
||||
is_docker_running() {
|
||||
if ! docker info >/dev/null 2>&1; then
|
||||
return 1 # Docker is not running
|
||||
else
|
||||
return 0 # Docker is running
|
||||
fi
|
||||
}
|
||||
|
||||
mkdir bin output
|
||||
|
||||
export GOBIN=$(pwd)/bin
|
||||
|
||||
# 1. Check and install Docker if not exists
|
||||
if ! command -v docker &> /dev/null; then
|
||||
echo "Docker not found, attempting to install..."
|
||||
|
||||
# Install Docker using official installation script
|
||||
curl -fsSL https://get.docker.com -o get-docker.sh
|
||||
sudo sh get-docker.sh
|
||||
rm get-docker.sh
|
||||
|
||||
# Verify installation
|
||||
if ! command -v docker &> /dev/null; then
|
||||
echo "Failed to install Docker"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Add current user to docker group (may require logout/login)
|
||||
sudo usermod -aG docker $USER
|
||||
echo "Docker installed successfully. You may need to logout and login again for changes to take effect."
|
||||
else
|
||||
echo "Docker already installed"
|
||||
fi
|
||||
|
||||
# 2. Install latest goctl version
|
||||
go install ../../..
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Failed to install goctl"
|
||||
exit 1
|
||||
fi
|
||||
echo "goctl installed successfully"
|
||||
|
||||
# 3. Generate swagger files
|
||||
echo "Generating swagger files..."
|
||||
./bin/goctl api swagger --api example.api --dir output
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Failed to generate swagger files"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! is_docker_running; then
|
||||
echo "Docker is not running, Pls start Docker first"
|
||||
fi
|
||||
|
||||
# 4. Clean up any existing swagger-ui container
|
||||
echo "Cleaning up existing swagger-ui containers..."
|
||||
docker rm -f swagger-ui 2>/dev/null && echo "Removed existing swagger-ui container"
|
||||
|
||||
# 5. Run swagger-ui in Docker
|
||||
echo "Starting swagger-ui in Docker..."
|
||||
docker run -d --name swagger-ui -p 8080:8080 -e SWAGGER_JSON=/tmp/example.json -v $(pwd)/output/example.json:/tmp/example.json swaggerapi/swagger-ui
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Failed to start swagger-ui container"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Waiting for swagger-ui to initialize..."
|
||||
sleep 1
|
||||
SWAGGER_URL="http://localhost:8080"
|
||||
echo -e "\nSwagger UI is ready at: \033[1;34m${SWAGGER_URL}\033[0m"
|
||||
echo "Opening in default browser..."
|
||||
|
||||
case "$(uname -s)" in
|
||||
Linux*) xdg-open "$SWAGGER_URL";;
|
||||
Darwin*) open "$SWAGGER_URL";;
|
||||
CYGWIN*|MINGW*|MSYS*) start "$SWAGGER_URL";;
|
||||
*) echo "System not supported";;
|
||||
esac
|
||||
123
tools/goctl/api/swagger/options.go
Normal file
123
tools/goctl/api/swagger/options.go
Normal file
@@ -0,0 +1,123 @@
|
||||
package swagger
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/zeromicro/go-zero/tools/goctl/api/spec"
|
||||
"github.com/zeromicro/go-zero/tools/goctl/util"
|
||||
)
|
||||
|
||||
func rangeValueFromOptions(options []string) (minimum *float64, maximum *float64, exclusiveMinimum bool, exclusiveMaximum bool) {
|
||||
if len(options) == 0 {
|
||||
return nil, nil, false, false
|
||||
}
|
||||
for _, option := range options {
|
||||
if strings.HasPrefix(option, rangeFlag) {
|
||||
val := option[6:]
|
||||
start, end := val[0], val[len(val)-1]
|
||||
if start != '[' && start != '(' {
|
||||
return nil, nil, false, false
|
||||
}
|
||||
if end != ']' && end != ')' {
|
||||
return nil, nil, false, false
|
||||
}
|
||||
exclusiveMinimum = start == '('
|
||||
exclusiveMaximum = end == ')'
|
||||
|
||||
content := val[1 : len(val)-1]
|
||||
idxColon := strings.Index(content, ":")
|
||||
if idxColon < 0 {
|
||||
return nil, nil, false, false
|
||||
}
|
||||
var (
|
||||
minStr, maxStr string
|
||||
minVal, maxVal *float64
|
||||
)
|
||||
minStr = util.TrimWhiteSpace(content[:idxColon])
|
||||
if len(val) >= idxColon+1 {
|
||||
maxStr = util.TrimWhiteSpace(content[idxColon+1:])
|
||||
}
|
||||
|
||||
if len(minStr) > 0 {
|
||||
min, err := strconv.ParseFloat(minStr, 64)
|
||||
if err != nil {
|
||||
return nil, nil, false, false
|
||||
}
|
||||
minVal = &min
|
||||
}
|
||||
|
||||
if len(maxStr) > 0 {
|
||||
max, err := strconv.ParseFloat(maxStr, 64)
|
||||
if err != nil {
|
||||
return nil, nil, false, false
|
||||
}
|
||||
maxVal = &max
|
||||
}
|
||||
|
||||
return minVal, maxVal, exclusiveMinimum, exclusiveMaximum
|
||||
}
|
||||
}
|
||||
return nil, nil, false, false
|
||||
}
|
||||
|
||||
func enumsValueFromOptions(options []string) []any {
|
||||
if len(options) == 0 {
|
||||
return []any{}
|
||||
}
|
||||
for _, option := range options {
|
||||
if strings.HasPrefix(option, enumFlag) {
|
||||
var resp = make([]any, 0)
|
||||
val := option[8:]
|
||||
fields := util.FieldsAndTrimSpace(val, func(r rune) bool {
|
||||
return r == '|'
|
||||
})
|
||||
for _, field := range fields {
|
||||
resp = append(resp, field)
|
||||
}
|
||||
return resp
|
||||
}
|
||||
}
|
||||
return []any{}
|
||||
}
|
||||
|
||||
func defValueFromOptions(options []string, apiType spec.Type) any {
|
||||
tp := sampleTypeFromGoType(apiType)
|
||||
return valueFromOptions(options, defFlag, tp)
|
||||
}
|
||||
|
||||
func exampleValueFromOptions(options []string, apiType spec.Type) any {
|
||||
tp := sampleTypeFromGoType(apiType)
|
||||
val := valueFromOptions(options, exampleFlag, tp)
|
||||
if val != nil {
|
||||
return val
|
||||
}
|
||||
return defValueFromOptions(options, apiType)
|
||||
}
|
||||
|
||||
func valueFromOptions(options []string, key string, tp string) any {
|
||||
if len(options) == 0 {
|
||||
return nil
|
||||
}
|
||||
for _, option := range options {
|
||||
if strings.HasPrefix(option, key) {
|
||||
s := option[len(key):]
|
||||
switch tp {
|
||||
case "integer":
|
||||
val, _ := strconv.ParseInt(s, 10, 64)
|
||||
return val
|
||||
case "boolean":
|
||||
val, _ := strconv.ParseBool(s)
|
||||
return val
|
||||
case "number":
|
||||
val, _ := strconv.ParseFloat(s, 64)
|
||||
return val
|
||||
case "string":
|
||||
return s
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
258
tools/goctl/api/swagger/options_test.go
Normal file
258
tools/goctl/api/swagger/options_test.go
Normal file
@@ -0,0 +1,258 @@
|
||||
package swagger
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/zeromicro/go-zero/tools/goctl/api/spec"
|
||||
)
|
||||
|
||||
func TestRangeValueFromOptions(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
options []string
|
||||
expectedMin *float64
|
||||
expectedMax *float64
|
||||
expectedExclMin bool
|
||||
expectedExclMax bool
|
||||
}{
|
||||
{
|
||||
name: "Valid range with inclusive bounds",
|
||||
options: []string{"range=[1.0:10.0]"},
|
||||
expectedMin: floatPtr(1.0),
|
||||
expectedMax: floatPtr(10.0),
|
||||
expectedExclMin: false,
|
||||
expectedExclMax: false,
|
||||
},
|
||||
{
|
||||
name: "Valid range with exclusive bounds",
|
||||
options: []string{"range=(1.0:10.0)"},
|
||||
expectedMin: floatPtr(1.0),
|
||||
expectedMax: floatPtr(10.0),
|
||||
expectedExclMin: true,
|
||||
expectedExclMax: true,
|
||||
},
|
||||
{
|
||||
name: "Invalid range format",
|
||||
options: []string{"range=1.0:10.0"},
|
||||
expectedMin: nil,
|
||||
expectedMax: nil,
|
||||
expectedExclMin: false,
|
||||
expectedExclMax: false,
|
||||
},
|
||||
{
|
||||
name: "Invalid range start",
|
||||
options: []string{"range=[a:1.0)"},
|
||||
expectedMin: nil,
|
||||
expectedMax: nil,
|
||||
expectedExclMin: false,
|
||||
expectedExclMax: false,
|
||||
},
|
||||
{
|
||||
name: "Missing range end",
|
||||
options: []string{"range=[1.0:)"},
|
||||
expectedMin: floatPtr(1.0),
|
||||
expectedMax: nil,
|
||||
expectedExclMin: false,
|
||||
expectedExclMax: true,
|
||||
},
|
||||
{
|
||||
name: "Missing range start and end",
|
||||
options: []string{"range=[:)"},
|
||||
expectedMin: nil,
|
||||
expectedMax: nil,
|
||||
expectedExclMin: false,
|
||||
expectedExclMax: true,
|
||||
},
|
||||
{
|
||||
name: "Missing range start",
|
||||
options: []string{"range=[:1.0)"},
|
||||
expectedMin: nil,
|
||||
expectedMax: floatPtr(1.0),
|
||||
expectedExclMin: false,
|
||||
expectedExclMax: true,
|
||||
},
|
||||
{
|
||||
name: "Invalid range end",
|
||||
options: []string{"range=[1.0:b)"},
|
||||
expectedMin: nil,
|
||||
expectedMax: nil,
|
||||
expectedExclMin: false,
|
||||
expectedExclMax: false,
|
||||
},
|
||||
{
|
||||
name: "Empty options",
|
||||
options: []string{},
|
||||
expectedMin: nil,
|
||||
expectedMax: nil,
|
||||
expectedExclMin: false,
|
||||
expectedExclMax: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
min, max, exclMin, exclMax := rangeValueFromOptions(tt.options)
|
||||
assert.Equal(t, tt.expectedMin, min)
|
||||
assert.Equal(t, tt.expectedMax, max)
|
||||
assert.Equal(t, tt.expectedExclMin, exclMin)
|
||||
assert.Equal(t, tt.expectedExclMax, exclMax)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnumsValueFromOptions(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
options []string
|
||||
expected []any
|
||||
}{
|
||||
{
|
||||
name: "Valid enums",
|
||||
options: []string{"options=a|b|c"},
|
||||
expected: []any{"a", "b", "c"},
|
||||
},
|
||||
{
|
||||
name: "Empty enums",
|
||||
options: []string{"options="},
|
||||
expected: []any{},
|
||||
},
|
||||
{
|
||||
name: "No enum option",
|
||||
options: []string{},
|
||||
expected: []any{},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := enumsValueFromOptions(tt.options)
|
||||
assert.Equal(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefValueFromOptions(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
options []string
|
||||
apiType spec.Type
|
||||
expected any
|
||||
}{
|
||||
{
|
||||
name: "Default integer value",
|
||||
options: []string{"default=42"},
|
||||
apiType: spec.PrimitiveType{RawName: "int"},
|
||||
expected: int64(42),
|
||||
},
|
||||
{
|
||||
name: "Default string value",
|
||||
options: []string{"default=hello"},
|
||||
apiType: spec.PrimitiveType{RawName: "string"},
|
||||
expected: "hello",
|
||||
},
|
||||
{
|
||||
name: "No default value",
|
||||
options: []string{},
|
||||
apiType: spec.PrimitiveType{RawName: "string"},
|
||||
expected: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := defValueFromOptions(tt.options, tt.apiType)
|
||||
assert.Equal(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExampleValueFromOptions(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
options []string
|
||||
apiType spec.Type
|
||||
expected any
|
||||
}{
|
||||
{
|
||||
name: "Example value present",
|
||||
options: []string{"example=3.14"},
|
||||
apiType: spec.PrimitiveType{RawName: "float"},
|
||||
expected: 3.14,
|
||||
},
|
||||
{
|
||||
name: "Fallback to default value",
|
||||
options: []string{"default=42"},
|
||||
apiType: spec.PrimitiveType{RawName: "int"},
|
||||
expected: int64(42),
|
||||
},
|
||||
{
|
||||
name: "Fallback to default value",
|
||||
options: []string{"default="},
|
||||
apiType: spec.PrimitiveType{RawName: "int"},
|
||||
expected: int64(0),
|
||||
},
|
||||
{
|
||||
name: "No example or default value",
|
||||
options: []string{},
|
||||
apiType: spec.PrimitiveType{RawName: "string"},
|
||||
expected: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
exampleValueFromOptions(tt.options, tt.apiType)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValueFromOptions(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
options []string
|
||||
key string
|
||||
tp string
|
||||
expected any
|
||||
}{
|
||||
{
|
||||
name: "Integer value",
|
||||
options: []string{"default=42"},
|
||||
key: "default=",
|
||||
tp: "integer",
|
||||
expected: int64(42),
|
||||
},
|
||||
{
|
||||
name: "Boolean value",
|
||||
options: []string{"default=true"},
|
||||
key: "default=",
|
||||
tp: "boolean",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "Number value",
|
||||
options: []string{"default=1.1"},
|
||||
key: "default=",
|
||||
tp: "number",
|
||||
expected: 1.1,
|
||||
},
|
||||
{
|
||||
name: "No matching key",
|
||||
options: []string{"example=42"},
|
||||
key: "default=",
|
||||
tp: "integer",
|
||||
expected: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := valueFromOptions(tt.options, tt.key, tt.tp)
|
||||
assert.Equal(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func floatPtr(f float64) *float64 {
|
||||
return &f
|
||||
}
|
||||
178
tools/goctl/api/swagger/parameter.go
Normal file
178
tools/goctl/api/swagger/parameter.go
Normal file
@@ -0,0 +1,178 @@
|
||||
package swagger
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/go-openapi/spec"
|
||||
apiSpec "github.com/zeromicro/go-zero/tools/goctl/api/spec"
|
||||
)
|
||||
|
||||
func parametersFromType(method string, tp apiSpec.Type) []spec.Parameter {
|
||||
if tp == nil {
|
||||
return []spec.Parameter{}
|
||||
}
|
||||
structType, ok := tp.(apiSpec.DefineStruct)
|
||||
if !ok {
|
||||
return []spec.Parameter{}
|
||||
}
|
||||
var (
|
||||
resp []spec.Parameter
|
||||
properties = map[string]spec.Schema{}
|
||||
requiredFields []string
|
||||
)
|
||||
rangeMemberAndDo(structType, func(tag *apiSpec.Tags, required bool, member apiSpec.Member) {
|
||||
headerTag, _ := tag.Get(tagHeader)
|
||||
hasHeader := headerTag != nil
|
||||
|
||||
pathParameterTag, _ := tag.Get(tagPath)
|
||||
hasPathParameter := pathParameterTag != nil
|
||||
|
||||
formTag, _ := tag.Get(tagForm)
|
||||
hasForm := formTag != nil
|
||||
|
||||
jsonTag, _ := tag.Get(tagJson)
|
||||
hasJson := jsonTag != nil
|
||||
if hasHeader {
|
||||
minimum, maximum, exclusiveMinimum, exclusiveMaximum := rangeValueFromOptions(headerTag.Options)
|
||||
resp = append(resp, spec.Parameter{
|
||||
CommonValidations: spec.CommonValidations{
|
||||
Maximum: maximum,
|
||||
ExclusiveMaximum: exclusiveMaximum,
|
||||
Minimum: minimum,
|
||||
ExclusiveMinimum: exclusiveMinimum,
|
||||
Enum: enumsValueFromOptions(headerTag.Options),
|
||||
},
|
||||
SimpleSchema: spec.SimpleSchema{
|
||||
Type: sampleTypeFromGoType(member.Type),
|
||||
Default: defValueFromOptions(headerTag.Options, member.Type),
|
||||
Example: exampleValueFromOptions(headerTag.Options, member.Type),
|
||||
Items: sampleItemsFromGoType(member.Type),
|
||||
},
|
||||
ParamProps: spec.ParamProps{
|
||||
In: paramsInHeader,
|
||||
Name: headerTag.Name,
|
||||
Description: formatComment(member.Comment),
|
||||
Required: required,
|
||||
},
|
||||
})
|
||||
}
|
||||
if hasPathParameter {
|
||||
minimum, maximum, exclusiveMinimum, exclusiveMaximum := rangeValueFromOptions(pathParameterTag.Options)
|
||||
resp = append(resp, spec.Parameter{
|
||||
CommonValidations: spec.CommonValidations{
|
||||
Maximum: maximum,
|
||||
ExclusiveMaximum: exclusiveMaximum,
|
||||
Minimum: minimum,
|
||||
ExclusiveMinimum: exclusiveMinimum,
|
||||
Enum: enumsValueFromOptions(pathParameterTag.Options),
|
||||
},
|
||||
SimpleSchema: spec.SimpleSchema{
|
||||
Type: sampleTypeFromGoType(member.Type),
|
||||
Default: defValueFromOptions(pathParameterTag.Options, member.Type),
|
||||
Example: exampleValueFromOptions(pathParameterTag.Options, member.Type),
|
||||
Items: sampleItemsFromGoType(member.Type),
|
||||
},
|
||||
ParamProps: spec.ParamProps{
|
||||
In: paramsInPath,
|
||||
Name: pathParameterTag.Name,
|
||||
Description: formatComment(member.Comment),
|
||||
Required: required,
|
||||
},
|
||||
})
|
||||
}
|
||||
if hasForm {
|
||||
minimum, maximum, exclusiveMinimum, exclusiveMaximum := rangeValueFromOptions(formTag.Options)
|
||||
if strings.EqualFold(method, http.MethodGet) {
|
||||
resp = append(resp, spec.Parameter{
|
||||
CommonValidations: spec.CommonValidations{
|
||||
Maximum: maximum,
|
||||
ExclusiveMaximum: exclusiveMaximum,
|
||||
Minimum: minimum,
|
||||
ExclusiveMinimum: exclusiveMinimum,
|
||||
Enum: enumsValueFromOptions(formTag.Options),
|
||||
},
|
||||
SimpleSchema: spec.SimpleSchema{
|
||||
Type: sampleTypeFromGoType(member.Type),
|
||||
Default: defValueFromOptions(formTag.Options, member.Type),
|
||||
Example: exampleValueFromOptions(formTag.Options, member.Type),
|
||||
Items: sampleItemsFromGoType(member.Type),
|
||||
},
|
||||
ParamProps: spec.ParamProps{
|
||||
In: paramsInQuery,
|
||||
Name: formTag.Name,
|
||||
Description: formatComment(member.Comment),
|
||||
Required: required,
|
||||
AllowEmptyValue: !required,
|
||||
},
|
||||
})
|
||||
} else {
|
||||
resp = append(resp, spec.Parameter{
|
||||
CommonValidations: spec.CommonValidations{
|
||||
Maximum: maximum,
|
||||
ExclusiveMaximum: exclusiveMaximum,
|
||||
Minimum: minimum,
|
||||
ExclusiveMinimum: exclusiveMinimum,
|
||||
Enum: enumsValueFromOptions(formTag.Options),
|
||||
},
|
||||
SimpleSchema: spec.SimpleSchema{
|
||||
Type: sampleTypeFromGoType(member.Type),
|
||||
Default: defValueFromOptions(formTag.Options, member.Type),
|
||||
Example: exampleValueFromOptions(formTag.Options, member.Type),
|
||||
Items: sampleItemsFromGoType(member.Type),
|
||||
},
|
||||
ParamProps: spec.ParamProps{
|
||||
In: paramsInForm,
|
||||
Name: formTag.Name,
|
||||
Description: formatComment(member.Comment),
|
||||
Required: required,
|
||||
AllowEmptyValue: !required,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
if hasJson {
|
||||
minimum, maximum, exclusiveMinimum, exclusiveMaximum := rangeValueFromOptions(jsonTag.Options)
|
||||
if required {
|
||||
requiredFields = append(requiredFields, jsonTag.Name)
|
||||
}
|
||||
p, r := propertiesFromType(member.Type)
|
||||
properties[jsonTag.Name] = spec.Schema{
|
||||
SwaggerSchemaProps: spec.SwaggerSchemaProps{
|
||||
Example: exampleValueFromOptions(jsonTag.Options, member.Type),
|
||||
},
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: formatComment(member.Comment),
|
||||
Type: typeFromGoType(member.Type),
|
||||
Default: defValueFromOptions(jsonTag.Options, member.Type),
|
||||
Maximum: maximum,
|
||||
ExclusiveMaximum: exclusiveMaximum,
|
||||
Minimum: minimum,
|
||||
ExclusiveMinimum: exclusiveMinimum,
|
||||
Enum: enumsValueFromOptions(jsonTag.Options),
|
||||
Items: itemsFromGoType(member.Type),
|
||||
Properties: p,
|
||||
Required: r,
|
||||
AdditionalProperties: mapFromGoType(member.Type),
|
||||
},
|
||||
}
|
||||
}
|
||||
})
|
||||
if len(properties) > 0 {
|
||||
resp = append(resp, spec.Parameter{
|
||||
ParamProps: spec.ParamProps{
|
||||
In: paramsInBody,
|
||||
Required: true,
|
||||
Schema: &spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: typeFromGoType(structType),
|
||||
Properties: properties,
|
||||
Required: requiredFields,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
return resp
|
||||
}
|
||||
105
tools/goctl/api/swagger/path.go
Normal file
105
tools/goctl/api/swagger/path.go
Normal file
@@ -0,0 +1,105 @@
|
||||
package swagger
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/go-openapi/spec"
|
||||
apiSpec "github.com/zeromicro/go-zero/tools/goctl/api/spec"
|
||||
)
|
||||
|
||||
func spec2Paths(info apiSpec.Info, srv apiSpec.Service) *spec.Paths {
|
||||
paths := &spec.Paths{
|
||||
Paths: make(map[string]spec.PathItem),
|
||||
}
|
||||
for _, group := range srv.Groups {
|
||||
prefix := path.Clean(strings.TrimPrefix(group.GetAnnotation("prefix"), "/"))
|
||||
for _, route := range group.Routes {
|
||||
routPath := pathVariable2SwaggerVariable(route.Path)
|
||||
if len(prefix) > 0 && prefix != "." {
|
||||
routPath = "/" + path.Clean(prefix) + routPath
|
||||
}
|
||||
pathItem := spec2Path(info, group, route)
|
||||
existPathItem, ok := paths.Paths[routPath]
|
||||
if !ok {
|
||||
paths.Paths[routPath] = pathItem
|
||||
} else {
|
||||
paths.Paths[routPath] = mergePathItem(existPathItem, pathItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
return paths
|
||||
}
|
||||
|
||||
func mergePathItem(old, new spec.PathItem) spec.PathItem {
|
||||
if new.Get != nil {
|
||||
old.Get = new.Get
|
||||
}
|
||||
if new.Put != nil {
|
||||
old.Put = new.Put
|
||||
}
|
||||
if new.Post != nil {
|
||||
old.Post = new.Post
|
||||
}
|
||||
if new.Delete != nil {
|
||||
old.Delete = new.Delete
|
||||
}
|
||||
if new.Options != nil {
|
||||
old.Options = new.Options
|
||||
}
|
||||
if new.Head != nil {
|
||||
old.Head = new.Head
|
||||
}
|
||||
if new.Patch != nil {
|
||||
old.Patch = new.Patch
|
||||
}
|
||||
if new.Parameters != nil {
|
||||
old.Parameters = new.Parameters
|
||||
}
|
||||
return old
|
||||
}
|
||||
|
||||
func spec2Path(info apiSpec.Info, group apiSpec.Group, route apiSpec.Route) spec.PathItem {
|
||||
op := &spec.Operation{
|
||||
OperationProps: spec.OperationProps{
|
||||
Description: getStringFromKVOrDefault(route.AtDoc.Properties, "description", ""),
|
||||
Consumes: consumesFromTypeOrDef(route.Method, route.RequestType),
|
||||
Produces: getListFromInfoOrDefault(route.AtDoc.Properties, "produces", []string{applicationJson}),
|
||||
Schemes: getListFromInfoOrDefault(route.AtDoc.Properties, "schemes", []string{schemeHttps}),
|
||||
Tags: getListFromInfoOrDefault(group.Annotation.Properties, "tags", []string{""}),
|
||||
Summary: getStringFromKVOrDefault(route.AtDoc.Properties, "summary", ""),
|
||||
Deprecated: getBoolFromKVOrDefault(route.AtDoc.Properties, "deprecated", false),
|
||||
Parameters: parametersFromType(route.Method, route.RequestType),
|
||||
Responses: jsonResponseFromType(info, route.ResponseType),
|
||||
},
|
||||
}
|
||||
externalDocsDescription := getStringFromKVOrDefault(route.AtDoc.Properties, "externalDocsDescription", "")
|
||||
externalDocsURL := getStringFromKVOrDefault(route.AtDoc.Properties, "externalDocsURL", "")
|
||||
if len(externalDocsDescription) > 0 || len(externalDocsURL) > 0 {
|
||||
op.ExternalDocs = &spec.ExternalDocumentation{
|
||||
Description: externalDocsDescription,
|
||||
URL: externalDocsURL,
|
||||
}
|
||||
|
||||
}
|
||||
item := spec.PathItem{}
|
||||
switch strings.ToUpper(route.Method) {
|
||||
case http.MethodGet:
|
||||
item.Get = op
|
||||
case http.MethodHead:
|
||||
item.Head = op
|
||||
case http.MethodPost:
|
||||
item.Post = op
|
||||
case http.MethodPut:
|
||||
item.Put = op
|
||||
case http.MethodPatch:
|
||||
item.Patch = op
|
||||
case http.MethodDelete:
|
||||
item.Delete = op
|
||||
case http.MethodOptions:
|
||||
item.Options = op
|
||||
default: // [http.MethodConnect,http.MethodTrace] not supported
|
||||
}
|
||||
return item
|
||||
}
|
||||
63
tools/goctl/api/swagger/properties.go
Normal file
63
tools/goctl/api/swagger/properties.go
Normal file
@@ -0,0 +1,63 @@
|
||||
package swagger
|
||||
|
||||
import (
|
||||
"github.com/go-openapi/spec"
|
||||
apiSpec "github.com/zeromicro/go-zero/tools/goctl/api/spec"
|
||||
)
|
||||
|
||||
func propertiesFromType(tp apiSpec.Type) (spec.SchemaProperties, []string) {
|
||||
var (
|
||||
properties = map[string]spec.Schema{}
|
||||
requiredFields []string
|
||||
)
|
||||
switch val := tp.(type) {
|
||||
case apiSpec.PointerType:
|
||||
return propertiesFromType(val.Type)
|
||||
case apiSpec.ArrayType:
|
||||
return propertiesFromType(val.Value)
|
||||
case apiSpec.DefineStruct, apiSpec.NestedStruct:
|
||||
rangeMemberAndDo(val, func(tag *apiSpec.Tags, required bool, member apiSpec.Member) {
|
||||
var (
|
||||
jsonTagString = member.Name
|
||||
minimum, maximum *float64
|
||||
exclusiveMinimum, exclusiveMaximum bool
|
||||
example, defaultValue any
|
||||
enum []any
|
||||
)
|
||||
jsonTag, _ := tag.Get(tagJson)
|
||||
if jsonTag != nil {
|
||||
jsonTagString = jsonTag.Name
|
||||
minimum, maximum, exclusiveMinimum, exclusiveMaximum = rangeValueFromOptions(jsonTag.Options)
|
||||
example = exampleValueFromOptions(jsonTag.Options, member.Type)
|
||||
defaultValue = defValueFromOptions(jsonTag.Options, member.Type)
|
||||
enum = enumsValueFromOptions(jsonTag.Options)
|
||||
}
|
||||
|
||||
if required {
|
||||
requiredFields = append(requiredFields, jsonTagString)
|
||||
}
|
||||
p, r := propertiesFromType(member.Type)
|
||||
properties[jsonTagString] = spec.Schema{
|
||||
SwaggerSchemaProps: spec.SwaggerSchemaProps{
|
||||
Example: example,
|
||||
},
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: formatComment(member.Comment),
|
||||
Type: typeFromGoType(member.Type),
|
||||
Default: defaultValue,
|
||||
Maximum: maximum,
|
||||
ExclusiveMaximum: exclusiveMaximum,
|
||||
Minimum: minimum,
|
||||
ExclusiveMinimum: exclusiveMinimum,
|
||||
Enum: enum,
|
||||
Items: itemsFromGoType(member.Type),
|
||||
Properties: p,
|
||||
Required: r,
|
||||
AdditionalProperties: mapFromGoType(member.Type),
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return properties, requiredFields
|
||||
}
|
||||
28
tools/goctl/api/swagger/response.go
Normal file
28
tools/goctl/api/swagger/response.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package swagger
|
||||
|
||||
import (
|
||||
"github.com/go-openapi/spec"
|
||||
apiSpec "github.com/zeromicro/go-zero/tools/goctl/api/spec"
|
||||
)
|
||||
|
||||
func jsonResponseFromType(info apiSpec.Info, tp apiSpec.Type) *spec.Responses {
|
||||
p, _ := propertiesFromType(tp)
|
||||
props := spec.SchemaProps{
|
||||
Type: typeFromGoType(tp),
|
||||
Properties: p,
|
||||
AdditionalProperties: mapFromGoType(tp),
|
||||
Items: itemsFromGoType(tp),
|
||||
}
|
||||
|
||||
return &spec.Responses{
|
||||
ResponsesProps: spec.ResponsesProps{
|
||||
Default: &spec.Response{
|
||||
ResponseProps: spec.ResponseProps{
|
||||
Schema: &spec.Schema{
|
||||
SchemaProps: wrapCodeMsgProps(props, info),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
326
tools/goctl/api/swagger/swagger.go
Normal file
326
tools/goctl/api/swagger/swagger.go
Normal file
@@ -0,0 +1,326 @@
|
||||
package swagger
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-openapi/spec"
|
||||
apiSpec "github.com/zeromicro/go-zero/tools/goctl/api/spec"
|
||||
"github.com/zeromicro/go-zero/tools/goctl/internal/version"
|
||||
"github.com/zeromicro/go-zero/tools/goctl/util"
|
||||
)
|
||||
|
||||
func spec2Swagger(api *apiSpec.ApiSpec) (*spec.Swagger, error) {
|
||||
extensions, info := specExtensions(api.Info)
|
||||
swagger := &spec.Swagger{
|
||||
VendorExtensible: spec.VendorExtensible{
|
||||
Extensions: extensions,
|
||||
},
|
||||
SwaggerProps: spec.SwaggerProps{
|
||||
Consumes: getListFromInfoOrDefault(api.Info.Properties, "consumes", []string{applicationJson}),
|
||||
Produces: getListFromInfoOrDefault(api.Info.Properties, "produces", []string{applicationJson}),
|
||||
Schemes: getListFromInfoOrDefault(api.Info.Properties, "schemes", []string{schemeHttps}),
|
||||
Swagger: swaggerVersion,
|
||||
Info: info,
|
||||
Host: getStringFromKVOrDefault(api.Info.Properties, "host", defaultHost),
|
||||
BasePath: getStringFromKVOrDefault(api.Info.Properties, "basePath", defaultBasePath),
|
||||
Paths: spec2Paths(api.Info, api.Service),
|
||||
},
|
||||
}
|
||||
|
||||
return swagger, nil
|
||||
}
|
||||
|
||||
func formatComment(comment string) string {
|
||||
s := strings.TrimPrefix(comment, "//")
|
||||
return strings.TrimSpace(s)
|
||||
}
|
||||
|
||||
func sampleItemsFromGoType(tp apiSpec.Type) *spec.Items {
|
||||
val, ok := tp.(apiSpec.ArrayType)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
item := val.Value
|
||||
switch item.(type) {
|
||||
case apiSpec.PrimitiveType:
|
||||
return &spec.Items{
|
||||
SimpleSchema: spec.SimpleSchema{
|
||||
Type: sampleTypeFromGoType(item),
|
||||
},
|
||||
}
|
||||
case apiSpec.ArrayType:
|
||||
return &spec.Items{
|
||||
SimpleSchema: spec.SimpleSchema{
|
||||
Type: sampleTypeFromGoType(item),
|
||||
Items: sampleItemsFromGoType(item),
|
||||
},
|
||||
}
|
||||
default: // unsupported type
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// itemsFromGoType returns the schema or array of the type, just for non json body parameters.
|
||||
func itemsFromGoType(tp apiSpec.Type) *spec.SchemaOrArray {
|
||||
array, ok := tp.(apiSpec.ArrayType)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return itemFromGoType(array)
|
||||
}
|
||||
|
||||
func mapFromGoType(tp apiSpec.Type) *spec.SchemaOrBool {
|
||||
mapType, ok := tp.(apiSpec.MapType)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
p, r := propertiesFromType(mapType.Value)
|
||||
return &spec.SchemaOrBool{
|
||||
Allows: true,
|
||||
Schema: &spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: typeFromGoType(mapType.Value),
|
||||
Items: itemsFromGoType(mapType.Value),
|
||||
Properties: p,
|
||||
Required: r,
|
||||
AdditionalProperties: mapFromGoType(mapType.Value),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// itemFromGoType returns the schema or array of the type, just for non json body parameters.
|
||||
func itemFromGoType(tp apiSpec.Type) *spec.SchemaOrArray {
|
||||
switch itemType := tp.(type) {
|
||||
case apiSpec.PrimitiveType:
|
||||
return &spec.SchemaOrArray{
|
||||
Schema: &spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: typeFromGoType(tp),
|
||||
},
|
||||
},
|
||||
}
|
||||
case apiSpec.DefineStruct:
|
||||
var (
|
||||
properties = map[string]spec.Schema{}
|
||||
requiredFields []string
|
||||
)
|
||||
rangeMemberAndDo(itemType, func(tag *apiSpec.Tags, required bool, member apiSpec.Member) {
|
||||
jsonTag, _ := tag.Get(tagJson)
|
||||
if jsonTag == nil {
|
||||
return
|
||||
}
|
||||
minimum, maximum, exclusiveMinimum, exclusiveMaximum := rangeValueFromOptions(jsonTag.Options)
|
||||
if required {
|
||||
requiredFields = append(requiredFields, jsonTag.Name)
|
||||
}
|
||||
p, r := propertiesFromType(member.Type)
|
||||
properties[jsonTag.Name] = spec.Schema{
|
||||
SwaggerSchemaProps: spec.SwaggerSchemaProps{
|
||||
Example: exampleValueFromOptions(jsonTag.Options, member.Type),
|
||||
},
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: formatComment(member.Comment),
|
||||
Type: typeFromGoType(member.Type),
|
||||
Default: defValueFromOptions(jsonTag.Options, member.Type),
|
||||
Maximum: maximum,
|
||||
ExclusiveMaximum: exclusiveMaximum,
|
||||
Minimum: minimum,
|
||||
ExclusiveMinimum: exclusiveMinimum,
|
||||
Enum: enumsValueFromOptions(jsonTag.Options),
|
||||
Items: itemsFromGoType(member.Type),
|
||||
Properties: p,
|
||||
Required: r,
|
||||
AdditionalProperties: mapFromGoType(member.Type),
|
||||
},
|
||||
}
|
||||
})
|
||||
return &spec.SchemaOrArray{
|
||||
Schema: &spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: typeFromGoType(itemType),
|
||||
Items: itemsFromGoType(itemType),
|
||||
Properties: properties,
|
||||
Required: requiredFields,
|
||||
AdditionalProperties: mapFromGoType(itemType),
|
||||
},
|
||||
},
|
||||
}
|
||||
case apiSpec.PointerType:
|
||||
return itemsFromGoType(itemType.Type)
|
||||
case apiSpec.ArrayType:
|
||||
p, r := propertiesFromType(itemType.Value)
|
||||
return &spec.SchemaOrArray{
|
||||
Schema: &spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: typeFromGoType(itemType.Value),
|
||||
Items: itemsFromGoType(itemType.Value),
|
||||
Properties: p,
|
||||
Required: r,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func typeFromGoType(tp apiSpec.Type) []string {
|
||||
switch val := tp.(type) {
|
||||
case apiSpec.PrimitiveType:
|
||||
res, ok := tpMapper[val.RawName]
|
||||
if ok {
|
||||
return []string{res}
|
||||
}
|
||||
case apiSpec.ArrayType:
|
||||
return []string{swaggerTypeArray}
|
||||
case apiSpec.DefineStruct, apiSpec.MapType:
|
||||
return []string{swaggerTypeObject}
|
||||
case apiSpec.PointerType:
|
||||
return typeFromGoType(val.Type)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func sampleTypeFromGoType(tp apiSpec.Type) string {
|
||||
switch val := tp.(type) {
|
||||
case apiSpec.PrimitiveType:
|
||||
return tpMapper[val.RawName]
|
||||
case apiSpec.ArrayType:
|
||||
return swaggerTypeArray
|
||||
case apiSpec.DefineStruct, apiSpec.MapType, apiSpec.NestedStruct:
|
||||
return swaggerTypeObject
|
||||
case apiSpec.PointerType:
|
||||
return sampleTypeFromGoType(val.Type)
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func typeContainsTag(structType apiSpec.DefineStruct, tag string) bool {
|
||||
for _, field := range structType.Members {
|
||||
tags, _ := apiSpec.Parse(field.Tag)
|
||||
for _, t := range tags.Tags() {
|
||||
if t.Key == tag {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func expandMembers(tp apiSpec.Type) []apiSpec.Member {
|
||||
var members []apiSpec.Member
|
||||
switch val := tp.(type) {
|
||||
case apiSpec.DefineStruct:
|
||||
for _, v := range val.Members {
|
||||
if v.IsInline {
|
||||
members = append(members, expandMembers(v.Type)...)
|
||||
continue
|
||||
}
|
||||
members = append(members, v)
|
||||
}
|
||||
case apiSpec.NestedStruct:
|
||||
for _, v := range val.Members {
|
||||
if v.IsInline {
|
||||
members = append(members, expandMembers(v.Type)...)
|
||||
continue
|
||||
}
|
||||
members = append(members, v)
|
||||
}
|
||||
}
|
||||
|
||||
return members
|
||||
}
|
||||
|
||||
func rangeMemberAndDo(structType apiSpec.Type, do func(tag *apiSpec.Tags, required bool, member apiSpec.Member)) {
|
||||
var members = expandMembers(structType)
|
||||
|
||||
for _, field := range members {
|
||||
var required = false
|
||||
for _, t := range field.Tags() {
|
||||
required = len(t.Options) > 0 && t.Options[0] != "optional"
|
||||
}
|
||||
tags, _ := apiSpec.Parse(field.Tag)
|
||||
do(tags, required, field)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func pathVariable2SwaggerVariable(path string) string {
|
||||
pathItems := strings.FieldsFunc(path, slashRune)
|
||||
var resp []string
|
||||
for _, v := range pathItems {
|
||||
if strings.HasPrefix(v, ":") {
|
||||
resp = append(resp, "{"+v[1:]+"}")
|
||||
} else {
|
||||
resp = append(resp, v)
|
||||
}
|
||||
}
|
||||
return "/" + filepath.Join(resp...)
|
||||
}
|
||||
|
||||
func wrapCodeMsgProps(properties spec.SchemaProps, api apiSpec.Info) spec.SchemaProps {
|
||||
wrapCodeMsg := getBoolFromKVOrDefault(api.Properties, "wrapCodeMsg", false)
|
||||
if !wrapCodeMsg {
|
||||
return properties
|
||||
}
|
||||
return spec.SchemaProps{
|
||||
Type: []string{swaggerTypeObject},
|
||||
Properties: spec.SchemaProperties{
|
||||
"code": {
|
||||
SwaggerSchemaProps: spec.SwaggerSchemaProps{
|
||||
Example: 0,
|
||||
},
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{swaggerTypeInteger},
|
||||
Description: getStringFromKVOrDefault(api.Properties, "bizCodeEnumDescription", "business code"),
|
||||
},
|
||||
},
|
||||
"msg": {
|
||||
SwaggerSchemaProps: spec.SwaggerSchemaProps{
|
||||
Example: "ok",
|
||||
},
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{swaggerTypeString},
|
||||
Description: "business message",
|
||||
},
|
||||
},
|
||||
"data": {
|
||||
SchemaProps: properties,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func specExtensions(api apiSpec.Info) (spec.Extensions, *spec.Info) {
|
||||
ext := spec.Extensions{}
|
||||
ext.Add("x-goctl-version", version.BuildVersion)
|
||||
ext.Add("x-description", "This is a goctl generated swagger file.")
|
||||
ext.Add("x-date", time.Now().Format("2006-01-02 15:04:05"))
|
||||
ext.Add("x-github", "https://github.com/zeromicro/go-zero")
|
||||
ext.Add("x-go-zero-doc", "https://go-zero.dev/")
|
||||
|
||||
info := &spec.Info{}
|
||||
info.Description = util.Unquote(api.Properties["description"])
|
||||
info.Title = util.Unquote(api.Properties["title"])
|
||||
info.TermsOfService = util.Unquote(api.Properties["termsOfService"])
|
||||
info.Version = util.Unquote(api.Properties["version"])
|
||||
|
||||
contactInfo := spec.ContactInfo{}
|
||||
contactInfo.Name = util.Unquote(api.Properties["contactName"])
|
||||
contactInfo.URL = util.Unquote(api.Properties["contactURL"])
|
||||
contactInfo.Email = util.Unquote(api.Properties["contactEmail"])
|
||||
if len(contactInfo.Name) > 0 || len(contactInfo.URL) > 0 || len(contactInfo.Email) > 0 {
|
||||
info.Contact = &contactInfo
|
||||
}
|
||||
|
||||
license := &spec.License{}
|
||||
license.Name = util.Unquote(api.Properties["licenseName"])
|
||||
license.URL = util.Unquote(api.Properties["licenseURL"])
|
||||
if len(license.Name) > 0 || len(license.URL) > 0 {
|
||||
info.License = license
|
||||
}
|
||||
return ext, info
|
||||
}
|
||||
25
tools/goctl/api/swagger/swagger_test.go
Normal file
25
tools/goctl/api/swagger/swagger_test.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package swagger
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_pathVariable2SwaggerVariable(t *testing.T) {
|
||||
testCases := []struct {
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{input: "/api/:id", expected: "/api/{id}"},
|
||||
{input: "/api/:id/details", expected: "/api/{id}/details"},
|
||||
{input: "/:version/api/:id", expected: "/{version}/api/{id}"},
|
||||
{input: "/api/v1", expected: "/api/v1"},
|
||||
{input: "/api/:id/:action", expected: "/api/{id}/{action}"},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
result := pathVariable2SwaggerVariable(tc.input)
|
||||
assert.Equal(t, tc.expected, result)
|
||||
}
|
||||
}
|
||||
27
tools/goctl/api/swagger/vars.go
Normal file
27
tools/goctl/api/swagger/vars.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package swagger
|
||||
|
||||
var (
|
||||
tpMapper = map[string]string{
|
||||
"uint8": swaggerTypeInteger,
|
||||
"uint16": swaggerTypeInteger,
|
||||
"uint32": swaggerTypeInteger,
|
||||
"uint64": swaggerTypeInteger,
|
||||
"int8": swaggerTypeInteger,
|
||||
"int16": swaggerTypeInteger,
|
||||
"int32": swaggerTypeInteger,
|
||||
"int64": swaggerTypeInteger,
|
||||
"int": swaggerTypeInteger,
|
||||
"uint": swaggerTypeInteger,
|
||||
"byte": swaggerTypeInteger,
|
||||
"float32": swaggerTypeNumber,
|
||||
"float64": swaggerTypeNumber,
|
||||
"string": swaggerTypeString,
|
||||
"bool": swaggerTypeBoolean,
|
||||
}
|
||||
commaRune = func(r rune) bool {
|
||||
return r == ','
|
||||
}
|
||||
slashRune = func(r rune) bool {
|
||||
return r == '/'
|
||||
}
|
||||
)
|
||||
@@ -6,6 +6,7 @@ require (
|
||||
github.com/DATA-DOG/go-sqlmock v1.5.2
|
||||
github.com/emicklei/proto v1.14.0
|
||||
github.com/fatih/structtag v1.2.0
|
||||
github.com/go-openapi/spec v0.21.1-0.20250328170532-a3928469592e
|
||||
github.com/go-sql-driver/mysql v1.9.0
|
||||
github.com/gookit/color v1.5.4
|
||||
github.com/iancoleman/strcase v0.3.0
|
||||
@@ -38,9 +39,9 @@ require (
|
||||
github.com/fatih/color v1.18.0 // indirect
|
||||
github.com/go-logr/logr v1.4.2 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.19.6 // indirect
|
||||
github.com/go-openapi/jsonreference v0.20.2 // indirect
|
||||
github.com/go-openapi/swag v0.22.4 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.21.1 // indirect
|
||||
github.com/go-openapi/jsonreference v0.21.0 // indirect
|
||||
github.com/go-openapi/swag v0.23.1 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang/mock v1.6.0 // indirect
|
||||
github.com/golang/protobuf v1.5.4 // indirect
|
||||
@@ -58,7 +59,7 @@ require (
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/compress v1.17.11 // indirect
|
||||
github.com/logrusorgru/aurora v2.0.3+incompatible // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/mailru/easyjson v0.9.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
|
||||
@@ -25,7 +25,6 @@ github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03V
|
||||
github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
@@ -45,13 +44,14 @@ github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE=
|
||||
github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs=
|
||||
github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE=
|
||||
github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k=
|
||||
github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
|
||||
github.com/go-openapi/swag v0.22.4 h1:QLMzNJnMGPRNDCbySlcj1x01tzU8/9LTTL9hZZZogBU=
|
||||
github.com/go-openapi/swag v0.22.4/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
|
||||
github.com/go-openapi/jsonpointer v0.21.1 h1:whnzv/pNXtK2FbX/W9yJfRmE2gsmkfahjMKB0fZvcic=
|
||||
github.com/go-openapi/jsonpointer v0.21.1/go.mod h1:50I1STOfbY1ycR8jGz8DaMeLCdXiI6aDteEdRNNzpdk=
|
||||
github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ=
|
||||
github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4=
|
||||
github.com/go-openapi/spec v0.21.1-0.20250328170532-a3928469592e h1:auobAirzhPsLHMso0NVMqK0QunuLDYCK83KnaVUM/RU=
|
||||
github.com/go-openapi/spec v0.21.1-0.20250328170532-a3928469592e/go.mod h1:NAKTe9SplQBxIUlHlsuId1jk1I7bWTVV/2q/GtdRi6g=
|
||||
github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU=
|
||||
github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0=
|
||||
github.com/go-sql-driver/mysql v1.9.0 h1:Y0zIbQXhQKmQgTp44Y1dp3wTXcn804QoTptLZT1vtvo=
|
||||
github.com/go-sql-driver/mysql v1.9.0/go.mod h1:pDetrLJeA3oMujJuvXc8RJoasr589B6A9fwzD3QMrqw=
|
||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
|
||||
@@ -102,19 +102,16 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o
|
||||
github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46/go.mod h1:yyMNCyc/Ib3bDTKd379tNMpB/7/H5TjM2Y9QJ5THLbE=
|
||||
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
|
||||
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/logrusorgru/aurora v2.0.3+incompatible h1:tOpm7WcpBTn4fjmVfgpQq0EfczGlG91VSDkswnjF5A8=
|
||||
github.com/logrusorgru/aurora v2.0.3+incompatible/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
|
||||
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=
|
||||
github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
@@ -151,8 +148,8 @@ github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0leargg
|
||||
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
|
||||
github.com/redis/go-redis/v9 v9.7.3 h1:YpPyAayJV+XErNsatSElgRZZVCwXX9QzkKYNvO7x0wM=
|
||||
github.com/redis/go-redis/v9 v9.7.3/go.mod h1:bGUrSggJ9X9GUmZpZNEOQKaANxSGgOEBRltRTZHSvrA=
|
||||
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
||||
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
||||
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
|
||||
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
|
||||
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||
@@ -169,7 +166,6 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
|
||||
@@ -71,6 +71,12 @@
|
||||
"api": "{{.goctl.api.api}}",
|
||||
"caller": "The web api caller",
|
||||
"unwrap": "Unwrap the webapi caller for import"
|
||||
},
|
||||
"swagger": {
|
||||
"short": "Generate swagger file from api",
|
||||
"dir": "{{.goctl.api.dir}}",
|
||||
"api": "{{.goctl.api.api}}",
|
||||
"yaml": "Generate swagger yaml file, default to json"
|
||||
}
|
||||
},
|
||||
"bug": {
|
||||
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
)
|
||||
|
||||
// BuildVersion is the version of goctl.
|
||||
const BuildVersion = "1.8.2"
|
||||
const BuildVersion = "1.8.3"
|
||||
|
||||
var tag = map[string]int{"pre-alpha": 0, "alpha": 1, "pre-bata": 2, "beta": 3, "released": 4, "": 5}
|
||||
|
||||
|
||||
@@ -1342,7 +1342,7 @@ func (p *Parser) parseKVExpression() *ast.KVExpr {
|
||||
expr.Colon = p.curTokenNode()
|
||||
|
||||
// token STRING
|
||||
if !p.advanceIfPeekTokenIs(token.STRING) {
|
||||
if !p.advanceIfPeekTokenIs(token.STRING, token.RAW_STRING) {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -121,3 +121,34 @@ func IsEmptyStringOrWhiteSpace(s string) bool {
|
||||
v := TrimWhiteSpace(s)
|
||||
return len(v) == 0
|
||||
}
|
||||
|
||||
func FieldsAndTrimSpace(s string, f func(r rune) bool) []string {
|
||||
fields := strings.FieldsFunc(s, f)
|
||||
var resp []string
|
||||
for _, v := range fields {
|
||||
val := TrimWhiteSpace(v)
|
||||
if len(val) > 0 {
|
||||
resp = append(resp, v)
|
||||
}
|
||||
}
|
||||
return resp
|
||||
}
|
||||
|
||||
func Unquote(s string) string {
|
||||
if len(s) == 0 {
|
||||
return s
|
||||
}
|
||||
left := s[0]
|
||||
|
||||
if left == '`' || left == '"' {
|
||||
s = s[1:len(s)]
|
||||
}
|
||||
if len(s) == 0 {
|
||||
return s
|
||||
}
|
||||
right := s[len(s)-1]
|
||||
if right == '`' || right == '"' {
|
||||
s = s[0 : len(s)-1]
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package util
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
"unicode"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
@@ -72,3 +73,67 @@ func TestEscapeGoKeyword(t *testing.T) {
|
||||
assert.False(t, isGolangKeyword(strings.Title(k)))
|
||||
}
|
||||
}
|
||||
|
||||
func TestFieldsAndTrimSpace(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
input string
|
||||
delimiter func(r rune) bool
|
||||
expected []string
|
||||
}{
|
||||
{
|
||||
name: "Comma-separated values",
|
||||
input: "a, b, c",
|
||||
delimiter: func(r rune) bool { return r == ',' },
|
||||
expected: []string{"a", " b", " c"},
|
||||
},
|
||||
{
|
||||
name: "Space-separated values",
|
||||
input: "a b c",
|
||||
delimiter: unicode.IsSpace,
|
||||
expected: []string{"a", "b", "c"},
|
||||
},
|
||||
{
|
||||
name: "Mixed whitespace",
|
||||
input: "a\tb\nc",
|
||||
delimiter: unicode.IsSpace,
|
||||
expected: []string{"a", "b", "c"},
|
||||
},
|
||||
{
|
||||
name: "Empty input",
|
||||
input: "",
|
||||
delimiter: unicode.IsSpace,
|
||||
expected: []string(nil),
|
||||
},
|
||||
{
|
||||
name: "Trailing and leading spaces",
|
||||
input: " a , b , c ",
|
||||
delimiter: func(r rune) bool { return r == ',' },
|
||||
expected: []string{" a ", " b ", " c "},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
result := FieldsAndTrimSpace(tc.input, tc.delimiter)
|
||||
assert.Equal(t, tc.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnquote(t *testing.T) {
|
||||
testCases := []struct {
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{input: `"hello"`, expected: `hello`},
|
||||
{input: "`world`", expected: `world`},
|
||||
{input: `"foo'bar"`, expected: `foo'bar`},
|
||||
{input: "", expected: ""},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
result := Unquote(tc.input)
|
||||
assert.Equal(t, tc.expected, result)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user