├── .gitignore ├── .gitmodules ├── .travis.yml ├── LICENSE ├── Makefile ├── README.md ├── doc.go ├── main.go ├── parser ├── app.go ├── app_test.go ├── const.go ├── controller.go ├── method.go ├── model.go ├── package.go ├── resource.go └── utils.go ├── swagger └── swagger.go └── test ├── main.go ├── pkg ├── api │ ├── api.go │ ├── data_structures.go │ ├── subpackage │ │ └── data_structures.go │ ├── subpackage_alias │ │ └── subpackage.go │ └── subpackage_dot │ │ └── subpackage.go └── router.go └── swagger.go /.gitignore: -------------------------------------------------------------------------------- 1 | swaggo 2 | example/example 3 | helper/ 4 | .DS_Store 5 | .vscode/ 6 | .idea/ 7 | .spread/ 8 | *.coverprofile 9 | debug 10 | swagger.json 11 | swagger.yaml -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "kpass"] 2 | path = examples/kpass 3 | url = https://github.com/seccom/kpass -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: go 3 | go: 4 | - 1.7 5 | - 1.8 6 | before_install: 7 | - go get -t -v ./parser/... 8 | - go get -t -v ./swagger/... 9 | - go get -t -v ./test/... 10 | - go get github.com/modocache/gover 11 | - go get github.com/mattn/goveralls 12 | script: 13 | - go test -coverprofile=swagger.coverprofile ./swagger 14 | - go test -coverprofile=parser.coverprofile ./parser 15 | - gover 16 | - goveralls -coverprofile=gover.coverprofile -service=travis-ci 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016-2017 Teambition 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | test: 2 | go test --race ./swagger 3 | go test --race ./parser 4 | 5 | cover: 6 | rm -f *.coverprofile 7 | go test -coverprofile=swagger.coverprofile ./swagger 8 | go test -coverprofile=parser.coverprofile ./parser 9 | gover 10 | go tool cover -html=gover.coverprofile 11 | rm -f *.coverprofile 12 | 13 | build: 14 | go build -o swaggo main.go 15 | 16 | .PHONY: test cover build 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Swaggo - `v0.2.8` 2 | ===== 3 | Parse annotations from Go code and generate [Swagger Documentation](http://swagger.io/) 4 | 5 | [![Build Status](http://img.shields.io/travis/teambition/swaggo.svg?style=flat-square)](https://travis-ci.org/teambition/swaggo) 6 | [![Coverage Status](http://img.shields.io/coveralls/teambition/swaggo.svg?style=flat-square)](https://coveralls.io/r/teambition/swaggo) 7 | [![License](http://img.shields.io/badge/license-mit-blue.svg?style=flat-square)](https://raw.githubusercontent.com/teambition/swaggo/master/LICENSE) 8 | 9 | 10 | 11 | - [About](#about) 12 | - [Quick Start Guide](#quick-start-guide) 13 | - [Install](#install) 14 | - [Declarative Comments Format](#declarative-comments-format) 15 | - [Usage](#usage) 16 | - [Kpass Example](#kpass-example) 17 | - [TODO(In the near future)](#todoin-the-near-future) 18 | 19 | 20 | 21 | ## About 22 | 23 | Generate API documentation from annotations in Go code. It's always used for you Go server application. 24 | The swagger file accords to the [Swagger Spec](https://github.com/OAI/OpenAPI-Specification) and displays it using 25 | [Swagger UI](https://github.com/swagger-api/swagger-ui)(this project dosn't provide). 26 | 27 | ## Quick Start Guide 28 | 29 | ### Install 30 | 31 | ```shell 32 | go get -u -v github.com/teambition/swaggo 33 | ``` 34 | 35 | ### Declarative Comments Format 36 | 37 | [中文](https://github.com/teambition/swaggo/wiki/Declarative-Comments-Format) 38 | 39 | ### Usage 40 | 41 | ```shell 42 | swaggo --help 43 | ``` 44 | 45 | ### Kpass Example 46 | 47 | [Kpass](https://github.com/seccom/kpass#swagger-document) 48 | 49 | ### TODO(In the near future) 50 | 51 | - [ ] Support API without Controller structure 52 | - [ ] Explain declarative comments format with english 53 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // convert comments to swagger-api-doc 2 | // just support swagger-2.0 3 | package main 4 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "os" 6 | 7 | "github.com/teambition/swaggo/parser" 8 | "github.com/urfave/cli" 9 | ) 10 | 11 | const ( 12 | version = "v0.2.8" 13 | ) 14 | 15 | func main() { 16 | app := cli.NewApp() 17 | app.Version = version 18 | app.Name = "swaggo" 19 | app.HelpName = "swaggo" 20 | app.Usage = "a utility for convert go annotations to swagger-doc" 21 | app.Flags = []cli.Flag{ 22 | cli.BoolFlag{ 23 | Name: "dev, d", 24 | Usage: "develop mode", 25 | }, 26 | cli.StringFlag{ 27 | Name: "swagger, s", 28 | Value: "./swagger.go", 29 | Usage: "where is the swagger.go file", 30 | }, 31 | cli.StringFlag{ 32 | Name: "project, p", 33 | Value: "./", 34 | Usage: "where is the project", 35 | }, 36 | cli.StringFlag{ 37 | Name: "output, o", 38 | Value: "./", 39 | Usage: "the output of the swagger file that was generated", 40 | }, 41 | cli.StringFlag{ 42 | Name: "type, t", 43 | Value: "json", 44 | Usage: "the type of swagger file (json or yaml)", 45 | }, 46 | } 47 | app.Action = func(c *cli.Context) error { 48 | if err := parser.Parse(c.String("project"), 49 | c.String("swagger"), 50 | c.String("output"), 51 | c.String("type"), 52 | c.Bool("dev")); err != nil { 53 | return err 54 | } 55 | return nil 56 | } 57 | if err := app.Run(os.Args); err != nil { 58 | log.Printf("[Error] %v", err) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /parser/app.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "go/parser" 7 | "go/token" 8 | "io/ioutil" 9 | "os" 10 | "path/filepath" 11 | "runtime" 12 | "strings" 13 | 14 | "github.com/teambition/swaggo/swagger" 15 | yaml "gopkg.in/yaml.v2" 16 | ) 17 | 18 | var ( 19 | vendor = "" 20 | goPaths = []string{} 21 | goRoot = "" 22 | devMode bool 23 | ) 24 | 25 | func init() { 26 | goPaths = filepath.SplitList(os.Getenv("GOPATH")) 27 | if len(goPaths) == 0 { 28 | panic("GOPATH environment variable is not set or empty") 29 | } 30 | goRoot = runtime.GOROOT() 31 | if goRoot == "" { 32 | panic("GOROOT environment variable is not set or empty") 33 | } 34 | } 35 | 36 | // Parse the project by args 37 | func Parse(projectPath, swaggerGo, output, t string, dev bool) (err error) { 38 | absPPath, err := filepath.Abs(projectPath) 39 | if err != nil { 40 | return err 41 | } 42 | vendor = filepath.Join(absPPath, "vendor") 43 | devMode = dev 44 | 45 | sw := swagger.NewV2() 46 | if err = doc2Swagger(projectPath, swaggerGo, dev, sw); err != nil { 47 | return 48 | } 49 | var ( 50 | data []byte 51 | filename string 52 | ) 53 | 54 | switch t { 55 | case "json": 56 | filename = jsonFile 57 | data, err = json.Marshal(sw) 58 | case "yaml": 59 | filename = yamlFile 60 | data, err = yaml.Marshal(sw) 61 | default: 62 | err = fmt.Errorf("missing swagger file type(%s), only support in (json, yaml)", t) 63 | } 64 | if err != nil { 65 | return 66 | } 67 | return ioutil.WriteFile(filepath.Join(output, filename), data, 0644) 68 | } 69 | 70 | func doc2Swagger(projectPath, swaggerGo string, dev bool, sw *swagger.Swagger) error { 71 | f, err := parser.ParseFile(token.NewFileSet(), swaggerGo, nil, parser.ParseComments) 72 | if err != nil { 73 | return err 74 | } 75 | // Analyse API comments 76 | if f.Comments != nil { 77 | for _, c := range f.Comments { 78 | for _, s := range strings.Split(c.Text(), "\n") { 79 | switch { 80 | case tagTrimPrefixAndSpace(&s, appVersion): 81 | sw.Infos.Version = s 82 | case tagTrimPrefixAndSpace(&s, appTitle): 83 | sw.Infos.Title = s 84 | case tagTrimPrefixAndSpace(&s, appDesc): 85 | if sw.Infos.Description != "" { 86 | sw.Infos.Description += "
" + s 87 | } else { 88 | sw.Infos.Description = s 89 | } 90 | case tagTrimPrefixAndSpace(&s, appTermsOfServiceURL): 91 | sw.Infos.TermsOfService = s 92 | case tagTrimPrefixAndSpace(&s, appContact): 93 | sw.Infos.Contact.EMail = s 94 | case tagTrimPrefixAndSpace(&s, appName): 95 | sw.Infos.Contact.Name = s 96 | case tagTrimPrefixAndSpace(&s, appURL): 97 | sw.Infos.Contact.URL = s 98 | case tagTrimPrefixAndSpace(&s, appLicenseURL): 99 | sw.Infos.License.URL = s 100 | case tagTrimPrefixAndSpace(&s, appLicense): 101 | sw.Infos.License.Name = s 102 | case tagTrimPrefixAndSpace(&s, appSchemes): 103 | sw.Schemes = strings.Split(s, ",") 104 | case tagTrimPrefixAndSpace(&s, appHost): 105 | sw.Host = s 106 | case tagTrimPrefixAndSpace(&s, appBasePath): 107 | sw.BasePath = s 108 | case tagTrimPrefixAndSpace(&s, appConsumes): 109 | sw.Consumes = contentTypeByDoc(s) 110 | case tagTrimPrefixAndSpace(&s, appProduces): 111 | sw.Produces = contentTypeByDoc(s) 112 | } 113 | } 114 | } 115 | } 116 | 117 | // Analyse controller package 118 | // like: 119 | // swagger.go 120 | // import ( 121 | // _ "path/to/ctrl1" 122 | // _ "path/to/ctrl2" 123 | // _ "path/to/ctrl3" 124 | // ) 125 | // // @APIVersion xxx 126 | // // @.... 127 | for _, im := range f.Imports { 128 | importPath := strings.Trim(im.Path.Value, "\"") 129 | p, err := newResoucre(importPath, true) 130 | if err != nil { 131 | return err 132 | } 133 | if err = p.run(sw); err != nil { 134 | return err 135 | } 136 | } 137 | return nil 138 | } 139 | -------------------------------------------------------------------------------- /parser/app_test.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "github.com/stretchr/testify/suite" 8 | "github.com/teambition/swaggo/swagger" 9 | ) 10 | 11 | func TestErrorPath(t *testing.T) { 12 | assert := assert.New(t) 13 | // error test 14 | projectPath := "../test" 15 | swaggerGo := "../test/swagger.go.err" 16 | dev := true 17 | as, err := NewAppSuite(projectPath, swaggerGo, dev) 18 | assert.Nil(as) 19 | assert.NotNil(err) 20 | } 21 | 22 | func TestAppSuite(t *testing.T) { 23 | assert := assert.New(t) 24 | // error test 25 | projectPath := "../test" 26 | swaggerGo := "../test/swagger.go" 27 | dev := true 28 | as, err := NewAppSuite(projectPath, swaggerGo, dev) 29 | assert.Nil(err) 30 | assert.NotNil(as) 31 | suite.Run(t, as) 32 | } 33 | 34 | type AppSuite struct { 35 | suite.Suite 36 | *swagger.Swagger 37 | } 38 | 39 | func NewAppSuite(projectPath, swaggerGo string, dev bool) (*AppSuite, error) { 40 | as := &AppSuite{Swagger: swagger.NewV2()} 41 | if err := doc2Swagger(projectPath, swaggerGo, dev, as.Swagger); err != nil { 42 | return nil, err 43 | } 44 | return as, nil 45 | } 46 | 47 | func (suite *AppSuite) TestSwagger() { 48 | assert := assert.New(suite.T()) 49 | assert.Equal("2.0", suite.SwaggerVersion) 50 | assert.Equal("Swagger Example API", suite.Infos.Title) 51 | assert.Equal("Swagger Example API", suite.Infos.Description) 52 | assert.Equal("1.0.0", suite.Infos.Version) 53 | assert.Equal("http://teambition.com/", suite.Infos.TermsOfService) 54 | // contact 55 | assert.Equal("swagger", suite.Infos.Contact.Name) 56 | assert.Equal("swagger@teambition.com", suite.Infos.Contact.EMail) 57 | assert.Equal("teambition.com", suite.Infos.Contact.URL) 58 | // license 59 | assert.Equal("Apache", suite.Infos.License.Name) 60 | assert.Equal("http://teambition.com/", suite.Infos.License.URL) 61 | // schemes 62 | assert.Equal([]string{"http", "wss"}, suite.Schemes) 63 | // consumes and produces 64 | assert.Equal([]string{"application/json", "text/plain", "application/xml", "text/html"}, suite.Consumes) 65 | assert.Equal([]string{"application/json", "text/plain", "application/xml", "text/html"}, suite.Produces) 66 | 67 | assert.Equal("127.0.0.1:3000", suite.Host) 68 | assert.Equal("/api", suite.BasePath) 69 | assert.Equal(7, len(suite.Paths)) 70 | router := suite.Paths["/testapi/get-string-by-int/{some_id}"] 71 | assert.NotNil(router) 72 | assert.NotNil(router.Get) 73 | assert.Equal([]string{"testapi"}, router.Get.Tags) 74 | assert.Equal("get string by ID summary
multi line", router.Get.Summary) 75 | assert.Equal("get string by ID desc
multi line", router.Get.Description) 76 | assert.Equal("testapi.GetStringByInt", router.Get.OperationID) 77 | assert.Equal([]string{"application/json", "text/plain", "application/xml", "text/html"}, router.Get.Consumes) 78 | assert.Equal([]string{"application/json", "text/plain", "application/xml", "text/html"}, router.Get.Produces) 79 | 80 | assert.Equal("path", router.Get.Parameters[0].In) 81 | assert.Equal("path_param", router.Get.Parameters[0].Name) 82 | assert.Equal("Some ID", router.Get.Parameters[0].Description) 83 | assert.Equal(true, router.Get.Parameters[0].Required) 84 | assert.Equal("integer", router.Get.Parameters[0].Type) 85 | assert.Equal("int32", router.Get.Parameters[0].Format) 86 | assert.Equal(123, router.Get.Parameters[0].Default) 87 | 88 | // 200 89 | assert.NotNil(router.Get.Responses["200"]) 90 | assert.Equal("string", router.Get.Responses["200"].Schema.Type) 91 | 92 | // 400 93 | assert.NotNil(router.Get.Responses["400"]) 94 | assert.Equal("We need ID!!", router.Get.Responses["400"].Description) 95 | assert.Equal("#/definitions/APIError", router.Get.Responses["400"].Schema.Ref) 96 | assert.Equal("object", router.Get.Responses["400"].Schema.Type) 97 | 98 | // 404 99 | assert.NotNil(router.Get.Responses["404"]) 100 | assert.Equal("Can not find ID", router.Get.Responses["404"].Description) 101 | assert.Equal("#/definitions/APIError", router.Get.Responses["404"].Schema.Ref) 102 | assert.Equal("object", router.Get.Responses["404"].Schema.Type) 103 | 104 | // definitions 105 | // APIError 106 | apiError := suite.Definitions["APIError"] 107 | assert.NotNil(apiError) 108 | assert.Equal("APIError", apiError.Title) 109 | assert.Equal("object", apiError.Type) 110 | assert.Equal("integer", apiError.Properties["ErrorCode"].Type) 111 | assert.Equal("int32", apiError.Properties["ErrorCode"].Format) 112 | assert.Equal("string", apiError.Properties["ErrorMessage"].Type) 113 | 114 | // inherit 115 | inhertStruct := suite.Definitions["StructureWithEmbededStructure"] 116 | assert.NotNil(inhertStruct) 117 | assert.Equal("StructureWithEmbededStructure", inhertStruct.Title) 118 | assert.Equal("object", inhertStruct.Type) 119 | 120 | assert.True(subset(inhertStruct.Required, []string{"id", "name", "age", "ctime", "sub", "i"})) 121 | assert.Equal("the user age", inhertStruct.Properties["age"].Description) 122 | assert.Equal(18, inhertStruct.Properties["age"].Default) 123 | assert.Equal("integer", inhertStruct.Properties["age"].Type) 124 | assert.Equal("int32", inhertStruct.Properties["age"].Format) 125 | 126 | assert.Equal("#/definitions/SimpleStructure_1", inhertStruct.Properties["sub"].Ref) 127 | assert.Equal("object", inhertStruct.Properties["sub"].Type) 128 | 129 | // tags 130 | assert.Equal(1, len(suite.Tags)) 131 | assert.Equal("testapi", suite.Tags[0].Name) 132 | assert.Equal("test apis", suite.Tags[0].Description) 133 | } 134 | -------------------------------------------------------------------------------- /parser/const.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | const ( 4 | jsonType = "json" 5 | xmlType = "xml" 6 | plainType = "plain" 7 | htmlType = "html" 8 | formType = "form" 9 | formDataType = "formData" 10 | streamType = "stream" 11 | ) 12 | 13 | var contentType = map[string]string{ 14 | jsonType: "application/json", 15 | xmlType: "application/xml", 16 | plainType: "text/plain", 17 | htmlType: "text/html", 18 | formType: "application/x-www-form-urlencoded", 19 | formDataType: "multipart/form-data", 20 | streamType: "application/octet-stream", 21 | } 22 | 23 | const ( 24 | jsonFile = "swagger.json" 25 | yamlFile = "swagger.yaml" 26 | ) 27 | 28 | const ( 29 | docPrefix = "@" 30 | // app tag 31 | appVersion = "@Version" 32 | appTitle = "@Title" 33 | appDesc = "@Description" 34 | appTermsOfServiceURL = "@TermsOfServiceUrl" 35 | appContact = "@Contact" 36 | appName = "@Name" 37 | appURL = "@URL" 38 | appLicenseURL = "@LicenseUrl" 39 | appLicense = "@License" 40 | appSchemes = "@Schemes" 41 | appHost = "@Host" 42 | appBasePath = "@BasePath" 43 | appConsumes = "@Consumes" 44 | appProduces = "@Produces" 45 | // controller tag 46 | ctrlPrivate = "@Private" 47 | ctrlName = "@Name" 48 | ctrlDesc = "@Description" 49 | // method tag 50 | methodPrivate = "@Private" // @Private 51 | methodTitle = "@Title" 52 | methodDesc = "@Description" 53 | methodSummary = "@Summary" 54 | methodSuccess = "@Success" 55 | methodParam = "@Param" 56 | methodFailure = "@Failure" 57 | methodDeprecated = "@Deprecated" 58 | methodConsumes = "@Consumes" 59 | methodProduces = "@Produces" 60 | methodRouter = "@Router" 61 | ) 62 | 63 | const ( 64 | query = "query" 65 | header = "header" 66 | path = "path" 67 | form = "form" 68 | body = "body" 69 | ) 70 | 71 | var paramType = map[string]string{ 72 | query: "query", 73 | header: "header", 74 | path: "path", 75 | form: "formData", 76 | body: "body", 77 | } 78 | -------------------------------------------------------------------------------- /parser/controller.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "go/ast" 5 | "strings" 6 | 7 | "github.com/teambition/swaggo/swagger" 8 | ) 9 | 10 | type controller struct { 11 | doc *ast.CommentGroup 12 | r *resource 13 | noStruct bool // there is no controller struct 14 | name string 15 | tagName string 16 | filename string 17 | methods []*method 18 | } 19 | 20 | func (ctrl *controller) parse(s *swagger.Swagger) (err error) { 21 | tag := &swagger.Tag{} 22 | for _, c := range strings.Split(ctrl.doc.Text(), "\n") { 23 | switch { 24 | case tagTrimPrefixAndSpace(&c, ctrlName): 25 | ctrl.tagName = c 26 | case tagTrimPrefixAndSpace(&c, ctrlDesc): 27 | if tag.Description != "" { 28 | tag.Description += "
" + c 29 | } else { 30 | tag.Description = c 31 | } 32 | case tagTrimPrefixAndSpace(&c, ctrlPrivate): 33 | // private controller 34 | if !devMode { 35 | return 36 | } 37 | } 38 | } 39 | if ctrl.tagName == "" { 40 | if ctrl.noStruct { 41 | // TODO 42 | // means no controller struct for methods 43 | return 44 | } 45 | ctrl.tagName = ctrl.name 46 | } 47 | tag.Name = ctrl.tagName 48 | s.Tags = append(s.Tags, tag) 49 | 50 | for _, m := range ctrl.methods { 51 | if err = m.parse(s); err != nil { 52 | return 53 | } 54 | } 55 | return 56 | } 57 | -------------------------------------------------------------------------------- /parser/method.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "fmt" 5 | "go/ast" 6 | "log" 7 | "strconv" 8 | "strings" 9 | 10 | "github.com/teambition/swaggo/swagger" 11 | ) 12 | 13 | // method the method of controllor 14 | type method struct { 15 | doc *ast.CommentGroup 16 | name string // function name 17 | filename string // where it is 18 | ctrl *controller 19 | } 20 | 21 | func (m *method) prettyErr(format string, e ...interface{}) error { 22 | f := "" 23 | if m.ctrl.name != "" { 24 | f = fmt.Sprintf("(%s:%s.%s) %s", m.filename, m.ctrl.name, m.name, format) 25 | } else { 26 | f = fmt.Sprintf("(%s:%s) %s", m.filename, m.name, format) 27 | } 28 | return fmt.Errorf(f, e...) 29 | } 30 | 31 | // parse Parse the api method annotations 32 | func (m *method) parse(s *swagger.Swagger) (err error) { 33 | var routerPath, HTTPMethod string 34 | tagName := m.ctrl.tagName 35 | opt := swagger.Operation{ 36 | Responses: make(map[string]*swagger.Response), 37 | Tags: []string{tagName}, 38 | } 39 | private := false 40 | for _, c := range strings.Split(m.doc.Text(), "\n") { 41 | switch { 42 | case tagTrimPrefixAndSpace(&c, methodPrivate): 43 | if !devMode { 44 | private = true 45 | break 46 | } 47 | case tagTrimPrefixAndSpace(&c, methodTitle): 48 | opt.OperationID = tagName + "." + c 49 | case tagTrimPrefixAndSpace(&c, methodDesc): 50 | if opt.Description != "" { 51 | opt.Description += "
" + c 52 | } else { 53 | opt.Description = c 54 | } 55 | case tagTrimPrefixAndSpace(&c, methodSummary): 56 | if opt.Summary != "" { 57 | opt.Summary += "
" + c 58 | } else { 59 | opt.Summary = c 60 | } 61 | case tagTrimPrefixAndSpace(&c, methodParam): 62 | para := swagger.Parameter{} 63 | p := getparams(c) 64 | if len(p) < 4 { 65 | err = m.prettyErr("comments %s shuold have 4 params at least", c) 66 | return 67 | } 68 | para.Name = p[0] 69 | switch p[1] { 70 | case query: 71 | fallthrough 72 | case header: 73 | fallthrough 74 | case path: 75 | fallthrough 76 | case form: 77 | fallthrough 78 | case body: 79 | break 80 | default: 81 | err = m.prettyErr("unknown param(%s) type(%s), type must in(query, header, path, form, body)", p[0], p[1]) 82 | return 83 | } 84 | para.In = paramType[p[1]] 85 | if err = m.ctrl.r.parseParam(s, ¶, m.filename, p[2]); err != nil { 86 | return 87 | } 88 | for idx, v := range p { 89 | switch idx { 90 | case 3: 91 | // required 92 | if v != "-" { 93 | para.Required, _ = strconv.ParseBool(v) 94 | } 95 | case 4: 96 | // description 97 | para.Description = strings.Trim(v, `" `) 98 | case 5: 99 | // default value 100 | if v != "-" { 101 | if para.Default, err = str2RealType(strings.Trim(v, `" `), p[2]); err != nil { 102 | err = m.prettyErr("parse default value of param(%s) type(%s) error(%v)", p[0], p[2], err) 103 | return 104 | } 105 | } 106 | } 107 | } 108 | opt.Parameters = append(opt.Parameters, ¶) 109 | case tagTrimPrefixAndSpace(&c, methodSuccess), tagTrimPrefixAndSpace(&c, methodFailure): 110 | sr := &swagger.Response{} 111 | p := getparams(c) 112 | respCode := "" 113 | for idx, v := range p { 114 | switch idx { 115 | case 0: 116 | respCode = v 117 | case 1: 118 | if v != "-" { 119 | sr.Schema = &swagger.Schema{} 120 | if err = m.ctrl.r.parseSchema(s, sr.Schema, m.filename, v); err != nil { 121 | return 122 | } 123 | } 124 | case 2: 125 | sr.Description = v 126 | default: 127 | err = m.prettyErr("response (%s) format error, need(code, type, description)", c) 128 | return 129 | } 130 | } 131 | opt.Responses[respCode] = sr 132 | case tagTrimPrefixAndSpace(&c, methodDeprecated): 133 | opt.Deprecated, _ = strconv.ParseBool(c) 134 | case tagTrimPrefixAndSpace(&c, methodConsumes): 135 | opt.Consumes = contentTypeByDoc(c) 136 | case tagTrimPrefixAndSpace(&c, methodProduces): 137 | opt.Produces = contentTypeByDoc(c) 138 | case tagTrimPrefixAndSpace(&c, methodRouter): 139 | // @Router / [post] 140 | elements := strings.Split(c, " ") 141 | if len(elements) == 0 { 142 | return m.prettyErr("should has Router information") 143 | } 144 | if len(elements) == 1 { 145 | HTTPMethod = "GET" 146 | routerPath = elements[0] 147 | } else { 148 | HTTPMethod = strings.ToUpper(elements[0]) 149 | routerPath = elements[1] 150 | } 151 | } 152 | } 153 | 154 | if routerPath != "" && !private { 155 | m.paramCheck(&opt) 156 | if s.Paths == nil { 157 | s.Paths = map[string]*swagger.Item{} 158 | } 159 | item, ok := s.Paths[routerPath] 160 | if !ok { 161 | item = &swagger.Item{} 162 | } 163 | var oldOpt *swagger.Operation 164 | switch HTTPMethod { 165 | case "GET": 166 | oldOpt = item.Get 167 | item.Get = &opt 168 | case "POST": 169 | oldOpt = item.Post 170 | item.Post = &opt 171 | case "PUT": 172 | oldOpt = item.Put 173 | item.Put = &opt 174 | case "PATCH": 175 | oldOpt = item.Patch 176 | item.Patch = &opt 177 | case "DELETE": 178 | oldOpt = item.Delete 179 | item.Delete = &opt 180 | case "HEAD": 181 | oldOpt = item.Head 182 | item.Head = &opt 183 | case "OPTIONS": 184 | oldOpt = item.Options 185 | item.Options = &opt 186 | } 187 | if oldOpt != nil { 188 | log.Println("[Warning]", m.prettyErr("router(%s %s) has existed in controller(%s)", HTTPMethod, routerPath, oldOpt.Tags[0])) 189 | } 190 | s.Paths[routerPath] = item 191 | } 192 | return 193 | } 194 | 195 | // paramCheck Verify the validity of parametes 196 | func (m *method) paramCheck(opt *swagger.Operation) { 197 | // swagger ui url (unique) 198 | if opt.OperationID == "" { 199 | opt.OperationID = m.ctrl.tagName + "." + m.name 200 | } 201 | 202 | hasFile, hasBody, hasForm, bodyWarn := false, false, false, false 203 | for _, v := range opt.Parameters { 204 | if v.Type == "file" && !hasFile { 205 | hasFile = true 206 | } 207 | switch v.In { 208 | case paramType[form]: 209 | hasForm = true 210 | case paramType[body]: 211 | if hasBody { 212 | if !bodyWarn { 213 | log.Println("[Warning]", m.prettyErr("more than one body-type existed in this method")) 214 | bodyWarn = true 215 | } 216 | } else { 217 | hasBody = true 218 | } 219 | case paramType[path]: 220 | if !v.Required { 221 | // path-type parameter must be required 222 | v.Required = true 223 | log.Println("[Warning]", m.prettyErr("path-type parameter(%s) must be required", v.Name)) 224 | } 225 | } 226 | } 227 | if hasBody && hasForm { 228 | log.Println("[Warning]", m.prettyErr("body-type and form-type cann't coexist")) 229 | } 230 | // If type is "file", the consumes MUST be 231 | // either "multipart/form-data", " application/x-www-form-urlencoded" 232 | // or both and the parameter MUST be in "formData". 233 | if hasFile { 234 | if hasBody { 235 | log.Println("[Warning]", m.prettyErr("file-data-type and body-type cann't coexist")) 236 | } 237 | if !(len(opt.Consumes) == 0 || subset(opt.Consumes, []string{contentType[formType], contentType[formDataType]})) { 238 | log.Println("[Warning]", m.prettyErr("file-data-type existed and this api's consumes must in(form, formData)")) 239 | } 240 | } 241 | } 242 | -------------------------------------------------------------------------------- /parser/model.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "go/ast" 7 | "reflect" 8 | "regexp" 9 | "strings" 10 | 11 | "github.com/teambition/swaggo/swagger" 12 | ) 13 | 14 | // feature if the expression is an anonymous member or an anonymous struct 15 | // it's useful for displays the model with swagger 16 | type feature int 17 | 18 | const ( 19 | noneFeature feature = iota 20 | anonMemberFeature // as an anonymous member type 21 | anonStructFeature // as an anonymous struct type 22 | ) 23 | 24 | // model the type of golang 25 | type model struct { 26 | ast.Expr // golang ast 27 | name string // the real name of model 28 | filename string // appear in which file 29 | p *pkg // appear in which package 30 | f feature 31 | } 32 | 33 | // newModel create a model with file path, schema expression and package object 34 | func newModel(filename string, e ast.Expr, p *pkg) *model { 35 | return &model{ 36 | Expr: e, 37 | filename: filename, 38 | p: p, 39 | } 40 | } 41 | 42 | // member the member of struct type with same environment 43 | func (m *model) member(e ast.Expr) *model { 44 | return newModel(m.filename, e, m.p) 45 | } 46 | 47 | // clone clone the model expect model's name 48 | func (m *model) clone(e ast.Expr) *model { 49 | nm := *m 50 | nm.name = "" 51 | nm.Expr = e 52 | return &nm 53 | } 54 | 55 | // inhertFeature inhert the feature from other model 56 | func (m *model) inhertFeature(other *model) *model { 57 | m.f = other.f 58 | return m 59 | } 60 | 61 | func (m *model) anonymousMember() *model { 62 | m.f = anonMemberFeature 63 | return m 64 | } 65 | 66 | func (m *model) anonymousStruct() *model { 67 | m.f = anonStructFeature 68 | return m 69 | } 70 | 71 | // parse parse the model in go code 72 | func (m *model) parse(s *swagger.Swagger) (r *result, err error) { 73 | switch t := m.Expr.(type) { 74 | case *ast.StarExpr: 75 | return m.clone(t.X).parse(s) 76 | case *ast.Ident, *ast.SelectorExpr: 77 | schema := fmt.Sprint(t) 78 | r = &result{} 79 | // []SomeStruct 80 | if strings.HasPrefix(schema, "[]") { 81 | schema = schema[2:] 82 | r.kind = arrayKind 83 | r.item, err = m.clone(ast.NewIdent(schema)).parse(s) 84 | return 85 | } 86 | // map[string]SomeStruct 87 | if strings.HasPrefix(schema, "map[string]") { 88 | schema = schema[11:] 89 | r.kind = mapKind 90 | r.item, err = m.clone(ast.NewIdent(schema)).parse(s) 91 | return 92 | } 93 | // &{foo Bar} to foo.Bar 94 | reInternalRepresentation := regexp.MustCompile("&\\{(\\w*) (\\w*)\\}") 95 | schema = string(reInternalRepresentation.ReplaceAll([]byte(schema), []byte("$1.$2"))) 96 | // check if is basic type 97 | if swaggerType, ok := basicTypes[schema]; ok { 98 | tmp := strings.Split(swaggerType, ":") 99 | typ := tmp[0] 100 | format := tmp[1] 101 | 102 | if strings.HasPrefix(typ, "[]") { 103 | schema = typ[2:] 104 | r.kind = arrayKind 105 | r.item, err = m.clone(ast.NewIdent(schema)).parse(s) 106 | } else { 107 | r.kind = innerKind 108 | r.buildin = schema 109 | r.sType = typ 110 | r.sFormat = format 111 | } 112 | return 113 | } 114 | nm, err := m.p.findModelBySchema(m.filename, schema) 115 | if err != nil { 116 | return nil, fmt.Errorf("findModelBySchema filename(%s) schema(%s) error(%v)", m.filename, schema, err) 117 | } 118 | return nm.inhertFeature(m).parse(s) 119 | case *ast.ArrayType: 120 | r = &result{kind: arrayKind} 121 | r.item, err = m.clone(t.Elt).parse(s) 122 | case *ast.MapType: 123 | r = &result{kind: mapKind} 124 | r.item, err = m.clone(t.Value).parse(s) 125 | case *ast.InterfaceType: 126 | return &result{kind: interfaceKind}, nil 127 | case *ast.StructType: 128 | r = &result{kind: objectKind, items: map[string]*result{}} 129 | // anonymous struct 130 | // type A struct { 131 | // B struct {} 132 | // } 133 | var key string 134 | if m.name == "" { 135 | m.anonymousStruct() 136 | } else { 137 | key = m.name 138 | r.title = m.name 139 | // find result cache 140 | if s.Definitions == nil { 141 | s.Definitions = map[string]*swagger.Schema{} 142 | } else if ips, ok := cachedModels[m.name]; ok { 143 | exsited := false 144 | for k, v := range ips { 145 | exsited = m.p.importPath == v.path 146 | if exsited { 147 | if k != 0 { 148 | key = fmt.Sprintf("%s_%d", m.name, k) 149 | } 150 | if m.f == anonMemberFeature { 151 | r = v.r 152 | return 153 | } 154 | _, ok := s.Definitions[key] 155 | if ok { 156 | r.ref = "#/definitions/" + key 157 | return 158 | } 159 | err = fmt.Errorf("the key(%s) must existed in swagger's definitions", key) 160 | return 161 | } 162 | } 163 | if !exsited { 164 | ips = append(ips, &kv{m.p.importPath, r}) 165 | cachedModels[m.name] = ips 166 | if len(ips) > 1 { 167 | key = fmt.Sprintf("%s_%d", m.name, len(ips)-1) 168 | } 169 | } 170 | } else { 171 | cachedModels[m.name] = []*kv{&kv{m.p.importPath, r}} 172 | } 173 | } 174 | 175 | for _, f := range t.Fields.List { 176 | var ( 177 | childR *result 178 | nm = m.member(f.Type) 179 | name string 180 | ) 181 | 182 | if len(f.Names) == 0 { 183 | // anonymous member 184 | // type A struct { 185 | // B 186 | // C 187 | // } 188 | nm.anonymousMember() 189 | } else { 190 | name = f.Names[0].Name 191 | } 192 | 193 | if childR, err = nm.parse(s); err != nil { 194 | return 195 | } 196 | if f.Tag != nil { 197 | var ( 198 | required bool 199 | tmpName string 200 | ignore bool 201 | ) 202 | if tmpName, childR.desc, childR.def, required, ignore, _ = parseTag(f.Tag.Value, childR.buildin); ignore { 203 | // hanppens when `josn:"-"` 204 | continue 205 | } 206 | if tmpName != "" { 207 | name = tmpName 208 | } 209 | if required { 210 | r.required = append(r.required, name) 211 | } 212 | } 213 | 214 | // must as a anonymous struct 215 | if nm.f == anonMemberFeature { 216 | hasKey := false 217 | for k1, v1 := range childR.items { 218 | for k2 := range r.items { 219 | if k1 == k2 { 220 | hasKey = true 221 | break 222 | } 223 | } 224 | if !hasKey { 225 | r.items[k1] = v1 226 | for _, v := range childR.required { 227 | if v == k1 { 228 | r.required = append(r.required, v) 229 | break 230 | } 231 | } 232 | } 233 | } 234 | } else { 235 | r.items[name] = childR 236 | } 237 | } 238 | 239 | if m.f != anonStructFeature { 240 | // cache the result and definitions for swagger's schema 241 | ss, err := r.convertToSchema() 242 | if err != nil { 243 | return nil, err 244 | } 245 | s.Definitions[key] = ss 246 | if m.f != anonMemberFeature { 247 | r.ref = "#/definitions/" + key 248 | } 249 | } 250 | } 251 | return 252 | } 253 | 254 | // cachedModels the cache of models 255 | // Format: 256 | // model name -> import path and result 257 | var cachedModels = map[string][]*kv{} 258 | 259 | type kv struct { 260 | path string 261 | r *result 262 | } 263 | 264 | type kind int 265 | 266 | const ( 267 | noneKind kind = iota 268 | innerKind 269 | arrayKind 270 | mapKind 271 | objectKind 272 | interfaceKind 273 | ) 274 | 275 | type result struct { 276 | kind kind 277 | title string 278 | buildin string // golang type 279 | sType string // swagger type 280 | sFormat string // swagger format 281 | def interface{} // default value 282 | desc string 283 | ref string 284 | item *result 285 | required []string 286 | items map[string]*result 287 | } 288 | 289 | func parseTag(tagStr, buildin string) (name, desc string, def interface{}, required, ignore bool, err error) { 290 | // parse tag for name 291 | stag := reflect.StructTag(strings.Trim(tagStr, "`")) 292 | // check jsonTag == "-" 293 | jsonTag := strings.Split(stag.Get("json"), ",") 294 | if len(jsonTag) != 0 { 295 | if jsonTag[0] == "-" { 296 | ignore = true 297 | return 298 | } 299 | name = jsonTag[0] 300 | } 301 | // swaggo:"(required),(desc),(default)" 302 | swaggoTag := stag.Get("swaggo") 303 | tmp := strings.Split(swaggoTag, ",") 304 | for k, v := range tmp { 305 | switch k { 306 | case 0: 307 | if v == "true" { 308 | required = true 309 | } 310 | case 1: 311 | desc = v 312 | case 2: 313 | if v != "" { 314 | def, err = str2RealType(v, buildin) 315 | } 316 | } 317 | } 318 | return 319 | } 320 | 321 | func (r *result) convertToSchema() (*swagger.Schema, error) { 322 | ss := &swagger.Schema{} 323 | switch r.kind { 324 | case objectKind: 325 | r.parseSchema(ss) 326 | default: 327 | return nil, errors.New("result need object kind") 328 | } 329 | return ss, nil 330 | } 331 | 332 | func (r *result) parseSchema(ss *swagger.Schema) { 333 | ss.Title = r.title 334 | // NOTE: 335 | // schema description not support now 336 | // ss.Description = r.desc 337 | switch r.kind { 338 | case innerKind: 339 | ss.Type = r.sType 340 | ss.Format = r.sFormat 341 | ss.Default = r.def 342 | case objectKind: 343 | ss.Type = "object" 344 | if r.ref != "" { 345 | ss.Ref = r.ref 346 | return 347 | } 348 | ss.Required = r.required 349 | if ss.Properties == nil { 350 | ss.Properties = make(map[string]*swagger.Propertie) 351 | } 352 | for k, v := range r.items { 353 | sp := &swagger.Propertie{} 354 | v.parsePropertie(sp) 355 | ss.Properties[k] = sp 356 | } 357 | case arrayKind: 358 | ss.Type = "array" 359 | ss.Items = &swagger.Schema{} 360 | r.item.parseSchema(ss.Items) 361 | case mapKind: 362 | ss.Type = "object" 363 | ss.AdditionalProperties = &swagger.Propertie{} 364 | r.item.parsePropertie(ss.AdditionalProperties) 365 | case interfaceKind: 366 | ss.Type = "object" 367 | } 368 | } 369 | 370 | func (r *result) parsePropertie(sp *swagger.Propertie) { 371 | sp.Description = r.desc 372 | switch r.kind { 373 | case innerKind: 374 | sp.Default = r.def 375 | sp.Type = r.sType 376 | sp.Format = r.sFormat 377 | case arrayKind: 378 | sp.Type = "array" 379 | sp.Items = &swagger.Propertie{} 380 | r.item.parsePropertie(sp.Items) 381 | case mapKind: 382 | sp.Type = "object" 383 | sp.AdditionalProperties = &swagger.Propertie{} 384 | r.item.parsePropertie(sp.AdditionalProperties) 385 | case objectKind: 386 | sp.Type = "object" 387 | if r.ref != "" { 388 | sp.Ref = r.ref 389 | return 390 | } 391 | sp.Required = r.required 392 | if sp.Properties == nil { 393 | sp.Properties = make(map[string]*swagger.Propertie) 394 | } 395 | for k, v := range r.items { 396 | tmpSp := &swagger.Propertie{} 397 | v.parsePropertie(tmpSp) 398 | sp.Properties[k] = tmpSp 399 | } 400 | case interfaceKind: 401 | sp.Type = "object" 402 | // TODO 403 | } 404 | } 405 | 406 | func (r *result) parseParam(sp *swagger.Parameter) error { 407 | switch sp.In { 408 | case body: 409 | if sp.Schema == nil { 410 | sp.Schema = &swagger.Schema{} 411 | } 412 | r.parseSchema(sp.Schema) 413 | default: 414 | switch r.kind { 415 | case innerKind: 416 | sp.Type = r.sType 417 | sp.Format = r.sFormat 418 | case arrayKind: 419 | sp.Type = "array" 420 | sp.Items = &swagger.ParameterItems{} 421 | if err := r.item.parseParamItem(sp.Items); err != nil { 422 | return err 423 | } 424 | default: 425 | // TODO 426 | // not support object and array in any value other than "body" 427 | return fmt.Errorf("not support(%d) in(%s) any value other than `body`", r.kind, sp.In) 428 | } 429 | } 430 | return nil 431 | } 432 | 433 | func (r *result) parseParamItem(sp *swagger.ParameterItems) error { 434 | switch r.kind { 435 | case innerKind: 436 | sp.Type = r.sType 437 | sp.Format = r.sFormat 438 | case arrayKind: 439 | sp.Type = "array" 440 | sp.Items = &swagger.ParameterItems{} 441 | if err := r.item.parseParamItem(sp.Items); err != nil { 442 | return err 443 | } 444 | default: 445 | // TODO 446 | // param not support object, map, interface 447 | return fmt.Errorf("not support(%d) in any value other than `body`", r.kind) 448 | } 449 | return nil 450 | } 451 | 452 | // inner type and swagger type 453 | var basicTypes = map[string]string{ 454 | "bool": "boolean:", 455 | "uint": "integer:int32", 456 | "uint8": "integer:int32", 457 | "uint16": "integer:int32", 458 | "uint32": "integer:int32", 459 | "uint64": "integer:int64", 460 | "int": "integer:int32", 461 | "int8": "integer:int32", 462 | "int16": "integer:int32", 463 | "int32": "integer:int32", 464 | "int64": "integer:int64", 465 | "uintptr": "integer:int64", 466 | "float32": "number:float", 467 | "float64": "number:double", 468 | "string": "string:", 469 | "complex64": "number:float", 470 | "complex128": "number:double", 471 | "byte": "string:byte", 472 | "rune": "string:byte", 473 | "time.Time": "string:date-time", 474 | "file": "file:", 475 | // option.XXX from code.teambition.com/soa/go-lib/pkg/option 476 | "option.Interface": "object:", 477 | "option.ObjectID": "string:", 478 | "option.ObjectIDs": "[]string:", 479 | "option.String": "string:", 480 | "option.Strings": "[]string:", 481 | "option.Time": "string:date-time", 482 | "option.Number": "number:int32", 483 | "option.Numbers": "[]number:int32", 484 | "option.Bool": "boolean:", 485 | "option.Bools": "[]boolean:", 486 | } 487 | -------------------------------------------------------------------------------- /parser/package.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "go/ast" 7 | "go/parser" 8 | "go/token" 9 | "log" 10 | "os" 11 | "strings" 12 | 13 | "github.com/teambition/swaggo/swagger" 14 | ) 15 | 16 | type pkg struct { 17 | *ast.Package 18 | localName string // alias name of package include "." 19 | importPath string // the import package name 20 | absPath string // whereis package in filesystem 21 | vendor string // project vendor for lookup package 22 | // filename -> import pkgs 23 | importPkgs map[string][]*pkg 24 | // model name -> model 25 | // cache of model struct 26 | models []*model 27 | } 28 | 29 | // newPackage 30 | func newPackage(localName, importPath string, justGoPath bool) (p *pkg, err error) { 31 | absPath := "" 32 | ok := false 33 | if justGoPath { 34 | absPath, ok = absPathFromGoPath(importPath) 35 | } else { 36 | if absPath, ok = absPathFromGoPath(importPath); !ok { 37 | absPath, ok = absPathFromGoRoot(importPath) 38 | } 39 | } 40 | if !ok { 41 | err = fmt.Errorf("package(%s) does not existed", importPath) 42 | return 43 | } 44 | pkgs, err := parser.ParseDir(token.NewFileSet(), absPath, func(info os.FileInfo) bool { 45 | name := info.Name() 46 | return !info.IsDir() && 47 | !strings.HasPrefix(name, ".") && 48 | // ignore the test file 49 | !strings.HasSuffix(name, "_test.go") 50 | }, parser.ParseComments) 51 | if err != nil { 52 | log.Printf("[Warning] %v\n", err) 53 | } 54 | 55 | for k, p := range pkgs { 56 | // ignore the main package 57 | if k == "main" { 58 | continue 59 | } 60 | return &pkg{ 61 | Package: p, 62 | localName: localName, 63 | importPath: importPath, 64 | vendor: vendor, 65 | absPath: absPath, 66 | importPkgs: map[string][]*pkg{}, 67 | models: []*model{}, 68 | }, nil 69 | } 70 | return 71 | } 72 | 73 | // parseSchema Parse schema in this code file 74 | func (p *pkg) parseSchema(s *swagger.Swagger, ss *swagger.Schema, filename, schema string) (err error) { 75 | r, err := newModel(filename, ast.NewIdent(schema), p).parse(s) 76 | if err != nil { 77 | return err 78 | } 79 | r.parseSchema(ss) 80 | return 81 | } 82 | 83 | // parseParam Parse param in this code file 84 | func (p *pkg) parseParam(s *swagger.Swagger, sp *swagger.Parameter, filename, schema string) (err error) { 85 | r, err := newModel(filename, ast.NewIdent(schema), p).parse(s) 86 | if err != nil { 87 | return err 88 | } 89 | return r.parseParam(sp) 90 | } 91 | 92 | // parseImports parse packages from file 93 | // when the qualified identifier has package name 94 | // or cann't be find in self(imported with `.`) 95 | func (p *pkg) parseImports(filename string) ([]*pkg, error) { 96 | f, ok := p.Files[filename] 97 | if !ok { 98 | return nil, fmt.Errorf("file(%s) doesn't existed in package(%s)", filename, p.importPath) 99 | } 100 | 101 | pkgs := []*pkg{} 102 | for _, im := range f.Imports { 103 | importPath := strings.Trim(im.Path.Value, "\"") 104 | // alias name 105 | localName := "" 106 | if im.Name != nil { 107 | localName = im.Name.Name 108 | } 109 | switch localName { 110 | case ".": 111 | // import . "lib/math" Sin 112 | // all the package's exported identifiers declared in that package's package block 113 | // will be declared in the importing source file's file block 114 | // and must be accessed without a qualifier. 115 | case "_": 116 | // import _ "path/to/package" cann't use 117 | // ignore the imported package 118 | case "": 119 | // import "lib/math" math.Sin 120 | default: 121 | // import m "lib/math" m.Sin 122 | } 123 | importPkg, err := newPackage(localName, importPath, false) 124 | if err != nil { 125 | return nil, err 126 | } 127 | pkgs = append(pkgs, importPkg) 128 | } 129 | p.importPkgs[filename] = pkgs 130 | return pkgs, nil 131 | } 132 | 133 | // importPackages find the cached packages or parse from file 134 | func (p *pkg) importPackages(filename string) ([]*pkg, error) { 135 | if pkgs, ok := p.importPkgs[filename]; ok { 136 | return pkgs, nil 137 | } 138 | return p.parseImports(filename) 139 | } 140 | 141 | var errModelNotFound = errors.New("model not found") 142 | 143 | // findModel find typeSpec in self by object name 144 | func (p *pkg) findModel(modelName string) (*model, error) { 145 | // check in cache 146 | for _, m := range p.models { 147 | if m.name == modelName { 148 | return m, nil 149 | } 150 | } 151 | 152 | // check in package 153 | for filename, f := range p.Files { 154 | for name, obj := range f.Scope.Objects { 155 | if name == modelName { 156 | ts, ok := obj.Decl.(*ast.TypeSpec) 157 | if ok { 158 | m := &model{ 159 | name: name, 160 | filename: filename, 161 | p: p, 162 | Expr: ts.Type, 163 | } 164 | p.models = append(p.models, m) 165 | return m, nil 166 | } 167 | return nil, fmt.Errorf("unsupport type(%#v) of model(%s)", obj.Decl, modelName) 168 | } 169 | } 170 | } 171 | return nil, errModelNotFound 172 | } 173 | 174 | func (p *pkg) findModelBySchema(filename, schema string) (model *model, err error) { 175 | expr := strings.Split(schema, ".") 176 | switch len(expr) { 177 | case 1: 178 | modelName := expr[0] 179 | if model, err = p.findModel(modelName); err != nil { 180 | if err != errModelNotFound { 181 | return 182 | } 183 | // perhaps in the package imported by `.` 184 | var pkgs []*pkg 185 | if pkgs, err = p.importPackages(filename); err != nil { 186 | return 187 | } 188 | for _, v := range pkgs { 189 | if v.localName == "." { 190 | if model, err = v.findModel(modelName); err != nil { 191 | if err == errModelNotFound { 192 | continue 193 | } 194 | return 195 | } 196 | } 197 | } 198 | } 199 | case 2: 200 | pkgName := expr[0] 201 | modelName := expr[1] 202 | var pkgs []*pkg 203 | if pkgs, err = p.importPackages(filename); err != nil { 204 | return 205 | } 206 | 207 | for _, v := range pkgs { 208 | switch v.localName { 209 | case "", "_": 210 | if v.Name == pkgName { 211 | return v.findModel(modelName) 212 | } 213 | default: 214 | if v.localName == pkgName { 215 | return v.findModel(modelName) 216 | } 217 | } 218 | } 219 | default: 220 | err = fmt.Errorf("unsupport schema format(%s) in file(%s)", schema, filename) 221 | return 222 | } 223 | if model == nil { 224 | err = fmt.Errorf("missing schema(%s) in file(%s)", schema, filename) 225 | return 226 | } 227 | return 228 | } 229 | -------------------------------------------------------------------------------- /parser/resource.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "fmt" 5 | "go/ast" 6 | "go/token" 7 | 8 | "github.com/teambition/swaggo/swagger" 9 | ) 10 | 11 | // resource api resource 12 | type resource struct { 13 | *pkg 14 | // maybe has several controllers 15 | controllers map[string]*controller // ctrl name -> ctrl 16 | } 17 | 18 | // newResoucre an api definition 19 | func newResoucre(importPath string, justGoPath bool) (*resource, error) { 20 | p, err := newPackage("_", importPath, justGoPath) 21 | if err != nil { 22 | return nil, err 23 | } 24 | 25 | r := &resource{ 26 | pkg: p, 27 | controllers: map[string]*controller{}, 28 | } 29 | for filename, f := range p.Files { 30 | for _, d := range f.Decls { 31 | switch specDecl := d.(type) { 32 | case *ast.FuncDecl: 33 | if specDecl.Recv != nil && len(specDecl.Recv.List) != 0 { 34 | if t, ok := specDecl.Recv.List[0].Type.(*ast.StarExpr); ok { 35 | if isDocComments(specDecl.Doc) { 36 | ctrlName := fmt.Sprint(t.X) 37 | if ctrl, ok := r.controllers[ctrlName]; !ok { 38 | m := &method{ 39 | doc: specDecl.Doc, 40 | filename: filename, 41 | name: specDecl.Name.Name, 42 | } 43 | ctrl = &controller{ 44 | r: r, 45 | filename: filename, 46 | name: ctrlName, 47 | methods: []*method{m}, 48 | } 49 | m.ctrl = ctrl 50 | r.controllers[ctrlName] = ctrl 51 | } else { 52 | ctrl.methods = append(ctrl.methods, &method{ 53 | doc: specDecl.Doc, 54 | filename: filename, 55 | name: specDecl.Name.Name, 56 | ctrl: ctrl, 57 | }) 58 | } 59 | } 60 | } 61 | } 62 | case *ast.GenDecl: 63 | if specDecl.Tok == token.TYPE { 64 | for _, s := range specDecl.Specs { 65 | t := s.(*ast.TypeSpec) 66 | switch t.Type.(type) { 67 | case *ast.StructType: 68 | if isDocComments(specDecl.Doc) { 69 | ctrlName := t.Name.String() 70 | if ctrl, ok := r.controllers[ctrlName]; !ok { 71 | r.controllers[ctrlName] = &controller{ 72 | doc: specDecl.Doc, 73 | r: r, 74 | filename: filename, 75 | name: t.Name.Name, 76 | } 77 | } else { 78 | ctrl.doc = specDecl.Doc 79 | } 80 | } 81 | } 82 | } 83 | } 84 | } 85 | } 86 | } 87 | return r, nil 88 | } 89 | 90 | // run gernerate swagger doc 91 | func (r *resource) run(s *swagger.Swagger) error { 92 | // parse controllers 93 | for _, ctrl := range r.controllers { 94 | if err := ctrl.parse(s); err != nil { 95 | return err 96 | } 97 | } 98 | return nil 99 | 100 | } 101 | -------------------------------------------------------------------------------- /parser/utils.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "go/ast" 5 | "os" 6 | "path/filepath" 7 | "strconv" 8 | "strings" 9 | "unicode" 10 | ) 11 | 12 | // fileExists check if the file existed 13 | func fileExists(name string) bool { 14 | _, err := os.Stat(name) 15 | if os.IsNotExist(err) { 16 | return false 17 | } 18 | return true 19 | } 20 | 21 | // str2RealType convert string type to inner type by `typ` 22 | func str2RealType(s string, typ string) (ret interface{}, err error) { 23 | switch typ { 24 | case "int", "int64", "int32", "int16", "int8": 25 | ret, err = strconv.Atoi(s) 26 | case "bool": 27 | ret, err = strconv.ParseBool(s) 28 | case "float64": 29 | ret, err = strconv.ParseFloat(s, 64) 30 | case "float32": 31 | ret, err = strconv.ParseFloat(s, 32) 32 | default: 33 | ret = s 34 | } 35 | return 36 | } 37 | 38 | func absPathFromGoPath(importPath string) (string, bool) { 39 | if vendor != "" { 40 | vendorImport := filepath.Join(vendor, importPath) 41 | if fileExists(vendorImport) { 42 | return vendorImport, true 43 | } 44 | } 45 | // find absolute path 46 | for _, goPath := range goPaths { 47 | wg, _ := filepath.EvalSymlinks(filepath.Join(goPath, "src", importPath)) 48 | if fileExists(wg) { 49 | return wg, true 50 | } 51 | } 52 | return "", false 53 | } 54 | 55 | func absPathFromGoRoot(importPath string) (string, bool) { 56 | wg, _ := filepath.EvalSymlinks(filepath.Join(goRoot, "src", importPath)) 57 | if fileExists(wg) { 58 | return wg, true 59 | } 60 | return "", false 61 | } 62 | 63 | // tagTrimPrefixAndSpace if prefix existed then trim it and trim space 64 | func tagTrimPrefixAndSpace(s *string, prefix string) bool { 65 | existed := strings.HasPrefix(*s, prefix) 66 | if existed { 67 | *s = strings.TrimPrefix(*s, prefix) 68 | *s = strings.TrimSpace(*s) 69 | } 70 | return existed 71 | } 72 | 73 | // isDocComments check if comments has `@` prefix 74 | func isDocComments(comments *ast.CommentGroup) bool { 75 | for _, c := range strings.Split(comments.Text(), "\n") { 76 | if strings.HasPrefix(c, docPrefix) { 77 | return true 78 | } 79 | } 80 | return false 81 | } 82 | 83 | // getparams analisys params return []string 84 | // @Param query form string true "The email for login" 85 | // @Success 200 string "Some Success" 86 | // @Failure 400 string "Some Failure" 87 | func getparams(str string) []string { 88 | var s []rune 89 | var j int 90 | var start bool 91 | var r []string 92 | var quoted int8 93 | for _, c := range []rune(str) { 94 | if unicode.IsSpace(c) && quoted == 0 { 95 | if !start { 96 | continue 97 | } else { 98 | start = false 99 | j++ 100 | r = append(r, string(s)) 101 | s = make([]rune, 0) 102 | continue 103 | } 104 | } 105 | 106 | start = true 107 | if c == '"' { 108 | quoted ^= 1 109 | continue 110 | } 111 | s = append(s, c) 112 | } 113 | if len(s) > 0 { 114 | r = append(r, string(s)) 115 | } 116 | return r 117 | } 118 | 119 | // contentTypeByDoc Get content types from comment 120 | func contentTypeByDoc(s string) []string { 121 | result := []string{} 122 | tmp := strings.Split(s, ",") 123 | for _, v := range tmp { 124 | result = append(result, contentType[v]) 125 | } 126 | return result 127 | } 128 | 129 | // subset returns true if the first array is completely 130 | // contained in the second array. There must be at least 131 | // the same number of duplicate values in second as there 132 | // are in first. 133 | func subset(first, second []string) bool { 134 | set := make(map[string]struct{}) 135 | for _, value := range second { 136 | set[value] = struct{}{} 137 | } 138 | 139 | for _, value := range first { 140 | if _, found := set[value]; !found { 141 | return false 142 | } 143 | } 144 | return true 145 | } 146 | -------------------------------------------------------------------------------- /swagger/swagger.go: -------------------------------------------------------------------------------- 1 | package swagger 2 | 3 | func NewV2() *Swagger { 4 | return &Swagger{SwaggerVersion: "2.0"} 5 | } 6 | 7 | // Swagger 2.0 8 | // Swagger list the resource 9 | type Swagger struct { 10 | SwaggerVersion string `json:"swagger,omitempty" yaml:"swagger,omitempty"` 11 | Infos Information `json:"info" yaml:"info"` 12 | Host string `json:"host,omitempty" yaml:"host,omitempty"` 13 | BasePath string `json:"basePath,omitempty" yaml:"basePath,omitempty"` 14 | Schemes []string `json:"schemes,omitempty" yaml:"schemes,omitempty"` 15 | Consumes []string `json:"consumes,omitempty" yaml:"consumes,omitempty"` 16 | Produces []string `json:"produces,omitempty" yaml:"produces,omitempty"` 17 | Paths map[string]*Item `json:"paths" yaml:"paths"` 18 | Definitions map[string]*Schema `json:"definitions,omitempty" yaml:"definitions,omitempty"` 19 | SecurityDefinitions map[string]*Security `json:"securityDefinitions,omitempty" yaml:"securityDefinitions,omitempty"` 20 | Security map[string][]string `json:"security,omitempty" yaml:"security,omitempty"` 21 | Tags []*Tag `json:"tags,omitempty" yaml:"tags,omitempty"` 22 | ExternalDocs *ExternalDocs `json:"externalDocs,omitempty" yaml:"externalDocs,omitempty"` 23 | } 24 | 25 | // Information Provides metadata about the API. The metadata can be used by the clients if needed. 26 | type Information struct { 27 | Title string `json:"title,omitempty" yaml:"title,omitempty"` 28 | Description string `json:"description,omitempty" yaml:"description,omitempty"` 29 | Version string `json:"version,omitempty" yaml:"version,omitempty"` 30 | TermsOfService string `json:"termsOfService,omitempty" yaml:"termsOfService,omitempty"` 31 | Contact Contact `json:"contact,omitempty" yaml:"contact,omitempty"` 32 | License License `json:"license,omitempty" yaml:"license,omitempty"` 33 | } 34 | 35 | // Contact information for the exposed API. 36 | type Contact struct { 37 | Name string `json:"name,omitempty" yaml:"name,omitempty"` 38 | URL string `json:"url,omitempty" yaml:"url,omitempty"` 39 | EMail string `json:"email,omitempty" yaml:"email,omitempty"` 40 | } 41 | 42 | // License information for the exposed API. 43 | type License struct { 44 | Name string `json:"name,omitempty" yaml:"name,omitempty"` 45 | URL string `json:"url,omitempty" yaml:"url,omitempty"` 46 | } 47 | 48 | // Item Describes the operations available on a single path. 49 | type Item struct { 50 | Ref string `json:"$ref,omitempty" yaml:"$ref,omitempty"` 51 | Get *Operation `json:"get,omitempty" yaml:"get,omitempty"` 52 | Put *Operation `json:"put,omitempty" yaml:"put,omitempty"` 53 | Post *Operation `json:"post,omitempty" yaml:"post,omitempty"` 54 | Delete *Operation `json:"delete,omitempty" yaml:"delete,omitempty"` 55 | Options *Operation `json:"options,omitempty" yaml:"options,omitempty"` 56 | Head *Operation `json:"head,omitempty" yaml:"head,omitempty"` 57 | Patch *Operation `json:"patch,omitempty" yaml:"patch,omitempty"` 58 | } 59 | 60 | // Operation Describes a single API operation on a path. 61 | type Operation struct { 62 | Tags []string `json:"tags,omitempty" yaml:"tags,omitempty"` 63 | Summary string `json:"summary,omitempty" yaml:"summary,omitempty"` 64 | Description string `json:"description,omitempty" yaml:"description,omitempty"` 65 | OperationID string `json:"operationId,omitempty" yaml:"operationId,omitempty"` 66 | Consumes []string `json:"consumes,omitempty" yaml:"consumes,omitempty"` 67 | Produces []string `json:"produces,omitempty" yaml:"produces,omitempty"` 68 | Schemes []string `json:"schemes,omitempty" yaml:"schemes,omitempty"` 69 | Parameters []*Parameter `json:"parameters,omitempty" yaml:"parameters,omitempty"` 70 | Responses map[string]*Response `json:"responses,omitempty" yaml:"responses,omitempty"` 71 | Deprecated bool `json:"deprecated,omitempty" yaml:"deprecated,omitempty"` 72 | } 73 | 74 | // Parameter Describes a single operation parameter. 75 | type Parameter struct { 76 | In string `json:"in,omitempty" yaml:"in,omitempty"` 77 | Name string `json:"name,omitempty" yaml:"name,omitempty"` 78 | Description string `json:"description,omitempty" yaml:"description,omitempty"` 79 | Required bool `json:"required,omitempty" yaml:"required,omitempty"` 80 | Schema *Schema `json:"schema,omitempty" yaml:"schema,omitempty"` 81 | Type string `json:"type,omitempty" yaml:"type,omitempty"` 82 | Format string `json:"format,omitempty" yaml:"format,omitempty"` 83 | Items *ParameterItems `json:"items,omitempty" yaml:"items,omitempty"` 84 | Default interface{} `json:"default,omitempty" yaml:"default,omitempty"` 85 | } 86 | 87 | // A limited subset of JSON-Schema's items object. It is used by parameter definitions that are not located in "body". 88 | // http://swagger.io/specification/#itemsObject 89 | type ParameterItems struct { 90 | Type string `json:"type,omitempty" yaml:"type,omitempty"` 91 | Format string `json:"format,omitempty" yaml:"format,omitempty"` 92 | Items *ParameterItems `json:"items,omitempty" yaml:"items,omitempty"` //Required if type is "array". Describes the type of items in the array. 93 | CollectionFormat string `json:"collectionFormat,omitempty" yaml:"collectionFormat,omitempty"` 94 | Default string `json:"default,omitempty" yaml:"default,omitempty"` 95 | } 96 | 97 | // Schema Object allows the definition of input and output data types. 98 | type Schema struct { 99 | Ref string `json:"$ref,omitempty" yaml:"$ref,omitempty"` 100 | Title string `json:"title,omitempty" yaml:"title,omitempty"` 101 | Format string `json:"format,omitempty" yaml:"format,omitempty"` 102 | Description string `json:"description,omitempty" yaml:"description,omitempty"` 103 | Default interface{} `json:"default,omitempty" yaml:"default,omitempty"` 104 | Required []string `json:"required,omitempty" yaml:"required,omitempty"` 105 | Type string `json:"type,omitempty" yaml:"type,omitempty"` 106 | Items *Schema `json:"items,omitempty" yaml:"items,omitempty"` 107 | AllOf []*Schema `json:"allOf,omitempty" yaml:"allOf,omitempty"` 108 | Properties map[string]*Propertie `json:"properties,omitempty" yaml:"properties,omitempty"` 109 | AdditionalProperties *Propertie `json:"additionalProperties,omitempty" yaml:"additionalProperties,omitempty"` 110 | } 111 | 112 | // Propertie are taken from the JSON Schema definition but their definitions were adjusted to the Swagger Specification 113 | type Propertie struct { 114 | Ref string `json:"$ref,omitempty" yaml:"$ref,omitempty"` 115 | Title string `json:"title,omitempty" yaml:"title,omitempty"` 116 | Description string `json:"description,omitempty" yaml:"description,omitempty"` 117 | Default interface{} `json:"default,omitempty" yaml:"default,omitempty"` 118 | Type string `json:"type,omitempty" yaml:"type,omitempty"` 119 | Example string `json:"example,omitempty" yaml:"example,omitempty"` 120 | Required []string `json:"required,omitempty" yaml:"required,omitempty"` 121 | Format string `json:"format,omitempty" yaml:"format,omitempty"` 122 | ReadOnly bool `json:"readOnly,omitempty" yaml:"readOnly,omitempty"` 123 | Properties map[string]*Propertie `json:"properties,omitempty" yaml:"properties,omitempty"` 124 | Items *Propertie `json:"items,omitempty" yaml:"items,omitempty"` 125 | AdditionalProperties *Propertie `json:"additionalProperties,omitempty" yaml:"additionalProperties,omitempty"` 126 | } 127 | 128 | // Response as they are returned from executing this operation. 129 | type Response struct { 130 | Description string `json:"description" yaml:"description"` 131 | Schema *Schema `json:"schema,omitempty" yaml:"schema,omitempty"` 132 | Ref string `json:"$ref,omitempty" yaml:"$ref,omitempty"` 133 | } 134 | 135 | // Security Allows the definition of a security scheme that can be used by the operations 136 | type Security struct { 137 | Type string `json:"type,omitempty" yaml:"type,omitempty"` // Valid values are "basic", "apiKey" or "oauth2". 138 | Description string `json:"description,omitempty" yaml:"description,omitempty"` 139 | Name string `json:"name,omitempty" yaml:"name,omitempty"` 140 | In string `json:"in,omitempty" yaml:"in,omitempty"` // Valid values are "query" or "header". 141 | Flow string `json:"flow,omitempty" yaml:"flow,omitempty"` // Valid values are "implicit", "password", "application" or "accessCode". 142 | AuthorizationURL string `json:"authorizationUrl,omitempty" yaml:"authorizationUrl,omitempty"` 143 | TokenURL string `json:"tokenUrl,omitempty" yaml:"tokenUrl,omitempty"` 144 | Scopes map[string]string `json:"scopes,omitempty" yaml:"scopes,omitempty"` // The available scopes for the OAuth2 security scheme. 145 | } 146 | 147 | // Tag Allows adding meta data to a single tag that is used by the Operation Object 148 | type Tag struct { 149 | Name string `json:"name,omitempty" yaml:"name,omitempty"` 150 | Description string `json:"description,omitempty" yaml:"description,omitempty"` 151 | ExternalDocs *ExternalDocs `json:"externalDocs,omitempty" yaml:"externalDocs,omitempty"` 152 | } 153 | 154 | // ExternalDocs include Additional external documentation 155 | type ExternalDocs struct { 156 | Description string `json:"description,omitempty" yaml:"description,omitempty"` 157 | URL string `json:"url,omitempty" yaml:"url,omitempty"` 158 | } 159 | -------------------------------------------------------------------------------- /test/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/teambition/swaggo/test/pkg" 7 | ) 8 | 9 | func main() { 10 | router := pkg.New() 11 | http.ListenAndServe("localhost:3000", router) 12 | } 13 | -------------------------------------------------------------------------------- /test/pkg/api/api.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "fmt" 5 | _ "os" 6 | 7 | "github.com/gocraft/web" 8 | "github.com/teambition/swaggo/test/pkg/api/subpackage" 9 | sub "github.com/teambition/swaggo/test/pkg/api/subpackage_alias" 10 | . "github.com/teambition/swaggo/test/pkg/api/subpackage_dot" 11 | ) 12 | 13 | var ( 14 | _ = sub.SubStructAlias{} 15 | _ = SubStructDot{} 16 | ) 17 | 18 | // @Name testapi 19 | // @Description test apis 20 | type Context struct { 21 | Response interface{} 22 | } 23 | 24 | func (c *Context) WriteResponse(response interface{}) { 25 | c.Response = response 26 | } 27 | 28 | // Title unique id 29 | // @Title GetStringByInt 30 | // 31 | // Deprecated show if this method has been deprecated 32 | // @Deprecated true 33 | // 34 | // Summary short explain it's action 35 | // @Summary get string by ID summary 36 | // @Summary multi line 37 | // 38 | // Description long explain about implement 39 | // @Description get string by ID desc 40 | // @Description multi line 41 | // 42 | // Consumes type include(json,plain,xml) 43 | // @Consumes json,plain,xml,html 44 | // 45 | // Produces type include(json,plain,xml,html) 46 | // @Produces json,plain,xml,html 47 | // 48 | // Param:param_name/param_type/data_type/required(optional)/describtion(optional)/defaul_value(optional) 49 | // value == "-" means optional 50 | // form and body params cann't coexist 51 | // path param must be required 52 | // if file type param exsited, all params must be form except path and query 53 | // @Param path_param path int - "Some ID" 123 54 | // @Param form_param form file - "Request Form" 55 | // @Param body_param body string - "Request Body" 56 | // @Param body_param_2 body string - "Request Body" 57 | // @Param query_param query []string - "Array" 58 | // @Param query_param_2 query [][]string - "Array Array" 59 | // 60 | // Success:response_code/data_type(optional)/describtion(optional) 61 | // @Success 200 string "Success" 62 | // @Success 201 SubStructDot "Success" 63 | // @Success 202 sub.SubStructAlias "Success" 64 | // @Success 203 StructureWithAnonymousStructure 65 | // @Success 204 map[string]string 66 | // 67 | // Failure:response_code/data_type(optional)/describtion(optional) 68 | // @Failure 400 APIError "We need ID!!" 69 | // @Failure 404 APIError "Can not find ID" 70 | // 71 | // Router:http_method/api_path 72 | // @Router GET /testapi/get-string-by-int/{some_id} 73 | func (c *Context) GetStringByInt(rw web.ResponseWriter, req *web.Request) { 74 | c.WriteResponse(fmt.Sprint("Some data for %s ID", req.PathParams["some_id"])) 75 | } 76 | 77 | // @Title GetStructByInt 78 | // @Summary get struct by ID 79 | // @Description get struct by ID 80 | // @Consumes json 81 | // @Produces json 82 | // @Param some_id path int true "Some ID" 83 | // @Param offset query int true "Offset" 84 | // @Param limit query int true "Limit" 85 | // @Success 200 StructureWithEmbededStructure "Success" 86 | // @Failure 400 APIError "We need ID!!" 87 | // @Failure 404 APIError "Can not find ID" 88 | // @Router GET /testapi/get-struct-by-int/{some_id} 89 | func (c *Context) GetStructByInt(rw web.ResponseWriter, req *web.Request) { 90 | c.WriteResponse(StructureWithEmbededStructure{}) 91 | } 92 | 93 | // @Title GetStruct2ByInt 94 | // @Summary get struct2 by ID 95 | // @Description get struct2 by ID 96 | // @Consumes json 97 | // @Produces json 98 | // @Param some_id path int true "Some ID" 99 | // @Param offset query int true "Offset" 100 | // @Param limit query int true "Limit" 101 | // @Success 200 StructureWithEmbededPointer "Success" 102 | // @Failure 400 APIError "We need ID!!" 103 | // @Failure 404 APIError "Can not find ID" 104 | // @Router GET /testapi/get-struct2-by-int/{some_id} 105 | func (c *Context) GetStruct2ByInt(rw web.ResponseWriter, req *web.Request) { 106 | c.WriteResponse(StructureWithEmbededPointer{}) 107 | } 108 | 109 | // @Title GetSimpleArrayByString 110 | // @Summary get simple array by ID 111 | // @Description get simple array by ID 112 | // @Consumes json 113 | // @Produces json 114 | // @Param some_id path string true "Some ID" 115 | // @Param offset query int true "Offset" 116 | // @Param limit query int true "Limit" 117 | // @Success 200 []string "Success" 118 | // @Failure 400 APIError "We need ID!!" 119 | // @Failure 404 APIError "Can not find ID" 120 | // @Router POST /testapi/get-simple-array-by-string/{some_id} 121 | func (c *Context) GetSimpleArrayByString(rw web.ResponseWriter, req *web.Request) { 122 | c.WriteResponse([]string{"one", "two", "three"}) 123 | } 124 | 125 | // @Title GetStructArrayByString 126 | // @Summary get struct array by ID 127 | // @Description get struct array by ID 128 | // @Consumes json 129 | // @Produces json 130 | // @Param some_id path string true "Some ID" "hello world" 131 | // @Param body body subpackage.SimpleStructure true 132 | // @Param limit query int true "Limit" 133 | // @Success 200 []subpackage.SimpleStructure "Success" 134 | // @Failure 400 APIError "We need ID!!" 135 | // @Failure 404 APIError "Can not find ID" 136 | // @Router PUT /testapi/get-struct-array-by-string/{some_id} 137 | func (c *Context) GetStructArrayByString(rw web.ResponseWriter, req *web.Request) { 138 | c.WriteResponse([]subpackage.SimpleStructure{ 139 | subpackage.SimpleStructure{}, 140 | subpackage.SimpleStructure{}, 141 | subpackage.SimpleStructure{}, 142 | }) 143 | } 144 | 145 | // @Title SameStruct 146 | // @Summary get struct array by ID 147 | // @Description get struct array by ID 148 | // @Consumes json 149 | // @Produces json 150 | // @Param some_id path string true "Some ID" 151 | // @Param offset query int true "Offset" 152 | // @Param body body []SimpleStructure true "Body" 153 | // @Param limit query int true "Limit" 154 | // @Success 200 []SimpleStructure "Success" 155 | // @Failure 400 APIError "We need ID!!" 156 | // @Failure 404 APIError "Can not find ID" 157 | // @Router PUT /testapi/get-same-struct-array-by-string/{some_id} 158 | func (c *Context) GetSameStructArraryByString(rw web.ResponseWriter, req *web.Request) { 159 | c.WriteResponse([]SimpleStructure{ 160 | SimpleStructure{}, 161 | SimpleStructure{}, 162 | SimpleStructure{}, 163 | }) 164 | } 165 | 166 | // @Title GetStruct3 167 | // @Summary get struct3 summary 168 | // @Description get struct3 desc 169 | // @Consumes json 170 | // @Produces json 171 | // @Success 200 SimpleStructure "Success" 172 | // @Success 201 SimpleStructure "Success" 173 | // @Failure 400 APIError "We need ID!!" 174 | // @Failure 404 APIError "Can not find ID" 175 | // @Router DELETE /testapi/get-struct3 176 | func (c *Context) DelStruct3(rw web.ResponseWriter, req *web.Request) { 177 | c.WriteResponse(StructureWithSlice{}) 178 | } 179 | 180 | // @Title GetStruct3 181 | // @Summary get struct3 182 | // @Description get struct3 183 | // @Consumes json 184 | // @Produces json 185 | // @Success 204 - "null" 186 | // @Success 200 StructureWithSlice "Success" 187 | // @Success 201 TypeInterface "Success" 188 | // @Failure 400 APIError "We need ID!!" 189 | // @Failure 404 APIError "Can not find ID" 190 | // @Router POST /testapi/get-struct3 191 | func (c *Context) PostStruct3(rw web.ResponseWriter, req *web.Request) { 192 | c.WriteResponse(StructureWithSlice{}) 193 | } 194 | -------------------------------------------------------------------------------- /test/pkg/api/data_structures.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/teambition/swaggo/test/pkg/api/subpackage" 7 | ) 8 | 9 | type TypeString string 10 | 11 | type TypeInterface interface { 12 | Hello() 13 | } 14 | 15 | type SimpleStructure struct { 16 | Id float32 `json:"id" swaggo:"true,dfsdfdsf,19"` 17 | Name string `json:"name" swaggo:"true,,xus"` 18 | Age int `json:"age" swaggo:"true,the user age,18"` 19 | CTime time.Time `json:"ctime" swaggo:"true,create time"` 20 | Sub subpackage.SimpleStructure `json:"sub" swaggo:"true"` 21 | I TypeInterface `json:"i" swaggo:"true"` 22 | T TypeString `json:"t"` 23 | Map map[string]string `json:"map", swaggo:",map type"` 24 | } 25 | 26 | type SimpleStructureWithAnnotations struct { 27 | Id int `json:"id"` 28 | Name string `json:"required,omitempty"` 29 | } 30 | 31 | type StructureWithSlice struct { 32 | Id int 33 | Name []byte 34 | } 35 | 36 | // hello 37 | type StructureWithEmbededStructure struct { 38 | SimpleStructure 39 | } 40 | type StructureWithEmbededPointer struct { 41 | *StructureWithSlice 42 | } 43 | 44 | type StructureWithAnonymousStructure struct { 45 | Anonymous []struct { 46 | Name string 47 | StructureWithSlice 48 | Anonymous []struct { 49 | Name string 50 | } 51 | } 52 | } 53 | 54 | type APIError struct { 55 | ErrorCode int 56 | ErrorMessage string 57 | } 58 | -------------------------------------------------------------------------------- /test/pkg/api/subpackage/data_structures.go: -------------------------------------------------------------------------------- 1 | package subpackage 2 | 3 | type SimpleStructure struct { 4 | Id int `json:"id" swaggo:"true,the user id,2"` 5 | Name string `json:"name" swaggo:",the user name,John Smith"` 6 | } 7 | -------------------------------------------------------------------------------- /test/pkg/api/subpackage_alias/subpackage.go: -------------------------------------------------------------------------------- 1 | package subpackage_alias 2 | 3 | type SubStructAlias struct { 4 | Name string 5 | Age string 6 | } 7 | -------------------------------------------------------------------------------- /test/pkg/api/subpackage_dot/subpackage.go: -------------------------------------------------------------------------------- 1 | package subpackage_dot 2 | 3 | type SubStructDot struct { 4 | Name string 5 | Aget int 6 | } 7 | -------------------------------------------------------------------------------- /test/pkg/router.go: -------------------------------------------------------------------------------- 1 | package pkg 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/gocraft/web" 7 | "github.com/teambition/swaggo/test/pkg/api" 8 | ) 9 | 10 | func New() *web.Router { 11 | router := web.New(api.Context{}). 12 | Middleware(web.LoggerMiddleware). 13 | Middleware(web.ShowErrorsMiddleware). 14 | Middleware(func(c *api.Context, rw web.ResponseWriter, req *web.Request, next web.NextMiddlewareFunc) { 15 | resultJSON, _ := json.Marshal(c.Response) 16 | rw.Write(resultJSON) 17 | }). 18 | Get("/testapi/get-string-by-int/{some_id}", (*api.Context).GetStringByInt). 19 | Get("/testapi/get-struct-by-int/{some_id}", (*api.Context).GetStructByInt). 20 | Get("/testapi/get-simple-array-by-string/{some_id}", (*api.Context).GetSimpleArrayByString). 21 | Get("/testapi/get-struct-array-by-string/{some_id}", (*api.Context).GetStructArrayByString). 22 | Post("/testapi/get-struct3", (*api.Context).PostStruct3). 23 | Delete("/testapi/get-struct3", (*api.Context).DelStruct3). 24 | Get("/testapi/get-struct2-by-int/{some_id}", (*api.Context).GetStruct2ByInt) 25 | return router 26 | } 27 | -------------------------------------------------------------------------------- /test/swagger.go: -------------------------------------------------------------------------------- 1 | // @Version 1.0.0 2 | // @Title Swagger Example API 3 | // @Description Swagger Example API 4 | // @Host 127.0.0.1:3000 5 | // @BasePath /api 6 | // @Name swagger 7 | // @Contact swagger@teambition.com 8 | // @URL teambition.com 9 | // @TermsOfServiceUrl http://teambition.com/ 10 | // @License Apache 11 | // @LicenseUrl http://teambition.com/ 12 | // @Schemes http,wss 13 | // @Consumes json,plain,xml,html 14 | // @Produces json,plain,xml,html 15 | package main 16 | 17 | import ( 18 | _ "github.com/teambition/swaggo/test/pkg/api" 19 | ) 20 | --------------------------------------------------------------------------------