├── .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 | [](https://travis-ci.org/teambition/swaggo)
6 | [](https://coveralls.io/r/teambition/swaggo)
7 | [](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 |
--------------------------------------------------------------------------------