├── examples
├── README.md
├── main_test.go
├── main.go
├── controller_store.go
├── controller_user.go
├── controller_pet.go
└── swagger.json
├── .gitignore
├── go.mod
├── .github
└── workflows
│ └── ci.yml
├── LICENSE
├── security.go
├── utils.go
├── converter.go
├── assets.go
├── generator.go
├── go.sum
├── validator.go
├── spec.go
├── internal.go
├── nop_test.go
├── nop.go
├── internal_test.go
├── README_zh-CN.md
├── spec_test.go
├── security_test.go
├── tag_test.go
├── tag.go
├── README.md
├── wrapper_test.go
├── wrapper.go
└── models.go
/examples/README.md:
--------------------------------------------------------------------------------
1 | This example shows how Echoswagger create a Swagger UI document page automatically.
2 |
3 | The doc contents in this example is very similar to the default example in http://editor.swagger.io/
4 |
5 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Binaries for programs and plugins
2 | *.exe
3 | *.exe~
4 | *.dll
5 | *.so
6 | *.dylib
7 |
8 | # Test binary, build with `go test -c`
9 | *.test
10 |
11 | # Output of the go coverage tool, specifically when used with LiteIDE
12 | *.out
13 |
14 | .DS_Store
15 | tmp
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/pangpanglabs/echoswagger
2 |
3 | go 1.12
4 |
5 | require (
6 | github.com/labstack/echo v3.3.10+incompatible
7 | github.com/labstack/gommon v0.3.0 // indirect
8 | github.com/stretchr/testify v1.4.0
9 | golang.org/x/crypto v0.0.0-20191219195013-becbf705a915 // indirect
10 | )
11 |
--------------------------------------------------------------------------------
/examples/main_test.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "encoding/json"
5 | "io/ioutil"
6 | "net/http/httptest"
7 | "testing"
8 |
9 | "github.com/labstack/echo"
10 | "github.com/pangpanglabs/echoswagger"
11 | "github.com/stretchr/testify/assert"
12 | )
13 |
14 | func TestMain(t *testing.T) {
15 | e := initServer()
16 | req := httptest.NewRequest(echo.GET, "/doc/swagger.json", nil)
17 | rec := httptest.NewRecorder()
18 | c := e.Echo().NewContext(req, rec)
19 | b, err := ioutil.ReadFile("./swagger.json")
20 | assert.Nil(t, err)
21 | s, err := e.(*echoswagger.Root).GetSpec(c, "/doc")
22 | assert.Nil(t, err)
23 | rs, err := json.Marshal(s)
24 | assert.Nil(t, err)
25 | assert.JSONEq(t, string(b), string(rs))
26 | }
27 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: Go
2 |
3 | on:
4 | push:
5 | branches: [ master, v2 ]
6 | pull_request:
7 | branches: [ master, v2 ]
8 |
9 | jobs:
10 |
11 | build:
12 | strategy:
13 | matrix:
14 | go-version: ["1.16", "1.13"]
15 | runs-on: ubuntu-latest
16 | steps:
17 | - name: Set up Go ${{ matrix.go-version }}
18 | uses: actions/setup-go@v2
19 | with:
20 | go-version: ${{ matrix.go-version }}
21 | id: go
22 |
23 | - uses: actions/checkout@v2
24 |
25 | - name: Build
26 | run: go build -v ./...
27 |
28 | - name: Test
29 | run: go test -race -coverprofile=coverage.txt -covermode=atomic ./...
30 |
31 | - name: Upload coverage to Codecov
32 | uses: codecov/codecov-action@v1
33 | with:
34 | file: ./coverage.txt
35 | fail_ci_if_error: false
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 陈文强
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 |
--------------------------------------------------------------------------------
/examples/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "github.com/labstack/echo"
5 | "github.com/pangpanglabs/echoswagger"
6 | )
7 |
8 | func main() {
9 | e := initServer().Echo()
10 | e.Logger.Fatal(e.Start(":1323"))
11 | }
12 |
13 | func initServer() echoswagger.ApiRoot {
14 | e := echo.New()
15 |
16 | se := echoswagger.New(e, "doc/", &echoswagger.Info{
17 | Title: "Swagger Petstore",
18 | Description: "This is a sample server Petstore server. You can find out more about Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, #swagger](http://swagger.io/irc/). For this sample, you can use the api key `special-key` to test the authorization filters.",
19 | Version: "1.0.0",
20 | TermsOfService: "http://swagger.io/terms/",
21 | Contact: &echoswagger.Contact{
22 | Email: "apiteam@swagger.io",
23 | },
24 | License: &echoswagger.License{
25 | Name: "Apache 2.0",
26 | URL: "http://www.apache.org/licenses/LICENSE-2.0.html",
27 | },
28 | })
29 |
30 | se.AddSecurityOAuth2("petstore_auth", "", echoswagger.OAuth2FlowImplicit,
31 | "http://petstore.swagger.io/oauth/dialog", "", map[string]string{
32 | "write:pets": "modify pets in your account",
33 | "read:pets": "read your pets",
34 | },
35 | ).AddSecurityAPIKey("api_key", "", echoswagger.SecurityInHeader)
36 |
37 | se.SetExternalDocs("Find out more about Swagger", "http://swagger.io").
38 | SetResponseContentType("application/xml", "application/json").
39 | SetUI(echoswagger.UISetting{DetachSpec: true, HideTop: true}).
40 | SetScheme("https", "http")
41 |
42 | PetController{}.Init(se.Group("pet", "/pet"))
43 | StoreController{}.Init(se.Group("store", "/store"))
44 | UserController{}.Init(se.Group("user", "/user"))
45 |
46 | return se
47 | }
48 |
--------------------------------------------------------------------------------
/security.go:
--------------------------------------------------------------------------------
1 | package echoswagger
2 |
3 | import "errors"
4 |
5 | type SecurityType string
6 |
7 | const (
8 | SecurityBasic SecurityType = "basic"
9 | SecurityOAuth2 SecurityType = "oauth2"
10 | SecurityAPIKey SecurityType = "apiKey"
11 | )
12 |
13 | type SecurityInType string
14 |
15 | const (
16 | SecurityInQuery SecurityInType = "query"
17 | SecurityInHeader SecurityInType = "header"
18 | )
19 |
20 | type OAuth2FlowType string
21 |
22 | const (
23 | OAuth2FlowImplicit OAuth2FlowType = "implicit"
24 | OAuth2FlowPassword OAuth2FlowType = "password"
25 | OAuth2FlowApplication OAuth2FlowType = "application"
26 | OAuth2FlowAccessCode OAuth2FlowType = "accessCode"
27 | )
28 |
29 | func (r *Root) checkSecurity(name string) bool {
30 | if name == "" {
31 | return false
32 | }
33 | if _, ok := r.spec.SecurityDefinitions[name]; ok {
34 | return false
35 | }
36 | return true
37 | }
38 |
39 | func setSecurity(security []map[string][]string, names ...string) []map[string][]string {
40 | m := make(map[string][]string)
41 | for _, name := range names {
42 | m[name] = make([]string, 0)
43 | }
44 | return append(security, m)
45 | }
46 |
47 | func setSecurityWithScope(security []map[string][]string, s ...map[string][]string) []map[string][]string {
48 | for _, t := range s {
49 | if len(t) == 0 {
50 | continue
51 | }
52 | for k, v := range t {
53 | if len(v) == 0 {
54 | t[k] = make([]string, 0)
55 | }
56 | }
57 | security = append(security, t)
58 | }
59 | return security
60 | }
61 |
62 | func (o *Operation) addSecurity(defs map[string]*SecurityDefinition, security []map[string][]string) error {
63 | for _, scy := range security {
64 | for k := range scy {
65 | if _, ok := defs[k]; !ok {
66 | return errors.New("echoswagger: not found SecurityDefinition with name: " + k)
67 | }
68 | }
69 | if containsMap(o.Security, scy) {
70 | continue
71 | }
72 | o.Security = append(o.Security, scy)
73 | }
74 | return nil
75 | }
76 |
--------------------------------------------------------------------------------
/utils.go:
--------------------------------------------------------------------------------
1 | package echoswagger
2 |
3 | import (
4 | "reflect"
5 | "strings"
6 | )
7 |
8 | func contains(list []string, s string) bool {
9 | for _, t := range list {
10 | if t == s {
11 | return true
12 | }
13 | }
14 | return false
15 | }
16 |
17 | func containsMap(list []map[string][]string, m map[string][]string) bool {
18 | LoopMaps:
19 | for _, t := range list {
20 | if len(t) != len(m) {
21 | continue
22 | }
23 | for k, v := range t {
24 | if mv, ok := m[k]; !ok || !equals(mv, v) {
25 | continue LoopMaps
26 | }
27 | }
28 | return true
29 | }
30 | return false
31 | }
32 |
33 | func equals(a []string, b []string) bool {
34 | if len(a) != len(b) {
35 | return false
36 | }
37 | for _, t := range a {
38 | if !contains(b, t) {
39 | return false
40 | }
41 | }
42 | return true
43 | }
44 |
45 | func indirect(v reflect.Value) reflect.Value {
46 | if v.Kind() == reflect.Ptr {
47 | ev := v.Elem()
48 | if !ev.IsValid() {
49 | ev = reflect.New(v.Type().Elem())
50 | }
51 | return indirect(ev)
52 | }
53 | return v
54 | }
55 |
56 | func indirectValue(p interface{}) reflect.Value {
57 | v := reflect.ValueOf(p)
58 | return indirect(v)
59 | }
60 |
61 | func indirectType(p interface{}) reflect.Type {
62 | t := reflect.TypeOf(p)
63 | LoopType:
64 | if t.Kind() == reflect.Ptr {
65 | t = t.Elem()
66 | goto LoopType
67 | }
68 | return t
69 | }
70 |
71 | // "" → "/"
72 | // "/" → "/"
73 | // "a" → "/a"
74 | // "/a" → "/a"
75 | // "/a/" → "/a/"
76 | func connectPath(paths ...string) string {
77 | var result string
78 | for i, path := range paths {
79 | // add prefix slash
80 | if len(path) == 0 || path[0] != '/' {
81 | path = "/" + path
82 | }
83 | // remove suffix slash, ignore last path
84 | if i != len(paths)-1 && len(path) != 0 && path[len(path)-1] == '/' {
85 | path = path[:len(path)-1]
86 | }
87 | result += path
88 | }
89 | return result
90 | }
91 |
92 | func removeTrailingSlash(path string) string {
93 | l := len(path) - 1
94 | if l > 0 && strings.HasSuffix(path, "/") {
95 | path = path[:l]
96 | }
97 | return path
98 | }
99 |
100 | func trimSuffixSlash(s, suffix string) string {
101 | s = connectPath(s)
102 | suffix = connectPath(suffix)
103 | s = removeTrailingSlash(s)
104 | suffix = removeTrailingSlash(suffix)
105 | return strings.TrimSuffix(s, suffix)
106 | }
107 |
--------------------------------------------------------------------------------
/converter.go:
--------------------------------------------------------------------------------
1 | package echoswagger
2 |
3 | import (
4 | "reflect"
5 | "strconv"
6 | "strings"
7 | "time"
8 | )
9 |
10 | // toSwaggerType returns type、format for a reflect.Type in swagger format
11 | func toSwaggerType(t reflect.Type) (string, string) {
12 | if t == reflect.TypeOf(time.Time{}) {
13 | return "string", "date-time"
14 | }
15 | switch t.Kind() {
16 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32,
17 | reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uintptr:
18 | return "integer", "int32"
19 | case reflect.Int64, reflect.Uint64:
20 | return "integer", "int64"
21 | case reflect.Float32:
22 | return "number", "float"
23 | case reflect.Float64:
24 | return "number", "double"
25 | case reflect.String:
26 | return "string", "string"
27 | case reflect.Bool:
28 | return "boolean", "boolean"
29 | case reflect.Struct:
30 | return "object", "object"
31 | case reflect.Map:
32 | return "object", "map"
33 | case reflect.Array, reflect.Slice:
34 | return "array", "array"
35 | case reflect.Ptr:
36 | return toSwaggerType(t.Elem())
37 | default:
38 | return "string", "string"
39 | }
40 | }
41 |
42 | // toSwaggerPath returns path in swagger format
43 | func toSwaggerPath(path string) string {
44 | var params []string
45 | for i := 0; i < len(path); i++ {
46 | if path[i] == ':' {
47 | j := i + 1
48 | for ; i < len(path) && path[i] != '/'; i++ {
49 | }
50 | params = append(params, path[j:i])
51 | }
52 | }
53 |
54 | for _, name := range params {
55 | path = strings.Replace(path, ":"+name, "{"+name+"}", 1)
56 | }
57 | return connectPath(path)
58 | }
59 |
60 | func converter(t reflect.Type) func(s string) (interface{}, error) {
61 | st, sf := toSwaggerType(t)
62 | if st == "integer" && sf == "int32" {
63 | return func(s string) (interface{}, error) {
64 | v, err := strconv.Atoi(s)
65 | return v, err
66 | }
67 | } else if st == "integer" && sf == "int64" {
68 | return func(s string) (interface{}, error) {
69 | v, err := strconv.ParseInt(s, 10, 64)
70 | return v, err
71 | }
72 | } else if st == "number" && sf == "float" {
73 | return func(s string) (interface{}, error) {
74 | v, err := strconv.ParseFloat(s, 32)
75 | return float32(v), err
76 | }
77 | } else if st == "number" && sf == "double" {
78 | return func(s string) (interface{}, error) {
79 | v, err := strconv.ParseFloat(s, 64)
80 | return v, err
81 | }
82 | } else if st == "boolean" && sf == "boolean" {
83 | return func(s string) (interface{}, error) {
84 | v, err := strconv.ParseBool(s)
85 | return v, err
86 | }
87 | } else if st == "array" && sf == "array" {
88 | return converter(t.Elem())
89 | } else {
90 | return func(s string) (interface{}, error) {
91 | return s, nil
92 | }
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/assets.go:
--------------------------------------------------------------------------------
1 | package echoswagger
2 |
3 | // CDN refer to https://cdnjs.com/libraries/swagger-ui
4 | const DefaultCDN = "https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/4.11.1"
5 |
6 | const SwaggerUIContent = `{{define "swagger"}}
7 |
8 |
9 |
10 |
11 | {{.title}}
12 |
13 |
14 |
15 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
87 |
88 |
89 | {{end}}`
90 |
--------------------------------------------------------------------------------
/generator.go:
--------------------------------------------------------------------------------
1 | package echoswagger
2 |
3 | import (
4 | "reflect"
5 | )
6 |
7 | func (Items) generate(t reflect.Type) *Items {
8 | st, sf := toSwaggerType(t)
9 | item := &Items{
10 | Type: st,
11 | }
12 | if st == "array" {
13 | item.Items = Items{}.generate(t.Elem())
14 | item.CollectionFormat = "multi"
15 | } else {
16 | item.Format = sf
17 | }
18 | return item
19 | }
20 |
21 | func (Parameter) generate(f reflect.StructField, in ParamInType) *Parameter {
22 | name, _ := getFieldName(f, in)
23 | if name == "-" {
24 | return nil
25 | }
26 | st, sf := toSwaggerType(f.Type)
27 | pm := &Parameter{
28 | Name: name,
29 | In: string(in),
30 | Type: st,
31 | }
32 | if st == "array" {
33 | pm.Items = Items{}.generate(f.Type.Elem())
34 | pm.CollectionFormat = "multi"
35 | } else {
36 | pm.Format = sf
37 | }
38 |
39 | pm.handleSwaggerTags(f, name, in)
40 | return pm
41 | }
42 |
43 | func (Header) generate(f reflect.StructField) *Header {
44 | name, _ := getFieldName(f, ParamInHeader)
45 | if name == "-" {
46 | return nil
47 | }
48 | st, sf := toSwaggerType(f.Type)
49 | h := &Header{
50 | Type: st,
51 | }
52 | if st == "array" {
53 | h.Items = Items{}.generate(f.Type.Elem())
54 | h.CollectionFormat = "multi"
55 | } else {
56 | h.Format = sf
57 | }
58 |
59 | h.handleSwaggerTags(f, name)
60 | return h
61 | }
62 |
63 | func (r *RawDefineDic) genSchema(v reflect.Value) *JSONSchema {
64 | if !v.IsValid() {
65 | return nil
66 | }
67 | v = indirect(v)
68 | st, sf := toSwaggerType(v.Type())
69 | schema := &JSONSchema{}
70 | if st == "array" {
71 | schema.Type = JSONType(st)
72 | if v.Len() == 0 {
73 | v = reflect.MakeSlice(v.Type(), 1, 1)
74 | }
75 | schema.Items = r.genSchema(v.Index(0))
76 | } else if st == "object" && sf == "map" {
77 | schema.Type = JSONType(st)
78 | if v.Len() == 0 {
79 | v = reflect.New(v.Type().Elem())
80 | } else {
81 | v = v.MapIndex(v.MapKeys()[0])
82 | }
83 | schema.AdditionalProperties = r.genSchema(v)
84 | } else if st == "object" {
85 | key := r.addDefinition(v)
86 | schema.Ref = DefPrefix + key
87 | } else {
88 | schema.Type = JSONType(st)
89 | schema.Format = sf
90 | zv := reflect.Zero(v.Type())
91 | if v.CanInterface() && zv.CanInterface() && v.Interface() != zv.Interface() {
92 | schema.Example = v.Interface()
93 | }
94 | }
95 | return schema
96 | }
97 |
98 | func (api) genHeader(v reflect.Value) map[string]*Header {
99 | rt := indirect(v).Type()
100 | if rt.Kind() != reflect.Struct {
101 | return nil
102 | }
103 | mh := make(map[string]*Header)
104 | for i := 0; i < rt.NumField(); i++ {
105 | f := rt.Field(i)
106 | h := Header{}.generate(f)
107 | if h != nil {
108 | name, _ := getFieldName(f, ParamInHeader)
109 | mh[name] = h
110 | }
111 | }
112 | return mh
113 | }
114 |
--------------------------------------------------------------------------------
/examples/controller_store.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "net/http"
5 | "time"
6 |
7 | "github.com/labstack/echo"
8 | "github.com/pangpanglabs/echoswagger"
9 | )
10 |
11 | type StoreController struct{}
12 |
13 | func (c StoreController) Init(g echoswagger.ApiGroup) {
14 | g.SetDescription("Access to Petstore orders")
15 |
16 | g.GET("/inventory", c.GetInventory).
17 | AddResponse(http.StatusOK, "successful operation", map[string]int32{}, nil).
18 | SetResponseContentType("application/json").
19 | SetOperationId("getInventory").
20 | SetDescription("Returns a map of status codes to quantities").
21 | SetSummary("Returns pet inventories by status").
22 | SetSecurity("api_key")
23 |
24 | type Order struct {
25 | Id int64 `json:"id"`
26 | PetId int64 `json:"petId"`
27 | Quantity int64 `json:"quantity"`
28 | ShipDate time.Time `json:"shipDate"`
29 | Status string `json:"status" swagger:"desc(Order Status),enum(placed|approved|delivered)"`
30 | Complete bool `json:"complete" swagger:"default(false)"`
31 | }
32 | g.POST("/order", c.CreateOrder).
33 | AddParamBody(Order{}, "body", "order placed for purchasing the pet", true).
34 | AddResponse(http.StatusOK, "successful operation", Order{}, nil).
35 | AddResponse(http.StatusBadRequest, "Invalid Order", nil, nil).
36 | SetOperationId("placeOrder").
37 | SetSummary("Place an order for a pet")
38 |
39 | type GetOrderId struct {
40 | orderId int64 `swagger:"max(10.0),min(1.0),desc(ID of pet that needs to be fetched)"`
41 | }
42 | g.GET("/order/{orderId}", c.GetOrderById).
43 | AddParamPathNested(&GetOrderId{}).
44 | AddResponse(http.StatusOK, "successful operation", Order{}, nil).
45 | AddResponse(http.StatusBadRequest, "Invalid ID supplied", nil, nil).
46 | AddResponse(http.StatusNotFound, "Order not found", nil, nil).
47 | SetOperationId("getOrderById").
48 | SetDescription("For valid response try integer IDs with value >= 1 and <= 10. Other values will generated exceptions").
49 | SetSummary("Find purchase order by ID")
50 |
51 | type DeleteOrderId struct {
52 | orderId int64 `swagger:"min(1.0),desc(ID of the order that needs to be deleted)"`
53 | }
54 | g.DELETE("/order/{orderId}", c.DeleteOrderById).
55 | AddParamPathNested(&DeleteOrderId{}).
56 | AddResponse(http.StatusBadRequest, "Invalid ID supplied", nil, nil).
57 | AddResponse(http.StatusNotFound, "Order not found", nil, nil).
58 | SetOperationId("deleteOrder").
59 | SetDescription("For valid response try integer IDs with positive integer value. Negative or non-integer values will generate API errors").
60 | SetSummary("Delete purchase order by ID")
61 | }
62 |
63 | func (StoreController) GetInventory(c echo.Context) error {
64 | return nil
65 | }
66 |
67 | func (StoreController) CreateOrder(c echo.Context) error {
68 | return nil
69 | }
70 |
71 | func (StoreController) GetOrderById(c echo.Context) error {
72 | return nil
73 | }
74 |
75 | func (StoreController) DeleteOrderById(c echo.Context) error {
76 | return nil
77 | }
78 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
3 | github.com/labstack/echo v3.3.10+incompatible h1:pGRcYk231ExFAyoAjAfD85kQzRJCRI8bbnE7CX5OEgg=
4 | github.com/labstack/echo v3.3.10+incompatible/go.mod h1:0INS7j/VjnFxD4E2wkz67b8cVwCLbBmJyDaka6Cmk1s=
5 | github.com/labstack/gommon v0.3.0 h1:JEeO0bvc78PKdyHxloTKiF8BD5iGrH8T6MSeGvSgob0=
6 | github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k=
7 | github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU=
8 | github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
9 | github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
10 | github.com/mattn/go-isatty v0.0.9 h1:d5US/mDsogSGW37IV293h//ZFaeajb69h+EHFsv2xGg=
11 | github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
12 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
13 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
14 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
15 | github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
16 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
17 | github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
18 | github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
19 | github.com/valyala/fasttemplate v1.0.1 h1:tY9CJiPnMXf1ERmG2EyK7gNUd+c6RKGD0IfU8WdUSz8=
20 | github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
21 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
22 | golang.org/x/crypto v0.0.0-20191219195013-becbf705a915 h1:aJ0ex187qoXrJHPo8ZasVTASQB7llQP6YeNzgDALPRk=
23 | golang.org/x/crypto v0.0.0-20191219195013-becbf705a915/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
24 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 h1:0GoQqolDA55aaLxZyTzK/Y2ePZzZTUrRacwib7cNsYQ=
25 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
26 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
27 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
28 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
29 | golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a h1:aYOabOQFp6Vj6W1F80affTUvO9UxmJRx8K0gsfABByQ=
30 | golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
31 | golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
32 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
33 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
34 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
35 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
36 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
37 |
--------------------------------------------------------------------------------
/validator.go:
--------------------------------------------------------------------------------
1 | package echoswagger
2 |
3 | import (
4 | "net/url"
5 | "reflect"
6 | "regexp"
7 | "time"
8 | )
9 |
10 | var emailRegexp = regexp.MustCompile("^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$")
11 |
12 | func isValidEmail(str string) bool {
13 | return emailRegexp.MatchString(str)
14 | }
15 |
16 | // See: https://github.com/swagger-api/swagger-js/blob/7414ad062ba9b6d9cc397c72e7561ec775b35a9f/lib/shred/parseUri.js#L28
17 | func isValidURL(str string) bool {
18 | if _, err := url.ParseRequestURI(str); err != nil {
19 | return false
20 | }
21 | return true
22 | }
23 |
24 | func isValidParam(t reflect.Type, nest, inner bool) bool {
25 | if t == nil {
26 | return false
27 | }
28 | switch t.Kind() {
29 | case reflect.Bool, reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
30 | reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr,
31 | reflect.Float32, reflect.Float64, reflect.String:
32 | if !nest || (nest && inner) {
33 | return true
34 | }
35 | case reflect.Array, reflect.Slice:
36 | return isValidParam(t.Elem(), nest, true)
37 | case reflect.Ptr:
38 | return isValidParam(t.Elem(), nest, inner)
39 | case reflect.Struct:
40 | if t == reflect.TypeOf(time.Time{}) && (!nest || nest && inner) {
41 | return true
42 | } else if !inner {
43 | for i := 0; i < t.NumField(); i++ {
44 | inner := true
45 | if t.Field(i).Type.Kind() == reflect.Struct && t.Field(i).Anonymous {
46 | inner = false
47 | }
48 | if !isValidParam(t.Field(i).Type, nest, inner) {
49 | return false
50 | }
51 | }
52 | return true
53 | }
54 | }
55 | return false
56 | }
57 |
58 | // isValidSchema reports a type is valid for body param.
59 | // valid case:
60 | // 1. Struct
61 | // 2. Struct{ A int64 }
62 | // 3. *[]Struct
63 | // 4. [][]Struct
64 | // 5. []Struct{ A []Struct }
65 | // 6. []Struct{ A Map[string]string }
66 | // 7. *Struct{ A []Map[int64]Struct }
67 | // 8. Map[string]string
68 | // 9. []int64
69 | // invalid case:
70 | // 1. interface{}
71 | // 2. Map[Struct]string
72 | func isValidSchema(t reflect.Type, inner bool, pres ...reflect.Type) bool {
73 | if t == nil {
74 | return false
75 | }
76 | for _, pre := range pres {
77 | if t == pre {
78 | return true
79 | }
80 | }
81 |
82 | switch t.Kind() {
83 | case reflect.Bool, reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
84 | reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr,
85 | reflect.Float32, reflect.Float64, reflect.String, reflect.Interface:
86 | return true
87 | case reflect.Array, reflect.Slice:
88 | return isValidSchema(t.Elem(), inner, pres...)
89 | case reflect.Map:
90 | return isBasicType(t.Key()) && isValidSchema(t.Elem(), true, pres...)
91 | case reflect.Ptr:
92 | return isValidSchema(t.Elem(), inner, pres...)
93 | case reflect.Struct:
94 | pres = append(pres, t)
95 | if t == reflect.TypeOf(time.Time{}) {
96 | return true
97 | }
98 | for i := 0; i < t.NumField(); i++ {
99 | if !isValidSchema(t.Field(i).Type, true, pres...) {
100 | return false
101 | }
102 | }
103 | return true
104 | }
105 | return false
106 | }
107 |
108 | func isBasicType(t reflect.Type) bool {
109 | if t == nil {
110 | return false
111 | }
112 | switch t.Kind() {
113 | case reflect.Bool, reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
114 | reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr,
115 | reflect.Float32, reflect.Float64, reflect.String:
116 | return true
117 | case reflect.Ptr:
118 | return isBasicType(t.Elem())
119 | case reflect.Struct:
120 | if t == reflect.TypeOf(time.Time{}) {
121 | return true
122 | }
123 | }
124 | return false
125 | }
126 |
127 | func isValidScheme(s string) bool {
128 | if s == "http" || s == "https" || s == "ws" || s == "wss" {
129 | return true
130 | }
131 | return false
132 | }
133 |
--------------------------------------------------------------------------------
/examples/controller_user.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "encoding/xml"
5 | "net/http"
6 | "time"
7 |
8 | "github.com/labstack/echo"
9 | "github.com/pangpanglabs/echoswagger"
10 | )
11 |
12 | type UserController struct{}
13 |
14 | func (c UserController) Init(g echoswagger.ApiGroup) {
15 | g.SetDescription("Operations about user").
16 | SetExternalDocs("Find out more about our store", "http://swagger.io")
17 |
18 | type User struct {
19 | X xml.Name `xml:"Users"`
20 | Id int64 `json:"id"`
21 | UserName string `json:"username"`
22 | FirstName string `json:"firstname"`
23 | LastName string `json:"lastname"`
24 | Email string `json:"email"`
25 | Password string `json:"password"`
26 | Phone string `json:"phone"`
27 | UserStatus int32 `json:"userStatus" swagger:"desc(User Status)"`
28 | }
29 | g.POST("", c.Create).
30 | AddParamBody(&User{}, "body", "Created user object", true).
31 | SetOperationId("createUser").
32 | SetDescription("This can only be done by the logged in user.").
33 | SetSummary("Create user")
34 |
35 | g.POST("/createWithArray", c.CreateWithArray).
36 | AddParamBody(&[]User{}, "body", "List of user object", true).
37 | SetOperationId("createUsersWithArrayInput").
38 | SetRequestContentType("application/json", "application/xml").
39 | SetSummary("Creates list of users with given input array")
40 |
41 | g.POST("/createWithList", c.CreateWithList).
42 | AddParamBody(&[]User{}, "body", "List of user object", true).
43 | SetOperationId("createUsersWithListInput").
44 | SetSummary("Creates list of users with given input array")
45 |
46 | type ResponseHeader struct {
47 | XRateLimit int32 `json:"X-Rate-Limit" swagger:"desc(calls per hour allowed by the user)"`
48 | XExpiresAfter time.Time `json:"X-Expires-After" swagger:"desc(date in UTC when token expires)"`
49 | }
50 | g.GET("/login", c.Login).
51 | AddParamQuery("", "username", "The user name for login", true).
52 | AddParamQuery("", "password", "The password for login in clear text", true).
53 | AddResponse(http.StatusOK, "successful operation", "", ResponseHeader{}).
54 | AddResponse(http.StatusBadRequest, "Invalid username/password supplied", nil, nil).
55 | SetOperationId("loginUser").
56 | SetSummary("Logs user into the system")
57 |
58 | g.GET("/logout", c.Logout).
59 | SetOperationId("logoutUser").
60 | SetSummary("Logs out current logged in user session")
61 |
62 | g.GET("/{username}", c.GetByUsername).
63 | AddParamPath("", "username", "The name that needs to be fetched. Use user1 for testing. ").
64 | AddResponse(http.StatusOK, "successful operation", &User{}, nil).
65 | AddResponse(http.StatusBadRequest, "Invalid username supplied", nil, nil).
66 | AddResponse(http.StatusNotFound, "User not found", nil, nil).
67 | SetOperationId("getUserByName").
68 | SetSummary("Get user by user name")
69 |
70 | g.PUT("/{username}", c.UpdateByUsername).
71 | AddParamPath("", "username", "name that need to be updated").
72 | AddParamBody(&User{}, "body", "Updated user object", true).
73 | AddResponse(http.StatusBadRequest, "Invalid user supplied", nil, nil).
74 | AddResponse(http.StatusNotFound, "User not found", nil, nil).
75 | SetOperationId("updateUser").
76 | SetDescription("This can only be done by the logged in user.").
77 | SetSummary("Updated user")
78 |
79 | g.DELETE("/{username}", c.DeleteByUsername).
80 | AddParamPath("", "username", "The name that needs to be deleted").
81 | AddResponse(http.StatusBadRequest, "Invalid username supplied", nil, nil).
82 | AddResponse(http.StatusNotFound, "User not found", nil, nil).
83 | SetOperationId("deleteUser").
84 | SetDescription("This can only be done by the logged in user.").
85 | SetSummary("Delete user")
86 | }
87 |
88 | func (UserController) Create(c echo.Context) error {
89 | return nil
90 | }
91 |
92 | func (UserController) CreateWithArray(c echo.Context) error {
93 | return nil
94 | }
95 |
96 | func (UserController) CreateWithList(c echo.Context) error {
97 | return nil
98 | }
99 |
100 | func (UserController) Login(c echo.Context) error {
101 | return nil
102 | }
103 |
104 | func (UserController) Logout(c echo.Context) error {
105 | return nil
106 | }
107 |
108 | func (UserController) GetByUsername(c echo.Context) error {
109 | return nil
110 | }
111 |
112 | func (UserController) UpdateByUsername(c echo.Context) error {
113 | return nil
114 | }
115 |
116 | func (UserController) DeleteByUsername(c echo.Context) error {
117 | return nil
118 | }
119 |
--------------------------------------------------------------------------------
/spec.go:
--------------------------------------------------------------------------------
1 | package echoswagger
2 |
3 | import (
4 | "encoding/xml"
5 | "net/http"
6 | "net/url"
7 | "reflect"
8 |
9 | "github.com/labstack/echo"
10 | )
11 |
12 | const (
13 | DefPrefix = "#/definitions/"
14 | SwaggerVersion = "2.0"
15 | SpecName = "swagger.json"
16 | )
17 |
18 | func (r *Root) specHandler(docPath string) echo.HandlerFunc {
19 | return func(c echo.Context) error {
20 | spec, err := r.GetSpec(c, docPath)
21 | if err != nil {
22 | return c.String(http.StatusInternalServerError, err.Error())
23 | }
24 | var basePath string
25 | if uri, err := url.ParseRequestURI(c.Request().Referer()); err == nil {
26 | basePath = trimSuffixSlash(uri.Path, docPath)
27 | spec.Host = uri.Host
28 | } else {
29 | basePath = trimSuffixSlash(c.Request().URL.Path, connectPath(docPath, SpecName))
30 | spec.Host = c.Request().Host
31 | }
32 | spec.BasePath = basePath
33 | return c.JSON(http.StatusOK, spec)
34 | }
35 | }
36 |
37 | // Generate swagger spec data, without host & basePath info
38 | func (r *Root) GetSpec(c echo.Context, docPath string) (Swagger, error) {
39 | r.once.Do(func() {
40 | r.err = r.genSpec(c)
41 | r.cleanUp()
42 | })
43 | if r.err != nil {
44 | return Swagger{}, r.err
45 | }
46 | return *r.spec, nil
47 | }
48 |
49 | func (r *Root) genSpec(c echo.Context) error {
50 | r.spec.Swagger = SwaggerVersion
51 | r.spec.Paths = make(map[string]interface{})
52 |
53 | for i := range r.groups {
54 | group := &r.groups[i]
55 | r.spec.Tags = append(r.spec.Tags, &group.tag)
56 | for j := range group.apis {
57 | a := &group.apis[j]
58 | if err := a.operation.addSecurity(r.spec.SecurityDefinitions, group.security); err != nil {
59 | return err
60 | }
61 | if err := r.transfer(a); err != nil {
62 | return err
63 | }
64 | }
65 | }
66 |
67 | for i := range r.apis {
68 | if err := r.transfer(&r.apis[i]); err != nil {
69 | return err
70 | }
71 | }
72 |
73 | for k, v := range *r.defs {
74 | r.spec.Definitions[k] = v.Schema
75 | }
76 | return nil
77 | }
78 |
79 | func (r *Root) transfer(a *api) error {
80 | if err := a.operation.addSecurity(r.spec.SecurityDefinitions, a.security); err != nil {
81 | return err
82 | }
83 |
84 | path := toSwaggerPath(a.route.Path)
85 | if len(a.operation.Responses) == 0 {
86 | a.operation.Responses["default"] = &Response{
87 | Description: "successful operation",
88 | }
89 | }
90 |
91 | if p, ok := r.spec.Paths[path]; ok {
92 | p.(*Path).oprationAssign(a.route.Method, &a.operation)
93 | } else {
94 | p := &Path{}
95 | p.oprationAssign(a.route.Method, &a.operation)
96 | r.spec.Paths[path] = p
97 | }
98 | return nil
99 | }
100 |
101 | func (p *Path) oprationAssign(method string, operation *Operation) {
102 | switch method {
103 | case echo.GET:
104 | p.Get = operation
105 | case echo.POST:
106 | p.Post = operation
107 | case echo.PUT:
108 | p.Put = operation
109 | case echo.DELETE:
110 | p.Delete = operation
111 | case echo.OPTIONS:
112 | p.Options = operation
113 | case echo.HEAD:
114 | p.Head = operation
115 | case echo.PATCH:
116 | p.Patch = operation
117 | }
118 | }
119 |
120 | func (r *Root) cleanUp() {
121 | r.echo = nil
122 | r.groups = nil
123 | r.apis = nil
124 | r.defs = nil
125 | }
126 |
127 | // addDefinition adds definition specification and returns
128 | // key of RawDefineDic
129 | func (r *RawDefineDic) addDefinition(v reflect.Value) string {
130 | exist, key := r.getKey(v)
131 | if exist {
132 | return key
133 | }
134 |
135 | schema := &JSONSchema{
136 | Type: "object",
137 | Properties: make(map[string]*JSONSchema),
138 | }
139 |
140 | (*r)[key] = RawDefine{
141 | Value: v,
142 | Schema: schema,
143 | }
144 |
145 | r.handleStruct(v, schema)
146 |
147 | if schema.XML == nil {
148 | schema.XML = &XMLSchema{}
149 | }
150 | if schema.XML.Name == "" {
151 | schema.XML.Name = v.Type().Name()
152 | }
153 | return key
154 | }
155 |
156 | // handleStruct handles fields of a struct
157 | func (r *RawDefineDic) handleStruct(v reflect.Value, schema *JSONSchema) {
158 | for i := 0; i < v.NumField(); i++ {
159 | f := v.Type().Field(i)
160 | name, hasTag := getFieldName(f, ParamInBody)
161 | if name == "-" {
162 | continue
163 | }
164 | if f.Type == reflect.TypeOf(xml.Name{}) {
165 | schema.handleXMLTags(f)
166 | continue
167 | }
168 | if f.Type.Kind() == reflect.Struct && f.Anonymous && !hasTag {
169 | r.handleStruct(v.Field(i), schema)
170 | continue
171 | }
172 | sp := r.genSchema(v.Field(i))
173 | sp.handleXMLTags(f)
174 | if sp.XML != nil {
175 | sp.handleChildXMLTags(sp.XML.Name, r)
176 | }
177 | schema.Properties[name] = sp
178 |
179 | schema.handleSwaggerTags(f, name)
180 | }
181 | }
182 |
--------------------------------------------------------------------------------
/internal.go:
--------------------------------------------------------------------------------
1 | package echoswagger
2 |
3 | import (
4 | "bytes"
5 | "encoding/json"
6 | "html/template"
7 | "net/http"
8 | "reflect"
9 |
10 | "github.com/labstack/echo"
11 | )
12 |
13 | type ParamInType string
14 |
15 | const (
16 | ParamInQuery ParamInType = "query"
17 | ParamInHeader ParamInType = "header"
18 | ParamInPath ParamInType = "path"
19 | ParamInFormData ParamInType = "formData"
20 | ParamInBody ParamInType = "body"
21 | )
22 |
23 | type UISetting struct {
24 | DetachSpec bool
25 | HideTop bool
26 | CDN string
27 | }
28 |
29 | type RawDefineDic map[string]RawDefine
30 |
31 | type RawDefine struct {
32 | Value reflect.Value
33 | Schema *JSONSchema
34 | }
35 |
36 | func (r *Root) docHandler(docPath string) echo.HandlerFunc {
37 | t, err := template.New("swagger").Parse(SwaggerUIContent)
38 | if err != nil {
39 | panic(err)
40 | }
41 | return func(c echo.Context) error {
42 | cdn := r.ui.CDN
43 | if cdn == "" {
44 | cdn = DefaultCDN
45 | }
46 | buf := new(bytes.Buffer)
47 | params := map[string]interface{}{
48 | "title": r.spec.Info.Title,
49 | "cdn": cdn,
50 | "specName": SpecName,
51 | }
52 | if !r.ui.DetachSpec {
53 | spec, err := r.GetSpec(c, docPath)
54 | if err != nil {
55 | return c.String(http.StatusInternalServerError, err.Error())
56 | }
57 | b, err := json.Marshal(spec)
58 | if err != nil {
59 | return c.String(http.StatusInternalServerError, err.Error())
60 | }
61 | params["spec"] = string(b)
62 | params["docPath"] = docPath
63 | params["hideTop"] = true
64 | } else {
65 | params["hideTop"] = r.ui.HideTop
66 | }
67 | if err := t.Execute(buf, params); err != nil {
68 | return c.String(http.StatusInternalServerError, err.Error())
69 | }
70 | return c.HTMLBlob(http.StatusOK, buf.Bytes())
71 | }
72 | }
73 |
74 | func (r *RawDefineDic) getKey(v reflect.Value) (bool, string) {
75 | for k, d := range *r {
76 | if reflect.DeepEqual(d.Value.Interface(), v.Interface()) {
77 | return true, k
78 | }
79 | }
80 | name := v.Type().Name()
81 | for k := range *r {
82 | if name == k {
83 | name += "_"
84 | }
85 | }
86 | return false, name
87 | }
88 |
89 | func (r *routers) appendRoute(route *echo.Route) *api {
90 | opr := Operation{
91 | Responses: make(map[string]*Response),
92 | }
93 | a := api{
94 | route: route,
95 | defs: r.defs,
96 | operation: opr,
97 | }
98 | r.apis = append(r.apis, a)
99 | return &r.apis[len(r.apis)-1]
100 | }
101 |
102 | func (g *api) addParams(p interface{}, in ParamInType, name, desc string, required, nest bool) Api {
103 | if !isValidParam(reflect.TypeOf(p), nest, false) {
104 | panic("echoswagger: invalid " + string(in) + " param")
105 | }
106 | rt := indirectType(p)
107 | st, sf := toSwaggerType(rt)
108 | if st == "object" && sf == "object" {
109 | g.operation.handleParamStruct(rt, in)
110 | } else {
111 | name = g.operation.rename(name)
112 | pm := &Parameter{
113 | Name: name,
114 | In: string(in),
115 | Description: desc,
116 | Required: required,
117 | Type: st,
118 | }
119 | if st == "array" {
120 | pm.Items = Items{}.generate(rt.Elem())
121 | pm.CollectionFormat = "multi"
122 | } else {
123 | pm.Format = sf
124 | }
125 | g.operation.Parameters = append(g.operation.Parameters, pm)
126 | }
127 | return g
128 | }
129 |
130 | func (g *api) addBodyParams(p interface{}, name, desc string, required bool) Api {
131 | if !isValidSchema(reflect.TypeOf(p), false) {
132 | panic("echoswagger: invalid body parameter")
133 | }
134 | for _, param := range g.operation.Parameters {
135 | if param.In == string(ParamInBody) {
136 | panic("echoswagger: multiple body parameters are not allowed")
137 | }
138 | }
139 |
140 | rv := indirectValue(p)
141 | pm := &Parameter{
142 | Name: name,
143 | In: string(ParamInBody),
144 | Description: desc,
145 | Required: required,
146 | Schema: g.defs.genSchema(rv),
147 | }
148 | g.operation.Parameters = append(g.operation.Parameters, pm)
149 | return g
150 | }
151 |
152 | func (o Operation) rename(s string) string {
153 | for _, p := range o.Parameters {
154 | if p.Name == s {
155 | return o.rename(s + "_")
156 | }
157 | }
158 | return s
159 | }
160 |
161 | func (o *Operation) handleParamStruct(rt reflect.Type, in ParamInType) {
162 | for i := 0; i < rt.NumField(); i++ {
163 | if rt.Field(i).Type.Kind() == reflect.Struct && rt.Field(i).Anonymous {
164 | o.handleParamStruct(rt.Field(i).Type, in)
165 | } else {
166 | pm := Parameter{}.generate(rt.Field(i), in)
167 | if pm != nil {
168 | pm.Name = o.rename(pm.Name)
169 | o.Parameters = append(o.Parameters, pm)
170 | }
171 | }
172 | }
173 | }
174 |
--------------------------------------------------------------------------------
/nop_test.go:
--------------------------------------------------------------------------------
1 | package echoswagger
2 |
3 | import (
4 | "net/http"
5 | "testing"
6 |
7 | "github.com/labstack/echo"
8 | "github.com/stretchr/testify/assert"
9 | )
10 |
11 | func testHandler(echo.Context) error { return nil }
12 |
13 | func TestNop(t *testing.T) {
14 | e := echo.New()
15 | r := NewNop(e)
16 |
17 | path := "test"
18 | expectHandler := "github.com/pangpanglabs/echoswagger.testHandler"
19 | a := r.Add(http.MethodConnect, path, testHandler)
20 | assert.Equal(t, a.Route().Method, http.MethodConnect)
21 | assert.Equal(t, a.Route().Name, expectHandler)
22 | assert.Equal(t, a.Route().Path, path)
23 |
24 | a = r.GET(path, testHandler)
25 | assert.Equal(t, a.Route().Method, http.MethodGet)
26 | assert.Equal(t, a.Route().Name, expectHandler)
27 | assert.Equal(t, a.Route().Path, path)
28 |
29 | a = r.POST(path, testHandler)
30 | assert.Equal(t, a.Route().Method, http.MethodPost)
31 | assert.Equal(t, a.Route().Name, expectHandler)
32 | assert.Equal(t, a.Route().Path, path)
33 |
34 | a = r.PUT(path, testHandler)
35 | assert.Equal(t, a.Route().Method, http.MethodPut)
36 | assert.Equal(t, a.Route().Name, expectHandler)
37 | assert.Equal(t, a.Route().Path, path)
38 |
39 | a = r.DELETE(path, testHandler)
40 | assert.Equal(t, a.Route().Method, http.MethodDelete)
41 | assert.Equal(t, a.Route().Name, expectHandler)
42 | assert.Equal(t, a.Route().Path, path)
43 |
44 | a = r.OPTIONS(path, testHandler)
45 | assert.Equal(t, a.Route().Method, http.MethodOptions)
46 | assert.Equal(t, a.Route().Name, expectHandler)
47 | assert.Equal(t, a.Route().Path, path)
48 |
49 | a = r.HEAD(path, testHandler)
50 | assert.Equal(t, a.Route().Method, http.MethodHead)
51 | assert.Equal(t, a.Route().Name, expectHandler)
52 | assert.Equal(t, a.Route().Path, path)
53 |
54 | a = r.PATCH(path, testHandler)
55 | assert.Equal(t, a.Route().Method, http.MethodPatch)
56 | assert.Equal(t, a.Route().Name, expectHandler)
57 | assert.Equal(t, a.Route().Path, path)
58 |
59 | g := r.Group("", "g/")
60 | assert.EqualValues(t, g.EchoGroup(), r.Echo().Group("g/"))
61 |
62 | eg := g.EchoGroup()
63 | assert.Equal(t, eg, r.BindGroup("", eg).EchoGroup())
64 |
65 | assert.Equal(t, r.SetRequestContentType(), r)
66 | assert.Equal(t, r.SetResponseContentType(), r)
67 | assert.Equal(t, r.SetExternalDocs("", ""), r)
68 | assert.Equal(t, r.AddSecurityBasic("", ""), r)
69 | assert.Equal(t, r.AddSecurityAPIKey("", "", ""), r)
70 | assert.Equal(t, r.AddSecurityOAuth2("", "", "", "", "", nil), r)
71 | assert.Equal(t, r.SetUI(UISetting{}), r)
72 | assert.Equal(t, r.SetScheme(), r)
73 | assert.Nil(t, r.GetRaw())
74 | assert.Equal(t, r.SetRaw(nil), r)
75 | assert.Equal(t, r.Echo(), e)
76 |
77 | expectPath := "g/" + path
78 | a = g.Add(http.MethodConnect, path, testHandler)
79 | assert.Equal(t, a.Route().Method, http.MethodConnect)
80 | assert.Equal(t, a.Route().Name, expectHandler)
81 | assert.Equal(t, a.Route().Path, expectPath)
82 |
83 | a = g.GET(path, testHandler)
84 | assert.Equal(t, a.Route().Method, http.MethodGet)
85 | assert.Equal(t, a.Route().Name, expectHandler)
86 | assert.Equal(t, a.Route().Path, expectPath)
87 |
88 | a = g.POST(path, testHandler)
89 | assert.Equal(t, a.Route().Method, http.MethodPost)
90 | assert.Equal(t, a.Route().Name, expectHandler)
91 | assert.Equal(t, a.Route().Path, expectPath)
92 |
93 | a = g.PUT(path, testHandler)
94 | assert.Equal(t, a.Route().Method, http.MethodPut)
95 | assert.Equal(t, a.Route().Name, expectHandler)
96 | assert.Equal(t, a.Route().Path, expectPath)
97 |
98 | a = g.DELETE(path, testHandler)
99 | assert.Equal(t, a.Route().Method, http.MethodDelete)
100 | assert.Equal(t, a.Route().Name, expectHandler)
101 | assert.Equal(t, a.Route().Path, expectPath)
102 |
103 | a = g.OPTIONS(path, testHandler)
104 | assert.Equal(t, a.Route().Method, http.MethodOptions)
105 | assert.Equal(t, a.Route().Name, expectHandler)
106 | assert.Equal(t, a.Route().Path, expectPath)
107 |
108 | a = g.HEAD(path, testHandler)
109 | assert.Equal(t, a.Route().Method, http.MethodHead)
110 | assert.Equal(t, a.Route().Name, expectHandler)
111 | assert.Equal(t, a.Route().Path, expectPath)
112 |
113 | a = g.PATCH(path, testHandler)
114 | assert.Equal(t, a.Route().Method, http.MethodPatch)
115 | assert.Equal(t, a.Route().Name, expectHandler)
116 | assert.Equal(t, a.Route().Path, expectPath)
117 |
118 | assert.Equal(t, g.SetDescription(""), g)
119 | assert.Equal(t, g.SetExternalDocs("", ""), g)
120 | assert.Equal(t, g.SetSecurity(), g)
121 | assert.Equal(t, g.SetSecurityWithScope(nil), g)
122 |
123 | assert.Equal(t, a.AddParamPath(nil, "", ""), a)
124 | assert.Equal(t, a.AddParamPathNested(nil), a)
125 | assert.Equal(t, a.AddParamQuery(nil, "", "", false), a)
126 | assert.Equal(t, a.AddParamQueryNested(nil), a)
127 | assert.Equal(t, a.AddParamForm(nil, "", "", false), a)
128 | assert.Equal(t, a.AddParamFormNested(nil), a)
129 | assert.Equal(t, a.AddParamHeader(nil, "", "", false), a)
130 | assert.Equal(t, a.AddParamHeaderNested(nil), a)
131 | assert.Equal(t, a.AddParamBody(nil, "", "", false), a)
132 | assert.Equal(t, a.AddParamFile("", "", false), a)
133 | assert.Equal(t, a.SetRequestContentType(), a)
134 | assert.Equal(t, a.SetResponseContentType(), a)
135 | assert.Equal(t, a.AddResponse(0, "", nil, nil), a)
136 | assert.Equal(t, a.SetOperationId(""), a)
137 | assert.Equal(t, a.SetDeprecated(), a)
138 | assert.Equal(t, a.SetDescription(""), a)
139 | assert.Equal(t, a.SetExternalDocs("", ""), a)
140 | assert.Equal(t, a.SetSummary(""), a)
141 | assert.Equal(t, a.SetSecurity(), a)
142 | assert.Equal(t, a.SetSecurityWithScope(nil), a)
143 | }
144 |
--------------------------------------------------------------------------------
/examples/controller_pet.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "net/http"
5 |
6 | "github.com/labstack/echo"
7 | "github.com/pangpanglabs/echoswagger"
8 | )
9 |
10 | type PetController struct{}
11 |
12 | func (c PetController) Init(g echoswagger.ApiGroup) {
13 | g.SetDescription("Everything about your Pets").
14 | SetExternalDocs("Find out more", "http://swagger.io")
15 |
16 | security := map[string][]string{
17 | "petstore_auth": {"write:pets", "read:pets"},
18 | }
19 |
20 | type Category struct {
21 | Id int64 `json:"id"`
22 | Name string `json:"name"`
23 | }
24 | type Tag struct {
25 | Id int64 `json:"id"`
26 | Name string `json:"name"`
27 | }
28 | type Pet struct {
29 | Id int64 `json:"id"`
30 | Category Category `json:"category"`
31 | Name string `json:"name" swagger:"required"`
32 | PhotoUrls []string `json:"photoUrls" xml:"photoUrl" swagger:"required"`
33 | Tags []Tag `json:"tags" xml:"tag"`
34 | Status string `json:"status" swagger:"enum(available|pending|sold),desc(pet status in the store)"`
35 | }
36 | pet := Pet{Name: "doggie"}
37 | g.POST("", c.Create).
38 | AddParamBody(&pet, "body", "Pet object that needs to be added to the store", true).
39 | AddResponse(http.StatusMethodNotAllowed, "Invalid input", nil, nil).
40 | SetRequestContentType("application/json", "application/xml").
41 | SetOperationId("addPet").
42 | SetSummary("Add a new pet to the store").
43 | SetSecurityWithScope(security)
44 |
45 | g.PUT("", c.Update).
46 | AddParamBody(&pet, "body", "Pet object that needs to be added to the store", true).
47 | AddResponse(http.StatusBadRequest, "Invalid ID supplied", nil, nil).
48 | AddResponse(http.StatusNotFound, "Pet not found", nil, nil).
49 | AddResponse(http.StatusMethodNotAllowed, "Validation exception", nil, nil).
50 | SetRequestContentType("application/json", "application/xml").
51 | SetOperationId("updatePet").
52 | SetSummary("Update an existing pet").
53 | SetSecurityWithScope(security)
54 |
55 | type StatusParam struct {
56 | Status []string `query:"status" swagger:"required,desc(Status values that need to be considered for filter),default(available),enum(available|pending|sold)"`
57 | }
58 | g.GET("/findByStatus", c.FindByStatus).
59 | AddParamQueryNested(&StatusParam{}).
60 | AddResponse(http.StatusOK, "successful operation", &[]Pet{pet}, nil).
61 | AddResponse(http.StatusBadRequest, "Invalid status value", nil, nil).
62 | SetOperationId("findPetsByStatus").
63 | SetDescription("Multiple status values can be provided with comma separated strings").
64 | SetSummary("Finds Pets by status").
65 | SetSecurityWithScope(security)
66 |
67 | g.GET("/findByTags", c.FindByTags).
68 | AddParamQuery([]string{}, "tags", "Tags to filter by", true).
69 | AddResponse(http.StatusOK, "successful operation", &[]Pet{pet}, nil).
70 | AddResponse(http.StatusBadRequest, "Invalid tag value", nil, nil).
71 | SetOperationId("findPetsByTags").
72 | SetDeprecated().
73 | SetDescription("Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing.").
74 | SetSummary("Finds Pets by tags").
75 | SetSecurityWithScope(security)
76 |
77 | g.GET("/{petId}", c.GetById).
78 | AddParamPath(0, "petId", "ID of pet to return").
79 | AddResponse(http.StatusOK, "successful operation", &pet, nil).
80 | AddResponse(http.StatusBadRequest, "Invalid ID supplied", nil, nil).
81 | AddResponse(http.StatusNotFound, "Pet not found", nil, nil).
82 | SetOperationId("getPetById").
83 | SetDescription("Returns a single pet").
84 | SetSummary("Find pet by ID").
85 | SetSecurity("api_key")
86 |
87 | g.POST("/{petId}", c.CreateById).
88 | AddParamPath(0, "petId", "ID of pet that needs to be updated").
89 | AddParamForm("", "name", "Updated name of the pet", false).
90 | AddParamForm("", "status", "Updated status of the pet", false).
91 | AddResponse(http.StatusMethodNotAllowed, "Invalid input", nil, nil).
92 | SetRequestContentType("application/x-www-form-urlencoded").
93 | SetOperationId("updatePetWithForm").
94 | SetSummary("Updates a pet in the store with form data").
95 | SetSecurityWithScope(security)
96 |
97 | g.DELETE("/{petId}", c.DeleteById).
98 | AddParamHeader("", "api_key", "", false).
99 | AddParamPath(int64(0), "petId", "Pet id to delete").
100 | AddResponse(http.StatusBadRequest, "Invalid ID supplied", nil, nil).
101 | AddResponse(http.StatusNotFound, "Pet not found", nil, nil).
102 | SetOperationId("deletePet").
103 | SetSummary("Deletes a pet").
104 | SetSecurityWithScope(security)
105 |
106 | type ApiResponse struct {
107 | Code int32 `json:"code"`
108 | Type string `json:"type"`
109 | Message string `json:"message"`
110 | }
111 | g.POST("/{petId}/uploadImage", c.UploadImageById).
112 | AddParamPath("", "petId", "ID of pet to update").
113 | AddParamForm("", "additionalMetadata", "Additional data to pass to server", false).
114 | AddParamFile("file", "file to upload", false).
115 | AddResponse(http.StatusOK, "successful operation", &ApiResponse{}, nil).
116 | SetRequestContentType("multipart/form-data").
117 | SetResponseContentType("application/json").
118 | SetOperationId("uploadFile").
119 | SetSummary("uploads an image").
120 | SetSecurityWithScope(security)
121 | }
122 |
123 | func (PetController) Create(c echo.Context) error {
124 | return nil
125 | }
126 |
127 | func (PetController) Update(c echo.Context) error {
128 | return nil
129 | }
130 |
131 | func (PetController) FindByStatus(c echo.Context) error {
132 | return nil
133 | }
134 |
135 | func (PetController) FindByTags(c echo.Context) error {
136 | return nil
137 | }
138 |
139 | func (PetController) GetById(c echo.Context) error {
140 | return nil
141 | }
142 |
143 | func (PetController) CreateById(c echo.Context) error {
144 | return nil
145 | }
146 |
147 | func (PetController) DeleteById(c echo.Context) error {
148 | return nil
149 | }
150 |
151 | func (PetController) UploadImageById(c echo.Context) error {
152 | return nil
153 | }
154 |
--------------------------------------------------------------------------------
/nop.go:
--------------------------------------------------------------------------------
1 | package echoswagger
2 |
3 | import (
4 | "github.com/labstack/echo"
5 | )
6 |
7 | type NopRoot struct {
8 | echo *echo.Echo
9 | }
10 |
11 | var _ ApiRoot = NewNop(nil)
12 |
13 | func NewNop(e *echo.Echo) ApiRoot {
14 | return &NopRoot{echo: e}
15 | }
16 |
17 | type nopGroup struct {
18 | echoGroup *echo.Group
19 | }
20 |
21 | type nopApi struct {
22 | route *echo.Route
23 | }
24 |
25 | func (r *NopRoot) Add(method string, path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) Api {
26 | return &nopApi{route: r.echo.Add(method, path, h, m...)}
27 | }
28 |
29 | func (r *NopRoot) GET(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) Api {
30 | return &nopApi{route: r.echo.GET(path, h, m...)}
31 | }
32 |
33 | func (r *NopRoot) POST(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) Api {
34 | return &nopApi{route: r.echo.POST(path, h, m...)}
35 | }
36 |
37 | func (r *NopRoot) PUT(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) Api {
38 | return &nopApi{route: r.echo.PUT(path, h, m...)}
39 | }
40 |
41 | func (r *NopRoot) DELETE(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) Api {
42 | return &nopApi{route: r.echo.DELETE(path, h, m...)}
43 | }
44 |
45 | func (r *NopRoot) OPTIONS(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) Api {
46 | return &nopApi{route: r.echo.OPTIONS(path, h, m...)}
47 | }
48 |
49 | func (r *NopRoot) HEAD(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) Api {
50 | return &nopApi{route: r.echo.HEAD(path, h, m...)}
51 | }
52 |
53 | func (r *NopRoot) PATCH(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) Api {
54 | return &nopApi{route: r.echo.PATCH(path, h, m...)}
55 | }
56 |
57 | func (r *NopRoot) Group(_ string, prefix string, m ...echo.MiddlewareFunc) ApiGroup {
58 | return &nopGroup{echoGroup: r.echo.Group(prefix, m...)}
59 | }
60 |
61 | func (r *NopRoot) BindGroup(_ string, g *echo.Group) ApiGroup {
62 | return &nopGroup{echoGroup: g}
63 | }
64 |
65 | func (r *NopRoot) SetRequestContentType(_ ...string) ApiRoot {
66 | return r
67 | }
68 |
69 | func (r *NopRoot) SetResponseContentType(_ ...string) ApiRoot {
70 | return r
71 | }
72 |
73 | func (r *NopRoot) SetExternalDocs(_, _ string) ApiRoot {
74 | return r
75 | }
76 |
77 | func (r *NopRoot) AddSecurityBasic(_, _ string) ApiRoot {
78 | return r
79 | }
80 |
81 | func (r *NopRoot) AddSecurityAPIKey(_, _ string, _ SecurityInType) ApiRoot {
82 | return r
83 | }
84 |
85 | func (r *NopRoot) AddSecurityOAuth2(_, _ string, _ OAuth2FlowType,
86 | _, _ string, _ map[string]string) ApiRoot {
87 | return r
88 | }
89 |
90 | func (r *NopRoot) SetUI(_ UISetting) ApiRoot {
91 | return r
92 | }
93 |
94 | func (r *NopRoot) SetScheme(_ ...string) ApiRoot {
95 | return r
96 | }
97 |
98 | func (r *NopRoot) GetRaw() *Swagger {
99 | return nil
100 | }
101 |
102 | func (r *NopRoot) SetRaw(_ *Swagger) ApiRoot {
103 | return r
104 | }
105 |
106 | func (r *NopRoot) Echo() *echo.Echo {
107 | return r.echo
108 | }
109 |
110 | func (g *nopGroup) Add(method, path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) Api {
111 | return &nopApi{route: g.echoGroup.Add(method, path, h, m...)}
112 | }
113 |
114 | func (g *nopGroup) GET(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) Api {
115 | return &nopApi{route: g.echoGroup.GET(path, h, m...)}
116 | }
117 |
118 | func (g *nopGroup) POST(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) Api {
119 | return &nopApi{route: g.echoGroup.POST(path, h, m...)}
120 | }
121 |
122 | func (g *nopGroup) PUT(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) Api {
123 | return &nopApi{route: g.echoGroup.PUT(path, h, m...)}
124 | }
125 |
126 | func (g *nopGroup) DELETE(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) Api {
127 | return &nopApi{route: g.echoGroup.DELETE(path, h, m...)}
128 | }
129 |
130 | func (g *nopGroup) OPTIONS(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) Api {
131 | return &nopApi{route: g.echoGroup.OPTIONS(path, h, m...)}
132 | }
133 |
134 | func (g *nopGroup) HEAD(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) Api {
135 | return &nopApi{route: g.echoGroup.HEAD(path, h, m...)}
136 | }
137 |
138 | func (g *nopGroup) PATCH(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) Api {
139 | return &nopApi{route: g.echoGroup.PATCH(path, h, m...)}
140 | }
141 |
142 | func (g *nopGroup) SetDescription(_ string) ApiGroup {
143 | return g
144 | }
145 |
146 | func (g *nopGroup) SetExternalDocs(_, _ string) ApiGroup {
147 | return g
148 | }
149 |
150 | func (g *nopGroup) SetSecurity(_ ...string) ApiGroup {
151 | return g
152 | }
153 |
154 | func (g *nopGroup) SetSecurityWithScope(_ map[string][]string) ApiGroup {
155 | return g
156 | }
157 |
158 | func (g *nopGroup) EchoGroup() *echo.Group {
159 | return g.echoGroup
160 | }
161 |
162 | func (a *nopApi) AddParamPath(_ interface{}, _, _ string) Api {
163 | return a
164 | }
165 |
166 | func (a *nopApi) AddParamPathNested(_ interface{}) Api {
167 | return a
168 | }
169 |
170 | func (a *nopApi) AddParamQuery(_ interface{}, _, _ string, _ bool) Api {
171 | return a
172 | }
173 |
174 | func (a *nopApi) AddParamQueryNested(_ interface{}) Api {
175 | return a
176 | }
177 |
178 | func (a *nopApi) AddParamForm(_ interface{}, _, _ string, _ bool) Api {
179 | return a
180 | }
181 |
182 | func (a *nopApi) AddParamFormNested(_ interface{}) Api {
183 | return a
184 | }
185 |
186 | func (a *nopApi) AddParamHeader(_ interface{}, _, _ string, _ bool) Api {
187 | return a
188 | }
189 |
190 | func (a *nopApi) AddParamHeaderNested(_ interface{}) Api {
191 | return a
192 | }
193 |
194 | func (a *nopApi) AddParamBody(_ interface{}, _, _ string, _ bool) Api {
195 | return a
196 | }
197 |
198 | func (a *nopApi) AddParamFile(_, _ string, _ bool) Api {
199 | return a
200 | }
201 |
202 | func (a *nopApi) SetRequestContentType(_ ...string) Api {
203 | return a
204 | }
205 |
206 | func (a *nopApi) SetResponseContentType(_ ...string) Api {
207 | return a
208 | }
209 |
210 | func (a *nopApi) AddResponse(_ int, _ string, _, _ interface{}) Api {
211 | return a
212 | }
213 |
214 | func (a *nopApi) SetOperationId(_ string) Api {
215 | return a
216 | }
217 |
218 | func (a *nopApi) SetDeprecated() Api {
219 | return a
220 | }
221 |
222 | func (a *nopApi) SetDescription(_ string) Api {
223 | return a
224 | }
225 |
226 | func (a *nopApi) SetExternalDocs(_, _ string) Api {
227 | return a
228 | }
229 |
230 | func (a *nopApi) SetSummary(_ string) Api {
231 | return a
232 | }
233 |
234 | func (a *nopApi) SetSecurity(_ ...string) Api {
235 | return a
236 | }
237 |
238 | func (a *nopApi) SetSecurityWithScope(_ map[string][]string) Api {
239 | return a
240 | }
241 |
242 | func (a *nopApi) Route() *echo.Route {
243 | return a.route
244 | }
245 |
--------------------------------------------------------------------------------
/internal_test.go:
--------------------------------------------------------------------------------
1 | package echoswagger
2 |
3 | import (
4 | "testing"
5 | "time"
6 |
7 | "github.com/stretchr/testify/assert"
8 | )
9 |
10 | func TestParamTypes(t *testing.T) {
11 | var pa interface{}
12 | var pb *int64
13 | var pc map[string]string
14 | var pd [][]float64
15 | type Parent struct {
16 | Child struct {
17 | Name string
18 | }
19 | }
20 | var pe Parent
21 | tests := []struct {
22 | p interface{}
23 | panic bool
24 | name string
25 | }{
26 | {
27 | p: pa,
28 | panic: true,
29 | name: "Interface type",
30 | },
31 | {
32 | p: &pa,
33 | panic: true,
34 | name: "Interface pointer type",
35 | },
36 | {
37 | p: &pb,
38 | panic: false,
39 | name: "Int type",
40 | },
41 | {
42 | p: &pc,
43 | panic: true,
44 | name: "Map type",
45 | },
46 | {
47 | p: nil,
48 | panic: true,
49 | name: "Nil type",
50 | },
51 | {
52 | p: 0,
53 | panic: false,
54 | name: "Int type",
55 | },
56 | {
57 | p: &pd,
58 | panic: false,
59 | name: "Array float64 type",
60 | },
61 | {
62 | p: &pe,
63 | panic: true,
64 | name: "Struct type",
65 | },
66 | }
67 | for _, tt := range tests {
68 | t.Run(tt.name, func(t *testing.T) {
69 | a := prepareApi()
70 | if tt.panic {
71 | assert.Panics(t, func() {
72 | a.AddParamPath(tt.p, tt.name, "")
73 | })
74 | } else {
75 | a.AddParamPath(tt.p, tt.name, "")
76 | sapi, ok := a.(*api)
77 | assert.Equal(t, ok, true)
78 | assert.Equal(t, len(sapi.operation.Parameters), 1)
79 | assert.Equal(t, tt.name, sapi.operation.Parameters[0].Name)
80 | }
81 | })
82 | }
83 | }
84 |
85 | func TestNestedParamTypes(t *testing.T) {
86 | var pa struct {
87 | ExpiredAt time.Time
88 | }
89 | type User struct {
90 | ExpiredAt time.Time
91 | }
92 | var pb struct {
93 | User User
94 | }
95 | type Org struct {
96 | Id int64 `json:"id"`
97 | Address string `json:"address"`
98 | }
99 | var pc struct {
100 | User
101 | Org
102 | }
103 | var pd struct {
104 | User
105 | Org `json:"org"` // this tag would be ignored
106 | }
107 |
108 | tests := []struct {
109 | p interface{}
110 | panic bool
111 | name string
112 | params []string
113 | }{
114 | {
115 | p: 0,
116 | panic: true,
117 | name: "Basic type",
118 | },
119 | {
120 | p: pa,
121 | panic: false,
122 | name: "Struct type",
123 | params: []string{"ExpiredAt"},
124 | },
125 | {
126 | p: pb,
127 | panic: true,
128 | name: "Nested struct type",
129 | },
130 | {
131 | p: pc,
132 | panic: false,
133 | name: "Embedded struct type",
134 | params: []string{"ExpiredAt", "id", "address"},
135 | },
136 | {
137 | p: pd,
138 | panic: false,
139 | name: "Embedded struct type with tag",
140 | params: []string{"ExpiredAt", "id", "address"},
141 | },
142 | }
143 | for _, tt := range tests {
144 | t.Run(tt.name, func(t *testing.T) {
145 | a := prepareApi()
146 | if tt.panic {
147 | assert.Panics(t, func() {
148 | a.AddParamPathNested(tt.p)
149 | })
150 | } else {
151 | a.AddParamPathNested(tt.p)
152 | sapi, ok := a.(*api)
153 | assert.Equal(t, ok, true)
154 | assert.Equal(t, len(sapi.operation.Parameters), len(tt.params))
155 | var params []string
156 | for _, p := range sapi.operation.Parameters {
157 | params = append(params, p.Name)
158 | }
159 | assert.ElementsMatch(t, params, tt.params)
160 | }
161 | })
162 | }
163 | }
164 |
165 | func TestSchemaTypes(t *testing.T) {
166 | var pa interface{}
167 | var pb map[string]string
168 | type PT struct {
169 | Name string
170 | ExpiredAt time.Time
171 | }
172 | var pc map[PT]string
173 | var pd PT
174 | var pe map[time.Time]string
175 | var pf map[*int]string
176 | type PU struct {
177 | Any interface{}
178 | }
179 | var pg PU
180 | var ph map[string]interface{}
181 | tests := []struct {
182 | p interface{}
183 | panic bool
184 | name string
185 | }{
186 | {
187 | p: pa,
188 | panic: true,
189 | name: "Interface type",
190 | },
191 | {
192 | p: nil,
193 | panic: true,
194 | name: "Nil type",
195 | },
196 | {
197 | p: "",
198 | panic: false,
199 | name: "String type",
200 | },
201 | {
202 | p: &pb,
203 | panic: false,
204 | name: "Map type",
205 | },
206 | {
207 | p: &pc,
208 | panic: true,
209 | name: "Map struct type",
210 | },
211 | {
212 | p: pd,
213 | panic: false,
214 | name: "Struct type",
215 | },
216 | {
217 | p: &pd,
218 | panic: false,
219 | name: "Struct pointer type",
220 | },
221 | {
222 | p: &pe,
223 | panic: false,
224 | name: "Map time.Time key type",
225 | },
226 | {
227 | p: &pf,
228 | panic: false,
229 | name: "Map pointer key type",
230 | },
231 | {
232 | p: &pg,
233 | panic: false,
234 | name: "Struct interface field type",
235 | },
236 | {
237 | p: &ph,
238 | panic: false,
239 | name: "Map interface value type",
240 | },
241 | }
242 | for _, tt := range tests {
243 | t.Run(tt.name, func(t *testing.T) {
244 | a := prepareApi()
245 | if tt.panic {
246 | assert.Panics(t, func() {
247 | a.AddParamBody(tt.p, tt.name, "", true)
248 | })
249 | } else {
250 | a.AddParamBody(tt.p, tt.name, "", true)
251 | sapi, ok := a.(*api)
252 | assert.Equal(t, ok, true)
253 | assert.Equal(t, len(sapi.operation.Parameters), 1)
254 | assert.Equal(t, tt.name, sapi.operation.Parameters[0].Name)
255 | }
256 | })
257 | }
258 | }
259 |
260 | type testUser struct {
261 | Id int64
262 | Name string
263 | Pets []testPet
264 | }
265 |
266 | type testPet struct {
267 | Id int64
268 | Masters []testUser
269 | }
270 |
271 | func TestSchemaRecursiveStruct(t *testing.T) {
272 | tests := []struct {
273 | p interface{}
274 | name string
275 | }{
276 | {
277 | p: &testUser{},
278 | name: "User",
279 | },
280 | {
281 | p: &testPet{},
282 | name: "Pet",
283 | },
284 | }
285 | for _, tt := range tests {
286 | t.Run(tt.name, func(t *testing.T) {
287 | a := prepareApi()
288 | a.AddParamBody(tt.p, tt.name, "", true)
289 | sapi, ok := a.(*api)
290 | assert.Equal(t, ok, true)
291 | assert.Equal(t, len(sapi.operation.Parameters), 1)
292 | assert.Equal(t, len(*sapi.defs), 2)
293 | assert.Equal(t, tt.name, sapi.operation.Parameters[0].Name)
294 | })
295 | }
296 | }
297 |
298 | func TestSchemaNestedStruct(t *testing.T) {
299 | type User struct {
300 | ExpiredAt time.Time
301 | }
302 | type Org struct {
303 | Id int64 `json:"id"`
304 | Address string `json:"address"`
305 | }
306 | var pa struct {
307 | User `json:"user"`
308 | Org
309 | }
310 | a := prepareApi()
311 | a.AddParamBody(pa, "pa", "", true)
312 | sapi, ok := a.(*api)
313 | assert.Equal(t, ok, true)
314 | assert.Equal(t, len(sapi.operation.Parameters), 1)
315 | assert.NotNil(t, (*sapi.defs)[""])
316 | assert.NotNil(t, (*sapi.defs)[""].Schema.Properties["address"])
317 | assert.NotNil(t, (*sapi.defs)[""].Schema.Properties["id"])
318 | assert.NotNil(t, (*sapi.defs)["User"])
319 | assert.NotNil(t, (*sapi.defs)["User"].Schema.Properties["ExpiredAt"])
320 | }
321 |
--------------------------------------------------------------------------------
/README_zh-CN.md:
--------------------------------------------------------------------------------
1 | [English](./README.md) | 简体中文
2 |
3 | # Echoswagger
4 | [Echo](https://github.com/labstack/echo) 框架的 [Swagger UI](https://github.com/swagger-api/swagger-ui) 生成器
5 |
6 | [](https://github.com/pangpanglabs/echoswagger/actions/workflows/ci.yml)
7 | [](https://goreportcard.com/report/github.com/pangpanglabs/echoswagger)
8 | [](https://pkg.go.dev/github.com/pangpanglabs/echoswagger)
9 | [](https://codecov.io/gh/pangpanglabs/echoswagger)
10 |
11 | ## 特性
12 | - 不依赖任何SwaggerUI的HTML/CSS文件
13 | - 与Echo高度整合,低侵入式设计
14 | - 利用强类型语言和链式编程的优势,简单易用
15 | - 及时的垃圾回收,低内存占用
16 |
17 | ## 安装
18 | ```
19 | go get github.com/pangpanglabs/echoswagger
20 | ```
21 |
22 | ## Go modules 支持
23 | 如果你的项目已经使用Go modules,你可以:
24 | - 选择v2版本的Echoswagger搭配Echo v4版本
25 | - 选择v1版本的Echoswagger搭配Echo v3及以下版本
26 |
27 | 使用v2版本,只需要:
28 | - `go get github.com/pangpanglabs/echoswagger/v2`
29 | - 在你的项目中import `github.com/labstack/echo/v4` 和 `github.com/pangpanglabs/echoswagger/v2`
30 |
31 | 同时,v1版本将继续更新。关于Go modules的详细内容,请参考 [Go Wiki](https://github.com/golang/go/wiki/Modules)
32 |
33 | ## 示例
34 | ```go
35 | package main
36 |
37 | import (
38 | "net/http"
39 |
40 | "github.com/labstack/echo"
41 | "github.com/pangpanglabs/echoswagger"
42 | )
43 |
44 | func main() {
45 | // ApiRoot with Echo instance
46 | r := echoswagger.New(echo.New(), "/doc", nil)
47 |
48 | // Routes with parameters & responses
49 | r.POST("/", createUser).
50 | AddParamBody(User{}, "body", "User input struct", true).
51 | AddResponse(http.StatusCreated, "successful", nil, nil)
52 |
53 | // Start server
54 | r.Echo().Logger.Fatal(r.Echo().Start(":1323"))
55 | }
56 |
57 | type User struct {
58 | Name string
59 | }
60 |
61 | // Handler
62 | func createUser(c echo.Context) error {
63 | return c.JSON(http.StatusCreated, nil)
64 | }
65 |
66 | ```
67 |
68 | ## 用法
69 | #### 用`New()`创建`ApiRoot`,此方法是对`echo.New()`方法的封装
70 | ```go
71 | r := echoswagger.New(echo.New(), "/doc", nil)
72 | ```
73 | 你可以用这个`ApiRoot`来:
74 | - 设置Security定义, 请求/响应Content-Type,UI选项,Scheme等。
75 | ```go
76 | r.AddSecurityAPIKey("JWT", "JWT Token", echoswagger.SecurityInHeader).
77 | SetRequestContentType("application/x-www-form-urlencoded", "multipart/form-data").
78 | SetUI(UISetting{HideTop: true}).
79 | SetScheme("https", "http")
80 | ```
81 | - 获取`echo.Echo`实例。
82 | ```go
83 | r.Echo()
84 | ```
85 | - 在默认组中注册一个GET、POST、PUT、DELETE、OPTIONS、HEAD或PATCH路由,这些是对Echo的注册路由方法的封装。
86 | 此方法返回一个`Api`实例。
87 | ```go
88 | r.GET("/:id", handler)
89 | ```
90 | - 以及: ↓
91 |
92 | #### 用`Group()`创建`ApiGroup`,此方法是对`echo.Group()`方法的封装
93 | ```go
94 | g := r.Group("Users", "/users")
95 | ```
96 | 你可以用这个`ApiGroup`来:
97 | - 设置描述,等。
98 | ```go
99 | g.SetDescription("The desc of group")
100 | ```
101 | - 为此组中的所有路由设置Security。
102 | ```go
103 | g.SetSecurity("JWT")
104 | ```
105 | - 获取`echo.Group`实例。
106 | ```go
107 | g.EchoGroup()
108 | ```
109 | - 以及: ↓
110 |
111 | #### 在`ApiGroup`中注册一个新的路由
112 | Echoswagger支持GET、POST、PUT、DELETE、OPTIONS、HEAD或PATCH方法,这些是对Echo的注册路由方法的封装。
113 | ```go
114 | a := g.GET("/:id", handler)
115 | ```
116 | 你可以使用此`Api`实例来:
117 | - 使用以下方法添加参数:
118 | ```go
119 | AddParamPath(p interface{}, name, desc string)
120 |
121 | AddParamPathNested(p interface{})
122 |
123 | AddParamQuery(p interface{}, name, desc string, required bool)
124 |
125 | AddParamQueryNested(p interface{})
126 |
127 | AddParamForm(p interface{}, name, desc string, required bool)
128 |
129 | AddParamFormNested(p interface{})
130 |
131 | AddParamHeader(p interface{}, name, desc string, required bool)
132 |
133 | AddParamHeaderNested(p interface{})
134 |
135 | AddParamBody(p interface{}, name, desc string, required bool)
136 |
137 | AddParamFile(name, desc string, required bool)
138 | ```
139 |
140 | 后缀带有`Nested`的方法把参数`p`的字段看做多个参数,所以它必须是结构体类型的。
141 |
142 | 例:
143 | ```go
144 | type SearchInput struct {
145 | Q string `query:"q" swagger:"desc(Keywords),required"`
146 | SkipCount int `query:"skipCount"`
147 | }
148 | a.AddParamQueryNested(SearchInput{})
149 | ```
150 | 等价于:
151 | ```go
152 | a.AddParamQuery("", "q", "Keywords", true).
153 | AddParamQuery(0, "skipCount", "", false)
154 | ```
155 | - 添加响应。
156 | ```go
157 | a.AddResponse(http.StatusOK, "response desc", body{}, nil)
158 | ```
159 | - 设置Security,请求/响应的Content-Type,概要,描述,等。
160 | ```go
161 | a.SetSecurity("JWT").
162 | SetResponseContentType("application/xml").
163 | SetSummary("The summary of API").
164 | SetDescription("The desc of API")
165 | ```
166 | - 获取`echo.Route`实例。
167 | ```go
168 | a.Route()
169 | ```
170 |
171 | #### 使用`swagger`标签,你可以在`AddParam...`方法中设置更多信息
172 | 例:
173 | ```go
174 | type User struct {
175 | Age int `swagger:"min(0),max(99)"`
176 | Gender string `swagger:"enum(male|female|other),required"`
177 | Money []float64 `swagger:"default(0),readOnly"`
178 | }
179 | a.AddParamBody(&User{}, "Body", "", true)
180 | ```
181 | 此定义等价于:
182 | ```json
183 | {
184 | "definitions": {
185 | "User": {
186 | "type": "object",
187 | "properties": {
188 | "Age": {
189 | "type": "integer",
190 | "format": "int32",
191 | "minimum": 0,
192 | "maximum": 99
193 | },
194 | "Gender": {
195 | "type": "string",
196 | "enum": [
197 | "male",
198 | "female",
199 | "other"
200 | ],
201 | "format": "string"
202 | },
203 | "Money": {
204 | "type": "array",
205 | "items": {
206 | "type": "number",
207 | "default": 0,
208 | "format": "double"
209 | },
210 | "readOnly": true
211 | }
212 | },
213 | "required": [
214 | "Gender"
215 | ]
216 | }
217 | }
218 | }
219 | ```
220 |
221 | **支持的`swagger`标签:**
222 |
223 | Tag | Type | Description
224 | ---|:---:|---
225 | desc | `string` | 描述。
226 | min | `number` | -
227 | max | `number` | -
228 | minLen | `integer` | -
229 | maxLen | `integer` | -
230 | allowEmpty | `boolean` | 设置传递空值参数的功能。 这仅对`query`或`formData`参数有效,并允许你发送仅具有名称或空值的参数。默认值为“false”。
231 | required | `boolean` | 确定此参数是否必需。如果参数是`in`“path”,则此属性默认为“true”。否则,可以设置此属性,其默认值为“false”。
232 | readOnly | `boolean` | 仅与Schema`"properties"`定义相关。将属性声明为“只读”。这意味着它可以作为响应的一部分发送,但绝不能作为请求的一部分发送。标记为“readOnly”的属性为“true”,不应位于已定义模式的“required”列表中。默认值为“false”。
233 | enum | [*] | 枚举值,多个值应以“\|”分隔。
234 | default | * | 默认值,该类型与字段的类型相同。
235 |
236 | #### 如果需要在某些情况下禁用Echoswagger,请使用`NewNop`方法。这样既不会生成路由也不会生成文档
237 | e.g.
238 | ```go
239 | e := echo.New()
240 | var se echoswagger.ApiRoot
241 | if os.Getenv("env") == "production" {
242 | // Disable SwaggerEcho in production enviroment
243 | se = echoswagger.NewNop(e)
244 | } else {
245 | se = echoswagger.New(e, "doc/", nil)
246 | }
247 | ```
248 |
249 | ## 参考
250 | [OpenAPI Specification 2.0](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md)
251 |
252 | ## License
253 |
254 | [MIT](https://github.com/pangpanglabs/echoswagger/blob/master/LICENSE)
255 |
--------------------------------------------------------------------------------
/spec_test.go:
--------------------------------------------------------------------------------
1 | package echoswagger
2 |
3 | import (
4 | "encoding/json"
5 | "net/http"
6 | "net/http/httptest"
7 | "reflect"
8 | "testing"
9 |
10 | "github.com/labstack/echo"
11 | "github.com/stretchr/testify/assert"
12 | )
13 |
14 | func TestSpec(t *testing.T) {
15 | t.Run("Basic", func(t *testing.T) {
16 | r := prepareApiRoot()
17 | e := r.(*Root).echo
18 | req := httptest.NewRequest(echo.GET, "/doc/swagger.json", nil)
19 | rec := httptest.NewRecorder()
20 | c := e.NewContext(req, rec)
21 | j := `{"swagger":"2.0","info":{"title":"Project APIs","version":""},"host":"example.com","paths":{}}`
22 | if assert.NoError(t, r.(*Root).specHandler("/doc/")(c)) {
23 | assert.Equal(t, http.StatusOK, rec.Code)
24 | assert.JSONEq(t, j, rec.Body.String())
25 | }
26 | })
27 |
28 | t.Run("BasicGenerater", func(t *testing.T) {
29 | r := prepareApiRoot()
30 | e := r.(*Root).echo
31 | req := httptest.NewRequest(echo.GET, "/doc/swagger.json", nil)
32 | rec := httptest.NewRecorder()
33 | c := e.NewContext(req, rec)
34 | j := `{"swagger":"2.0","info":{"title":"Project APIs","version":""},"paths":{}}`
35 | s, err := r.(*Root).GetSpec(c, "/doc/")
36 | assert.Nil(t, err)
37 | rs, err := json.Marshal(s)
38 | assert.Nil(t, err)
39 | assert.JSONEq(t, j, string(rs))
40 | })
41 |
42 | t.Run("BasicIntegrated", func(t *testing.T) {
43 | r := prepareApiRoot()
44 | r.SetUI(UISetting{DetachSpec: false})
45 | e := r.(*Root).echo
46 | req := httptest.NewRequest(echo.GET, "/doc", nil)
47 | rec := httptest.NewRecorder()
48 | c := e.NewContext(req, rec)
49 | j := `{"swagger":"2.0","info":{"title":"Project APIs","version":""},"paths":{}}`
50 | s, err := r.(*Root).GetSpec(c, "/doc/")
51 | assert.Nil(t, err)
52 | rs, err := json.Marshal(s)
53 | assert.Nil(t, err)
54 | assert.JSONEq(t, j, string(rs))
55 | })
56 |
57 | t.Run("Methods", func(t *testing.T) {
58 | r := prepareApiRoot()
59 | var h echo.HandlerFunc
60 | r.GET("/", h)
61 | r.POST("/", h)
62 | r.PUT("/", h)
63 | r.DELETE("/", h)
64 | r.OPTIONS("/", h)
65 | r.HEAD("/", h)
66 | r.PATCH("/", h)
67 | e := r.(*Root).echo
68 | req := httptest.NewRequest(echo.GET, "/doc/swagger.json", nil)
69 | rec := httptest.NewRecorder()
70 | c := e.NewContext(req, rec)
71 | if assert.NoError(t, r.(*Root).specHandler("/doc")(c)) {
72 | assert.Equal(t, http.StatusOK, rec.Code)
73 | s := r.(*Root).spec
74 | assert.Len(t, s.Paths, 1)
75 | assert.NotNil(t, s.Paths["/"].(*Path).Get)
76 | assert.NotNil(t, s.Paths["/"].(*Path).Post)
77 | assert.NotNil(t, s.Paths["/"].(*Path).Put)
78 | assert.NotNil(t, s.Paths["/"].(*Path).Delete)
79 | assert.NotNil(t, s.Paths["/"].(*Path).Options)
80 | assert.NotNil(t, s.Paths["/"].(*Path).Head)
81 | assert.NotNil(t, s.Paths["/"].(*Path).Patch)
82 | }
83 | })
84 |
85 | t.Run("CleanUp", func(t *testing.T) {
86 | r := prepareApiRoot()
87 | e := r.(*Root).echo
88 | g := r.Group("Users", "users")
89 |
90 | var ha echo.HandlerFunc
91 | g.DELETE("/:id", ha)
92 |
93 | var hb echo.HandlerFunc
94 | r.GET("/ping", hb)
95 |
96 | req := httptest.NewRequest(echo.GET, "/doc/swagger.json", nil)
97 | rec := httptest.NewRecorder()
98 | c := e.NewContext(req, rec)
99 | j := `{"swagger":"2.0","info":{"title":"Project APIs","version":""},"host":"example.com","paths":{"/ping":{"get":{"responses":{"default":{"description":"successful operation"}}}},"/users/{id}":{"delete":{"tags":["Users"],"responses":{"default":{"description":"successful operation"}}}}},"tags":[{"name":"Users"}]}`
100 | if assert.NoError(t, r.(*Root).specHandler("/doc")(c)) {
101 | assert.Equal(t, http.StatusOK, rec.Code)
102 | assert.JSONEq(t, j, rec.Body.String())
103 | }
104 |
105 | assert.Nil(t, r.(*Root).echo)
106 | assert.Nil(t, r.(*Root).defs)
107 | assert.Len(t, r.(*Root).groups, 0)
108 | assert.Len(t, r.(*Root).apis, 0)
109 | })
110 | }
111 |
112 | func TestReferer(t *testing.T) {
113 | tests := []struct {
114 | name, referer, host, docPath, basePath string
115 | }{
116 | {
117 | referer: "http://localhost:1323/doc",
118 | host: "localhost:1323",
119 | docPath: "/doc",
120 | name: "A",
121 | basePath: "",
122 | },
123 | {
124 | referer: "http://localhost:1323/doc",
125 | host: "localhost:1323",
126 | docPath: "/doc/",
127 | name: "B",
128 | basePath: "",
129 | },
130 | {
131 | referer: "http://localhost:1323/doc/",
132 | host: "localhost:1323",
133 | docPath: "/doc",
134 | name: "C",
135 | basePath: "",
136 | },
137 | {
138 | referer: "http://localhost:1323/api/v1/doc",
139 | host: "localhost:1323",
140 | docPath: "/doc",
141 | name: "D",
142 | basePath: "/api/v1",
143 | },
144 | {
145 | referer: "1/doc",
146 | host: "127.0.0.1",
147 | docPath: "/doc",
148 | name: "E",
149 | basePath: "",
150 | },
151 | {
152 | referer: "http://user:pass@github.com",
153 | host: "github.com",
154 | docPath: "/",
155 | name: "F",
156 | basePath: "",
157 | },
158 | {
159 | referer: "https://www.github.com/v1/docs/?q=1",
160 | host: "www.github.com",
161 | docPath: "/docs/",
162 | name: "G",
163 | basePath: "/v1",
164 | },
165 | {
166 | referer: "https://www.github.com/?q=1#tag=TAG",
167 | host: "www.github.com",
168 | docPath: "",
169 | name: "H",
170 | basePath: "",
171 | },
172 | {
173 | referer: "https://www.github.com/",
174 | host: "www.github.com",
175 | docPath: "/doc",
176 | name: "I",
177 | basePath: "/",
178 | },
179 | }
180 | for _, tt := range tests {
181 | t.Run(tt.name, func(t *testing.T) {
182 | r := prepareApiRoot()
183 | e := r.(*Root).echo
184 | req := httptest.NewRequest(echo.GET, "http://127.0.0.1/doc/swagger.json", nil)
185 | req.Header.Add("referer", tt.referer)
186 | rec := httptest.NewRecorder()
187 | c := e.NewContext(req, rec)
188 | if assert.NoError(t, r.(*Root).specHandler(tt.docPath)(c)) {
189 | assert.Equal(t, http.StatusOK, rec.Code)
190 | var v struct {
191 | Host string `json:"host"`
192 | BasePath string `json:"basePath"`
193 | }
194 | err := json.Unmarshal(rec.Body.Bytes(), &v)
195 | assert.NoError(t, err)
196 | assert.Equal(t, tt.host, v.Host)
197 | assert.Equal(t, tt.basePath, v.BasePath)
198 | }
199 | })
200 | }
201 | }
202 |
203 | func TestAddDefinition(t *testing.T) {
204 | type DA struct {
205 | Name string
206 | DB struct {
207 | Name string
208 | }
209 | }
210 | var da DA
211 | r := prepareApiRoot()
212 | var h echo.HandlerFunc
213 | a := r.GET("/", h)
214 | a.AddParamBody(&da, "DA", "DA Struct", false)
215 | assert.Equal(t, len(a.(*api).operation.Parameters), 1)
216 | assert.Equal(t, "DA", a.(*api).operation.Parameters[0].Name)
217 | assert.Equal(t, "DA Struct", a.(*api).operation.Parameters[0].Description)
218 | assert.Equal(t, "body", a.(*api).operation.Parameters[0].In)
219 | assert.NotNil(t, a.(*api).operation.Parameters[0].Schema)
220 | assert.Equal(t, "#/definitions/DA", a.(*api).operation.Parameters[0].Schema.Ref)
221 |
222 | assert.NotNil(t, a.(*api).defs)
223 | assert.Equal(t, reflect.ValueOf(&da).Elem(), (*a.(*api).defs)["DA"].Value)
224 | assert.Equal(t, reflect.ValueOf(&da.DB).Elem(), (*a.(*api).defs)[""].Value)
225 |
226 | e := r.(*Root).echo
227 | req := httptest.NewRequest(echo.GET, "/doc/swagger.json", nil)
228 | rec := httptest.NewRecorder()
229 | c := e.NewContext(req, rec)
230 | if assert.NoError(t, r.(*Root).specHandler("/doc")(c)) {
231 | assert.Equal(t, http.StatusOK, rec.Code)
232 | assert.Len(t, r.(*Root).spec.Definitions, 2)
233 | }
234 | }
235 |
--------------------------------------------------------------------------------
/security_test.go:
--------------------------------------------------------------------------------
1 | package echoswagger
2 |
3 | import (
4 | "net/http/httptest"
5 | "testing"
6 |
7 | "github.com/labstack/echo"
8 | "github.com/stretchr/testify/assert"
9 | )
10 |
11 | func TestSecurity(t *testing.T) {
12 | r := New(echo.New(), "doc/", nil)
13 | scope := map[string]string{
14 | "read:users": "read users",
15 | "write:users": "modify users",
16 | }
17 | r.AddSecurityOAuth2("OAuth2", "OAuth2 Auth", OAuth2FlowAccessCode, "http://petstore.swagger.io/oauth/dialog", "", scope)
18 | r.AddSecurityOAuth2("", "OAuth2 Auth", OAuth2FlowAccessCode, "http://petstore.swagger.io/oauth/dialog", "", scope)
19 | r.AddSecurityAPIKey("JWT", "JWT Token", SecurityInQuery)
20 | r.AddSecurityAPIKey("", "JWT Token", SecurityInHeader)
21 | r.AddSecurityBasic("Basic", "Basic Auth")
22 | r.AddSecurityBasic("Basic", "Basic Auth")
23 |
24 | spec := r.(*Root).spec
25 | assert.Len(t, spec.SecurityDefinitions, 3)
26 | assert.Equal(t, spec.SecurityDefinitions, map[string]*SecurityDefinition{
27 | "JWT": {
28 | Type: "apiKey",
29 | Description: "JWT Token",
30 | Name: "JWT",
31 | In: string(SecurityInQuery),
32 | },
33 | "Basic": {
34 | Type: "basic",
35 | Description: "Basic Auth",
36 | },
37 | "OAuth2": {
38 | Type: "oauth2",
39 | Description: "OAuth2 Auth",
40 | Flow: string(OAuth2FlowAccessCode),
41 | AuthorizationURL: "http://petstore.swagger.io/oauth/dialog",
42 | TokenURL: "",
43 | Scopes: scope,
44 | },
45 | })
46 |
47 | t.Run("Or2Security", func(t *testing.T) {
48 | var h func(e echo.Context) error
49 | g := r.Group("OrGroup", "org")
50 | a := g.GET("/or", h)
51 |
52 | a.SetSecurity("JWT", "Basic")
53 | assert.Len(t, a.(*api).security, 1)
54 | assert.Len(t, a.(*api).security[0], 2)
55 | assert.Equal(t, a.(*api).security[0], map[string][]string{
56 | "JWT": {},
57 | "Basic": {},
58 | })
59 |
60 | g.SetSecurity("JWT", "Basic")
61 | assert.Len(t, g.(*group).security, 1)
62 | assert.Len(t, g.(*group).security[0], 2)
63 | assert.Equal(t, g.(*group).security[0], map[string][]string{
64 | "JWT": {},
65 | "Basic": {},
66 | })
67 | })
68 |
69 | t.Run("And2Security", func(t *testing.T) {
70 | var h func(e echo.Context) error
71 | g := r.Group("AndGroup", "andg")
72 | a := g.GET("/and", h)
73 |
74 | a.SetSecurity("JWT")
75 | a.SetSecurity("Basic")
76 | assert.Len(t, a.(*api).security, 2)
77 | assert.Len(t, a.(*api).security[0], 1)
78 | assert.Equal(t, a.(*api).security[0], map[string][]string{
79 | "JWT": {},
80 | })
81 | assert.Len(t, a.(*api).security[1], 1)
82 | assert.Equal(t, a.(*api).security[1], map[string][]string{
83 | "Basic": {},
84 | })
85 |
86 | g.SetSecurity("JWT")
87 | g.SetSecurity("Basic")
88 | assert.Len(t, g.(*group).security, 2)
89 | assert.Len(t, g.(*group).security[0], 1)
90 | assert.Equal(t, g.(*group).security[0], map[string][]string{
91 | "JWT": {},
92 | })
93 | assert.Len(t, g.(*group).security[1], 1)
94 | assert.Equal(t, g.(*group).security[1], map[string][]string{
95 | "Basic": {},
96 | })
97 | })
98 |
99 | t.Run("OAuth2Security", func(t *testing.T) {
100 | var h func(e echo.Context) error
101 | g := r.Group("OAuth2Group", "oauth2g")
102 | a := g.GET("/oauth2", h)
103 |
104 | s := map[string][]string{
105 | "OAuth2": {"write:users", "read:users"},
106 | }
107 | a.SetSecurityWithScope(s)
108 | assert.Len(t, a.(*api).security, 1)
109 | assert.Len(t, a.(*api).security[0], 1)
110 | assert.Equal(t, a.(*api).security[0], map[string][]string{
111 | "OAuth2": {"write:users", "read:users"},
112 | })
113 |
114 | g.SetSecurityWithScope(s)
115 | assert.Len(t, g.(*group).security, 1)
116 | assert.Len(t, g.(*group).security[0], 1)
117 | assert.Equal(t, g.(*group).security[0], map[string][]string{
118 | "OAuth2": {"write:users", "read:users"},
119 | })
120 | })
121 |
122 | t.Run("OAuth2SecuritySpecial", func(t *testing.T) {
123 | var h func(e echo.Context) error
124 | g := r.Group("OAuth2GroupSpecial", "oauth2sg")
125 | a := g.GET("/oauth2s", h)
126 |
127 | s1 := map[string][]string{}
128 | a.SetSecurityWithScope(s1)
129 | assert.Len(t, a.(*api).security, 0)
130 |
131 | s2 := map[string][]string{
132 | "OAuth2": {},
133 | }
134 | g.SetSecurityWithScope(s2)
135 | assert.Len(t, g.(*group).security, 1)
136 | assert.Len(t, g.(*group).security[0], 1)
137 | assert.Equal(t, g.(*group).security[0], map[string][]string{
138 | "OAuth2": {},
139 | })
140 | })
141 |
142 | t.Run("RepeatSecurity", func(t *testing.T) {
143 | var h func(e echo.Context) error
144 | g := r.Group("RepeatGroup", "repeatg")
145 | a := g.GET("/repeat", h)
146 |
147 | a.SetSecurity("JWT")
148 | assert.Len(t, a.(*api).security, 1)
149 |
150 | g.SetSecurity("JWT")
151 | assert.Len(t, g.(*group).security, 1)
152 |
153 | e := r.(*Root).echo
154 | req := httptest.NewRequest(echo.GET, "/doc/swagger.json", nil)
155 | rec := httptest.NewRecorder()
156 | c := e.NewContext(req, rec)
157 | if assert.NoError(t, r.(*Root).genSpec(c)) {
158 | o := r.(*Root).spec.Paths["/repeatg/repeat"]
159 | assert.NotNil(t, o)
160 | assert.Len(t, o.(*Path).Get.Security, 1)
161 | assert.Len(t, o.(*Path).Get.Security[0], 1)
162 | assert.Equal(t, o.(*Path).Get.Security[0], map[string][]string{
163 | "JWT": {},
164 | })
165 | }
166 | })
167 |
168 | t.Run("NotFoundSecurity", func(t *testing.T) {
169 | var h func(e echo.Context) error
170 | g := r.Group("NotFoundGroup", "nfg")
171 | a := g.GET("/notfound", h)
172 |
173 | a.SetSecurity("AuthKey")
174 | assert.Len(t, a.(*api).security, 1)
175 |
176 | e := r.(*Root).echo
177 | req := httptest.NewRequest(echo.GET, "/doc/swagger.json", nil)
178 | rec := httptest.NewRecorder()
179 | c := e.NewContext(req, rec)
180 | assert.Error(t, r.(*Root).genSpec(c))
181 | })
182 |
183 | t.Run("EmptySecurity", func(t *testing.T) {
184 | var h func(e echo.Context) error
185 | g := r.Group("EmptyGroup", "eg")
186 | a := g.GET("/empty", h)
187 |
188 | g.SetSecurity()
189 | assert.Len(t, g.(*group).security, 0)
190 |
191 | a.SetSecurity()
192 | assert.Len(t, a.(*api).security, 0)
193 | })
194 | }
195 |
196 | func TestSecurityRepeat(t *testing.T) {
197 | r := New(echo.New(), "doc/", nil)
198 | scope := map[string]string{
199 | "read:users": "read users",
200 | "write:users": "modify users",
201 | }
202 | r.AddSecurityOAuth2("OAuth2", "OAuth2 Auth", OAuth2FlowAccessCode, "http://petstore.swagger.io/oauth/dialog", "", scope)
203 | r.AddSecurityAPIKey("JWT", "JWT Token", SecurityInQuery)
204 | r.AddSecurityBasic("Basic", "Basic Auth")
205 |
206 | t.Run("RepeatSecurity", func(t *testing.T) {
207 | h := func(e echo.Context) error {
208 | return nil
209 | }
210 | a := r.GET("/repeat", h)
211 |
212 | sa := map[string][]string{
213 | "OAuth2": {"write:users", "read:users"},
214 | }
215 | sb := map[string][]string{
216 | "OAuth2": {"write:users"},
217 | }
218 | sc := map[string][]string{
219 | "OAuth2": {"write:spots"},
220 | }
221 | a.SetSecurityWithScope(sa)
222 | a.SetSecurityWithScope(sb)
223 | a.SetSecurityWithScope(sc)
224 | a.SetSecurity("JWT", "Basic")
225 | a.SetSecurity("JWT")
226 | a.SetSecurity("Basic")
227 | a.SetSecurity("JWT")
228 | assert.Len(t, a.(*api).security, 7)
229 | assert.Len(t, a.(*api).security[0], 1)
230 | assert.Len(t, a.(*api).security[1], 1)
231 | assert.Len(t, a.(*api).security[2], 1)
232 | assert.Len(t, a.(*api).security[3], 2)
233 | assert.Len(t, a.(*api).security[4], 1)
234 | assert.Len(t, a.(*api).security[5], 1)
235 | assert.Len(t, a.(*api).security[6], 1)
236 |
237 | req := httptest.NewRequest(echo.GET, "/doc/swagger.json", nil)
238 | rec := httptest.NewRecorder()
239 | c := r.(*Root).echo.NewContext(req, rec)
240 | assert.NoError(t, r.(*Root).genSpec(c))
241 | router := r.(*Root).spec.Paths["/repeat"]
242 | se := router.(*Path).Get.Security
243 |
244 | assert.Len(t, se, 6)
245 | })
246 | }
247 |
--------------------------------------------------------------------------------
/tag_test.go:
--------------------------------------------------------------------------------
1 | package echoswagger
2 |
3 | import (
4 | "encoding/xml"
5 | "net/http"
6 | "strconv"
7 | "testing"
8 |
9 | "github.com/stretchr/testify/assert"
10 | )
11 |
12 | func TestSchemaSwaggerTags(t *testing.T) {
13 | type Spot struct {
14 | Address string `swagger:"desc(Address of Spot)"`
15 | Matrix [][]bool `swagger:"default(true)"`
16 | }
17 |
18 | type User struct {
19 | Age int `swagger:"min(0),max(99)"`
20 | Gender string `swagger:"enum(male|female|other),required"`
21 | CarNos string `swagger:"minLen(5),maxLen(8)"`
22 | Spots []*Spot `swagger:"required"`
23 | Money **float64 `swagger:"default(0),readOnly"`
24 | }
25 |
26 | a := prepareApi()
27 | a.AddParamBody(&User{}, "Body", "", true)
28 | sapi := a.(*api)
29 | assert.Len(t, sapi.operation.Parameters, 1)
30 | assert.Len(t, *sapi.defs, 2)
31 |
32 | su := (*sapi.defs)["User"].Schema
33 | pu := su.Properties
34 | assert.NotNil(t, su)
35 | assert.NotNil(t, pu)
36 | assert.Len(t, su.Required, 2)
37 | assert.ElementsMatch(t, su.Required, []string{"Spots", "Gender"})
38 | assert.Equal(t, *pu["Age"].Minimum, float64(0))
39 | assert.Equal(t, *pu["Age"].Maximum, float64(99))
40 | assert.Len(t, pu["Gender"].Enum, 3)
41 | assert.ElementsMatch(t, pu["Gender"].Enum, []string{"male", "female", "other"})
42 | assert.Equal(t, *pu["CarNos"].MinLength, int(5))
43 | assert.Equal(t, *pu["CarNos"].MaxLength, int(8))
44 | assert.Equal(t, pu["Money"].DefaultValue, float64(0))
45 | assert.Equal(t, pu["Money"].ReadOnly, true)
46 |
47 | ss := (*sapi.defs)["Spot"].Schema
48 | ps := ss.Properties
49 | assert.NotNil(t, ss)
50 | assert.NotNil(t, ps)
51 | assert.Equal(t, ps["Address"].Description, "Address of Spot")
52 | assert.Equal(t, ps["Matrix"].Items.Items.DefaultValue, true)
53 | }
54 |
55 | func TestParamSwaggerTags(t *testing.T) {
56 | type SearchInput struct {
57 | Q string `query:"q" swagger:"minLen(5),maxLen(8)"`
58 | BrandIds string `query:"brandIds" swagger:"allowEmpty"`
59 | Sortby [][]string `query:"sortby" swagger:"default(id),allowEmpty"`
60 | Order []int `query:"order" swagger:"enum(0|1|n)"`
61 | SkipCount int `query:"skipCount" swagger:"min(0),max(999)"`
62 | MaxResultCount int `query:"maxResultCount" swagger:"desc(items count in one page)"`
63 | }
64 |
65 | a := prepareApi()
66 | a.AddParamQueryNested(SearchInput{})
67 | o := a.(*api).operation
68 | assert.Len(t, o.Parameters, 6)
69 | assert.Equal(t, *o.Parameters[0].MinLength, 5)
70 | assert.Equal(t, *o.Parameters[0].MaxLength, 8)
71 | assert.Equal(t, o.Parameters[1].AllowEmptyValue, true)
72 | assert.Equal(t, o.Parameters[2].AllowEmptyValue, true)
73 | assert.Equal(t, o.Parameters[2].Items.Items.Default, "id")
74 | assert.Equal(t, o.Parameters[2].Items.CollectionFormat, "multi")
75 | assert.ElementsMatch(t, o.Parameters[3].Items.Enum, []int{0, 1})
76 | assert.Equal(t, o.Parameters[3].CollectionFormat, "multi")
77 | assert.Equal(t, *o.Parameters[4].Minimum, float64(0))
78 | assert.Equal(t, *o.Parameters[4].Maximum, float64(999))
79 | assert.Equal(t, o.Parameters[5].Description, "items count in one page")
80 | }
81 |
82 | func TestHeaderSwaggerTags(t *testing.T) {
83 | type SearchInput struct {
84 | Q string `json:"q" swagger:"minLen(5),maxLen(8)"`
85 | Enable bool `json:"-"`
86 | Sortby [][]string `json:"sortby" swagger:"default(id)"`
87 | Order []int `json:"order" swagger:"enum(0|1|n)"`
88 | SkipCount int `json:"skipCount" swagger:"min(0),max(999)"`
89 | MaxResultCount int `json:"maxResultCount" swagger:"desc(items count in one page)"`
90 | }
91 |
92 | a := prepareApi()
93 | a.AddResponse(http.StatusOK, "Resp", nil, SearchInput{})
94 | o := a.(*api).operation
95 | c := strconv.Itoa(http.StatusOK)
96 | h := o.Responses[c].Headers
97 | assert.Len(t, h, 5)
98 | assert.Equal(t, *h["q"].MinLength, 5)
99 | assert.Equal(t, *h["q"].MaxLength, 8)
100 | assert.Equal(t, h["sortby"].Items.Items.Default, "id")
101 | assert.Equal(t, h["sortby"].Items.CollectionFormat, "multi")
102 | assert.ElementsMatch(t, h["order"].Items.Enum, []int{0, 1})
103 | assert.Equal(t, h["order"].CollectionFormat, "multi")
104 | assert.Equal(t, *h["skipCount"].Minimum, float64(0))
105 | assert.Equal(t, *h["skipCount"].Maximum, float64(999))
106 | assert.Equal(t, h["maxResultCount"].Description, "items count in one page")
107 | }
108 |
109 | func TestXMLTags(t *testing.T) {
110 | type Spot struct {
111 | Id int64 `xml:",attr"`
112 | Comment string `xml:",comment"`
113 | Address string `xml:"AddressDetail"`
114 | Enable bool `xml:"-"`
115 | }
116 |
117 | type User struct {
118 | X xml.Name `xml:"Users"`
119 | Spots []*Spot `xml:"Spots>Spot"`
120 | }
121 |
122 | a := prepareApi()
123 | a.AddParamBody(&User{}, "Body", "", true)
124 | sapi, ok := a.(*api)
125 | assert.Equal(t, ok, true)
126 | assert.Len(t, sapi.operation.Parameters, 1)
127 | assert.Len(t, *sapi.defs, 2)
128 |
129 | su := (*sapi.defs)["User"].Schema
130 | pu := su.Properties
131 | assert.NotNil(t, su)
132 | assert.NotNil(t, pu)
133 | assert.Equal(t, su.XML.Name, "Users")
134 | assert.NotNil(t, pu["Spots"].XML)
135 | assert.Equal(t, pu["Spots"].XML.Name, "Spots")
136 | assert.Equal(t, pu["Spots"].XML.Wrapped, true)
137 |
138 | ss := (*sapi.defs)["Spot"].Schema
139 | ps := ss.Properties
140 | assert.NotNil(t, ss)
141 | assert.NotNil(t, ps)
142 | assert.Equal(t, ss.XML.Name, "Spot")
143 | assert.Equal(t, ps["Id"].XML.Attribute, "Id")
144 | assert.Nil(t, ps["Comment"].XML)
145 | assert.Equal(t, ps["Address"].XML.Name, "AddressDetail")
146 | assert.Nil(t, ps["Enable"].XML)
147 | }
148 |
149 | func TestEnumInSchema(t *testing.T) {
150 | type User struct {
151 | Id int64 `swagger:"enum(0|-1|200000|9.9)"`
152 | Age int `swagger:"enum(0|-1|200000|9.9)"`
153 | Status string `swagger:"enum(normal|stop)"`
154 | Amount float64 `swagger:"enum(0|-0.1|ok|200.555)"`
155 | Grade float32 `swagger:"enum(0|-0.5|ok|200.5)"`
156 | Deleted bool `swagger:"enum(t|F),default(True)"`
157 | }
158 |
159 | a := prepareApi()
160 | a.AddParamBody(&User{}, "Body", "", true)
161 | sapi, ok := a.(*api)
162 | assert.Equal(t, ok, true)
163 | assert.Len(t, sapi.operation.Parameters, 1)
164 | assert.Len(t, *sapi.defs, 1)
165 |
166 | s := (*sapi.defs)["User"].Schema
167 | assert.NotNil(t, s)
168 |
169 | p := s.Properties
170 |
171 | assert.Len(t, p["Id"].Enum, 3)
172 | assert.ElementsMatch(t, p["Id"].Enum, []interface{}{int64(0), int64(-1), int64(200000)})
173 |
174 | assert.Len(t, p["Age"].Enum, 3)
175 | assert.ElementsMatch(t, p["Age"].Enum, []interface{}{0, -1, 200000})
176 |
177 | assert.Len(t, p["Status"].Enum, 2)
178 | assert.ElementsMatch(t, p["Status"].Enum, []interface{}{"normal", "stop"})
179 |
180 | assert.Len(t, p["Amount"].Enum, 3)
181 | assert.ElementsMatch(t, p["Amount"].Enum, []interface{}{float64(0), float64(-0.1), float64(200.555)})
182 |
183 | assert.Len(t, p["Grade"].Enum, 3)
184 | assert.ElementsMatch(t, p["Grade"].Enum, []interface{}{float32(0), float32(-0.5), float32(200.5)})
185 |
186 | assert.Len(t, p["Deleted"].Enum, 2)
187 | assert.ElementsMatch(t, p["Deleted"].Enum, []interface{}{true, false})
188 | assert.Equal(t, p["Deleted"].DefaultValue, true)
189 | }
190 |
191 | func TestExampleInSchema(t *testing.T) {
192 | u := struct {
193 | Id int64
194 | Age int
195 | Status string
196 | Amount float64
197 | Grade float32
198 | Deleted bool
199 | }{
200 | Id: 10000000001,
201 | Age: 18,
202 | Status: "normal",
203 | Amount: 195.50,
204 | Grade: 5.5,
205 | Deleted: true,
206 | }
207 |
208 | a := prepareApi()
209 | a.AddParamBody(u, "Body", "", true)
210 | sapi, ok := a.(*api)
211 | assert.Equal(t, ok, true)
212 | assert.Len(t, sapi.operation.Parameters, 1)
213 | assert.Len(t, *sapi.defs, 1)
214 |
215 | s := (*sapi.defs)[""].Schema
216 | assert.NotNil(t, s)
217 |
218 | p := s.Properties
219 |
220 | assert.Equal(t, p["Id"].Example, u.Id)
221 | assert.Equal(t, p["Age"].Example, u.Age)
222 | assert.Equal(t, p["Status"].Example, u.Status)
223 | assert.Equal(t, p["Amount"].Example, u.Amount)
224 | assert.Equal(t, p["Grade"].Example, u.Grade)
225 | assert.Equal(t, p["Deleted"].Example, u.Deleted)
226 | }
227 |
--------------------------------------------------------------------------------
/tag.go:
--------------------------------------------------------------------------------
1 | package echoswagger
2 |
3 | import (
4 | "reflect"
5 | "strconv"
6 | "strings"
7 | )
8 |
9 | // getTag reports is a tag exists and it's content
10 | // search tagName in all tags when index = -1
11 | func getTag(field reflect.StructField, tagName string, index int) (bool, string) {
12 | t := field.Tag.Get(tagName)
13 | s := strings.Split(t, ",")
14 |
15 | if len(s) < index+1 {
16 | return false, ""
17 | }
18 |
19 | return true, strings.TrimSpace(s[index])
20 | }
21 |
22 | func getSwaggerTags(field reflect.StructField) map[string]string {
23 | t := field.Tag.Get("swagger")
24 | r := make(map[string]string)
25 | for _, v := range strings.Split(t, ",") {
26 | leftIndex := strings.Index(v, "(")
27 | rightIndex := strings.LastIndex(v, ")")
28 | if leftIndex > 0 && rightIndex > leftIndex {
29 | r[v[:leftIndex]] = v[leftIndex+1 : rightIndex]
30 | } else {
31 | r[v] = ""
32 | }
33 | }
34 | return r
35 | }
36 |
37 | func getFieldName(f reflect.StructField, in ParamInType) (string, bool) {
38 | var name string
39 | switch in {
40 | case ParamInQuery:
41 | name = f.Tag.Get("query")
42 | case ParamInFormData:
43 | name = f.Tag.Get("form")
44 | case ParamInBody, ParamInHeader, ParamInPath:
45 | _, name = getTag(f, "json", 0)
46 | }
47 | if name != "" {
48 | return name, true
49 | } else {
50 | return f.Name, false
51 | }
52 | }
53 |
54 | func (p *Parameter) handleSwaggerTags(field reflect.StructField, name string, in ParamInType) {
55 | tags := getSwaggerTags(field)
56 |
57 | if t, ok := tags["desc"]; ok {
58 | p.Description = t
59 | }
60 | if t, ok := tags["min"]; ok {
61 | if m, err := strconv.ParseFloat(t, 64); err == nil {
62 | p.Minimum = &m
63 | }
64 | }
65 | if t, ok := tags["max"]; ok {
66 | if m, err := strconv.ParseFloat(t, 64); err == nil {
67 | p.Maximum = &m
68 | }
69 | }
70 | if t, ok := tags["minLen"]; ok {
71 | if m, err := strconv.Atoi(t); err == nil {
72 | p.MinLength = &m
73 | }
74 | }
75 | if t, ok := tags["maxLen"]; ok {
76 | if m, err := strconv.Atoi(t); err == nil {
77 | p.MaxLength = &m
78 | }
79 | }
80 | if _, ok := tags["allowEmpty"]; ok {
81 | p.AllowEmptyValue = true
82 | }
83 | if _, ok := tags["required"]; ok || in == ParamInPath {
84 | p.Required = true
85 | }
86 |
87 | convert := converter(field.Type)
88 | if t, ok := tags["enum"]; ok {
89 | enums := strings.Split(t, "|")
90 | var es []interface{}
91 | for _, s := range enums {
92 | v, err := convert(s)
93 | if err != nil {
94 | continue
95 | }
96 | es = append(es, v)
97 | }
98 | p.Enum = es
99 | }
100 | if t, ok := tags["default"]; ok {
101 | v, err := convert(t)
102 | if err == nil {
103 | p.Default = v
104 | }
105 | }
106 |
107 | // Move part of tags in Parameter to Items
108 | if p.Type == "array" {
109 | items := p.Items.latest()
110 | items.Minimum = p.Minimum
111 | items.Maximum = p.Maximum
112 | items.MinLength = p.MinLength
113 | items.MaxLength = p.MaxLength
114 | items.Enum = p.Enum
115 | items.Default = p.Default
116 | p.Minimum = nil
117 | p.Maximum = nil
118 | p.MinLength = nil
119 | p.MaxLength = nil
120 | p.Enum = nil
121 | p.Default = nil
122 | }
123 | }
124 |
125 | func (s *JSONSchema) handleSwaggerTags(f reflect.StructField, name string) {
126 | propSchema := s.Properties[name]
127 | tags := getSwaggerTags(f)
128 |
129 | if t, ok := tags["desc"]; ok {
130 | propSchema.Description = t
131 | }
132 | if t, ok := tags["min"]; ok {
133 | if m, err := strconv.ParseFloat(t, 64); err == nil {
134 | propSchema.Minimum = &m
135 | }
136 | }
137 | if t, ok := tags["max"]; ok {
138 | if m, err := strconv.ParseFloat(t, 64); err == nil {
139 | propSchema.Maximum = &m
140 | }
141 | }
142 | if t, ok := tags["minLen"]; ok {
143 | if m, err := strconv.Atoi(t); err == nil {
144 | propSchema.MinLength = &m
145 | }
146 | }
147 | if t, ok := tags["maxLen"]; ok {
148 | if m, err := strconv.Atoi(t); err == nil {
149 | propSchema.MaxLength = &m
150 | }
151 | }
152 | if _, ok := tags["required"]; ok {
153 | s.Required = append(s.Required, name)
154 | }
155 | if _, ok := tags["readOnly"]; ok {
156 | propSchema.ReadOnly = true
157 | }
158 |
159 | convert := converter(f.Type)
160 | if t, ok := tags["enum"]; ok {
161 | enums := strings.Split(t, "|")
162 | var es []interface{}
163 | for _, s := range enums {
164 | v, err := convert(s)
165 | if err != nil {
166 | continue
167 | }
168 | es = append(es, v)
169 | }
170 | propSchema.Enum = es
171 | }
172 | if t, ok := tags["default"]; ok {
173 | v, err := convert(t)
174 | if err == nil {
175 | propSchema.DefaultValue = v
176 | }
177 | }
178 |
179 | // Move part of tags in Schema to Items
180 | if propSchema.Type == "array" {
181 | items := propSchema.Items.latest()
182 | items.Minimum = propSchema.Minimum
183 | items.Maximum = propSchema.Maximum
184 | items.MinLength = propSchema.MinLength
185 | items.MaxLength = propSchema.MaxLength
186 | items.Enum = propSchema.Enum
187 | items.DefaultValue = propSchema.DefaultValue
188 | propSchema.Minimum = nil
189 | propSchema.Maximum = nil
190 | propSchema.MinLength = nil
191 | propSchema.MaxLength = nil
192 | propSchema.Enum = nil
193 | propSchema.DefaultValue = nil
194 | }
195 | }
196 |
197 | func (h *Header) handleSwaggerTags(f reflect.StructField, name string) {
198 | tags := getSwaggerTags(f)
199 |
200 | if t, ok := tags["desc"]; ok {
201 | h.Description = t
202 | }
203 | if t, ok := tags["min"]; ok {
204 | if m, err := strconv.ParseFloat(t, 64); err == nil {
205 | h.Minimum = &m
206 | }
207 | }
208 | if t, ok := tags["max"]; ok {
209 | if m, err := strconv.ParseFloat(t, 64); err == nil {
210 | h.Maximum = &m
211 | }
212 | }
213 | if t, ok := tags["minLen"]; ok {
214 | if m, err := strconv.Atoi(t); err == nil {
215 | h.MinLength = &m
216 | }
217 | }
218 | if t, ok := tags["maxLen"]; ok {
219 | if m, err := strconv.Atoi(t); err == nil {
220 | h.MaxLength = &m
221 | }
222 | }
223 |
224 | convert := converter(f.Type)
225 | if t, ok := tags["enum"]; ok {
226 | enums := strings.Split(t, "|")
227 | var es []interface{}
228 | for _, s := range enums {
229 | v, err := convert(s)
230 | if err != nil {
231 | continue
232 | }
233 | es = append(es, v)
234 | }
235 | h.Enum = es
236 | }
237 | if t, ok := tags["default"]; ok {
238 | v, err := convert(t)
239 | if err == nil {
240 | h.Default = v
241 | }
242 | }
243 |
244 | // Move part of tags in Header to Items
245 | if h.Type == "array" {
246 | items := h.Items.latest()
247 | items.Minimum = h.Minimum
248 | items.Maximum = h.Maximum
249 | items.MinLength = h.MinLength
250 | items.MaxLength = h.MaxLength
251 | items.Enum = h.Enum
252 | items.Default = h.Default
253 | h.Minimum = nil
254 | h.Maximum = nil
255 | h.MinLength = nil
256 | h.MaxLength = nil
257 | h.Enum = nil
258 | h.Default = nil
259 | }
260 | }
261 |
262 | func (t *Items) latest() *Items {
263 | if t.Items != nil {
264 | return t.Items.latest()
265 | }
266 | return t
267 | }
268 |
269 | func (s *JSONSchema) latest() *JSONSchema {
270 | if s.Items != nil {
271 | return s.Items.latest()
272 | }
273 | return s
274 | }
275 |
276 | // Not support nested elements tag eg:"a>b>c"
277 | // Not support tags: ",chardata", ",cdata", ",comment"
278 | // Not support embedded structure with tag ",innerxml"
279 | // Only support nested elements tag in array type eg:"Name []string `xml:"names>name"`"
280 | func (s *JSONSchema) handleXMLTags(f reflect.StructField) {
281 | b, a := getTag(f, "xml", 1)
282 | if b && contains([]string{"chardata", "cdata", "comment"}, a) {
283 | return
284 | }
285 |
286 | if b, t := getTag(f, "xml", 0); b {
287 | if t == "-" || s.Ref != "" {
288 | return
289 | } else if t == "" {
290 | t = f.Name
291 | }
292 |
293 | if s.XML == nil {
294 | s.XML = &XMLSchema{}
295 | }
296 | if a == "attr" {
297 | s.XML.Attribute = t
298 | } else {
299 | s.XML.Name = t
300 | }
301 | }
302 | }
303 |
304 | func (s *JSONSchema) handleChildXMLTags(rest string, r *RawDefineDic) {
305 | if rest == "" {
306 | return
307 | }
308 |
309 | if s.Items == nil && s.Ref == "" {
310 | if s.XML == nil {
311 | s.XML = &XMLSchema{}
312 | }
313 | s.XML.Name = rest
314 | } else if s.Ref != "" {
315 | key := s.Ref[len(DefPrefix):]
316 | if sc, ok := (*r)[key]; ok && sc.Schema != nil {
317 | if sc.Schema.XML == nil {
318 | sc.Schema.XML = &XMLSchema{}
319 | }
320 | sc.Schema.XML.Name = rest
321 | }
322 | } else {
323 | if s.XML == nil {
324 | s.XML = &XMLSchema{}
325 | }
326 | s.XML.Wrapped = true
327 | i := strings.Index(rest, ">")
328 | if i <= 0 {
329 | s.XML.Name = rest
330 | } else {
331 | s.XML.Name = rest[:i]
332 | rest = rest[i+1:]
333 | s.Items.handleChildXMLTags(rest, r)
334 | }
335 | }
336 | }
337 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | English | [简体中文](./README_zh-CN.md)
2 |
3 | # Echoswagger
4 | [Swagger UI](https://github.com/swagger-api/swagger-ui) generator for [Echo](https://github.com/labstack/echo) framework
5 |
6 | [](https://github.com/pangpanglabs/echoswagger/actions/workflows/ci.yml)
7 | [](https://goreportcard.com/report/github.com/pangpanglabs/echoswagger)
8 | [](https://pkg.go.dev/github.com/pangpanglabs/echoswagger)
9 | [](https://codecov.io/gh/pangpanglabs/echoswagger)
10 |
11 | ## Feature
12 | - No SwaggerUI HTML/CSS dependency
13 | - Highly integrated with Echo, low intrusive design
14 | - Take advantage of the strong typing language and chain programming to make it easy to use
15 | - Recycle garbage in time, low memory usage
16 |
17 | ## Installation
18 | ```
19 | go get github.com/pangpanglabs/echoswagger
20 | ```
21 |
22 | ## Go modules support
23 | If your project has migrated to Go modules, you can:
24 | - Choose v2 version of Echoswagger for Echo version v4.
25 | - Choose v1 version of Echoswagger for Echo version <= v3.
26 |
27 | To use v2 version, just do:
28 | - `go get github.com/pangpanglabs/echoswagger/v2`
29 | - import `github.com/labstack/echo/v4` and `github.com/pangpanglabs/echoswagger/v2` in your project
30 |
31 | Meanwhile, v1 version will still be updated if needed. For more details of Go modules, please refer to [Go Wiki](https://github.com/golang/go/wiki/Modules).
32 |
33 | ## Example
34 | ```go
35 | package main
36 |
37 | import (
38 | "net/http"
39 |
40 | "github.com/labstack/echo"
41 | "github.com/pangpanglabs/echoswagger"
42 | )
43 |
44 | func main() {
45 | // ApiRoot with Echo instance
46 | r := echoswagger.New(echo.New(), "/doc", nil)
47 |
48 | // Routes with parameters & responses
49 | r.POST("/", createUser).
50 | AddParamBody(User{}, "body", "User input struct", true).
51 | AddResponse(http.StatusCreated, "successful", nil, nil)
52 |
53 | // Start server
54 | r.Echo().Logger.Fatal(r.Echo().Start(":1323"))
55 | }
56 |
57 | type User struct {
58 | Name string
59 | }
60 |
61 | // Handler
62 | func createUser(c echo.Context) error {
63 | return c.JSON(http.StatusCreated, nil)
64 | }
65 |
66 | ```
67 |
68 | ## Usage
69 | #### Create a `ApiRoot` with `New()`, which is a wrapper of `echo.New()`
70 | ```go
71 | r := echoswagger.New(echo.New(), "/doc", nil)
72 | ```
73 | You can use the result `ApiRoot` instance to:
74 | - Setup Security definitions, request/response Content-Types, UI options, Scheme, etc.
75 | ```go
76 | r.AddSecurityAPIKey("JWT", "JWT Token", echoswagger.SecurityInHeader).
77 | SetRequestContentType("application/x-www-form-urlencoded", "multipart/form-data").
78 | SetUI(UISetting{HideTop: true}).
79 | SetScheme("https", "http")
80 | ```
81 | - Get `echo.Echo` instance.
82 | ```go
83 | r.Echo()
84 | ```
85 | - Registers a new GET, POST, PUT, DELETE, OPTIONS, HEAD or PATCH route in default group, these are wrappers of Echo's create route methods.
86 | It returns a new `Api` instance.
87 | ```go
88 | r.GET("/:id", handler)
89 | ```
90 | - And: ↓
91 |
92 | #### Create a `ApiGroup` with `Group()`, which is a wrapper of `echo.Group()`
93 | ```go
94 | g := r.Group("Users", "/users")
95 | ```
96 | You can use the result `ApiGroup` instance to:
97 | - Set description, etc.
98 | ```go
99 | g.SetDescription("The desc of group")
100 | ```
101 | - Set security for all routes in this group.
102 | ```go
103 | g.SetSecurity("JWT")
104 | ```
105 | - Get `echo.Group` instance.
106 | ```go
107 | g.EchoGroup()
108 | ```
109 | - And: ↓
110 |
111 | #### Registers a new route in `ApiGroup`
112 | GET, POST, PUT, DELETE, OPTIONS, HEAD or PATCH methods are supported by Echoswagger, these are wrappers of Echo's create route methods.
113 | ```go
114 | a := g.GET("/:id", handler)
115 | ```
116 | You can use the result `Api` instance to:
117 | - Add parameter with these methods:
118 | ```go
119 | AddParamPath(p interface{}, name, desc string)
120 |
121 | AddParamPathNested(p interface{})
122 |
123 | AddParamQuery(p interface{}, name, desc string, required bool)
124 |
125 | AddParamQueryNested(p interface{})
126 |
127 | AddParamForm(p interface{}, name, desc string, required bool)
128 |
129 | AddParamFormNested(p interface{})
130 |
131 | AddParamHeader(p interface{}, name, desc string, required bool)
132 |
133 | AddParamHeaderNested(p interface{})
134 |
135 | AddParamBody(p interface{}, name, desc string, required bool)
136 |
137 | AddParamFile(name, desc string, required bool)
138 | ```
139 |
140 | The methods which name's suffix are `Nested` means these methods treat parameter `p` 's fields as paramters, so it must be a struct type.
141 |
142 | e.g.
143 | ```go
144 | type SearchInput struct {
145 | Q string `query:"q" swagger:"desc(Keywords),required"`
146 | SkipCount int `query:"skipCount"`
147 | }
148 | a.AddParamQueryNested(SearchInput{})
149 | ```
150 | Is equivalent to:
151 | ```go
152 | a.AddParamQuery("", "q", "Keywords", true).
153 | AddParamQuery(0, "skipCount", "", false)
154 | ```
155 | - Add responses.
156 | ```go
157 | a.AddResponse(http.StatusOK, "response desc", body{}, nil)
158 | ```
159 | - Set Security, request/response Content-Types, summary, description, etc.
160 | ```go
161 | a.SetSecurity("JWT").
162 | SetResponseContentType("application/xml").
163 | SetSummary("The summary of API").
164 | SetDescription("The desc of API")
165 | ```
166 | - Get `echo.Route` instance.
167 | ```go
168 | a.Route()
169 | ```
170 |
171 | #### With `swagger` tag, you can set more info with `AddParam...` methods.
172 | e.g.
173 | ```go
174 | type User struct {
175 | Age int `swagger:"min(0),max(99)"`
176 | Gender string `swagger:"enum(male|female|other),required"`
177 | Money []float64 `swagger:"default(0),readOnly"`
178 | }
179 | a.AddParamBody(&User{}, "Body", "", true)
180 | ```
181 | The definition is equivalent to:
182 | ```json
183 | {
184 | "definitions": {
185 | "User": {
186 | "type": "object",
187 | "properties": {
188 | "Age": {
189 | "type": "integer",
190 | "format": "int32",
191 | "minimum": 0,
192 | "maximum": 99
193 | },
194 | "Gender": {
195 | "type": "string",
196 | "enum": [
197 | "male",
198 | "female",
199 | "other"
200 | ],
201 | "format": "string"
202 | },
203 | "Money": {
204 | "type": "array",
205 | "items": {
206 | "type": "number",
207 | "default": 0,
208 | "format": "double"
209 | },
210 | "readOnly": true
211 | }
212 | },
213 | "required": [
214 | "Gender"
215 | ]
216 | }
217 | }
218 | }
219 | ```
220 |
221 | **Supported `swagger` tags:**
222 |
223 | Tag | Type | Description
224 | ---|:---:|---
225 | desc | `string` | Description.
226 | min | `number` | -
227 | max | `number` | -
228 | minLen | `integer` | -
229 | maxLen | `integer` | -
230 | allowEmpty | `boolean` | Sets the ability to pass empty-valued parameters. This is valid only for either `query` or `formData` parameters and allows you to send a parameter with a name only or an empty value. Default value is `false`.
231 | required | `boolean` | Determines whether this parameter is mandatory. If the parameter is `in` "path", this property is `true` without setting. Otherwise, the property MAY be included and its default value is `false`.
232 | readOnly | `boolean` | Relevant only for Schema `"properties"` definitions. Declares the property as "read only". This means that it MAY be sent as part of a response but MUST NOT be sent as part of the request. Properties marked as `readOnly` being `true` SHOULD NOT be in the `required` list of the defined schema. Default value is `false`.
233 | enum | [*] | Enumerate value, multiple values should be separated by "\|"
234 | default | * | Default value, which type is same as the field's type.
235 |
236 | #### If you want to disable Echoswagger in some situation, please use `NewNop` method. It would neither create router nor generate any doc.
237 | e.g.
238 | ```go
239 | e := echo.New()
240 | var se echoswagger.ApiRoot
241 | if os.Getenv("env") == "production" {
242 | // Disable SwaggerEcho in production enviroment
243 | se = echoswagger.NewNop(e)
244 | } else {
245 | se = echoswagger.New(e, "doc/", nil)
246 | }
247 | ```
248 |
249 | ## Reference
250 | [OpenAPI Specification 2.0](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md)
251 |
252 | ## License
253 |
254 | [MIT](https://github.com/pangpanglabs/echoswagger/blob/master/LICENSE)
255 |
--------------------------------------------------------------------------------
/wrapper_test.go:
--------------------------------------------------------------------------------
1 | package echoswagger
2 |
3 | import (
4 | "net/http"
5 | "net/http/httptest"
6 | "strconv"
7 | "testing"
8 | "time"
9 |
10 | "github.com/labstack/echo"
11 | "github.com/stretchr/testify/assert"
12 | )
13 |
14 | func prepareApiRoot() ApiRoot {
15 | r := New(echo.New(), "doc/", nil)
16 | r.SetUI(UISetting{DetachSpec: true})
17 | return r
18 | }
19 |
20 | func prepareApiGroup() ApiGroup {
21 | r := prepareApiRoot()
22 | return r.Group("G", "/g")
23 | }
24 |
25 | func prepareApi() Api {
26 | g := prepareApiGroup()
27 | var h func(e echo.Context) error
28 | return g.POST("", h)
29 | }
30 |
31 | func TestNew(t *testing.T) {
32 | tests := []struct {
33 | echo *echo.Echo
34 | docPath string
35 | info *Info
36 | expectPaths []string
37 | panic bool
38 | name string
39 | }{
40 | {
41 | echo: echo.New(),
42 | docPath: "doc/",
43 | info: nil,
44 | expectPaths: []string{"/doc/", "/doc/swagger.json"},
45 | panic: false,
46 | name: "Normal",
47 | },
48 | {
49 | echo: echo.New(),
50 | docPath: "doc",
51 | info: &Info{
52 | Title: "Test project",
53 | Contact: &Contact{
54 | URL: "https://github.com/pangpanglabs/echoswagger",
55 | },
56 | },
57 | expectPaths: []string{"/doc", "/doc/swagger.json"},
58 | panic: false,
59 | name: "Path slash suffix",
60 | },
61 | {
62 | echo: nil,
63 | panic: true,
64 | name: "Panic",
65 | },
66 | }
67 | for _, tt := range tests {
68 | t.Run(tt.name, func(t *testing.T) {
69 | if tt.panic {
70 | assert.Panics(t, func() {
71 | New(tt.echo, tt.docPath, tt.info)
72 | })
73 | } else {
74 | apiRoot := New(tt.echo, tt.docPath, tt.info)
75 | assert.NotNil(t, apiRoot.(*Root))
76 |
77 | r := apiRoot.(*Root)
78 | assert.NotNil(t, r.spec)
79 |
80 | if tt.info == nil {
81 | assert.Equal(t, r.spec.Info.Title, "Project APIs")
82 | } else {
83 | assert.Equal(t, r.spec.Info, tt.info)
84 | }
85 |
86 | assert.NotNil(t, r.echo)
87 | assert.Len(t, r.echo.Routes(), 2)
88 | res := r.echo.Routes()
89 | paths := []string{res[0].Path, res[1].Path}
90 | assert.ElementsMatch(t, paths, tt.expectPaths)
91 | }
92 | })
93 | }
94 | }
95 |
96 | func TestPath(t *testing.T) {
97 | tests := []struct {
98 | docInput string
99 | docOutput, specOutput string
100 | name string
101 | }{
102 | {
103 | docInput: "doc/",
104 | docOutput: "/doc/",
105 | specOutput: "/doc/swagger.json",
106 | name: "A",
107 | }, {
108 | docInput: "",
109 | docOutput: "/",
110 | specOutput: "/swagger.json",
111 | name: "B",
112 | }, {
113 | docInput: "/doc",
114 | docOutput: "/doc",
115 | specOutput: "/doc/swagger.json",
116 | name: "C",
117 | },
118 | }
119 | for _, tt := range tests {
120 | t.Run(tt.name, func(t *testing.T) {
121 | apiRoot := New(echo.New(), tt.docInput, nil)
122 | r := apiRoot.(*Root)
123 | assert.NotNil(t, r.echo)
124 | assert.Len(t, r.echo.Routes(), 2)
125 | res := r.echo.Routes()
126 | paths := []string{res[0].Path, res[1].Path}
127 | assert.ElementsMatch(t, paths, []string{tt.docOutput, tt.specOutput})
128 | })
129 | }
130 | }
131 |
132 | func TestGroup(t *testing.T) {
133 | r := prepareApiRoot()
134 | t.Run("Normal", func(t *testing.T) {
135 | g := r.Group("Users", "users")
136 | assert.Equal(t, g.(*group).defs, r.(*Root).defs)
137 | })
138 |
139 | t.Run("Invalid name", func(t *testing.T) {
140 | assert.Panics(t, func() {
141 | r.Group("", "")
142 | })
143 | })
144 |
145 | t.Run("Repeat name", func(t *testing.T) {
146 | ga := r.Group("Users", "users")
147 | assert.Equal(t, ga.(*group).tag.Name, "Users")
148 |
149 | gb := r.Group("Users", "users")
150 | assert.Equal(t, gb.(*group).tag.Name, "Users")
151 | })
152 | }
153 |
154 | func TestBindGroup(t *testing.T) {
155 | r := prepareApiRoot()
156 | e := r.Echo()
157 | apiGroup := e.Group("/api")
158 |
159 | var h echo.HandlerFunc
160 | t.Run("Include", func(t *testing.T) {
161 | v1Group := apiGroup.Group("/v1")
162 | g := r.BindGroup("APIv1", v1Group)
163 | assert.Equal(t, g.(*group).tag.Name, "APIv1")
164 |
165 | g.GET("/in", h)
166 | assert.Len(t, g.(*group).apis, 1)
167 | assert.Equal(t, g.(*group).apis[0].route.Path, "/api/v1/in")
168 | })
169 |
170 | t.Run("Exclude", func(t *testing.T) {
171 | v2Group := apiGroup.Group("/v2")
172 | g := r.BindGroup("APIv2", v2Group)
173 | assert.Equal(t, g.(*group).tag.Name, "APIv2")
174 |
175 | v2Group.GET("/ex", h)
176 | assert.Len(t, g.(*group).apis, 0)
177 | })
178 | }
179 |
180 | func TestRouters(t *testing.T) {
181 | r := prepareApiRoot()
182 | var h echo.HandlerFunc
183 | r.GET("/:id", h)
184 | r.POST("/:id", h)
185 | r.PUT("/:id", h)
186 | r.DELETE("/:id", h)
187 | r.OPTIONS("/:id", h)
188 | r.HEAD("/:id", h)
189 | r.PATCH("/:id", h)
190 | assert.Len(t, r.(*Root).apis, 7)
191 |
192 | g := prepareApiGroup()
193 | g.GET("/:id", h)
194 | g.POST("/:id", h)
195 | g.PUT("/:id", h)
196 | g.DELETE("/:id", h)
197 | g.OPTIONS("/:id", h)
198 | g.HEAD("/:id", h)
199 | g.PATCH("/:id", h)
200 | assert.Len(t, g.(*group).apis, 7)
201 | }
202 |
203 | func TestAddParam(t *testing.T) {
204 | name := "name"
205 | desc := "Param desc"
206 | type nested struct {
207 | Name string `json:"name" form:"name" query:"name"`
208 | Enable bool `json:"-" form:"-" query:"-"`
209 | }
210 |
211 | t.Run("File", func(t *testing.T) {
212 | a := prepareApi()
213 | a.AddParamFile(name, desc, true)
214 | assert.Len(t, a.(*api).operation.Parameters, 1)
215 | assert.Equal(t, a.(*api).operation.Parameters[0].Name, name)
216 | assert.Equal(t, a.(*api).operation.Parameters[0].In, string(ParamInFormData))
217 | assert.Equal(t, a.(*api).operation.Parameters[0].Description, desc)
218 | assert.Equal(t, a.(*api).operation.Parameters[0].Required, true)
219 | assert.Equal(t, a.(*api).operation.Parameters[0].Type, "file")
220 | })
221 |
222 | t.Run("Path", func(t *testing.T) {
223 | a := prepareApi()
224 | a.AddParamPath(time.Now(), name, desc)
225 | assert.Len(t, a.(*api).operation.Parameters, 1)
226 | assert.Equal(t, a.(*api).operation.Parameters[0].Name, name)
227 | assert.Equal(t, a.(*api).operation.Parameters[0].In, string(ParamInPath))
228 | assert.Equal(t, a.(*api).operation.Parameters[0].Description, desc)
229 | assert.Equal(t, a.(*api).operation.Parameters[0].Required, true)
230 | assert.Equal(t, a.(*api).operation.Parameters[0].Type, "string")
231 |
232 | a.AddParamPathNested(&nested{})
233 | assert.Len(t, a.(*api).operation.Parameters, 2)
234 | assert.Equal(t, a.(*api).operation.Parameters[1].Name, "name_")
235 | assert.Equal(t, a.(*api).operation.Parameters[1].In, string(ParamInPath))
236 | assert.Equal(t, a.(*api).operation.Parameters[1].Type, "string")
237 | })
238 |
239 | t.Run("Query", func(t *testing.T) {
240 | a := prepareApi()
241 | a.AddParamQuery(time.Now(), name, desc, true)
242 | assert.Len(t, a.(*api).operation.Parameters, 1)
243 | assert.Equal(t, a.(*api).operation.Parameters[0].Name, name)
244 | assert.Equal(t, a.(*api).operation.Parameters[0].In, string(ParamInQuery))
245 | assert.Equal(t, a.(*api).operation.Parameters[0].Description, desc)
246 | assert.Equal(t, a.(*api).operation.Parameters[0].Required, true)
247 | assert.Equal(t, a.(*api).operation.Parameters[0].Type, "string")
248 |
249 | a.AddParamQueryNested(&nested{})
250 | assert.Len(t, a.(*api).operation.Parameters, 2)
251 | assert.Equal(t, a.(*api).operation.Parameters[1].Name, "name_")
252 | assert.Equal(t, a.(*api).operation.Parameters[1].In, string(ParamInQuery))
253 | assert.Equal(t, a.(*api).operation.Parameters[1].Type, "string")
254 | })
255 |
256 | t.Run("FormData", func(t *testing.T) {
257 | a := prepareApi()
258 | a.AddParamForm(time.Now(), name, desc, true)
259 | assert.Len(t, a.(*api).operation.Parameters, 1)
260 | assert.Equal(t, a.(*api).operation.Parameters[0].Name, name)
261 | assert.Equal(t, a.(*api).operation.Parameters[0].In, string(ParamInFormData))
262 | assert.Equal(t, a.(*api).operation.Parameters[0].Description, desc)
263 | assert.Equal(t, a.(*api).operation.Parameters[0].Required, true)
264 | assert.Equal(t, a.(*api).operation.Parameters[0].Type, "string")
265 |
266 | a.AddParamFormNested(&nested{})
267 | assert.Len(t, a.(*api).operation.Parameters, 2)
268 | assert.Equal(t, a.(*api).operation.Parameters[1].Name, "name_")
269 | assert.Equal(t, a.(*api).operation.Parameters[1].In, string(ParamInFormData))
270 | assert.Equal(t, a.(*api).operation.Parameters[1].Type, "string")
271 | })
272 |
273 | t.Run("Header", func(t *testing.T) {
274 | a := prepareApi()
275 | a.AddParamHeader(time.Now(), name, desc, true)
276 | assert.Len(t, a.(*api).operation.Parameters, 1)
277 | assert.Equal(t, a.(*api).operation.Parameters[0].Name, name)
278 | assert.Equal(t, a.(*api).operation.Parameters[0].In, string(ParamInHeader))
279 | assert.Equal(t, a.(*api).operation.Parameters[0].Description, desc)
280 | assert.Equal(t, a.(*api).operation.Parameters[0].Required, true)
281 | assert.Equal(t, a.(*api).operation.Parameters[0].Type, "string")
282 |
283 | a.AddParamHeaderNested(&nested{})
284 | assert.Len(t, a.(*api).operation.Parameters, 2)
285 | assert.Equal(t, a.(*api).operation.Parameters[1].Name, "name_")
286 | assert.Equal(t, a.(*api).operation.Parameters[1].In, string(ParamInHeader))
287 | assert.Equal(t, a.(*api).operation.Parameters[1].Type, "string")
288 | })
289 | }
290 |
291 | func TestAddSchema(t *testing.T) {
292 | type body struct {
293 | Name string `json:"name"`
294 | Enable bool `json:"-"`
295 | }
296 |
297 | t.Run("Multiple", func(t *testing.T) {
298 | a := prepareApi()
299 | a.AddParamBody(&body{}, "body", "body desc", true)
300 | assert.Len(t, a.(*api).operation.Parameters, 1)
301 | assert.Equal(t, a.(*api).operation.Parameters[0].Name, "body")
302 | assert.Equal(t, a.(*api).operation.Parameters[0].In, string(ParamInBody))
303 | assert.Equal(t, a.(*api).operation.Parameters[0].Description, "body desc")
304 | assert.Equal(t, a.(*api).operation.Parameters[0].Required, true)
305 |
306 | assert.Panics(t, func() {
307 | a.AddParamBody(body{}, "body", "body desc", true)
308 | })
309 | })
310 |
311 | t.Run("GetKey", func(t *testing.T) {
312 | a := prepareApi()
313 | a.AddParamBody(&body{}, "body", "body desc", true)
314 | assert.Len(t, a.(*api).operation.Parameters, 1)
315 | assert.Equal(t, a.(*api).operation.Parameters[0].Name, "body")
316 | assert.Equal(t, a.(*api).operation.Parameters[0].In, string(ParamInBody))
317 | assert.Equal(t, a.(*api).operation.Parameters[0].Description, "body desc")
318 | assert.Equal(t, a.(*api).operation.Parameters[0].Required, true)
319 |
320 | a.AddResponse(http.StatusOK, "response desc", body{}, nil)
321 | ca := strconv.Itoa(http.StatusOK)
322 | assert.Len(t, a.(*api).operation.Responses, 1)
323 | assert.Equal(t, a.(*api).operation.Responses[ca].Description, "response desc")
324 |
325 | assert.NotNil(t, a.(*api).defs)
326 | da := a.(*api).defs
327 | assert.Len(t, (*da), 1)
328 | assert.NotNil(t, (*da)["body"])
329 |
330 | a.AddResponse(http.StatusBadRequest, "response desc", body{Name: "name"}, nil)
331 | cb := strconv.Itoa(http.StatusBadRequest)
332 | assert.Len(t, a.(*api).operation.Responses, 2)
333 | assert.Equal(t, a.(*api).operation.Responses[cb].Description, "response desc")
334 |
335 | assert.NotNil(t, a.(*api).defs)
336 | db := a.(*api).defs
337 | assert.Len(t, (*db), 2)
338 | assert.NotNil(t, (*db)["body"])
339 | })
340 | }
341 |
342 | func TestAddResponse(t *testing.T) {
343 | a := prepareApi()
344 | a.AddResponse(http.StatusOK, "successful", nil, nil)
345 | var f = func() {}
346 | assert.Panics(t, func() {
347 | a.AddResponse(http.StatusBadRequest, "bad request", f, nil)
348 | })
349 | assert.Panics(t, func() {
350 | a.AddResponse(http.StatusBadRequest, "bad request", nil, time.Now())
351 | })
352 | }
353 |
354 | func TestUI(t *testing.T) {
355 | t.Run("DefaultCDN", func(t *testing.T) {
356 | r := New(echo.New(), "doc/", nil)
357 | se := r.(*Root)
358 | req := httptest.NewRequest(echo.GET, "/doc/", nil)
359 | rec := httptest.NewRecorder()
360 | c := se.echo.NewContext(req, rec)
361 | h := se.docHandler("doc/")
362 |
363 | if assert.NoError(t, h(c)) {
364 | assert.Equal(t, http.StatusOK, rec.Code)
365 | assert.Contains(t, rec.Body.String(), DefaultCDN)
366 | }
367 | })
368 |
369 | t.Run("SetUI", func(t *testing.T) {
370 | r := New(echo.New(), "doc/", nil)
371 | se := r.(*Root)
372 | req := httptest.NewRequest(echo.GET, "/doc/", nil)
373 | rec := httptest.NewRecorder()
374 | c := se.echo.NewContext(req, rec)
375 | h := se.docHandler("doc/")
376 |
377 | cdn := "https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/3.18.0"
378 | r.SetUI(UISetting{
379 | HideTop: true,
380 | CDN: cdn,
381 | })
382 |
383 | if assert.NoError(t, h(c)) {
384 | assert.Equal(t, http.StatusOK, rec.Code)
385 | assert.Contains(t, rec.Body.String(), cdn)
386 | assert.NotContains(t, rec.Body.String(), `var specStr = ""`)
387 | assert.Contains(t, rec.Body.String(), "#swagger-ui>.swagger-container>.topbar")
388 | }
389 |
390 | r.SetUI(UISetting{
391 | DetachSpec: true,
392 | })
393 |
394 | if assert.NoError(t, h(c)) {
395 | assert.Equal(t, http.StatusOK, rec.Code)
396 | assert.Contains(t, rec.Body.String(), `var specStr = ""`)
397 | assert.Contains(t, rec.Body.String(), "#swagger-ui>.swagger-container>.topbar")
398 | }
399 | })
400 | }
401 |
402 | func TestScheme(t *testing.T) {
403 | r := prepareApiRoot()
404 | schemes := []string{"http", "https"}
405 | r.SetScheme(schemes...)
406 | assert.ElementsMatch(t, r.(*Root).spec.Schemes, schemes)
407 |
408 | assert.Panics(t, func() {
409 | r.SetScheme("grpc")
410 | })
411 | }
412 |
413 | func TestRaw(t *testing.T) {
414 | r := prepareApiRoot()
415 | s := r.GetRaw()
416 | assert.NotNil(t, s)
417 | assert.NotNil(t, s.Info)
418 | assert.Equal(t, s.Info.Version, "")
419 |
420 | s.Info.Version = "1.0"
421 | r.SetRaw(s)
422 | assert.Equal(t, s.Info.Version, "1.0")
423 | }
424 |
425 | func TestContentType(t *testing.T) {
426 | c := []string{"application/x-www-form-urlencoded", "multipart/form-data"}
427 | p := []string{"application/vnd.github.v3+json", "application/vnd.github.v3.raw+json", "application/vnd.github.v3.text+json"}
428 |
429 | t.Run("In Root", func(t *testing.T) {
430 | r := prepareApiRoot()
431 | r.SetRequestContentType(c...)
432 | r.SetResponseContentType(p...)
433 | assert.NotNil(t, r.(*Root))
434 | assert.NotNil(t, r.(*Root).spec)
435 | assert.Len(t, r.(*Root).spec.Consumes, 2)
436 | assert.ElementsMatch(t, r.(*Root).spec.Consumes, c)
437 | assert.Len(t, r.(*Root).spec.Produces, 3)
438 | assert.ElementsMatch(t, r.(*Root).spec.Produces, p)
439 | })
440 |
441 | t.Run("In Api", func(t *testing.T) {
442 | a := prepareApi()
443 | a.SetRequestContentType(c...)
444 | a.SetResponseContentType(p...)
445 | assert.NotNil(t, a.(*api))
446 | assert.Len(t, a.(*api).operation.Consumes, 2)
447 | assert.ElementsMatch(t, a.(*api).operation.Consumes, c)
448 | assert.Len(t, a.(*api).operation.Produces, 3)
449 | assert.ElementsMatch(t, a.(*api).operation.Produces, p)
450 | })
451 | }
452 |
453 | func TestOperationId(t *testing.T) {
454 | id := "TestOperation"
455 |
456 | a := prepareApi()
457 | a.SetOperationId(id)
458 | assert.Equal(t, a.(*api).operation.OperationID, id)
459 | }
460 |
461 | func TestDeprecated(t *testing.T) {
462 | a := prepareApi()
463 | a.SetDeprecated()
464 | assert.Equal(t, a.(*api).operation.Deprecated, true)
465 | }
466 |
467 | func TestDescription(t *testing.T) {
468 | d := "Test desc"
469 |
470 | g := prepareApiGroup()
471 | g.SetDescription(d)
472 | assert.Equal(t, g.(*group).tag.Description, d)
473 |
474 | a := prepareApi()
475 | a.SetDescription(d)
476 | assert.Equal(t, a.(*api).operation.Description, d)
477 | }
478 |
479 | func TestExternalDocs(t *testing.T) {
480 | e := ExternalDocs{
481 | Description: "Test desc",
482 | URL: "http://127.0.0.1/",
483 | }
484 |
485 | r := prepareApiRoot()
486 | r.SetExternalDocs(e.Description, e.URL)
487 | assert.Equal(t, r.(*Root).spec.ExternalDocs, &e)
488 |
489 | g := prepareApiGroup()
490 | g.SetExternalDocs(e.Description, e.URL)
491 | assert.Equal(t, g.(*group).tag.ExternalDocs, &e)
492 |
493 | a := prepareApi()
494 | a.SetExternalDocs(e.Description, e.URL)
495 | assert.Equal(t, a.(*api).operation.ExternalDocs, &e)
496 | }
497 |
498 | func TestSummary(t *testing.T) {
499 | s := "Test summary"
500 |
501 | a := prepareApi()
502 | a.SetSummary(s)
503 | assert.Equal(t, a.(*api).operation.Summary, s)
504 | }
505 |
506 | func TestEcho(t *testing.T) {
507 | r := prepareApiRoot()
508 | assert.NotNil(t, r.Echo())
509 |
510 | g := prepareApiGroup()
511 | assert.NotNil(t, g.EchoGroup())
512 |
513 | a := prepareApi()
514 | assert.NotNil(t, a.Route())
515 | }
516 |
517 | func TestHandlers(t *testing.T) {
518 | t.Run("ErrorGroupSecurity", func(t *testing.T) {
519 | r := prepareApiRoot()
520 | e := r.(*Root).echo
521 | var h echo.HandlerFunc
522 | r.Group("G", "/g").SetSecurity("JWT").GET("/", h)
523 | req := httptest.NewRequest(echo.GET, "/doc/swagger.json", nil)
524 | rec := httptest.NewRecorder()
525 | c := e.NewContext(req, rec)
526 | if assert.NoError(t, r.(*Root).specHandler("/doc")(c)) {
527 | assert.Equal(t, http.StatusInternalServerError, rec.Code)
528 | }
529 | if assert.NoError(t, r.(*Root).docHandler("/doc")(c)) {
530 | assert.Equal(t, http.StatusInternalServerError, rec.Code)
531 | }
532 | })
533 |
534 | t.Run("ErrorApiSecurity", func(t *testing.T) {
535 | r := prepareApiRoot()
536 | e := r.(*Root).echo
537 | var h echo.HandlerFunc
538 | r.GET("/", h).SetSecurity("JWT")
539 | req := httptest.NewRequest(echo.GET, "/doc/swagger.json", nil)
540 | rec := httptest.NewRecorder()
541 | c := e.NewContext(req, rec)
542 | if assert.NoError(t, r.(*Root).specHandler("/doc")(c)) {
543 | assert.Equal(t, http.StatusInternalServerError, rec.Code)
544 | }
545 | if assert.NoError(t, r.(*Root).docHandler("/doc")(c)) {
546 | assert.Equal(t, http.StatusInternalServerError, rec.Code)
547 | }
548 | })
549 | }
550 |
--------------------------------------------------------------------------------
/wrapper.go:
--------------------------------------------------------------------------------
1 | package echoswagger
2 |
3 | import (
4 | "reflect"
5 | "strconv"
6 | "sync"
7 |
8 | "github.com/labstack/echo"
9 | )
10 |
11 | /*
12 | TODO:
13 | 1.pattern
14 | 2.opreationId 重复判断
15 |
16 | Notice:
17 | 1.不会对Email和URL进行验证,因为不影响页面的正常显示
18 | 2.SetSecurity/SetSecurityWithScope 传多个参数表示Security之间是AND关系;多次调用SetSecurity/SetSecurityWithScope Security之间是OR关系
19 | 3.只支持基本类型的Map Key
20 | */
21 |
22 | type ApiRouter interface {
23 |
24 | // Add overrides `Echo#Add()` and creates Api.
25 | Add(method, path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) Api
26 |
27 | // GET overrides `Echo#GET()` and creates Api.
28 | GET(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) Api
29 |
30 | // POST overrides `Echo#POST()` and creates Api.
31 | POST(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) Api
32 |
33 | // PUT overrides `Echo#PUT()` and creates Api.
34 | PUT(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) Api
35 |
36 | // DELETE overrides `Echo#DELETE()` and creates Api.
37 | DELETE(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) Api
38 |
39 | // OPTIONS overrides `Echo#OPTIONS()` and creates Api.
40 | OPTIONS(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) Api
41 |
42 | // HEAD overrides `Echo#HEAD()` and creates Api.
43 | HEAD(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) Api
44 |
45 | // PATCH overrides `Echo#PATCH()` and creates Api.
46 | PATCH(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) Api
47 | }
48 |
49 | type ApiRoot interface {
50 | ApiRouter
51 |
52 | // Bind an existing Echo group
53 | // note: this is usually used to create nested Echo groups
54 | // you still need to add route by ApiGroup instance but not the param Echo group
55 | // any routes created directly by the Echo group will not be added to the swagger spec.
56 | BindGroup(name string, g *echo.Group) ApiGroup
57 |
58 | // Group overrides `Echo#Group()` and creates ApiGroup.
59 | Group(name, prefix string, m ...echo.MiddlewareFunc) ApiGroup
60 |
61 | // SetRequestContentType sets request content types.
62 | SetRequestContentType(types ...string) ApiRoot
63 |
64 | // SetResponseContentType sets response content types.
65 | SetResponseContentType(types ...string) ApiRoot
66 |
67 | // SetExternalDocs sets external docs.
68 | SetExternalDocs(desc, url string) ApiRoot
69 |
70 | // AddSecurityBasic adds `SecurityDefinition` with type basic.
71 | AddSecurityBasic(name, desc string) ApiRoot
72 |
73 | // AddSecurityAPIKey adds `SecurityDefinition` with type apikey.
74 | AddSecurityAPIKey(name, desc string, in SecurityInType) ApiRoot
75 |
76 | // AddSecurityOAuth2 adds `SecurityDefinition` with type oauth2.
77 | AddSecurityOAuth2(name, desc string, flow OAuth2FlowType, authorizationUrl, tokenUrl string, scopes map[string]string) ApiRoot
78 |
79 | // SetUI sets UI setting.
80 | // If DetachSpec is false, HideTop will not take effect
81 | SetUI(ui UISetting) ApiRoot
82 |
83 | // SetScheme sets available protocol schemes.
84 | SetScheme(schemes ...string) ApiRoot
85 |
86 | // GetRaw returns raw `Swagger`. Only special case should use.
87 | GetRaw() *Swagger
88 |
89 | // SetRaw sets raw `Swagger` to ApiRoot. Only special case should use.
90 | SetRaw(s *Swagger) ApiRoot
91 |
92 | // Echo returns the embedded Echo instance
93 | Echo() *echo.Echo
94 | }
95 |
96 | type ApiGroup interface {
97 | ApiRouter
98 |
99 | // SetDescription sets description for ApiGroup.
100 | SetDescription(desc string) ApiGroup
101 |
102 | // SetExternalDocs sets external docs for ApiGroup.
103 | SetExternalDocs(desc, url string) ApiGroup
104 |
105 | // SetSecurity sets Security for all operations within the ApiGroup
106 | // which names are reigistered by AddSecurity... functions.
107 | SetSecurity(names ...string) ApiGroup
108 |
109 | // SetSecurityWithScope sets Security with scopes for all operations
110 | // within the ApiGroup which names are reigistered
111 | // by AddSecurity... functions.
112 | // Should only use when Security type is oauth2.
113 | SetSecurityWithScope(s map[string][]string) ApiGroup
114 |
115 | // EchoGroup returns the embedded `echo.Group` instance.
116 | EchoGroup() *echo.Group
117 | }
118 |
119 | type Api interface {
120 | // AddParamPath adds path parameter.
121 | AddParamPath(p interface{}, name, desc string) Api
122 |
123 | // AddParamPathNested adds path parameters nested in p.
124 | // P must be struct type.
125 | AddParamPathNested(p interface{}) Api
126 |
127 | // AddParamQuery adds query parameter.
128 | AddParamQuery(p interface{}, name, desc string, required bool) Api
129 |
130 | // AddParamQueryNested adds query parameters nested in p.
131 | // P must be struct type.
132 | AddParamQueryNested(p interface{}) Api
133 |
134 | // AddParamForm adds formData parameter.
135 | AddParamForm(p interface{}, name, desc string, required bool) Api
136 |
137 | // AddParamFormNested adds formData parameters nested in p.
138 | // P must be struct type.
139 | AddParamFormNested(p interface{}) Api
140 |
141 | // AddParamHeader adds header parameter.
142 | AddParamHeader(p interface{}, name, desc string, required bool) Api
143 |
144 | // AddParamHeaderNested adds header parameters nested in p.
145 | // P must be struct type.
146 | AddParamHeaderNested(p interface{}) Api
147 |
148 | // AddParamBody adds body parameter.
149 | AddParamBody(p interface{}, name, desc string, required bool) Api
150 |
151 | // AddParamFile adds file parameter.
152 | AddParamFile(name, desc string, required bool) Api
153 |
154 | // AddResponse adds response for Api.
155 | // Header must be struct type.
156 | AddResponse(code int, desc string, schema interface{}, header interface{}) Api
157 |
158 | // SetRequestContentType sets request content types.
159 | SetRequestContentType(types ...string) Api
160 |
161 | // SetResponseContentType sets response content types.
162 | SetResponseContentType(types ...string) Api
163 |
164 | // SetOperationId sets operationId
165 | SetOperationId(id string) Api
166 |
167 | // SetDeprecated marks Api as deprecated.
168 | SetDeprecated() Api
169 |
170 | // SetDescription sets description.
171 | SetDescription(desc string) Api
172 |
173 | // SetExternalDocs sets external docs.
174 | SetExternalDocs(desc, url string) Api
175 |
176 | // SetSummary sets summary.
177 | SetSummary(summary string) Api
178 |
179 | // SetSecurity sets Security which names are reigistered
180 | // by AddSecurity... functions.
181 | SetSecurity(names ...string) Api
182 |
183 | // SetSecurityWithScope sets Security for Api which names are
184 | // reigistered by AddSecurity... functions.
185 | // Should only use when Security type is oauth2.
186 | SetSecurityWithScope(s map[string][]string) Api
187 |
188 | // Route returns the embedded `echo.Route` instance.
189 | Route() *echo.Route
190 | }
191 |
192 | type routers struct {
193 | apis []api
194 | defs *RawDefineDic
195 | }
196 |
197 | type Root struct {
198 | routers
199 | spec *Swagger
200 | echo *echo.Echo
201 | groups []group
202 | ui UISetting
203 | once sync.Once
204 | err error
205 | }
206 |
207 | type group struct {
208 | routers
209 | echoGroup *echo.Group
210 | security []map[string][]string
211 | tag Tag
212 | }
213 |
214 | type api struct {
215 | route *echo.Route
216 | defs *RawDefineDic
217 | security []map[string][]string
218 | operation Operation
219 | }
220 |
221 | // New creates ApiRoot instance.
222 | // Multiple ApiRoot are allowed in one project.
223 | func New(e *echo.Echo, docPath string, i *Info, m ...echo.MiddlewareFunc) ApiRoot {
224 | if e == nil {
225 | panic("echoswagger: invalid Echo instance")
226 | }
227 |
228 | if i == nil {
229 | i = &Info{
230 | Title: "Project APIs",
231 | }
232 | }
233 | defs := make(RawDefineDic)
234 | r := &Root{
235 | echo: e,
236 | spec: &Swagger{
237 | Info: i,
238 | SecurityDefinitions: make(map[string]*SecurityDefinition),
239 | Definitions: make(map[string]*JSONSchema),
240 | },
241 | routers: routers{
242 | defs: &defs,
243 | },
244 | }
245 |
246 | e.GET(connectPath(docPath), r.docHandler(docPath), m...)
247 | e.GET(connectPath(docPath, SpecName), r.specHandler(docPath), m...)
248 | return r
249 | }
250 |
251 | func (r *Root) Add(method, path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) Api {
252 | return r.appendRoute(r.echo.Add(method, path, h, m...))
253 | }
254 |
255 | func (r *Root) GET(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) Api {
256 | return r.appendRoute(r.echo.GET(path, h, m...))
257 | }
258 |
259 | func (r *Root) POST(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) Api {
260 | return r.appendRoute(r.echo.POST(path, h, m...))
261 | }
262 |
263 | func (r *Root) PUT(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) Api {
264 | return r.appendRoute(r.echo.PUT(path, h, m...))
265 | }
266 |
267 | func (r *Root) DELETE(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) Api {
268 | return r.appendRoute(r.echo.DELETE(path, h, m...))
269 | }
270 |
271 | func (r *Root) OPTIONS(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) Api {
272 | return r.appendRoute(r.echo.OPTIONS(path, h, m...))
273 | }
274 |
275 | func (r *Root) HEAD(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) Api {
276 | return r.appendRoute(r.echo.HEAD(path, h, m...))
277 | }
278 |
279 | func (r *Root) PATCH(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) Api {
280 | return r.appendRoute(r.echo.PATCH(path, h, m...))
281 | }
282 |
283 | func (r *Root) Group(name, prefix string, m ...echo.MiddlewareFunc) ApiGroup {
284 | if name == "" {
285 | panic("echoswagger: invalid name of ApiGroup")
286 | }
287 | echoGroup := r.echo.Group(prefix, m...)
288 | group := group{
289 | echoGroup: echoGroup,
290 | routers: routers{
291 | defs: r.defs,
292 | },
293 | }
294 | group.tag = Tag{Name: name}
295 | r.groups = append(r.groups, group)
296 | return &r.groups[len(r.groups)-1]
297 | }
298 |
299 | func (r *Root) BindGroup(name string, g *echo.Group) ApiGroup {
300 | if name == "" {
301 | panic("echoswagger: invalid name of ApiGroup")
302 | }
303 | group := group{
304 | echoGroup: g,
305 | routers: routers{
306 | defs: r.defs,
307 | },
308 | }
309 | group.tag = Tag{Name: name}
310 | r.groups = append(r.groups, group)
311 | return &r.groups[len(r.groups)-1]
312 | }
313 |
314 | func (r *Root) SetRequestContentType(types ...string) ApiRoot {
315 | r.spec.Consumes = types
316 | return r
317 | }
318 |
319 | func (r *Root) SetResponseContentType(types ...string) ApiRoot {
320 | r.spec.Produces = types
321 | return r
322 | }
323 |
324 | func (r *Root) SetExternalDocs(desc, url string) ApiRoot {
325 | r.spec.ExternalDocs = &ExternalDocs{
326 | Description: desc,
327 | URL: url,
328 | }
329 | return r
330 | }
331 |
332 | func (r *Root) AddSecurityBasic(name, desc string) ApiRoot {
333 | if !r.checkSecurity(name) {
334 | return r
335 | }
336 | sd := &SecurityDefinition{
337 | Type: string(SecurityBasic),
338 | Description: desc,
339 | }
340 | r.spec.SecurityDefinitions[name] = sd
341 | return r
342 | }
343 |
344 | func (r *Root) AddSecurityAPIKey(name, desc string, in SecurityInType) ApiRoot {
345 | if !r.checkSecurity(name) {
346 | return r
347 | }
348 | sd := &SecurityDefinition{
349 | Type: string(SecurityAPIKey),
350 | Description: desc,
351 | Name: name,
352 | In: string(in),
353 | }
354 | r.spec.SecurityDefinitions[name] = sd
355 | return r
356 | }
357 |
358 | func (r *Root) AddSecurityOAuth2(name, desc string, flow OAuth2FlowType, authorizationUrl, tokenUrl string, scopes map[string]string) ApiRoot {
359 | if !r.checkSecurity(name) {
360 | return r
361 | }
362 | sd := &SecurityDefinition{
363 | Type: string(SecurityOAuth2),
364 | Description: desc,
365 | Flow: string(flow),
366 | AuthorizationURL: authorizationUrl,
367 | TokenURL: tokenUrl,
368 | Scopes: scopes,
369 | }
370 | r.spec.SecurityDefinitions[name] = sd
371 | return r
372 | }
373 |
374 | func (r *Root) SetUI(ui UISetting) ApiRoot {
375 | r.ui = ui
376 | return r
377 | }
378 |
379 | func (r *Root) SetScheme(schemes ...string) ApiRoot {
380 | for _, s := range schemes {
381 | if !isValidScheme(s) {
382 | panic("echoswagger: invalid protocol scheme")
383 | }
384 | }
385 | r.spec.Schemes = schemes
386 | return r
387 | }
388 |
389 | func (r *Root) GetRaw() *Swagger {
390 | return r.spec
391 | }
392 |
393 | func (r *Root) SetRaw(s *Swagger) ApiRoot {
394 | r.spec = s
395 | return r
396 | }
397 |
398 | func (r *Root) Echo() *echo.Echo {
399 | return r.echo
400 | }
401 |
402 | func (g *group) Add(method, path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) Api {
403 | a := g.appendRoute(g.echoGroup.Add(method, path, h, m...))
404 | a.operation.Tags = []string{g.tag.Name}
405 | return a
406 | }
407 |
408 | func (g *group) GET(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) Api {
409 | a := g.appendRoute(g.echoGroup.GET(path, h, m...))
410 | a.operation.Tags = []string{g.tag.Name}
411 | return a
412 | }
413 |
414 | func (g *group) POST(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) Api {
415 | a := g.appendRoute(g.echoGroup.POST(path, h, m...))
416 | a.operation.Tags = []string{g.tag.Name}
417 | return a
418 | }
419 |
420 | func (g *group) PUT(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) Api {
421 | a := g.appendRoute(g.echoGroup.PUT(path, h, m...))
422 | a.operation.Tags = []string{g.tag.Name}
423 | return a
424 | }
425 |
426 | func (g *group) DELETE(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) Api {
427 | a := g.appendRoute(g.echoGroup.DELETE(path, h, m...))
428 | a.operation.Tags = []string{g.tag.Name}
429 | return a
430 | }
431 |
432 | func (g *group) OPTIONS(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) Api {
433 | a := g.appendRoute(g.echoGroup.OPTIONS(path, h, m...))
434 | a.operation.Tags = []string{g.tag.Name}
435 | return a
436 | }
437 |
438 | func (g *group) HEAD(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) Api {
439 | a := g.appendRoute(g.echoGroup.HEAD(path, h, m...))
440 | a.operation.Tags = []string{g.tag.Name}
441 | return a
442 | }
443 |
444 | func (g *group) PATCH(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) Api {
445 | a := g.appendRoute(g.echoGroup.PATCH(path, h, m...))
446 | a.operation.Tags = []string{g.tag.Name}
447 | return a
448 | }
449 |
450 | func (g *group) SetDescription(desc string) ApiGroup {
451 | g.tag.Description = desc
452 | return g
453 | }
454 |
455 | func (g *group) SetExternalDocs(desc, url string) ApiGroup {
456 | g.tag.ExternalDocs = &ExternalDocs{
457 | Description: desc,
458 | URL: url,
459 | }
460 | return g
461 | }
462 |
463 | func (g *group) SetSecurity(names ...string) ApiGroup {
464 | if len(names) == 0 {
465 | return g
466 | }
467 | g.security = setSecurity(g.security, names...)
468 | return g
469 | }
470 |
471 | func (g *group) SetSecurityWithScope(s map[string][]string) ApiGroup {
472 | g.security = setSecurityWithScope(g.security, s)
473 | return g
474 | }
475 |
476 | func (g *group) EchoGroup() *echo.Group {
477 | return g.echoGroup
478 | }
479 |
480 | func (a *api) AddParamPath(p interface{}, name, desc string) Api {
481 | return a.addParams(p, ParamInPath, name, desc, true, false)
482 | }
483 |
484 | func (a *api) AddParamPathNested(p interface{}) Api {
485 | return a.addParams(p, ParamInPath, "", "", true, true)
486 | }
487 |
488 | func (a *api) AddParamQuery(p interface{}, name, desc string, required bool) Api {
489 | return a.addParams(p, ParamInQuery, name, desc, required, false)
490 | }
491 |
492 | func (a *api) AddParamQueryNested(p interface{}) Api {
493 | return a.addParams(p, ParamInQuery, "", "", false, true)
494 | }
495 |
496 | func (a *api) AddParamForm(p interface{}, name, desc string, required bool) Api {
497 | return a.addParams(p, ParamInFormData, name, desc, required, false)
498 | }
499 |
500 | func (a *api) AddParamFormNested(p interface{}) Api {
501 | return a.addParams(p, ParamInFormData, "", "", false, true)
502 | }
503 |
504 | func (a *api) AddParamHeader(p interface{}, name, desc string, required bool) Api {
505 | return a.addParams(p, ParamInHeader, name, desc, required, false)
506 | }
507 |
508 | func (a *api) AddParamHeaderNested(p interface{}) Api {
509 | return a.addParams(p, ParamInHeader, "", "", false, true)
510 | }
511 |
512 | func (a *api) AddParamBody(p interface{}, name, desc string, required bool) Api {
513 | return a.addBodyParams(p, name, desc, required)
514 | }
515 |
516 | func (a *api) AddParamFile(name, desc string, required bool) Api {
517 | name = a.operation.rename(name)
518 | a.operation.Parameters = append(a.operation.Parameters, &Parameter{
519 | Name: name,
520 | In: string(ParamInFormData),
521 | Description: desc,
522 | Required: required,
523 | Type: "file",
524 | })
525 | return a
526 | }
527 |
528 | func (a *api) AddResponse(code int, desc string, schema interface{}, header interface{}) Api {
529 | r := &Response{
530 | Description: desc,
531 | }
532 |
533 | st := reflect.TypeOf(schema)
534 | if st != nil {
535 | if !isValidSchema(st, false) {
536 | panic("echoswagger: invalid response schema")
537 | }
538 | r.Schema = a.defs.genSchema(reflect.ValueOf(schema))
539 | }
540 |
541 | ht := reflect.TypeOf(header)
542 | if ht != nil {
543 | if !isValidParam(reflect.TypeOf(header), true, false) {
544 | panic("echoswagger: invalid response header")
545 | }
546 | r.Headers = a.genHeader(reflect.ValueOf(header))
547 | }
548 |
549 | cstr := strconv.Itoa(code)
550 | a.operation.Responses[cstr] = r
551 | return a
552 | }
553 |
554 | func (a *api) SetRequestContentType(types ...string) Api {
555 | a.operation.Consumes = types
556 | return a
557 | }
558 |
559 | func (a *api) SetResponseContentType(types ...string) Api {
560 | a.operation.Produces = types
561 | return a
562 | }
563 |
564 | func (a *api) SetOperationId(id string) Api {
565 | a.operation.OperationID = id
566 | return a
567 | }
568 |
569 | func (a *api) SetDeprecated() Api {
570 | a.operation.Deprecated = true
571 | return a
572 | }
573 |
574 | func (a *api) SetDescription(desc string) Api {
575 | a.operation.Description = desc
576 | return a
577 | }
578 |
579 | func (a *api) SetExternalDocs(desc, url string) Api {
580 | a.operation.ExternalDocs = &ExternalDocs{
581 | Description: desc,
582 | URL: url,
583 | }
584 | return a
585 | }
586 |
587 | func (a *api) SetSummary(summary string) Api {
588 | a.operation.Summary = summary
589 | return a
590 | }
591 |
592 | func (a *api) SetSecurity(names ...string) Api {
593 | if len(names) == 0 {
594 | return a
595 | }
596 | a.security = setSecurity(a.security, names...)
597 | return a
598 | }
599 |
600 | func (a *api) SetSecurityWithScope(s map[string][]string) Api {
601 | a.security = setSecurityWithScope(a.security, s)
602 | return a
603 | }
604 |
605 | func (a *api) Route() *echo.Route {
606 | return a.route
607 | }
608 |
--------------------------------------------------------------------------------
/models.go:
--------------------------------------------------------------------------------
1 | package echoswagger
2 |
3 | type (
4 | // Swagger represents an instance of a swagger object.
5 | // See https://swagger.io/specification/
6 | Swagger struct {
7 | Swagger string `json:"swagger,omitempty"`
8 | Info *Info `json:"info,omitempty"`
9 | Host string `json:"host,omitempty"`
10 | BasePath string `json:"basePath,omitempty"`
11 | Schemes []string `json:"schemes,omitempty"`
12 | Consumes []string `json:"consumes,omitempty"`
13 | Produces []string `json:"produces,omitempty"`
14 | Paths map[string]interface{} `json:"paths"`
15 | Definitions map[string]*JSONSchema `json:"definitions,omitempty"`
16 | Parameters map[string]*Parameter `json:"parameters,omitempty"`
17 | Responses map[string]*Response `json:"responses,omitempty"`
18 | SecurityDefinitions map[string]*SecurityDefinition `json:"securityDefinitions,omitempty"`
19 | Tags []*Tag `json:"tags,omitempty"`
20 | ExternalDocs *ExternalDocs `json:"externalDocs,omitempty"`
21 | }
22 |
23 | // Info provides metadata about the API. The metadata can be used by the clients if needed,
24 | // and can be presented in the Swagger-UI for convenience.
25 | Info struct {
26 | Title string `json:"title,omitempty"`
27 | Description string `json:"description,omitempty"`
28 | TermsOfService string `json:"termsOfService,omitempty"`
29 | Contact *Contact `json:"contact,omitempty"`
30 | License *License `json:"license,omitempty"`
31 | Version string `json:"version"`
32 | Extensions map[string]interface{} `json:"-"`
33 | }
34 |
35 | // Contact contains the API contact information.
36 | Contact struct {
37 | // Name of the contact person/organization
38 | Name string `json:"name,omitempty"`
39 | // Email address of the contact person/organization
40 | Email string `json:"email,omitempty"`
41 | // URL pointing to the contact information
42 | URL string `json:"url,omitempty"`
43 | }
44 |
45 | // License contains the license information for the API.
46 | License struct {
47 | // Name of license used for the API
48 | Name string `json:"name,omitempty"`
49 | // URL to the license used for the API
50 | URL string `json:"url,omitempty"`
51 | }
52 |
53 | // Path holds the relative paths to the individual endpoints.
54 | Path struct {
55 | // Ref allows for an external definition of this path item.
56 | Ref string `json:"$ref,omitempty"`
57 | // Get defines a GET operation on this path.
58 | Get *Operation `json:"get,omitempty"`
59 | // Put defines a PUT operation on this path.
60 | Put *Operation `json:"put,omitempty"`
61 | // Post defines a POST operation on this path.
62 | Post *Operation `json:"post,omitempty"`
63 | // Delete defines a DELETE operation on this path.
64 | Delete *Operation `json:"delete,omitempty"`
65 | // Options defines a OPTIONS operation on this path.
66 | Options *Operation `json:"options,omitempty"`
67 | // Head defines a HEAD operation on this path.
68 | Head *Operation `json:"head,omitempty"`
69 | // Patch defines a PATCH operation on this path.
70 | Patch *Operation `json:"patch,omitempty"`
71 | // Parameters is the list of parameters that are applicable for all the operations
72 | // described under this path.
73 | Parameters []*Parameter `json:"parameters,omitempty"`
74 | // Extensions defines the swagger extensions.
75 | Extensions map[string]interface{} `json:"-"`
76 | }
77 |
78 | // Operation describes a single API operation on a path.
79 | Operation struct {
80 | // Tags is a list of tags for API documentation control. Tags can be used for
81 | // logical grouping of operations by resources or any other qualifier.
82 | Tags []string `json:"tags,omitempty"`
83 | // Summary is a short summary of what the operation does. For maximum readability
84 | // in the swagger-ui, this field should be less than 120 characters.
85 | Summary string `json:"summary,omitempty"`
86 | // Description is a verbose explanation of the operation behavior.
87 | // GFM syntax can be used for rich text representation.
88 | Description string `json:"description,omitempty"`
89 | // ExternalDocs points to additional external documentation for this operation.
90 | ExternalDocs *ExternalDocs `json:"externalDocs,omitempty"`
91 | // OperationID is a unique string used to identify the operation.
92 | OperationID string `json:"operationId,omitempty"`
93 | // Consumes is a list of MIME types the operation can consume.
94 | Consumes []string `json:"consumes,omitempty"`
95 | // Produces is a list of MIME types the operation can produce.
96 | Produces []string `json:"produces,omitempty"`
97 | // Parameters is a list of parameters that are applicable for this operation.
98 | Parameters []*Parameter `json:"parameters,omitempty"`
99 | // Responses is the list of possible responses as they are returned from executing
100 | // this operation.
101 | Responses map[string]*Response `json:"responses,omitempty"`
102 | // Schemes is the transfer protocol for the operation.
103 | Schemes []string `json:"schemes,omitempty"`
104 | // Deprecated declares this operation to be deprecated.
105 | Deprecated bool `json:"deprecated,omitempty"`
106 | // Secury is a declaration of which security schemes are applied for this operation.
107 | Security []map[string][]string `json:"security,omitempty"`
108 | // Extensions defines the swagger extensions.
109 | Extensions map[string]interface{} `json:"-"`
110 | }
111 |
112 | // Parameter describes a single operation parameter.
113 | Parameter struct {
114 | // Name of the parameter. Parameter names are case sensitive.
115 | Name string `json:"name"`
116 | // In is the location of the parameter.
117 | // Possible values are "query", "header", "path", "formData" or "body".
118 | In string `json:"in"`
119 | // Description is`a brief description of the parameter.
120 | // GFM syntax can be used for rich text representation.
121 | Description string `json:"description,omitempty"`
122 | // Required determines whether this parameter is mandatory.
123 | Required bool `json:"required"`
124 | // Schema defining the type used for the body parameter, only if "in" is body
125 | Schema *JSONSchema `json:"schema,omitempty"`
126 |
127 | // properties below only apply if "in" is not body
128 |
129 | // Type of the parameter. Since the parameter is not located at the request body,
130 | // it is limited to simple types (that is, not an object).
131 | Type string `json:"type,omitempty"`
132 | // Format is the extending format for the previously mentioned type.
133 | Format string `json:"format,omitempty"`
134 | // AllowEmptyValue sets the ability to pass empty-valued parameters.
135 | // This is valid only for either query or formData parameters and allows you to
136 | // send a parameter with a name only or an empty value. Default value is false.
137 | AllowEmptyValue bool `json:"allowEmptyValue,omitempty"`
138 | // Items describes the type of items in the array if type is "array".
139 | Items *Items `json:"items,omitempty"`
140 | // CollectionFormat determines the format of the array if type array is used.
141 | // Possible values are csv, ssv, tsv, pipes and multi.
142 | CollectionFormat string `json:"collectionFormat,omitempty"`
143 | // Default declares the value of the parameter that the server will use if none is
144 | // provided, for example a "count" to control the number of results per page might
145 | // default to 100 if not supplied by the client in the request.
146 | Default interface{} `json:"default,omitempty"`
147 | Maximum *float64 `json:"maximum,omitempty"`
148 | ExclusiveMaximum bool `json:"exclusiveMaximum,omitempty"`
149 | Minimum *float64 `json:"minimum,omitempty"`
150 | ExclusiveMinimum bool `json:"exclusiveMinimum,omitempty"`
151 | MaxLength *int `json:"maxLength,omitempty"`
152 | MinLength *int `json:"minLength,omitempty"`
153 | Pattern string `json:"pattern,omitempty"`
154 | MaxItems *int `json:"maxItems,omitempty"`
155 | MinItems *int `json:"minItems,omitempty"`
156 | UniqueItems bool `json:"uniqueItems,omitempty"`
157 | Enum []interface{} `json:"enum,omitempty"`
158 | MultipleOf float64 `json:"multipleOf,omitempty"`
159 | // Extensions defines the swagger extensions.
160 | Extensions map[string]interface{} `json:"-"`
161 | }
162 |
163 | // Response describes an operation response.
164 | Response struct {
165 | // Description of the response. GFM syntax can be used for rich text representation.
166 | Description string `json:"description,omitempty"`
167 | // Schema is a definition of the response structure. It can be a primitive,
168 | // an array or an object. If this field does not exist, it means no content is
169 | // returned as part of the response. As an extension to the Schema Object, its root
170 | // type value may also be "file".
171 | Schema *JSONSchema `json:"schema,omitempty"`
172 | // Headers is a list of headers that are sent with the response.
173 | Headers map[string]*Header `json:"headers,omitempty"`
174 | // Ref references a global API response.
175 | // This field is exclusive with the other fields of Response.
176 | Ref string `json:"$ref,omitempty"`
177 | // Extensions defines the swagger extensions.
178 | Extensions map[string]interface{} `json:"-"`
179 | }
180 |
181 | // Header represents a header parameter.
182 | Header struct {
183 | // Description is`a brief description of the parameter.
184 | // GFM syntax can be used for rich text representation.
185 | Description string `json:"description,omitempty"`
186 | // Type of the header. it is limited to simple types (that is, not an object).
187 | Type string `json:"type,omitempty"`
188 | // Format is the extending format for the previously mentioned type.
189 | Format string `json:"format,omitempty"`
190 | // Items describes the type of items in the array if type is "array".
191 | Items *Items `json:"items,omitempty"`
192 | // CollectionFormat determines the format of the array if type array is used.
193 | // Possible values are csv, ssv, tsv, pipes and multi.
194 | CollectionFormat string `json:"collectionFormat,omitempty"`
195 | // Default declares the value of the parameter that the server will use if none is
196 | // provided, for example a "count" to control the number of results per page might
197 | // default to 100 if not supplied by the client in the request.
198 | Default interface{} `json:"default,omitempty"`
199 | Maximum *float64 `json:"maximum,omitempty"`
200 | ExclusiveMaximum bool `json:"exclusiveMaximum,omitempty"`
201 | Minimum *float64 `json:"minimum,omitempty"`
202 | ExclusiveMinimum bool `json:"exclusiveMinimum,omitempty"`
203 | MaxLength *int `json:"maxLength,omitempty"`
204 | MinLength *int `json:"minLength,omitempty"`
205 | Pattern string `json:"pattern,omitempty"`
206 | MaxItems *int `json:"maxItems,omitempty"`
207 | MinItems *int `json:"minItems,omitempty"`
208 | UniqueItems bool `json:"uniqueItems,omitempty"`
209 | Enum []interface{} `json:"enum,omitempty"`
210 | MultipleOf float64 `json:"multipleOf,omitempty"`
211 | }
212 |
213 | // SecurityDefinition allows the definition of a security scheme that can be used by the
214 | // operations. Supported schemes are basic authentication, an API key (either as a header or
215 | // as a query parameter) and OAuth2's common flows (implicit, password, application and
216 | // access code).
217 | SecurityDefinition struct {
218 | // Type of the security scheme. Valid values are "basic", "apiKey" or "oauth2".
219 | Type string `json:"type"`
220 | // Description for security scheme
221 | Description string `json:"description,omitempty"`
222 | // Name of the header or query parameter to be used when type is "apiKey".
223 | Name string `json:"name,omitempty"`
224 | // In is the location of the API key when type is "apiKey".
225 | // Valid values are "query" or "header".
226 | In string `json:"in,omitempty"`
227 | // Flow is the flow used by the OAuth2 security scheme when type is "oauth2"
228 | // Valid values are "implicit", "password", "application" or "accessCode".
229 | Flow string `json:"flow,omitempty"`
230 | // The oauth2 authorization URL to be used for this flow.
231 | AuthorizationURL string `json:"authorizationUrl,omitempty"`
232 | // TokenURL is the token URL to be used for this flow.
233 | TokenURL string `json:"tokenUrl,omitempty"`
234 | // Scopes list the available scopes for the OAuth2 security scheme.
235 | Scopes map[string]string `json:"scopes,omitempty"`
236 | // Extensions defines the swagger extensions.
237 | Extensions map[string]interface{} `json:"-"`
238 | }
239 |
240 | // Scope corresponds to an available scope for an OAuth2 security scheme.
241 | Scope struct {
242 | // Description for scope
243 | Description string `json:"description,omitempty"`
244 | }
245 |
246 | // ExternalDocs allows referencing an external resource for extended documentation.
247 | ExternalDocs struct {
248 | // Description is a short description of the target documentation.
249 | // GFM syntax can be used for rich text representation.
250 | Description string `json:"description,omitempty"`
251 | // URL for the target documentation.
252 | URL string `json:"url"`
253 | }
254 |
255 | // Items is a limited subset of JSON-Schema's items object. It is used by parameter
256 | // definitions that are not located in "body".
257 | Items struct {
258 | // Type of the items. it is limited to simple types (that is, not an object).
259 | Type string `json:"type,omitempty"`
260 | // Format is the extending format for the previously mentioned type.
261 | Format string `json:"format,omitempty"`
262 | // Items describes the type of items in the array if type is "array".
263 | Items *Items `json:"items,omitempty"`
264 | // CollectionFormat determines the format of the array if type array is used.
265 | // Possible values are csv, ssv, tsv, pipes and multi.
266 | CollectionFormat string `json:"collectionFormat,omitempty"`
267 | // Default declares the value of the parameter that the server will use if none is
268 | // provided, for example a "count" to control the number of results per page might
269 | // default to 100 if not supplied by the client in the request.
270 | Default interface{} `json:"default,omitempty"`
271 | Maximum *float64 `json:"maximum,omitempty"`
272 | ExclusiveMaximum bool `json:"exclusiveMaximum,omitempty"`
273 | Minimum *float64 `json:"minimum,omitempty"`
274 | ExclusiveMinimum bool `json:"exclusiveMinimum,omitempty"`
275 | MaxLength *int `json:"maxLength,omitempty"`
276 | MinLength *int `json:"minLength,omitempty"`
277 | Pattern string `json:"pattern,omitempty"`
278 | MaxItems *int `json:"maxItems,omitempty"`
279 | MinItems *int `json:"minItems,omitempty"`
280 | UniqueItems bool `json:"uniqueItems,omitempty"`
281 | Enum []interface{} `json:"enum,omitempty"`
282 | MultipleOf float64 `json:"multipleOf,omitempty"`
283 | }
284 |
285 | // Tag allows adding meta data to a single tag that is used by the Operation Object. It is
286 | // not mandatory to have a Tag Object per tag used there.
287 | Tag struct {
288 | // Name of the tag.
289 | Name string `json:"name,omitempty"`
290 | // Description is a short description of the tag.
291 | // GFM syntax can be used for rich text representation.
292 | Description string `json:"description,omitempty"`
293 | // ExternalDocs is additional external documentation for this tag.
294 | ExternalDocs *ExternalDocs `json:"externalDocs,omitempty"`
295 | // Extensions defines the swagger extensions.
296 | Extensions map[string]interface{} `json:"-"`
297 | }
298 |
299 | JSONSchema struct {
300 | Schema string `json:"$schema,omitempty"`
301 | // Core schema
302 | ID string `json:"id,omitempty"`
303 | Title string `json:"title,omitempty"`
304 | Type JSONType `json:"type,omitempty"`
305 | Items *JSONSchema `json:"items,omitempty"`
306 | Properties map[string]*JSONSchema `json:"properties,omitempty"`
307 | Definitions map[string]*JSONSchema `json:"definitions,omitempty"`
308 | Description string `json:"description,omitempty"`
309 | DefaultValue interface{} `json:"default,omitempty"`
310 | Example interface{} `json:"example,omitempty"`
311 |
312 | // Hyper schema
313 | Media *JSONMedia `json:"media,omitempty"`
314 | ReadOnly bool `json:"readOnly,omitempty"`
315 | Ref string `json:"$ref,omitempty"`
316 | XML *XMLSchema `json:"xml,omitempty"`
317 |
318 | // Validation
319 | Enum []interface{} `json:"enum,omitempty"`
320 | Format string `json:"format,omitempty"`
321 | Pattern string `json:"pattern,omitempty"`
322 | Minimum *float64 `json:"minimum,omitempty"`
323 | Maximum *float64 `json:"maximum,omitempty"`
324 | MinLength *int `json:"minLength,omitempty"`
325 | MaxLength *int `json:"maxLength,omitempty"`
326 | Required []string `json:"required,omitempty"`
327 | AdditionalProperties *JSONSchema `json:"additionalProperties,omitempty"`
328 |
329 | // Union
330 | AnyOf []*JSONSchema `json:"anyOf,omitempty"`
331 | }
332 |
333 | // JSONType is the JSON type enum.
334 | JSONType string
335 |
336 | // JSONMedia represents a "media" field in a JSON hyper schema.
337 | JSONMedia struct {
338 | BinaryEncoding string `json:"binaryEncoding,omitempty"`
339 | Type string `json:"type,omitempty"`
340 | }
341 |
342 | // JSONLink represents a "link" field in a JSON hyper schema.
343 | JSONLink struct {
344 | Title string `json:"title,omitempty"`
345 | Description string `json:"description,omitempty"`
346 | Rel string `json:"rel,omitempty"`
347 | Href string `json:"href,omitempty"`
348 | Method string `json:"method,omitempty"`
349 | Schema *JSONSchema `json:"schema,omitempty"`
350 | TargetSchema *JSONSchema `json:"targetSchema,omitempty"`
351 | MediaType string `json:"mediaType,omitempty"`
352 | EncType string `json:"encType,omitempty"`
353 | }
354 |
355 | XMLSchema struct {
356 | Name string `json:"name,omitempty"`
357 | Namespace string `json:"namespace,omitempty"`
358 | Prefix string `json:"prefix,omitempty"`
359 | Attribute string `json:"attribute,omitempty"`
360 | Wrapped bool `json:"wrapped,omitempty"`
361 | }
362 | )
363 |
--------------------------------------------------------------------------------
/examples/swagger.json:
--------------------------------------------------------------------------------
1 | {
2 | "swagger": "2.0",
3 | "info": {
4 | "title": "Swagger Petstore",
5 | "description": "This is a sample server Petstore server. You can find out more about Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, #swagger](http://swagger.io/irc/). For this sample, you can use the api key `special-key` to test the authorization filters.",
6 | "termsOfService": "http://swagger.io/terms/",
7 | "contact": {
8 | "email": "apiteam@swagger.io"
9 | },
10 | "license": {
11 | "name": "Apache 2.0",
12 | "url": "http://www.apache.org/licenses/LICENSE-2.0.html"
13 | },
14 | "version": "1.0.0"
15 | },
16 | "schemes": [
17 | "https",
18 | "http"
19 | ],
20 | "produces": [
21 | "application/xml",
22 | "application/json"
23 | ],
24 | "paths": {
25 | "/pet": {
26 | "put": {
27 | "tags": [
28 | "pet"
29 | ],
30 | "summary": "Update an existing pet",
31 | "operationId": "updatePet",
32 | "consumes": [
33 | "application/json",
34 | "application/xml"
35 | ],
36 | "parameters": [
37 | {
38 | "name": "body",
39 | "in": "body",
40 | "description": "Pet object that needs to be added to the store",
41 | "required": true,
42 | "schema": {
43 | "$ref": "#/definitions/Pet"
44 | }
45 | }
46 | ],
47 | "responses": {
48 | "400": {
49 | "description": "Invalid ID supplied"
50 | },
51 | "404": {
52 | "description": "Pet not found"
53 | },
54 | "405": {
55 | "description": "Validation exception"
56 | }
57 | },
58 | "security": [
59 | {
60 | "petstore_auth": [
61 | "write:pets",
62 | "read:pets"
63 | ]
64 | }
65 | ]
66 | },
67 | "post": {
68 | "tags": [
69 | "pet"
70 | ],
71 | "summary": "Add a new pet to the store",
72 | "operationId": "addPet",
73 | "consumes": [
74 | "application/json",
75 | "application/xml"
76 | ],
77 | "parameters": [
78 | {
79 | "name": "body",
80 | "in": "body",
81 | "description": "Pet object that needs to be added to the store",
82 | "required": true,
83 | "schema": {
84 | "$ref": "#/definitions/Pet"
85 | }
86 | }
87 | ],
88 | "responses": {
89 | "405": {
90 | "description": "Invalid input"
91 | }
92 | },
93 | "security": [
94 | {
95 | "petstore_auth": [
96 | "write:pets",
97 | "read:pets"
98 | ]
99 | }
100 | ]
101 | }
102 | },
103 | "/pet/findByStatus": {
104 | "get": {
105 | "tags": [
106 | "pet"
107 | ],
108 | "summary": "Finds Pets by status",
109 | "description": "Multiple status values can be provided with comma separated strings",
110 | "operationId": "findPetsByStatus",
111 | "parameters": [
112 | {
113 | "name": "status",
114 | "in": "query",
115 | "description": "Status values that need to be considered for filter",
116 | "required": true,
117 | "type": "array",
118 | "items": {
119 | "type": "string",
120 | "format": "string",
121 | "default": "available",
122 | "enum": [
123 | "available",
124 | "pending",
125 | "sold"
126 | ]
127 | },
128 | "collectionFormat": "multi"
129 | }
130 | ],
131 | "responses": {
132 | "200": {
133 | "description": "successful operation",
134 | "schema": {
135 | "type": "array",
136 | "items": {
137 | "$ref": "#/definitions/Pet"
138 | }
139 | }
140 | },
141 | "400": {
142 | "description": "Invalid status value"
143 | }
144 | },
145 | "security": [
146 | {
147 | "petstore_auth": [
148 | "write:pets",
149 | "read:pets"
150 | ]
151 | }
152 | ]
153 | }
154 | },
155 | "/pet/findByTags": {
156 | "get": {
157 | "tags": [
158 | "pet"
159 | ],
160 | "summary": "Finds Pets by tags",
161 | "description": "Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing.",
162 | "operationId": "findPetsByTags",
163 | "parameters": [
164 | {
165 | "name": "tags",
166 | "in": "query",
167 | "description": "Tags to filter by",
168 | "required": true,
169 | "type": "array",
170 | "items": {
171 | "type": "string",
172 | "format": "string"
173 | },
174 | "collectionFormat": "multi"
175 | }
176 | ],
177 | "responses": {
178 | "200": {
179 | "description": "successful operation",
180 | "schema": {
181 | "type": "array",
182 | "items": {
183 | "$ref": "#/definitions/Pet"
184 | }
185 | }
186 | },
187 | "400": {
188 | "description": "Invalid tag value"
189 | }
190 | },
191 | "deprecated": true,
192 | "security": [
193 | {
194 | "petstore_auth": [
195 | "write:pets",
196 | "read:pets"
197 | ]
198 | }
199 | ]
200 | }
201 | },
202 | "/pet/{petId}": {
203 | "get": {
204 | "tags": [
205 | "pet"
206 | ],
207 | "summary": "Find pet by ID",
208 | "description": "Returns a single pet",
209 | "operationId": "getPetById",
210 | "parameters": [
211 | {
212 | "name": "petId",
213 | "in": "path",
214 | "description": "ID of pet to return",
215 | "required": true,
216 | "type": "integer",
217 | "format": "int32"
218 | }
219 | ],
220 | "responses": {
221 | "200": {
222 | "description": "successful operation",
223 | "schema": {
224 | "$ref": "#/definitions/Pet"
225 | }
226 | },
227 | "400": {
228 | "description": "Invalid ID supplied"
229 | },
230 | "404": {
231 | "description": "Pet not found"
232 | }
233 | },
234 | "security": [
235 | {
236 | "api_key": []
237 | }
238 | ]
239 | },
240 | "post": {
241 | "tags": [
242 | "pet"
243 | ],
244 | "summary": "Updates a pet in the store with form data",
245 | "operationId": "updatePetWithForm",
246 | "consumes": [
247 | "application/x-www-form-urlencoded"
248 | ],
249 | "parameters": [
250 | {
251 | "name": "petId",
252 | "in": "path",
253 | "description": "ID of pet that needs to be updated",
254 | "required": true,
255 | "type": "integer",
256 | "format": "int32"
257 | },
258 | {
259 | "name": "name",
260 | "in": "formData",
261 | "description": "Updated name of the pet",
262 | "required": false,
263 | "type": "string",
264 | "format": "string"
265 | },
266 | {
267 | "name": "status",
268 | "in": "formData",
269 | "description": "Updated status of the pet",
270 | "required": false,
271 | "type": "string",
272 | "format": "string"
273 | }
274 | ],
275 | "responses": {
276 | "405": {
277 | "description": "Invalid input"
278 | }
279 | },
280 | "security": [
281 | {
282 | "petstore_auth": [
283 | "write:pets",
284 | "read:pets"
285 | ]
286 | }
287 | ]
288 | },
289 | "delete": {
290 | "tags": [
291 | "pet"
292 | ],
293 | "summary": "Deletes a pet",
294 | "operationId": "deletePet",
295 | "parameters": [
296 | {
297 | "name": "api_key",
298 | "in": "header",
299 | "required": false,
300 | "type": "string",
301 | "format": "string"
302 | },
303 | {
304 | "name": "petId",
305 | "in": "path",
306 | "description": "Pet id to delete",
307 | "required": true,
308 | "type": "integer",
309 | "format": "int64"
310 | }
311 | ],
312 | "responses": {
313 | "400": {
314 | "description": "Invalid ID supplied"
315 | },
316 | "404": {
317 | "description": "Pet not found"
318 | }
319 | },
320 | "security": [
321 | {
322 | "petstore_auth": [
323 | "write:pets",
324 | "read:pets"
325 | ]
326 | }
327 | ]
328 | }
329 | },
330 | "/pet/{petId}/uploadImage": {
331 | "post": {
332 | "tags": [
333 | "pet"
334 | ],
335 | "summary": "uploads an image",
336 | "operationId": "uploadFile",
337 | "consumes": [
338 | "multipart/form-data"
339 | ],
340 | "produces": [
341 | "application/json"
342 | ],
343 | "parameters": [
344 | {
345 | "name": "petId",
346 | "in": "path",
347 | "description": "ID of pet to update",
348 | "required": true,
349 | "type": "string",
350 | "format": "string"
351 | },
352 | {
353 | "name": "additionalMetadata",
354 | "in": "formData",
355 | "description": "Additional data to pass to server",
356 | "required": false,
357 | "type": "string",
358 | "format": "string"
359 | },
360 | {
361 | "name": "file",
362 | "in": "formData",
363 | "description": "file to upload",
364 | "required": false,
365 | "type": "file"
366 | }
367 | ],
368 | "responses": {
369 | "200": {
370 | "description": "successful operation",
371 | "schema": {
372 | "$ref": "#/definitions/ApiResponse"
373 | }
374 | }
375 | },
376 | "security": [
377 | {
378 | "petstore_auth": [
379 | "write:pets",
380 | "read:pets"
381 | ]
382 | }
383 | ]
384 | }
385 | },
386 | "/store/inventory": {
387 | "get": {
388 | "tags": [
389 | "store"
390 | ],
391 | "summary": "Returns pet inventories by status",
392 | "description": "Returns a map of status codes to quantities",
393 | "operationId": "getInventory",
394 | "produces": [
395 | "application/json"
396 | ],
397 | "responses": {
398 | "200": {
399 | "description": "successful operation",
400 | "schema": {
401 | "type": "object",
402 | "additionalProperties": {
403 | "type": "integer",
404 | "format": "int32"
405 | }
406 | }
407 | }
408 | },
409 | "security": [
410 | {
411 | "api_key": []
412 | }
413 | ]
414 | }
415 | },
416 | "/store/order": {
417 | "post": {
418 | "tags": [
419 | "store"
420 | ],
421 | "summary": "Place an order for a pet",
422 | "operationId": "placeOrder",
423 | "parameters": [
424 | {
425 | "name": "body",
426 | "in": "body",
427 | "description": "order placed for purchasing the pet",
428 | "required": true,
429 | "schema": {
430 | "$ref": "#/definitions/Order"
431 | }
432 | }
433 | ],
434 | "responses": {
435 | "200": {
436 | "description": "successful operation",
437 | "schema": {
438 | "$ref": "#/definitions/Order"
439 | }
440 | },
441 | "400": {
442 | "description": "Invalid Order"
443 | }
444 | }
445 | }
446 | },
447 | "/store/order/{orderId}": {
448 | "get": {
449 | "tags": [
450 | "store"
451 | ],
452 | "summary": "Find purchase order by ID",
453 | "description": "For valid response try integer IDs with value \u003e= 1 and \u003c= 10. Other values will generated exceptions",
454 | "operationId": "getOrderById",
455 | "parameters": [
456 | {
457 | "name": "orderId",
458 | "in": "path",
459 | "description": "ID of pet that needs to be fetched",
460 | "required": true,
461 | "type": "integer",
462 | "format": "int64",
463 | "maximum": 10,
464 | "minimum": 1
465 | }
466 | ],
467 | "responses": {
468 | "200": {
469 | "description": "successful operation",
470 | "schema": {
471 | "$ref": "#/definitions/Order"
472 | }
473 | },
474 | "400": {
475 | "description": "Invalid ID supplied"
476 | },
477 | "404": {
478 | "description": "Order not found"
479 | }
480 | }
481 | },
482 | "delete": {
483 | "tags": [
484 | "store"
485 | ],
486 | "summary": "Delete purchase order by ID",
487 | "description": "For valid response try integer IDs with positive integer value. Negative or non-integer values will generate API errors",
488 | "operationId": "deleteOrder",
489 | "parameters": [
490 | {
491 | "name": "orderId",
492 | "in": "path",
493 | "description": "ID of the order that needs to be deleted",
494 | "required": true,
495 | "type": "integer",
496 | "format": "int64",
497 | "minimum": 1
498 | }
499 | ],
500 | "responses": {
501 | "400": {
502 | "description": "Invalid ID supplied"
503 | },
504 | "404": {
505 | "description": "Order not found"
506 | }
507 | }
508 | }
509 | },
510 | "/user": {
511 | "post": {
512 | "tags": [
513 | "user"
514 | ],
515 | "summary": "Create user",
516 | "description": "This can only be done by the logged in user.",
517 | "operationId": "createUser",
518 | "parameters": [
519 | {
520 | "name": "body",
521 | "in": "body",
522 | "description": "Created user object",
523 | "required": true,
524 | "schema": {
525 | "$ref": "#/definitions/User"
526 | }
527 | }
528 | ],
529 | "responses": {
530 | "default": {
531 | "description": "successful operation"
532 | }
533 | }
534 | }
535 | },
536 | "/user/createWithArray": {
537 | "post": {
538 | "tags": [
539 | "user"
540 | ],
541 | "summary": "Creates list of users with given input array",
542 | "operationId": "createUsersWithArrayInput",
543 | "consumes": [
544 | "application/json",
545 | "application/xml"
546 | ],
547 | "parameters": [
548 | {
549 | "name": "body",
550 | "in": "body",
551 | "description": "List of user object",
552 | "required": true,
553 | "schema": {
554 | "type": "array",
555 | "items": {
556 | "$ref": "#/definitions/User"
557 | }
558 | }
559 | }
560 | ],
561 | "responses": {
562 | "default": {
563 | "description": "successful operation"
564 | }
565 | }
566 | }
567 | },
568 | "/user/createWithList": {
569 | "post": {
570 | "tags": [
571 | "user"
572 | ],
573 | "summary": "Creates list of users with given input array",
574 | "operationId": "createUsersWithListInput",
575 | "parameters": [
576 | {
577 | "name": "body",
578 | "in": "body",
579 | "description": "List of user object",
580 | "required": true,
581 | "schema": {
582 | "type": "array",
583 | "items": {
584 | "$ref": "#/definitions/User"
585 | }
586 | }
587 | }
588 | ],
589 | "responses": {
590 | "default": {
591 | "description": "successful operation"
592 | }
593 | }
594 | }
595 | },
596 | "/user/login": {
597 | "get": {
598 | "tags": [
599 | "user"
600 | ],
601 | "summary": "Logs user into the system",
602 | "operationId": "loginUser",
603 | "parameters": [
604 | {
605 | "name": "username",
606 | "in": "query",
607 | "description": "The user name for login",
608 | "required": true,
609 | "type": "string",
610 | "format": "string"
611 | },
612 | {
613 | "name": "password",
614 | "in": "query",
615 | "description": "The password for login in clear text",
616 | "required": true,
617 | "type": "string",
618 | "format": "string"
619 | }
620 | ],
621 | "responses": {
622 | "200": {
623 | "description": "successful operation",
624 | "schema": {
625 | "type": "string",
626 | "format": "string"
627 | },
628 | "headers": {
629 | "X-Expires-After": {
630 | "description": "date in UTC when token expires",
631 | "type": "string",
632 | "format": "date-time"
633 | },
634 | "X-Rate-Limit": {
635 | "description": "calls per hour allowed by the user",
636 | "type": "integer",
637 | "format": "int32"
638 | }
639 | }
640 | },
641 | "400": {
642 | "description": "Invalid username/password supplied"
643 | }
644 | }
645 | }
646 | },
647 | "/user/logout": {
648 | "get": {
649 | "tags": [
650 | "user"
651 | ],
652 | "summary": "Logs out current logged in user session",
653 | "operationId": "logoutUser",
654 | "responses": {
655 | "default": {
656 | "description": "successful operation"
657 | }
658 | }
659 | }
660 | },
661 | "/user/{username}": {
662 | "get": {
663 | "tags": [
664 | "user"
665 | ],
666 | "summary": "Get user by user name",
667 | "operationId": "getUserByName",
668 | "parameters": [
669 | {
670 | "name": "username",
671 | "in": "path",
672 | "description": "The name that needs to be fetched. Use user1 for testing. ",
673 | "required": true,
674 | "type": "string",
675 | "format": "string"
676 | }
677 | ],
678 | "responses": {
679 | "200": {
680 | "description": "successful operation",
681 | "schema": {
682 | "$ref": "#/definitions/User"
683 | }
684 | },
685 | "400": {
686 | "description": "Invalid username supplied"
687 | },
688 | "404": {
689 | "description": "User not found"
690 | }
691 | }
692 | },
693 | "put": {
694 | "tags": [
695 | "user"
696 | ],
697 | "summary": "Updated user",
698 | "description": "This can only be done by the logged in user.",
699 | "operationId": "updateUser",
700 | "parameters": [
701 | {
702 | "name": "username",
703 | "in": "path",
704 | "description": "name that need to be updated",
705 | "required": true,
706 | "type": "string",
707 | "format": "string"
708 | },
709 | {
710 | "name": "body",
711 | "in": "body",
712 | "description": "Updated user object",
713 | "required": true,
714 | "schema": {
715 | "$ref": "#/definitions/User"
716 | }
717 | }
718 | ],
719 | "responses": {
720 | "400": {
721 | "description": "Invalid user supplied"
722 | },
723 | "404": {
724 | "description": "User not found"
725 | }
726 | }
727 | },
728 | "delete": {
729 | "tags": [
730 | "user"
731 | ],
732 | "summary": "Delete user",
733 | "description": "This can only be done by the logged in user.",
734 | "operationId": "deleteUser",
735 | "parameters": [
736 | {
737 | "name": "username",
738 | "in": "path",
739 | "description": "The name that needs to be deleted",
740 | "required": true,
741 | "type": "string",
742 | "format": "string"
743 | }
744 | ],
745 | "responses": {
746 | "400": {
747 | "description": "Invalid username supplied"
748 | },
749 | "404": {
750 | "description": "User not found"
751 | }
752 | }
753 | }
754 | }
755 | },
756 | "definitions": {
757 | "ApiResponse": {
758 | "type": "object",
759 | "properties": {
760 | "code": {
761 | "type": "integer",
762 | "xml": {
763 | "name": "Code"
764 | },
765 | "format": "int32"
766 | },
767 | "message": {
768 | "type": "string",
769 | "xml": {
770 | "name": "Message"
771 | },
772 | "format": "string"
773 | },
774 | "type": {
775 | "type": "string",
776 | "xml": {
777 | "name": "Type"
778 | },
779 | "format": "string"
780 | }
781 | },
782 | "xml": {
783 | "name": "ApiResponse"
784 | }
785 | },
786 | "Category": {
787 | "type": "object",
788 | "properties": {
789 | "id": {
790 | "type": "integer",
791 | "xml": {
792 | "name": "Id"
793 | },
794 | "format": "int64"
795 | },
796 | "name": {
797 | "type": "string",
798 | "xml": {
799 | "name": "Name"
800 | },
801 | "format": "string"
802 | }
803 | },
804 | "xml": {
805 | "name": "Category"
806 | }
807 | },
808 | "Order": {
809 | "type": "object",
810 | "properties": {
811 | "complete": {
812 | "type": "boolean",
813 | "default": false,
814 | "xml": {
815 | "name": "Complete"
816 | },
817 | "format": "boolean"
818 | },
819 | "id": {
820 | "type": "integer",
821 | "xml": {
822 | "name": "Id"
823 | },
824 | "format": "int64"
825 | },
826 | "petId": {
827 | "type": "integer",
828 | "xml": {
829 | "name": "PetId"
830 | },
831 | "format": "int64"
832 | },
833 | "quantity": {
834 | "type": "integer",
835 | "xml": {
836 | "name": "Quantity"
837 | },
838 | "format": "int64"
839 | },
840 | "shipDate": {
841 | "type": "string",
842 | "xml": {
843 | "name": "ShipDate"
844 | },
845 | "format": "date-time"
846 | },
847 | "status": {
848 | "type": "string",
849 | "description": "Order Status",
850 | "xml": {
851 | "name": "Status"
852 | },
853 | "enum": [
854 | "placed",
855 | "approved",
856 | "delivered"
857 | ],
858 | "format": "string"
859 | }
860 | },
861 | "xml": {
862 | "name": "Order"
863 | }
864 | },
865 | "Pet": {
866 | "type": "object",
867 | "properties": {
868 | "category": {
869 | "$ref": "#/definitions/Category"
870 | },
871 | "id": {
872 | "type": "integer",
873 | "xml": {
874 | "name": "Id"
875 | },
876 | "format": "int64"
877 | },
878 | "name": {
879 | "type": "string",
880 | "example": "doggie",
881 | "xml": {
882 | "name": "Name"
883 | },
884 | "format": "string"
885 | },
886 | "photoUrls": {
887 | "type": "array",
888 | "items": {
889 | "type": "string",
890 | "format": "string"
891 | },
892 | "xml": {
893 | "name": "photoUrl",
894 | "wrapped": true
895 | }
896 | },
897 | "status": {
898 | "type": "string",
899 | "description": "pet status in the store",
900 | "xml": {
901 | "name": "Status"
902 | },
903 | "enum": [
904 | "available",
905 | "pending",
906 | "sold"
907 | ],
908 | "format": "string"
909 | },
910 | "tags": {
911 | "type": "array",
912 | "items": {
913 | "$ref": "#/definitions/Tag"
914 | },
915 | "xml": {
916 | "name": "tag",
917 | "wrapped": true
918 | }
919 | }
920 | },
921 | "xml": {
922 | "name": "Pet"
923 | },
924 | "required": [
925 | "name",
926 | "photoUrls"
927 | ]
928 | },
929 | "Tag": {
930 | "type": "object",
931 | "properties": {
932 | "id": {
933 | "type": "integer",
934 | "xml": {
935 | "name": "Id"
936 | },
937 | "format": "int64"
938 | },
939 | "name": {
940 | "type": "string",
941 | "xml": {
942 | "name": "Name"
943 | },
944 | "format": "string"
945 | }
946 | },
947 | "xml": {
948 | "name": "Tag"
949 | }
950 | },
951 | "User": {
952 | "type": "object",
953 | "properties": {
954 | "email": {
955 | "type": "string",
956 | "xml": {
957 | "name": "Email"
958 | },
959 | "format": "string"
960 | },
961 | "firstname": {
962 | "type": "string",
963 | "xml": {
964 | "name": "FirstName"
965 | },
966 | "format": "string"
967 | },
968 | "id": {
969 | "type": "integer",
970 | "xml": {
971 | "name": "Id"
972 | },
973 | "format": "int64"
974 | },
975 | "lastname": {
976 | "type": "string",
977 | "xml": {
978 | "name": "LastName"
979 | },
980 | "format": "string"
981 | },
982 | "password": {
983 | "type": "string",
984 | "xml": {
985 | "name": "Password"
986 | },
987 | "format": "string"
988 | },
989 | "phone": {
990 | "type": "string",
991 | "xml": {
992 | "name": "Phone"
993 | },
994 | "format": "string"
995 | },
996 | "userStatus": {
997 | "type": "integer",
998 | "description": "User Status",
999 | "xml": {
1000 | "name": "UserStatus"
1001 | },
1002 | "format": "int32"
1003 | },
1004 | "username": {
1005 | "type": "string",
1006 | "xml": {
1007 | "name": "UserName"
1008 | },
1009 | "format": "string"
1010 | }
1011 | },
1012 | "xml": {
1013 | "name": "Users"
1014 | }
1015 | }
1016 | },
1017 | "securityDefinitions": {
1018 | "api_key": {
1019 | "type": "apiKey",
1020 | "name": "api_key",
1021 | "in": "header"
1022 | },
1023 | "petstore_auth": {
1024 | "type": "oauth2",
1025 | "flow": "implicit",
1026 | "authorizationUrl": "http://petstore.swagger.io/oauth/dialog",
1027 | "scopes": {
1028 | "read:pets": "read your pets",
1029 | "write:pets": "modify pets in your account"
1030 | }
1031 | }
1032 | },
1033 | "tags": [
1034 | {
1035 | "name": "pet",
1036 | "description": "Everything about your Pets",
1037 | "externalDocs": {
1038 | "description": "Find out more",
1039 | "url": "http://swagger.io"
1040 | }
1041 | },
1042 | {
1043 | "name": "store",
1044 | "description": "Access to Petstore orders"
1045 | },
1046 | {
1047 | "name": "user",
1048 | "description": "Operations about user",
1049 | "externalDocs": {
1050 | "description": "Find out more about our store",
1051 | "url": "http://swagger.io"
1052 | }
1053 | }
1054 | ],
1055 | "externalDocs": {
1056 | "description": "Find out more about Swagger",
1057 | "url": "http://swagger.io"
1058 | }
1059 | }
--------------------------------------------------------------------------------