#7 Fix relative path & add set scheme function

This commit is contained in:
ElvinChan 2018-09-16 16:35:29 +08:00 committed by ElvinChan
parent e37e18e720
commit 3127d329c8
13 changed files with 190 additions and 55 deletions

View File

@ -31,7 +31,7 @@ import (
func main() { func main() {
// ApiRoot with Echo instance // ApiRoot with Echo instance
r := echoswagger.New(echo.New(), "/v1", "doc/", nil) r := echoswagger.New(echo.New(), "", "doc/", nil)
// Routes with parameters & responses // Routes with parameters & responses
r.POST("/", createUser). r.POST("/", createUser).
@ -58,12 +58,15 @@ func createUser(c echo.Context) error {
``` ```
r := echoswagger.New(echo.New(), "/v1", "doc/", nil) 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: 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). r.AddSecurityAPIKey("JWT", "JWT Token", echoswagger.SecurityInHeader).
SetRequestContentType("application/x-www-form-urlencoded", "multipart/form-data"). SetRequestContentType("application/x-www-form-urlencoded", "multipart/form-data").
SetUI(UISetting{HideTop: true}) SetUI(UISetting{HideTop: true}).
SetScheme("https", "http")
``` ```
- Get `echo.Echo` instance. - Get `echo.Echo` instance.
``` ```

View File

@ -31,7 +31,7 @@ import (
func main() { func main() {
// ApiRoot with Echo instance // ApiRoot with Echo instance
r := echoswagger.New(echo.New(), "/v1", "doc/", nil) r := echoswagger.New(echo.New(), "", "doc/", nil)
// Routes with parameters & responses // Routes with parameters & responses
r.POST("/", createUser). r.POST("/", createUser).
@ -58,12 +58,15 @@ func createUser(c echo.Context) error {
``` ```
r := echoswagger.New(echo.New(), "/v1", "doc/", nil) 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`来: 你可以用这个`ApiRoot`来:
- 设置Security定义, 请求/响应Content-TypeUI选项等。 - 设置Security定义, 请求/响应Content-TypeUI选项Scheme等。
``` ```
r.AddSecurityAPIKey("JWT", "JWT Token", echoswagger.SecurityInHeader). r.AddSecurityAPIKey("JWT", "JWT Token", echoswagger.SecurityInHeader).
SetRequestContentType("application/x-www-form-urlencoded", "multipart/form-data"). SetRequestContentType("application/x-www-form-urlencoded", "multipart/form-data").
SetUI(UISetting{HideTop: true}) SetUI(UISetting{HideTop: true}).
SetScheme("https", "http")
``` ```
- 获取`echo.Echo`实例。 - 获取`echo.Echo`实例。
``` ```

View File

@ -1,7 +1,7 @@
package echoswagger package echoswagger
// CDN refer to https://www.jsdelivr.com/package/npm/swagger-ui-dist // 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"}} const SwaggerUIContent = `{{define "swagger"}}
<!DOCTYPE html> <!DOCTYPE html>
@ -50,7 +50,7 @@ const SwaggerUIContent = `{{define "swagger"}}
// Build a system // Build a system
const ui = SwaggerUIBundle({ const ui = SwaggerUIBundle({
url: {{.url}}, url: this.window.location.origin+{{.path}},
dom_id: '#swagger-ui', dom_id: '#swagger-ui',
deepLinking: true, deepLinking: true,
presets: [ presets: [

View File

@ -54,14 +54,7 @@ func toSwaggerPath(path string) string {
for _, name := range params { for _, name := range params {
path = strings.Replace(path, ":"+name, "{"+name+"}", 1) path = strings.Replace(path, ":"+name, "{"+name+"}", 1)
} }
return proccessPath(path) return connectPath(path)
}
func proccessPath(path string) string {
if len(path) == 0 || path[0] != '/' {
path = "/" + path
}
return path
} }
func converter(t reflect.Type) func(s string) (interface{}, error) { func converter(t reflect.Type) func(s string) (interface{}, error) {

View File

@ -13,7 +13,7 @@ func main() {
func initServer() echoswagger.ApiRoot { func initServer() echoswagger.ApiRoot {
e := echo.New() e := echo.New()
se := echoswagger.New(e, "/v2", "doc/", &echoswagger.Info{ se := echoswagger.New(e, "", "doc/", &echoswagger.Info{
Title: "Swagger Petstore", 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.", 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", Version: "1.0.0",
@ -36,7 +36,8 @@ func initServer() echoswagger.ApiRoot {
se.SetExternalDocs("Find out more about Swagger", "http://swagger.io"). se.SetExternalDocs("Find out more about Swagger", "http://swagger.io").
SetResponseContentType("application/xml", "application/json"). SetResponseContentType("application/xml", "application/json").
SetUI(echoswagger.UISetting{HideTop: true}) SetUI(echoswagger.UISetting{HideTop: true}).
SetScheme("https", "http")
PetController{}.Init(se.Group("pet", "/pet")) PetController{}.Init(se.Group("pet", "/pet"))
StoreController{}.Init(se.Group("store", "/store")) StoreController{}.Init(se.Group("store", "/store"))

View File

@ -14,8 +14,9 @@
"version": "1.0.0" "version": "1.0.0"
}, },
"host": "example.com", "host": "example.com",
"basePath": "/v2", "basePath": "/",
"schemes": [ "schemes": [
"https",
"http" "http"
], ],
"produces": [ "produces": [

View File

@ -31,21 +31,21 @@ type RawDefine struct {
Schema *JSONSchema 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) t, err := template.New("swagger").Parse(SwaggerUIContent)
if err != nil { if err != nil {
panic(err) panic(err)
} }
return func(c echo.Context) error { return func(c echo.Context) error {
cdn := DefaultCDN cdn := r.ui.CDN
if r.ui.CDN != "" { if cdn == "" {
cdn = r.ui.CDN cdn = DefaultCDN
} }
buf := new(bytes.Buffer) buf := new(bytes.Buffer)
t.Execute(buf, map[string]interface{}{ t.Execute(buf, map[string]interface{}{
"title": r.spec.Info.Title, "title": r.spec.Info.Title,
"url": c.Scheme() + "://" + c.Request().Host + swaggerPath,
"hideTop": r.ui.HideTop, "hideTop": r.ui.HideTop,
"path": realSpecPath,
"cdn": cdn, "cdn": cdn,
}) })
return c.HTMLBlob(http.StatusOK, buf.Bytes()) return c.HTMLBlob(http.StatusOK, buf.Bytes())

View File

@ -28,7 +28,6 @@ func (r *Root) genSpec(c echo.Context) error {
r.spec.Swagger = SwaggerVersion r.spec.Swagger = SwaggerVersion
r.spec.Paths = make(map[string]interface{}) r.spec.Paths = make(map[string]interface{})
r.spec.Host = c.Request().Host r.spec.Host = c.Request().Host
r.spec.Schemes = []string{c.Scheme()}
for i := range r.groups { for i := range r.groups {
group := &r.groups[i] group := &r.groups[i]

View File

@ -19,7 +19,7 @@ func TestSpec(t *testing.T) {
req := httptest.NewRequest(echo.GET, "/doc/swagger.json", nil) req := httptest.NewRequest(echo.GET, "/doc/swagger.json", nil)
rec := httptest.NewRecorder() rec := httptest.NewRecorder()
c := e.NewContext(req, rec) 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)) { if assert.NoError(t, r.(*Root).Spec(c)) {
assert.Equal(t, http.StatusOK, rec.Code) assert.Equal(t, http.StatusOK, rec.Code)
assert.JSONEq(t, j, rec.Body.String()) 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) req := httptest.NewRequest(echo.GET, "/doc/swagger.json", nil)
rec := httptest.NewRecorder() rec := httptest.NewRecorder()
c := e.NewContext(req, rec) 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)) { if assert.NoError(t, r.(*Root).Spec(c)) {
assert.Equal(t, http.StatusOK, rec.Code) assert.Equal(t, http.StatusOK, rec.Code)
assert.JSONEq(t, j, rec.Body.String()) assert.JSONEq(t, j, rec.Body.String())

View File

@ -64,3 +64,24 @@ LoopType:
} }
return t 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
}

View File

@ -112,3 +112,10 @@ func isBasicType(t reflect.Type) bool {
} }
return false return false
} }
func isValidScheme(s string) bool {
if s == "http" || s == "https" || s == "ws" || s == "wss" {
return true
}
return false
}

View File

@ -15,9 +15,8 @@ TODO:
Notice: Notice:
1.不会对Email和URL进行验证因为不影响页面的正常显示 1.不会对Email和URL进行验证因为不影响页面的正常显示
2.只支持对应于SwaggerUI页面的Schema不支持swsww等协议 2.SetSecurity/SetSecurityWithScope 传多个参数表示Security之间是AND关系多次调用SetSecurity/SetSecurityWithScope Security之间是OR关系
3.SetSecurity/SetSecurityWithScope 传多个参数表示Security之间是AND关系多次调用SetSecurity/SetSecurityWithScope Security之间是OR关系 3.只支持基本类型的Map Key
4.只支持基本类型的Map Key
*/ */
type ApiRouter interface { type ApiRouter interface {
@ -70,6 +69,9 @@ type ApiRoot interface {
// SetUI sets UI setting. // SetUI sets UI setting.
SetUI(ui UISetting) ApiRoot SetUI(ui UISetting) ApiRoot
// SetScheme sets available protocol schemes.
SetScheme(schemes ...string) ApiRoot
// GetRaw returns raw `Swagger`. Only special case should use. // GetRaw returns raw `Swagger`. Only special case should use.
GetRaw() *Swagger GetRaw() *Swagger
@ -211,14 +213,6 @@ func New(e *echo.Echo, basePath, docPath string, i *Info) ApiRoot {
if e == nil { if e == nil {
panic("echoswagger: invalid Echo instance") 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 { if i == nil {
i = &Info{ i = &Info{
@ -231,7 +225,7 @@ func New(e *echo.Echo, basePath, docPath string, i *Info) ApiRoot {
spec: &Swagger{ spec: &Swagger{
Info: i, Info: i,
SecurityDefinitions: make(map[string]*SecurityDefinition), SecurityDefinitions: make(map[string]*SecurityDefinition),
BasePath: basePath, BasePath: connectPath(basePath),
Definitions: make(map[string]*JSONSchema), Definitions: make(map[string]*JSONSchema),
}, },
routers: routers{ 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) e.GET(specPath, r.Spec)
return r return r
} }
@ -365,6 +361,16 @@ func (r *Root) SetUI(ui UISetting) ApiRoot {
return r 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 { func (r *Root) GetRaw() *Swagger {
return r.spec return r.spec
} }

View File

@ -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) { func TestGroup(t *testing.T) {
r := prepareApiRoot() r := prepareApiRoot()
t.Run("Normal", func(t *testing.T) { t.Run("Normal", func(t *testing.T) {
@ -292,24 +366,51 @@ func TestAddResponse(t *testing.T) {
} }
func TestUI(t *testing.T) { func TestUI(t *testing.T) {
r := New(echo.New(), "/", "doc/", nil) t.Run("DefaultCDN", func(t *testing.T) {
se := r.(*Root) r := New(echo.New(), "/", "doc/", nil)
req := httptest.NewRequest(echo.GET, "/doc/", nil) se := r.(*Root)
rec := httptest.NewRecorder() req := httptest.NewRequest(echo.GET, "/doc/", nil)
c := se.echo.NewContext(req, rec) rec := httptest.NewRecorder()
h := se.docHandler("/doc/swagger.json") c := se.echo.NewContext(req, rec)
h := se.docHandler("/doc/swagger.json")
cdn := "https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/3.18.0" if assert.NoError(t, h(c)) {
r.SetUI(UISetting{ assert.Equal(t, http.StatusOK, rec.Code)
HideTop: true, assert.Contains(t, rec.Body.String(), DefaultCDN)
CDN: cdn, }
}) })
if assert.NoError(t, h(c)) { t.Run("SetUI", func(t *testing.T) {
assert.Equal(t, http.StatusOK, rec.Code) r := New(echo.New(), "/", "doc/", nil)
assert.Contains(t, rec.Body.String(), cdn) se := r.(*Root)
assert.Contains(t, rec.Body.String(), "#swagger-ui>.swagger-container>.topbar") 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) { func TestRaw(t *testing.T) {