forked from ebhomengo/niki
1088 lines
29 KiB
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
|
|
}
|