2025-09-30 08:16:44 +00:00
|
|
|
package swagger
|
|
|
|
|
|
|
|
import (
|
|
|
|
"encoding/json"
|
|
|
|
"strings"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/go-openapi/spec"
|
|
|
|
apiSpec "github.com/zeromicro/go-zero/tools/goctl/api/spec"
|
|
|
|
)
|
|
|
|
|
|
|
|
func spec2Swagger(api *apiSpec.ApiSpec) (*spec.Swagger, error) {
|
2025-09-30 09:33:29 +00:00
|
|
|
ctx := contextFromApi(api)
|
2025-09-30 08:16:44 +00:00
|
|
|
extensions, info := specExtensions(api.Info)
|
|
|
|
var securityDefinitions spec.SecurityDefinitions
|
|
|
|
securityDefinitionsFromJson := getStringFromKVOrDefault(api.Info.Properties, "securityDefinitionsFromJson", `{}`)
|
|
|
|
_ = json.Unmarshal([]byte(securityDefinitionsFromJson), &securityDefinitions)
|
|
|
|
swagger := &spec.Swagger{
|
|
|
|
VendorExtensible: spec.VendorExtensible{
|
|
|
|
Extensions: extensions,
|
|
|
|
},
|
|
|
|
SwaggerProps: spec.SwaggerProps{
|
|
|
|
Definitions: definitionsFromTypes(ctx, api.Types),
|
|
|
|
Consumes: getListFromInfoOrDefault(api.Info.Properties, propertyKeyConsumes, []string{applicationJson}),
|
|
|
|
Produces: getListFromInfoOrDefault(api.Info.Properties, propertyKeyProduces, []string{applicationJson}),
|
|
|
|
Schemes: getListFromInfoOrDefault(api.Info.Properties, propertyKeySchemes, []string{schemeHttps}),
|
|
|
|
Swagger: swaggerVersion,
|
|
|
|
Info: info,
|
|
|
|
Host: getStringFromKVOrDefault(api.Info.Properties, propertyKeyHost, ""),
|
|
|
|
BasePath: getStringFromKVOrDefault(api.Info.Properties, propertyKeyBasePath, defaultBasePath),
|
|
|
|
Paths: spec2Paths(ctx, api.Service),
|
|
|
|
SecurityDefinitions: securityDefinitions,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
return swagger, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func formatComment(comment string) string {
|
|
|
|
s := strings.TrimPrefix(comment, "//")
|
|
|
|
return strings.TrimSpace(s)
|
|
|
|
}
|
|
|
|
|
|
|
|
func sampleItemsFromGoType(ctx Context, 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(ctx, item),
|
|
|
|
},
|
|
|
|
}
|
|
|
|
case apiSpec.ArrayType:
|
|
|
|
return &spec.Items{
|
|
|
|
SimpleSchema: spec.SimpleSchema{
|
|
|
|
Type: sampleTypeFromGoType(ctx, item),
|
|
|
|
Items: sampleItemsFromGoType(ctx, item),
|
|
|
|
},
|
|
|
|
}
|
|
|
|
default: // unsupported type
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// itemsFromGoType returns the schema or array of the type, just for non json body parameters.
|
|
|
|
func itemsFromGoType(ctx Context, tp apiSpec.Type) *spec.SchemaOrArray {
|
|
|
|
array, ok := tp.(apiSpec.ArrayType)
|
|
|
|
if !ok {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
return itemFromGoType(ctx, array.Value)
|
|
|
|
}
|
|
|
|
|
|
|
|
func mapFromGoType(ctx Context, tp apiSpec.Type) *spec.SchemaOrBool {
|
|
|
|
mapType, ok := tp.(apiSpec.MapType)
|
|
|
|
if !ok {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
var schema = &spec.Schema{
|
|
|
|
SchemaProps: spec.SchemaProps{
|
|
|
|
Type: typeFromGoType(ctx, mapType.Value),
|
|
|
|
AdditionalProperties: mapFromGoType(ctx, mapType.Value),
|
|
|
|
},
|
|
|
|
}
|
|
|
|
switch sampleTypeFromGoType(ctx, mapType.Value) {
|
|
|
|
case swaggerTypeArray:
|
|
|
|
schema.Items = itemsFromGoType(ctx, mapType.Value)
|
|
|
|
case swaggerTypeObject:
|
|
|
|
p, r := propertiesFromType(ctx, mapType.Value)
|
|
|
|
schema.Properties = p
|
|
|
|
schema.Required = r
|
|
|
|
}
|
|
|
|
return &spec.SchemaOrBool{
|
|
|
|
Allows: true,
|
|
|
|
Schema: schema,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// itemFromGoType returns the schema or array of the type, just for non json body parameters.
|
|
|
|
func itemFromGoType(ctx Context, tp apiSpec.Type) *spec.SchemaOrArray {
|
|
|
|
switch itemType := tp.(type) {
|
|
|
|
case apiSpec.PrimitiveType:
|
|
|
|
return &spec.SchemaOrArray{
|
|
|
|
Schema: &spec.Schema{
|
|
|
|
SchemaProps: spec.SchemaProps{
|
|
|
|
Type: typeFromGoType(ctx, tp),
|
|
|
|
},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
case apiSpec.DefineStruct, apiSpec.NestedStruct, apiSpec.MapType:
|
|
|
|
properties, requiredFields := propertiesFromType(ctx, itemType)
|
|
|
|
return &spec.SchemaOrArray{
|
|
|
|
Schema: &spec.Schema{
|
|
|
|
SchemaProps: spec.SchemaProps{
|
|
|
|
Type: typeFromGoType(ctx, itemType),
|
|
|
|
Items: itemsFromGoType(ctx, itemType),
|
|
|
|
Properties: properties,
|
|
|
|
Required: requiredFields,
|
|
|
|
AdditionalProperties: mapFromGoType(ctx, itemType),
|
|
|
|
},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
case apiSpec.PointerType:
|
|
|
|
return itemFromGoType(ctx, itemType.Type)
|
|
|
|
case apiSpec.ArrayType:
|
|
|
|
return &spec.SchemaOrArray{
|
|
|
|
Schema: &spec.Schema{
|
|
|
|
SchemaProps: spec.SchemaProps{
|
|
|
|
Type: typeFromGoType(ctx, itemType),
|
|
|
|
Items: itemsFromGoType(ctx, itemType),
|
|
|
|
},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func typeFromGoType(ctx Context, 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(ctx, val.Type)
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func sampleTypeFromGoType(ctx Context, 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(ctx, val.Type)
|
|
|
|
default:
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func typeContainsTag(ctx Context, structType apiSpec.DefineStruct, tag string) bool {
|
|
|
|
members := expandMembers(ctx, structType)
|
|
|
|
for _, member := range members {
|
|
|
|
tags, _ := apiSpec.Parse(member.Tag)
|
|
|
|
if _, err := tags.Get(tag); err == nil {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
func expandMembers(ctx Context, 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(ctx, v.Type)...)
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
members = append(members, v)
|
|
|
|
}
|
|
|
|
case apiSpec.NestedStruct:
|
|
|
|
for _, v := range val.Members {
|
|
|
|
if v.IsInline {
|
|
|
|
members = append(members, expandMembers(ctx, v.Type)...)
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
members = append(members, v)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return members
|
|
|
|
}
|
|
|
|
|
|
|
|
func rangeMemberAndDo(ctx Context, structType apiSpec.Type, do func(tag *apiSpec.Tags, required bool, member apiSpec.Member)) {
|
|
|
|
var members = expandMembers(ctx, structType)
|
|
|
|
|
|
|
|
for _, field := range members {
|
|
|
|
tags, _ := apiSpec.Parse(field.Tag)
|
2025-10-01 09:44:41 +00:00
|
|
|
required := isRequired(ctx, tags, field)
|
2025-09-30 08:16:44 +00:00
|
|
|
do(tags, required, field)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-10-01 09:44:41 +00:00
|
|
|
func isRequired(ctx Context, tags *apiSpec.Tags, member apiSpec.Member) bool {
|
|
|
|
// Check if the field type is a pointer - pointer types are optional
|
|
|
|
if _, isPointer := member.Type.(apiSpec.PointerType); isPointer {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
2025-09-30 08:16:44 +00:00
|
|
|
tag, err := tags.Get(tagJson)
|
|
|
|
if err == nil {
|
|
|
|
return !isOptional(ctx, tag.Options)
|
|
|
|
}
|
|
|
|
tag, err = tags.Get(tagForm)
|
|
|
|
if err == nil {
|
|
|
|
return !isOptional(ctx, tag.Options)
|
|
|
|
}
|
|
|
|
tag, err = tags.Get(tagPath)
|
|
|
|
if err == nil {
|
|
|
|
return !isOptional(ctx, tag.Options)
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
func isOptional(_ Context, options []string) bool {
|
|
|
|
for _, option := range options {
|
2025-10-01 09:44:41 +00:00
|
|
|
if option == optionalFlag || option == "omitempty" {
|
2025-09-30 08:16:44 +00:00
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
func pathVariable2SwaggerVariable(_ Context, path string) string {
|
|
|
|
pathItems := strings.FieldsFunc(path, slashRune)
|
|
|
|
resp := make([]string, 0, len(pathItems))
|
|
|
|
for _, v := range pathItems {
|
|
|
|
if strings.HasPrefix(v, ":") {
|
|
|
|
resp = append(resp, "{"+v[1:]+"}")
|
|
|
|
} else {
|
|
|
|
resp = append(resp, v)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return "/" + strings.Join(resp, "/")
|
|
|
|
}
|
|
|
|
|
|
|
|
func wrapCodeMsgProps(ctx Context, properties spec.SchemaProps, atDoc apiSpec.AtDoc) spec.SchemaProps {
|
|
|
|
if !ctx.WrapCodeMsg {
|
|
|
|
return properties
|
|
|
|
}
|
|
|
|
globalCodeDesc := ctx.BizCodeEnumDescription
|
|
|
|
methodCodeDesc := getStringFromKVOrDefault(atDoc.Properties, propertyKeyBizCodeEnumDescription, globalCodeDesc)
|
|
|
|
return spec.SchemaProps{
|
|
|
|
Type: []string{swaggerTypeObject},
|
|
|
|
Properties: spec.SchemaProperties{
|
|
|
|
"code": {
|
|
|
|
SwaggerSchemaProps: spec.SwaggerSchemaProps{
|
|
|
|
Example: 0,
|
|
|
|
},
|
|
|
|
SchemaProps: spec.SchemaProps{
|
|
|
|
Type: []string{swaggerTypeInteger},
|
|
|
|
Description: methodCodeDesc,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
"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-generator", "go-doc")
|
|
|
|
ext.Add("x-description", "This is a go-doc generated swagger file.")
|
|
|
|
ext.Add("x-date", time.Now().Format(time.DateTime))
|
|
|
|
ext.Add("x-github", "https://github.com/danielchan-25/go-doc")
|
|
|
|
ext.Add("x-source", "go-zero API specification")
|
|
|
|
|
|
|
|
info := &spec.Info{}
|
|
|
|
info.Title = getStringFromKVOrDefault(api.Properties, propertyKeyTitle, "")
|
|
|
|
info.Description = getStringFromKVOrDefault(api.Properties, propertyKeyDescription, "")
|
|
|
|
info.TermsOfService = getStringFromKVOrDefault(api.Properties, propertyKeyTermsOfService, "")
|
|
|
|
info.Version = getStringFromKVOrDefault(api.Properties, propertyKeyVersion, "1.0")
|
|
|
|
|
|
|
|
contactInfo := spec.ContactInfo{}
|
|
|
|
contactInfo.Name = getStringFromKVOrDefault(api.Properties, propertyKeyContactName, "")
|
|
|
|
contactInfo.URL = getStringFromKVOrDefault(api.Properties, propertyKeyContactURL, "")
|
|
|
|
contactInfo.Email = getStringFromKVOrDefault(api.Properties, propertyKeyContactEmail, "")
|
|
|
|
if len(contactInfo.Name) > 0 || len(contactInfo.URL) > 0 || len(contactInfo.Email) > 0 {
|
|
|
|
info.Contact = &contactInfo
|
|
|
|
}
|
|
|
|
|
|
|
|
license := &spec.License{}
|
|
|
|
license.Name = getStringFromKVOrDefault(api.Properties, propertyKeyLicenseName, "")
|
|
|
|
license.URL = getStringFromKVOrDefault(api.Properties, propertyKeyLicenseURL, "")
|
|
|
|
if len(license.Name) > 0 || len(license.URL) > 0 {
|
|
|
|
info.License = license
|
|
|
|
}
|
|
|
|
return ext, info
|
|
|
|
}
|