2014-06-19 13:09:17 +00:00
// 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.
2017-03-06 23:58:53 +00:00
package swaggergen
2014-06-18 04:19:03 +00:00
import (
"encoding/json"
"errors"
"fmt"
"go/ast"
"go/parser"
"go/token"
"os"
"path"
"path/filepath"
2014-06-23 14:00:57 +00:00
"reflect"
2014-06-21 13:27:12 +00:00
"regexp"
2017-02-21 08:30:55 +00:00
"runtime"
2014-06-18 04:19:03 +00:00
"strconv"
"strings"
2014-06-20 11:47:03 +00:00
"unicode"
2014-06-18 04:19:03 +00:00
2016-09-14 21:09:54 +00:00
"gopkg.in/yaml.v2"
2014-06-18 04:19:03 +00:00
"github.com/astaxie/beego/swagger"
"github.com/astaxie/beego/utils"
2017-03-06 23:58:53 +00:00
beeLogger "github.com/beego/bee/logger"
2014-06-18 04:19:03 +00:00
)
2014-06-20 11:47:03 +00:00
const (
ajson = "application/json"
axml = "application/xml"
aplain = "text/plain"
ahtml = "text/html"
)
2016-08-01 06:40:43 +00:00
var pkgCache map [ string ] struct { } //pkg:controller:function:comments comments: key:value
2014-06-18 04:19:03 +00:00
var controllerComments map [ string ] string
var importlist map [ string ] string
2016-08-08 08:44:49 +00:00
var controllerList map [ string ] map [ string ] * swagger . Item //controllername Paths items
2016-08-01 06:40:43 +00:00
var modelsList map [ string ] map [ string ] swagger . Schema
var rootapi swagger . Swagger
2016-09-24 22:34:26 +00:00
var astPkgs map [ string ] * ast . Package
// refer to builtin.go
var basicTypes = map [ string ] string {
2017-03-01 10:42:14 +00:00
"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
2017-03-06 23:58:53 +00:00
"time.Time" : "string:string" ,
2017-03-01 10:42:14 +00:00
}
var stdlibObject = map [ string ] string {
"&{time Time}" : "time.Time" ,
2016-09-24 22:34:26 +00:00
}
2014-06-18 04:19:03 +00:00
func init ( ) {
2016-08-01 06:40:43 +00:00
pkgCache = make ( map [ string ] struct { } )
2014-06-18 04:19:03 +00:00
controllerComments = make ( map [ string ] string )
importlist = make ( map [ string ] string )
2016-08-08 08:44:49 +00:00
controllerList = make ( map [ string ] map [ string ] * swagger . Item )
2016-08-01 06:40:43 +00:00
modelsList = make ( map [ string ] map [ string ] swagger . Schema )
2016-09-24 22:34:26 +00:00
astPkgs = map [ string ] * ast . Package { }
}
2017-03-06 23:58:53 +00:00
func ParsePackagesFromDir ( dirpath string ) {
2016-11-20 10:46:42 +00:00
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
}
2016-12-14 15:57:59 +00:00
if ! strings . Contains ( fpath , "vendor" ) && ! strings . Contains ( fpath , "tests" ) {
2016-11-20 10:46:42 +00:00
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 {
2017-03-06 23:58:53 +00:00
beeLogger . Log . Warnf ( "%s" , err )
2016-09-24 22:34:26 +00:00
}
}
2016-11-20 10:46:42 +00:00
func parsePackageFromDir ( path string ) error {
2016-09-24 22:34:26 +00:00
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 {
2016-11-20 10:46:42 +00:00
return err
2016-09-24 22:34:26 +00:00
}
2016-11-13 14:14:48 +00:00
2016-09-24 22:34:26 +00:00
for k , v := range folderPkgs {
astPkgs [ k ] = v
}
2016-11-20 10:46:42 +00:00
return nil
2014-06-18 04:19:03 +00:00
}
2017-03-06 23:58:53 +00:00
func GenerateDocs ( curpath string ) {
2014-06-18 04:19:03 +00:00
fset := token . NewFileSet ( )
f , err := parser . ParseFile ( fset , path . Join ( curpath , "routers" , "router.go" ) , nil , parser . ParseComments )
if err != nil {
2017-03-15 16:44:29 +00:00
beeLogger . Log . Fatalf ( "Error while parsing router.go: %s" , err )
2014-06-18 04:19:03 +00:00
}
2016-08-01 06:40:43 +00:00
rootapi . Infos = swagger . Information { }
rootapi . SwaggerVersion = "2.0"
2016-11-13 14:14:48 +00:00
// Analyse API comments
2014-06-18 04:19:03 +00:00
if f . Comments != nil {
for _ , c := range f . Comments {
for _ , s := range strings . Split ( c . Text ( ) , "\n" ) {
if strings . HasPrefix ( s , "@APIVersion" ) {
2016-08-01 06:40:43 +00:00
rootapi . Infos . Version = strings . TrimSpace ( s [ len ( "@APIVersion" ) : ] )
2014-06-18 04:19:03 +00:00
} else if strings . HasPrefix ( s , "@Title" ) {
2016-08-01 06:40:43 +00:00
rootapi . Infos . Title = strings . TrimSpace ( s [ len ( "@Title" ) : ] )
2014-06-18 04:19:03 +00:00
} else if strings . HasPrefix ( s , "@Description" ) {
2016-08-01 06:40:43 +00:00
rootapi . Infos . Description = strings . TrimSpace ( s [ len ( "@Description" ) : ] )
2014-06-18 04:19:03 +00:00
} else if strings . HasPrefix ( s , "@TermsOfServiceUrl" ) {
2016-08-13 06:45:12 +00:00
rootapi . Infos . TermsOfService = strings . TrimSpace ( s [ len ( "@TermsOfServiceUrl" ) : ] )
2014-06-18 04:19:03 +00:00
} else if strings . HasPrefix ( s , "@Contact" ) {
2016-08-01 06:40:43 +00:00
rootapi . Infos . Contact . EMail = strings . TrimSpace ( s [ len ( "@Contact" ) : ] )
2016-09-14 21:09:54 +00:00
} 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" ) : ] )
2016-11-14 11:07:22 +00:00
} else if strings . HasPrefix ( s , "@LicenseUrl" ) {
2016-09-22 14:26:39 +00:00
if rootapi . Infos . License == nil {
2016-11-20 10:46:42 +00:00
rootapi . Infos . License = & swagger . License { URL : strings . TrimSpace ( s [ len ( "@LicenseUrl" ) : ] ) }
2016-09-22 14:26:39 +00:00
} else {
2016-11-20 10:46:42 +00:00
rootapi . Infos . License . URL = strings . TrimSpace ( s [ len ( "@LicenseUrl" ) : ] )
2016-09-22 14:26:39 +00:00
}
2016-11-14 11:07:22 +00:00
} else if strings . HasPrefix ( s , "@License" ) {
2016-09-22 14:26:39 +00:00
if rootapi . Infos . License == nil {
2016-11-20 10:46:42 +00:00
rootapi . Infos . License = & swagger . License { Name : strings . TrimSpace ( s [ len ( "@License" ) : ] ) }
2016-09-22 14:26:39 +00:00
} else {
2016-11-14 11:07:22 +00:00
rootapi . Infos . License . Name = strings . TrimSpace ( s [ len ( "@License" ) : ] )
2016-09-22 14:26:39 +00:00
}
2016-09-14 21:09:54 +00:00
} 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" ) : ] )
2014-06-18 04:19:03 +00:00
}
}
}
}
2016-11-13 14:14:48 +00:00
// Analyse controller package
2014-06-18 04:19:03 +00:00
for _ , im := range f . Imports {
2015-05-14 07:56:19 +00:00
localName := ""
if im . Name != nil {
localName = im . Name . Name
}
2016-11-13 14:14:48 +00:00
analyseControllerPkg ( localName , im . Path . Value )
2014-06-18 04:19:03 +00:00
}
for _ , d := range f . Decls {
switch specDecl := d . ( type ) {
case * ast . FuncDecl :
for _ , l := range specDecl . Body . List {
2016-08-08 08:44:49 +00:00
switch stmt := l . ( type ) {
2014-06-18 04:19:03 +00:00
case * ast . AssignStmt :
2016-08-08 08:44:49 +00:00
for _ , l := range stmt . Rhs {
2014-08-01 15:40:48 +00:00
if v , ok := l . ( * ast . CallExpr ) ; ok {
2016-11-13 14:14:48 +00:00
// Analyse NewNamespace, it will return version and the subfunction
2016-08-18 15:31:09 +00:00
if selName := v . Fun . ( * ast . SelectorExpr ) . Sel . String ( ) ; selName != "NewNamespace" {
continue
}
2016-11-13 14:14:48 +00:00
version , params := analyseNewNamespace ( v )
2016-08-18 15:31:09 +00:00
if rootapi . BasePath == "" && version != "" {
rootapi . BasePath = version
}
2014-08-01 15:40:48 +00:00
for _ , p := range params {
switch pp := p . ( type ) {
case * ast . CallExpr :
2016-08-08 08:44:49 +00:00
controllerName := ""
2014-08-01 15:40:48 +00:00
if selname := pp . Fun . ( * ast . SelectorExpr ) . Sel . String ( ) ; selname == "NSNamespace" {
2016-11-13 14:14:48 +00:00
s , params := analyseNewNamespace ( pp )
2014-08-01 15:40:48 +00:00
for _ , sp := range params {
switch pp := sp . ( type ) {
case * ast . CallExpr :
if pp . Fun . ( * ast . SelectorExpr ) . Sel . String ( ) == "NSInclude" {
2016-11-13 14:14:48 +00:00
controllerName = analyseNSInclude ( s , pp )
2016-08-08 08:44:49 +00:00
if v , ok := controllerComments [ controllerName ] ; ok {
rootapi . Tags = append ( rootapi . Tags , swagger . Tag {
2016-08-16 15:32:13 +00:00
Name : strings . Trim ( s , "/" ) ,
2016-08-08 08:44:49 +00:00
Description : v ,
} )
}
2014-08-01 15:40:48 +00:00
}
2014-06-18 04:19:03 +00:00
}
}
2016-08-08 08:44:49 +00:00
} else if selname == "NSInclude" {
2016-11-13 14:14:48 +00:00
controllerName = analyseNSInclude ( "" , pp )
2014-08-01 15:40:48 +00:00
if v , ok := controllerComments [ controllerName ] ; ok {
2016-08-08 08:44:49 +00:00
rootapi . Tags = append ( rootapi . Tags , swagger . Tag {
Name : controllerName , // if the NSInclude has no prefix, we use the controllername as the tag
Description : v ,
} )
2014-08-01 15:40:48 +00:00
}
2014-06-18 04:19:03 +00:00
}
}
}
}
2014-08-01 15:40:48 +00:00
2014-06-18 04:19:03 +00:00
}
}
}
}
}
2016-08-01 06:40:43 +00:00
os . Mkdir ( path . Join ( curpath , "swagger" ) , 0755 )
fd , err := os . Create ( path . Join ( curpath , "swagger" , "swagger.json" ) )
2017-03-06 23:58:53 +00:00
if err != nil {
panic ( err )
}
2016-09-14 21:09:54 +00:00
fdyml , err := os . Create ( path . Join ( curpath , "swagger" , "swagger.yml" ) )
2014-06-18 04:19:03 +00:00
if err != nil {
panic ( err )
}
2016-09-14 21:09:54 +00:00
defer fdyml . Close ( )
2016-08-01 06:40:43 +00:00
defer fd . Close ( )
2016-08-13 06:45:12 +00:00
dt , err := json . MarshalIndent ( rootapi , "" , " " )
2016-09-14 21:09:54 +00:00
dtyml , erryml := yaml . Marshal ( rootapi )
if err != nil || erryml != nil {
2016-08-13 06:45:12 +00:00
panic ( err )
}
_ , err = fd . Write ( dt )
2016-09-14 21:09:54 +00:00
_ , erryml = fdyml . Write ( dtyml )
if err != nil || erryml != nil {
2014-06-18 04:19:03 +00:00
panic ( err )
}
}
2016-11-13 14:14:48 +00:00
// analyseNewNamespace returns version and the others params
func analyseNewNamespace ( ce * ast . CallExpr ) ( first string , others [ ] ast . Expr ) {
2014-06-18 04:19:03 +00:00
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
}
2016-11-13 14:14:48 +00:00
func analyseNSInclude ( baseurl string , ce * ast . CallExpr ) string {
2014-06-18 04:19:03 +00:00
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 {
2016-08-08 08:44:49 +00:00
for rt , item := range apis {
tag := ""
if baseurl != "" {
2016-08-13 06:45:12 +00:00
rt = baseurl + rt
2016-08-16 15:32:13 +00:00
tag = strings . Trim ( baseurl , "/" )
2016-08-08 08:44:49 +00:00
} else {
tag = cname
}
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 }
}
2016-08-08 12:48:51 +00:00
if len ( rootapi . Paths ) == 0 {
rootapi . Paths = make ( map [ string ] * swagger . Item )
}
2016-08-17 00:02:21 +00:00
rt = urlReplace ( rt )
2016-08-08 08:44:49 +00:00
rootapi . Paths [ rt ] = item
2014-06-19 08:40:55 +00:00
}
}
2014-06-18 04:19:03 +00:00
}
return cname
}
2016-11-13 14:14:48 +00:00
func analyseControllerPkg ( localName , pkgpath string ) {
2014-06-18 04:19:03 +00:00
pkgpath = strings . Trim ( pkgpath , "\"" )
2014-09-04 14:35:30 +00:00
if isSystemPackage ( pkgpath ) {
return
}
2016-08-01 06:40:43 +00:00
if pkgpath == "github.com/astaxie/beego" {
return
}
2015-05-14 07:56:19 +00:00
if localName != "" {
importlist [ localName ] = pkgpath
} else {
pps := strings . Split ( pkgpath , "/" )
importlist [ pps [ len ( pps ) - 1 ] ] = pkgpath
}
2014-06-18 04:19:03 +00:00
gopath := os . Getenv ( "GOPATH" )
if gopath == "" {
2017-03-15 16:44:29 +00:00
beeLogger . Log . Fatal ( "GOPATH environment variable is not set or empty" )
2014-06-18 04:19:03 +00:00
}
pkgRealpath := ""
wgopath := filepath . SplitList ( gopath )
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
}
2016-08-08 08:44:49 +00:00
pkgCache [ pkgpath ] = struct { } { }
2014-06-18 04:19:03 +00:00
} else {
2017-03-15 16:44:29 +00:00
beeLogger . Log . Fatalf ( "Package '%s' does not exist in the GOPATH" , pkgpath )
2014-06-18 04:19:03 +00:00
}
2016-11-13 14:14:48 +00:00
2014-06-18 04:19:03 +00:00
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 {
2017-03-15 16:44:29 +00:00
beeLogger . Log . Fatalf ( "Error while parsing dir at '%s': %s" , pkgpath , err )
2014-06-18 04:19:03 +00:00
}
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 {
2016-11-13 14:14:48 +00:00
// Parse controller method
2014-06-18 04:19:03 +00:00
parserComments ( specDecl . Doc , specDecl . Name . String ( ) , fmt . Sprint ( t . X ) , pkgpath )
}
}
case * ast . GenDecl :
2016-08-01 06:40:43 +00:00
if specDecl . Tok == token . TYPE {
2014-06-18 04:19:03 +00:00
for _ , s := range specDecl . Specs {
switch tp := s . ( * ast . TypeSpec ) . Type . ( type ) {
case * ast . StructType :
_ = tp . Struct
2016-11-13 14:14:48 +00:00
// Parse controller definition comments
2016-08-08 08:44:49 +00:00
if strings . TrimSpace ( specDecl . Doc . Text ( ) ) != "" {
controllerComments [ pkgpath + s . ( * ast . TypeSpec ) . Name . String ( ) ] = specDecl . Doc . Text ( )
}
2014-06-18 04:19:03 +00:00
}
}
}
}
}
}
}
}
2014-09-04 14:35:30 +00:00
func isSystemPackage ( pkgpath string ) bool {
2016-11-13 14:14:48 +00:00
goroot := os . Getenv ( "GOROOT" )
2017-02-21 08:30:55 +00:00
if goroot == "" {
goroot = runtime . GOROOT ( )
}
2014-09-04 14:35:30 +00:00
if goroot == "" {
2017-03-15 16:44:29 +00:00
beeLogger . Log . Fatalf ( "GOROOT environment variable is not set or empty" )
2014-09-04 14:35:30 +00:00
}
2016-11-13 14:14:48 +00:00
2014-09-04 14:35:30 +00:00
wg , _ := filepath . EvalSymlinks ( filepath . Join ( goroot , "src" , "pkg" , pkgpath ) )
if utils . FileExists ( wg ) {
return true
}
2015-01-03 13:17:59 +00:00
//TODO(zh):support go1.4
wg , _ = filepath . EvalSymlinks ( filepath . Join ( goroot , "src" , pkgpath ) )
2017-03-06 23:58:53 +00:00
return utils . FileExists ( wg )
2014-09-04 14:35:30 +00:00
}
2016-08-20 07:12:15 +00:00
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
}
2014-06-18 04:19:03 +00:00
// parse the func comments
func parserComments ( comments * ast . CommentGroup , funcName , controllerName , pkgpath string ) error {
2016-08-08 08:44:49 +00:00
var routerPath string
var HTTPMethod string
2016-08-08 12:48:51 +00:00
opts := swagger . Operation {
Responses : make ( map [ string ] swagger . Response ) ,
}
2014-06-18 04:19:03 +00:00
if comments != nil && comments . List != nil {
for _ , c := range comments . List {
t := strings . TrimSpace ( strings . TrimLeft ( c . Text , "//" ) )
if strings . HasPrefix ( t , "@router" ) {
2014-06-20 11:49:43 +00:00
elements := strings . TrimSpace ( t [ len ( "@router" ) : ] )
2014-06-18 04:19:03 +00:00
e1 := strings . SplitN ( elements , " " , 2 )
if len ( e1 ) < 1 {
return errors . New ( "you should has router infomation" )
}
2016-08-17 00:02:21 +00:00
routerPath = e1 [ 0 ]
2014-06-18 04:19:03 +00:00
if len ( e1 ) == 2 && e1 [ 1 ] != "" {
e1 = strings . SplitN ( e1 [ 1 ] , " " , 2 )
2016-08-08 08:44:49 +00:00
HTTPMethod = strings . ToUpper ( strings . Trim ( e1 [ 0 ] , "[]" ) )
2014-06-18 04:19:03 +00:00
} else {
2016-08-08 08:44:49 +00:00
HTTPMethod = "GET"
2014-06-18 04:19:03 +00:00
}
} else if strings . HasPrefix ( t , "@Title" ) {
2016-08-08 08:44:49 +00:00
opts . OperationID = controllerName + "." + strings . TrimSpace ( t [ len ( "@Title" ) : ] )
2014-06-18 04:19:03 +00:00
} else if strings . HasPrefix ( t , "@Description" ) {
2016-08-25 16:32:39 +00:00
opts . Description = strings . TrimSpace ( t [ len ( "@Description" ) : ] )
} else if strings . HasPrefix ( t , "@Summary" ) {
opts . Summary = strings . TrimSpace ( t [ len ( "@Summary" ) : ] )
2014-06-18 04:19:03 +00:00
} else if strings . HasPrefix ( t , "@Success" ) {
2014-06-20 11:47:03 +00:00
ss := strings . TrimSpace ( t [ len ( "@Success" ) : ] )
2016-08-08 08:44:49 +00:00
rs := swagger . Response { }
2016-08-20 07:12:15 +00:00
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 == "" {
2017-03-15 16:44:29 +00:00
beeLogger . Log . Fatalf ( "[%s.%s] Schema must follow {object} or {array}" , controllerName , funcName )
2014-06-20 11:47:03 +00:00
}
2016-08-20 07:12:15 +00:00
if strings . HasPrefix ( schemaName , "[]" ) {
schemaName = schemaName [ 2 : ]
isArray = true
2014-06-19 08:40:55 +00:00
}
2016-08-20 07:12:15 +00:00
schema := swagger . Schema { }
if sType , ok := basicTypes [ schemaName ] ; ok {
2016-08-19 16:14:39 +00:00
typeFormat := strings . Split ( sType , ":" )
2016-08-20 07:12:15 +00:00
schema . Type = typeFormat [ 0 ]
schema . Format = typeFormat [ 1 ]
2016-08-19 16:14:39 +00:00
} else {
2016-09-24 22:34:26 +00:00
m , mod , realTypes := getModel ( schemaName )
2016-08-20 07:12:15 +00:00
schema . Ref = "#/definitions/" + m
2016-08-19 16:14:39 +00:00
if _ , ok := modelsList [ pkgpath + controllerName ] ; ! ok {
2017-03-06 23:58:53 +00:00
modelsList [ pkgpath + controllerName ] = make ( map [ string ] swagger . Schema )
2016-08-19 16:14:39 +00:00
}
2016-08-20 07:12:15 +00:00
modelsList [ pkgpath + controllerName ] [ schemaName ] = mod
2016-09-24 22:34:26 +00:00
appendModels ( pkgpath , controllerName , realTypes )
2016-08-19 16:14:39 +00:00
}
2016-08-20 07:12:15 +00:00
if isArray {
rs . Schema = & swagger . Schema {
2016-08-24 03:07:57 +00:00
Type : "array" ,
2016-08-20 07:12:15 +00:00
Items : & schema ,
}
2016-08-24 03:07:57 +00:00
} else {
2016-08-20 07:12:15 +00:00
rs . Schema = & schema
}
rs . Description = strings . TrimSpace ( ss [ pos : ] )
} else {
rs . Description = strings . TrimSpace ( ss )
2014-06-19 08:40:55 +00:00
}
2016-08-20 07:12:15 +00:00
opts . Responses [ respCode ] = rs
2014-06-18 04:19:03 +00:00
} else if strings . HasPrefix ( t , "@Param" ) {
para := swagger . Parameter { }
2014-06-18 10:50:08 +00:00
p := getparams ( strings . TrimSpace ( t [ len ( "@Param " ) : ] ) )
2014-06-20 11:47:03 +00:00
if len ( p ) < 4 {
2017-03-15 16:44:29 +00:00
beeLogger . Log . Fatal ( controllerName + "_" + funcName + "'s comments @Param should have at least 4 params" )
2014-06-20 11:47:03 +00:00
}
2014-06-18 04:19:03 +00:00
para . Name = p [ 0 ]
2016-08-18 10:21:23 +00:00
switch p [ 1 ] {
case "query" :
fallthrough
case "header" :
fallthrough
case "path" :
fallthrough
case "formData" :
fallthrough
case "body" :
2016-08-18 13:03:27 +00:00
break
2016-08-18 10:21:23 +00:00
default :
2017-03-15 16:44:29 +00:00
beeLogger . Log . Warnf ( "[%s.%s] Unknown param location: %s. Possible values are `query`, `header`, `path`, `formData` or `body`.\n" , controllerName , funcName , p [ 1 ] )
2016-08-18 10:21:23 +00:00
}
2016-08-08 08:44:49 +00:00
para . In = p [ 1 ]
2014-06-24 04:06:45 +00:00
pp := strings . Split ( p [ 2 ] , "." )
2016-08-13 06:45:12 +00:00
typ := pp [ len ( pp ) - 1 ]
if len ( pp ) >= 2 {
2016-09-24 22:34:26 +00:00
m , mod , realTypes := getModel ( p [ 2 ] )
2016-08-13 06:45:12 +00:00
para . Schema = & swagger . Schema {
2016-08-17 15:48:38 +00:00
Ref : "#/definitions/" + m ,
2016-08-13 06:45:12 +00:00
}
2016-08-17 15:48:38 +00:00
if _ , ok := modelsList [ pkgpath + controllerName ] ; ! ok {
2017-03-06 23:58:53 +00:00
modelsList [ pkgpath + controllerName ] = make ( map [ string ] swagger . Schema )
2016-08-17 15:48:38 +00:00
}
modelsList [ pkgpath + controllerName ] [ typ ] = mod
2016-09-24 22:34:26 +00:00
appendModels ( pkgpath , controllerName , realTypes )
2016-08-13 06:45:12 +00:00
} else {
2016-08-20 03:17:30 +00:00
isArray := false
paraType := ""
paraFormat := ""
if strings . HasPrefix ( typ , "[]" ) {
typ = typ [ 2 : ]
isArray = true
}
2016-08-13 06:45:12 +00:00
if typ == "string" || typ == "number" || typ == "integer" || typ == "boolean" ||
typ == "array" || typ == "file" {
2016-08-20 03:17:30 +00:00
paraType = typ
2016-08-18 09:38:26 +00:00
} else if sType , ok := basicTypes [ typ ] ; ok {
typeFormat := strings . Split ( sType , ":" )
2016-08-20 03:17:30 +00:00
paraType = typeFormat [ 0 ]
paraFormat = typeFormat [ 1 ]
2016-08-18 09:40:59 +00:00
} else {
2017-03-15 16:44:29 +00:00
beeLogger . Log . Warnf ( "[%s.%s] Unknown param type: %s\n" , controllerName , funcName , typ )
2016-08-13 06:45:12 +00:00
}
2016-08-20 03:17:30 +00:00
if isArray {
para . Type = "array"
para . Items = & swagger . ParameterItems {
Type : paraType ,
Format : paraFormat ,
}
2016-08-18 09:40:59 +00:00
} else {
2016-08-20 03:17:30 +00:00
para . Type = paraType
para . Format = paraFormat
2016-08-13 06:45:12 +00:00
}
}
2016-10-29 14:36:41 +00:00
switch len ( p ) {
case 5 :
2014-06-18 04:19:03 +00:00
para . Required , _ = strconv . ParseBool ( p [ 3 ] )
2016-08-13 06:45:12 +00:00
para . Description = strings . Trim ( p [ 4 ] , ` " ` )
2016-10-29 14:36:41 +00:00
case 6 :
para . Default = str2RealType ( p [ 3 ] , para . Type )
para . Required , _ = strconv . ParseBool ( p [ 4 ] )
para . Description = strings . Trim ( p [ 5 ] , ` " ` )
default :
2016-08-13 06:45:12 +00:00
para . Description = strings . Trim ( p [ 3 ] , ` " ` )
2014-06-18 04:19:03 +00:00
}
opts . Parameters = append ( opts . Parameters , para )
} else if strings . HasPrefix ( t , "@Failure" ) {
2016-08-08 08:44:49 +00:00
rs := swagger . Response { }
2014-06-20 11:47:03 +00:00
st := strings . TrimSpace ( t [ len ( "@Failure" ) : ] )
2014-06-18 04:19:03 +00:00
var cd [ ] rune
2014-06-20 11:47:03 +00:00
var start bool
2014-06-18 04:19:03 +00:00
for i , s := range st {
2014-06-20 11:47:03 +00:00
if unicode . IsSpace ( s ) {
if start {
2016-08-08 08:44:49 +00:00
rs . Description = strings . TrimSpace ( st [ i + 1 : ] )
2014-06-20 11:47:03 +00:00
break
} else {
continue
}
2014-06-18 04:19:03 +00:00
}
2014-06-20 11:47:03 +00:00
start = true
2014-06-18 04:19:03 +00:00
cd = append ( cd , s )
}
2016-08-08 08:44:49 +00:00
opts . Responses [ string ( cd ) ] = rs
} else if strings . HasPrefix ( t , "@Deprecated" ) {
opts . Deprecated , _ = strconv . ParseBool ( strings . TrimSpace ( t [ len ( "@Deprecated" ) : ] ) )
2014-06-20 11:47:03 +00:00
} 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 )
}
}
2014-06-18 04:19:03 +00:00
}
}
}
2016-08-08 08:44:49 +00:00
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
}
2014-06-18 04:19:03 +00:00
} else {
2016-08-08 08:44:49 +00:00
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
2016-08-13 06:45:12 +00:00
case "HEAD" :
2016-08-08 08:44:49 +00:00
item . Head = & opts
case "OPTIONS" :
item . Options = & opts
2014-06-18 04:19:03 +00:00
}
2016-08-08 08:44:49 +00:00
controllerList [ pkgpath + controllerName ] [ routerPath ] = item
2014-06-18 04:19:03 +00:00
}
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 {
2014-06-20 11:47:03 +00:00
var s [ ] rune
2014-06-18 04:19:03 +00:00
var j int
var start bool
var r [ ] string
2016-10-29 14:36:41 +00:00
var quoted int8
for _ , c := range [ ] rune ( str ) {
if unicode . IsSpace ( c ) && quoted == 0 {
2014-06-18 04:19:03 +00:00
if ! start {
continue
} else {
start = false
j ++
r = append ( r , string ( s ) )
2014-06-20 11:47:03 +00:00
s = make ( [ ] rune , 0 )
2014-06-18 04:19:03 +00:00
continue
}
}
2016-10-29 14:36:41 +00:00
2014-06-18 04:19:03 +00:00
start = true
2016-10-29 14:36:41 +00:00
if c == '"' {
quoted ^ = 1
continue
}
2014-06-18 04:19:03 +00:00
s = append ( s , c )
}
2016-10-29 14:36:41 +00:00
if len ( s ) > 0 {
r = append ( r , string ( s ) )
}
2014-06-18 04:19:03 +00:00
return r
}
2014-06-19 08:40:55 +00:00
2016-09-24 22:34:26 +00:00
func getModel ( str string ) ( objectname string , m swagger . Schema , realTypes [ ] string ) {
2014-06-19 08:40:55 +00:00
strs := strings . Split ( str , "." )
objectname = strs [ len ( strs ) - 1 ]
2016-09-24 22:34:26 +00:00
packageName := ""
2016-08-13 06:45:12 +00:00
m . Type = "object"
2014-06-19 08:40:55 +00:00
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
}
2016-09-24 22:34:26 +00:00
packageName = pkg . Name
parseObject ( d , k , & m , & realTypes , astPkgs , pkg . Name )
2016-08-24 22:42:23 +00:00
}
}
}
}
if m . Title == "" {
2017-03-15 16:44:29 +00:00
beeLogger . Log . Warnf ( "Cannot find the object: %s" , str )
2016-08-24 22:42:23 +00:00
// TODO remove when all type have been supported
//os.Exit(1)
}
if len ( rootapi . Definitions ) == 0 {
rootapi . Definitions = make ( map [ string ] swagger . Schema )
}
2016-09-24 22:34:26 +00:00
objectname = packageName + "." + objectname
2016-08-24 22:42:23 +00:00
rootapi . Definitions [ objectname ] = m
return
}
2016-09-24 22:34:26 +00:00
func parseObject ( d * ast . Object , k string , m * swagger . Schema , realTypes * [ ] string , astPkgs map [ string ] * ast . Package , packageName string ) {
2016-08-24 22:42:23 +00:00
ts , ok := d . Decl . ( * ast . TypeSpec )
if ! ok {
2017-03-11 08:57:06 +00:00
beeLogger . Log . Fatalf ( "Unknown type without TypeSec: %v\n" , d )
2016-08-24 22:42:23 +00:00
}
// 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 {
2016-09-24 22:34:26 +00:00
realType := ""
2016-08-24 22:42:23 +00:00
isSlice , realType , sType := typeAnalyser ( field )
2016-09-24 22:34:26 +00:00
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
}
}
2016-08-24 22:42:23 +00:00
* 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 ] ,
2014-06-19 08:40:55 +00:00
}
2016-08-24 22:42:23 +00:00
} else {
mp . Items = & swagger . Propertie {
Ref : "#/definitions/" + realType ,
2014-06-19 08:40:55 +00:00
}
2016-08-24 22:42:23 +00:00
}
} else {
2016-09-22 15:04:58 +00:00
if sType == "object" {
mp . Ref = "#/definitions/" + realType
} else if isBasicType ( realType ) {
2016-08-24 22:42:23 +00:00
typeFormat := strings . Split ( sType , ":" )
mp . Type = typeFormat [ 0 ]
mp . Format = typeFormat [ 1 ]
2016-09-22 15:04:58 +00:00
} else if realType == "map" {
typeFormat := strings . Split ( sType , ":" )
mp . AdditionalProperties = & swagger . Propertie {
Type : typeFormat [ 0 ] ,
Format : typeFormat [ 1 ] ,
}
2016-08-24 22:42:23 +00:00
}
}
if field . Names != nil {
2014-10-22 14:45:26 +00:00
2016-08-24 22:42:23 +00:00
// set property name as field name
var name = field . Names [ 0 ] . Name
2014-10-22 14:45:26 +00:00
2016-08-24 22:42:23 +00:00
// if no tag skip tag processing
if field . Tag == nil {
m . Properties [ name ] = mp
continue
}
2014-10-22 14:45:26 +00:00
2016-08-24 22:42:23 +00:00
var tagValues [ ] string
2016-10-29 14:36:41 +00:00
2016-08-24 22:42:23 +00:00
stag := reflect . StructTag ( strings . Trim ( field . Tag . Value , "`" ) )
2016-10-29 14:36:41 +00:00
2016-10-08 08:35:26 +00:00
defaultValue := stag . Get ( "doc" )
2016-10-29 14:36:41 +00:00
if defaultValue != "" {
2016-10-08 08:35:26 +00:00
r , _ := regexp . Compile ( ` default\((.*)\) ` )
if r . MatchString ( defaultValue ) {
res := r . FindStringSubmatch ( defaultValue )
2016-10-29 14:36:41 +00:00
mp . Default = str2RealType ( res [ 1 ] , realType )
2016-10-10 14:36:51 +00:00
2016-10-29 14:36:41 +00:00
} else {
2017-03-15 16:44:29 +00:00
beeLogger . Log . Warnf ( "Invalid default value: %s" , defaultValue )
2016-10-08 08:35:26 +00:00
}
}
2016-10-29 14:36:41 +00:00
2016-08-24 22:42:23 +00:00
tag := stag . Get ( "json" )
2014-10-22 14:45:26 +00:00
2016-08-24 22:42:23 +00:00
if tag != "" {
tagValues = strings . Split ( tag , "," )
}
2014-10-22 14:45:26 +00:00
2016-08-24 22:42:23 +00:00
// dont add property if json tag first value is "-"
if len ( tagValues ) == 0 || tagValues [ 0 ] != "-" {
2014-10-22 14:45:26 +00:00
2016-08-24 22:42:23 +00:00
// 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 ]
}
2014-10-22 14:45:26 +00:00
2016-08-24 22:42:23 +00:00
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
}
2014-10-22 14:45:26 +00:00
2016-08-24 22:42:23 +00:00
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 ) {
2016-09-24 22:34:26 +00:00
parseObject ( obj , nameOfObj , m , realTypes , astPkgs , pkg . Name )
2014-06-20 04:03:42 +00:00
}
2014-06-19 08:40:55 +00:00
}
}
}
}
}
}
}
2014-06-21 13:27:12 +00:00
2016-08-16 15:32:13 +00:00
func typeAnalyser ( f * ast . Field ) ( isSlice bool , realType , swaggerType string ) {
2014-06-23 13:25:12 +00:00
if arr , ok := f . Type . ( * ast . ArrayType ) ; ok {
2014-06-23 13:38:19 +00:00
if isBasicType ( fmt . Sprint ( arr . Elt ) ) {
2016-08-16 15:32:13 +00:00
return false , fmt . Sprintf ( "[]%v" , arr . Elt ) , basicTypes [ fmt . Sprint ( arr . Elt ) ]
2014-06-23 13:25:12 +00:00
}
2014-06-23 14:00:57 +00:00
if mp , ok := arr . Elt . ( * ast . MapType ) ; ok {
2016-08-16 15:32:13 +00:00
return false , fmt . Sprintf ( "map[%v][%v]" , mp . Key , mp . Value ) , "object"
2014-06-23 13:25:12 +00:00
}
if star , ok := arr . Elt . ( * ast . StarExpr ) ; ok {
2016-08-16 15:32:13 +00:00
return true , fmt . Sprint ( star . X ) , "object"
2014-06-24 04:06:45 +00:00
}
2016-08-16 15:32:13 +00:00
return true , fmt . Sprint ( arr . Elt ) , "object"
2016-07-22 23:05:01 +00:00
}
switch t := f . Type . ( type ) {
case * ast . StarExpr :
2016-08-16 15:32:13 +00:00
return false , fmt . Sprint ( t . X ) , "object"
case * ast . MapType :
2016-09-22 15:04:58 +00:00
val := fmt . Sprintf ( "%v" , t . Value )
if isBasicType ( val ) {
return false , "map" , basicTypes [ val ]
}
return false , val , "object"
2016-08-16 15:32:13 +00:00
}
2017-03-01 10:42:14 +00:00
basicType := fmt . Sprint ( f . Type )
if object , isStdLibObject := stdlibObject [ basicType ] ; isStdLibObject {
basicType = object
}
if k , ok := basicTypes [ basicType ] ; ok {
return false , basicType , k
2014-06-21 13:27:12 +00:00
}
2017-03-01 10:42:14 +00:00
return false , basicType , "object"
2014-06-21 13:27:12 +00:00
}
func isBasicType ( Type string ) bool {
2016-08-16 15:32:13 +00:00
if _ , ok := basicTypes [ Type ] ; ok {
return true
2014-06-21 13:27:12 +00:00
}
return false
}
// append models
2016-09-24 22:34:26 +00:00
func appendModels ( pkgpath , controllerName string , realTypes [ ] string ) {
2014-06-21 13:27:12 +00:00
for _ , realType := range realTypes {
2014-06-24 04:06:45 +00:00
if realType != "" && ! isBasicType ( strings . TrimLeft ( realType , "[]" ) ) &&
! strings . HasPrefix ( realType , "map" ) && ! strings . HasPrefix ( realType , "&" ) {
2016-09-24 22:34:26 +00:00
if _ , ok := modelsList [ pkgpath + controllerName ] [ realType ] ; ok {
2014-08-01 08:04:09 +00:00
continue
2014-06-23 13:25:12 +00:00
}
2016-09-24 22:34:26 +00:00
_ , mod , newRealTypes := getModel ( realType )
modelsList [ pkgpath + controllerName ] [ realType ] = mod
appendModels ( pkgpath , controllerName , newRealTypes )
2014-06-21 13:27:12 +00:00
}
}
}
2016-08-16 15:32:13 +00:00
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 , "/" )
}
2016-10-29 14:36:41 +00:00
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 {
2017-03-15 16:44:29 +00:00
beeLogger . Log . Warnf ( "Invalid default value type '%s': %s" , typ , s )
2016-10-29 14:36:41 +00:00
return s
}
return ret
}