mirror of https://github.com/beego/bee.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
957 lines
26 KiB
957 lines
26 KiB
// 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 ( |
|
"encoding/json" |
|
"errors" |
|
"fmt" |
|
"go/ast" |
|
"go/parser" |
|
"go/token" |
|
"os" |
|
"path" |
|
"path/filepath" |
|
"reflect" |
|
"regexp" |
|
"runtime" |
|
"strconv" |
|
"strings" |
|
"unicode" |
|
|
|
"gopkg.in/yaml.v2" |
|
|
|
"github.com/astaxie/beego/swagger" |
|
"github.com/astaxie/beego/utils" |
|
beeLogger "github.com/beego/bee/logger" |
|
bu "github.com/beego/bee/utils" |
|
) |
|
|
|
const ( |
|
ajson = "application/json" |
|
axml = "application/xml" |
|
aplain = "text/plain" |
|
ahtml = "text/html" |
|
) |
|
|
|
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 rootapi swagger.Swagger |
|
var astPkgs map[string]*ast.Package |
|
|
|
// 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:string", |
|
} |
|
|
|
var stdlibObject = map[string]string{ |
|
"&{time Time}": "time.Time", |
|
} |
|
|
|
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) |
|
astPkgs = map[string]*ast.Package{} |
|
} |
|
|
|
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 |
|
} |
|
|
|
if !strings.Contains(fpath, "vendor") && !strings.Contains(fpath, "tests") { |
|
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 k, v := range folderPkgs { |
|
astPkgs[k] = v |
|
} |
|
|
|
return nil |
|
} |
|
|
|
func GenerateDocs(curpath string) { |
|
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) |
|
} |
|
|
|
rootapi.Infos = swagger.Information{} |
|
rootapi.SwaggerVersion = "2.0" |
|
|
|
// Analyse API comments |
|
if f.Comments != nil { |
|
for _, c := range f.Comments { |
|
for _, s := range strings.Split(c.Text(), "\n") { |
|
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 = 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"):]) |
|
} |
|
} |
|
} |
|
} |
|
|
|
// Analyse controller package |
|
for _, im := range f.Imports { |
|
localName := "" |
|
if im.Name != nil { |
|
localName = im.Name.Name |
|
} |
|
analyseControllerPkg(path.Join(curpath, "vendor"), 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 { |
|
// Analyse NewNamespace, it will return version and the subfunction |
|
if selName := v.Fun.(*ast.SelectorExpr).Sel.String(); selName != "NewNamespace" { |
|
continue |
|
} |
|
version, params := analyseNewNamespace(v) |
|
if rootapi.BasePath == "" && version != "" { |
|
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) |
|
for _, sp := range params { |
|
switch pp := sp.(type) { |
|
case *ast.CallExpr: |
|
if pp.Fun.(*ast.SelectorExpr).Sel.String() == "NSInclude" { |
|
controllerName = analyseNSInclude(s, pp) |
|
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) |
|
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, |
|
}) |
|
} |
|
} |
|
} |
|
} |
|
} |
|
|
|
} |
|
} |
|
} |
|
} |
|
} |
|
os.Mkdir(path.Join(curpath, "swagger"), 0755) |
|
fd, err := os.Create(path.Join(curpath, "swagger", "swagger.json")) |
|
if err != nil { |
|
panic(err) |
|
} |
|
fdyml, err := os.Create(path.Join(curpath, "swagger", "swagger.yml")) |
|
if err != nil { |
|
panic(err) |
|
} |
|
defer fdyml.Close() |
|
defer fd.Close() |
|
dt, err := json.MarshalIndent(rootapi, "", " ") |
|
dtyml, erryml := yaml.Marshal(rootapi) |
|
if err != nil || erryml != nil { |
|
panic(err) |
|
} |
|
_, err = fd.Write(dt) |
|
_, erryml = fdyml.Write(dtyml) |
|
if err != nil || erryml != nil { |
|
panic(err) |
|
} |
|
} |
|
|
|
// 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) string { |
|
cname := "" |
|
for _, p := range ce.Args { |
|
x := p.(*ast.UnaryExpr).X.(*ast.CompositeLit).Type.(*ast.SelectorExpr) |
|
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(vendorPath, localName, pkgpath string) { |
|
pkgpath = strings.Trim(pkgpath, "\"") |
|
if isSystemPackage(pkgpath) { |
|
return |
|
} |
|
if pkgpath == "github.com/astaxie/beego" { |
|
return |
|
} |
|
if localName != "" { |
|
importlist[localName] = pkgpath |
|
} else { |
|
pps := strings.Split(pkgpath, "/") |
|
importlist[pps[len(pps)-1]] = pkgpath |
|
} |
|
gopaths := bu.GetGOPATHs() |
|
if len(gopaths) == 0 { |
|
beeLogger.Log.Fatal("GOPATH environment variable is not set or empty") |
|
} |
|
pkgRealpath := "" |
|
|
|
wg, _ := filepath.EvalSymlinks(filepath.Join(vendorPath, pkgpath)) |
|
if utils.FileExists(wg) { |
|
pkgRealpath = wg |
|
} else { |
|
wgopath := gopaths |
|
for _, wg := range wgopath { |
|
wg, _ = filepath.EvalSymlinks(filepath.Join(wg, "src", pkgpath)) |
|
if utils.FileExists(wg) { |
|
pkgRealpath = wg |
|
break |
|
} |
|
} |
|
} |
|
if pkgRealpath != "" { |
|
if _, ok := pkgCache[pkgpath]; ok { |
|
return |
|
} |
|
pkgCache[pkgpath] = struct{}{} |
|
} else { |
|
beeLogger.Log.Fatalf("Package '%s' does not exist in the GOPATH or vendor path", 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.Doc, specDecl.Name.String(), 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(comments *ast.CommentGroup, funcName, controllerName, pkgpath string) error { |
|
var routerPath string |
|
var HTTPMethod string |
|
opts := swagger.Operation{ |
|
Responses: make(map[string]swagger.Response), |
|
} |
|
if comments != nil && comments.List != nil { |
|
for _, c := range comments.List { |
|
t := strings.TrimSpace(strings.TrimLeft(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 infomation") |
|
} |
|
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 = 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: "array", |
|
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") |
|
} |
|
para.Name = p[0] |
|
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 { |
|
m, mod, realTypes := getModel(p[2]) |
|
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 { |
|
isArray := false |
|
paraType := "" |
|
paraFormat := "" |
|
if strings.HasPrefix(typ, "[]") { |
|
typ = typ[2:] |
|
isArray = true |
|
} |
|
if typ == "string" || typ == "number" || typ == "integer" || typ == "boolean" || |
|
typ == "array" || typ == "file" { |
|
paraType = typ |
|
} else if sType, ok := basicTypes[typ]; ok { |
|
typeFormat := strings.Split(sType, ":") |
|
paraType = typeFormat[0] |
|
paraFormat = typeFormat[1] |
|
} else { |
|
beeLogger.Log.Warnf("[%s.%s] Unknown param type: %s\n", controllerName, funcName, typ) |
|
} |
|
if isArray { |
|
para.Type = "array" |
|
para.Items = &swagger.ParameterItems{ |
|
Type: paraType, |
|
Format: paraFormat, |
|
} |
|
} else { |
|
para.Type = paraType |
|
para.Format = paraFormat |
|
} |
|
} |
|
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) |
|
} |
|
} |
|
} |
|
} |
|
} |
|
if routerPath != "" { |
|
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{} |
|
} |
|
switch HTTPMethod { |
|
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 |
|
} |
|
|
|
// 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 []rune(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) (objectname string, m swagger.Schema, realTypes []string) { |
|
strs := strings.Split(str, ".") |
|
objectname = strs[len(strs)-1] |
|
packageName := "" |
|
m.Type = "object" |
|
for _, pkg := range astPkgs { |
|
for _, fl := range pkg.Files { |
|
for k, d := range fl.Scope.Objects { |
|
if d.Kind == ast.Typ { |
|
if k != objectname { |
|
continue |
|
} |
|
packageName = pkg.Name |
|
parseObject(d, k, &m, &realTypes, astPkgs, pkg.Name) |
|
} |
|
} |
|
} |
|
} |
|
if m.Title == "" { |
|
beeLogger.Log.Warnf("Cannot find the object: %s", str) |
|
// TODO remove when all type have been supported |
|
//os.Exit(1) |
|
} |
|
if len(rootapi.Definitions) == 0 { |
|
rootapi.Definitions = make(map[string]swagger.Schema) |
|
} |
|
objectname = packageName + "." + objectname |
|
rootapi.Definitions[objectname] = m |
|
return |
|
} |
|
|
|
func parseObject(d *ast.Object, k string, m *swagger.Schema, realTypes *[]string, astPkgs map[string]*ast.Package, packageName string) { |
|
ts, ok := d.Decl.(*ast.TypeSpec) |
|
if !ok { |
|
beeLogger.Log.Fatalf("Unknown type without TypeSec: %v\n", d) |
|
} |
|
// TODO support other types, such as `ArrayType`, `MapType`, `InterfaceType` etc... |
|
st, ok := ts.Type.(*ast.StructType) |
|
if !ok { |
|
return |
|
} |
|
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 == "object" { |
|
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 |
|
} |
|
} |
|
*realTypes = append(*realTypes, realType) |
|
mp := swagger.Propertie{} |
|
if isSlice { |
|
mp.Type = "array" |
|
if isBasicType(realType) { |
|
typeFormat := strings.Split(sType, ":") |
|
mp.Items = &swagger.Propertie{ |
|
Type: typeFormat[0], |
|
Format: typeFormat[1], |
|
} |
|
} else { |
|
mp.Items = &swagger.Propertie{ |
|
Ref: "#/definitions/" + realType, |
|
} |
|
} |
|
} else { |
|
if sType == "object" { |
|
mp.Ref = "#/definitions/" + realType |
|
} else if isBasicType(realType) { |
|
typeFormat := strings.Split(sType, ":") |
|
mp.Type = typeFormat[0] |
|
mp.Format = typeFormat[1] |
|
} else if realType == "map" { |
|
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) |
|
} |
|
} |
|
|
|
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] |
|
} |
|
|
|
if thrifttag := stag.Get("thrift"); thrifttag != "" { |
|
ts := strings.Split(thrifttag, ",") |
|
if ts[0] != "" { |
|
name = ts[0] |
|
} |
|
} |
|
if required := stag.Get("required"); required != "" { |
|
m.Required = append(m.Required, name) |
|
} |
|
if desc := stag.Get("description"); desc != "" { |
|
mp.Description = desc |
|
} |
|
|
|
m.Properties[name] = mp |
|
} |
|
if ignore := stag.Get("ignore"); ignore != "" { |
|
continue |
|
} |
|
} else { |
|
for _, pkg := range astPkgs { |
|
for _, fl := range pkg.Files { |
|
for nameOfObj, obj := range fl.Scope.Objects { |
|
if obj.Name == fmt.Sprint(field.Type) { |
|
parseObject(obj, nameOfObj, m, realTypes, astPkgs, pkg.Name) |
|
} |
|
} |
|
} |
|
} |
|
} |
|
} |
|
} |
|
} |
|
|
|
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 false, fmt.Sprintf("[]%v", arr.Elt), basicTypes[fmt.Sprint(arr.Elt)] |
|
} |
|
if mp, ok := arr.Elt.(*ast.MapType); ok { |
|
return false, fmt.Sprintf("map[%v][%v]", mp.Key, mp.Value), "object" |
|
} |
|
if star, ok := arr.Elt.(*ast.StarExpr); ok { |
|
return true, fmt.Sprint(star.X), "object" |
|
} |
|
return true, fmt.Sprint(arr.Elt), "object" |
|
} |
|
switch t := f.Type.(type) { |
|
case *ast.StarExpr: |
|
return false, fmt.Sprint(t.X), "object" |
|
case *ast.MapType: |
|
val := fmt.Sprintf("%v", t.Value) |
|
if isBasicType(val) { |
|
return false, "map", basicTypes[val] |
|
} |
|
return false, val, "object" |
|
} |
|
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, "object" |
|
} |
|
|
|
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, "map") && !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 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:] + "}" |
|
} |
|
} |
|
} |
|
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 "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 |
|
}
|
|
|