diff --git a/README.md b/README.md index 96ea53b..aa1c61b 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ import ( func main() { // ApiRoot with Echo instance - r := echoswagger.New(echo.New(), "", "doc/", nil) + r := echoswagger.New(echo.New(), "doc/", nil) // Routes with parameters & responses r.POST("/", createUser). diff --git a/README_zh-CN.md b/README_zh-CN.md index 0726be4..6729e9e 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(), "", "doc/", nil) + r := echoswagger.New(echo.New(), "doc/", nil) // Routes with parameters & responses r.POST("/", createUser). diff --git a/assets.go b/assets.go index 25d008f..0dc4bab 100644 --- a/assets.go +++ b/assets.go @@ -51,9 +51,11 @@ const SwaggerUIContent = `{{define "swagger"}} if (!window.location.pathname.endsWith("/")) { specPath = "/" + specPath } + var spec = "{{.spec}}" // Build a system const ui = SwaggerUIBundle({ url: window.location.origin+window.location.pathname+specPath, + spec: spec ? JSON.parse(spec) : undefined, dom_id: '#swagger-ui', deepLinking: true, presets: [ diff --git a/examples/main_test.go b/examples/main_test.go index 075c61f..cc9f8b9 100644 --- a/examples/main_test.go +++ b/examples/main_test.go @@ -1,8 +1,8 @@ package main import ( + "encoding/json" "io/ioutil" - "net/http" "net/http/httptest" "testing" @@ -18,8 +18,9 @@ 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).SpecHandler("/doc")(c)) { - assert.Equal(t, http.StatusOK, rec.Code) - assert.JSONEq(t, string(b), rec.Body.String()) - } + s, err := e.(*echoswagger.Root).GetSpec(c, "/doc") + assert.Nil(t, err) + rs, err := json.Marshal(s) + assert.Nil(t, err) + assert.JSONEq(t, string(b), string(rs)) } diff --git a/internal.go b/internal.go index 2a494c7..a1eb833 100644 --- a/internal.go +++ b/internal.go @@ -2,6 +2,7 @@ package echoswagger import ( "bytes" + "encoding/json" "html/template" "net/http" "reflect" @@ -20,8 +21,9 @@ const ( ) type UISetting struct { - HideTop bool - CDN string + DetachSpec bool + HideTop bool + CDN string } type RawDefineDic map[string]RawDefine @@ -31,7 +33,7 @@ type RawDefine struct { Schema *JSONSchema } -func (r *Root) docHandler() echo.HandlerFunc { +func (r *Root) docHandler(docPath string) echo.HandlerFunc { t, err := template.New("swagger").Parse(SwaggerUIContent) if err != nil { panic(err) @@ -42,12 +44,26 @@ func (r *Root) docHandler() echo.HandlerFunc { cdn = DefaultCDN } buf := new(bytes.Buffer) - t.Execute(buf, map[string]interface{}{ + params := map[string]interface{}{ "title": r.spec.Info.Title, - "hideTop": r.ui.HideTop, "cdn": cdn, "specName": SpecName, - }) + } + if !r.ui.DetachSpec { + spec, err := r.GetSpec(c, docPath) + if err != nil { + return c.String(http.StatusInternalServerError, err.Error()) + } + b, err := json.Marshal(spec) + if err != nil { + return c.String(http.StatusInternalServerError, err.Error()) + } + params["spec"] = string(b) + params["hideTop"] = true + } else { + params["hideTop"] = r.ui.HideTop + } + t.Execute(buf, params) return c.HTMLBlob(http.StatusOK, buf.Bytes()) } } diff --git a/spec.go b/spec.go index ff39999..1711942 100644 --- a/spec.go +++ b/spec.go @@ -15,28 +15,38 @@ const ( SpecName = "swagger.json" ) -func (r *Root) SpecHandler(docPath string) echo.HandlerFunc { +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()) + spec, err := r.GetSpec(c, docPath) + if err != nil { + return c.String(http.StatusInternalServerError, 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) + return c.JSON(http.StatusOK, spec) } } +// Generate swagger spec data +func (r *Root) GetSpec(c echo.Context, docPath string) (Swagger, error) { + r.once.Do(func() { + r.err = r.genSpec(c) + r.cleanUp() + }) + if r.err != nil { + return Swagger{}, r.err + } + swagger := *r.spec + var basePath string + if uri, err := url.ParseRequestURI(c.Request().Referer()); err == nil { + basePath = trimSuffixSlash(uri.Path, docPath) + swagger.Host = uri.Host + } else { + basePath = trimSuffixSlash(c.Request().URL.Path, connectPath(docPath, SpecName)) + swagger.Host = c.Request().Host + } + swagger.BasePath = connectPath(basePath) + return swagger, nil +} + func (r *Root) genSpec(c echo.Context) error { r.spec.Swagger = SwaggerVersion r.spec.Paths = make(map[string]interface{}) diff --git a/spec_test.go b/spec_test.go index 4383753..f5cd800 100644 --- a/spec_test.go +++ b/spec_test.go @@ -21,12 +21,26 @@ 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).SpecHandler("/doc/")(c)) { + if assert.NoError(t, r.(*Root).specHandler("/doc/")(c)) { assert.Equal(t, http.StatusOK, rec.Code) assert.JSONEq(t, j, rec.Body.String()) } }) + t.Run("BasicGenerater", func(t *testing.T) { + r := prepareApiRoot() + e := r.(*Root).echo + 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":"/","paths":{}}` + s, err := r.(*Root).GetSpec(c, "/doc/") + assert.Nil(t, err) + rs, err := json.Marshal(s) + assert.Nil(t, err) + assert.JSONEq(t, j, string(rs)) + }) + t.Run("Methods", func(t *testing.T) { r := prepareApiRoot() var h echo.HandlerFunc @@ -41,7 +55,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).SpecHandler("/doc")(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) @@ -55,32 +69,6 @@ func TestSpec(t *testing.T) { } }) - t.Run("ErrorGroupSecurity", func(t *testing.T) { - r := prepareApiRoot() - e := r.(*Root).echo - var h echo.HandlerFunc - r.Group("G", "/g").SetSecurity("JWT").GET("/", h) - req := httptest.NewRequest(echo.GET, "/doc/swagger.json", nil) - rec := httptest.NewRecorder() - c := e.NewContext(req, rec) - if assert.NoError(t, r.(*Root).SpecHandler("/doc")(c)) { - assert.Equal(t, http.StatusInternalServerError, rec.Code) - } - }) - - t.Run("ErrorApiSecurity", func(t *testing.T) { - r := prepareApiRoot() - e := r.(*Root).echo - var h echo.HandlerFunc - r.GET("/", h).SetSecurity("JWT") - req := httptest.NewRequest(echo.GET, "/doc/swagger.json", nil) - rec := httptest.NewRecorder() - c := e.NewContext(req, rec) - if assert.NoError(t, r.(*Root).SpecHandler("/doc")(c)) { - assert.Equal(t, http.StatusInternalServerError, rec.Code) - } - }) - t.Run("CleanUp", func(t *testing.T) { r := prepareApiRoot() e := r.(*Root).echo @@ -96,7 +84,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).SpecHandler("/doc")(c)) { + if assert.NoError(t, r.(*Root).specHandler("/doc")(c)) { assert.Equal(t, http.StatusOK, rec.Code) assert.JSONEq(t, j, rec.Body.String()) } @@ -184,7 +172,7 @@ func TestReferer(t *testing.T) { req.Header.Add("referer", tt.referer) rec := httptest.NewRecorder() c := e.NewContext(req, rec) - if assert.NoError(t, r.(*Root).SpecHandler(tt.docPath)(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"` @@ -226,7 +214,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).SpecHandler("/doc")(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) } diff --git a/wrapper.go b/wrapper.go index c29d7c1..3b967f6 100644 --- a/wrapper.go +++ b/wrapper.go @@ -67,6 +67,7 @@ type ApiRoot interface { AddSecurityOAuth2(name, desc string, flow OAuth2FlowType, authorizationUrl, tokenUrl string, scopes map[string]string) ApiRoot // SetUI sets UI setting. + // If DetachSpec is false, HideTop will not take effect SetUI(ui UISetting) ApiRoot // SetScheme sets available protocol schemes. @@ -232,8 +233,8 @@ func New(e *echo.Echo, docPath string, i *Info) ApiRoot { }, } - e.GET(connectPath(docPath), r.docHandler()) - e.GET(connectPath(docPath, SpecName), r.SpecHandler(docPath)) + e.GET(connectPath(docPath), r.docHandler(docPath)) + e.GET(connectPath(docPath, SpecName), r.specHandler(docPath)) return r } diff --git a/wrapper_test.go b/wrapper_test.go index 1bc2ee0..72fa846 100644 --- a/wrapper_test.go +++ b/wrapper_test.go @@ -330,7 +330,7 @@ func TestUI(t *testing.T) { req := httptest.NewRequest(echo.GET, "/doc/", nil) rec := httptest.NewRecorder() c := se.echo.NewContext(req, rec) - h := se.docHandler() + h := se.docHandler("doc/") if assert.NoError(t, h(c)) { assert.Equal(t, http.StatusOK, rec.Code) @@ -344,7 +344,7 @@ func TestUI(t *testing.T) { req := httptest.NewRequest(echo.GET, "/doc/", nil) rec := httptest.NewRecorder() c := se.echo.NewContext(req, rec) - h := se.docHandler() + h := se.docHandler("doc/") cdn := "https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/3.18.0" r.SetUI(UISetting{ @@ -355,6 +355,17 @@ func TestUI(t *testing.T) { 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(), `{\x22swagger\x22:`) + assert.Contains(t, rec.Body.String(), "#swagger-ui>.swagger-container>.topbar") + } + + r.SetUI(UISetting{ + DetachSpec: true, + }) + + if assert.NoError(t, h(c)) { + assert.Equal(t, http.StatusOK, rec.Code) + assert.NotContains(t, rec.Body.String(), "{\x22swagger\x22:") assert.Contains(t, rec.Body.String(), "#swagger-ui>.swagger-container>.topbar") } }) @@ -474,3 +485,37 @@ func TestEcho(t *testing.T) { a := prepareApi() assert.NotNil(t, a.Route()) } + +func TestHandlers(t *testing.T) { + t.Run("ErrorGroupSecurity", func(t *testing.T) { + r := prepareApiRoot() + e := r.(*Root).echo + var h echo.HandlerFunc + r.Group("G", "/g").SetSecurity("JWT").GET("/", h) + req := httptest.NewRequest(echo.GET, "/doc/swagger.json", nil) + rec := httptest.NewRecorder() + c := e.NewContext(req, rec) + if assert.NoError(t, r.(*Root).specHandler("/doc")(c)) { + assert.Equal(t, http.StatusInternalServerError, rec.Code) + } + if assert.NoError(t, r.(*Root).docHandler("/doc")(c)) { + assert.Equal(t, http.StatusInternalServerError, rec.Code) + } + }) + + t.Run("ErrorApiSecurity", func(t *testing.T) { + r := prepareApiRoot() + e := r.(*Root).echo + var h echo.HandlerFunc + r.GET("/", h).SetSecurity("JWT") + req := httptest.NewRequest(echo.GET, "/doc/swagger.json", nil) + rec := httptest.NewRecorder() + c := e.NewContext(req, rec) + if assert.NoError(t, r.(*Root).specHandler("/doc")(c)) { + assert.Equal(t, http.StatusInternalServerError, rec.Code) + } + if assert.NoError(t, r.(*Root).docHandler("/doc")(c)) { + assert.Equal(t, http.StatusInternalServerError, rec.Code) + } + }) +}