#21 Integrated doc HTML and spec JSON into one request

This commit is contained in:
elvinchan 2019-05-12 23:10:04 +08:00
parent 818f3e3ce0
commit 526c519e76
9 changed files with 128 additions and 65 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(), "", "doc/", nil) r := echoswagger.New(echo.New(), "doc/", nil)
// Routes with parameters & responses // Routes with parameters & responses
r.POST("/", createUser). r.POST("/", createUser).

View File

@ -31,7 +31,7 @@ import (
func main() { func main() {
// ApiRoot with Echo instance // ApiRoot with Echo instance
r := echoswagger.New(echo.New(), "", "doc/", nil) r := echoswagger.New(echo.New(), "doc/", nil)
// Routes with parameters & responses // Routes with parameters & responses
r.POST("/", createUser). r.POST("/", createUser).

View File

@ -51,9 +51,11 @@ const SwaggerUIContent = `{{define "swagger"}}
if (!window.location.pathname.endsWith("/")) { if (!window.location.pathname.endsWith("/")) {
specPath = "/" + specPath specPath = "/" + specPath
} }
var spec = "{{.spec}}"
// Build a system // Build a system
const ui = SwaggerUIBundle({ const ui = SwaggerUIBundle({
url: window.location.origin+window.location.pathname+specPath, url: window.location.origin+window.location.pathname+specPath,
spec: spec ? JSON.parse(spec) : undefined,
dom_id: '#swagger-ui', dom_id: '#swagger-ui',
deepLinking: true, deepLinking: true,
presets: [ presets: [

View File

@ -1,8 +1,8 @@
package main package main
import ( import (
"encoding/json"
"io/ioutil" "io/ioutil"
"net/http"
"net/http/httptest" "net/http/httptest"
"testing" "testing"
@ -18,8 +18,9 @@ func TestMain(t *testing.T) {
c := e.Echo().NewContext(req, rec) c := e.Echo().NewContext(req, rec)
b, err := ioutil.ReadFile("./swagger.json") b, err := ioutil.ReadFile("./swagger.json")
assert.Nil(t, err) assert.Nil(t, err)
if assert.NoError(t, e.(*echoswagger.Root).SpecHandler("/doc")(c)) { s, err := e.(*echoswagger.Root).GetSpec(c, "/doc")
assert.Equal(t, http.StatusOK, rec.Code) assert.Nil(t, err)
assert.JSONEq(t, string(b), rec.Body.String()) rs, err := json.Marshal(s)
} assert.Nil(t, err)
assert.JSONEq(t, string(b), string(rs))
} }

View File

@ -2,6 +2,7 @@ package echoswagger
import ( import (
"bytes" "bytes"
"encoding/json"
"html/template" "html/template"
"net/http" "net/http"
"reflect" "reflect"
@ -20,6 +21,7 @@ const (
) )
type UISetting struct { type UISetting struct {
DetachSpec bool
HideTop bool HideTop bool
CDN string CDN string
} }
@ -31,7 +33,7 @@ type RawDefine struct {
Schema *JSONSchema Schema *JSONSchema
} }
func (r *Root) docHandler() echo.HandlerFunc { func (r *Root) docHandler(docPath 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)
@ -42,12 +44,26 @@ func (r *Root) docHandler() echo.HandlerFunc {
cdn = DefaultCDN cdn = DefaultCDN
} }
buf := new(bytes.Buffer) buf := new(bytes.Buffer)
t.Execute(buf, map[string]interface{}{ params := map[string]interface{}{
"title": r.spec.Info.Title, "title": r.spec.Info.Title,
"hideTop": r.ui.HideTop,
"cdn": cdn, "cdn": cdn,
"specName": SpecName, "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()) return c.HTMLBlob(http.StatusOK, buf.Bytes())
} }
} }

24
spec.go
View File

@ -15,26 +15,36 @@ const (
SpecName = "swagger.json" 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 { return func(c echo.Context) error {
spec, err := r.GetSpec(c, docPath)
if err != nil {
return c.String(http.StatusInternalServerError, err.Error())
}
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.once.Do(func() {
r.err = r.genSpec(c) r.err = r.genSpec(c)
r.cleanUp() r.cleanUp()
}) })
if r.err != nil { if r.err != nil {
return c.String(http.StatusInternalServerError, r.err.Error()) return Swagger{}, r.err
} }
swagger := *r.spec
var basePath string var basePath string
if uri, err := url.ParseRequestURI(c.Request().Referer()); err == nil { if uri, err := url.ParseRequestURI(c.Request().Referer()); err == nil {
basePath = trimSuffixSlash(uri.Path, docPath) basePath = trimSuffixSlash(uri.Path, docPath)
r.spec.Host = uri.Host swagger.Host = uri.Host
} else { } else {
basePath = trimSuffixSlash(c.Request().URL.Path, connectPath(docPath, SpecName)) basePath = trimSuffixSlash(c.Request().URL.Path, connectPath(docPath, SpecName))
r.spec.Host = c.Request().Host swagger.Host = c.Request().Host
}
r.spec.BasePath = connectPath(basePath)
return c.JSON(http.StatusOK, r.spec)
} }
swagger.BasePath = connectPath(basePath)
return swagger, nil
} }
func (r *Root) genSpec(c echo.Context) error { func (r *Root) genSpec(c echo.Context) error {

View File

@ -21,12 +21,26 @@ func TestSpec(t *testing.T) {
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":"/","paths":{}}` 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.Equal(t, http.StatusOK, rec.Code)
assert.JSONEq(t, j, rec.Body.String()) 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) { t.Run("Methods", func(t *testing.T) {
r := prepareApiRoot() r := prepareApiRoot()
var h echo.HandlerFunc var h echo.HandlerFunc
@ -41,7 +55,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)
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.Equal(t, http.StatusOK, rec.Code)
s := r.(*Root).spec s := r.(*Root).spec
assert.Len(t, s.Paths, 1) 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) { t.Run("CleanUp", func(t *testing.T) {
r := prepareApiRoot() r := prepareApiRoot()
e := r.(*Root).echo e := r.(*Root).echo
@ -96,7 +84,7 @@ func TestSpec(t *testing.T) {
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":"/","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).SpecHandler("/doc")(c)) { if assert.NoError(t, r.(*Root).specHandler("/doc")(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())
} }
@ -184,7 +172,7 @@ func TestReferer(t *testing.T) {
req.Header.Add("referer", tt.referer) req.Header.Add("referer", tt.referer)
rec := httptest.NewRecorder() rec := httptest.NewRecorder()
c := e.NewContext(req, rec) 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) assert.Equal(t, http.StatusOK, rec.Code)
var v struct { var v struct {
Host string `json:"host"` Host string `json:"host"`
@ -226,7 +214,7 @@ func TestAddDefinition(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)
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.Equal(t, http.StatusOK, rec.Code)
assert.Len(t, r.(*Root).spec.Definitions, 2) assert.Len(t, r.(*Root).spec.Definitions, 2)
} }

View File

@ -67,6 +67,7 @@ type ApiRoot interface {
AddSecurityOAuth2(name, desc string, flow OAuth2FlowType, authorizationUrl, tokenUrl string, scopes map[string]string) ApiRoot AddSecurityOAuth2(name, desc string, flow OAuth2FlowType, authorizationUrl, tokenUrl string, scopes map[string]string) ApiRoot
// SetUI sets UI setting. // SetUI sets UI setting.
// If DetachSpec is false, HideTop will not take effect
SetUI(ui UISetting) ApiRoot SetUI(ui UISetting) ApiRoot
// SetScheme sets available protocol schemes. // 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), r.docHandler(docPath))
e.GET(connectPath(docPath, SpecName), r.SpecHandler(docPath)) e.GET(connectPath(docPath, SpecName), r.specHandler(docPath))
return r return r
} }

View File

@ -330,7 +330,7 @@ func TestUI(t *testing.T) {
req := httptest.NewRequest(echo.GET, "/doc/", nil) req := httptest.NewRequest(echo.GET, "/doc/", nil)
rec := httptest.NewRecorder() rec := httptest.NewRecorder()
c := se.echo.NewContext(req, rec) c := se.echo.NewContext(req, rec)
h := se.docHandler() h := se.docHandler("doc/")
if assert.NoError(t, h(c)) { if assert.NoError(t, h(c)) {
assert.Equal(t, http.StatusOK, rec.Code) assert.Equal(t, http.StatusOK, rec.Code)
@ -344,7 +344,7 @@ func TestUI(t *testing.T) {
req := httptest.NewRequest(echo.GET, "/doc/", nil) req := httptest.NewRequest(echo.GET, "/doc/", nil)
rec := httptest.NewRecorder() rec := httptest.NewRecorder()
c := se.echo.NewContext(req, rec) 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" cdn := "https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/3.18.0"
r.SetUI(UISetting{ r.SetUI(UISetting{
@ -355,6 +355,17 @@ func TestUI(t *testing.T) {
if assert.NoError(t, h(c)) { if assert.NoError(t, h(c)) {
assert.Equal(t, http.StatusOK, rec.Code) assert.Equal(t, http.StatusOK, rec.Code)
assert.Contains(t, rec.Body.String(), cdn) 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") assert.Contains(t, rec.Body.String(), "#swagger-ui>.swagger-container>.topbar")
} }
}) })
@ -474,3 +485,37 @@ func TestEcho(t *testing.T) {
a := prepareApi() a := prepareApi()
assert.NotNil(t, a.Route()) 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)
}
})
}