├── 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 | [![Ci](https://github.com/pangpanglabs/echoswagger/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/pangpanglabs/echoswagger/actions/workflows/ci.yml) 7 | [![Go Report Card](https://goreportcard.com/badge/github.com/pangpanglabs/echoswagger)](https://goreportcard.com/report/github.com/pangpanglabs/echoswagger) 8 | [![Go Reference](https://pkg.go.dev/badge/github.com/pangpanglabs/echoswagger.svg)](https://pkg.go.dev/github.com/pangpanglabs/echoswagger) 9 | [![codecov](https://codecov.io/gh/pangpanglabs/echoswagger/branch/master/graph/badge.svg)](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 | [![Ci](https://github.com/pangpanglabs/echoswagger/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/pangpanglabs/echoswagger/actions/workflows/ci.yml) 7 | [![Go Report Card](https://goreportcard.com/badge/github.com/pangpanglabs/echoswagger)](https://goreportcard.com/report/github.com/pangpanglabs/echoswagger) 8 | [![Go Reference](https://pkg.go.dev/badge/github.com/pangpanglabs/echoswagger.svg)](https://pkg.go.dev/github.com/pangpanglabs/echoswagger) 9 | [![codecov](https://codecov.io/gh/pangpanglabs/echoswagger/branch/master/graph/badge.svg)](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 | } --------------------------------------------------------------------------------