Repository: go-openapi/runtime
Pull request #184 (2020-08-31 19:34:09)

feat: support serving rapidoc ui
Based off of redoc implementation

Ref: https://mrin9.github.io/RapiDoc/

codecov[bot]: # [Codecov](https://codecov.io/gh/go-openapi/runtime/pull/184?src=pr&el=h1) Report
> Merging [#184](https://codecov.io/gh/go-openapi/runtime/pull/184?src=pr&el=desc) into [master](https://codecov.io/gh/go-openapi/runtime/commit/1c9920cd23ed5a8a1413138dc62e9f5707a29401?el=desc) will **decrease** coverage by `0.00%`.
> The diff coverage is `80.00%`.

[![Impacted file tree graph](https://codecov.io/gh/go-openapi/runtime/pull/184/graphs/tree.svg?width=650&height=150&src=pr&token=6wl4NsM8aQ)](https://codecov.io/gh/go-openapi/runtime/pull/184?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master     #184      +/-   ##
==========================================
- Coverage   80.45%   80.44%   -0.01%     
==========================================
  Files          41       42       +1     
  Lines        2440     2470      +30     
==========================================
+ Hits         1963     1987      +24     
- Misses        372      378       +6     
  Partials      105      105              
```


| [Impacted Files](https://codecov.io/gh/go-openapi/runtime/pull/184?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [middleware/rapidoc.go](https://codecov.io/gh/go-openapi/runtime/pull/184/diff?src=pr&el=tree#diff-bWlkZGxld2FyZS9yYXBpZG9jLmdv) | `80.00% <80.00%> (ø)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/go-openapi/runtime/pull/184?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/go-openapi/runtime/pull/184?src=pr&el=footer). Last update [1c9920c...90e3eaf](https://codecov.io/gh/go-openapi/runtime/pull/184?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/interfaces.go b/interfaces.go
index 65de0aa..e334128 100644
--- a/interfaces.go
+++ b/interfaces.go
@@ -15,6 +15,7 @@
 package runtime
 
 import (
+	"context"
 	"io"
 	"net/http"
 
@@ -101,3 +102,11 @@ type Authorizer interface {
 type Validatable interface {
 	Validate(strfmt.Registry) error
 }
+
+// ContextValidatable types implementing this interface allow customizing their validation
+// this will be used instead of the reflective validation based on the spec document.
+// the implementations are assumed to have been generated by the swagger tool so they should
+// contain all the context validations obtained from the spec
+type ContextValidatable interface {
+	ContextValidate(context.Context, strfmt.Registry) error
+}
diff --git a/middleware/rapidoc.go b/middleware/rapidoc.go
new file mode 100644
index 0000000..4be330d
--- /dev/null
+++ b/middleware/rapidoc.go
@@ -0,0 +1,90 @@
+package middleware
+
+import (
+	"bytes"
+	"fmt"
+	"html/template"
+	"net/http"
+	"path"
+)
+
+// RapiDocOpts configures the RapiDoc middlewares
+type RapiDocOpts struct {
+	// BasePath for the UI path, defaults to: /
+	BasePath string
+	// Path combines with BasePath for the full UI path, defaults to: docs
+	Path string
+	// SpecURL the url to find the spec for
+	SpecURL string
+	// RapiDocURL for the js that generates the rapidoc site, defaults to: https://cdn.jsdelivr.net/npm/rapidoc/bundles/rapidoc.standalone.js
+	RapiDocURL string
+	// Title for the documentation site, default to: API documentation
+	Title string
+}
+
+// EnsureDefaults in case some options are missing
+func (r *RapiDocOpts) EnsureDefaults() {
+	if r.BasePath == "" {
+		r.BasePath = "/"
+	}
+	if r.Path == "" {
+		r.Path = "docs"
+	}
+	if r.SpecURL == "" {
+		r.SpecURL = "/swagger.json"
+	}
+	if r.RapiDocURL == "" {
+		r.RapiDocURL = rapidocLatest
+	}
+	if r.Title == "" {
+		r.Title = "API documentation"
+	}
+}
+
+// RapiDoc creates a middleware to serve a documentation site for a swagger spec.
+// This allows for altering the spec before starting the http listener.
+//
+func RapiDoc(opts RapiDocOpts, next http.Handler) http.Handler {
+	opts.EnsureDefaults()
+
+	pth := path.Join(opts.BasePath, opts.Path)
+	tmpl := template.Must(template.New("rapidoc").Parse(rapidocTemplate))
+
+	buf := bytes.NewBuffer(nil)
+	_ = tmpl.Execute(buf, opts)
+	b := buf.Bytes()
+
+	return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
+		if r.URL.Path == pth {
+			rw.Header().Set("Content-Type", "text/html; charset=utf-8")
+			rw.WriteHeader(http.StatusOK)
+
+			_, _ = rw.Write(b)
+			return
+		}
+
+		if next == nil {
+			rw.Header().Set("Content-Type", "text/plain")
+			rw.WriteHeader(http.StatusNotFound)
+			_, _ = rw.Write([]byte(fmt.Sprintf("%q not found", pth)))
+			return
+		}
+		next.ServeHTTP(rw, r)
+	})
+}
+
+const (
+	rapidocLatest   = "https://unpkg.com/rapidoc/dist/rapidoc-min.js"
+	rapidocTemplate = `<!doctype html>
+<html>
+<head>
+  <title>{{ .Title }}</title>
+  <meta charset="utf-8"> <!-- Important: rapi-doc uses utf8 charecters -->
+  <script type="module" src="{{ .RapiDocURL }}"></script>
+</head>
+<body>
+  <rapi-doc spec-url="{{ .SpecURL }}"></rapi-doc>
+</body>
+</html>
+`
+)


Test patch:
diff --git a/middleware/rapidoc_test.go b/middleware/rapidoc_test.go
new file mode 100644
index 0000000..b007465
--- /dev/null
+++ b/middleware/rapidoc_test.go
@@ -0,0 +1,22 @@
+package middleware
+
+import (
+	"net/http"
+	"net/http/httptest"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestRapiDocMiddleware(t *testing.T) {
+	rapidoc := RapiDoc(RapiDocOpts{}, nil)
+
+	req, _ := http.NewRequest("GET", "/docs", nil)
+	recorder := httptest.NewRecorder()
+	rapidoc.ServeHTTP(recorder, req)
+	assert.Equal(t, 200, recorder.Code)
+	assert.Equal(t, "text/html; charset=utf-8", recorder.Header().Get("Content-Type"))
+	assert.Contains(t, recorder.Body.String(), "<title>API documentation</title>")
+	assert.Contains(t, recorder.Body.String(), "<rapi-doc spec-url=\"/swagger.json\"></rapi-doc>")
+	assert.Contains(t, recorder.Body.String(), rapidocLatest)
+}