mirror of
https://github.com/beego/bee.git
synced 2024-11-25 20:10:55 +00:00
1568 lines
43 KiB
Go
1568 lines
43 KiB
Go
// Copyright 2013 bee authors
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License"): you may
|
|
// not use this file except in compliance with the License. You may obtain
|
|
// a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
// License for the specific language governing permissions and limitations
|
|
// under the License.
|
|
|
|
package swaggergen
|
|
|
|
import (
|
|
"bufio"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"go/ast"
|
|
"go/build"
|
|
"go/parser"
|
|
"go/token"
|
|
"io"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"reflect"
|
|
"regexp"
|
|
"runtime"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"unicode"
|
|
|
|
yaml "gopkg.in/yaml.v2"
|
|
|
|
bu "github.com/beego/bee/v2/utils"
|
|
|
|
beeLogger "github.com/beego/bee/v2/logger"
|
|
"github.com/beego/beego/v2/core/utils"
|
|
"github.com/beego/beego/v2/server/web/swagger"
|
|
)
|
|
|
|
const (
|
|
ajson = "application/json"
|
|
axml = "application/xml"
|
|
aplain = "text/plain"
|
|
ahtml = "text/html"
|
|
aform = "multipart/form-data"
|
|
)
|
|
|
|
const (
|
|
astTypeArray = "array"
|
|
astTypeObject = "object"
|
|
astTypeMap = "map"
|
|
)
|
|
|
|
const defaultNamespacePrefix = ""
|
|
|
|
var pkgCache map[string]struct{} //pkg:controller:function:comments comments: key:value
|
|
var controllerComments map[string]string
|
|
var importlist map[string]string
|
|
var controllerList map[string]map[string]*swagger.Item //controllername Paths items
|
|
var modelsList map[string]map[string]swagger.Schema
|
|
|
|
var rootapiSingle = false
|
|
var rootapiDefault swagger.Swagger
|
|
var rootapiMap map[string]*swagger.Swagger // version, swagger
|
|
var astPkgs []*ast.Package
|
|
var pkgLoadedCache map[string]struct{}
|
|
|
|
// refer to builtin.go
|
|
var basicTypes = map[string]string{
|
|
"bool": "boolean:",
|
|
"uint": "integer:int32",
|
|
"uint8": "integer:int32",
|
|
"uint16": "integer:int32",
|
|
"uint32": "integer:int32",
|
|
"uint64": "integer:int64",
|
|
"int": "integer:int64",
|
|
"int8": "integer:int32",
|
|
"int16": "integer:int32",
|
|
"int32": "integer:int32",
|
|
"int64": "integer:int64",
|
|
"uintptr": "integer:int64",
|
|
"float32": "number:float",
|
|
"float64": "number:double",
|
|
"string": "string:",
|
|
"complex64": "number:float",
|
|
"complex128": "number:double",
|
|
"byte": "string:byte",
|
|
"rune": "string:byte",
|
|
// builtin golang objects
|
|
"time.Time": "string:datetime",
|
|
"json.RawMessage": "object:",
|
|
}
|
|
|
|
var stdlibObject = map[string]string{
|
|
"&{time Time}": "time.Time",
|
|
"&{json RawMessage}": "json.RawMessage",
|
|
}
|
|
|
|
var customObject = map[string]string{
|
|
"&{base ObjectID}": "string",
|
|
}
|
|
|
|
func init() {
|
|
pkgCache = make(map[string]struct{})
|
|
controllerComments = make(map[string]string)
|
|
importlist = make(map[string]string)
|
|
controllerList = make(map[string]map[string]*swagger.Item)
|
|
modelsList = make(map[string]map[string]swagger.Schema)
|
|
rootapiMap = make(map[string]*swagger.Swagger)
|
|
astPkgs = make([]*ast.Package, 0)
|
|
pkgLoadedCache = make(map[string]struct{})
|
|
}
|
|
|
|
// parsePackagesFromDir parses packages from a given directory
|
|
func parsePackagesFromDir(dirpath string) {
|
|
c := make(chan error)
|
|
|
|
go func() {
|
|
filepath.Walk(dirpath, func(fpath string, fileInfo os.FileInfo, err error) error {
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
if !fileInfo.IsDir() {
|
|
return nil
|
|
}
|
|
|
|
// skip folder if it's a 'vendor' folder within dirpath or its child,
|
|
// all 'tests' folders and dot folders wihin dirpath
|
|
d, _ := filepath.Rel(dirpath, fpath)
|
|
if !(d == "vendor" || strings.HasPrefix(d, "vendor"+string(os.PathSeparator))) &&
|
|
!strings.Contains(d, "tests") &&
|
|
!(d[0] == '.') {
|
|
err = parsePackageFromDir(fpath)
|
|
if err != nil {
|
|
// Send the error to through the channel and continue walking
|
|
c <- fmt.Errorf("error while parsing directory: %s", err.Error())
|
|
return nil
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
close(c)
|
|
}()
|
|
|
|
for err := range c {
|
|
beeLogger.Log.Warnf("%s", err)
|
|
}
|
|
}
|
|
|
|
func parsePackageFromDir(path string) error {
|
|
fileSet := token.NewFileSet()
|
|
folderPkgs, err := parser.ParseDir(fileSet, path, func(info os.FileInfo) bool {
|
|
name := info.Name()
|
|
return !info.IsDir() && !strings.HasPrefix(name, ".") && strings.HasSuffix(name, ".go")
|
|
}, parser.ParseComments)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, v := range folderPkgs {
|
|
astPkgs = append(astPkgs, v)
|
|
}
|
|
|
|
if len(folderPkgs) != 0 {
|
|
workPath := bu.GetBeeWorkPath()
|
|
parentPath := filepath.Dir(workPath)
|
|
rel, err := filepath.Rel(parentPath, path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
pkgLoadedCache[rel] = struct{}{}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// GenerateDocs generates documentations for a given path.
|
|
func GenerateDocs(curpath string) {
|
|
pkgspath := curpath
|
|
workspace := os.Getenv("BeeWorkspace")
|
|
if workspace != "" {
|
|
pkgspath = workspace
|
|
}
|
|
parsePackagesFromDir(pkgspath)
|
|
|
|
fset := token.NewFileSet()
|
|
f, err := parser.ParseFile(fset, filepath.Join(curpath, "routers", "router.go"), nil, parser.ParseComments)
|
|
if err != nil {
|
|
beeLogger.Log.Fatalf("Error while parsing router.go: %s", err)
|
|
}
|
|
|
|
// Analyse API comments
|
|
if f.Comments != nil {
|
|
for _, c := range f.Comments {
|
|
var namespacePrefix string
|
|
rootapi := swagger.Swagger{
|
|
Infos: swagger.Information{},
|
|
SwaggerVersion: "2.0",
|
|
}
|
|
for _, s := range strings.Split(c.Text(), "\n") {
|
|
if strings.HasPrefix(s, "@NamespacePrefix") {
|
|
namespacePrefix = strings.TrimSpace(s[len("@NamespacePrefix"):])
|
|
if _, exist := rootapiMap[namespacePrefix]; !exist {
|
|
rootapiMap[namespacePrefix] = &rootapi
|
|
}
|
|
} else if strings.HasPrefix(s, "@APIVersion") {
|
|
rootapi.Infos.Version = strings.TrimSpace(s[len("@APIVersion"):])
|
|
} else if strings.HasPrefix(s, "@Title") {
|
|
rootapi.Infos.Title = strings.TrimSpace(s[len("@Title"):])
|
|
} else if strings.HasPrefix(s, "@Description") {
|
|
rootapi.Infos.Description += fmt.Sprintf("%s\n", strings.TrimSpace(s[len("@Description"):]))
|
|
} else if strings.HasPrefix(s, "@TermsOfServiceUrl") {
|
|
rootapi.Infos.TermsOfService = strings.TrimSpace(s[len("@TermsOfServiceUrl"):])
|
|
} else if strings.HasPrefix(s, "@Contact") {
|
|
rootapi.Infos.Contact.EMail = strings.TrimSpace(s[len("@Contact"):])
|
|
} else if strings.HasPrefix(s, "@Name") {
|
|
rootapi.Infos.Contact.Name = strings.TrimSpace(s[len("@Name"):])
|
|
} else if strings.HasPrefix(s, "@URL") {
|
|
rootapi.Infos.Contact.URL = strings.TrimSpace(s[len("@URL"):])
|
|
} else if strings.HasPrefix(s, "@LicenseUrl") {
|
|
if rootapi.Infos.License == nil {
|
|
rootapi.Infos.License = &swagger.License{URL: strings.TrimSpace(s[len("@LicenseUrl"):])}
|
|
} else {
|
|
rootapi.Infos.License.URL = strings.TrimSpace(s[len("@LicenseUrl"):])
|
|
}
|
|
} else if strings.HasPrefix(s, "@License") {
|
|
if rootapi.Infos.License == nil {
|
|
rootapi.Infos.License = &swagger.License{Name: strings.TrimSpace(s[len("@License"):])}
|
|
} else {
|
|
rootapi.Infos.License.Name = strings.TrimSpace(s[len("@License"):])
|
|
}
|
|
} else if strings.HasPrefix(s, "@Schemes") {
|
|
rootapi.Schemes = strings.Split(strings.TrimSpace(s[len("@Schemes"):]), ",")
|
|
} else if strings.HasPrefix(s, "@Host") {
|
|
rootapi.Host = strings.TrimSpace(s[len("@Host"):])
|
|
} else if strings.HasPrefix(s, "@SecurityDefinition") {
|
|
if len(rootapi.SecurityDefinitions) == 0 {
|
|
rootapi.SecurityDefinitions = make(map[string]swagger.Security)
|
|
}
|
|
var out swagger.Security
|
|
p := getparams(strings.TrimSpace(s[len("@SecurityDefinition"):]))
|
|
if len(p) < 2 {
|
|
beeLogger.Log.Fatalf("Not enough params for security: %d\n", len(p))
|
|
}
|
|
out.Type = p[1]
|
|
switch out.Type {
|
|
case "oauth2":
|
|
if len(p) < 6 {
|
|
beeLogger.Log.Fatalf("Not enough params for oauth2: %d\n", len(p))
|
|
}
|
|
if !(p[3] == "implicit" || p[3] == "password" || p[3] == "application" || p[3] == "accessCode") {
|
|
beeLogger.Log.Fatalf("Unknown flow type: %s. Possible values are `implicit`, `password`, `application` or `accessCode`.\n", p[1])
|
|
}
|
|
out.AuthorizationURL = p[2]
|
|
out.Flow = p[3]
|
|
if len(p)%2 != 0 {
|
|
out.Description = strings.Trim(p[len(p)-1], `" `)
|
|
}
|
|
out.Scopes = make(map[string]string)
|
|
for i := 4; i < len(p)-1; i += 2 {
|
|
out.Scopes[p[i]] = strings.Trim(p[i+1], `" `)
|
|
}
|
|
case "apiKey":
|
|
if len(p) < 4 {
|
|
beeLogger.Log.Fatalf("Not enough params for apiKey: %d\n", len(p))
|
|
}
|
|
if !(p[3] == "header" || p[3] == "query") {
|
|
beeLogger.Log.Fatalf("Unknown in type: %s. Possible values are `query` or `header`.\n", p[4])
|
|
}
|
|
out.Name = p[2]
|
|
out.In = p[3]
|
|
if len(p) > 4 {
|
|
out.Description = strings.Trim(p[4], `" `)
|
|
}
|
|
case "basic":
|
|
if len(p) > 2 {
|
|
out.Description = strings.Trim(p[2], `" `)
|
|
}
|
|
default:
|
|
beeLogger.Log.Fatalf("Unknown security type: %s. Possible values are `oauth2`, `apiKey` or `basic`.\n", p[1])
|
|
}
|
|
rootapi.SecurityDefinitions[p[0]] = out
|
|
} else if strings.HasPrefix(s, "@Security") {
|
|
if len(rootapi.Security) == 0 {
|
|
rootapi.Security = make([]map[string][]string, 0)
|
|
}
|
|
rootapi.Security = append(rootapi.Security, getSecurity(s))
|
|
}
|
|
}
|
|
|
|
_, namespacePrefixExist := rootapiMap[namespacePrefix]
|
|
if !namespacePrefixExist && !rootapiSingle {
|
|
rootapiSingle = true
|
|
rootapiDefault = rootapi
|
|
}
|
|
}
|
|
}
|
|
// Analyse controller package
|
|
for _, im := range f.Imports {
|
|
localName := ""
|
|
if im.Name != nil {
|
|
localName = im.Name.Name
|
|
}
|
|
analyseControllerPkg(localName, im.Path.Value)
|
|
}
|
|
|
|
for _, d := range f.Decls {
|
|
switch specDecl := d.(type) {
|
|
case *ast.FuncDecl:
|
|
for _, l := range specDecl.Body.List {
|
|
switch stmt := l.(type) {
|
|
case *ast.AssignStmt:
|
|
for _, l := range stmt.Rhs {
|
|
if v, ok := l.(*ast.CallExpr); ok {
|
|
// Analyze NewNamespace, it will return version and the subfunction
|
|
selExpr, selOK := v.Fun.(*ast.SelectorExpr)
|
|
if !selOK || selExpr.Sel.Name != "NewNamespace" {
|
|
continue
|
|
}
|
|
|
|
version, params := analyseNewNamespace(v)
|
|
var rootapi *swagger.Swagger
|
|
if rootapiSingle {
|
|
rootapi = &rootapiDefault
|
|
} else {
|
|
_rootapi, rootapiExist := rootapiMap[version]
|
|
if !rootapiExist {
|
|
rootapi = &swagger.Swagger{
|
|
Infos: swagger.Information{},
|
|
SwaggerVersion: "2.0",
|
|
}
|
|
rootapiMap[version] = rootapi
|
|
} else {
|
|
rootapi = _rootapi
|
|
}
|
|
}
|
|
|
|
if rootapi.BasePath == "" {
|
|
rootapi.BasePath = version
|
|
}
|
|
|
|
for _, p := range params {
|
|
switch pp := p.(type) {
|
|
case *ast.CallExpr:
|
|
var controllerName string
|
|
if selname := pp.Fun.(*ast.SelectorExpr).Sel.String(); selname == "NSNamespace" {
|
|
s, params := analyseNewNamespace(pp)
|
|
if rootapi.BasePath == "" {
|
|
s = path.Join(version, s)
|
|
}
|
|
|
|
for _, sp := range params {
|
|
switch pp := sp.(type) {
|
|
case *ast.CallExpr:
|
|
if pp.Fun.(*ast.SelectorExpr).Sel.String() == "NSInclude" {
|
|
controllerName = analyseNSInclude(s, pp, rootapi)
|
|
if v, ok := controllerComments[controllerName]; ok {
|
|
rootapi.Tags = append(rootapi.Tags, swagger.Tag{
|
|
Name: strings.Trim(s, "/"),
|
|
Description: v,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} else if selname == "NSInclude" {
|
|
controllerName = analyseNSInclude("", pp, rootapi)
|
|
if v, ok := controllerComments[controllerName]; ok {
|
|
rootapi.Tags = append(rootapi.Tags, swagger.Tag{
|
|
Name: controllerName, // if the NSInclude has no prefix, we use the controllername as the tag
|
|
Description: v,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if rootapiSingle {
|
|
rootapiMap[defaultNamespacePrefix] = &rootapiDefault
|
|
}
|
|
|
|
for version, api := range rootapiMap {
|
|
api.Definitions = rootapiDefault.Definitions
|
|
|
|
dir := path.Join(curpath, "swagger", version)
|
|
os.MkdirAll(dir, 0755)
|
|
|
|
fd, err := os.Create(path.Join(dir, "swagger.json"))
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
defer fd.Close()
|
|
|
|
fdyml, err := os.Create(path.Join(dir, "swagger.yml"))
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
defer fdyml.Close()
|
|
|
|
dt, err := json.MarshalIndent(api, "", " ")
|
|
dtyml, erryml := yaml.Marshal(api)
|
|
if err != nil || erryml != nil {
|
|
panic(err)
|
|
}
|
|
_, err = fd.Write(dt)
|
|
_, erryml = fdyml.Write(dtyml)
|
|
if err != nil || erryml != nil {
|
|
panic(err)
|
|
}
|
|
}
|
|
|
|
modifySwaggerIndexFile(curpath, rootapiSingle, rootapiMap)
|
|
}
|
|
|
|
// analyseNewNamespace returns version and the others params
|
|
func analyseNewNamespace(ce *ast.CallExpr) (first string, others []ast.Expr) {
|
|
for i, p := range ce.Args {
|
|
if i == 0 {
|
|
switch pp := p.(type) {
|
|
case *ast.BasicLit:
|
|
first = strings.Trim(pp.Value, `"`)
|
|
}
|
|
continue
|
|
}
|
|
others = append(others, p)
|
|
}
|
|
return
|
|
}
|
|
|
|
func analyseNSInclude(baseurl string, ce *ast.CallExpr, rootapi *swagger.Swagger) string {
|
|
cname := ""
|
|
for _, p := range ce.Args {
|
|
var x *ast.SelectorExpr
|
|
var p1 interface{} = p
|
|
if ident, ok := p1.(*ast.Ident); ok {
|
|
if assign, ok := ident.Obj.Decl.(*ast.AssignStmt); ok {
|
|
if len(assign.Rhs) > 0 {
|
|
p1 = assign.Rhs[0].(*ast.UnaryExpr)
|
|
}
|
|
}
|
|
}
|
|
if _, ok := p1.(*ast.UnaryExpr); ok {
|
|
x = p1.(*ast.UnaryExpr).X.(*ast.CompositeLit).Type.(*ast.SelectorExpr)
|
|
} else {
|
|
beeLogger.Log.Warnf("Couldn't determine type\n")
|
|
continue
|
|
}
|
|
if v, ok := importlist[fmt.Sprint(x.X)]; ok {
|
|
cname = v + x.Sel.Name
|
|
}
|
|
if apis, ok := controllerList[cname]; ok {
|
|
for rt, item := range apis {
|
|
tag := cname
|
|
if baseurl != "" {
|
|
rt = baseurl + rt
|
|
tag = strings.Trim(baseurl, "/")
|
|
}
|
|
if item.Get != nil {
|
|
item.Get.Tags = []string{tag}
|
|
}
|
|
if item.Post != nil {
|
|
item.Post.Tags = []string{tag}
|
|
}
|
|
if item.Put != nil {
|
|
item.Put.Tags = []string{tag}
|
|
}
|
|
if item.Patch != nil {
|
|
item.Patch.Tags = []string{tag}
|
|
}
|
|
if item.Head != nil {
|
|
item.Head.Tags = []string{tag}
|
|
}
|
|
if item.Delete != nil {
|
|
item.Delete.Tags = []string{tag}
|
|
}
|
|
if item.Options != nil {
|
|
item.Options.Tags = []string{tag}
|
|
}
|
|
if len(rootapi.Paths) == 0 {
|
|
rootapi.Paths = make(map[string]*swagger.Item)
|
|
}
|
|
rt = urlReplace(rt)
|
|
rootapi.Paths[rt] = item
|
|
}
|
|
}
|
|
}
|
|
return cname
|
|
}
|
|
|
|
func analyseControllerPkg(localName, pkgpath string) {
|
|
pkgpath = strings.Trim(pkgpath, "\"")
|
|
if isSystemPackage(pkgpath) {
|
|
return
|
|
}
|
|
if pkgpath == "github.com/beego/beego/v2/server/web" {
|
|
return
|
|
}
|
|
if localName != "" {
|
|
importlist[localName] = pkgpath
|
|
} else {
|
|
pps := strings.Split(pkgpath, "/")
|
|
importlist[pps[len(pps)-1]] = pkgpath
|
|
}
|
|
|
|
pkg, err := build.Default.Import(pkgpath, ".", build.FindOnly)
|
|
if err != nil {
|
|
beeLogger.Log.Fatalf("Package %s cannot be imported: %v", pkgpath, err)
|
|
}
|
|
pkgRealpath := pkg.Dir
|
|
if pkgRealpath != "" {
|
|
if _, ok := pkgCache[pkgpath]; ok {
|
|
return
|
|
}
|
|
pkgCache[pkgpath] = struct{}{}
|
|
} else {
|
|
beeLogger.Log.Fatalf("Package '%s' does not have source directory", pkgpath)
|
|
}
|
|
|
|
fileSet := token.NewFileSet()
|
|
astPkgs, err := parser.ParseDir(fileSet, pkgRealpath, func(info os.FileInfo) bool {
|
|
name := info.Name()
|
|
return !info.IsDir() && !strings.HasPrefix(name, ".") && strings.HasSuffix(name, ".go")
|
|
}, parser.ParseComments)
|
|
if err != nil {
|
|
beeLogger.Log.Fatalf("Error while parsing dir at '%s': %s", pkgpath, err)
|
|
}
|
|
|
|
for _, pkg := range astPkgs {
|
|
for _, fl := range pkg.Files {
|
|
for _, d := range fl.Decls {
|
|
switch specDecl := d.(type) {
|
|
case *ast.FuncDecl:
|
|
if specDecl.Recv != nil && len(specDecl.Recv.List) > 0 {
|
|
if t, ok := specDecl.Recv.List[0].Type.(*ast.StarExpr); ok {
|
|
// Parse controller method
|
|
parserComments(specDecl, fmt.Sprint(t.X), pkgpath)
|
|
}
|
|
}
|
|
case *ast.GenDecl:
|
|
if specDecl.Tok == token.TYPE {
|
|
for _, s := range specDecl.Specs {
|
|
switch tp := s.(*ast.TypeSpec).Type.(type) {
|
|
case *ast.StructType:
|
|
_ = tp.Struct
|
|
// Parse controller definition comments
|
|
if strings.TrimSpace(specDecl.Doc.Text()) != "" {
|
|
controllerComments[pkgpath+s.(*ast.TypeSpec).Name.String()] = specDecl.Doc.Text()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func isSystemPackage(pkgpath string) bool {
|
|
goroot := os.Getenv("GOROOT")
|
|
if goroot == "" {
|
|
goroot = runtime.GOROOT()
|
|
}
|
|
if goroot == "" {
|
|
beeLogger.Log.Fatalf("GOROOT environment variable is not set or empty")
|
|
}
|
|
|
|
wg, _ := filepath.EvalSymlinks(filepath.Join(goroot, "src", "pkg", pkgpath))
|
|
if utils.FileExists(wg) {
|
|
return true
|
|
}
|
|
|
|
//TODO(zh):support go1.4
|
|
wg, _ = filepath.EvalSymlinks(filepath.Join(goroot, "src", pkgpath))
|
|
return utils.FileExists(wg)
|
|
}
|
|
|
|
func peekNextSplitString(ss string) (s string, spacePos int) {
|
|
spacePos = strings.IndexFunc(ss, unicode.IsSpace)
|
|
if spacePos < 0 {
|
|
s = ss
|
|
spacePos = len(ss)
|
|
} else {
|
|
s = strings.TrimSpace(ss[:spacePos])
|
|
}
|
|
return
|
|
}
|
|
|
|
// parse the func comments
|
|
func parserComments(f *ast.FuncDecl, controllerName, pkgpath string) error {
|
|
var routerPath string
|
|
var HTTPMethod string
|
|
opts := swagger.Operation{
|
|
Responses: make(map[string]swagger.Response),
|
|
}
|
|
funcName := f.Name.String()
|
|
comments := f.Doc
|
|
funcParamMap := buildParamMap(f.Type.Params)
|
|
//TODO: resultMap := buildParamMap(f.Type.Results)
|
|
if comments != nil && comments.List != nil {
|
|
for _, c := range comments.List {
|
|
t := strings.TrimSpace(strings.TrimPrefix(c.Text, "//"))
|
|
if strings.HasPrefix(t, "@router") {
|
|
elements := strings.TrimSpace(t[len("@router"):])
|
|
e1 := strings.SplitN(elements, " ", 2)
|
|
if len(e1) < 1 {
|
|
return errors.New("you should has router information")
|
|
}
|
|
routerPath = e1[0]
|
|
if len(e1) == 2 && e1[1] != "" {
|
|
e1 = strings.SplitN(e1[1], " ", 2)
|
|
HTTPMethod = strings.ToUpper(strings.Trim(e1[0], "[]"))
|
|
} else {
|
|
HTTPMethod = "GET"
|
|
}
|
|
} else if strings.HasPrefix(t, "@Title") {
|
|
opts.OperationID = controllerName + "." + strings.TrimSpace(t[len("@Title"):])
|
|
} else if strings.HasPrefix(t, "@Description") {
|
|
opts.Description += fmt.Sprintf("%s\n<br>", strings.TrimSpace(t[len("@Description"):]))
|
|
} else if strings.HasPrefix(t, "@Summary") {
|
|
opts.Summary = strings.TrimSpace(t[len("@Summary"):])
|
|
} else if strings.HasPrefix(t, "@Success") {
|
|
ss := strings.TrimSpace(t[len("@Success"):])
|
|
rs := swagger.Response{}
|
|
respCode, pos := peekNextSplitString(ss)
|
|
ss = strings.TrimSpace(ss[pos:])
|
|
respType, pos := peekNextSplitString(ss)
|
|
if respType == "{object}" || respType == "{array}" {
|
|
isArray := respType == "{array}"
|
|
ss = strings.TrimSpace(ss[pos:])
|
|
schemaName, pos := peekNextSplitString(ss)
|
|
if schemaName == "" {
|
|
beeLogger.Log.Fatalf("[%s.%s] Schema must follow {object} or {array}", controllerName, funcName)
|
|
}
|
|
if strings.HasPrefix(schemaName, "[]") {
|
|
schemaName = schemaName[2:]
|
|
isArray = true
|
|
}
|
|
schema := swagger.Schema{}
|
|
if sType, ok := basicTypes[schemaName]; ok {
|
|
typeFormat := strings.Split(sType, ":")
|
|
schema.Type = typeFormat[0]
|
|
schema.Format = typeFormat[1]
|
|
} else {
|
|
m, mod, realTypes := getModel(schemaName)
|
|
schema.Ref = "#/definitions/" + m
|
|
if _, ok := modelsList[pkgpath+controllerName]; !ok {
|
|
modelsList[pkgpath+controllerName] = make(map[string]swagger.Schema)
|
|
}
|
|
modelsList[pkgpath+controllerName][schemaName] = mod
|
|
appendModels(pkgpath, controllerName, realTypes)
|
|
}
|
|
if isArray {
|
|
rs.Schema = &swagger.Schema{
|
|
Type: astTypeArray,
|
|
Items: &schema,
|
|
}
|
|
} else {
|
|
rs.Schema = &schema
|
|
}
|
|
rs.Description = strings.TrimSpace(ss[pos:])
|
|
} else {
|
|
rs.Description = strings.TrimSpace(ss)
|
|
}
|
|
opts.Responses[respCode] = rs
|
|
} else if strings.HasPrefix(t, "@Param") {
|
|
para := swagger.Parameter{}
|
|
p := getparams(strings.TrimSpace(t[len("@Param "):]))
|
|
if len(p) < 4 {
|
|
beeLogger.Log.Fatal(controllerName + "_" + funcName + "'s comments @Param should have at least 4 params")
|
|
}
|
|
paramNames := strings.SplitN(p[0], "=>", 2)
|
|
para.Name = paramNames[0]
|
|
funcParamName := para.Name
|
|
if len(paramNames) > 1 {
|
|
funcParamName = paramNames[1]
|
|
}
|
|
paramType, ok := funcParamMap[funcParamName]
|
|
if ok {
|
|
delete(funcParamMap, funcParamName)
|
|
}
|
|
|
|
switch p[1] {
|
|
case "query":
|
|
fallthrough
|
|
case "header":
|
|
fallthrough
|
|
case "path":
|
|
fallthrough
|
|
case "formData":
|
|
fallthrough
|
|
case "body":
|
|
break
|
|
default:
|
|
beeLogger.Log.Warnf("[%s.%s] Unknown param location: %s. Possible values are `query`, `header`, `path`, `formData` or `body`.\n", controllerName, funcName, p[1])
|
|
}
|
|
para.In = p[1]
|
|
pp := strings.Split(p[2], ".")
|
|
typ := pp[len(pp)-1]
|
|
if len(pp) >= 2 {
|
|
isArray := false
|
|
if p[1] == "body" && strings.HasPrefix(p[2], "[]") {
|
|
p[2] = p[2][2:]
|
|
isArray = true
|
|
}
|
|
m, mod, realTypes := getModel(p[2])
|
|
if isArray {
|
|
para.Schema = &swagger.Schema{
|
|
Type: astTypeArray,
|
|
Items: &swagger.Schema{
|
|
Ref: "#/definitions/" + m,
|
|
},
|
|
}
|
|
} else {
|
|
para.Schema = &swagger.Schema{
|
|
Ref: "#/definitions/" + m,
|
|
}
|
|
}
|
|
|
|
if _, ok := modelsList[pkgpath+controllerName]; !ok {
|
|
modelsList[pkgpath+controllerName] = make(map[string]swagger.Schema)
|
|
}
|
|
modelsList[pkgpath+controllerName][typ] = mod
|
|
appendModels(pkgpath, controllerName, realTypes)
|
|
} else {
|
|
if typ == "auto" {
|
|
typ = paramType
|
|
}
|
|
setParamType(¶, typ, pkgpath, controllerName)
|
|
}
|
|
switch len(p) {
|
|
case 5:
|
|
para.Required, _ = strconv.ParseBool(p[3])
|
|
para.Description = strings.Trim(p[4], `" `)
|
|
case 6:
|
|
para.Default = str2RealType(p[3], para.Type)
|
|
para.Required, _ = strconv.ParseBool(p[4])
|
|
para.Description = strings.Trim(p[5], `" `)
|
|
default:
|
|
para.Description = strings.Trim(p[3], `" `)
|
|
}
|
|
opts.Parameters = append(opts.Parameters, para)
|
|
} else if strings.HasPrefix(t, "@Failure") {
|
|
rs := swagger.Response{}
|
|
st := strings.TrimSpace(t[len("@Failure"):])
|
|
var cd []rune
|
|
var start bool
|
|
for i, s := range st {
|
|
if unicode.IsSpace(s) {
|
|
if start {
|
|
rs.Description = strings.TrimSpace(st[i+1:])
|
|
break
|
|
} else {
|
|
continue
|
|
}
|
|
}
|
|
start = true
|
|
cd = append(cd, s)
|
|
}
|
|
opts.Responses[string(cd)] = rs
|
|
} else if strings.HasPrefix(t, "@Deprecated") {
|
|
opts.Deprecated, _ = strconv.ParseBool(strings.TrimSpace(t[len("@Deprecated"):]))
|
|
} else if strings.HasPrefix(t, "@Accept") {
|
|
accepts := strings.Split(strings.TrimSpace(strings.TrimSpace(t[len("@Accept"):])), ",")
|
|
for _, a := range accepts {
|
|
switch a {
|
|
case "json":
|
|
opts.Consumes = append(opts.Consumes, ajson)
|
|
opts.Produces = append(opts.Produces, ajson)
|
|
case "xml":
|
|
opts.Consumes = append(opts.Consumes, axml)
|
|
opts.Produces = append(opts.Produces, axml)
|
|
case "plain":
|
|
opts.Consumes = append(opts.Consumes, aplain)
|
|
opts.Produces = append(opts.Produces, aplain)
|
|
case "html":
|
|
opts.Consumes = append(opts.Consumes, ahtml)
|
|
opts.Produces = append(opts.Produces, ahtml)
|
|
case "form":
|
|
opts.Consumes = append(opts.Consumes, aform)
|
|
}
|
|
}
|
|
} else if strings.HasPrefix(t, "@Security") {
|
|
if len(opts.Security) == 0 {
|
|
opts.Security = make([]map[string][]string, 0)
|
|
}
|
|
opts.Security = append(opts.Security, getSecurity(t))
|
|
}
|
|
}
|
|
}
|
|
routerPath = urlReplace(routerPath)
|
|
if routerPath != "" {
|
|
//Go over function parameters which were not mapped and create swagger params for them
|
|
for name, typ := range funcParamMap {
|
|
para := swagger.Parameter{}
|
|
para.Name = name
|
|
setParamType(¶, typ, pkgpath, controllerName)
|
|
if paramInPath(name, routerPath) {
|
|
para.In = "path"
|
|
} else {
|
|
para.In = "query"
|
|
}
|
|
opts.Parameters = append(opts.Parameters, para)
|
|
}
|
|
|
|
var item *swagger.Item
|
|
if itemList, ok := controllerList[pkgpath+controllerName]; ok {
|
|
if it, ok := itemList[routerPath]; !ok {
|
|
item = &swagger.Item{}
|
|
} else {
|
|
item = it
|
|
}
|
|
} else {
|
|
controllerList[pkgpath+controllerName] = make(map[string]*swagger.Item)
|
|
item = &swagger.Item{}
|
|
}
|
|
for _, hm := range strings.Split(HTTPMethod, ",") {
|
|
switch hm {
|
|
case "GET":
|
|
item.Get = &opts
|
|
case "POST":
|
|
item.Post = &opts
|
|
case "PUT":
|
|
item.Put = &opts
|
|
case "PATCH":
|
|
item.Patch = &opts
|
|
case "DELETE":
|
|
item.Delete = &opts
|
|
case "HEAD":
|
|
item.Head = &opts
|
|
case "OPTIONS":
|
|
item.Options = &opts
|
|
}
|
|
}
|
|
controllerList[pkgpath+controllerName][routerPath] = item
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func setParamType(para *swagger.Parameter, typ string, pkgpath, controllerName string) {
|
|
isArray := false
|
|
paraType := ""
|
|
paraFormat := ""
|
|
|
|
if strings.HasPrefix(typ, "[]") {
|
|
typ = typ[2:]
|
|
isArray = true
|
|
}
|
|
if typ == "string" || typ == "number" || typ == "integer" || typ == "boolean" ||
|
|
typ == astTypeArray || typ == "file" {
|
|
paraType = typ
|
|
if para.In == "body" {
|
|
para.Schema = &swagger.Schema{
|
|
Type: paraType,
|
|
}
|
|
}
|
|
} else if sType, ok := basicTypes[typ]; ok {
|
|
typeFormat := strings.Split(sType, ":")
|
|
paraType = typeFormat[0]
|
|
paraFormat = typeFormat[1]
|
|
if para.In == "body" {
|
|
para.Schema = &swagger.Schema{
|
|
Type: paraType,
|
|
Format: paraFormat,
|
|
}
|
|
}
|
|
} else {
|
|
m, mod, realTypes := getModel(typ)
|
|
para.Schema = &swagger.Schema{
|
|
Ref: "#/definitions/" + m,
|
|
}
|
|
if _, ok := modelsList[pkgpath+controllerName]; !ok {
|
|
modelsList[pkgpath+controllerName] = make(map[string]swagger.Schema)
|
|
}
|
|
modelsList[pkgpath+controllerName][typ] = mod
|
|
appendModels(pkgpath, controllerName, realTypes)
|
|
}
|
|
if isArray {
|
|
if para.In == "body" {
|
|
para.Schema = &swagger.Schema{
|
|
Type: astTypeArray,
|
|
Items: &swagger.Schema{
|
|
Type: paraType,
|
|
Format: paraFormat,
|
|
},
|
|
}
|
|
} else {
|
|
para.Type = astTypeArray
|
|
para.Items = &swagger.ParameterItems{
|
|
Type: paraType,
|
|
Format: paraFormat,
|
|
}
|
|
}
|
|
} else {
|
|
para.Type = paraType
|
|
para.Format = paraFormat
|
|
}
|
|
|
|
}
|
|
|
|
func paramInPath(name, route string) bool {
|
|
return strings.HasSuffix(route, ":"+name) ||
|
|
strings.Contains(route, ":"+name+"/")
|
|
}
|
|
|
|
func getFunctionParamType(t ast.Expr) string {
|
|
switch paramType := t.(type) {
|
|
case *ast.Ident:
|
|
return paramType.Name
|
|
// case *ast.Ellipsis:
|
|
// result := getFunctionParamType(paramType.Elt)
|
|
// result.array = true
|
|
// return result
|
|
case *ast.ArrayType:
|
|
return "[]" + getFunctionParamType(paramType.Elt)
|
|
case *ast.StarExpr:
|
|
return getFunctionParamType(paramType.X)
|
|
case *ast.SelectorExpr:
|
|
return getFunctionParamType(paramType.X) + "." + paramType.Sel.Name
|
|
default:
|
|
return ""
|
|
|
|
}
|
|
}
|
|
|
|
func buildParamMap(list *ast.FieldList) map[string]string {
|
|
i := 0
|
|
result := map[string]string{}
|
|
if list != nil {
|
|
funcParams := list.List
|
|
for _, fparam := range funcParams {
|
|
param := getFunctionParamType(fparam.Type)
|
|
var paramName string
|
|
if len(fparam.Names) > 0 {
|
|
paramName = fparam.Names[0].Name
|
|
} else {
|
|
paramName = fmt.Sprint(i)
|
|
i++
|
|
}
|
|
result[paramName] = param
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
// analisys params return []string
|
|
// @Param query form string true "The email for login"
|
|
// [query form string true "The email for login"]
|
|
func getparams(str string) []string {
|
|
var s []rune
|
|
var j int
|
|
var start bool
|
|
var r []string
|
|
var quoted int8
|
|
for _, c := range str {
|
|
if unicode.IsSpace(c) && quoted == 0 {
|
|
if !start {
|
|
continue
|
|
} else {
|
|
start = false
|
|
j++
|
|
r = append(r, string(s))
|
|
s = make([]rune, 0)
|
|
continue
|
|
}
|
|
}
|
|
|
|
start = true
|
|
if c == '"' {
|
|
quoted ^= 1
|
|
continue
|
|
}
|
|
s = append(s, c)
|
|
}
|
|
if len(s) > 0 {
|
|
r = append(r, string(s))
|
|
}
|
|
return r
|
|
}
|
|
|
|
func getModel(str string) (definitionName string, m swagger.Schema, realTypes []string) {
|
|
strs := strings.Split(str, ".")
|
|
// strs = [packageName].[objectName]
|
|
packageName := strs[0]
|
|
objectname := strs[len(strs)-1]
|
|
|
|
// Default all swagger schemas to object, if no other type is found
|
|
m.Type = astTypeObject
|
|
|
|
L:
|
|
for _, pkg := range astPkgs {
|
|
if strs[0] == pkg.Name {
|
|
for _, fl := range pkg.Files {
|
|
for k, d := range fl.Scope.Objects {
|
|
if d.Kind == ast.Typ {
|
|
if k != objectname {
|
|
// Still searching for the right object
|
|
continue
|
|
}
|
|
|
|
parseObject(fl.Imports, d, k, &m, &realTypes, astPkgs, packageName)
|
|
|
|
// When we've found the correct object, we can stop searching
|
|
break L
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if m.Title == "" {
|
|
// Don't log when error has already been logged
|
|
if _, found := rootapiDefault.Definitions[str]; !found {
|
|
beeLogger.Log.Warnf("Cannot find the object: %s", str)
|
|
}
|
|
m.Title = objectname
|
|
// TODO remove when all type have been supported
|
|
}
|
|
if len(rootapiDefault.Definitions) == 0 {
|
|
rootapiDefault.Definitions = make(map[string]swagger.Schema)
|
|
}
|
|
rootapiDefault.Definitions[str] = m
|
|
return str, m, realTypes
|
|
}
|
|
|
|
func parseObject(imports []*ast.ImportSpec, d *ast.Object, k string, m *swagger.Schema, realTypes *[]string, astPkgs []*ast.Package, packageName string) {
|
|
ts, ok := d.Decl.(*ast.TypeSpec)
|
|
if !ok {
|
|
beeLogger.Log.Fatalf("Unknown type without TypeSec: %v", d)
|
|
}
|
|
// TODO support other types, such as `MapType`, `InterfaceType` etc...
|
|
switch t := ts.Type.(type) {
|
|
case *ast.ArrayType:
|
|
m.Title = k
|
|
m.Type = astTypeArray
|
|
if isBasicType(fmt.Sprint(t.Elt)) {
|
|
typeFormat := strings.Split(basicTypes[fmt.Sprint(t.Elt)], ":")
|
|
m.Format = typeFormat[0]
|
|
} else {
|
|
objectName := packageName + "." + fmt.Sprint(t.Elt)
|
|
if _, ok := rootapiDefault.Definitions[objectName]; !ok {
|
|
objectName, _, _ = getModel(objectName)
|
|
}
|
|
m.Items = &swagger.Schema{
|
|
Ref: "#/definitions/" + objectName,
|
|
}
|
|
}
|
|
case *ast.Ident:
|
|
parseIdent(t, k, m, astPkgs)
|
|
case *ast.StructType:
|
|
parseStruct(imports, t, k, m, realTypes, astPkgs, packageName)
|
|
}
|
|
}
|
|
|
|
// parse as enum, in the package, find out all consts with the same type
|
|
func parseIdent(st *ast.Ident, k string, m *swagger.Schema, astPkgs []*ast.Package) {
|
|
m.Title = k
|
|
basicType := fmt.Sprint(st)
|
|
if object, isStdLibObject := stdlibObject[basicType]; isStdLibObject {
|
|
basicType = object
|
|
}
|
|
if t, ok := basicTypes[basicType]; ok {
|
|
typeFormat := strings.Split(t, ":")
|
|
m.Type = typeFormat[0]
|
|
m.Format = typeFormat[1]
|
|
}
|
|
enums := make(map[int]string)
|
|
enumValues := make(map[int]interface{})
|
|
for _, pkg := range astPkgs {
|
|
for _, fl := range pkg.Files {
|
|
for _, obj := range fl.Scope.Objects {
|
|
if obj.Kind == ast.Con {
|
|
vs, ok := obj.Decl.(*ast.ValueSpec)
|
|
if !ok {
|
|
beeLogger.Log.Fatalf("Unknown type without ValueSpec: %v", vs)
|
|
}
|
|
|
|
ti, ok := vs.Type.(*ast.Ident)
|
|
if !ok {
|
|
// TODO type inference, iota not support yet
|
|
continue
|
|
}
|
|
// Only add the enums that are defined by the current identifier
|
|
if ti.Name != k {
|
|
continue
|
|
}
|
|
|
|
// For all names and values, aggregate them by it's position so that we can sort them later.
|
|
for i, val := range vs.Values {
|
|
v, ok := val.(*ast.BasicLit)
|
|
if !ok {
|
|
beeLogger.Log.Warnf("Unknown type without BasicLit: %v", v)
|
|
continue
|
|
}
|
|
enums[int(val.Pos())] = fmt.Sprintf("%s = %s", vs.Names[i].Name, v.Value)
|
|
switch v.Kind {
|
|
case token.INT:
|
|
vv, err := strconv.Atoi(v.Value)
|
|
if err != nil {
|
|
beeLogger.Log.Warnf("Unknown type with BasicLit to int: %v", v.Value)
|
|
continue
|
|
}
|
|
enumValues[int(val.Pos())] = vv
|
|
case token.FLOAT:
|
|
vv, err := strconv.ParseFloat(v.Value, 64)
|
|
if err != nil {
|
|
beeLogger.Log.Warnf("Unknown type with BasicLit to int: %v", v.Value)
|
|
continue
|
|
}
|
|
enumValues[int(val.Pos())] = vv
|
|
default:
|
|
enumValues[int(val.Pos())] = strings.Trim(v.Value, `"`)
|
|
}
|
|
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// Sort the enums by position
|
|
if len(enums) > 0 {
|
|
var keys []int
|
|
for k := range enums {
|
|
keys = append(keys, k)
|
|
}
|
|
sort.Ints(keys)
|
|
for _, k := range keys {
|
|
m.Enum = append(m.Enum, enums[k])
|
|
}
|
|
// Automatically use the first enum value as the example.
|
|
m.Example = enumValues[keys[0]]
|
|
}
|
|
|
|
}
|
|
|
|
func parseStruct(imports []*ast.ImportSpec, st *ast.StructType, k string, m *swagger.Schema, realTypes *[]string, astPkgs []*ast.Package, packageName string) {
|
|
m.Title = k
|
|
if st.Fields.List != nil {
|
|
m.Properties = make(map[string]swagger.Propertie)
|
|
for _, field := range st.Fields.List {
|
|
isSlice, realType, sType := typeAnalyser(field)
|
|
if (isSlice && isBasicType(realType)) || sType == astTypeObject {
|
|
if len(strings.Split(realType, " ")) > 1 {
|
|
realType = strings.Replace(realType, " ", ".", -1)
|
|
realType = strings.Replace(realType, "&", "", -1)
|
|
realType = strings.Replace(realType, "{", "", -1)
|
|
realType = strings.Replace(realType, "}", "", -1)
|
|
} else {
|
|
realType = packageName + "." + realType
|
|
}
|
|
}
|
|
|
|
if !isBasicType(realType) && sType == astTypeObject {
|
|
checkAndLoadPackage(imports, realType, packageName)
|
|
}
|
|
|
|
*realTypes = append(*realTypes, realType)
|
|
mp := swagger.Propertie{}
|
|
isObject := false
|
|
if isSlice {
|
|
mp.Type = astTypeArray
|
|
if t, ok := basicTypes[(strings.Replace(realType, "[]", "", -1))]; ok {
|
|
typeFormat := strings.Split(t, ":")
|
|
mp.Items = &swagger.Propertie{
|
|
Type: typeFormat[0],
|
|
Format: typeFormat[1],
|
|
}
|
|
} else {
|
|
mp.Items = &swagger.Propertie{
|
|
Ref: "#/definitions/" + realType,
|
|
}
|
|
}
|
|
} else {
|
|
if sType == astTypeObject {
|
|
isObject = true
|
|
mp.Ref = "#/definitions/" + realType
|
|
} else if isBasicType(realType) {
|
|
typeFormat := strings.Split(sType, ":")
|
|
mp.Type = typeFormat[0]
|
|
mp.Format = typeFormat[1]
|
|
} else if realType == astTypeMap {
|
|
typeFormat := strings.Split(sType, ":")
|
|
mp.AdditionalProperties = &swagger.Propertie{
|
|
Type: typeFormat[0],
|
|
Format: typeFormat[1],
|
|
}
|
|
}
|
|
}
|
|
if field.Names != nil {
|
|
|
|
// set property name as field name
|
|
var name = field.Names[0].Name
|
|
|
|
// if no tag skip tag processing
|
|
if field.Tag == nil {
|
|
m.Properties[name] = mp
|
|
continue
|
|
}
|
|
|
|
var tagValues []string
|
|
|
|
stag := reflect.StructTag(strings.Trim(field.Tag.Value, "`"))
|
|
|
|
defaultValue := stag.Get("doc")
|
|
if defaultValue != "" {
|
|
r, _ := regexp.Compile(`default\((.*)\)`)
|
|
if r.MatchString(defaultValue) {
|
|
res := r.FindStringSubmatch(defaultValue)
|
|
mp.Default = str2RealType(res[1], realType)
|
|
|
|
} else {
|
|
beeLogger.Log.Warnf("Invalid default value: %s", defaultValue)
|
|
}
|
|
}
|
|
|
|
if ignore := stag.Get("ignore"); ignore != "" {
|
|
continue
|
|
}
|
|
|
|
tag := stag.Get("json")
|
|
if tag != "" {
|
|
tagValues = strings.Split(tag, ",")
|
|
}
|
|
|
|
// dont add property if json tag first value is "-"
|
|
if len(tagValues) == 0 || tagValues[0] != "-" {
|
|
|
|
// set property name to the left most json tag value only if is not omitempty
|
|
if len(tagValues) > 0 && tagValues[0] != "omitempty" {
|
|
name = tagValues[0]
|
|
}
|
|
|
|
// set property type to the second tag value only if it is not omitempty and isBasicType
|
|
if len(tagValues) > 1 && tagValues[1] != "omitempty" && isBasicType(tagValues[1]) {
|
|
typeFormat := strings.Split(basicTypes[tagValues[1]], ":")
|
|
mp.Type = typeFormat[0]
|
|
mp.Format = typeFormat[1]
|
|
mp.Ref = ""
|
|
}
|
|
|
|
if thrifttag := stag.Get("thrift"); thrifttag != "" {
|
|
ts := strings.Split(thrifttag, ",")
|
|
if ts[0] != "" {
|
|
name = ts[0]
|
|
}
|
|
}
|
|
if required := stag.Get("required"); strings.EqualFold(required, "true") {
|
|
m.Required = append(m.Required, name)
|
|
}
|
|
if desc := stag.Get("description"); desc != "" {
|
|
mp.Description = desc
|
|
}
|
|
|
|
if mp.Description == "" && field.Comment != nil {
|
|
mp.Description = strings.TrimSpace(field.Comment.Text())
|
|
}
|
|
|
|
if example := stag.Get("example"); example != "" && !isObject && !isSlice {
|
|
mp.Example = str2RealType(example, realType)
|
|
}
|
|
|
|
m.Properties[name] = mp
|
|
}
|
|
} else {
|
|
// only parse case of when embedded field is TypeName
|
|
// cases of *TypeName and Interface are not handled, maybe useless for swagger spec
|
|
tag := ""
|
|
if field.Tag != nil {
|
|
stag := reflect.StructTag(strings.Trim(field.Tag.Value, "`"))
|
|
if ignore := stag.Get("ignore"); ignore != "" {
|
|
continue
|
|
}
|
|
tag = stag.Get("json")
|
|
}
|
|
|
|
if tag != "" {
|
|
tagValues := strings.Split(tag, ",")
|
|
if tagValues[0] == "-" {
|
|
//if json tag is "-", omit
|
|
continue
|
|
} else {
|
|
//if json tag is "something", output: something #definition/pkgname.Type
|
|
m.Properties[tagValues[0]] = mp
|
|
continue
|
|
}
|
|
} else {
|
|
//if no json tag, expand all fields of the type here
|
|
nm := &swagger.Schema{}
|
|
for _, pkg := range astPkgs {
|
|
for _, fl := range pkg.Files {
|
|
for nameOfObj, obj := range fl.Scope.Objects {
|
|
if pkg.Name+"."+obj.Name == realType {
|
|
parseObject(imports, obj, nameOfObj, nm, realTypes, astPkgs, pkg.Name)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
for name, p := range nm.Properties {
|
|
m.Properties[name] = p
|
|
}
|
|
m.Required = append(m.Required, nm.Required...)
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func typeAnalyser(f *ast.Field) (isSlice bool, realType, swaggerType string) {
|
|
if arr, ok := f.Type.(*ast.ArrayType); ok {
|
|
if isBasicType(fmt.Sprint(arr.Elt)) {
|
|
return true, fmt.Sprintf("[]%v", arr.Elt), basicTypes[fmt.Sprint(arr.Elt)]
|
|
}
|
|
if object, isCustomObject := customObject[fmt.Sprint(arr.Elt)]; isCustomObject {
|
|
return true, fmt.Sprintf("[]%v", object), basicTypes[object]
|
|
}
|
|
if mp, ok := arr.Elt.(*ast.MapType); ok {
|
|
return false, fmt.Sprintf("map[%v][%v]", mp.Key, mp.Value), astTypeObject
|
|
}
|
|
if star, ok := arr.Elt.(*ast.StarExpr); ok {
|
|
return true, fmt.Sprint(star.X), astTypeObject
|
|
}
|
|
return true, fmt.Sprint(arr.Elt), astTypeObject
|
|
}
|
|
switch t := f.Type.(type) {
|
|
case *ast.StarExpr:
|
|
basicType := fmt.Sprint(t.X)
|
|
if object, isStdLibObject := stdlibObject[basicType]; isStdLibObject {
|
|
basicType = object
|
|
}
|
|
if k, ok := basicTypes[basicType]; ok {
|
|
return false, basicType, k
|
|
}
|
|
return false, basicType, astTypeObject
|
|
case *ast.MapType:
|
|
val := fmt.Sprintf("%v", t.Value)
|
|
if isBasicType(val) {
|
|
return false, astTypeMap, basicTypes[val]
|
|
}
|
|
return false, val, astTypeObject
|
|
case *ast.InterfaceType:
|
|
return false, "interface", astTypeObject
|
|
}
|
|
basicType := fmt.Sprint(f.Type)
|
|
if object, isStdLibObject := stdlibObject[basicType]; isStdLibObject {
|
|
basicType = object
|
|
}
|
|
if k, ok := basicTypes[basicType]; ok {
|
|
return false, basicType, k
|
|
}
|
|
return false, basicType, astTypeObject
|
|
}
|
|
|
|
func isBasicType(Type string) bool {
|
|
if _, ok := basicTypes[Type]; ok {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// append models
|
|
func appendModels(pkgpath, controllerName string, realTypes []string) {
|
|
for _, realType := range realTypes {
|
|
if realType != "" && !isBasicType(strings.TrimLeft(realType, "[]")) &&
|
|
!strings.HasPrefix(realType, astTypeMap) && !strings.HasPrefix(realType, "&") {
|
|
if _, ok := modelsList[pkgpath+controllerName][realType]; ok {
|
|
continue
|
|
}
|
|
_, mod, newRealTypes := getModel(realType)
|
|
modelsList[pkgpath+controllerName][realType] = mod
|
|
appendModels(pkgpath, controllerName, newRealTypes)
|
|
}
|
|
}
|
|
}
|
|
|
|
func getSecurity(t string) (security map[string][]string) {
|
|
security = make(map[string][]string)
|
|
p := getparams(strings.TrimSpace(t[len("@Security"):]))
|
|
if len(p) == 0 {
|
|
beeLogger.Log.Fatalf("No params for security specified\n")
|
|
}
|
|
security[p[0]] = make([]string, 0)
|
|
for i := 1; i < len(p); i++ {
|
|
security[p[0]] = append(security[p[0]], p[i])
|
|
}
|
|
return
|
|
}
|
|
|
|
func urlReplace(src string) string {
|
|
pt := strings.Split(src, "/")
|
|
for i, p := range pt {
|
|
if len(p) > 0 {
|
|
if p[0] == ':' {
|
|
pt[i] = "{" + p[1:] + "}"
|
|
} else if p[0] == '?' && p[1] == ':' {
|
|
pt[i] = "{" + p[2:] + "}"
|
|
}
|
|
|
|
if pt[i][0] == '{' && strings.Contains(pt[i], ":") {
|
|
pt[i] = pt[i][:strings.Index(pt[i], ":")] + "}"
|
|
} else if pt[i][0] == '{' && strings.Contains(pt[i], "(") {
|
|
pt[i] = pt[i][:strings.Index(pt[i], "(")] + "}"
|
|
}
|
|
}
|
|
}
|
|
return strings.Join(pt, "/")
|
|
}
|
|
|
|
func str2RealType(s string, typ string) interface{} {
|
|
var err error
|
|
var ret interface{}
|
|
|
|
switch typ {
|
|
case "int", "int64", "int32", "int16", "int8":
|
|
ret, err = strconv.Atoi(s)
|
|
case "uint", "uint64", "uint32", "uint16", "uint8":
|
|
ret, err = strconv.ParseUint(s, 10, 0)
|
|
case "bool":
|
|
ret, err = strconv.ParseBool(s)
|
|
case "float64":
|
|
ret, err = strconv.ParseFloat(s, 64)
|
|
case "float32":
|
|
ret, err = strconv.ParseFloat(s, 32)
|
|
default:
|
|
return s
|
|
}
|
|
|
|
if err != nil {
|
|
beeLogger.Log.Warnf("Invalid default value type '%s': %s", typ, s)
|
|
return s
|
|
}
|
|
|
|
return ret
|
|
}
|
|
|
|
func checkAndLoadPackage(imports []*ast.ImportSpec, realType, curPkgName string) {
|
|
arr := strings.Split(realType, ".")
|
|
if len(arr) != 2 {
|
|
return
|
|
}
|
|
objectPkgName := arr[0]
|
|
if objectPkgName == curPkgName {
|
|
return
|
|
}
|
|
pkgPath := ""
|
|
for _, im := range imports {
|
|
importPath := ""
|
|
if im.Path != nil {
|
|
importPath = strings.Trim(im.Path.Value, `"`)
|
|
}
|
|
|
|
if importPath == "" {
|
|
continue
|
|
}
|
|
|
|
if im.Name != nil && im.Name.Name == objectPkgName {
|
|
pkgPath = importPath
|
|
break
|
|
}
|
|
|
|
_, pkgName := filepath.Split(importPath)
|
|
if pkgName == objectPkgName {
|
|
pkgPath = importPath
|
|
break
|
|
}
|
|
}
|
|
|
|
if pkgPath == "" {
|
|
beeLogger.Log.Warnf("%s missing import package", realType)
|
|
return
|
|
}
|
|
|
|
if isSystemPackage(pkgPath) {
|
|
return
|
|
}
|
|
if _, ok := pkgLoadedCache[pkgPath]; ok {
|
|
return
|
|
}
|
|
|
|
pkg, err := build.Default.Import(pkgPath, ".", build.FindOnly)
|
|
if err != nil {
|
|
beeLogger.Log.Warnf("Package %s cannot be imported, err:%v", pkgPath, err)
|
|
return
|
|
}
|
|
pkgRealpath := pkg.Dir
|
|
|
|
fileSet := token.NewFileSet()
|
|
pkgs, err := parser.ParseDir(fileSet, pkgRealpath, func(info os.FileInfo) bool {
|
|
name := info.Name()
|
|
return !info.IsDir() && !strings.HasPrefix(name, ".") && strings.HasSuffix(name, ".go")
|
|
}, parser.ParseComments)
|
|
if err != nil {
|
|
beeLogger.Log.Warnf("Error while parsing dir at '%s': %s", pkgRealpath, err)
|
|
}
|
|
|
|
for _, pkg := range pkgs {
|
|
astPkgs = append(astPkgs, pkg)
|
|
}
|
|
|
|
pkgLoadedCache[pkgPath] = struct{}{}
|
|
}
|
|
|
|
func modifySwaggerIndexFile(curpath string, rootapiSingle bool, rootapiMap map[string]*swagger.Swagger) {
|
|
swaggerIndexFilename := "index.html"
|
|
swaggerIndexFullname := path.Join(curpath, "swagger", swaggerIndexFilename)
|
|
|
|
swaggerJsonFilename := "swagger.json"
|
|
swaggerUIBundleSign := `SwaggerUIBundle(`
|
|
var swaggerUIBundleSignPass bool
|
|
|
|
if _, err := os.Stat(swaggerIndexFullname); !os.IsNotExist(err) {
|
|
var swaggerJsonUrl string
|
|
if rootapiSingle {
|
|
swaggerJsonUrl = fmt.Sprintf(` url: "%s",`, swaggerJsonFilename)
|
|
} else {
|
|
urls := make([]string, 0)
|
|
for namespace, _ := range rootapiMap {
|
|
namespace = strings.TrimLeft(namespace, "/")
|
|
urls = append(urls, fmt.Sprintf(`{url: "%s", name: "%s"}`, path.Join(namespace, swaggerJsonFilename), namespace))
|
|
}
|
|
swaggerJsonUrl = fmt.Sprintf(` urls: [%s],`, strings.Join(urls, ", "))
|
|
}
|
|
|
|
var indexFileContent string
|
|
if f, err := os.Open(swaggerIndexFullname); err == nil {
|
|
defer f.Close()
|
|
|
|
buf := bufio.NewReader(f)
|
|
for {
|
|
line, _, c := buf.ReadLine()
|
|
if c == io.EOF {
|
|
break
|
|
}
|
|
|
|
if strings.Contains(string(line), swaggerUIBundleSign) {
|
|
swaggerUIBundleSignPass = true
|
|
}
|
|
if swaggerUIBundleSignPass && (strings.Contains(string(line), "url:") || strings.Contains(string(line), "urls:")) {
|
|
indexFileContent += swaggerJsonUrl + "\n"
|
|
} else {
|
|
indexFileContent += string(line) + "\n"
|
|
}
|
|
}
|
|
}
|
|
|
|
if fw, err := os.OpenFile(swaggerIndexFullname, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666); err == nil {
|
|
defer fw.Close()
|
|
|
|
w := bufio.NewWriter(fw)
|
|
w.WriteString(indexFileContent)
|
|
w.Flush()
|
|
}
|
|
}
|
|
}
|