feature/goctl-api-swagger (#4780)

This commit is contained in:
kesonan
2025-04-17 22:38:55 +08:00
committed by GitHub
parent 801c283478
commit 9c478626d2
38 changed files with 2445 additions and 23 deletions

View File

@@ -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)
}
}

View File

@@ -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

View 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
}

View 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"}))
}

View 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
}
}

View 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)
}

View 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 = "/"
)

View 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}
}

View 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)
})
}
}

View File

@@ -0,0 +1,4 @@
*.json
*.yaml
bin
output

View 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)
}

View 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)
}

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 MiB

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

View 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

View 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
}

View 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
}

View 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
}

View 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
}

View 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
}

View 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),
},
},
},
},
}
}

View 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
}

View 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)
}
}

View 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 == '/'
}
)