#19 Retrieve bathPath automatically

This commit is contained in:
elvinchan 2019-05-11 22:27:52 +08:00
parent 27beb50b64
commit cb60195b27
12 changed files with 150 additions and 125 deletions

View File

@ -56,10 +56,8 @@ func createUser(c echo.Context) error {
## Usage
#### Create a `ApiRoot` with `New()`, which is a wrapper of `echo.New()`
```
r := echoswagger.New(echo.New(), "/v1", "doc/", nil)
r := echoswagger.New(echo.New(), "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, Scheme, etc.
```

View File

@ -58,8 +58,6 @@ 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选项Scheme等。
```

View File

@ -47,10 +47,13 @@ const SwaggerUIContent = `{{define "swagger"}}
<script src="{{.cdn}}/swagger-ui-standalone-preset.js" crossorigin="anonymous"></script>
<script>
window.onload = function() {
var specPath = "{{.specName}}"
if (!window.location.pathname.endsWith("/")) {
specPath = "/" + specPath
}
// Build a system
const ui = SwaggerUIBundle({
url: this.window.location.origin+{{.path}},
url: window.location.origin+window.location.pathname+specPath,
dom_id: '#swagger-ui',
deepLinking: true,
presets: [

View File

@ -13,7 +13,7 @@ func main() {
func initServer() echoswagger.ApiRoot {
e := echo.New()
se := echoswagger.New(e, "", "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",

View File

@ -18,7 +18,7 @@ func TestMain(t *testing.T) {
c := e.Echo().NewContext(req, rec)
b, err := ioutil.ReadFile("./swagger.json")
assert.Nil(t, err)
if assert.NoError(t, e.(*echoswagger.Root).Spec(c)) {
if assert.NoError(t, e.(*echoswagger.Root).SpecHandler("/doc")(c)) {
assert.Equal(t, http.StatusOK, rec.Code)
assert.JSONEq(t, string(b), rec.Body.String())
}

View File

@ -31,7 +31,7 @@ type RawDefine struct {
Schema *JSONSchema
}
func (r *Root) docHandler(realSpecPath string) echo.HandlerFunc {
func (r *Root) docHandler() echo.HandlerFunc {
t, err := template.New("swagger").Parse(SwaggerUIContent)
if err != nil {
panic(err)
@ -43,10 +43,10 @@ func (r *Root) docHandler(realSpecPath string) echo.HandlerFunc {
}
buf := new(bytes.Buffer)
t.Execute(buf, map[string]interface{}{
"title": r.spec.Info.Title,
"hideTop": r.ui.HideTop,
"path": realSpecPath,
"cdn": cdn,
"title": r.spec.Info.Title,
"hideTop": r.ui.HideTop,
"cdn": cdn,
"specName": SpecName,
})
return c.HTMLBlob(http.StatusOK, buf.Bytes())
}

View File

@ -9,7 +9,7 @@ import (
)
func TestSecurity(t *testing.T) {
r := New(echo.New(), "/", "doc/", nil)
r := New(echo.New(), "doc/", nil)
scope := map[string]string{
"read:users": "read users",
"write:users": "modify users",
@ -194,7 +194,7 @@ func TestSecurity(t *testing.T) {
}
func TestSecurityRepeat(t *testing.T) {
r := New(echo.New(), "/", "doc/", nil)
r := New(echo.New(), "doc/", nil)
scope := map[string]string{
"read:users": "read users",
"write:users": "modify users",

33
spec.go
View File

@ -12,22 +12,29 @@ import (
const (
DefPrefix = "#/definitions/"
SwaggerVersion = "2.0"
SpecName = "swagger.json"
)
func (r *Root) Spec(c echo.Context) error {
r.once.Do(func() {
r.err = r.genSpec(c)
r.cleanUp()
})
if r.err != nil {
return c.String(http.StatusInternalServerError, r.err.Error())
func (r *Root) SpecHandler(docPath string) echo.HandlerFunc {
return func(c echo.Context) error {
r.once.Do(func() {
r.err = r.genSpec(c)
r.cleanUp()
})
if r.err != nil {
return c.String(http.StatusInternalServerError, r.err.Error())
}
var basePath string
if uri, err := url.ParseRequestURI(c.Request().Referer()); err == nil {
basePath = trimSuffixSlash(uri.Path, docPath)
r.spec.Host = uri.Host
} else {
basePath = trimSuffixSlash(c.Request().URL.Path, connectPath(docPath, SpecName))
r.spec.Host = c.Request().Host
}
r.spec.BasePath = connectPath(basePath)
return c.JSON(http.StatusOK, r.spec)
}
if uri, err := url.ParseRequestURI(c.Request().Referer()); err == nil {
r.spec.Host = uri.Host
} else {
r.spec.Host = c.Request().Host
}
return c.JSON(http.StatusOK, r.spec)
}
func (r *Root) genSpec(c echo.Context) error {

View File

@ -21,7 +21,7 @@ func TestSpec(t *testing.T) {
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
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).SpecHandler("/doc/")(c)) {
assert.Equal(t, http.StatusOK, rec.Code)
assert.JSONEq(t, j, rec.Body.String())
}
@ -41,7 +41,7 @@ func TestSpec(t *testing.T) {
req := httptest.NewRequest(echo.GET, "/doc/swagger.json", nil)
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
if assert.NoError(t, r.(*Root).Spec(c)) {
if assert.NoError(t, r.(*Root).SpecHandler("/doc")(c)) {
assert.Equal(t, http.StatusOK, rec.Code)
s := r.(*Root).spec
assert.Len(t, s.Paths, 1)
@ -63,7 +63,7 @@ func TestSpec(t *testing.T) {
req := httptest.NewRequest(echo.GET, "/doc/swagger.json", nil)
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
if assert.NoError(t, r.(*Root).Spec(c)) {
if assert.NoError(t, r.(*Root).SpecHandler("/doc")(c)) {
assert.Equal(t, http.StatusInternalServerError, rec.Code)
}
})
@ -76,7 +76,7 @@ func TestSpec(t *testing.T) {
req := httptest.NewRequest(echo.GET, "/doc/swagger.json", nil)
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
if assert.NoError(t, r.(*Root).Spec(c)) {
if assert.NoError(t, r.(*Root).SpecHandler("/doc")(c)) {
assert.Equal(t, http.StatusInternalServerError, rec.Code)
}
})
@ -96,7 +96,7 @@ func TestSpec(t *testing.T) {
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
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).SpecHandler("/doc")(c)) {
assert.Equal(t, http.StatusOK, rec.Code)
assert.JSONEq(t, j, rec.Body.String())
}
@ -108,29 +108,72 @@ func TestSpec(t *testing.T) {
})
}
func TestRefererHost(t *testing.T) {
func TestReferer(t *testing.T) {
tests := []struct {
name, referer, host string
name, referer, host, docPath, basePath string
}{
{
referer: "http://localhost:1323/doc",
host: "localhost:1323",
name: "A",
referer: "http://localhost:1323/doc",
host: "localhost:1323",
docPath: "/doc",
name: "A",
basePath: "/",
},
{
referer: "1/doc",
host: "127.0.0.1",
name: "B",
referer: "http://localhost:1323/doc",
host: "localhost:1323",
docPath: "/doc/",
name: "B",
basePath: "/",
},
{
referer: "http://user:pass@github.com",
host: "github.com",
name: "C",
referer: "http://localhost:1323/doc/",
host: "localhost:1323",
docPath: "/doc",
name: "C",
basePath: "/",
},
{
referer: "https://www.github.com?q=1",
host: "www.github.com",
name: "D",
referer: "http://localhost:1323/api/v1/doc",
host: "localhost:1323",
docPath: "/doc",
name: "D",
basePath: "/api/v1",
},
{
referer: "1/doc",
host: "127.0.0.1",
docPath: "/doc",
name: "E",
basePath: "/",
},
{
referer: "http://user:pass@github.com",
host: "github.com",
docPath: "/",
name: "F",
basePath: "/",
},
{
referer: "https://www.github.com/v1/docs/?q=1",
host: "www.github.com",
docPath: "/docs/",
name: "G",
basePath: "/v1",
},
{
referer: "https://www.github.com/?q=1#tag=TAG",
host: "www.github.com",
docPath: "",
name: "H",
basePath: "/",
},
{
referer: "https://www.github.com/",
host: "www.github.com",
docPath: "/doc",
name: "I",
basePath: "/",
},
}
for _, tt := range tests {
@ -141,14 +184,16 @@ func TestRefererHost(t *testing.T) {
req.Header.Add("referer", tt.referer)
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
if assert.NoError(t, r.(*Root).Spec(c)) {
if assert.NoError(t, r.(*Root).SpecHandler(tt.docPath)(c)) {
assert.Equal(t, http.StatusOK, rec.Code)
var v struct {
Host string `json:"host"`
Host string `json:"host"`
BasePath string `json:"basePath"`
}
err := json.Unmarshal(rec.Body.Bytes(), &v)
assert.NoError(t, err)
assert.Equal(t, tt.host, v.Host)
assert.Equal(t, tt.basePath, v.BasePath)
}
})
}
@ -181,7 +226,7 @@ func TestAddDefinition(t *testing.T) {
req := httptest.NewRequest(echo.GET, "/doc/swagger.json", nil)
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
if assert.NoError(t, r.(*Root).Spec(c)) {
if assert.NoError(t, r.(*Root).SpecHandler("/doc")(c)) {
assert.Equal(t, http.StatusOK, rec.Code)
assert.Len(t, r.(*Root).spec.Definitions, 2)
}

View File

@ -1,6 +1,9 @@
package echoswagger
import "reflect"
import (
"reflect"
"strings"
)
func contains(list []string, s string) bool {
for _, t := range list {
@ -85,3 +88,19 @@ func connectPath(paths ...string) string {
}
return result
}
func removeTrailingSlash(path string) string {
l := len(path) - 1
if l > 0 && strings.HasSuffix(path, "/") {
path = path[:l]
}
return path
}
func trimSuffixSlash(s, suffix string) string {
s = connectPath(s)
suffix = connectPath(suffix)
s = removeTrailingSlash(s)
suffix = removeTrailingSlash(suffix)
return strings.TrimSuffix(s, suffix)
}

View File

@ -209,7 +209,7 @@ type api struct {
// New creates ApiRoot instance.
// Multiple ApiRoot are allowed in one project.
func New(e *echo.Echo, basePath, docPath string, i *Info) ApiRoot {
func New(e *echo.Echo, docPath string, i *Info) ApiRoot {
if e == nil {
panic("echoswagger: invalid Echo instance")
}
@ -225,7 +225,6 @@ func New(e *echo.Echo, basePath, docPath string, i *Info) ApiRoot {
spec: &Swagger{
Info: i,
SecurityDefinitions: make(map[string]*SecurityDefinition),
BasePath: connectPath(basePath),
Definitions: make(map[string]*JSONSchema),
},
routers: routers{
@ -233,10 +232,8 @@ func New(e *echo.Echo, basePath, docPath string, i *Info) ApiRoot {
},
}
specPath := connectPath(docPath, "swagger.json")
realSpecPath := connectPath(basePath, specPath)
e.GET(connectPath(docPath), r.docHandler(realSpecPath))
e.GET(specPath, r.Spec)
e.GET(connectPath(docPath), r.docHandler())
e.GET(connectPath(docPath, SpecName), r.SpecHandler(docPath))
return r
}

View File

@ -12,7 +12,7 @@ import (
)
func prepareApiRoot() ApiRoot {
return New(echo.New(), "/", "doc/", nil)
return New(echo.New(), "doc/", nil)
}
func prepareApiGroup() ApiGroup {
@ -29,7 +29,6 @@ func prepareApi() Api {
func TestNew(t *testing.T) {
tests := []struct {
echo *echo.Echo
basePath string
docPath string
info *Info
expectPaths []string
@ -38,7 +37,6 @@ func TestNew(t *testing.T) {
}{
{
echo: echo.New(),
basePath: "/",
docPath: "doc/",
info: nil,
expectPaths: []string{"/doc/", "/doc/swagger.json"},
@ -46,9 +44,8 @@ func TestNew(t *testing.T) {
name: "Normal",
},
{
echo: echo.New(),
basePath: "/",
docPath: "doc",
echo: echo.New(),
docPath: "doc",
info: &Info{
Title: "Test project",
Contact: &Contact{
@ -69,16 +66,15 @@ func TestNew(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
if tt.panic {
assert.Panics(t, func() {
New(tt.echo, tt.basePath, tt.docPath, tt.info)
New(tt.echo, tt.docPath, tt.info)
})
} else {
apiRoot := New(tt.echo, tt.basePath, tt.docPath, tt.info)
apiRoot := New(tt.echo, tt.docPath, tt.info)
assert.NotNil(t, apiRoot.(*Root))
r := apiRoot.(*Root)
assert.NotNil(t, r.spec)
assert.Equal(t, r.spec.BasePath, tt.basePath)
if tt.info == nil {
assert.Equal(t, r.spec.Info.Title, "Project APIs")
} else {
@ -97,74 +93,36 @@ func TestNew(t *testing.T) {
func TestPath(t *testing.T) {
tests := []struct {
baseInput, docInput string
baseOutput, docOutput, specOutput, realSpecOutput string
name string
docInput string
docOutput, specOutput string
name string
}{
{
baseInput: "/",
docInput: "doc/",
baseOutput: "/",
docOutput: "/doc/",
specOutput: "/doc/swagger.json",
realSpecOutput: "/doc/swagger.json",
name: "A",
docInput: "doc/",
docOutput: "/doc/",
specOutput: "/doc/swagger.json",
name: "A",
}, {
baseInput: "",
docInput: "",
baseOutput: "/",
docOutput: "/",
specOutput: "/swagger.json",
realSpecOutput: "/swagger.json",
name: "B",
docInput: "",
docOutput: "/",
specOutput: "/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",
docInput: "/doc",
docOutput: "/doc",
specOutput: "/doc/swagger.json",
name: "C",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
apiRoot := New(echo.New(), tt.baseInput, tt.docInput, nil)
apiRoot := New(echo.New(), 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))
})
}
}
@ -367,12 +325,12 @@ func TestAddResponse(t *testing.T) {
func TestUI(t *testing.T) {
t.Run("DefaultCDN", func(t *testing.T) {
r := New(echo.New(), "/", "doc/", nil)
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")
h := se.docHandler()
if assert.NoError(t, h(c)) {
assert.Equal(t, http.StatusOK, rec.Code)
@ -381,12 +339,12 @@ func TestUI(t *testing.T) {
})
t.Run("SetUI", func(t *testing.T) {
r := New(echo.New(), "/", "doc/", nil)
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")
h := se.docHandler()
cdn := "https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/3.18.0"
r.SetUI(UISetting{