niki/vendor/github.com/swaggo/swag/v2/parserv3.go

1088 lines
29 KiB
Go

package swag
import (
"encoding/json"
"fmt"
"go/ast"
"go/token"
"net/http"
"reflect"
"regexp"
"sort"
"strings"
"github.com/pkg/errors"
"github.com/sv-tools/openapi/spec"
)
// FieldParserFactoryV3 create FieldParser.
type FieldParserFactoryV3 func(ps *Parser, file *ast.File, field *ast.Field) FieldParserV3
// FieldParserV3 parse struct field.
type FieldParserV3 interface {
ShouldSkip() bool
FieldName() (string, error)
FormName() string
CustomSchema() (*spec.RefOrSpec[spec.Schema], error)
ComplementSchema(schema *spec.RefOrSpec[spec.Schema]) error
IsRequired() (bool, error)
}
// GetOpenAPI returns *spec.OpenAPI which is the root document object for the API specification.
func (p *Parser) GetOpenAPI() *spec.OpenAPI {
return p.openAPI
}
var (
serversURLPattern = regexp.MustCompile(`\{([^}]+)\}`)
serversVariablesPattern = regexp.MustCompile(`^(\w+)\s+(.+)$`)
)
func (p *Parser) parseGeneralAPIInfoV3(comments []string) error {
previousAttribute := ""
// parsing classic meta data model
for line := 0; line < len(comments); line++ {
commentLine := comments[line]
commentLine = strings.TrimSpace(commentLine)
if len(commentLine) == 0 {
continue
}
fields := FieldsByAnySpace(commentLine, 2)
attribute := fields[0]
var value string
if len(fields) > 1 {
value = fields[1]
}
switch attr := strings.ToLower(attribute); attr {
case versionAttr, titleAttr, tosAttr, licNameAttr, licURLAttr, conNameAttr, conURLAttr, conEmailAttr:
setspecInfo(p.openAPI, attr, value)
case descriptionAttr:
if previousAttribute == attribute {
p.openAPI.Info.Spec.Description += "\n" + value
continue
}
setspecInfo(p.openAPI, attr, value)
case descriptionMarkdownAttr:
commentInfo, err := getMarkdownForTag("api", p.markdownFileDir)
if err != nil {
return err
}
setspecInfo(p.openAPI, attr, string(commentInfo))
case "@host":
if len(p.openAPI.Servers) == 0 {
server := spec.NewServer()
server.Spec.URL = value
p.openAPI.Servers = append(p.openAPI.Servers, server)
}
println("@host is deprecated use servers instead")
case "@basepath":
if len(p.openAPI.Servers) == 0 {
server := spec.NewServer()
p.openAPI.Servers = append(p.openAPI.Servers, server)
}
p.openAPI.Servers[0].Spec.URL += value
println("@basepath is deprecated use servers instead")
case acceptAttr:
println("acceptAttribute is deprecated, as there is no such field on top level in spec V3.1")
case produceAttr:
println("produce is deprecated, as there is no such field on top level in spec V3.1")
case "@schemes":
println("@schemes is deprecated use servers instead")
case "@tag.name":
tag := &spec.Extendable[spec.Tag]{
Spec: &spec.Tag{
Name: value,
},
}
p.openAPI.Tags = append(p.openAPI.Tags, tag)
case "@tag.description":
tag := p.openAPI.Tags[len(p.openAPI.Tags)-1]
tag.Spec.Description = value
case "@tag.description.markdown":
tag := p.openAPI.Tags[len(p.openAPI.Tags)-1]
commentInfo, err := getMarkdownForTag(tag.Spec.Name, p.markdownFileDir)
if err != nil {
return err
}
tag.Spec.Description = string(commentInfo)
case "@tag.docs.url":
tag := p.openAPI.Tags[len(p.openAPI.Tags)-1]
tag.Spec.ExternalDocs = spec.NewExternalDocs()
tag.Spec.ExternalDocs.Spec.URL = value
case "@tag.docs.description":
tag := p.openAPI.Tags[len(p.openAPI.Tags)-1]
if tag.Spec.ExternalDocs == nil {
return fmt.Errorf("%s needs to come after a @tags.docs.url", attribute)
}
tag.Spec.ExternalDocs.Spec.Description = value
case secBasicAttr, secAPIKeyAttr, secApplicationAttr, secImplicitAttr, secPasswordAttr, secAccessCodeAttr, secBearerAuthAttr:
key, scheme, err := parseSecAttributesV3(attribute, comments, &line)
if err != nil {
return err
}
schemeSpec := spec.NewSecuritySchemeSpec()
schemeSpec.Spec.Spec = scheme
if p.openAPI.Components.Spec.SecuritySchemes == nil {
p.openAPI.Components.Spec.SecuritySchemes = make(map[string]*spec.RefOrSpec[spec.Extendable[spec.SecurityScheme]])
}
p.openAPI.Components.Spec.SecuritySchemes[key] = schemeSpec
case "@query.collection.format":
p.collectionFormatInQuery = TransToValidCollectionFormat(value)
case extDocsDescAttr, extDocsURLAttr:
if p.openAPI.ExternalDocs == nil {
p.openAPI.ExternalDocs = spec.NewExternalDocs()
}
switch attr {
case extDocsDescAttr:
p.openAPI.ExternalDocs.Spec.Description = value
case extDocsURLAttr:
p.openAPI.ExternalDocs.Spec.URL = value
}
case "@x-taggroups":
originalAttribute := strings.Split(commentLine, " ")[0]
if len(value) == 0 {
return fmt.Errorf("annotation %s need a value", attribute)
}
var valueJSON interface{}
if err := json.Unmarshal([]byte(value), &valueJSON); err != nil {
return fmt.Errorf("annotation %s need a valid json value. error: %s", originalAttribute, err.Error())
}
p.openAPI.Info.Extensions[originalAttribute[1:]] = valueJSON
case "@servers.url":
server := spec.NewServer()
server.Spec.URL = value
matches := serversURLPattern.FindAllStringSubmatch(value, -1)
server.Spec.Variables = make(map[string]*spec.Extendable[spec.ServerVariable])
for _, match := range matches {
server.Spec.Variables[match[1]] = spec.NewServerVariable()
}
p.openAPI.Servers = append(p.openAPI.Servers, server)
case "@servers.description":
server := p.openAPI.Servers[len(p.openAPI.Servers)-1]
server.Spec.Description = value
case "@servers.variables.enum":
server := p.openAPI.Servers[len(p.openAPI.Servers)-1]
matches := serversVariablesPattern.FindStringSubmatch(value)
if len(matches) > 0 {
variable, ok := server.Spec.Variables[matches[1]]
if !ok {
p.debug.Printf("Variables are not detected.")
continue
}
variable.Spec.Enum = append(variable.Spec.Enum, matches[2])
}
case "@servers.variables.default":
server := p.openAPI.Servers[len(p.openAPI.Servers)-1]
matches := serversVariablesPattern.FindStringSubmatch(value)
if len(matches) > 0 {
variable, ok := server.Spec.Variables[matches[1]]
if !ok {
p.debug.Printf("Variables are not detected.")
continue
}
variable.Spec.Default = matches[2]
}
case "@servers.variables.description":
server := p.openAPI.Servers[len(p.openAPI.Servers)-1]
matches := serversVariablesPattern.FindStringSubmatch(value)
if len(matches) > 0 {
variable, ok := server.Spec.Variables[matches[1]]
if !ok {
p.debug.Printf("Variables are not detected.")
continue
}
variable.Spec.Default = matches[2]
}
case "@servers.variables.description.markdown":
server := p.openAPI.Servers[len(p.openAPI.Servers)-1]
matches := serversVariablesPattern.FindStringSubmatch(value)
if len(matches) > 0 {
variable, ok := server.Spec.Variables[matches[1]]
if !ok {
p.debug.Printf("Variables are not detected.")
continue
}
commentInfo, err := getMarkdownForTag(matches[1], p.markdownFileDir)
if err != nil {
return err
}
variable.Spec.Description = string(commentInfo)
}
default:
if strings.HasPrefix(attribute, "@x-") {
err := p.parseExtensionsV3(value, attribute)
if err != nil {
return errors.Wrap(err, "could not parse extension comment")
}
}
}
previousAttribute = attribute
}
return nil
}
func (p *Parser) parseExtensionsV3(value, attribute string) error {
extensionName := attribute[1:]
// // for each security definition
// for _, v := range p.openAPI.Components.Spec.SecuritySchemes{
// // check if extension exists
// _, extExistsInSecurityDef := v.VendorExtensible.Extensions.GetString(extensionName)
// // if it exists in at least one, then we stop iterating
// if extExistsInSecurityDef {
// return nil
// }
// }
if len(value) == 0 {
return fmt.Errorf("annotation %s need a value", attribute)
}
if p.openAPI.Info.Extensions == nil {
p.openAPI.Info.Extensions = map[string]any{}
}
var valueJSON interface{}
err := json.Unmarshal([]byte(value), &valueJSON)
if err != nil {
return fmt.Errorf("annotation %s need a valid json value. error: %s", attribute, err.Error())
}
if strings.Contains(extensionName, "logo") {
p.openAPI.Info.Extensions[extensionName] = valueJSON
return nil
}
p.openAPI.Info.Extensions[attribute[1:]] = valueJSON
return nil
}
func setspecInfo(openAPI *spec.OpenAPI, attribute, value string) {
switch attribute {
case versionAttr:
openAPI.Info.Spec.Version = value
case titleAttr:
openAPI.Info.Spec.Title = value
case tosAttr:
openAPI.Info.Spec.TermsOfService = value
case descriptionAttr:
openAPI.Info.Spec.Description = value
case conNameAttr:
if openAPI.Info.Spec.Contact == nil {
openAPI.Info.Spec.Contact = spec.NewContact()
}
openAPI.Info.Spec.Contact.Spec.Name = value
case conEmailAttr:
if openAPI.Info.Spec.Contact == nil {
openAPI.Info.Spec.Contact = spec.NewContact()
}
openAPI.Info.Spec.Contact.Spec.Email = value
case conURLAttr:
if openAPI.Info.Spec.Contact == nil {
openAPI.Info.Spec.Contact = spec.NewContact()
}
openAPI.Info.Spec.Contact.Spec.URL = value
case licNameAttr:
if openAPI.Info.Spec.License == nil {
openAPI.Info.Spec.License = spec.NewLicense()
}
openAPI.Info.Spec.License.Spec.Name = value
case licURLAttr:
if openAPI.Info.Spec.License == nil {
openAPI.Info.Spec.License = spec.NewLicense()
}
openAPI.Info.Spec.License.Spec.URL = value
}
}
func parseSecAttributesV3(context string, lines []string, index *int) (string, *spec.SecurityScheme, error) {
const (
in = "@in"
name = "@name"
descriptionAttr = "@description"
tokenURL = "@tokenurl"
authorizationURL = "@authorizationurl"
)
var search []string
attribute := strings.ToLower(FieldsByAnySpace(lines[*index], 2)[0])
switch attribute {
case secBasicAttr:
scheme := spec.SecurityScheme{
Type: "http",
Scheme: "basic",
}
return "basic", &scheme, nil
case secAPIKeyAttr:
search = []string{in, name}
case secApplicationAttr, secPasswordAttr:
search = []string{tokenURL, in, name}
case secImplicitAttr:
search = []string{authorizationURL, in}
case secAccessCodeAttr:
search = []string{tokenURL, authorizationURL, in}
case secBearerAuthAttr:
scheme := spec.SecurityScheme{
Type: "http",
Scheme: "bearer",
BearerFormat: "JWT",
}
return "bearerauth", &scheme, nil
}
// For the first line we get the attributes in the context parameter, so we skip to the next one
*index++
attrMap, scopes := make(map[string]string), make(map[string]string)
extensions, description := make(map[string]interface{}), ""
for ; *index < len(lines); *index++ {
v := strings.TrimSpace(lines[*index])
if len(v) == 0 {
continue
}
fields := FieldsByAnySpace(v, 2)
securityAttr := strings.ToLower(fields[0])
var value string
if len(fields) > 1 {
value = fields[1]
}
for _, findTerm := range search {
if securityAttr == findTerm {
attrMap[securityAttr] = value
break
}
}
isExists, err := isExistsScope(securityAttr)
if err != nil {
return "", nil, err
}
if isExists {
scopes[securityAttr[len(scopeAttrPrefix):]] = v[len(securityAttr):]
}
if strings.HasPrefix(securityAttr, "@x-") {
// Add the custom attribute without the @
extensions[securityAttr[1:]] = value
}
// Not mandatory field
if securityAttr == descriptionAttr {
description = value
}
// next securityDefinitions
if strings.Index(securityAttr, "@securitydefinitions.") == 0 {
// Go back to the previous line and break
*index--
break
}
}
if len(attrMap) != len(search) {
return "", nil, fmt.Errorf("%s is %v required", context, search)
}
scheme := &spec.SecurityScheme{}
key := getSecurityDefinitionKey(lines)
switch attribute {
case secAPIKeyAttr:
scheme.Type = "apiKey"
scheme.In = attrMap[in]
scheme.Name = attrMap[name]
case secApplicationAttr:
scheme.Type = "oauth2"
scheme.In = attrMap[in]
scheme.Flows = spec.NewOAuthFlows()
scheme.Flows.Spec.ClientCredentials = spec.NewOAuthFlow()
scheme.Flows.Spec.ClientCredentials.Spec.TokenURL = attrMap[tokenURL]
scheme.Flows.Spec.ClientCredentials.Spec.Scopes = make(map[string]string)
for k, v := range scopes {
scheme.Flows.Spec.ClientCredentials.Spec.Scopes[k] = v
}
case secImplicitAttr:
scheme.Type = "oauth2"
scheme.In = attrMap[in]
scheme.Flows = spec.NewOAuthFlows()
scheme.Flows.Spec.Implicit = spec.NewOAuthFlow()
scheme.Flows.Spec.Implicit.Spec.AuthorizationURL = attrMap[authorizationURL]
scheme.Flows.Spec.Implicit.Spec.Scopes = make(map[string]string)
for k, v := range scopes {
scheme.Flows.Spec.Implicit.Spec.Scopes[k] = v
}
case secPasswordAttr:
scheme.Type = "oauth2"
scheme.In = attrMap[in]
scheme.Flows = spec.NewOAuthFlows()
scheme.Flows.Spec.Password = spec.NewOAuthFlow()
scheme.Flows.Spec.Password.Spec.TokenURL = attrMap[tokenURL]
scheme.Flows.Spec.Password.Spec.Scopes = make(map[string]string)
for k, v := range scopes {
scheme.Flows.Spec.Password.Spec.Scopes[k] = v
}
case secAccessCodeAttr:
scheme.Type = "oauth2"
scheme.In = attrMap[in]
scheme.Flows = spec.NewOAuthFlows()
scheme.Flows.Spec.AuthorizationCode = spec.NewOAuthFlow()
scheme.Flows.Spec.AuthorizationCode.Spec.AuthorizationURL = attrMap[authorizationURL]
scheme.Flows.Spec.AuthorizationCode.Spec.TokenURL = attrMap[tokenURL]
}
scheme.Description = description
if scheme.Flows != nil && scheme.Flows.Extensions == nil && len(extensions) > 0 {
scheme.Flows.Extensions = make(map[string]interface{})
}
for k, v := range extensions {
scheme.Flows.Extensions[k] = v
}
return key, scheme, nil
}
func getSecurityDefinitionKey(lines []string) string {
for _, line := range lines {
if strings.HasPrefix(strings.ToLower(line), "@securitydefinitions") {
splittedLine := strings.Split(line, " ")
return splittedLine[len(splittedLine)-1]
}
}
return ""
}
// ParseRouterAPIInfoV3 parses router api info for given astFile.
func (p *Parser) ParseRouterAPIInfoV3(fileInfo *AstFileInfo) error {
for _, astDescription := range fileInfo.File.Decls {
if (fileInfo.ParseFlag & ParseOperations) == ParseNone {
continue
}
astDeclaration, ok := astDescription.(*ast.FuncDecl)
if !ok || astDeclaration.Doc == nil || astDeclaration.Doc.List == nil {
continue
}
if p.matchTags(astDeclaration.Doc.List) &&
matchExtension(p.parseExtension, astDeclaration.Doc.List) {
// for per 'function' comment, create a new 'Operation' object
operation := NewOperationV3(p, SetCodeExampleFilesDirectoryV3(p.codeExampleFilesDir))
for _, comment := range astDeclaration.Doc.List {
err := operation.ParseComment(comment.Text, fileInfo.File)
if err != nil {
return fmt.Errorf("ParseComment error in file %s :%+v", fileInfo.Path, err)
}
}
// workaround until we replace the produce comment with a new @Success syntax
// We first need to setup all responses before we can set the mimetypes
err := operation.ProcessProduceComment()
if err != nil {
return err
}
err = processRouterOperationV3(p, operation)
if err != nil {
return err
}
}
}
return nil
}
func processRouterOperationV3(p *Parser, o *OperationV3) error {
for _, routeProperties := range o.RouterProperties {
var (
pathItem *spec.RefOrSpec[spec.Extendable[spec.PathItem]]
ok bool
)
pathItem, ok = p.openAPI.Paths.Spec.Paths[routeProperties.Path]
if !ok {
pathItem = &spec.RefOrSpec[spec.Extendable[spec.PathItem]]{
Spec: &spec.Extendable[spec.PathItem]{
Spec: &spec.PathItem{},
},
}
}
op := refRouteMethodOpV3(pathItem.Spec.Spec, routeProperties.HTTPMethod)
// check if we already have an operation for this path and method
if *op != nil {
err := fmt.Errorf("route %s %s is declared multiple times", routeProperties.HTTPMethod, routeProperties.Path)
if p.Strict {
return err
}
p.debug.Printf("warning: %s\n", err)
}
*op = &o.Operation
p.openAPI.Paths.Spec.Paths[routeProperties.Path] = pathItem
}
return nil
}
func refRouteMethodOpV3(item *spec.PathItem, method string) **spec.Operation {
switch method {
case http.MethodGet:
if item.Get == nil {
item.Get = &spec.Extendable[spec.Operation]{}
}
return &item.Get.Spec
case http.MethodPost:
if item.Post == nil {
item.Post = &spec.Extendable[spec.Operation]{}
}
return &item.Post.Spec
case http.MethodDelete:
if item.Delete == nil {
item.Delete = &spec.Extendable[spec.Operation]{}
}
return &item.Delete.Spec
case http.MethodPut:
if item.Put == nil {
item.Put = &spec.Extendable[spec.Operation]{}
}
return &item.Put.Spec
case http.MethodPatch:
if item.Patch == nil {
item.Patch = &spec.Extendable[spec.Operation]{}
}
return &item.Patch.Spec
case http.MethodHead:
if item.Head == nil {
item.Head = &spec.Extendable[spec.Operation]{}
}
return &item.Head.Spec
case http.MethodOptions:
if item.Options == nil {
item.Options = &spec.Extendable[spec.Operation]{}
}
return &item.Options.Spec
default:
return nil
}
}
func (p *Parser) getTypeSchemaV3(typeName string, file *ast.File, ref bool) (*spec.RefOrSpec[spec.Schema], error) {
if override, ok := p.Overrides[typeName]; ok {
p.debug.Printf("Override detected for %s: using %s instead", typeName, override)
schema, err := parseObjectSchemaV3(p, override, file)
if err != nil {
return nil, err
}
return schema, nil
}
if IsInterfaceLike(typeName) {
return spec.NewSchemaSpec(), nil
}
if IsGolangPrimitiveType(typeName) {
return PrimitiveSchemaV3(TransToValidSchemeType(typeName)), nil
}
schemaType, err := convertFromSpecificToPrimitive(typeName)
if err == nil {
return PrimitiveSchemaV3(schemaType), nil
}
typeSpecDef := p.packages.FindTypeSpec(typeName, file)
if typeSpecDef == nil {
p.packages.FindTypeSpec(typeName, file) // uncomment for debugging
return nil, fmt.Errorf("cannot find type definition: %s", typeName)
}
if override, ok := p.Overrides[typeSpecDef.FullPath()]; ok {
if override == "" {
p.debug.Printf("Override detected for %s: ignoring", typeSpecDef.FullPath())
return nil, ErrSkippedField
}
p.debug.Printf("Override detected for %s: using %s instead", typeSpecDef.FullPath(), override)
separator := strings.LastIndex(override, ".")
if separator == -1 {
// treat as a swaggertype tag
parts := strings.Split(override, ",")
return BuildCustomSchemaV3(parts)
}
typeSpecDef = p.packages.findTypeSpec(override[0:separator], override[separator+1:])
}
schema, ok := p.parsedSchemasV3[typeSpecDef]
if !ok {
var err error
schema, err = p.ParseDefinitionV3(typeSpecDef)
if err != nil {
if err == ErrRecursiveParseStruct && ref {
return p.getRefTypeSchemaV3(typeSpecDef, schema), nil
}
return nil, err
}
}
if ref {
if IsComplexSchemaV3(schema) {
return p.getRefTypeSchemaV3(typeSpecDef, schema), nil
}
// if it is a simple schema, just return a copy
newSchema := *schema.Schema
return spec.NewRefOrSpec(nil, &newSchema), nil
}
return spec.NewRefOrSpec(nil, schema.Schema), nil
}
// ParseDefinitionV3 parses given type spec that corresponds to the type under
// given name and package, and populates swagger schema definitions registry
// with a schema for the given type
func (p *Parser) ParseDefinitionV3(typeSpecDef *TypeSpecDef) (*SchemaV3, error) {
typeName := typeSpecDef.TypeName()
schema, found := p.parsedSchemasV3[typeSpecDef]
if found {
p.debug.Printf("Skipping '%s', already parsed.", typeName)
return schema, nil
}
if p.isInStructStack(typeSpecDef) {
p.debug.Printf("Skipping '%s', recursion detected.", typeName)
return &SchemaV3{
Name: typeName,
PkgPath: typeSpecDef.PkgPath,
Schema: PrimitiveSchemaV3(OBJECT).Spec,
},
ErrRecursiveParseStruct
}
p.structStack = append(p.structStack, typeSpecDef)
p.debug.Printf("Generating %s", typeName)
definition, err := p.parseTypeExprV3(typeSpecDef.File, typeSpecDef.TypeSpec.Type, false)
if err != nil {
p.debug.Printf("Error parsing type definition '%s': %s", typeName, err)
return nil, err
}
if definition.Spec.Description == "" {
fillDefinitionDescriptionV3(p, definition.Spec, typeSpecDef.File, typeSpecDef)
}
if len(typeSpecDef.Enums) > 0 {
var varNames []string
var enumComments = make(map[string]string)
for _, value := range typeSpecDef.Enums {
definition.Spec.Enum = append(definition.Spec.Enum, value.Value)
varNames = append(varNames, value.key)
if len(value.Comment) > 0 {
enumComments[value.key] = value.Comment
}
}
if definition.Spec.Extensions == nil {
definition.Spec.Extensions = make(map[string]any)
}
definition.Spec.Extensions[enumVarNamesExtension] = varNames
if len(enumComments) > 0 {
definition.Spec.Extensions[enumCommentsExtension] = enumComments
}
}
sch := SchemaV3{
Name: typeName,
PkgPath: typeSpecDef.PkgPath,
Schema: definition.Spec,
}
p.parsedSchemasV3[typeSpecDef] = &sch
// update an empty schema as a result of recursion
s2, found := p.outputSchemasV3[typeSpecDef]
if found {
p.openAPI.Components.Spec.Schemas[s2.Name] = definition
}
return &sch, nil
}
// fillDefinitionDescription additionally fills fields in definition (spec.Schema)
// TODO: If .go file contains many types, it may work for a long time
func fillDefinitionDescriptionV3(parser *Parser, definition *spec.Schema, file *ast.File, typeSpecDef *TypeSpecDef) {
for _, astDeclaration := range file.Decls {
generalDeclaration, ok := astDeclaration.(*ast.GenDecl)
if !ok || generalDeclaration.Tok != token.TYPE {
continue
}
for _, astSpec := range generalDeclaration.Specs {
typeSpec, ok := astSpec.(*ast.TypeSpec)
if !ok || typeSpec != typeSpecDef.TypeSpec {
continue
}
var typeName string
if typeSpec.Name != nil {
typeName = typeSpec.Name.Name
}
text, err := parser.extractDeclarationDescription(typeName, typeSpec.Comment, generalDeclaration.Doc)
if err != nil {
parser.debug.Printf("Error extracting declaration description: %s", err)
continue
}
definition.Description = text
}
}
}
// parseTypeExprV3 parses given type expression that corresponds to the type under
// given name and package, and returns swagger schema for it.
func (p *Parser) parseTypeExprV3(file *ast.File, typeExpr ast.Expr, ref bool) (*spec.RefOrSpec[spec.Schema], error) {
const errMessage = "parse type expression v3"
switch expr := typeExpr.(type) {
// type Foo interface{}
case *ast.InterfaceType:
return spec.NewSchemaSpec(), nil
// type Foo struct {...}
case *ast.StructType:
return p.parseStructV3(file, expr.Fields)
// type Foo Baz
case *ast.Ident:
result, err := p.getTypeSchemaV3(expr.Name, file, ref)
if err != nil {
return nil, errors.Wrap(err, errMessage)
}
return result, nil
// type Foo *Baz
case *ast.StarExpr:
return p.parseTypeExprV3(file, expr.X, ref)
// type Foo pkg.Bar
case *ast.SelectorExpr:
if xIdent, ok := expr.X.(*ast.Ident); ok {
result, err := p.getTypeSchemaV3(fullTypeName(xIdent.Name, expr.Sel.Name), file, ref)
if err != nil {
return nil, errors.Wrap(err, errMessage)
}
return result, nil
}
// type Foo []Baz
case *ast.ArrayType:
itemSchema, err := p.parseTypeExprV3(file, expr.Elt, true)
if err != nil {
return nil, err
}
if itemSchema == nil {
schema := &spec.Schema{}
schema.Type = spec.NewSingleOrArray(ARRAY)
schema.Items = spec.NewBoolOrSchema(false, spec.NewSchemaSpec())
p.debug.Printf("Creating array with empty item schema %v", expr.Elt)
return spec.NewRefOrSpec(nil, schema), nil
}
result := &spec.Schema{}
result.Type = spec.NewSingleOrArray(ARRAY)
result.Items = spec.NewBoolOrSchema(false, itemSchema)
return spec.NewRefOrSpec(nil, result), nil
// type Foo map[string]Bar
case *ast.MapType:
if _, ok := expr.Value.(*ast.InterfaceType); ok {
result := &spec.Schema{}
result.AdditionalProperties = spec.NewBoolOrSchema(false, spec.NewSchemaSpec())
result.Type = spec.NewSingleOrArray(OBJECT)
return spec.NewRefOrSpec(nil, result), nil
}
schema, err := p.parseTypeExprV3(file, expr.Value, true)
if err != nil {
return nil, err
}
result := &spec.Schema{}
result.AdditionalProperties = spec.NewBoolOrSchema(false, schema)
result.Type = spec.NewSingleOrArray(OBJECT)
return spec.NewRefOrSpec(nil, result), nil
case *ast.FuncType:
return nil, ErrFuncTypeField
// ...
}
return p.parseGenericTypeExprV3(file, typeExpr)
}
func (p *Parser) parseStructV3(file *ast.File, fields *ast.FieldList) (*spec.RefOrSpec[spec.Schema], error) {
required, properties := make([]string, 0), make(map[string]*spec.RefOrSpec[spec.Schema])
for _, field := range fields.List {
fieldProps, requiredFromAnon, err := p.parseStructFieldV3(file, field)
if err != nil {
if err == ErrFuncTypeField || err == ErrSkippedField {
continue
}
return nil, err
}
if len(fieldProps) == 0 {
continue
}
required = append(required, requiredFromAnon...)
for k, v := range fieldProps {
properties[k] = v
}
}
sort.Strings(required)
result := spec.NewSchemaSpec()
result.Spec.Type = spec.NewSingleOrArray(OBJECT)
result.Spec.Properties = properties
result.Spec.Required = required
return result, nil
}
func (p *Parser) parseStructFieldV3(file *ast.File, field *ast.Field) (map[string]*spec.RefOrSpec[spec.Schema], []string, error) {
if field.Tag != nil {
skip, ok := reflect.StructTag(strings.ReplaceAll(field.Tag.Value, "`", "")).Lookup("swaggerignore")
if ok && strings.EqualFold(skip, "true") {
return nil, nil, nil
}
}
ps := p.fieldParserFactoryV3(p, file, field)
if ps.ShouldSkip() {
return nil, nil, nil
}
fieldName, err := ps.FieldName()
if err != nil {
return nil, nil, err
}
if fieldName == "" {
typeName, err := getFieldType(file, field.Type, nil)
if err != nil {
return nil, nil, err
}
schema, err := p.getTypeSchemaV3(typeName, file, false)
if err != nil {
return nil, nil, err
}
if len(schema.Spec.Type) > 0 && schema.Spec.Type[0] == OBJECT {
if len(schema.Spec.Properties) == 0 {
return nil, nil, nil
}
properties := make(map[string]*spec.RefOrSpec[spec.Schema])
for k, v := range schema.Spec.Properties {
properties[k] = v
}
return properties, schema.Spec.Required, nil
}
// for alias type of non-struct types ,such as array,map, etc. ignore field tag.
return map[string]*spec.RefOrSpec[spec.Schema]{
typeName: schema,
}, nil, nil
}
schema, err := ps.CustomSchema()
if err != nil {
return nil, nil, err
}
if schema == nil {
typeName, err := getFieldType(file, field.Type, nil)
if err == nil {
// named type
schema, err = p.getTypeSchemaV3(typeName, file, true)
if err != nil {
return nil, nil, err
}
} else {
// unnamed type
parsedSchema, err := p.parseTypeExprV3(file, field.Type, false)
if err != nil {
return nil, nil, err
}
schema = parsedSchema
}
}
err = ps.ComplementSchema(schema)
if err != nil {
return nil, nil, err
}
var tagRequired []string
required, err := ps.IsRequired()
if err != nil {
return nil, nil, err
}
if required {
tagRequired = append(tagRequired, fieldName)
}
if formName := ps.FormName(); len(formName) > 0 {
if schema.Spec.Extensions == nil {
schema.Spec.Extensions = make(map[string]any)
}
schema.Spec.Extensions[formTag] = formName
}
return map[string]*spec.RefOrSpec[spec.Schema]{fieldName: schema}, tagRequired, nil
}
func (p *Parser) getRefTypeSchemaV3(typeSpecDef *TypeSpecDef, schema *SchemaV3) *spec.RefOrSpec[spec.Schema] {
_, ok := p.outputSchemasV3[typeSpecDef]
if !ok {
if p.openAPI.Components.Spec.Schemas == nil {
p.openAPI.Components.Spec.Schemas = make(map[string]*spec.RefOrSpec[spec.Schema])
}
p.openAPI.Components.Spec.Schemas[schema.Name] = spec.NewSchemaSpec()
if schema.Schema != nil {
p.openAPI.Components.Spec.Schemas[schema.Name] = spec.NewRefOrSpec(nil, schema.Schema)
}
p.outputSchemasV3[typeSpecDef] = schema
}
refSchema := RefSchemaV3(schema.Name)
return refSchema
}
// GetSchemaTypePathV3 get path of schema type.
func (p *Parser) GetSchemaTypePathV3(schema *spec.RefOrSpec[spec.Schema], depth int) []string {
if schema == nil || depth == 0 {
return nil
}
name := ""
if schema.Ref != nil {
name = schema.Ref.Ref
}
if name != "" {
if pos := strings.LastIndexByte(name, '/'); pos >= 0 {
name = name[pos+1:]
if schema, ok := p.openAPI.Components.Spec.Schemas[name]; ok {
return p.GetSchemaTypePathV3(schema, depth)
}
}
return nil
}
if schema.Spec != nil && len(schema.Spec.Type) > 0 {
switch schema.Spec.Type[0] {
case ARRAY:
depth--
s := []string{schema.Spec.Type[0]}
return append(s, p.GetSchemaTypePathV3(schema.Spec.Items.Schema, depth)...)
case OBJECT:
if schema.Spec.AdditionalProperties != nil && schema.Spec.AdditionalProperties.Schema != nil {
// for map
depth--
s := []string{schema.Spec.Type[0]}
return append(s, p.GetSchemaTypePathV3(schema.Spec.AdditionalProperties.Schema, depth)...)
}
}
return []string{schema.Spec.Type[0]}
}
println("found schema with no Type, returning any")
return []string{ANY}
}
func (p *Parser) getSchemaByRef(ref *spec.Ref) *spec.Schema {
searchString := strings.ReplaceAll(ref.Ref, "#/components/schemas/", "")
return p.openAPI.Components.Spec.Schemas[searchString].Spec
}