diff --git a/README.md b/README.md index 07a67cd..c20d244 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ import ( func main() { // ApiRoot with Echo instance - r := echoswagger.New(echo.New(), "/v1", "doc/", nil) + r := echoswagger.New(echo.New(), "", "doc/", nil) // Routes with parameters & responses r.POST("/", createUser). @@ -58,12 +58,15 @@ func createUser(c echo.Context) error { ``` r := echoswagger.New(echo.New(), "/v1", "doc/", nil) ``` +> Note: The parameter `basePath` is generally used when the access root path is not the root directory of the website after application is deployed. For example, the URL of an API in the program running locally is: `http://localhost:1323/users`, the actual URL after deployed to server is: `https://www.xxx.com/legacy-api/users`, then, when running locally, `basePath` should be `/`, when running on server, `basePath` should be `/legacy-api`. + You can use the result `ApiRoot` instance to: -- Setup Security definitions, request/response Content-Types, UI options, etc. +- Setup Security definitions, request/response Content-Types, UI options, Scheme, etc. ``` r.AddSecurityAPIKey("JWT", "JWT Token", echoswagger.SecurityInHeader). SetRequestContentType("application/x-www-form-urlencoded", "multipart/form-data"). - SetUI(UISetting{HideTop: true}) + SetUI(UISetting{HideTop: true}). + SetScheme("https", "http") ``` - Get `echo.Echo` instance. ``` diff --git a/README_zh-CN.md b/README_zh-CN.md index 11a1da1..cbd3f5e 100644 --- a/README_zh-CN.md +++ b/README_zh-CN.md @@ -31,7 +31,7 @@ import ( func main() { // ApiRoot with Echo instance - r := echoswagger.New(echo.New(), "/v1", "doc/", nil) + r := echoswagger.New(echo.New(), "", "doc/", nil) // Routes with parameters & responses r.POST("/", createUser). @@ -58,12 +58,15 @@ func createUser(c echo.Context) error { ``` r := echoswagger.New(echo.New(), "/v1", "doc/", nil) ``` +> 注意:参数`basePath`一般用于程序部署后访问路径并非网站根目录时的情况,比如程序运行在本地的某个API的URL为:`http://localhost:1323/users`,部署至服务器后的实际URL为:`https://www.xxx.com/legacy-api/users`,则本地运行时,`basePath`应该传入`/`, 部署至服务器时,`basePath`应该传入`/legacy-api`。 + 你可以用这个`ApiRoot`来: -- 设置Security定义, 请求/响应Content-Type,UI选项,等。 +- 设置Security定义, 请求/响应Content-Type,UI选项,Scheme等。 ``` r.AddSecurityAPIKey("JWT", "JWT Token", echoswagger.SecurityInHeader). SetRequestContentType("application/x-www-form-urlencoded", "multipart/form-data"). - SetUI(UISetting{HideTop: true}) + SetUI(UISetting{HideTop: true}). + SetScheme("https", "http") ``` - 获取`echo.Echo`实例。 ``` diff --git a/assets.go b/assets.go index 731c455..a9938dd 100644 --- a/assets.go +++ b/assets.go @@ -1,7 +1,7 @@ package echoswagger // CDN refer to https://www.jsdelivr.com/package/npm/swagger-ui-dist -const DefaultCDN = "https://cdn.jsdelivr.net/npm/swagger-ui-dist@3.18.3-republish2" +const DefaultCDN = "https://cdn.jsdelivr.net/npm/swagger-ui-dist@3.19.0" const SwaggerUIContent = `{{define "swagger"}} @@ -50,7 +50,7 @@ const SwaggerUIContent = `{{define "swagger"}} // Build a system const ui = SwaggerUIBundle({ - url: {{.url}}, + url: this.window.location.origin+{{.path}}, dom_id: '#swagger-ui', deepLinking: true, presets: [ diff --git a/converter.go b/converter.go index 8f092a1..e20d357 100644 --- a/converter.go +++ b/converter.go @@ -54,14 +54,7 @@ func toSwaggerPath(path string) string { for _, name := range params { path = strings.Replace(path, ":"+name, "{"+name+"}", 1) } - return proccessPath(path) -} - -func proccessPath(path string) string { - if len(path) == 0 || path[0] != '/' { - path = "/" + path - } - return path + return connectPath(path) } func converter(t reflect.Type) func(s string) (interface{}, error) { diff --git a/examples/main.go b/examples/main.go index 75b164f..bcf09ec 100644 --- a/examples/main.go +++ b/examples/main.go @@ -13,7 +13,7 @@ func main() { func initServer() echoswagger.ApiRoot { e := echo.New() - se := echoswagger.New(e, "/v2", "doc/", &echoswagger.Info{ + se := echoswagger.New(e, "", "doc/", &echoswagger.Info{ Title: "Swagger Petstore", 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.", Version: "1.0.0", @@ -36,7 +36,8 @@ func initServer() echoswagger.ApiRoot { se.SetExternalDocs("Find out more about Swagger", "http://swagger.io"). SetResponseContentType("application/xml", "application/json"). - SetUI(echoswagger.UISetting{HideTop: true}) + SetUI(echoswagger.UISetting{HideTop: true}). + SetScheme("https", "http") PetController{}.Init(se.Group("pet", "/pet")) StoreController{}.Init(se.Group("store", "/store")) diff --git a/examples/swagger.json b/examples/swagger.json index 18d5691..60053bc 100644 --- a/examples/swagger.json +++ b/examples/swagger.json @@ -14,8 +14,9 @@ "version": "1.0.0" }, "host": "example.com", - "basePath": "/v2", + "basePath": "/", "schemes": [ + "https", "http" ], "produces": [ diff --git a/internal.go b/internal.go index f31db99..0a85e30 100644 --- a/internal.go +++ b/internal.go @@ -31,21 +31,21 @@ type RawDefine struct { Schema *JSONSchema } -func (r *Root) docHandler(swaggerPath string) echo.HandlerFunc { +func (r *Root) docHandler(realSpecPath string) echo.HandlerFunc { t, err := template.New("swagger").Parse(SwaggerUIContent) if err != nil { panic(err) } return func(c echo.Context) error { - cdn := DefaultCDN - if r.ui.CDN != "" { - cdn = r.ui.CDN + cdn := r.ui.CDN + if cdn == "" { + cdn = DefaultCDN } buf := new(bytes.Buffer) t.Execute(buf, map[string]interface{}{ "title": r.spec.Info.Title, - "url": c.Scheme() + "://" + c.Request().Host + swaggerPath, "hideTop": r.ui.HideTop, + "path": realSpecPath, "cdn": cdn, }) return c.HTMLBlob(http.StatusOK, buf.Bytes()) diff --git a/spec.go b/spec.go index 2082d24..680bc8b 100644 --- a/spec.go +++ b/spec.go @@ -28,7 +28,6 @@ func (r *Root) genSpec(c echo.Context) error { r.spec.Swagger = SwaggerVersion r.spec.Paths = make(map[string]interface{}) r.spec.Host = c.Request().Host - r.spec.Schemes = []string{c.Scheme()} for i := range r.groups { group := &r.groups[i] diff --git a/spec_test.go b/spec_test.go index c21c633..df65ed1 100644 --- a/spec_test.go +++ b/spec_test.go @@ -19,7 +19,7 @@ func TestSpec(t *testing.T) { req := httptest.NewRequest(echo.GET, "/doc/swagger.json", nil) rec := httptest.NewRecorder() c := e.NewContext(req, rec) - j := `{"swagger":"2.0","info":{"title":"Project APIs","version":""},"host":"example.com","basePath":"/","schemes":["http"],"paths":{}}` + j := `{"swagger":"2.0","info":{"title":"Project APIs","version":""},"host":"example.com","basePath":"/","paths":{}}` if assert.NoError(t, r.(*Root).Spec(c)) { assert.Equal(t, http.StatusOK, rec.Code) assert.JSONEq(t, j, rec.Body.String()) @@ -94,7 +94,7 @@ func TestSpec(t *testing.T) { req := httptest.NewRequest(echo.GET, "/doc/swagger.json", nil) rec := httptest.NewRecorder() c := e.NewContext(req, rec) - j := `{"swagger":"2.0","info":{"title":"Project APIs","version":""},"host":"example.com","basePath":"/","schemes":["http"],"paths":{"/ping":{"get":{"responses":{"default":{"description":"successful operation"}}}},"/users/{id}":{"delete":{"tags":["Users"],"responses":{"default":{"description":"successful operation"}}}}},"tags":[{"name":"Users"}]}` + j := `{"swagger":"2.0","info":{"title":"Project APIs","version":""},"host":"example.com","basePath":"/","paths":{"/ping":{"get":{"responses":{"default":{"description":"successful operation"}}}},"/users/{id}":{"delete":{"tags":["Users"],"responses":{"default":{"description":"successful operation"}}}}},"tags":[{"name":"Users"}]}` if assert.NoError(t, r.(*Root).Spec(c)) { assert.Equal(t, http.StatusOK, rec.Code) assert.JSONEq(t, j, rec.Body.String()) diff --git a/utils.go b/utils.go index 2ac203c..a8a66a2 100644 --- a/utils.go +++ b/utils.go @@ -64,3 +64,24 @@ LoopType: } return t } + +// "" → "/" +// "/" → "/" +// "a" → "/a" +// "/a" → "/a" +// "/a/" → "/a/" +func connectPath(paths ...string) string { + var result string + for i, path := range paths { + // add prefix slash + if len(path) == 0 || path[0] != '/' { + path = "/" + path + } + // remove suffix slash, ignore last path + if i != len(paths)-1 && len(path) != 0 && path[len(path)-1] == '/' { + path = path[:len(path)-1] + } + result += path + } + return result +} diff --git a/validator.go b/validator.go index a86b97c..49fe5d1 100644 --- a/validator.go +++ b/validator.go @@ -112,3 +112,10 @@ func isBasicType(t reflect.Type) bool { } return false } + +func isValidScheme(s string) bool { + if s == "http" || s == "https" || s == "ws" || s == "wss" { + return true + } + return false +} diff --git a/wrapper.go b/wrapper.go index ab4b743..db9e5ff 100644 --- a/wrapper.go +++ b/wrapper.go @@ -15,9 +15,8 @@ TODO: Notice: 1.不会对Email和URL进行验证,因为不影响页面的正常显示 -2.只支持对应于SwaggerUI页面的Schema,不支持sw、sww等协议 -3.SetSecurity/SetSecurityWithScope 传多个参数表示Security之间是AND关系;多次调用SetSecurity/SetSecurityWithScope Security之间是OR关系 -4.只支持基本类型的Map Key +2.SetSecurity/SetSecurityWithScope 传多个参数表示Security之间是AND关系;多次调用SetSecurity/SetSecurityWithScope Security之间是OR关系 +3.只支持基本类型的Map Key */ type ApiRouter interface { @@ -70,6 +69,9 @@ type ApiRoot interface { // SetUI sets UI setting. SetUI(ui UISetting) ApiRoot + // SetScheme sets available protocol schemes. + SetScheme(schemes ...string) ApiRoot + // GetRaw returns raw `Swagger`. Only special case should use. GetRaw() *Swagger @@ -211,14 +213,6 @@ func New(e *echo.Echo, basePath, docPath string, i *Info) ApiRoot { if e == nil { panic("echoswagger: invalid Echo instance") } - basePath = proccessPath(basePath) - docPath = proccessPath(docPath) - - var connector string - if docPath[len(docPath)-1] != '/' { - connector = "/" - } - specPath := docPath + connector + "swagger.json" if i == nil { i = &Info{ @@ -231,7 +225,7 @@ func New(e *echo.Echo, basePath, docPath string, i *Info) ApiRoot { spec: &Swagger{ Info: i, SecurityDefinitions: make(map[string]*SecurityDefinition), - BasePath: basePath, + BasePath: connectPath(basePath), Definitions: make(map[string]*JSONSchema), }, routers: routers{ @@ -239,7 +233,9 @@ func New(e *echo.Echo, basePath, docPath string, i *Info) ApiRoot { }, } - e.GET(docPath, r.docHandler(specPath)) + specPath := connectPath(docPath, "swagger.json") + realSpecPath := connectPath(basePath, specPath) + e.GET(connectPath(docPath), r.docHandler(realSpecPath)) e.GET(specPath, r.Spec) return r } @@ -365,6 +361,16 @@ func (r *Root) SetUI(ui UISetting) ApiRoot { return r } +func (r *Root) SetScheme(schemes ...string) ApiRoot { + for _, s := range schemes { + if !isValidScheme(s) { + panic("echoswagger: invalid protocol scheme") + } + } + r.spec.Schemes = schemes + return r +} + func (r *Root) GetRaw() *Swagger { return r.spec } diff --git a/wrapper_test.go b/wrapper_test.go index 3cfc273..3d4ab53 100644 --- a/wrapper_test.go +++ b/wrapper_test.go @@ -95,6 +95,80 @@ func TestNew(t *testing.T) { } } +func TestPath(t *testing.T) { + tests := []struct { + baseInput, docInput string + baseOutput, docOutput, specOutput, realSpecOutput string + name string + }{ + { + baseInput: "/", + docInput: "doc/", + baseOutput: "/", + docOutput: "/doc/", + specOutput: "/doc/swagger.json", + realSpecOutput: "/doc/swagger.json", + name: "A", + }, { + baseInput: "", + docInput: "", + baseOutput: "/", + docOutput: "/", + specOutput: "/swagger.json", + realSpecOutput: "/swagger.json", + name: "B", + }, { + baseInput: "/omni-api", + docInput: "/doc", + baseOutput: "/omni-api", + docOutput: "/doc", + specOutput: "/doc/swagger.json", + realSpecOutput: "/omni-api/doc/swagger.json", + name: "C", + }, { + baseInput: "/omni-api/", + docInput: "", + baseOutput: "/omni-api/", + docOutput: "/", + specOutput: "/swagger.json", + realSpecOutput: "/omni-api/swagger.json", + name: "D", + }, { + baseInput: "/omni-api", + docInput: "doc/", + baseOutput: "/omni-api", + docOutput: "/doc/", + specOutput: "/doc/swagger.json", + realSpecOutput: "/omni-api/doc/swagger.json", + name: "F", + }, { + baseInput: "omni-api", + docInput: "doc/", + baseOutput: "/omni-api", + docOutput: "/doc/", + specOutput: "/doc/swagger.json", + realSpecOutput: "/omni-api/doc/swagger.json", + name: "G", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + apiRoot := New(echo.New(), tt.baseInput, tt.docInput, nil) + r := apiRoot.(*Root) + + assert.Equal(t, r.spec.BasePath, tt.baseOutput) + + assert.NotNil(t, r.echo) + assert.Len(t, r.echo.Routes(), 2) + res := r.echo.Routes() + paths := []string{res[0].Path, res[1].Path} + assert.ElementsMatch(t, paths, []string{tt.docOutput, tt.specOutput}) + + assert.Equal(t, tt.realSpecOutput, connectPath(tt.baseOutput, tt.specOutput)) + }) + } +} + func TestGroup(t *testing.T) { r := prepareApiRoot() t.Run("Normal", func(t *testing.T) { @@ -292,24 +366,51 @@ func TestAddResponse(t *testing.T) { } func TestUI(t *testing.T) { - r := New(echo.New(), "/", "doc/", nil) - se := r.(*Root) - req := httptest.NewRequest(echo.GET, "/doc/", nil) - rec := httptest.NewRecorder() - c := se.echo.NewContext(req, rec) - h := se.docHandler("/doc/swagger.json") + t.Run("DefaultCDN", func(t *testing.T) { + r := New(echo.New(), "/", "doc/", nil) + se := r.(*Root) + req := httptest.NewRequest(echo.GET, "/doc/", nil) + rec := httptest.NewRecorder() + c := se.echo.NewContext(req, rec) + h := se.docHandler("/doc/swagger.json") - cdn := "https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/3.18.0" - r.SetUI(UISetting{ - HideTop: true, - CDN: cdn, + if assert.NoError(t, h(c)) { + assert.Equal(t, http.StatusOK, rec.Code) + assert.Contains(t, rec.Body.String(), DefaultCDN) + } }) - if assert.NoError(t, h(c)) { - assert.Equal(t, http.StatusOK, rec.Code) - assert.Contains(t, rec.Body.String(), cdn) - assert.Contains(t, rec.Body.String(), "#swagger-ui>.swagger-container>.topbar") - } + t.Run("SetUI", func(t *testing.T) { + r := New(echo.New(), "/", "doc/", nil) + se := r.(*Root) + req := httptest.NewRequest(echo.GET, "/doc/", nil) + rec := httptest.NewRecorder() + c := se.echo.NewContext(req, rec) + h := se.docHandler("/doc/swagger.json") + + cdn := "https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/3.18.0" + r.SetUI(UISetting{ + HideTop: true, + CDN: cdn, + }) + + if assert.NoError(t, h(c)) { + assert.Equal(t, http.StatusOK, rec.Code) + assert.Contains(t, rec.Body.String(), cdn) + assert.Contains(t, rec.Body.String(), "#swagger-ui>.swagger-container>.topbar") + } + }) +} + +func TestScheme(t *testing.T) { + r := prepareApiRoot() + schemes := []string{"http", "https"} + r.SetScheme(schemes...) + assert.ElementsMatch(t, r.(*Root).spec.Schemes, schemes) + + assert.Panics(t, func() { + r.SetScheme("grpc") + }) } func TestRaw(t *testing.T) {