Merge pull request #8 from elvinchan/7

#7 Fix relative path & add set scheme function
This commit is contained in:
陈文强 2018-09-17 09:09:38 +08:00 committed by GitHub
commit 0e96d943fb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 190 additions and 55 deletions

View File

@ -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.
```

View File

@ -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-TypeUI选项等。
- 设置Security定义, 请求/响应Content-TypeUI选项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`实例。
```

View File

@ -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"}}
<!DOCTYPE html>
@ -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: [

View File

@ -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) {

View File

@ -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"))

View File

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

View File

@ -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())

View File

@ -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]

View File

@ -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())

View File

@ -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
}

View File

@ -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
}

View File

@ -15,9 +15,8 @@ TODO:
Notice:
1.不会对Email和URL进行验证因为不影响页面的正常显示
2.只支持对应于SwaggerUI页面的Schema不支持swsww等协议
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
}

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) {
r := prepareApiRoot()
t.Run("Normal", func(t *testing.T) {
@ -292,6 +366,21 @@ func TestAddResponse(t *testing.T) {
}
func TestUI(t *testing.T) {
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")
if assert.NoError(t, h(c)) {
assert.Equal(t, http.StatusOK, rec.Code)
assert.Contains(t, rec.Body.String(), DefaultCDN)
}
})
t.Run("SetUI", func(t *testing.T) {
r := New(echo.New(), "/", "doc/", nil)
se := r.(*Root)
req := httptest.NewRequest(echo.GET, "/doc/", nil)
@ -310,6 +399,18 @@ func TestUI(t *testing.T) {
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) {