Migrate to new swagger docs generation (#18)
This commit is contained in:
892
vendor/github.com/swaggo/swag/parser.go
generated
vendored
Normal file
892
vendor/github.com/swaggo/swag/parser.go
generated
vendored
Normal file
@ -0,0 +1,892 @@
|
||||
package swag
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go/ast"
|
||||
goparser "go/parser"
|
||||
"go/token"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"github.com/go-openapi/jsonreference"
|
||||
"github.com/go-openapi/spec"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
// CamelCase indicates using CamelCase strategy for struct field.
|
||||
CamelCase = "camelcase"
|
||||
|
||||
// PascalCase indicates using PascalCase strategy for struct field.
|
||||
PascalCase = "pascalcase"
|
||||
|
||||
// SnakeCase indicates using SnakeCase strategy for struct field.
|
||||
SnakeCase = "snakecase"
|
||||
)
|
||||
|
||||
// Parser implements a parser for Go source files.
|
||||
type Parser struct {
|
||||
// swagger represents the root document object for the API specification
|
||||
swagger *spec.Swagger
|
||||
|
||||
//files is a map that stores map[real_go_file_path][astFile]
|
||||
files map[string]*ast.File
|
||||
|
||||
// TypeDefinitions is a map that stores [package name][type name][*ast.TypeSpec]
|
||||
TypeDefinitions map[string]map[string]*ast.TypeSpec
|
||||
|
||||
// CustomPrimitiveTypes is a map that stores custom primitive types to actual golang types [type name][string]
|
||||
CustomPrimitiveTypes map[string]string
|
||||
|
||||
//registerTypes is a map that stores [refTypeName][*ast.TypeSpec]
|
||||
registerTypes map[string]*ast.TypeSpec
|
||||
|
||||
PropNamingStrategy string
|
||||
}
|
||||
|
||||
// New creates a new Parser with default properties.
|
||||
func New() *Parser {
|
||||
parser := &Parser{
|
||||
swagger: &spec.Swagger{
|
||||
SwaggerProps: spec.SwaggerProps{
|
||||
Info: &spec.Info{
|
||||
InfoProps: spec.InfoProps{
|
||||
Contact: &spec.ContactInfo{},
|
||||
License: &spec.License{},
|
||||
},
|
||||
},
|
||||
Paths: &spec.Paths{
|
||||
Paths: make(map[string]spec.PathItem),
|
||||
},
|
||||
Definitions: make(map[string]spec.Schema),
|
||||
},
|
||||
},
|
||||
files: make(map[string]*ast.File),
|
||||
TypeDefinitions: make(map[string]map[string]*ast.TypeSpec),
|
||||
CustomPrimitiveTypes: make(map[string]string),
|
||||
registerTypes: make(map[string]*ast.TypeSpec),
|
||||
}
|
||||
return parser
|
||||
}
|
||||
|
||||
// ParseAPI parses general api info for gived searchDir and mainAPIFile
|
||||
func (parser *Parser) ParseAPI(searchDir string, mainAPIFile string) error {
|
||||
log.Println("Generate general API Info")
|
||||
if err := parser.getAllGoFileInfo(searchDir); err != nil {
|
||||
return err
|
||||
}
|
||||
parser.ParseGeneralAPIInfo(path.Join(searchDir, mainAPIFile))
|
||||
|
||||
for _, astFile := range parser.files {
|
||||
parser.ParseType(astFile)
|
||||
}
|
||||
|
||||
for _, astFile := range parser.files {
|
||||
parser.ParseRouterAPIInfo(astFile)
|
||||
}
|
||||
|
||||
parser.ParseDefinitions()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ParseGeneralAPIInfo parses general api info for gived mainAPIFile path
|
||||
func (parser *Parser) ParseGeneralAPIInfo(mainAPIFile string) error {
|
||||
fileSet := token.NewFileSet()
|
||||
fileTree, err := goparser.ParseFile(fileSet, mainAPIFile, nil, goparser.ParseComments)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "cannot parse soure files")
|
||||
}
|
||||
|
||||
parser.swagger.Swagger = "2.0"
|
||||
securityMap := map[string]*spec.SecurityScheme{}
|
||||
|
||||
// templated defaults
|
||||
parser.swagger.Info.Version = "{{.Version}}"
|
||||
parser.swagger.Info.Title = "{{.Title}}"
|
||||
parser.swagger.Info.Description = "{{.Description}}"
|
||||
parser.swagger.Host = "{{.Host}}"
|
||||
parser.swagger.BasePath = "{{.BasePath}}"
|
||||
|
||||
if fileTree.Comments != nil {
|
||||
for _, comment := range fileTree.Comments {
|
||||
comments := strings.Split(comment.Text(), "\n")
|
||||
for _, commentLine := range comments {
|
||||
attribute := strings.ToLower(strings.Split(commentLine, " ")[0])
|
||||
switch attribute {
|
||||
case "@version":
|
||||
parser.swagger.Info.Version = strings.TrimSpace(commentLine[len(attribute):])
|
||||
case "@title":
|
||||
parser.swagger.Info.Title = strings.TrimSpace(commentLine[len(attribute):])
|
||||
case "@description":
|
||||
parser.swagger.Info.Description = strings.TrimSpace(commentLine[len(attribute):])
|
||||
case "@termsofservice":
|
||||
parser.swagger.Info.TermsOfService = strings.TrimSpace(commentLine[len(attribute):])
|
||||
case "@contact.name":
|
||||
parser.swagger.Info.Contact.Name = strings.TrimSpace(commentLine[len(attribute):])
|
||||
case "@contact.email":
|
||||
parser.swagger.Info.Contact.Email = strings.TrimSpace(commentLine[len(attribute):])
|
||||
case "@contact.url":
|
||||
parser.swagger.Info.Contact.URL = strings.TrimSpace(commentLine[len(attribute):])
|
||||
case "@license.name":
|
||||
parser.swagger.Info.License.Name = strings.TrimSpace(commentLine[len(attribute):])
|
||||
case "@license.url":
|
||||
parser.swagger.Info.License.URL = strings.TrimSpace(commentLine[len(attribute):])
|
||||
case "@host":
|
||||
parser.swagger.Host = strings.TrimSpace(commentLine[len(attribute):])
|
||||
case "@basepath":
|
||||
parser.swagger.BasePath = strings.TrimSpace(commentLine[len(attribute):])
|
||||
case "@schemes":
|
||||
parser.swagger.Schemes = GetSchemes(commentLine)
|
||||
}
|
||||
}
|
||||
|
||||
for i := 0; i < len(comments); i++ {
|
||||
attribute := strings.ToLower(strings.Split(comments[i], " ")[0])
|
||||
switch attribute {
|
||||
case "@securitydefinitions.basic":
|
||||
securityMap[strings.TrimSpace(comments[i][len(attribute):])] = spec.BasicAuth()
|
||||
case "@securitydefinitions.apikey":
|
||||
attrMap := map[string]string{}
|
||||
for _, v := range comments[i+1:] {
|
||||
securityAttr := strings.ToLower(strings.Split(v, " ")[0])
|
||||
if securityAttr == "@in" || securityAttr == "@name" {
|
||||
attrMap[securityAttr] = strings.TrimSpace(v[len(securityAttr):])
|
||||
}
|
||||
// next securityDefinitions
|
||||
if strings.Index(securityAttr, "@securitydefinitions.") == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
if len(attrMap) != 2 {
|
||||
log.Panic("@securitydefinitions.apikey is @name and @in required")
|
||||
}
|
||||
securityMap[strings.TrimSpace(comments[i][len(attribute):])] = spec.APIKeyAuth(attrMap["@name"], attrMap["@in"])
|
||||
case "@securitydefinitions.oauth2.application":
|
||||
attrMap := map[string]string{}
|
||||
scopes := map[string]string{}
|
||||
for _, v := range comments[i+1:] {
|
||||
securityAttr := strings.ToLower(strings.Split(v, " ")[0])
|
||||
if securityAttr == "@tokenurl" {
|
||||
attrMap[securityAttr] = strings.TrimSpace(v[len(securityAttr):])
|
||||
} else if isExistsScope(securityAttr) {
|
||||
scopes[getScopeScheme(securityAttr)] = v[len(securityAttr):]
|
||||
}
|
||||
// next securityDefinitions
|
||||
if strings.Index(securityAttr, "@securitydefinitions.") == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
if len(attrMap) != 1 {
|
||||
log.Panic("@securitydefinitions.oauth2.application is @tokenUrl required")
|
||||
}
|
||||
securityScheme := spec.OAuth2Application(attrMap["@tokenurl"])
|
||||
for scope, description := range scopes {
|
||||
securityScheme.AddScope(scope, description)
|
||||
}
|
||||
securityMap[strings.TrimSpace(comments[i][len(attribute):])] = securityScheme
|
||||
case "@securitydefinitions.oauth2.implicit":
|
||||
attrMap := map[string]string{}
|
||||
scopes := map[string]string{}
|
||||
for _, v := range comments[i+1:] {
|
||||
securityAttr := strings.ToLower(strings.Split(v, " ")[0])
|
||||
if securityAttr == "@authorizationurl" {
|
||||
attrMap[securityAttr] = strings.TrimSpace(v[len(securityAttr):])
|
||||
} else if isExistsScope(securityAttr) {
|
||||
scopes[getScopeScheme(securityAttr)] = v[len(securityAttr):]
|
||||
}
|
||||
// next securityDefinitions
|
||||
if strings.Index(securityAttr, "@securitydefinitions.") == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
if len(attrMap) != 1 {
|
||||
log.Panic("@securitydefinitions.oauth2.implicit is @authorizationUrl required")
|
||||
}
|
||||
securityScheme := spec.OAuth2Implicit(attrMap["@authorizationurl"])
|
||||
for scope, description := range scopes {
|
||||
securityScheme.AddScope(scope, description)
|
||||
}
|
||||
securityMap[strings.TrimSpace(comments[i][len(attribute):])] = securityScheme
|
||||
case "@securitydefinitions.oauth2.password":
|
||||
attrMap := map[string]string{}
|
||||
scopes := map[string]string{}
|
||||
for _, v := range comments[i+1:] {
|
||||
securityAttr := strings.ToLower(strings.Split(v, " ")[0])
|
||||
if securityAttr == "@tokenurl" {
|
||||
attrMap[securityAttr] = strings.TrimSpace(v[len(securityAttr):])
|
||||
} else if isExistsScope(securityAttr) {
|
||||
scopes[getScopeScheme(securityAttr)] = v[len(securityAttr):]
|
||||
}
|
||||
// next securityDefinitions
|
||||
if strings.Index(securityAttr, "@securitydefinitions.") == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
if len(attrMap) != 1 {
|
||||
log.Panic("@securitydefinitions.oauth2.password is @tokenUrl required")
|
||||
}
|
||||
securityScheme := spec.OAuth2Password(attrMap["@tokenurl"])
|
||||
for scope, description := range scopes {
|
||||
securityScheme.AddScope(scope, description)
|
||||
}
|
||||
securityMap[strings.TrimSpace(comments[i][len(attribute):])] = securityScheme
|
||||
case "@securitydefinitions.oauth2.accesscode":
|
||||
attrMap := map[string]string{}
|
||||
scopes := map[string]string{}
|
||||
for _, v := range comments[i+1:] {
|
||||
securityAttr := strings.ToLower(strings.Split(v, " ")[0])
|
||||
if securityAttr == "@tokenurl" || securityAttr == "@authorizationurl" {
|
||||
attrMap[securityAttr] = strings.TrimSpace(v[len(securityAttr):])
|
||||
} else if isExistsScope(securityAttr) {
|
||||
scopes[getScopeScheme(securityAttr)] = v[len(securityAttr):]
|
||||
}
|
||||
// next securityDefinitions
|
||||
if strings.Index(securityAttr, "@securitydefinitions.") == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
if len(attrMap) != 2 {
|
||||
log.Panic("@securitydefinitions.oauth2.accessCode is @tokenUrl and @authorizationUrl required")
|
||||
}
|
||||
securityScheme := spec.OAuth2AccessToken(attrMap["@authorizationurl"], attrMap["@tokenurl"])
|
||||
for scope, description := range scopes {
|
||||
securityScheme.AddScope(scope, description)
|
||||
}
|
||||
securityMap[strings.TrimSpace(comments[i][len(attribute):])] = securityScheme
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(securityMap) > 0 {
|
||||
parser.swagger.SecurityDefinitions = securityMap
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getScopeScheme(scope string) string {
|
||||
scopeValue := scope[strings.Index(scope, "@scope."):]
|
||||
if scopeValue == "" {
|
||||
panic("@scope is empty")
|
||||
}
|
||||
return scope[len("@scope."):]
|
||||
}
|
||||
|
||||
func isExistsScope(scope string) bool {
|
||||
s := strings.Fields(scope)
|
||||
for _, v := range s {
|
||||
if strings.Index(v, "@scope.") != -1 {
|
||||
if strings.Index(v, ",") != -1 {
|
||||
panic("@scope can't use comma(,) get=" + v)
|
||||
}
|
||||
}
|
||||
}
|
||||
return strings.Index(scope, "@scope.") != -1
|
||||
}
|
||||
|
||||
// GetSchemes parses swagger schemes for given commentLine
|
||||
func GetSchemes(commentLine string) []string {
|
||||
attribute := strings.ToLower(strings.Split(commentLine, " ")[0])
|
||||
return strings.Split(strings.TrimSpace(commentLine[len(attribute):]), " ")
|
||||
}
|
||||
|
||||
// ParseRouterAPIInfo parses router api info for given astFile
|
||||
func (parser *Parser) ParseRouterAPIInfo(astFile *ast.File) {
|
||||
for _, astDescription := range astFile.Decls {
|
||||
switch astDeclaration := astDescription.(type) {
|
||||
case *ast.FuncDecl:
|
||||
if astDeclaration.Doc != nil && astDeclaration.Doc.List != nil {
|
||||
operation := NewOperation() //for per 'function' comment, create a new 'Operation' object
|
||||
operation.parser = parser
|
||||
for _, comment := range astDeclaration.Doc.List {
|
||||
if err := operation.ParseComment(comment.Text, astFile); err != nil {
|
||||
log.Panicf("ParseComment panic:%+v", err)
|
||||
}
|
||||
}
|
||||
var pathItem spec.PathItem
|
||||
var ok bool
|
||||
|
||||
if pathItem, ok = parser.swagger.Paths.Paths[operation.Path]; !ok {
|
||||
pathItem = spec.PathItem{}
|
||||
}
|
||||
switch strings.ToUpper(operation.HTTPMethod) {
|
||||
case http.MethodGet:
|
||||
pathItem.Get = &operation.Operation
|
||||
case http.MethodPost:
|
||||
pathItem.Post = &operation.Operation
|
||||
case http.MethodDelete:
|
||||
pathItem.Delete = &operation.Operation
|
||||
case http.MethodPut:
|
||||
pathItem.Put = &operation.Operation
|
||||
case http.MethodPatch:
|
||||
pathItem.Patch = &operation.Operation
|
||||
case http.MethodHead:
|
||||
pathItem.Head = &operation.Operation
|
||||
case http.MethodOptions:
|
||||
pathItem.Options = &operation.Operation
|
||||
}
|
||||
|
||||
parser.swagger.Paths.Paths[operation.Path] = pathItem
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ParseType parses type info for given astFile.
|
||||
func (parser *Parser) ParseType(astFile *ast.File) {
|
||||
if _, ok := parser.TypeDefinitions[astFile.Name.String()]; !ok {
|
||||
parser.TypeDefinitions[astFile.Name.String()] = make(map[string]*ast.TypeSpec)
|
||||
}
|
||||
|
||||
for _, astDeclaration := range astFile.Decls {
|
||||
if generalDeclaration, ok := astDeclaration.(*ast.GenDecl); ok && generalDeclaration.Tok == token.TYPE {
|
||||
for _, astSpec := range generalDeclaration.Specs {
|
||||
if typeSpec, ok := astSpec.(*ast.TypeSpec); ok {
|
||||
typeName := fmt.Sprintf("%v", typeSpec.Type)
|
||||
// check if its a custom primitive type
|
||||
if IsGolangPrimitiveType(typeName) {
|
||||
parser.CustomPrimitiveTypes[typeSpec.Name.String()] = TransToValidSchemeType(typeName)
|
||||
} else {
|
||||
parser.TypeDefinitions[astFile.Name.String()][typeSpec.Name.String()] = typeSpec
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ParseDefinitions parses Swagger Api definitions.
|
||||
func (parser *Parser) ParseDefinitions() {
|
||||
for refTypeName, typeSpec := range parser.registerTypes {
|
||||
ss := strings.Split(refTypeName, ".")
|
||||
pkgName := ss[0]
|
||||
parser.ParseDefinition(pkgName, typeSpec, typeSpec.Name.Name)
|
||||
}
|
||||
}
|
||||
|
||||
var structStacks []string
|
||||
|
||||
// isNotRecurringNestStruct check if a structure that is not a not repeating
|
||||
func isNotRecurringNestStruct(refTypeName string, structStacks []string) bool {
|
||||
for _, v := range structStacks {
|
||||
if refTypeName == v {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// ParseDefinition TODO: NEEDS COMMENT INFO
|
||||
func (parser *Parser) ParseDefinition(pkgName string, typeSpec *ast.TypeSpec, typeName string) {
|
||||
var refTypeName string
|
||||
if len(pkgName) > 0 {
|
||||
refTypeName = pkgName + "." + typeName
|
||||
} else {
|
||||
refTypeName = typeName
|
||||
}
|
||||
if _, already := parser.swagger.Definitions[refTypeName]; already {
|
||||
log.Println("Skipping '" + refTypeName + "', already present.")
|
||||
return
|
||||
}
|
||||
properties := make(map[string]spec.Schema)
|
||||
// stop repetitive structural parsing
|
||||
if isNotRecurringNestStruct(refTypeName, structStacks) {
|
||||
structStacks = append(structStacks, refTypeName)
|
||||
parser.parseTypeSpec(pkgName, typeSpec, properties)
|
||||
}
|
||||
structStacks = []string{}
|
||||
|
||||
// created sorted list of properties keys so when we iterate over them it's deterministic
|
||||
ks := make([]string, 0, len(properties))
|
||||
for k := range properties {
|
||||
ks = append(ks, k)
|
||||
}
|
||||
sort.Strings(ks)
|
||||
|
||||
requiredFields := make([]string, 0)
|
||||
|
||||
// iterate over keys list instead of map to avoid the random shuffle of the order that go does for maps
|
||||
for _, k := range ks {
|
||||
prop := properties[k]
|
||||
|
||||
// todo find the pkgName of the property type
|
||||
tname := prop.SchemaProps.Type[0]
|
||||
if _, ok := parser.TypeDefinitions[pkgName][tname]; ok {
|
||||
tspec := parser.TypeDefinitions[pkgName][tname]
|
||||
parser.ParseDefinition(pkgName, tspec, tname)
|
||||
}
|
||||
if tname != "object" {
|
||||
requiredFields = append(requiredFields, prop.SchemaProps.Required...)
|
||||
prop.SchemaProps.Required = make([]string, 0)
|
||||
}
|
||||
properties[k] = prop
|
||||
}
|
||||
log.Println("Generating " + refTypeName)
|
||||
parser.swagger.Definitions[refTypeName] = spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{"object"},
|
||||
Properties: properties,
|
||||
Required: requiredFields,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (parser *Parser) parseTypeSpec(pkgName string, typeSpec *ast.TypeSpec, properties map[string]spec.Schema) {
|
||||
switch typeSpec.Type.(type) {
|
||||
case *ast.StructType:
|
||||
structDecl := typeSpec.Type.(*ast.StructType)
|
||||
fields := structDecl.Fields.List
|
||||
|
||||
for _, field := range fields {
|
||||
if field.Names == nil { //anonymous field
|
||||
parser.parseAnonymousField(pkgName, field, properties)
|
||||
} else {
|
||||
props := parser.parseStruct(pkgName, field)
|
||||
for k, v := range props {
|
||||
properties[k] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case *ast.ArrayType:
|
||||
log.Println("ParseDefinitions not supported 'Array' yet.")
|
||||
case *ast.InterfaceType:
|
||||
log.Println("ParseDefinitions not supported 'Interface' yet.")
|
||||
case *ast.MapType:
|
||||
log.Println("ParseDefinitions not supported 'Map' yet.")
|
||||
}
|
||||
}
|
||||
|
||||
type structField struct {
|
||||
name string
|
||||
schemaType string
|
||||
arrayType string
|
||||
formatType string
|
||||
isRequired bool
|
||||
crossPkg string
|
||||
exampleValue interface{}
|
||||
maximum *float64
|
||||
minimum *float64
|
||||
maxLength *int64
|
||||
minLength *int64
|
||||
enums []interface{}
|
||||
defaultValue interface{}
|
||||
}
|
||||
|
||||
func (parser *Parser) parseStruct(pkgName string, field *ast.Field) (properties map[string]spec.Schema) {
|
||||
properties = map[string]spec.Schema{}
|
||||
structField := parser.parseField(field)
|
||||
if structField.name == "" {
|
||||
return
|
||||
}
|
||||
var desc string
|
||||
if field.Doc != nil {
|
||||
desc = strings.TrimSpace(field.Doc.Text())
|
||||
}
|
||||
// TODO: find package of schemaType and/or arrayType
|
||||
|
||||
if structField.crossPkg != "" {
|
||||
pkgName = structField.crossPkg
|
||||
}
|
||||
if _, ok := parser.TypeDefinitions[pkgName][structField.schemaType]; ok { // user type field
|
||||
// write definition if not yet present
|
||||
parser.ParseDefinition(pkgName, parser.TypeDefinitions[pkgName][structField.schemaType], structField.schemaType)
|
||||
properties[structField.name] = spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{"object"}, // to avoid swagger validation error
|
||||
Description: desc,
|
||||
Ref: spec.Ref{
|
||||
Ref: jsonreference.MustCreateRef("#/definitions/" + pkgName + "." + structField.schemaType),
|
||||
},
|
||||
},
|
||||
}
|
||||
} else if structField.schemaType == "array" { // array field type
|
||||
// if defined -- ref it
|
||||
if _, ok := parser.TypeDefinitions[pkgName][structField.arrayType]; ok { // user type in array
|
||||
parser.ParseDefinition(pkgName, parser.TypeDefinitions[pkgName][structField.arrayType], structField.arrayType)
|
||||
properties[structField.name] = spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{structField.schemaType},
|
||||
Description: desc,
|
||||
Items: &spec.SchemaOrArray{
|
||||
Schema: &spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Ref: spec.Ref{
|
||||
Ref: jsonreference.MustCreateRef("#/definitions/" + pkgName + "." + structField.arrayType),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
} else { // standard type in array
|
||||
required := make([]string, 0)
|
||||
if structField.isRequired {
|
||||
required = append(required, structField.name)
|
||||
}
|
||||
|
||||
properties[structField.name] = spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{structField.schemaType},
|
||||
Description: desc,
|
||||
Format: structField.formatType,
|
||||
Required: required,
|
||||
Items: &spec.SchemaOrArray{
|
||||
Schema: &spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{structField.arrayType},
|
||||
Maximum: structField.maximum,
|
||||
Minimum: structField.minimum,
|
||||
MaxLength: structField.maxLength,
|
||||
MinLength: structField.minLength,
|
||||
Enum: structField.enums,
|
||||
Default: structField.defaultValue,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
SwaggerSchemaProps: spec.SwaggerSchemaProps{
|
||||
Example: structField.exampleValue,
|
||||
},
|
||||
}
|
||||
}
|
||||
} else {
|
||||
required := make([]string, 0)
|
||||
if structField.isRequired {
|
||||
required = append(required, structField.name)
|
||||
}
|
||||
properties[structField.name] = spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{structField.schemaType},
|
||||
Description: desc,
|
||||
Format: structField.formatType,
|
||||
Required: required,
|
||||
Maximum: structField.maximum,
|
||||
Minimum: structField.minimum,
|
||||
MaxLength: structField.maxLength,
|
||||
MinLength: structField.minLength,
|
||||
Enum: structField.enums,
|
||||
Default: structField.defaultValue,
|
||||
},
|
||||
SwaggerSchemaProps: spec.SwaggerSchemaProps{
|
||||
Example: structField.exampleValue,
|
||||
},
|
||||
}
|
||||
nestStruct, ok := field.Type.(*ast.StructType)
|
||||
if ok {
|
||||
props := map[string]spec.Schema{}
|
||||
nestRequired := make([]string, 0)
|
||||
for _, v := range nestStruct.Fields.List {
|
||||
p := parser.parseStruct(pkgName, v)
|
||||
for k, v := range p {
|
||||
if v.SchemaProps.Type[0] != "object" {
|
||||
nestRequired = append(nestRequired, v.SchemaProps.Required...)
|
||||
v.SchemaProps.Required = make([]string, 0)
|
||||
}
|
||||
props[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
properties[structField.name] = spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{structField.schemaType},
|
||||
Description: desc,
|
||||
Format: structField.formatType,
|
||||
Properties: props,
|
||||
Required: nestRequired,
|
||||
Maximum: structField.maximum,
|
||||
Minimum: structField.minimum,
|
||||
MaxLength: structField.maxLength,
|
||||
MinLength: structField.minLength,
|
||||
Enum: structField.enums,
|
||||
Default: structField.defaultValue,
|
||||
},
|
||||
SwaggerSchemaProps: spec.SwaggerSchemaProps{
|
||||
Example: structField.exampleValue,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (parser *Parser) parseAnonymousField(pkgName string, field *ast.Field, properties map[string]spec.Schema) {
|
||||
// check if ast Field is Ident type
|
||||
astTypeIdent, okTypeIdent := field.Type.(*ast.Ident)
|
||||
|
||||
// if ast Field is not Ident type we check if it's StarExpr
|
||||
// because it might be a pointer to an Ident
|
||||
if !okTypeIdent {
|
||||
if astTypeStar, okTypeStar := field.Type.(*ast.StarExpr); okTypeStar {
|
||||
astTypeIdent, okTypeIdent = astTypeStar.X.(*ast.Ident)
|
||||
}
|
||||
}
|
||||
|
||||
if okTypeIdent {
|
||||
findPgkName := pkgName
|
||||
findBaseTypeName := astTypeIdent.Name
|
||||
ss := strings.Split(astTypeIdent.Name, ".")
|
||||
if len(ss) > 1 {
|
||||
findPgkName = ss[0]
|
||||
findBaseTypeName = ss[1]
|
||||
}
|
||||
|
||||
baseTypeSpec := parser.TypeDefinitions[findPgkName][findBaseTypeName]
|
||||
parser.parseTypeSpec(findPgkName, baseTypeSpec, properties)
|
||||
}
|
||||
}
|
||||
|
||||
func (parser *Parser) parseField(field *ast.Field) *structField {
|
||||
prop := getPropertyName(field, parser)
|
||||
if len(prop.ArrayType) == 0 {
|
||||
CheckSchemaType(prop.SchemaType)
|
||||
} else {
|
||||
CheckSchemaType("array")
|
||||
}
|
||||
structField := &structField{
|
||||
name: field.Names[0].Name,
|
||||
schemaType: prop.SchemaType,
|
||||
arrayType: prop.ArrayType,
|
||||
crossPkg: prop.CrossPkg,
|
||||
}
|
||||
|
||||
switch parser.PropNamingStrategy {
|
||||
case SnakeCase:
|
||||
structField.name = toSnakeCase(structField.name)
|
||||
case PascalCase:
|
||||
//use struct field name
|
||||
case CamelCase:
|
||||
structField.name = toLowerCamelCase(structField.name)
|
||||
default:
|
||||
structField.name = toLowerCamelCase(structField.name)
|
||||
}
|
||||
|
||||
if field.Tag == nil {
|
||||
return structField
|
||||
}
|
||||
// `json:"tag"` -> json:"tag"
|
||||
structTag := reflect.StructTag(strings.Replace(field.Tag.Value, "`", "", -1))
|
||||
jsonTag := structTag.Get("json")
|
||||
// json:"tag,hoge"
|
||||
if strings.Contains(jsonTag, ",") {
|
||||
// json:",hoge"
|
||||
if strings.HasPrefix(jsonTag, ",") {
|
||||
jsonTag = ""
|
||||
} else {
|
||||
jsonTag = strings.SplitN(jsonTag, ",", 2)[0]
|
||||
}
|
||||
}
|
||||
if jsonTag == "-" {
|
||||
structField.name = ""
|
||||
} else if jsonTag != "" {
|
||||
structField.name = jsonTag
|
||||
}
|
||||
|
||||
if typeTag := structTag.Get("swaggertype"); typeTag != "" {
|
||||
parts := strings.Split(typeTag, ",")
|
||||
if 0 < len(parts) && len(parts) <= 2 {
|
||||
newSchemaType := parts[0]
|
||||
newArrayType := structField.arrayType
|
||||
if len(parts) >= 2 && newSchemaType == "array" {
|
||||
newArrayType = parts[1]
|
||||
}
|
||||
|
||||
CheckSchemaType(newSchemaType)
|
||||
CheckSchemaType(newArrayType)
|
||||
structField.schemaType = newSchemaType
|
||||
structField.arrayType = newArrayType
|
||||
}
|
||||
}
|
||||
if exampleTag := structTag.Get("example"); exampleTag != "" {
|
||||
structField.exampleValue = defineTypeOfExample(structField.schemaType, structField.arrayType, exampleTag)
|
||||
}
|
||||
if formatTag := structTag.Get("format"); formatTag != "" {
|
||||
structField.formatType = formatTag
|
||||
}
|
||||
if bindingTag := structTag.Get("binding"); bindingTag != "" {
|
||||
for _, val := range strings.Split(bindingTag, ",") {
|
||||
if val == "required" {
|
||||
structField.isRequired = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if validateTag := structTag.Get("validate"); validateTag != "" {
|
||||
for _, val := range strings.Split(validateTag, ",") {
|
||||
if val == "required" {
|
||||
structField.isRequired = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if enumsTag := structTag.Get("enums"); enumsTag != "" {
|
||||
enumType := structField.schemaType
|
||||
if structField.schemaType == "array" {
|
||||
enumType = structField.arrayType
|
||||
}
|
||||
|
||||
for _, e := range strings.Split(enumsTag, ",") {
|
||||
structField.enums = append(structField.enums, defineType(enumType, e))
|
||||
}
|
||||
}
|
||||
if defaultTag := structTag.Get("default"); defaultTag != "" {
|
||||
structField.defaultValue = defineType(structField.schemaType, defaultTag)
|
||||
}
|
||||
|
||||
if IsNumericType(structField.schemaType) || IsNumericType(structField.arrayType) {
|
||||
structField.maximum = getFloatTag(structTag, "maximum")
|
||||
structField.minimum = getFloatTag(structTag, "minimum")
|
||||
}
|
||||
if structField.schemaType == "string" || structField.arrayType == "string" {
|
||||
structField.maxLength = getIntTag(structTag, "maxLength")
|
||||
structField.minLength = getIntTag(structTag, "minLength")
|
||||
}
|
||||
|
||||
return structField
|
||||
}
|
||||
|
||||
func getFloatTag(structTag reflect.StructTag, tagName string) *float64 {
|
||||
strValue := structTag.Get(tagName)
|
||||
if strValue == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
value, err := strconv.ParseFloat(strValue, 64)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("can't parse numeric value of %q tag: %v", tagName, err))
|
||||
}
|
||||
|
||||
return &value
|
||||
}
|
||||
|
||||
func getIntTag(structTag reflect.StructTag, tagName string) *int64 {
|
||||
strValue := structTag.Get(tagName)
|
||||
if strValue == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
value, err := strconv.ParseInt(strValue, 10, 64)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("can't parse numeric value of %q tag: %v", tagName, err))
|
||||
}
|
||||
|
||||
return &value
|
||||
}
|
||||
|
||||
func toSnakeCase(in string) string {
|
||||
runes := []rune(in)
|
||||
length := len(runes)
|
||||
|
||||
var out []rune
|
||||
for i := 0; i < length; i++ {
|
||||
if i > 0 && unicode.IsUpper(runes[i]) && ((i+1 < length && unicode.IsLower(runes[i+1])) || unicode.IsLower(runes[i-1])) {
|
||||
out = append(out, '_')
|
||||
}
|
||||
out = append(out, unicode.ToLower(runes[i]))
|
||||
}
|
||||
return string(out)
|
||||
}
|
||||
|
||||
func toLowerCamelCase(in string) string {
|
||||
runes := []rune(in)
|
||||
|
||||
var out []rune
|
||||
flag := false
|
||||
for i, curr := range runes {
|
||||
if (i == 0 && unicode.IsUpper(curr)) || (flag && unicode.IsUpper(curr)) {
|
||||
out = append(out, unicode.ToLower(curr))
|
||||
flag = true
|
||||
} else {
|
||||
out = append(out, curr)
|
||||
flag = false
|
||||
}
|
||||
}
|
||||
|
||||
return string(out)
|
||||
}
|
||||
|
||||
// defineTypeOfExample example value define the type (object and array unsupported)
|
||||
func defineTypeOfExample(schemaType, arrayType, exampleValue string) interface{} {
|
||||
switch schemaType {
|
||||
case "string":
|
||||
return exampleValue
|
||||
case "number":
|
||||
v, err := strconv.ParseFloat(exampleValue, 64)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("example value %s can't convert to %s err: %s", exampleValue, schemaType, err))
|
||||
}
|
||||
return v
|
||||
case "integer":
|
||||
v, err := strconv.Atoi(exampleValue)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("example value %s can't convert to %s err: %s", exampleValue, schemaType, err))
|
||||
}
|
||||
return v
|
||||
case "boolean":
|
||||
v, err := strconv.ParseBool(exampleValue)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("example value %s can't convert to %s err: %s", exampleValue, schemaType, err))
|
||||
}
|
||||
return v
|
||||
case "array":
|
||||
values := strings.Split(exampleValue, ",")
|
||||
result := make([]interface{}, 0)
|
||||
for _, value := range values {
|
||||
result = append(result, defineTypeOfExample(arrayType, "", value))
|
||||
}
|
||||
return result
|
||||
default:
|
||||
panic(fmt.Errorf("%s is unsupported type in example value", schemaType))
|
||||
}
|
||||
}
|
||||
|
||||
// GetAllGoFileInfo gets all Go source files information for given searchDir.
|
||||
func (parser *Parser) getAllGoFileInfo(searchDir string) error {
|
||||
return filepath.Walk(searchDir, parser.visit)
|
||||
}
|
||||
|
||||
func (parser *Parser) visit(path string, f os.FileInfo, err error) error {
|
||||
if err := Skip(f); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if ext := filepath.Ext(path); ext == ".go" {
|
||||
fset := token.NewFileSet() // positions are relative to fset
|
||||
astFile, err := goparser.ParseFile(fset, path, nil, goparser.ParseComments)
|
||||
if err != nil {
|
||||
log.Panicf("ParseFile panic:%+v", err)
|
||||
}
|
||||
|
||||
parser.files[path] = astFile
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Skip returns filepath.SkipDir error if match vendor and hidden folder
|
||||
func Skip(f os.FileInfo) error {
|
||||
// exclude vendor folder
|
||||
if f.IsDir() && f.Name() == "vendor" {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
|
||||
// exclude all hidden folder
|
||||
if f.IsDir() && len(f.Name()) > 1 && f.Name()[0] == '.' {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetSwagger returns *spec.Swagger which is the root document object for the API specification.
|
||||
func (parser *Parser) GetSwagger() *spec.Swagger {
|
||||
return parser.swagger
|
||||
}
|
Reference in New Issue
Block a user