forked from ebhomengo/niki
1
0
Fork 0
niki/vendor/github.com/swaggo/swag/parser.go

2975 lines
49 KiB
Go
Raw Normal View History

2024-05-14 13:07:09 +00:00
package swag
import (
"context"
"encoding/json"
"errors"
"fmt"
"go/ast"
"go/build"
goparser "go/parser"
"go/token"
"log"
"net/http"
"os"
"os/exec"
"path/filepath"
"reflect"
"sort"
"strconv"
"strings"
"github.com/KyleBanks/depth"
"github.com/go-openapi/spec"
)
const (
2024-05-14 13:07:09 +00:00
// CamelCase indicates using CamelCase strategy for struct field.
2024-05-14 13:07:09 +00:00
CamelCase = "camelcase"
// PascalCase indicates using PascalCase strategy for struct field.
2024-05-14 13:07:09 +00:00
PascalCase = "pascalcase"
// SnakeCase indicates using SnakeCase strategy for struct field.
2024-05-14 13:07:09 +00:00
SnakeCase = "snakecase"
idAttr = "@id"
acceptAttr = "@accept"
produceAttr = "@produce"
paramAttr = "@param"
successAttr = "@success"
failureAttr = "@failure"
responseAttr = "@response"
headerAttr = "@header"
tagsAttr = "@tags"
routerAttr = "@router"
deprecatedRouterAttr = "@deprecatedrouter"
summaryAttr = "@summary"
deprecatedAttr = "@deprecated"
securityAttr = "@security"
titleAttr = "@title"
conNameAttr = "@contact.name"
conURLAttr = "@contact.url"
conEmailAttr = "@contact.email"
licNameAttr = "@license.name"
licURLAttr = "@license.url"
versionAttr = "@version"
descriptionAttr = "@description"
2024-05-14 13:07:09 +00:00
descriptionMarkdownAttr = "@description.markdown"
secBasicAttr = "@securitydefinitions.basic"
secAPIKeyAttr = "@securitydefinitions.apikey"
secApplicationAttr = "@securitydefinitions.oauth2.application"
secImplicitAttr = "@securitydefinitions.oauth2.implicit"
secPasswordAttr = "@securitydefinitions.oauth2.password"
secAccessCodeAttr = "@securitydefinitions.oauth2.accesscode"
tosAttr = "@termsofservice"
extDocsDescAttr = "@externaldocs.description"
extDocsURLAttr = "@externaldocs.url"
xCodeSamplesAttr = "@x-codesamples"
scopeAttrPrefix = "@scope."
stateAttr = "@state"
2024-05-14 13:07:09 +00:00
)
// ParseFlag determine what to parse
2024-05-14 13:07:09 +00:00
type ParseFlag int
const (
2024-05-14 13:07:09 +00:00
// ParseNone parse nothing
2024-05-14 13:07:09 +00:00
ParseNone ParseFlag = 0x00
2024-05-14 13:07:09 +00:00
// ParseModels parse models
2024-05-14 13:07:09 +00:00
ParseModels = 0x01
2024-05-14 13:07:09 +00:00
// ParseOperations parse operations
2024-05-14 13:07:09 +00:00
ParseOperations = 0x02
2024-05-14 13:07:09 +00:00
// ParseAll parse operations and models
2024-05-14 13:07:09 +00:00
ParseAll = ParseOperations | ParseModels
)
var (
2024-05-14 13:07:09 +00:00
// ErrRecursiveParseStruct recursively parsing struct.
2024-05-14 13:07:09 +00:00
ErrRecursiveParseStruct = errors.New("recursively parsing struct")
// ErrFuncTypeField field type is func.
2024-05-14 13:07:09 +00:00
ErrFuncTypeField = errors.New("field type is func")
// ErrFailedConvertPrimitiveType Failed to convert for swag to interpretable type.
2024-05-14 13:07:09 +00:00
ErrFailedConvertPrimitiveType = errors.New("swag property: failed convert primitive type")
// ErrSkippedField .swaggo specifies field should be skipped.
2024-05-14 13:07:09 +00:00
ErrSkippedField = errors.New("field is skipped by global overrides")
)
var allMethod = map[string]struct{}{
http.MethodGet: {},
http.MethodPut: {},
http.MethodPost: {},
http.MethodDelete: {},
2024-05-14 13:07:09 +00:00
http.MethodOptions: {},
http.MethodHead: {},
http.MethodPatch: {},
2024-05-14 13:07:09 +00:00
}
// Parser implements a parser for Go source files.
2024-05-14 13:07:09 +00:00
type Parser struct {
2024-05-14 13:07:09 +00:00
// swagger represents the root document object for the API specification
2024-05-14 13:07:09 +00:00
swagger *spec.Swagger
// packages store entities of APIs, definitions, file, package path etc. and their relations
2024-05-14 13:07:09 +00:00
packages *PackagesDefinitions
// parsedSchemas store schemas which have been parsed from ast.TypeSpec
2024-05-14 13:07:09 +00:00
parsedSchemas map[*TypeSpecDef]*Schema
// outputSchemas store schemas which will be export to swagger
2024-05-14 13:07:09 +00:00
outputSchemas map[*TypeSpecDef]*Schema
// PropNamingStrategy naming strategy
2024-05-14 13:07:09 +00:00
PropNamingStrategy string
// ParseVendor parse vendor folder
2024-05-14 13:07:09 +00:00
ParseVendor bool
// ParseDependencies whether swag should be parse outside dependency folder: 0 none, 1 models, 2 operations, 3 all
2024-05-14 13:07:09 +00:00
ParseDependency ParseFlag
// ParseInternal whether swag should parse internal packages
2024-05-14 13:07:09 +00:00
ParseInternal bool
// Strict whether swag should error or warn when it detects cases which are most likely user errors
2024-05-14 13:07:09 +00:00
Strict bool
// RequiredByDefault set validation required for all fields by default
2024-05-14 13:07:09 +00:00
RequiredByDefault bool
// structStack stores full names of the structures that were already parsed or are being parsed now
2024-05-14 13:07:09 +00:00
structStack []*TypeSpecDef
// markdownFileDir holds the path to the folder, where markdown files are stored
2024-05-14 13:07:09 +00:00
markdownFileDir string
// codeExampleFilesDir holds path to the folder, where code example files are stored
2024-05-14 13:07:09 +00:00
codeExampleFilesDir string
// collectionFormatInQuery set the default collectionFormat otherwise then 'csv' for array in query params
2024-05-14 13:07:09 +00:00
collectionFormatInQuery string
// excludes excludes dirs and files in SearchDir
2024-05-14 13:07:09 +00:00
excludes map[string]struct{}
// packagePrefix is a list of package path prefixes, packages that do not
2024-05-14 13:07:09 +00:00
// match any one of them will be excluded when searching.
2024-05-14 13:07:09 +00:00
packagePrefix []string
// tells parser to include only specific extension
2024-05-14 13:07:09 +00:00
parseExtension string
// debugging output goes here
2024-05-14 13:07:09 +00:00
debug Debugger
// fieldParserFactory create FieldParser
2024-05-14 13:07:09 +00:00
fieldParserFactory FieldParserFactory
// Overrides allows global replacements of types. A blank replacement will be skipped.
2024-05-14 13:07:09 +00:00
Overrides map[string]string
// parseGoList whether swag use go list to parse dependency
2024-05-14 13:07:09 +00:00
parseGoList bool
// tags to filter the APIs after
2024-05-14 13:07:09 +00:00
tags map[string]struct{}
// HostState is the state of the host
2024-05-14 13:07:09 +00:00
HostState string
}
// FieldParserFactory create FieldParser.
2024-05-14 13:07:09 +00:00
type FieldParserFactory func(ps *Parser, field *ast.Field) FieldParser
// FieldParser parse struct field.
2024-05-14 13:07:09 +00:00
type FieldParser interface {
ShouldSkip() bool
2024-05-14 13:07:09 +00:00
FieldName() (string, error)
2024-05-14 13:07:09 +00:00
FormName() string
2024-05-14 13:07:09 +00:00
HeaderName() string
2024-05-14 13:07:09 +00:00
PathName() string
2024-05-14 13:07:09 +00:00
CustomSchema() (*spec.Schema, error)
2024-05-14 13:07:09 +00:00
ComplementSchema(schema *spec.Schema) error
2024-05-14 13:07:09 +00:00
IsRequired() (bool, error)
}
// Debugger is the interface that wraps the basic Printf method.
2024-05-14 13:07:09 +00:00
type Debugger interface {
Printf(format string, v ...interface{})
}
// New creates a new Parser with default properties.
2024-05-14 13:07:09 +00:00
func New(options ...func(*Parser)) *Parser {
2024-05-14 13:07:09 +00:00
parser := &Parser{
2024-05-14 13:07:09 +00:00
swagger: &spec.Swagger{
2024-05-14 13:07:09 +00:00
SwaggerProps: spec.SwaggerProps{
2024-05-14 13:07:09 +00:00
Info: &spec.Info{
2024-05-14 13:07:09 +00:00
InfoProps: spec.InfoProps{
2024-05-14 13:07:09 +00:00
Contact: &spec.ContactInfo{},
2024-05-14 13:07:09 +00:00
License: nil,
},
2024-05-14 13:07:09 +00:00
VendorExtensible: spec.VendorExtensible{
2024-05-14 13:07:09 +00:00
Extensions: spec.Extensions{},
},
},
2024-05-14 13:07:09 +00:00
Paths: &spec.Paths{
2024-05-14 13:07:09 +00:00
Paths: make(map[string]spec.PathItem),
2024-05-14 13:07:09 +00:00
VendorExtensible: spec.VendorExtensible{
2024-05-14 13:07:09 +00:00
Extensions: nil,
},
},
Definitions: make(map[string]spec.Schema),
2024-05-14 13:07:09 +00:00
SecurityDefinitions: make(map[string]*spec.SecurityScheme),
},
2024-05-14 13:07:09 +00:00
VendorExtensible: spec.VendorExtensible{
2024-05-14 13:07:09 +00:00
Extensions: nil,
},
},
packages: NewPackagesDefinitions(),
debug: log.New(os.Stdout, "", log.LstdFlags),
parsedSchemas: make(map[*TypeSpecDef]*Schema),
outputSchemas: make(map[*TypeSpecDef]*Schema),
excludes: make(map[string]struct{}),
tags: make(map[string]struct{}),
2024-05-14 13:07:09 +00:00
fieldParserFactory: newTagBaseFieldParser,
Overrides: make(map[string]string),
2024-05-14 13:07:09 +00:00
}
for _, option := range options {
2024-05-14 13:07:09 +00:00
option(parser)
2024-05-14 13:07:09 +00:00
}
parser.packages.debug = parser.debug
return parser
2024-05-14 13:07:09 +00:00
}
// SetParseDependency sets whether to parse the dependent packages.
2024-05-14 13:07:09 +00:00
func SetParseDependency(parseDependency int) func(*Parser) {
2024-05-14 13:07:09 +00:00
return func(p *Parser) {
2024-05-14 13:07:09 +00:00
p.ParseDependency = ParseFlag(parseDependency)
2024-05-14 13:07:09 +00:00
if p.packages != nil {
2024-05-14 13:07:09 +00:00
p.packages.parseDependency = p.ParseDependency
2024-05-14 13:07:09 +00:00
}
2024-05-14 13:07:09 +00:00
}
2024-05-14 13:07:09 +00:00
}
// SetMarkdownFileDirectory sets the directory to search for markdown files.
2024-05-14 13:07:09 +00:00
func SetMarkdownFileDirectory(directoryPath string) func(*Parser) {
2024-05-14 13:07:09 +00:00
return func(p *Parser) {
2024-05-14 13:07:09 +00:00
p.markdownFileDir = directoryPath
2024-05-14 13:07:09 +00:00
}
2024-05-14 13:07:09 +00:00
}
// SetCodeExamplesDirectory sets the directory to search for code example files.
2024-05-14 13:07:09 +00:00
func SetCodeExamplesDirectory(directoryPath string) func(*Parser) {
2024-05-14 13:07:09 +00:00
return func(p *Parser) {
2024-05-14 13:07:09 +00:00
p.codeExampleFilesDir = directoryPath
2024-05-14 13:07:09 +00:00
}
2024-05-14 13:07:09 +00:00
}
// SetExcludedDirsAndFiles sets directories and files to be excluded when searching.
2024-05-14 13:07:09 +00:00
func SetExcludedDirsAndFiles(excludes string) func(*Parser) {
2024-05-14 13:07:09 +00:00
return func(p *Parser) {
2024-05-14 13:07:09 +00:00
for _, f := range strings.Split(excludes, ",") {
2024-05-14 13:07:09 +00:00
f = strings.TrimSpace(f)
2024-05-14 13:07:09 +00:00
if f != "" {
2024-05-14 13:07:09 +00:00
f = filepath.Clean(f)
2024-05-14 13:07:09 +00:00
p.excludes[f] = struct{}{}
2024-05-14 13:07:09 +00:00
}
2024-05-14 13:07:09 +00:00
}
2024-05-14 13:07:09 +00:00
}
2024-05-14 13:07:09 +00:00
}
// SetPackagePrefix sets a list of package path prefixes from a comma-separated
2024-05-14 13:07:09 +00:00
// string, packages that do not match any one of them will be excluded when
2024-05-14 13:07:09 +00:00
// searching.
2024-05-14 13:07:09 +00:00
func SetPackagePrefix(packagePrefix string) func(*Parser) {
2024-05-14 13:07:09 +00:00
return func(p *Parser) {
2024-05-14 13:07:09 +00:00
for _, f := range strings.Split(packagePrefix, ",") {
2024-05-14 13:07:09 +00:00
f = strings.TrimSpace(f)
2024-05-14 13:07:09 +00:00
if f != "" {
2024-05-14 13:07:09 +00:00
p.packagePrefix = append(p.packagePrefix, f)
2024-05-14 13:07:09 +00:00
}
2024-05-14 13:07:09 +00:00
}
2024-05-14 13:07:09 +00:00
}
2024-05-14 13:07:09 +00:00
}
// SetTags sets the tags to be included
2024-05-14 13:07:09 +00:00
func SetTags(include string) func(*Parser) {
2024-05-14 13:07:09 +00:00
return func(p *Parser) {
2024-05-14 13:07:09 +00:00
for _, f := range strings.Split(include, ",") {
2024-05-14 13:07:09 +00:00
f = strings.TrimSpace(f)
2024-05-14 13:07:09 +00:00
if f != "" {
2024-05-14 13:07:09 +00:00
p.tags[f] = struct{}{}
2024-05-14 13:07:09 +00:00
}
2024-05-14 13:07:09 +00:00
}
2024-05-14 13:07:09 +00:00
}
2024-05-14 13:07:09 +00:00
}
// SetParseExtension parses only those operations which match given extension
2024-05-14 13:07:09 +00:00
func SetParseExtension(parseExtension string) func(*Parser) {
2024-05-14 13:07:09 +00:00
return func(p *Parser) {
2024-05-14 13:07:09 +00:00
p.parseExtension = parseExtension
2024-05-14 13:07:09 +00:00
}
2024-05-14 13:07:09 +00:00
}
// SetStrict sets whether swag should error or warn when it detects cases which are most likely user errors.
2024-05-14 13:07:09 +00:00
func SetStrict(strict bool) func(*Parser) {
2024-05-14 13:07:09 +00:00
return func(p *Parser) {
2024-05-14 13:07:09 +00:00
p.Strict = strict
2024-05-14 13:07:09 +00:00
}
2024-05-14 13:07:09 +00:00
}
// SetDebugger allows the use of user-defined implementations.
2024-05-14 13:07:09 +00:00
func SetDebugger(logger Debugger) func(parser *Parser) {
2024-05-14 13:07:09 +00:00
return func(p *Parser) {
2024-05-14 13:07:09 +00:00
if logger != nil {
2024-05-14 13:07:09 +00:00
p.debug = logger
2024-05-14 13:07:09 +00:00
}
2024-05-14 13:07:09 +00:00
}
2024-05-14 13:07:09 +00:00
}
// SetFieldParserFactory allows the use of user-defined implementations.
2024-05-14 13:07:09 +00:00
func SetFieldParserFactory(factory FieldParserFactory) func(parser *Parser) {
2024-05-14 13:07:09 +00:00
return func(p *Parser) {
2024-05-14 13:07:09 +00:00
p.fieldParserFactory = factory
2024-05-14 13:07:09 +00:00
}
2024-05-14 13:07:09 +00:00
}
// SetOverrides allows the use of user-defined global type overrides.
2024-05-14 13:07:09 +00:00
func SetOverrides(overrides map[string]string) func(parser *Parser) {
2024-05-14 13:07:09 +00:00
return func(p *Parser) {
2024-05-14 13:07:09 +00:00
for k, v := range overrides {
2024-05-14 13:07:09 +00:00
p.Overrides[k] = v
2024-05-14 13:07:09 +00:00
}
2024-05-14 13:07:09 +00:00
}
2024-05-14 13:07:09 +00:00
}
// SetCollectionFormat set default collection format
2024-05-14 13:07:09 +00:00
func SetCollectionFormat(collectionFormat string) func(*Parser) {
2024-05-14 13:07:09 +00:00
return func(p *Parser) {
2024-05-14 13:07:09 +00:00
p.collectionFormatInQuery = collectionFormat
2024-05-14 13:07:09 +00:00
}
2024-05-14 13:07:09 +00:00
}
// ParseUsingGoList sets whether swag use go list to parse dependency
2024-05-14 13:07:09 +00:00
func ParseUsingGoList(enabled bool) func(parser *Parser) {
2024-05-14 13:07:09 +00:00
return func(p *Parser) {
2024-05-14 13:07:09 +00:00
p.parseGoList = enabled
2024-05-14 13:07:09 +00:00
}
2024-05-14 13:07:09 +00:00
}
// ParseAPI parses general api info for given searchDir and mainAPIFile.
2024-05-14 13:07:09 +00:00
func (parser *Parser) ParseAPI(searchDir string, mainAPIFile string, parseDepth int) error {
2024-05-14 13:07:09 +00:00
return parser.ParseAPIMultiSearchDir([]string{searchDir}, mainAPIFile, parseDepth)
2024-05-14 13:07:09 +00:00
}
// skipPackageByPrefix returns true the given pkgpath does not match
2024-05-14 13:07:09 +00:00
// any user-defined package path prefixes.
2024-05-14 13:07:09 +00:00
func (parser *Parser) skipPackageByPrefix(pkgpath string) bool {
2024-05-14 13:07:09 +00:00
if len(parser.packagePrefix) == 0 {
2024-05-14 13:07:09 +00:00
return false
2024-05-14 13:07:09 +00:00
}
2024-05-14 13:07:09 +00:00
for _, prefix := range parser.packagePrefix {
2024-05-14 13:07:09 +00:00
if strings.HasPrefix(pkgpath, prefix) {
2024-05-14 13:07:09 +00:00
return false
2024-05-14 13:07:09 +00:00
}
2024-05-14 13:07:09 +00:00
}
2024-05-14 13:07:09 +00:00
return true
2024-05-14 13:07:09 +00:00
}
// ParseAPIMultiSearchDir is like ParseAPI but for multiple search dirs.
2024-05-14 13:07:09 +00:00
func (parser *Parser) ParseAPIMultiSearchDir(searchDirs []string, mainAPIFile string, parseDepth int) error {
2024-05-14 13:07:09 +00:00
for _, searchDir := range searchDirs {
2024-05-14 13:07:09 +00:00
parser.debug.Printf("Generate general API Info, search dir:%s", searchDir)
packageDir, err := getPkgName(searchDir)
2024-05-14 13:07:09 +00:00
if err != nil {
2024-05-14 13:07:09 +00:00
parser.debug.Printf("warning: failed to get package name in dir: %s, error: %s", searchDir, err.Error())
2024-05-14 13:07:09 +00:00
}
err = parser.getAllGoFileInfo(packageDir, searchDir)
2024-05-14 13:07:09 +00:00
if err != nil {
2024-05-14 13:07:09 +00:00
return err
2024-05-14 13:07:09 +00:00
}
2024-05-14 13:07:09 +00:00
}
absMainAPIFilePath, err := filepath.Abs(filepath.Join(searchDirs[0], mainAPIFile))
2024-05-14 13:07:09 +00:00
if err != nil {
2024-05-14 13:07:09 +00:00
return err
2024-05-14 13:07:09 +00:00
}
// Use 'go list' command instead of depth.Resolve()
2024-05-14 13:07:09 +00:00
if parser.ParseDependency > 0 {
2024-05-14 13:07:09 +00:00
if parser.parseGoList {
2024-05-14 13:07:09 +00:00
pkgs, err := listPackages(context.Background(), filepath.Dir(absMainAPIFilePath), nil, "-deps")
2024-05-14 13:07:09 +00:00
if err != nil {
2024-05-14 13:07:09 +00:00
return fmt.Errorf("pkg %s cannot find all dependencies, %s", filepath.Dir(absMainAPIFilePath), err)
2024-05-14 13:07:09 +00:00
}
length := len(pkgs)
2024-05-14 13:07:09 +00:00
for i := 0; i < length; i++ {
2024-05-14 13:07:09 +00:00
err := parser.getAllGoFileInfoFromDepsByList(pkgs[i], parser.ParseDependency)
2024-05-14 13:07:09 +00:00
if err != nil {
2024-05-14 13:07:09 +00:00
return err
2024-05-14 13:07:09 +00:00
}
2024-05-14 13:07:09 +00:00
}
2024-05-14 13:07:09 +00:00
} else {
2024-05-14 13:07:09 +00:00
var t depth.Tree
2024-05-14 13:07:09 +00:00
t.ResolveInternal = true
2024-05-14 13:07:09 +00:00
t.MaxDepth = parseDepth
pkgName, err := getPkgName(filepath.Dir(absMainAPIFilePath))
2024-05-14 13:07:09 +00:00
if err != nil {
2024-05-14 13:07:09 +00:00
return err
2024-05-14 13:07:09 +00:00
}
err = t.Resolve(pkgName)
2024-05-14 13:07:09 +00:00
if err != nil {
2024-05-14 13:07:09 +00:00
return fmt.Errorf("pkg %s cannot find all dependencies, %s", pkgName, err)
2024-05-14 13:07:09 +00:00
}
2024-05-14 13:07:09 +00:00
for i := 0; i < len(t.Root.Deps); i++ {
2024-05-14 13:07:09 +00:00
err := parser.getAllGoFileInfoFromDeps(&t.Root.Deps[i], parser.ParseDependency)
2024-05-14 13:07:09 +00:00
if err != nil {
2024-05-14 13:07:09 +00:00
return err
2024-05-14 13:07:09 +00:00
}
2024-05-14 13:07:09 +00:00
}
2024-05-14 13:07:09 +00:00
}
2024-05-14 13:07:09 +00:00
}
err = parser.ParseGeneralAPIInfo(absMainAPIFilePath)
2024-05-14 13:07:09 +00:00
if err != nil {
2024-05-14 13:07:09 +00:00
return err
2024-05-14 13:07:09 +00:00
}
parser.parsedSchemas, err = parser.packages.ParseTypes()
2024-05-14 13:07:09 +00:00
if err != nil {
2024-05-14 13:07:09 +00:00
return err
2024-05-14 13:07:09 +00:00
}
err = parser.packages.RangeFiles(parser.ParseRouterAPIInfo)
2024-05-14 13:07:09 +00:00
if err != nil {
2024-05-14 13:07:09 +00:00
return err
2024-05-14 13:07:09 +00:00
}
return parser.checkOperationIDUniqueness()
2024-05-14 13:07:09 +00:00
}
func getPkgName(searchDir string) (string, error) {
2024-05-14 13:07:09 +00:00
cmd := exec.Command("go", "list", "-f={{.ImportPath}}")
2024-05-14 13:07:09 +00:00
cmd.Dir = searchDir
var stdout, stderr strings.Builder
cmd.Stdout = &stdout
2024-05-14 13:07:09 +00:00
cmd.Stderr = &stderr
if err := cmd.Run(); err != nil {
2024-05-14 13:07:09 +00:00
return "", fmt.Errorf("execute go list command, %s, stdout:%s, stderr:%s", err, stdout.String(), stderr.String())
2024-05-14 13:07:09 +00:00
}
outStr, _ := stdout.String(), stderr.String()
if outStr[0] == '_' { // will shown like _/{GOPATH}/src/{YOUR_PACKAGE} when NOT enable GO MODULE.
2024-05-14 13:07:09 +00:00
outStr = strings.TrimPrefix(outStr, "_"+build.Default.GOPATH+"/src/")
2024-05-14 13:07:09 +00:00
}
f := strings.Split(outStr, "\n")
outStr = f[0]
return outStr, nil
2024-05-14 13:07:09 +00:00
}
// ParseGeneralAPIInfo parses general api info for given mainAPIFile path.
2024-05-14 13:07:09 +00:00
func (parser *Parser) ParseGeneralAPIInfo(mainAPIFile string) error {
2024-05-14 13:07:09 +00:00
fileTree, err := goparser.ParseFile(token.NewFileSet(), mainAPIFile, nil, goparser.ParseComments)
2024-05-14 13:07:09 +00:00
if err != nil {
2024-05-14 13:07:09 +00:00
return fmt.Errorf("cannot parse source files %s: %s", mainAPIFile, err)
2024-05-14 13:07:09 +00:00
}
parser.swagger.Swagger = "2.0"
for _, comment := range fileTree.Comments {
2024-05-14 13:07:09 +00:00
comments := strings.Split(comment.Text(), "\n")
2024-05-14 13:07:09 +00:00
if !isGeneralAPIComment(comments) {
2024-05-14 13:07:09 +00:00
continue
2024-05-14 13:07:09 +00:00
}
err = parseGeneralAPIInfo(parser, comments)
2024-05-14 13:07:09 +00:00
if err != nil {
2024-05-14 13:07:09 +00:00
return err
2024-05-14 13:07:09 +00:00
}
2024-05-14 13:07:09 +00:00
}
return nil
2024-05-14 13:07:09 +00:00
}
func parseGeneralAPIInfo(parser *Parser, comments []string) error {
2024-05-14 13:07:09 +00:00
previousAttribute := ""
// parsing classic meta data model
2024-05-14 13:07:09 +00:00
for line := 0; line < len(comments); line++ {
2024-05-14 13:07:09 +00:00
commentLine := comments[line]
2024-05-14 13:07:09 +00:00
commentLine = strings.TrimSpace(commentLine)
2024-05-14 13:07:09 +00:00
if len(commentLine) == 0 {
2024-05-14 13:07:09 +00:00
continue
2024-05-14 13:07:09 +00:00
}
2024-05-14 13:07:09 +00:00
fields := FieldsByAnySpace(commentLine, 2)
attribute := fields[0]
2024-05-14 13:07:09 +00:00
var value string
2024-05-14 13:07:09 +00:00
if len(fields) > 1 {
2024-05-14 13:07:09 +00:00
value = fields[1]
2024-05-14 13:07:09 +00:00
}
switch attr := strings.ToLower(attribute); attr {
2024-05-14 13:07:09 +00:00
case versionAttr, titleAttr, tosAttr, licNameAttr, licURLAttr, conNameAttr, conURLAttr, conEmailAttr:
2024-05-14 13:07:09 +00:00
setSwaggerInfo(parser.swagger, attr, value)
2024-05-14 13:07:09 +00:00
case descriptionAttr:
2024-05-14 13:07:09 +00:00
if previousAttribute == attribute {
2024-05-14 13:07:09 +00:00
parser.swagger.Info.Description += "\n" + value
continue
2024-05-14 13:07:09 +00:00
}
setSwaggerInfo(parser.swagger, attr, value)
2024-05-14 13:07:09 +00:00
case descriptionMarkdownAttr:
2024-05-14 13:07:09 +00:00
commentInfo, err := getMarkdownForTag("api", parser.markdownFileDir)
2024-05-14 13:07:09 +00:00
if err != nil {
2024-05-14 13:07:09 +00:00
return err
2024-05-14 13:07:09 +00:00
}
setSwaggerInfo(parser.swagger, descriptionAttr, string(commentInfo))
case "@host":
2024-05-14 13:07:09 +00:00
parser.swagger.Host = value
2024-05-14 13:07:09 +00:00
case "@hoststate":
2024-05-14 13:07:09 +00:00
fields = FieldsByAnySpace(commentLine, 3)
2024-05-14 13:07:09 +00:00
if len(fields) != 3 {
2024-05-14 13:07:09 +00:00
return fmt.Errorf("%s needs 3 arguments", attribute)
2024-05-14 13:07:09 +00:00
}
2024-05-14 13:07:09 +00:00
if parser.HostState == fields[1] {
2024-05-14 13:07:09 +00:00
parser.swagger.Host = fields[2]
2024-05-14 13:07:09 +00:00
}
2024-05-14 13:07:09 +00:00
case "@basepath":
2024-05-14 13:07:09 +00:00
parser.swagger.BasePath = value
case acceptAttr:
2024-05-14 13:07:09 +00:00
err := parser.ParseAcceptComment(value)
2024-05-14 13:07:09 +00:00
if err != nil {
2024-05-14 13:07:09 +00:00
return err
2024-05-14 13:07:09 +00:00
}
2024-05-14 13:07:09 +00:00
case produceAttr:
2024-05-14 13:07:09 +00:00
err := parser.ParseProduceComment(value)
2024-05-14 13:07:09 +00:00
if err != nil {
2024-05-14 13:07:09 +00:00
return err
2024-05-14 13:07:09 +00:00
}
2024-05-14 13:07:09 +00:00
case "@schemes":
2024-05-14 13:07:09 +00:00
parser.swagger.Schemes = strings.Split(value, " ")
2024-05-14 13:07:09 +00:00
case "@tag.name":
2024-05-14 13:07:09 +00:00
parser.swagger.Tags = append(parser.swagger.Tags, spec.Tag{
2024-05-14 13:07:09 +00:00
TagProps: spec.TagProps{
2024-05-14 13:07:09 +00:00
Name: value,
},
})
2024-05-14 13:07:09 +00:00
case "@tag.description":
2024-05-14 13:07:09 +00:00
tag := parser.swagger.Tags[len(parser.swagger.Tags)-1]
2024-05-14 13:07:09 +00:00
tag.TagProps.Description = value
2024-05-14 13:07:09 +00:00
replaceLastTag(parser.swagger.Tags, tag)
2024-05-14 13:07:09 +00:00
case "@tag.description.markdown":
2024-05-14 13:07:09 +00:00
tag := parser.swagger.Tags[len(parser.swagger.Tags)-1]
commentInfo, err := getMarkdownForTag(tag.TagProps.Name, parser.markdownFileDir)
2024-05-14 13:07:09 +00:00
if err != nil {
2024-05-14 13:07:09 +00:00
return err
2024-05-14 13:07:09 +00:00
}
tag.TagProps.Description = string(commentInfo)
2024-05-14 13:07:09 +00:00
replaceLastTag(parser.swagger.Tags, tag)
2024-05-14 13:07:09 +00:00
case "@tag.docs.url":
2024-05-14 13:07:09 +00:00
tag := parser.swagger.Tags[len(parser.swagger.Tags)-1]
2024-05-14 13:07:09 +00:00
tag.TagProps.ExternalDocs = &spec.ExternalDocumentation{
URL: value,
2024-05-14 13:07:09 +00:00
Description: "",
}
replaceLastTag(parser.swagger.Tags, tag)
2024-05-14 13:07:09 +00:00
case "@tag.docs.description":
2024-05-14 13:07:09 +00:00
tag := parser.swagger.Tags[len(parser.swagger.Tags)-1]
2024-05-14 13:07:09 +00:00
if tag.TagProps.ExternalDocs == nil {
2024-05-14 13:07:09 +00:00
return fmt.Errorf("%s needs to come after a @tags.docs.url", attribute)
2024-05-14 13:07:09 +00:00
}
tag.TagProps.ExternalDocs.Description = value
2024-05-14 13:07:09 +00:00
replaceLastTag(parser.swagger.Tags, tag)
case secBasicAttr, secAPIKeyAttr, secApplicationAttr, secImplicitAttr, secPasswordAttr, secAccessCodeAttr:
2024-05-14 13:07:09 +00:00
scheme, err := parseSecAttributes(attribute, comments, &line)
2024-05-14 13:07:09 +00:00
if err != nil {
2024-05-14 13:07:09 +00:00
return err
2024-05-14 13:07:09 +00:00
}
parser.swagger.SecurityDefinitions[value] = scheme
case securityAttr:
2024-05-14 13:07:09 +00:00
parser.swagger.Security = append(parser.swagger.Security, parseSecurity(value))
case "@query.collection.format":
2024-05-14 13:07:09 +00:00
parser.collectionFormatInQuery = TransToValidCollectionFormat(value)
case extDocsDescAttr, extDocsURLAttr:
2024-05-14 13:07:09 +00:00
if parser.swagger.ExternalDocs == nil {
2024-05-14 13:07:09 +00:00
parser.swagger.ExternalDocs = new(spec.ExternalDocumentation)
2024-05-14 13:07:09 +00:00
}
2024-05-14 13:07:09 +00:00
switch attr {
2024-05-14 13:07:09 +00:00
case extDocsDescAttr:
2024-05-14 13:07:09 +00:00
parser.swagger.ExternalDocs.Description = value
2024-05-14 13:07:09 +00:00
case extDocsURLAttr:
2024-05-14 13:07:09 +00:00
parser.swagger.ExternalDocs.URL = value
2024-05-14 13:07:09 +00:00
}
default:
2024-05-14 13:07:09 +00:00
if strings.HasPrefix(attribute, "@x-") {
2024-05-14 13:07:09 +00:00
extensionName := attribute[1:]
extExistsInSecurityDef := false
2024-05-14 13:07:09 +00:00
// for each security definition
2024-05-14 13:07:09 +00:00
for _, v := range parser.swagger.SecurityDefinitions {
2024-05-14 13:07:09 +00:00
// check if extension exists
2024-05-14 13:07:09 +00:00
_, extExistsInSecurityDef = v.VendorExtensible.Extensions.GetString(extensionName)
2024-05-14 13:07:09 +00:00
// if it exists in at least one, then we stop iterating
2024-05-14 13:07:09 +00:00
if extExistsInSecurityDef {
2024-05-14 13:07:09 +00:00
break
2024-05-14 13:07:09 +00:00
}
2024-05-14 13:07:09 +00:00
}
// if it is present on security def, don't add it again
2024-05-14 13:07:09 +00:00
if extExistsInSecurityDef {
2024-05-14 13:07:09 +00:00
break
2024-05-14 13:07:09 +00:00
}
if len(value) == 0 {
2024-05-14 13:07:09 +00:00
return fmt.Errorf("annotation %s need a value", attribute)
2024-05-14 13:07:09 +00:00
}
var valueJSON interface{}
2024-05-14 13:07:09 +00:00
err := json.Unmarshal([]byte(value), &valueJSON)
2024-05-14 13:07:09 +00:00
if err != nil {
2024-05-14 13:07:09 +00:00
return fmt.Errorf("annotation %s need a valid json value", attribute)
2024-05-14 13:07:09 +00:00
}
if strings.Contains(extensionName, "logo") {
2024-05-14 13:07:09 +00:00
parser.swagger.Info.Extensions.Add(extensionName, valueJSON)
2024-05-14 13:07:09 +00:00
} else {
2024-05-14 13:07:09 +00:00
if parser.swagger.Extensions == nil {
2024-05-14 13:07:09 +00:00
parser.swagger.Extensions = make(map[string]interface{})
2024-05-14 13:07:09 +00:00
}
parser.swagger.Extensions[attribute[1:]] = valueJSON
2024-05-14 13:07:09 +00:00
}
2024-05-14 13:07:09 +00:00
}
2024-05-14 13:07:09 +00:00
}
previousAttribute = attribute
2024-05-14 13:07:09 +00:00
}
return nil
2024-05-14 13:07:09 +00:00
}
func setSwaggerInfo(swagger *spec.Swagger, attribute, value string) {
2024-05-14 13:07:09 +00:00
switch attribute {
2024-05-14 13:07:09 +00:00
case versionAttr:
2024-05-14 13:07:09 +00:00
swagger.Info.Version = value
2024-05-14 13:07:09 +00:00
case titleAttr:
2024-05-14 13:07:09 +00:00
swagger.Info.Title = value
2024-05-14 13:07:09 +00:00
case tosAttr:
2024-05-14 13:07:09 +00:00
swagger.Info.TermsOfService = value
2024-05-14 13:07:09 +00:00
case descriptionAttr:
2024-05-14 13:07:09 +00:00
swagger.Info.Description = value
2024-05-14 13:07:09 +00:00
case conNameAttr:
2024-05-14 13:07:09 +00:00
swagger.Info.Contact.Name = value
2024-05-14 13:07:09 +00:00
case conEmailAttr:
2024-05-14 13:07:09 +00:00
swagger.Info.Contact.Email = value
2024-05-14 13:07:09 +00:00
case conURLAttr:
2024-05-14 13:07:09 +00:00
swagger.Info.Contact.URL = value
2024-05-14 13:07:09 +00:00
case licNameAttr:
2024-05-14 13:07:09 +00:00
swagger.Info.License = initIfEmpty(swagger.Info.License)
2024-05-14 13:07:09 +00:00
swagger.Info.License.Name = value
2024-05-14 13:07:09 +00:00
case licURLAttr:
2024-05-14 13:07:09 +00:00
swagger.Info.License = initIfEmpty(swagger.Info.License)
2024-05-14 13:07:09 +00:00
swagger.Info.License.URL = value
2024-05-14 13:07:09 +00:00
}
2024-05-14 13:07:09 +00:00
}
func parseSecAttributes(context string, lines []string, index *int) (*spec.SecurityScheme, error) {
2024-05-14 13:07:09 +00:00
const (
in = "@in"
name = "@name"
descriptionAttr = "@description"
tokenURL = "@tokenurl"
2024-05-14 13:07:09 +00:00
authorizationURL = "@authorizationurl"
)
var search []string
attribute := strings.ToLower(FieldsByAnySpace(lines[*index], 2)[0])
2024-05-14 13:07:09 +00:00
switch attribute {
2024-05-14 13:07:09 +00:00
case secBasicAttr:
2024-05-14 13:07:09 +00:00
return spec.BasicAuth(), nil
2024-05-14 13:07:09 +00:00
case secAPIKeyAttr:
2024-05-14 13:07:09 +00:00
search = []string{in, name}
2024-05-14 13:07:09 +00:00
case secApplicationAttr, secPasswordAttr:
2024-05-14 13:07:09 +00:00
search = []string{tokenURL}
2024-05-14 13:07:09 +00:00
case secImplicitAttr:
2024-05-14 13:07:09 +00:00
search = []string{authorizationURL}
2024-05-14 13:07:09 +00:00
case secAccessCodeAttr:
2024-05-14 13:07:09 +00:00
search = []string{tokenURL, authorizationURL}
2024-05-14 13:07:09 +00:00
}
// For the first line we get the attributes in the context parameter, so we skip to the next one
2024-05-14 13:07:09 +00:00
*index++
attrMap, scopes := make(map[string]string), make(map[string]string)
2024-05-14 13:07:09 +00:00
extensions, description := make(map[string]interface{}), ""
loopline:
2024-05-14 13:07:09 +00:00
for ; *index < len(lines); *index++ {
2024-05-14 13:07:09 +00:00
v := strings.TrimSpace(lines[*index])
2024-05-14 13:07:09 +00:00
if len(v) == 0 {
2024-05-14 13:07:09 +00:00
continue
2024-05-14 13:07:09 +00:00
}
fields := FieldsByAnySpace(v, 2)
2024-05-14 13:07:09 +00:00
securityAttr := strings.ToLower(fields[0])
2024-05-14 13:07:09 +00:00
var value string
2024-05-14 13:07:09 +00:00
if len(fields) > 1 {
2024-05-14 13:07:09 +00:00
value = fields[1]
2024-05-14 13:07:09 +00:00
}
for _, findterm := range search {
2024-05-14 13:07:09 +00:00
if securityAttr == findterm {
2024-05-14 13:07:09 +00:00
attrMap[securityAttr] = value
2024-05-14 13:07:09 +00:00
continue loopline
2024-05-14 13:07:09 +00:00
}
2024-05-14 13:07:09 +00:00
}
if isExists, err := isExistsScope(securityAttr); err != nil {
2024-05-14 13:07:09 +00:00
return nil, err
2024-05-14 13:07:09 +00:00
} else if isExists {
2024-05-14 13:07:09 +00:00
scopes[securityAttr[len(scopeAttrPrefix):]] = value
2024-05-14 13:07:09 +00:00
continue
2024-05-14 13:07:09 +00:00
}
if strings.HasPrefix(securityAttr, "@x-") {
2024-05-14 13:07:09 +00:00
// Add the custom attribute without the @
2024-05-14 13:07:09 +00:00
extensions[securityAttr[1:]] = value
2024-05-14 13:07:09 +00:00
continue
2024-05-14 13:07:09 +00:00
}
// Not mandatory field
2024-05-14 13:07:09 +00:00
if securityAttr == descriptionAttr {
2024-05-14 13:07:09 +00:00
description = value
2024-05-14 13:07:09 +00:00
}
// next securityDefinitions
2024-05-14 13:07:09 +00:00
if strings.Index(securityAttr, "@securitydefinitions.") == 0 {
2024-05-14 13:07:09 +00:00
// Go back to the previous line and break
2024-05-14 13:07:09 +00:00
*index--
break
2024-05-14 13:07:09 +00:00
}
2024-05-14 13:07:09 +00:00
}
if len(attrMap) != len(search) {
2024-05-14 13:07:09 +00:00
return nil, fmt.Errorf("%s is %v required", context, search)
2024-05-14 13:07:09 +00:00
}
var scheme *spec.SecurityScheme
switch attribute {
2024-05-14 13:07:09 +00:00
case secAPIKeyAttr:
2024-05-14 13:07:09 +00:00
scheme = spec.APIKeyAuth(attrMap[name], attrMap[in])
2024-05-14 13:07:09 +00:00
case secApplicationAttr:
2024-05-14 13:07:09 +00:00
scheme = spec.OAuth2Application(attrMap[tokenURL])
2024-05-14 13:07:09 +00:00
case secImplicitAttr:
2024-05-14 13:07:09 +00:00
scheme = spec.OAuth2Implicit(attrMap[authorizationURL])
2024-05-14 13:07:09 +00:00
case secPasswordAttr:
2024-05-14 13:07:09 +00:00
scheme = spec.OAuth2Password(attrMap[tokenURL])
2024-05-14 13:07:09 +00:00
case secAccessCodeAttr:
2024-05-14 13:07:09 +00:00
scheme = spec.OAuth2AccessToken(attrMap[authorizationURL], attrMap[tokenURL])
2024-05-14 13:07:09 +00:00
}
scheme.Description = description
for extKey, extValue := range extensions {
2024-05-14 13:07:09 +00:00
scheme.AddExtension(extKey, extValue)
2024-05-14 13:07:09 +00:00
}
for scope, scopeDescription := range scopes {
2024-05-14 13:07:09 +00:00
scheme.AddScope(scope, scopeDescription)
2024-05-14 13:07:09 +00:00
}
return scheme, nil
2024-05-14 13:07:09 +00:00
}
func parseSecurity(commentLine string) map[string][]string {
2024-05-14 13:07:09 +00:00
securityMap := make(map[string][]string)
for _, securityOption := range strings.Split(commentLine, "||") {
2024-05-14 13:07:09 +00:00
securityOption = strings.TrimSpace(securityOption)
left, right := strings.Index(securityOption, "["), strings.Index(securityOption, "]")
if !(left == -1 && right == -1) {
2024-05-14 13:07:09 +00:00
scopes := securityOption[left+1 : right]
var options []string
for _, scope := range strings.Split(scopes, ",") {
2024-05-14 13:07:09 +00:00
options = append(options, strings.TrimSpace(scope))
2024-05-14 13:07:09 +00:00
}
securityKey := securityOption[0:left]
2024-05-14 13:07:09 +00:00
securityMap[securityKey] = append(securityMap[securityKey], options...)
2024-05-14 13:07:09 +00:00
} else {
2024-05-14 13:07:09 +00:00
securityKey := strings.TrimSpace(securityOption)
2024-05-14 13:07:09 +00:00
securityMap[securityKey] = []string{}
2024-05-14 13:07:09 +00:00
}
2024-05-14 13:07:09 +00:00
}
return securityMap
2024-05-14 13:07:09 +00:00
}
func initIfEmpty(license *spec.License) *spec.License {
2024-05-14 13:07:09 +00:00
if license == nil {
2024-05-14 13:07:09 +00:00
return new(spec.License)
2024-05-14 13:07:09 +00:00
}
return license
2024-05-14 13:07:09 +00:00
}
// ParseAcceptComment parses comment for given `accept` comment string.
2024-05-14 13:07:09 +00:00
func (parser *Parser) ParseAcceptComment(commentLine string) error {
2024-05-14 13:07:09 +00:00
return parseMimeTypeList(commentLine, &parser.swagger.Consumes, "%v accept type can't be accepted")
2024-05-14 13:07:09 +00:00
}
// ParseProduceComment parses comment for given `produce` comment string.
2024-05-14 13:07:09 +00:00
func (parser *Parser) ParseProduceComment(commentLine string) error {
2024-05-14 13:07:09 +00:00
return parseMimeTypeList(commentLine, &parser.swagger.Produces, "%v produce type can't be accepted")
2024-05-14 13:07:09 +00:00
}
func isGeneralAPIComment(comments []string) bool {
2024-05-14 13:07:09 +00:00
for _, commentLine := range comments {
2024-05-14 13:07:09 +00:00
commentLine = strings.TrimSpace(commentLine)
2024-05-14 13:07:09 +00:00
if len(commentLine) == 0 {
2024-05-14 13:07:09 +00:00
continue
2024-05-14 13:07:09 +00:00
}
2024-05-14 13:07:09 +00:00
attribute := strings.ToLower(FieldsByAnySpace(commentLine, 2)[0])
2024-05-14 13:07:09 +00:00
switch attribute {
2024-05-14 13:07:09 +00:00
// The @summary, @router, @success, @failure annotation belongs to Operation
2024-05-14 13:07:09 +00:00
case summaryAttr, routerAttr, successAttr, failureAttr, responseAttr:
2024-05-14 13:07:09 +00:00
return false
2024-05-14 13:07:09 +00:00
}
2024-05-14 13:07:09 +00:00
}
return true
2024-05-14 13:07:09 +00:00
}
func getMarkdownForTag(tagName string, dirPath string) ([]byte, error) {
2024-05-14 13:07:09 +00:00
dirEntries, err := os.ReadDir(dirPath)
2024-05-14 13:07:09 +00:00
if err != nil {
2024-05-14 13:07:09 +00:00
return nil, err
2024-05-14 13:07:09 +00:00
}
for _, entry := range dirEntries {
2024-05-14 13:07:09 +00:00
if entry.IsDir() {
2024-05-14 13:07:09 +00:00
continue
2024-05-14 13:07:09 +00:00
}
fileName := entry.Name()
if !strings.Contains(fileName, ".md") {
2024-05-14 13:07:09 +00:00
continue
2024-05-14 13:07:09 +00:00
}
if strings.Contains(fileName, tagName) {
2024-05-14 13:07:09 +00:00
fullPath := filepath.Join(dirPath, fileName)
commentInfo, err := os.ReadFile(fullPath)
2024-05-14 13:07:09 +00:00
if err != nil {
2024-05-14 13:07:09 +00:00
return nil, fmt.Errorf("Failed to read markdown file %s error: %s ", fullPath, err)
2024-05-14 13:07:09 +00:00
}
return commentInfo, nil
2024-05-14 13:07:09 +00:00
}
2024-05-14 13:07:09 +00:00
}
return nil, fmt.Errorf("Unable to find markdown file for tag %s in the given directory", tagName)
2024-05-14 13:07:09 +00:00
}
func isExistsScope(scope string) (bool, error) {
2024-05-14 13:07:09 +00:00
s := strings.Fields(scope)
2024-05-14 13:07:09 +00:00
for _, v := range s {
2024-05-14 13:07:09 +00:00
if strings.HasPrefix(v, scopeAttrPrefix) {
2024-05-14 13:07:09 +00:00
if strings.Contains(v, ",") {
2024-05-14 13:07:09 +00:00
return false, fmt.Errorf("@scope can't use comma(,) get=" + v)
2024-05-14 13:07:09 +00:00
}
2024-05-14 13:07:09 +00:00
}
2024-05-14 13:07:09 +00:00
}
return strings.HasPrefix(scope, scopeAttrPrefix), nil
2024-05-14 13:07:09 +00:00
}
func getTagsFromComment(comment string) (tags []string) {
2024-05-14 13:07:09 +00:00
commentLine := strings.TrimSpace(strings.TrimLeft(comment, "/"))
2024-05-14 13:07:09 +00:00
if len(commentLine) == 0 {
2024-05-14 13:07:09 +00:00
return nil
2024-05-14 13:07:09 +00:00
}
attribute := strings.Fields(commentLine)[0]
2024-05-14 13:07:09 +00:00
lineRemainder, lowerAttribute := strings.TrimSpace(commentLine[len(attribute):]), strings.ToLower(attribute)
if lowerAttribute == tagsAttr {
2024-05-14 13:07:09 +00:00
for _, tag := range strings.Split(lineRemainder, ",") {
2024-05-14 13:07:09 +00:00
tags = append(tags, strings.TrimSpace(tag))
2024-05-14 13:07:09 +00:00
}
2024-05-14 13:07:09 +00:00
}
2024-05-14 13:07:09 +00:00
return
}
func (parser *Parser) matchTags(comments []*ast.Comment) (match bool) {
2024-05-14 13:07:09 +00:00
if len(parser.tags) == 0 {
2024-05-14 13:07:09 +00:00
return true
2024-05-14 13:07:09 +00:00
}
match = false
2024-05-14 13:07:09 +00:00
for _, comment := range comments {
2024-05-14 13:07:09 +00:00
for _, tag := range getTagsFromComment(comment.Text) {
2024-05-14 13:07:09 +00:00
if _, has := parser.tags["!"+tag]; has {
2024-05-14 13:07:09 +00:00
return false
2024-05-14 13:07:09 +00:00
}
2024-05-14 13:07:09 +00:00
if _, has := parser.tags[tag]; has {
2024-05-14 13:07:09 +00:00
match = true // keep iterating as it may contain a tag that is excluded
2024-05-14 13:07:09 +00:00
}
2024-05-14 13:07:09 +00:00
}
2024-05-14 13:07:09 +00:00
}
if !match {
2024-05-14 13:07:09 +00:00
// If all tags are negation then we should return true
2024-05-14 13:07:09 +00:00
for key := range parser.tags {
2024-05-14 13:07:09 +00:00
if key[0] != '!' {
2024-05-14 13:07:09 +00:00
return false
2024-05-14 13:07:09 +00:00
}
2024-05-14 13:07:09 +00:00
}
2024-05-14 13:07:09 +00:00
}
2024-05-14 13:07:09 +00:00
return true
2024-05-14 13:07:09 +00:00
}
func matchExtension(extensionToMatch string, comments []*ast.Comment) (match bool) {
2024-05-14 13:07:09 +00:00
if len(extensionToMatch) != 0 {
2024-05-14 13:07:09 +00:00
for _, comment := range comments {
2024-05-14 13:07:09 +00:00
commentLine := strings.TrimSpace(strings.TrimLeft(comment.Text, "/"))
2024-05-14 13:07:09 +00:00
fields := FieldsByAnySpace(commentLine, 2)
2024-05-14 13:07:09 +00:00
if len(fields) > 0 {
2024-05-14 13:07:09 +00:00
lowerAttribute := strings.ToLower(fields[0])
if lowerAttribute == fmt.Sprintf("@x-%s", strings.ToLower(extensionToMatch)) {
2024-05-14 13:07:09 +00:00
return true
2024-05-14 13:07:09 +00:00
}
2024-05-14 13:07:09 +00:00
}
2024-05-14 13:07:09 +00:00
}
2024-05-14 13:07:09 +00:00
return false
2024-05-14 13:07:09 +00:00
}
2024-05-14 13:07:09 +00:00
return true
2024-05-14 13:07:09 +00:00
}
// ParseRouterAPIInfo parses router api info for given astFile.
2024-05-14 13:07:09 +00:00
func (parser *Parser) ParseRouterAPIInfo(fileInfo *AstFileInfo) error {
2024-05-14 13:07:09 +00:00
DeclsLoop:
2024-05-14 13:07:09 +00:00
for _, astDescription := range fileInfo.File.Decls {
2024-05-14 13:07:09 +00:00
if (fileInfo.ParseFlag & ParseOperations) == ParseNone {
2024-05-14 13:07:09 +00:00
continue
2024-05-14 13:07:09 +00:00
}
2024-05-14 13:07:09 +00:00
astDeclaration, ok := astDescription.(*ast.FuncDecl)
2024-05-14 13:07:09 +00:00
if ok && astDeclaration.Doc != nil && astDeclaration.Doc.List != nil {
2024-05-14 13:07:09 +00:00
if parser.matchTags(astDeclaration.Doc.List) &&
2024-05-14 13:07:09 +00:00
matchExtension(parser.parseExtension, astDeclaration.Doc.List) {
2024-05-14 13:07:09 +00:00
// for per 'function' comment, create a new 'Operation' object
2024-05-14 13:07:09 +00:00
operation := NewOperation(parser, SetCodeExampleFilesDirectory(parser.codeExampleFilesDir))
2024-05-14 13:07:09 +00:00
for _, comment := range astDeclaration.Doc.List {
2024-05-14 13:07:09 +00:00
err := operation.ParseComment(comment.Text, fileInfo.File)
2024-05-14 13:07:09 +00:00
if err != nil {
2024-05-14 13:07:09 +00:00
return fmt.Errorf("ParseComment error in file %s :%+v", fileInfo.Path, err)
2024-05-14 13:07:09 +00:00
}
2024-05-14 13:07:09 +00:00
if operation.State != "" && operation.State != parser.HostState {
2024-05-14 13:07:09 +00:00
continue DeclsLoop
2024-05-14 13:07:09 +00:00
}
2024-05-14 13:07:09 +00:00
}
2024-05-14 13:07:09 +00:00
err := processRouterOperation(parser, operation)
2024-05-14 13:07:09 +00:00
if err != nil {
2024-05-14 13:07:09 +00:00
return err
2024-05-14 13:07:09 +00:00
}
2024-05-14 13:07:09 +00:00
}
2024-05-14 13:07:09 +00:00
}
2024-05-14 13:07:09 +00:00
}
return nil
2024-05-14 13:07:09 +00:00
}
func refRouteMethodOp(item *spec.PathItem, method string) (op **spec.Operation) {
2024-05-14 13:07:09 +00:00
switch method {
2024-05-14 13:07:09 +00:00
case http.MethodGet:
2024-05-14 13:07:09 +00:00
op = &item.Get
2024-05-14 13:07:09 +00:00
case http.MethodPost:
2024-05-14 13:07:09 +00:00
op = &item.Post
2024-05-14 13:07:09 +00:00
case http.MethodDelete:
2024-05-14 13:07:09 +00:00
op = &item.Delete
2024-05-14 13:07:09 +00:00
case http.MethodPut:
2024-05-14 13:07:09 +00:00
op = &item.Put
2024-05-14 13:07:09 +00:00
case http.MethodPatch:
2024-05-14 13:07:09 +00:00
op = &item.Patch
2024-05-14 13:07:09 +00:00
case http.MethodHead:
2024-05-14 13:07:09 +00:00
op = &item.Head
2024-05-14 13:07:09 +00:00
case http.MethodOptions:
2024-05-14 13:07:09 +00:00
op = &item.Options
2024-05-14 13:07:09 +00:00
}
return
2024-05-14 13:07:09 +00:00
}
func processRouterOperation(parser *Parser, operation *Operation) error {
2024-05-14 13:07:09 +00:00
for _, routeProperties := range operation.RouterProperties {
2024-05-14 13:07:09 +00:00
var (
pathItem spec.PathItem
ok bool
2024-05-14 13:07:09 +00:00
)
pathItem, ok = parser.swagger.Paths.Paths[routeProperties.Path]
2024-05-14 13:07:09 +00:00
if !ok {
2024-05-14 13:07:09 +00:00
pathItem = spec.PathItem{}
2024-05-14 13:07:09 +00:00
}
op := refRouteMethodOp(&pathItem, routeProperties.HTTPMethod)
// check if we already have an operation for this path and method
2024-05-14 13:07:09 +00:00
if *op != nil {
2024-05-14 13:07:09 +00:00
err := fmt.Errorf("route %s %s is declared multiple times", routeProperties.HTTPMethod, routeProperties.Path)
2024-05-14 13:07:09 +00:00
if parser.Strict {
2024-05-14 13:07:09 +00:00
return err
2024-05-14 13:07:09 +00:00
}
parser.debug.Printf("warning: %s\n", err)
2024-05-14 13:07:09 +00:00
}
if len(operation.RouterProperties) > 1 {
2024-05-14 13:07:09 +00:00
newOp := *operation
2024-05-14 13:07:09 +00:00
var validParams []spec.Parameter
2024-05-14 13:07:09 +00:00
for _, param := range newOp.Operation.OperationProps.Parameters {
2024-05-14 13:07:09 +00:00
if param.In == "path" && !strings.Contains(routeProperties.Path, param.Name) {
2024-05-14 13:07:09 +00:00
// This path param is not actually contained in the path, skip adding it to the final params
2024-05-14 13:07:09 +00:00
continue
2024-05-14 13:07:09 +00:00
}
2024-05-14 13:07:09 +00:00
validParams = append(validParams, param)
2024-05-14 13:07:09 +00:00
}
2024-05-14 13:07:09 +00:00
newOp.Operation.OperationProps.Parameters = validParams
2024-05-14 13:07:09 +00:00
*op = &newOp.Operation
2024-05-14 13:07:09 +00:00
} else {
2024-05-14 13:07:09 +00:00
*op = &operation.Operation
2024-05-14 13:07:09 +00:00
}
if routeProperties.Deprecated {
2024-05-14 13:07:09 +00:00
(*op).Deprecated = routeProperties.Deprecated
2024-05-14 13:07:09 +00:00
}
parser.swagger.Paths.Paths[routeProperties.Path] = pathItem
2024-05-14 13:07:09 +00:00
}
return nil
2024-05-14 13:07:09 +00:00
}
func convertFromSpecificToPrimitive(typeName string) (string, error) {
2024-05-14 13:07:09 +00:00
name := typeName
2024-05-14 13:07:09 +00:00
if strings.ContainsRune(name, '.') {
2024-05-14 13:07:09 +00:00
name = strings.Split(name, ".")[1]
2024-05-14 13:07:09 +00:00
}
switch strings.ToUpper(name) {
2024-05-14 13:07:09 +00:00
case "TIME", "OBJECTID", "UUID":
2024-05-14 13:07:09 +00:00
return STRING, nil
2024-05-14 13:07:09 +00:00
case "DECIMAL":
2024-05-14 13:07:09 +00:00
return NUMBER, nil
2024-05-14 13:07:09 +00:00
}
return typeName, ErrFailedConvertPrimitiveType
2024-05-14 13:07:09 +00:00
}
func (parser *Parser) getTypeSchema(typeName string, file *ast.File, ref bool) (*spec.Schema, error) {
2024-05-14 13:07:09 +00:00
if override, ok := parser.Overrides[typeName]; ok {
2024-05-14 13:07:09 +00:00
parser.debug.Printf("Override detected for %s: using %s instead", typeName, override)
2024-05-14 13:07:09 +00:00
return parseObjectSchema(parser, override, file)
2024-05-14 13:07:09 +00:00
}
if IsInterfaceLike(typeName) {
2024-05-14 13:07:09 +00:00
return &spec.Schema{}, nil
2024-05-14 13:07:09 +00:00
}
2024-05-14 13:07:09 +00:00
if IsGolangPrimitiveType(typeName) {
2024-05-14 13:07:09 +00:00
return PrimitiveSchema(TransToValidSchemeType(typeName)), nil
2024-05-14 13:07:09 +00:00
}
schemaType, err := convertFromSpecificToPrimitive(typeName)
2024-05-14 13:07:09 +00:00
if err == nil {
2024-05-14 13:07:09 +00:00
return PrimitiveSchema(schemaType), nil
2024-05-14 13:07:09 +00:00
}
typeSpecDef := parser.packages.FindTypeSpec(typeName, file)
2024-05-14 13:07:09 +00:00
if typeSpecDef == nil {
2024-05-14 13:07:09 +00:00
return nil, fmt.Errorf("cannot find type definition: %s", typeName)
2024-05-14 13:07:09 +00:00
}
if override, ok := parser.Overrides[typeSpecDef.FullPath()]; ok {
2024-05-14 13:07:09 +00:00
if override == "" {
2024-05-14 13:07:09 +00:00
parser.debug.Printf("Override detected for %s: ignoring", typeSpecDef.FullPath())
return nil, ErrSkippedField
2024-05-14 13:07:09 +00:00
}
parser.debug.Printf("Override detected for %s: using %s instead", typeSpecDef.FullPath(), override)
separator := strings.LastIndex(override, ".")
2024-05-14 13:07:09 +00:00
if separator == -1 {
2024-05-14 13:07:09 +00:00
// treat as a swaggertype tag
2024-05-14 13:07:09 +00:00
parts := strings.Split(override, ",")
return BuildCustomSchema(parts)
2024-05-14 13:07:09 +00:00
}
typeSpecDef = parser.packages.findTypeSpec(override[0:separator], override[separator+1:])
2024-05-14 13:07:09 +00:00
}
schema, ok := parser.parsedSchemas[typeSpecDef]
2024-05-14 13:07:09 +00:00
if !ok {
2024-05-14 13:07:09 +00:00
var err error
schema, err = parser.ParseDefinition(typeSpecDef)
2024-05-14 13:07:09 +00:00
if err != nil {
2024-05-14 13:07:09 +00:00
if err == ErrRecursiveParseStruct && ref {
2024-05-14 13:07:09 +00:00
return parser.getRefTypeSchema(typeSpecDef, schema), nil
2024-05-14 13:07:09 +00:00
}
2024-05-14 13:07:09 +00:00
return nil, fmt.Errorf("%s: %w", typeName, err)
2024-05-14 13:07:09 +00:00
}
2024-05-14 13:07:09 +00:00
}
if ref {
2024-05-14 13:07:09 +00:00
if IsComplexSchema(schema.Schema) {
2024-05-14 13:07:09 +00:00
return parser.getRefTypeSchema(typeSpecDef, schema), nil
2024-05-14 13:07:09 +00:00
}
2024-05-14 13:07:09 +00:00
// if it is a simple schema, just return a copy
2024-05-14 13:07:09 +00:00
newSchema := *schema.Schema
2024-05-14 13:07:09 +00:00
return &newSchema, nil
2024-05-14 13:07:09 +00:00
}
return schema.Schema, nil
2024-05-14 13:07:09 +00:00
}
func (parser *Parser) getRefTypeSchema(typeSpecDef *TypeSpecDef, schema *Schema) *spec.Schema {
2024-05-14 13:07:09 +00:00
_, ok := parser.outputSchemas[typeSpecDef]
2024-05-14 13:07:09 +00:00
if !ok {
2024-05-14 13:07:09 +00:00
parser.swagger.Definitions[schema.Name] = spec.Schema{}
if schema.Schema != nil {
2024-05-14 13:07:09 +00:00
parser.swagger.Definitions[schema.Name] = *schema.Schema
2024-05-14 13:07:09 +00:00
}
parser.outputSchemas[typeSpecDef] = schema
2024-05-14 13:07:09 +00:00
}
refSchema := RefSchema(schema.Name)
return refSchema
2024-05-14 13:07:09 +00:00
}
func (parser *Parser) isInStructStack(typeSpecDef *TypeSpecDef) bool {
2024-05-14 13:07:09 +00:00
for _, specDef := range parser.structStack {
2024-05-14 13:07:09 +00:00
if typeSpecDef == specDef {
2024-05-14 13:07:09 +00:00
return true
2024-05-14 13:07:09 +00:00
}
2024-05-14 13:07:09 +00:00
}
return false
2024-05-14 13:07:09 +00:00
}
// ParseDefinition parses given type spec that corresponds to the type under
2024-05-14 13:07:09 +00:00
// given name and package, and populates swagger schema definitions registry
2024-05-14 13:07:09 +00:00
// with a schema for the given type
2024-05-14 13:07:09 +00:00
func (parser *Parser) ParseDefinition(typeSpecDef *TypeSpecDef) (*Schema, error) {
2024-05-14 13:07:09 +00:00
typeName := typeSpecDef.TypeName()
2024-05-14 13:07:09 +00:00
schema, found := parser.parsedSchemas[typeSpecDef]
2024-05-14 13:07:09 +00:00
if found {
2024-05-14 13:07:09 +00:00
parser.debug.Printf("Skipping '%s', already parsed.", typeName)
return schema, nil
2024-05-14 13:07:09 +00:00
}
if parser.isInStructStack(typeSpecDef) {
2024-05-14 13:07:09 +00:00
parser.debug.Printf("Skipping '%s', recursion detected.", typeName)
return &Schema{
Name: typeName,
2024-05-14 13:07:09 +00:00
PkgPath: typeSpecDef.PkgPath,
Schema: PrimitiveSchema(OBJECT),
2024-05-14 13:07:09 +00:00
},
2024-05-14 13:07:09 +00:00
ErrRecursiveParseStruct
2024-05-14 13:07:09 +00:00
}
parser.structStack = append(parser.structStack, typeSpecDef)
parser.debug.Printf("Generating %s", typeName)
definition, err := parser.parseTypeExpr(typeSpecDef.File, typeSpecDef.TypeSpec.Type, false)
2024-05-14 13:07:09 +00:00
if err != nil {
2024-05-14 13:07:09 +00:00
parser.debug.Printf("Error parsing type definition '%s': %s", typeName, err)
2024-05-14 13:07:09 +00:00
return nil, err
2024-05-14 13:07:09 +00:00
}
if definition.Description == "" {
2024-05-14 13:07:09 +00:00
fillDefinitionDescription(definition, typeSpecDef.File, typeSpecDef)
2024-05-14 13:07:09 +00:00
}
if len(typeSpecDef.Enums) > 0 {
2024-05-14 13:07:09 +00:00
var varnames []string
2024-05-14 13:07:09 +00:00
var enumComments = make(map[string]string)
2024-05-14 13:07:09 +00:00
for _, value := range typeSpecDef.Enums {
2024-05-14 13:07:09 +00:00
definition.Enum = append(definition.Enum, value.Value)
2024-05-14 13:07:09 +00:00
varnames = append(varnames, value.key)
2024-05-14 13:07:09 +00:00
if len(value.Comment) > 0 {
2024-05-14 13:07:09 +00:00
enumComments[value.key] = value.Comment
2024-05-14 13:07:09 +00:00
}
2024-05-14 13:07:09 +00:00
}
2024-05-14 13:07:09 +00:00
if definition.Extensions == nil {
2024-05-14 13:07:09 +00:00
definition.Extensions = make(spec.Extensions)
2024-05-14 13:07:09 +00:00
}
2024-05-14 13:07:09 +00:00
definition.Extensions[enumVarNamesExtension] = varnames
2024-05-14 13:07:09 +00:00
if len(enumComments) > 0 {
2024-05-14 13:07:09 +00:00
definition.Extensions[enumCommentsExtension] = enumComments
2024-05-14 13:07:09 +00:00
}
2024-05-14 13:07:09 +00:00
}
sch := Schema{
Name: typeName,
2024-05-14 13:07:09 +00:00
PkgPath: typeSpecDef.PkgPath,
Schema: definition,
2024-05-14 13:07:09 +00:00
}
2024-05-14 13:07:09 +00:00
parser.parsedSchemas[typeSpecDef] = &sch
// update an empty schema as a result of recursion
2024-05-14 13:07:09 +00:00
s2, found := parser.outputSchemas[typeSpecDef]
2024-05-14 13:07:09 +00:00
if found {
2024-05-14 13:07:09 +00:00
parser.swagger.Definitions[s2.Name] = *definition
2024-05-14 13:07:09 +00:00
}
return &sch, nil
2024-05-14 13:07:09 +00:00
}
func fullTypeName(parts ...string) string {
2024-05-14 13:07:09 +00:00
return strings.Join(parts, ".")
2024-05-14 13:07:09 +00:00
}
// fillDefinitionDescription additionally fills fields in definition (spec.Schema)
2024-05-14 13:07:09 +00:00
// TODO: If .go file contains many types, it may work for a long time
2024-05-14 13:07:09 +00:00
func fillDefinitionDescription(definition *spec.Schema, file *ast.File, typeSpecDef *TypeSpecDef) {
2024-05-14 13:07:09 +00:00
if file == nil {
2024-05-14 13:07:09 +00:00
return
2024-05-14 13:07:09 +00:00
}
2024-05-14 13:07:09 +00:00
for _, astDeclaration := range file.Decls {
2024-05-14 13:07:09 +00:00
generalDeclaration, ok := astDeclaration.(*ast.GenDecl)
2024-05-14 13:07:09 +00:00
if !ok || generalDeclaration.Tok != token.TYPE {
2024-05-14 13:07:09 +00:00
continue
2024-05-14 13:07:09 +00:00
}
for _, astSpec := range generalDeclaration.Specs {
2024-05-14 13:07:09 +00:00
typeSpec, ok := astSpec.(*ast.TypeSpec)
2024-05-14 13:07:09 +00:00
if !ok || typeSpec != typeSpecDef.TypeSpec {
2024-05-14 13:07:09 +00:00
continue
2024-05-14 13:07:09 +00:00
}
definition.Description =
2024-05-14 13:07:09 +00:00
extractDeclarationDescription(typeSpec.Doc, typeSpec.Comment, generalDeclaration.Doc)
2024-05-14 13:07:09 +00:00
}
2024-05-14 13:07:09 +00:00
}
2024-05-14 13:07:09 +00:00
}
// extractDeclarationDescription gets first description
2024-05-14 13:07:09 +00:00
// from attribute descriptionAttr in commentGroups (ast.CommentGroup)
2024-05-14 13:07:09 +00:00
func extractDeclarationDescription(commentGroups ...*ast.CommentGroup) string {
2024-05-14 13:07:09 +00:00
var description string
for _, commentGroup := range commentGroups {
2024-05-14 13:07:09 +00:00
if commentGroup == nil {
2024-05-14 13:07:09 +00:00
continue
2024-05-14 13:07:09 +00:00
}
isHandlingDescription := false
for _, comment := range commentGroup.List {
2024-05-14 13:07:09 +00:00
commentText := strings.TrimSpace(strings.TrimLeft(comment.Text, "/"))
2024-05-14 13:07:09 +00:00
if len(commentText) == 0 {
2024-05-14 13:07:09 +00:00
continue
2024-05-14 13:07:09 +00:00
}
2024-05-14 13:07:09 +00:00
attribute := FieldsByAnySpace(commentText, 2)[0]
if strings.ToLower(attribute) != descriptionAttr {
2024-05-14 13:07:09 +00:00
if !isHandlingDescription {
2024-05-14 13:07:09 +00:00
continue
2024-05-14 13:07:09 +00:00
}
break
2024-05-14 13:07:09 +00:00
}
isHandlingDescription = true
2024-05-14 13:07:09 +00:00
description += " " + strings.TrimSpace(commentText[len(attribute):])
2024-05-14 13:07:09 +00:00
}
2024-05-14 13:07:09 +00:00
}
return strings.TrimLeft(description, " ")
2024-05-14 13:07:09 +00:00
}
// parseTypeExpr parses given type expression that corresponds to the type under
2024-05-14 13:07:09 +00:00
// given name and package, and returns swagger schema for it.
2024-05-14 13:07:09 +00:00
func (parser *Parser) parseTypeExpr(file *ast.File, typeExpr ast.Expr, ref bool) (*spec.Schema, error) {
2024-05-14 13:07:09 +00:00
switch expr := typeExpr.(type) {
2024-05-14 13:07:09 +00:00
// type Foo interface{}
2024-05-14 13:07:09 +00:00
case *ast.InterfaceType:
2024-05-14 13:07:09 +00:00
return &spec.Schema{}, nil
// type Foo struct {...}
2024-05-14 13:07:09 +00:00
case *ast.StructType:
2024-05-14 13:07:09 +00:00
return parser.parseStruct(file, expr.Fields)
// type Foo Baz
2024-05-14 13:07:09 +00:00
case *ast.Ident:
2024-05-14 13:07:09 +00:00
return parser.getTypeSchema(expr.Name, file, ref)
// type Foo *Baz
2024-05-14 13:07:09 +00:00
case *ast.StarExpr:
2024-05-14 13:07:09 +00:00
return parser.parseTypeExpr(file, expr.X, ref)
// type Foo pkg.Bar
2024-05-14 13:07:09 +00:00
case *ast.SelectorExpr:
2024-05-14 13:07:09 +00:00
if xIdent, ok := expr.X.(*ast.Ident); ok {
2024-05-14 13:07:09 +00:00
return parser.getTypeSchema(fullTypeName(xIdent.Name, expr.Sel.Name), file, ref)
2024-05-14 13:07:09 +00:00
}
2024-05-14 13:07:09 +00:00
// type Foo []Baz
2024-05-14 13:07:09 +00:00
case *ast.ArrayType:
2024-05-14 13:07:09 +00:00
itemSchema, err := parser.parseTypeExpr(file, expr.Elt, true)
2024-05-14 13:07:09 +00:00
if err != nil {
2024-05-14 13:07:09 +00:00
return nil, err
2024-05-14 13:07:09 +00:00
}
return spec.ArrayProperty(itemSchema), nil
2024-05-14 13:07:09 +00:00
// type Foo map[string]Bar
2024-05-14 13:07:09 +00:00
case *ast.MapType:
2024-05-14 13:07:09 +00:00
if _, ok := expr.Value.(*ast.InterfaceType); ok {
2024-05-14 13:07:09 +00:00
return spec.MapProperty(nil), nil
2024-05-14 13:07:09 +00:00
}
2024-05-14 13:07:09 +00:00
schema, err := parser.parseTypeExpr(file, expr.Value, true)
2024-05-14 13:07:09 +00:00
if err != nil {
2024-05-14 13:07:09 +00:00
return nil, err
2024-05-14 13:07:09 +00:00
}
return spec.MapProperty(schema), nil
case *ast.FuncType:
2024-05-14 13:07:09 +00:00
return nil, ErrFuncTypeField
2024-05-14 13:07:09 +00:00
// ...
2024-05-14 13:07:09 +00:00
}
return parser.parseGenericTypeExpr(file, typeExpr)
2024-05-14 13:07:09 +00:00
}
func (parser *Parser) parseStruct(file *ast.File, fields *ast.FieldList) (*spec.Schema, error) {
2024-05-14 13:07:09 +00:00
required, properties := make([]string, 0), make(map[string]spec.Schema)
for _, field := range fields.List {
2024-05-14 13:07:09 +00:00
fieldProps, requiredFromAnon, err := parser.parseStructField(file, field)
2024-05-14 13:07:09 +00:00
if err != nil {
2024-05-14 13:07:09 +00:00
if errors.Is(err, ErrFuncTypeField) || errors.Is(err, ErrSkippedField) {
2024-05-14 13:07:09 +00:00
continue
2024-05-14 13:07:09 +00:00
}
return nil, err
2024-05-14 13:07:09 +00:00
}
if len(fieldProps) == 0 {
2024-05-14 13:07:09 +00:00
continue
2024-05-14 13:07:09 +00:00
}
required = append(required, requiredFromAnon...)
for k, v := range fieldProps {
2024-05-14 13:07:09 +00:00
properties[k] = v
2024-05-14 13:07:09 +00:00
}
2024-05-14 13:07:09 +00:00
}
sort.Strings(required)
return &spec.Schema{
2024-05-14 13:07:09 +00:00
SchemaProps: spec.SchemaProps{
Type: []string{OBJECT},
2024-05-14 13:07:09 +00:00
Properties: properties,
Required: required,
2024-05-14 13:07:09 +00:00
},
}, nil
2024-05-14 13:07:09 +00:00
}
func (parser *Parser) parseStructField(file *ast.File, field *ast.Field) (map[string]spec.Schema, []string, error) {
2024-05-14 13:07:09 +00:00
if field.Tag != nil {
2024-05-14 13:07:09 +00:00
skip, ok := reflect.StructTag(strings.ReplaceAll(field.Tag.Value, "`", "")).Lookup("swaggerignore")
2024-05-14 13:07:09 +00:00
if ok && strings.EqualFold(skip, "true") {
2024-05-14 13:07:09 +00:00
return nil, nil, nil
2024-05-14 13:07:09 +00:00
}
2024-05-14 13:07:09 +00:00
}
ps := parser.fieldParserFactory(parser, field)
if ps.ShouldSkip() {
2024-05-14 13:07:09 +00:00
return nil, nil, nil
2024-05-14 13:07:09 +00:00
}
fieldName, err := ps.FieldName()
2024-05-14 13:07:09 +00:00
if err != nil {
2024-05-14 13:07:09 +00:00
return nil, nil, err
2024-05-14 13:07:09 +00:00
}
if fieldName == "" {
2024-05-14 13:07:09 +00:00
typeName, err := getFieldType(file, field.Type, nil)
2024-05-14 13:07:09 +00:00
if err != nil {
2024-05-14 13:07:09 +00:00
return nil, nil, fmt.Errorf("%s: %w", fieldName, err)
2024-05-14 13:07:09 +00:00
}
schema, err := parser.getTypeSchema(typeName, file, false)
2024-05-14 13:07:09 +00:00
if err != nil {
2024-05-14 13:07:09 +00:00
return nil, nil, fmt.Errorf("%s: %w", fieldName, err)
2024-05-14 13:07:09 +00:00
}
if len(schema.Type) > 0 && schema.Type[0] == OBJECT {
2024-05-14 13:07:09 +00:00
if len(schema.Properties) == 0 {
2024-05-14 13:07:09 +00:00
return nil, nil, nil
2024-05-14 13:07:09 +00:00
}
properties := map[string]spec.Schema{}
2024-05-14 13:07:09 +00:00
for k, v := range schema.Properties {
2024-05-14 13:07:09 +00:00
properties[k] = v
2024-05-14 13:07:09 +00:00
}
return properties, schema.SchemaProps.Required, nil
2024-05-14 13:07:09 +00:00
}
2024-05-14 13:07:09 +00:00
// for alias type of non-struct types ,such as array,map, etc. ignore field tag.
2024-05-14 13:07:09 +00:00
return map[string]spec.Schema{typeName: *schema}, nil, nil
}
schema, err := ps.CustomSchema()
2024-05-14 13:07:09 +00:00
if err != nil {
2024-05-14 13:07:09 +00:00
return nil, nil, fmt.Errorf("%s: %w", fieldName, err)
2024-05-14 13:07:09 +00:00
}
if schema == nil {
2024-05-14 13:07:09 +00:00
typeName, err := getFieldType(file, field.Type, nil)
2024-05-14 13:07:09 +00:00
if err == nil {
2024-05-14 13:07:09 +00:00
// named type
2024-05-14 13:07:09 +00:00
schema, err = parser.getTypeSchema(typeName, file, true)
2024-05-14 13:07:09 +00:00
} else {
2024-05-14 13:07:09 +00:00
// unnamed type
2024-05-14 13:07:09 +00:00
schema, err = parser.parseTypeExpr(file, field.Type, false)
2024-05-14 13:07:09 +00:00
}
if err != nil {
2024-05-14 13:07:09 +00:00
return nil, nil, fmt.Errorf("%s: %w", fieldName, err)
2024-05-14 13:07:09 +00:00
}
2024-05-14 13:07:09 +00:00
}
err = ps.ComplementSchema(schema)
2024-05-14 13:07:09 +00:00
if err != nil {
2024-05-14 13:07:09 +00:00
return nil, nil, fmt.Errorf("%s: %w", fieldName, err)
2024-05-14 13:07:09 +00:00
}
var tagRequired []string
required, err := ps.IsRequired()
2024-05-14 13:07:09 +00:00
if err != nil {
2024-05-14 13:07:09 +00:00
return nil, nil, fmt.Errorf("%s: %w", fieldName, err)
2024-05-14 13:07:09 +00:00
}
if required {
2024-05-14 13:07:09 +00:00
tagRequired = append(tagRequired, fieldName)
2024-05-14 13:07:09 +00:00
}
if schema.Extensions == nil {
2024-05-14 13:07:09 +00:00
schema.Extensions = make(spec.Extensions)
2024-05-14 13:07:09 +00:00
}
2024-05-14 13:07:09 +00:00
if formName := ps.FormName(); len(formName) > 0 {
2024-05-14 13:07:09 +00:00
schema.Extensions["formData"] = formName
2024-05-14 13:07:09 +00:00
}
2024-05-14 13:07:09 +00:00
if headerName := ps.HeaderName(); len(headerName) > 0 {
2024-05-14 13:07:09 +00:00
schema.Extensions["header"] = headerName
2024-05-14 13:07:09 +00:00
}
2024-05-14 13:07:09 +00:00
if pathName := ps.PathName(); len(pathName) > 0 {
2024-05-14 13:07:09 +00:00
schema.Extensions["path"] = pathName
2024-05-14 13:07:09 +00:00
}
return map[string]spec.Schema{fieldName: *schema}, tagRequired, nil
2024-05-14 13:07:09 +00:00
}
func getFieldType(file *ast.File, field ast.Expr, genericParamTypeDefs map[string]*genericTypeSpec) (string, error) {
2024-05-14 13:07:09 +00:00
switch fieldType := field.(type) {
2024-05-14 13:07:09 +00:00
case *ast.Ident:
2024-05-14 13:07:09 +00:00
return fieldType.Name, nil
2024-05-14 13:07:09 +00:00
case *ast.SelectorExpr:
2024-05-14 13:07:09 +00:00
packageName, err := getFieldType(file, fieldType.X, genericParamTypeDefs)
2024-05-14 13:07:09 +00:00
if err != nil {
2024-05-14 13:07:09 +00:00
return "", err
2024-05-14 13:07:09 +00:00
}
return fullTypeName(packageName, fieldType.Sel.Name), nil
2024-05-14 13:07:09 +00:00
case *ast.StarExpr:
2024-05-14 13:07:09 +00:00
fullName, err := getFieldType(file, fieldType.X, genericParamTypeDefs)
2024-05-14 13:07:09 +00:00
if err != nil {
2024-05-14 13:07:09 +00:00
return "", err
2024-05-14 13:07:09 +00:00
}
return fullName, nil
2024-05-14 13:07:09 +00:00
default:
2024-05-14 13:07:09 +00:00
return getGenericFieldType(file, field, genericParamTypeDefs)
2024-05-14 13:07:09 +00:00
}
2024-05-14 13:07:09 +00:00
}
func (parser *Parser) getUnderlyingSchema(schema *spec.Schema) *spec.Schema {
2024-05-14 13:07:09 +00:00
if schema == nil {
2024-05-14 13:07:09 +00:00
return nil
2024-05-14 13:07:09 +00:00
}
if url := schema.Ref.GetURL(); url != nil {
2024-05-14 13:07:09 +00:00
if pos := strings.LastIndexByte(url.Fragment, '/'); pos >= 0 {
2024-05-14 13:07:09 +00:00
name := url.Fragment[pos+1:]
2024-05-14 13:07:09 +00:00
if schema, ok := parser.swagger.Definitions[name]; ok {
2024-05-14 13:07:09 +00:00
return &schema
2024-05-14 13:07:09 +00:00
}
2024-05-14 13:07:09 +00:00
}
2024-05-14 13:07:09 +00:00
}
if len(schema.AllOf) > 0 {
2024-05-14 13:07:09 +00:00
merged := &spec.Schema{}
2024-05-14 13:07:09 +00:00
MergeSchema(merged, schema)
2024-05-14 13:07:09 +00:00
for _, s := range schema.AllOf {
2024-05-14 13:07:09 +00:00
MergeSchema(merged, parser.getUnderlyingSchema(&s))
2024-05-14 13:07:09 +00:00
}
2024-05-14 13:07:09 +00:00
return merged
2024-05-14 13:07:09 +00:00
}
2024-05-14 13:07:09 +00:00
return nil
2024-05-14 13:07:09 +00:00
}
// GetSchemaTypePath get path of schema type.
2024-05-14 13:07:09 +00:00
func (parser *Parser) GetSchemaTypePath(schema *spec.Schema, depth int) []string {
2024-05-14 13:07:09 +00:00
if schema == nil || depth == 0 {
2024-05-14 13:07:09 +00:00
return nil
2024-05-14 13:07:09 +00:00
}
if underlying := parser.getUnderlyingSchema(schema); underlying != nil {
2024-05-14 13:07:09 +00:00
return parser.GetSchemaTypePath(underlying, depth)
2024-05-14 13:07:09 +00:00
}
if len(schema.Type) > 0 {
2024-05-14 13:07:09 +00:00
switch schema.Type[0] {
2024-05-14 13:07:09 +00:00
case ARRAY:
2024-05-14 13:07:09 +00:00
depth--
s := []string{schema.Type[0]}
return append(s, parser.GetSchemaTypePath(schema.Items.Schema, depth)...)
2024-05-14 13:07:09 +00:00
case OBJECT:
2024-05-14 13:07:09 +00:00
if schema.AdditionalProperties != nil && schema.AdditionalProperties.Schema != nil {
2024-05-14 13:07:09 +00:00
// for map
2024-05-14 13:07:09 +00:00
depth--
s := []string{schema.Type[0]}
return append(s, parser.GetSchemaTypePath(schema.AdditionalProperties.Schema, depth)...)
2024-05-14 13:07:09 +00:00
}
2024-05-14 13:07:09 +00:00
}
return []string{schema.Type[0]}
2024-05-14 13:07:09 +00:00
}
return []string{ANY}
2024-05-14 13:07:09 +00:00
}
func replaceLastTag(slice []spec.Tag, element spec.Tag) {
2024-05-14 13:07:09 +00:00
slice = append(slice[:len(slice)-1], element)
2024-05-14 13:07:09 +00:00
}
// defineTypeOfExample example value define the type (object and array unsupported).
2024-05-14 13:07:09 +00:00
func defineTypeOfExample(schemaType, arrayType, exampleValue string) (interface{}, error) {
2024-05-14 13:07:09 +00:00
switch schemaType {
2024-05-14 13:07:09 +00:00
case STRING:
2024-05-14 13:07:09 +00:00
return exampleValue, nil
2024-05-14 13:07:09 +00:00
case NUMBER:
2024-05-14 13:07:09 +00:00
v, err := strconv.ParseFloat(exampleValue, 64)
2024-05-14 13:07:09 +00:00
if err != nil {
2024-05-14 13:07:09 +00:00
return nil, fmt.Errorf("example value %s can't convert to %s err: %s", exampleValue, schemaType, err)
2024-05-14 13:07:09 +00:00
}
return v, nil
2024-05-14 13:07:09 +00:00
case INTEGER:
2024-05-14 13:07:09 +00:00
v, err := strconv.Atoi(exampleValue)
2024-05-14 13:07:09 +00:00
if err != nil {
2024-05-14 13:07:09 +00:00
return nil, fmt.Errorf("example value %s can't convert to %s err: %s", exampleValue, schemaType, err)
2024-05-14 13:07:09 +00:00
}
return v, nil
2024-05-14 13:07:09 +00:00
case BOOLEAN:
2024-05-14 13:07:09 +00:00
v, err := strconv.ParseBool(exampleValue)
2024-05-14 13:07:09 +00:00
if err != nil {
2024-05-14 13:07:09 +00:00
return nil, fmt.Errorf("example value %s can't convert to %s err: %s", exampleValue, schemaType, err)
2024-05-14 13:07:09 +00:00
}
return v, nil
2024-05-14 13:07:09 +00:00
case ARRAY:
2024-05-14 13:07:09 +00:00
values := strings.Split(exampleValue, ",")
2024-05-14 13:07:09 +00:00
result := make([]interface{}, 0)
2024-05-14 13:07:09 +00:00
for _, value := range values {
2024-05-14 13:07:09 +00:00
v, err := defineTypeOfExample(arrayType, "", value)
2024-05-14 13:07:09 +00:00
if err != nil {
2024-05-14 13:07:09 +00:00
return nil, err
2024-05-14 13:07:09 +00:00
}
result = append(result, v)
2024-05-14 13:07:09 +00:00
}
return result, nil
2024-05-14 13:07:09 +00:00
case OBJECT:
2024-05-14 13:07:09 +00:00
if arrayType == "" {
2024-05-14 13:07:09 +00:00
return nil, fmt.Errorf("%s is unsupported type in example value `%s`", schemaType, exampleValue)
2024-05-14 13:07:09 +00:00
}
values := strings.Split(exampleValue, ",")
result := map[string]interface{}{}
for _, value := range values {
2024-05-14 13:07:09 +00:00
mapData := strings.SplitN(value, ":", 2)
if len(mapData) == 2 {
2024-05-14 13:07:09 +00:00
v, err := defineTypeOfExample(arrayType, "", mapData[1])
2024-05-14 13:07:09 +00:00
if err != nil {
2024-05-14 13:07:09 +00:00
return nil, err
2024-05-14 13:07:09 +00:00
}
result[mapData[0]] = v
continue
2024-05-14 13:07:09 +00:00
}
return nil, fmt.Errorf("example value %s should format: key:value", exampleValue)
2024-05-14 13:07:09 +00:00
}
return result, nil
2024-05-14 13:07:09 +00:00
}
return nil, fmt.Errorf("%s is unsupported type in example value %s", schemaType, exampleValue)
2024-05-14 13:07:09 +00:00
}
// GetAllGoFileInfo gets all Go source files information for given searchDir.
2024-05-14 13:07:09 +00:00
func (parser *Parser) getAllGoFileInfo(packageDir, searchDir string) error {
2024-05-14 13:07:09 +00:00
if parser.skipPackageByPrefix(packageDir) {
2024-05-14 13:07:09 +00:00
return nil // ignored by user-defined package path prefixes
2024-05-14 13:07:09 +00:00
}
2024-05-14 13:07:09 +00:00
return filepath.Walk(searchDir, func(path string, f os.FileInfo, _ error) error {
2024-05-14 13:07:09 +00:00
err := parser.Skip(path, f)
2024-05-14 13:07:09 +00:00
if err != nil {
2024-05-14 13:07:09 +00:00
return err
2024-05-14 13:07:09 +00:00
}
if f.IsDir() {
2024-05-14 13:07:09 +00:00
return nil
2024-05-14 13:07:09 +00:00
}
relPath, err := filepath.Rel(searchDir, path)
2024-05-14 13:07:09 +00:00
if err != nil {
2024-05-14 13:07:09 +00:00
return err
2024-05-14 13:07:09 +00:00
}
return parser.parseFile(filepath.ToSlash(filepath.Dir(filepath.Clean(filepath.Join(packageDir, relPath)))), path, nil, ParseAll)
2024-05-14 13:07:09 +00:00
})
2024-05-14 13:07:09 +00:00
}
func (parser *Parser) getAllGoFileInfoFromDeps(pkg *depth.Pkg, parseFlag ParseFlag) error {
2024-05-14 13:07:09 +00:00
ignoreInternal := pkg.Internal && !parser.ParseInternal
2024-05-14 13:07:09 +00:00
if ignoreInternal || !pkg.Resolved { // ignored internal and not resolved dependencies
2024-05-14 13:07:09 +00:00
return nil
2024-05-14 13:07:09 +00:00
}
if pkg.Raw != nil && parser.skipPackageByPrefix(pkg.Raw.ImportPath) {
2024-05-14 13:07:09 +00:00
return nil // ignored by user-defined package path prefixes
2024-05-14 13:07:09 +00:00
}
// Skip cgo
2024-05-14 13:07:09 +00:00
if pkg.Raw == nil && pkg.Name == "C" {
2024-05-14 13:07:09 +00:00
return nil
2024-05-14 13:07:09 +00:00
}
srcDir := pkg.Raw.Dir
files, err := os.ReadDir(srcDir) // only parsing files in the dir(don't contain sub dir files)
2024-05-14 13:07:09 +00:00
if err != nil {
2024-05-14 13:07:09 +00:00
return err
2024-05-14 13:07:09 +00:00
}
for _, f := range files {
2024-05-14 13:07:09 +00:00
if f.IsDir() {
2024-05-14 13:07:09 +00:00
continue
2024-05-14 13:07:09 +00:00
}
path := filepath.Join(srcDir, f.Name())
2024-05-14 13:07:09 +00:00
if err := parser.parseFile(pkg.Name, path, nil, parseFlag); err != nil {
2024-05-14 13:07:09 +00:00
return err
2024-05-14 13:07:09 +00:00
}
2024-05-14 13:07:09 +00:00
}
for i := 0; i < len(pkg.Deps); i++ {
2024-05-14 13:07:09 +00:00
if err := parser.getAllGoFileInfoFromDeps(&pkg.Deps[i], parseFlag); err != nil {
2024-05-14 13:07:09 +00:00
return err
2024-05-14 13:07:09 +00:00
}
2024-05-14 13:07:09 +00:00
}
return nil
2024-05-14 13:07:09 +00:00
}
func (parser *Parser) parseFile(packageDir, path string, src interface{}, flag ParseFlag) error {
2024-05-14 13:07:09 +00:00
if strings.HasSuffix(strings.ToLower(path), "_test.go") || filepath.Ext(path) != ".go" {
2024-05-14 13:07:09 +00:00
return nil
2024-05-14 13:07:09 +00:00
}
return parser.packages.ParseFile(packageDir, path, src, flag)
2024-05-14 13:07:09 +00:00
}
func (parser *Parser) checkOperationIDUniqueness() error {
2024-05-14 13:07:09 +00:00
// operationsIds contains all operationId annotations to check it's unique
2024-05-14 13:07:09 +00:00
operationsIds := make(map[string]string)
for path, item := range parser.swagger.Paths.Paths {
2024-05-14 13:07:09 +00:00
var method, id string
for method = range allMethod {
2024-05-14 13:07:09 +00:00
op := refRouteMethodOp(&item, method)
2024-05-14 13:07:09 +00:00
if *op != nil {
2024-05-14 13:07:09 +00:00
id = (**op).ID
break
2024-05-14 13:07:09 +00:00
}
2024-05-14 13:07:09 +00:00
}
if id == "" {
2024-05-14 13:07:09 +00:00
continue
2024-05-14 13:07:09 +00:00
}
current := fmt.Sprintf("%s %s", method, path)
previous, ok := operationsIds[id]
2024-05-14 13:07:09 +00:00
if ok {
2024-05-14 13:07:09 +00:00
return fmt.Errorf(
2024-05-14 13:07:09 +00:00
"duplicated @id annotation '%s' found in '%s', previously declared in: '%s'",
2024-05-14 13:07:09 +00:00
id, current, previous)
2024-05-14 13:07:09 +00:00
}
operationsIds[id] = current
2024-05-14 13:07:09 +00:00
}
return nil
2024-05-14 13:07:09 +00:00
}
// Skip returns filepath.SkipDir error if match vendor and hidden folder.
2024-05-14 13:07:09 +00:00
func (parser *Parser) Skip(path string, f os.FileInfo) error {
2024-05-14 13:07:09 +00:00
return walkWith(parser.excludes, parser.ParseVendor)(path, f)
2024-05-14 13:07:09 +00:00
}
func walkWith(excludes map[string]struct{}, parseVendor bool) func(path string, fileInfo os.FileInfo) error {
2024-05-14 13:07:09 +00:00
return func(path string, f os.FileInfo) error {
2024-05-14 13:07:09 +00:00
if f.IsDir() {
2024-05-14 13:07:09 +00:00
if !parseVendor && f.Name() == "vendor" || // ignore "vendor"
2024-05-14 13:07:09 +00:00
f.Name() == "docs" || // exclude docs
2024-05-14 13:07:09 +00:00
len(f.Name()) > 1 && f.Name()[0] == '.' && f.Name() != ".." { // exclude all hidden folder
2024-05-14 13:07:09 +00:00
return filepath.SkipDir
2024-05-14 13:07:09 +00:00
}
if excludes != nil {
2024-05-14 13:07:09 +00:00
if _, ok := excludes[path]; ok {
2024-05-14 13:07:09 +00:00
return filepath.SkipDir
2024-05-14 13:07:09 +00:00
}
2024-05-14 13:07:09 +00:00
}
2024-05-14 13:07:09 +00:00
}
return nil
2024-05-14 13:07:09 +00:00
}
2024-05-14 13:07:09 +00:00
}
// GetSwagger returns *spec.Swagger which is the root document object for the API specification.
2024-05-14 13:07:09 +00:00
func (parser *Parser) GetSwagger() *spec.Swagger {
2024-05-14 13:07:09 +00:00
return parser.swagger
2024-05-14 13:07:09 +00:00
}
// addTestType just for tests.
2024-05-14 13:07:09 +00:00
func (parser *Parser) addTestType(typename string) {
2024-05-14 13:07:09 +00:00
typeDef := &TypeSpecDef{}
2024-05-14 13:07:09 +00:00
parser.packages.uniqueDefinitions[typename] = typeDef
2024-05-14 13:07:09 +00:00
parser.parsedSchemas[typeDef] = &Schema{
2024-05-14 13:07:09 +00:00
PkgPath: "",
Name: typename,
Schema: PrimitiveSchema(OBJECT),
2024-05-14 13:07:09 +00:00
}
2024-05-14 13:07:09 +00:00
}