diff --git a/CHANGELOG.md b/CHANGELOG.md
index 546b974d..50761e5b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,7 @@
### `reitit-core`
+* `linear-router` now works with unnamed catch-all parameters, e.g. `"/files/*"`
* `match-by-path` encodes parameters into strings using (internal) `reitit.impl/IntoString` protocol. Handles all of: strings, numbers, keywords, booleans, objects. Fixes [#75](https://github.com/metosin/reitit/issues/75).
```clj
@@ -23,7 +24,7 @@
* `reitit.ring/default-handler` now works correctly with async ring
* new helper `reitit.ring/router` to compose routes outside of a router.
-* `reitit.ring/create-resource-handler` function to serve static routes. See (docs)[https://metosin.github.io/reitit/ring/static.html].
+* `reitit.ring/create-resource-handler` function to serve static routes. See [docs](https://metosin.github.io/reitit/ring/static.html).
* new dependencies:
@@ -33,47 +34,24 @@
### `reitit-swagger`
-* New module to produce swagger-docs from routing tree, including `Coercion` definitions. Works with both middleware & interceptors and Schema & Spec. See [docs](https://metosin.github.io/reitit/swagger.html).
+* New module to produce swagger-docs from routing tree, including `Coercion` definitions. Works with both middleware & interceptors and Schema & Spec. See [docs](https://metosin.github.io/reitit/ring/swagger.html).
+
+### `reitit-swagger-ui`
+
+New module to server pre-integrated [Swagger-ui](https://github.com/swagger-api/swagger-ui). See [docs](https://metosin.github.io/reitit/ring/swagger.html#swagger-ui).
+
+* new dependencies:
```clj
-(require '[reitit.ring :as ring])
-(require '[reitit.swagger :as swagger])
-(require '[reitit.ring.coercion :as rrc])
-(require '[reitit.coercion.spec :as spec])
-(require '[reitit.coercion.schema :as schema])
+[metosin/jsonista "0.2.0"]
+[metosin/ring-swagger-ui "2.2.10"]
+```
-(require '[schema.core :refer [Int]])
+### dependencies
-(ring/ring-handler
- (ring/router
- ["/api"
- {:swagger {:id ::math}}
-
- ["/swagger.json"
- {:get {:no-doc true
- :swagger {:info {:title "my-api"}}
- :handler (swagger/create-swagger-handler)}}]
-
- ["/spec" {:coercion spec/coercion}
- ["/plus"
- {:get {:summary "plus"
- :parameters {:query {:x int?, :y int?}}
- :responses {200 {:body {:total int?}}}
- :handler (fn [{{{:keys [x y]} :query} :parameters}]
- {:status 200, :body {:total (+ x y)}})}}]]
-
- ["/schema" {:coercion schema/coercion}
- ["/plus"
- {:get {:summary "plus"
- :parameters {:query {:x Int, :y Int}}
- :responses {200 {:body {:total Int}}}
- :handler (fn [{{{:keys [x y]} :query} :parameters}]
- {:status 200, :body {:total (+ x y)}})}}]]]
-
- {:data {:middleware [rrc/coerce-exceptions-middleware
- rrc/coerce-request-middleware
- rrc/coerce-response-middleware
- swagger/swagger-feature]}}))
+```clj
+[metosin/spec-tools "0.7.0"] is available but we use "0.6.1"
+[metosin/schema-tools "0.10.2"] is available but we use "0.10.1"
```
## 0.1.0 (2018-2-19)
diff --git a/README.md b/README.md
index b24a7559..2cad80b1 100644
--- a/README.md
+++ b/README.md
@@ -37,7 +37,8 @@ Optionally, the parts can be required separately:
[metosin/reitit-ring "0.1.1-SNAPSHOT"] ; ring-router
[metosin/reitit-spec "0.1.1-SNAPSHOT"] ; spec coercion
[metosin/reitit-schema "0.1.1-SNAPSHOT"] ; schema coercion
-[metosin/reitit-swagger "0.1.1-SNAPSHOT"] ; swagger docs
+[metosin/reitit-swagger "0.1.1-SNAPSHOT"] ; swagger
+[metosin/reitit-swagger-ui "0.1.1-SNAPSHOT"] ; swagger-ui
```
## Quick start
diff --git a/doc/README.md b/doc/README.md
index 103d83ea..e5e05981 100644
--- a/doc/README.md
+++ b/doc/README.md
@@ -29,7 +29,8 @@ Optionally, the parts can be required separately:
[metosin/reitit-ring "0.1.1-SNAPSHOT"] ; ring-router
[metosin/reitit-spec "0.1.1-SNAPSHOT"] ; spec coercion
[metosin/reitit-schema "0.1.1-SNAPSHOT"] ; schema coercion
-[metosin/reitit-swagger "0.1.1-SNAPSHOT"] ; swagger docs
+[metosin/reitit-swagger "0.1.1-SNAPSHOT"] ; swagger
+[metosin/reitit-swagger-ui "0.1.1-SNAPSHOT"] ; swagger-ui
```
For discussions, there is a [#reitit](https://clojurians.slack.com/messages/reitit/) channel in [Clojurians slack](http://clojurians.net/).
diff --git a/doc/SUMMARY.md b/doc/SUMMARY.md
index a477bd95..41312dbb 100644
--- a/doc/SUMMARY.md
+++ b/doc/SUMMARY.md
@@ -28,7 +28,7 @@
* [Pluggable Coercion](ring/coercion.md)
* [Route Data Validation](ring/route_data_validation.md)
* [Compiling Middleware](ring/compiling_middleware.md)
+ * [Swagger Support](ring/swagger.md)
* [Performance](performance.md)
* [Interceptors (WIP)](interceptors.md)
-* [Swagger-support](swagger.md)
* [FAQ](faq.md)
diff --git a/doc/images/swagger.png b/doc/images/swagger.png
new file mode 100644
index 00000000..46d28517
Binary files /dev/null and b/doc/images/swagger.png differ
diff --git a/doc/ring/README.md b/doc/ring/README.md
index 623dac80..233547f4 100644
--- a/doc/ring/README.md
+++ b/doc/ring/README.md
@@ -8,3 +8,4 @@
* [Pluggable Coercion](coercion.md)
* [Route Data Validation](route_data_validation.md)
* [Compiling Middleware](compiling_middleware.md)
+* [Swagger Support](swagger.md)
diff --git a/doc/ring/swagger.md b/doc/ring/swagger.md
new file mode 100644
index 00000000..5840708f
--- /dev/null
+++ b/doc/ring/swagger.md
@@ -0,0 +1,251 @@
+# Swagger Support
+
+```
+[metosin/reitit-swagger "0.1.1-SNAPSHOT"]
+```
+
+Reitit supports [Swagger2](https://swagger.io/) documentation, thanks to [schema-tools](https://github.com/metosin/schema-tools) and [spec-tools](https://github.com/metosin/spec-tools). Documentation is extracted from route definitions, coercion `:parameters` and `:responses` and from a set of new documentation keys.
+
+To enable swagger-documentation for a ring-router:
+
+1. annotate you routes with swagger-data
+2. mount a swagger-handler to serve the swagger-spec
+3. optionally mount a swagger-ui to visualize the swagger-spec
+
+## Swagger data
+
+The following route data keys contribute to the generated swagger specification:
+
+| key | description |
+| --------------|-------------|
+| :swagger | map of any swagger-data. Must have `:id` (keyword or sequence of keywords) to identify the api
+| :no-doc | optional boolean to exclude endpoint from api docs
+| :tags | optional set of strings of keywords tags for an endpoint api docs
+| :summary | optional short string summary of an endpoint
+| :description | optional long description of an endpoint. Supports http://spec.commonmark.org/
+
+Coercion keys also contribute to the docs:
+
+| key | description |
+| --------------|-------------|
+| :parameters | optional input parameters for a route, in a format defined by the coercion
+| :responses | optional descriptions of responess, in a format defined by coercion
+
+There is a `reitit.swagger.swagger-feature`, which acts as both a `Middleware` and an `Interceptor` that is not participating in any request processing - it just defines the route data specs for the routes it's mounted to. It is only needed if the [route data validation](route_data_validation.html) is turned on.
+
+## Swagger spec
+
+To serve the actual [Swagger Specification](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md), there is `reitit.swagger/create-swagger-handler`. It takes no arguments and returns a ring-handler which collects at request-time data from all routes for the same swagger api and returns a formatted Swagger spesification as Clojure data, to be encoded by a response formatter.
+
+If you need to post-process the generated spec, just wrap the handler with a custom `Middleware` or an `Interceptor`.
+
+## Swagger-ui
+
+[Swagger-ui](https://github.com/swagger-api/swagger-ui) is a user interface to visualize and interact with the Swagger spesification. To make things easy, there is a pre-integrated version of the swagger-ui as a separate module.
+
+```
+[metosin/reitit-swagger-ui "0.1.1-SNAPSHOT"]
+```
+
+`reitit.swagger-ui/create-swagger-ui-hander` can be used to create a ring-handler to serve the swagger-ui. It accepts the following options:
+
+| key | description |
+| -----------------|-------------|
+| :parameter | optional name of the wildcard parameter, defaults to unnamed keyword `:`
+| :root | optional resource root, defaults to `"swagger-ui"`
+| :url | path to swagger endpoint, defaults to `/swagger.json`
+| :path | optional path to mount the handler to. Works only if mounted outside of a router.
+| :config | parameters passed to swaggger-ui, keys transformed into camelCase. See [the docs](https://github.com/swagger-api/swagger-ui/blob/master/docs/usage/configuration.md)
+
+We use swagger-ui from [ring-swagger-ui](https://github.com/metosin/ring-swagger-ui), which can be easily configured from routing application. It stores files `swagger-ui` in the resource classpath.
+
+Webjars also hosts a [version](https://github.com/webjars/swagger-ui) of the swagger-ui.
+
+**NOTE**: Currently, swagger-ui module is just for Clojure. ClojureScript-support welcome as a PR!
+
+## Examples
+
+### Simple example
+
+* two routes in a single swagger-api `::api`
+* swagger-spec served from `"/swagger.json"`
+* swagger-ui mounted to `"/"`
+
+```clj
+(require '[reitit.ring :as ring])
+(require '[reitit.swagger :as swagger])
+(require '[reitit.swagger-ui :as swagger-ui])
+
+(def app
+ (ring/ring-handler
+ (ring/router
+ [["/api"
+ ["/ping" {:get (constantly "ping")}]
+ ["/pong" {:post (constantly "pong")}]]
+ ["/swagger.json"
+ {:get {:no-doc true
+ :handler (swagger/create-swagger-handler)}}]]
+ {:data {:swagger {:id ::api}}}) ;; for all routes
+ (swagger-ui/create-swagger-ui-handler {:path "/"})))
+```
+
+The generated swagger spec:
+
+```clj
+(app {:request-method :get :uri "/swagger.json"})
+;{:status 200
+; :body {:swagger "2.0"
+; :x-id #{:user/api}
+; :paths {"/api/ping" {:get {}}
+; "/api/pong" {:post {}}}}}
+```
+
+Swagger-ui:
+
+```clj
+(app {:request-method :get :uri "/"})
+; ... the swagger-ui index-page, configured correctly
+```
+
+### More complete example
+
+* `clojure.spec` and `Schema` coercion
+* swagger data (`:tags`, `:produces`, `:consumes`)
+* swagger-spec served from `"/api/swagger.json"`
+* swagger-ui mounted to `"/"`
+* [Muuntaja](https://github.com/metosin/muuntaja) for request & response formatting
+* `wrap-params` to capture query & path parameters
+* missed routes are handled by `create-default-handler`
+* served via [ring-jetty](https://github.com/ring-clojure/ring/tree/master/ring-jetty-adapter)
+
+Whole example project is in [`/examples/ring-swagger`](https://github.com/metosin/reitit/tree/master/examples/ring-swagger).
+
+```clj
+(require '[reitit.ring :as ring]
+(require '[reitit.swagger :as swagger]
+(require '[reitit.swagger-ui :as swagger-ui]
+;; coercion
+(require '[reitit.ring.coercion :as rrc]
+(require '[reitit.coercion.spec :as spec]
+(require '[reitit.coercion.schema :as schema]
+(require '[schema.core :refer [Int]]
+;; web server
+(require '[ring.adapter.jetty :as jetty]
+(require '[ring.middleware.params]
+(require '[muuntaja.middleware]))
+
+(def app
+ (ring/ring-handler
+ (ring/router
+ ["/api"
+ {:swagger {:id ::math}}
+
+ ["/swagger.json"
+ {:get {:no-doc true
+ :swagger {:info {:title "my-api"}}
+ :handler (swagger/create-swagger-handler)}}]
+
+ ["/spec"
+ {:coercion spec/coercion
+ :swagger {:tags ["spec"]}}
+
+ ["/plus"
+ {:get {:summary "plus with spec"
+ :parameters {:query {:x int?, :y int?}}
+ :responses {200 {:body {:total int?}}}
+ :handler (fn [{{{:keys [x y]} :query} :parameters}]
+ {:status 200
+ :body {:total (+ x y)}})}}]]
+
+ ["/schema"
+ {:coercion schema/coercion
+ :swagger {:tags ["schema"]}}
+
+ ["/plus"
+ {:get {:summary "plus with schema"
+ :parameters {:query {:x Int, :y Int}}
+ :responses {200 {:body {:total Int}}}
+ :handler (fn [{{{:keys [x y]} :query} :parameters}]
+ {:status 200
+ :body {:total (+ x y)}})}}]]]
+
+ {:data {:middleware [ring.middleware.params/wrap-params
+ muuntaja.middleware/wrap-format
+ swagger/swagger-feature
+ rrc/coerce-exceptions-middleware
+ rrc/coerce-request-middleware
+ rrc/coerce-response-middleware]
+ :swagger {:produces #{"application/json"
+ "application/edn"
+ "application/transit+json"}
+ :consumes #{"application/json"
+ "application/edn"
+ "application/transit+json"}}}})
+ (ring/routes
+ (swagger-ui/create-swagger-ui-handler
+ {:path "", :url "/api/swagger.json"})
+ (ring/create-default-handler))))
+
+(defn start []
+ (jetty/run-jetty #'app {:port 3000, :join? false})
+ (println "server running in port 3000"))
+```
+
+http://localhost:3000 should render now the swagger-ui:
+
+
+
+## Advanced
+
+Route data in path `[:swagger :id]` can be either a keyword or a sequence of keywords. This enables one route to be part of multiple swagger apis. Normal route data [scoping rules](../basics/route_data.html#nested-route-data) rules apply.
+
+Example with:
+
+* 4 routes
+* 2 swagger apis `::one` and `::two`
+* 3 swagger specs
+
+```clj
+(require '[reitit.ring :as ring])
+(require '[reitit.swagger :as swagger])
+
+(def ping-route
+ ["/ping" {:get (constantly "ping")}])
+
+(def spec-route
+ ["/swagger.json"
+ {:get {:no-doc true
+ :handler (swagger/create-swagger-handler)}}])
+
+(def app
+ (ring/ring-handler
+ (ring/router
+ [["/common" {:swagger {:id #{::one ::two}}} ping-route]
+ ["/one" {:swagger {:id ::one}} ping-route spec-route]
+ ["/two" {:swagger {:id ::two}} ping-route spec-route
+ ["/deep" {:swagger {:id ::one}} ping-route]]
+ ["/one-two" {:swagger {:id #{::one ::two}}} spec-route]])))
+```
+
+```clj
+(-> {:request-method :get, :uri "/one/swagger.json"} app :body :paths keys)
+; ("/common/ping" "/one/ping" "/two/deep/ping")
+```
+
+```clj
+(-> {:request-method :get, :uri "/two/swagger.json"} app :body :paths keys)
+; ("/common/ping" "/two/ping")
+```
+
+```clj
+(-> {:request-method :get, :uri "/one-two/swagger.json"} app :body :paths keys)
+; ("/common/ping" "/one/ping" "/two/ping" "/two/deep/ping")
+```
+
+### TODO
+
+* create a data-driven version of [Muuntaja](https://github.com/metosin/muuntaja) that integrates into `:produces` and `:consumes`
+* ClojureScript
+ * example for [Macchiato](https://github.com/macchiato-framework)
+ * body formatting
+ * resource handling
diff --git a/doc/swagger.md b/doc/swagger.md
deleted file mode 100644
index e410464c..00000000
--- a/doc/swagger.md
+++ /dev/null
@@ -1,58 +0,0 @@
-# Swagger
-
-Reitit supports [Swagger](https://swagger.io/) to generate route documentation. Documentation is extracted from existing coercion definitions `:parameters`, `:responses` and from a set of new doumentation keys.
-
-### Example
-
-Current `reitit-swagger` draft (with `reitit-ring` & data-specs):
-
-
-```clj
-(require '[reitit.ring :as ring])
-(require '[reitit.ring.swagger :as swagger])
-(require '[reitit.ring.coercion :as rrc])
-(require '[reitit.coercion.spec :as spec])
-
-(def app
- (ring/ring-handler
- (ring/router
- ["/api"
-
- ;; identify a swagger api
- ;; there can be several in a routing tree
- {:swagger {:id :math}}
-
- ;; the (undocumented) swagger spec endpoint
- ["/swagger.json"
- {:get {:no-doc true
- :swagger {:info {:title "my-api"}}
- :handler (swagger/create-swagger-handler)}}]
-
- ;; the (undocumented) swagger-ui
- ;; [org.webjars/swagger-ui "3.13.4"]
- ["/docs/*"
- {:get {:no-doc true
- :handler (ring/create-resource-handler
- {:root "META-INF/resources/webjars/swagger-ui"})}}]
-
- ["/minus"
- {:get {:summary "minus"
- :parameters {:query {:x int?, :y int?}}
- :responses {200 {:body {:total int?}}}
- :handler (fn [{{{:keys [x y]} :query} :parameters}]
- {:status 200, :body {:total (- x y)}})}}]
-
- ["/plus"
- {:get {:summary "plus"
- :parameters {:query {:x int?, :y int?}}
- :responses {200 {:body {:total int?}}}
- :handler (fn [{{{:keys [x y]} :query} :parameters}]
- {:status 200, :body {:total (+ x y)}})}}]]
-
- {:data {:middleware [rrc/coerce-exceptions-middleware
- rrc/coerce-request-middleware
- rrc/coerce-response-middleware
- ;; provides just route data specs
- swagger/swagger-feature]
- :coercion spec/coercion}})))
-```
diff --git a/examples/ring-swagger/project.clj b/examples/ring-swagger/project.clj
index a47a801f..f3133c0c 100644
--- a/examples/ring-swagger/project.clj
+++ b/examples/ring-swagger/project.clj
@@ -3,6 +3,5 @@
:dependencies [[org.clojure/clojure "1.9.0"]
[ring "1.6.3"]
[metosin/muuntaja "0.5.0"]
- [org.webjars/swagger-ui "3.13.6"]
[metosin/reitit "0.1.1-SNAPSHOT"]]
:repl-options {:init-ns example.server})
diff --git a/examples/ring-swagger/src/example/server.clj b/examples/ring-swagger/src/example/server.clj
index 0dbef160..03033c6c 100644
--- a/examples/ring-swagger/src/example/server.clj
+++ b/examples/ring-swagger/src/example/server.clj
@@ -1,6 +1,7 @@
(ns example.server
(:require [reitit.ring :as ring]
[reitit.swagger :as swagger]
+ [reitit.swagger-ui :as swagger-ui]
[reitit.ring.coercion :as rrc]
[reitit.coercion.spec :as spec]
[reitit.coercion.schema :as schema]
@@ -13,43 +14,53 @@
(def app
(ring/ring-handler
(ring/router
- [["/api"
- {:swagger {:id ::math}}
+ ["/api"
+ {:swagger {:id ::math}}
- ["/swagger.json"
- {:get {:no-doc true
- :swagger {:info {:title "my-api"}}
- :handler (swagger/create-swagger-handler)}}]
+ ["/swagger.json"
+ {:get {:no-doc true
+ :swagger {:info {:title "my-api"}}
+ :handler (swagger/create-swagger-handler)}}]
- ["/spec" {:coercion spec/coercion}
- ["/plus"
- {:get {:summary "plus"
- :parameters {:query {:x int?, :y int?}}
- :responses {200 {:body {:total int?}}}
- :handler (fn [{{{:keys [x y]} :query} :parameters}]
- {:status 200, :body {:total (+ x y)}})}}]]
+ ["/spec"
+ {:coercion spec/coercion
+ :swagger {:tags ["spec"]}}
- ["/schema" {:coercion schema/coercion}
- ["/plus"
- {:get {:summary "plus"
- :parameters {:query {:x Int, :y Int}}
- :responses {200 {:body {:total Int}}}
- :handler (fn [{{{:keys [x y]} :query} :parameters}]
- {:status 200, :body {:total (+ x y)}})}}]]]
+ ["/plus"
+ {:get {:summary "plus with spec"
+ :parameters {:query {:x int?, :y int?}}
+ :responses {200 {:body {:total int?}}}
+ :handler (fn [{{{:keys [x y]} :query} :parameters}]
+ {:status 200
+ :body {:total (+ x y)}})}}]]
- ["/api-docs/*"
- {:no-doc true
- :handler (ring/create-resource-handler
- {:root "META-INF/resources/webjars/swagger-ui/3.13.6"})}]]
+ ["/schema"
+ {:coercion schema/coercion
+ :swagger {:tags ["schema"]}}
+
+ ["/plus"
+ {:get {:summary "plus with schema"
+ :parameters {:query {:x Int, :y Int}}
+ :responses {200 {:body {:total Int}}}
+ :handler (fn [{{{:keys [x y]} :query} :parameters}]
+ {:status 200
+ :body {:total (+ x y)}})}}]]]
{:data {:middleware [ring.middleware.params/wrap-params
muuntaja.middleware/wrap-format
swagger/swagger-feature
rrc/coerce-exceptions-middleware
rrc/coerce-request-middleware
- rrc/coerce-response-middleware]}})
+ rrc/coerce-response-middleware]
+ :swagger {:produces #{"application/json"
+ "application/edn"
+ "application/transit+json"}
+ :consumes #{"application/json"
+ "application/edn"
+ "application/transit+json"}}}})
(ring/routes
- (ring/create-resource-handler {:path "/"})
+ (swagger-ui/create-swagger-ui-handler
+ {:path "", :url "/api/swagger.json"})
(ring/create-default-handler))))
(defn start []
diff --git a/examples/ring-swagger/swagger-ui.png b/examples/ring-swagger/swagger-ui.png
index e3ae1b66..46d28517 100644
Binary files a/examples/ring-swagger/swagger-ui.png and b/examples/ring-swagger/swagger-ui.png differ
diff --git a/examples/ring-swagger/swagger.png b/examples/ring-swagger/swagger.png
new file mode 100644
index 00000000..46d28517
Binary files /dev/null and b/examples/ring-swagger/swagger.png differ
diff --git a/modules/reitit-core/src/reitit/impl.cljc b/modules/reitit-core/src/reitit/impl.cljc
index 9795cf11..1c654d36 100644
--- a/modules/reitit-core/src/reitit/impl.cljc
+++ b/modules/reitit-core/src/reitit/impl.cljc
@@ -56,7 +56,7 @@
(update-in [:path-parts] conj key)
(update-in [:path-params] conj key)
(assoc-in [:path-constraints key] "([^/]+)"))))
- #"^\*(.+)$" :>> (fn [[_ token]]
+ #"^\*(.*)$" :>> (fn [[_ token]]
(let [key (keyword token)]
(-> out
(update-in [:path-parts] conj key)
diff --git a/modules/reitit-ring/src/reitit/ring.cljc b/modules/reitit-ring/src/reitit/ring.cljc
index 2fd09a98..9d5f830e 100644
--- a/modules/reitit-ring/src/reitit/ring.cljc
+++ b/modules/reitit-ring/src/reitit/ring.cljc
@@ -69,6 +69,8 @@
#?(:clj
;; TODO: optimize for perf
+ ;; TODO: ring.middleware.not-modified/wrap-not-modified
+ ;; TODO: ring.middleware.head/wrap-head
(defn create-resource-handler
"A ring handler for serving classpath resources, configured via options:
@@ -82,19 +84,20 @@
| :allow-symlinks? | allow symlinks that lead to paths outside the root classpath directories, defaults to `false`"
([]
(create-resource-handler nil))
- ([{:keys [parameter root path loader allow-symlinks? index-files]
+ ([{:keys [parameter root path loader allow-symlinks? index-files paths]
:or {parameter (keyword "")
root "public"
- index-files ["index.html"]}}]
+ index-files ["index.html"]
+ paths (constantly nil)}}]
(let [options {:root root, :loader loader, :allow-symlinks? allow-symlinks?}
- path-size (count path)
+ path-size (inc (count path))
create (fn [handler]
(fn
([request] (handler request))
([request respond _] (respond (handler request)))))
resource-response (fn [path accept]
(if-let [path (accept path)]
- (if-let [response (response/resource-response path options)]
+ (if-let [response (or (paths path) (response/resource-response path options))]
(response/content-type response (mime-type/ext-mime-type path)))))
path-or-index-response (fn [path accept]
(or (resource-response path accept)
diff --git a/modules/reitit-spec/src/reitit/coercion/spec.cljc b/modules/reitit-spec/src/reitit/coercion/spec.cljc
index 72e9dc84..d24f08c5 100644
--- a/modules/reitit-spec/src/reitit/coercion/spec.cljc
+++ b/modules/reitit-spec/src/reitit/coercion/spec.cljc
@@ -2,27 +2,33 @@
(:require [clojure.spec.alpha :as s]
[spec-tools.core :as st #?@(:cljs [:refer [Spec]])]
[spec-tools.data-spec :as ds]
- [spec-tools.conform :as conform]
+ [spec-tools.transform :as stt]
[spec-tools.swagger.core :as swagger]
[reitit.coercion :as coercion]
[clojure.set :as set])
#?(:clj
(:import (spec_tools.core Spec))))
-(def string-conforming
- (st/type-conforming
- (merge
- conform/string-type-conforming
- conform/strip-extra-keys-type-conforming)))
+(def string-transformer
+ (st/type-transformer
+ {:name :string
+ :decoders (merge
+ stt/string-type-decoders
+ stt/strip-extra-keys-type-decoders)
+ :encoders stt/string-type-encoders
+ :default-encoder stt/any->any}))
-(def json-conforming
- (st/type-conforming
- (merge
- conform/json-type-conforming
- conform/strip-extra-keys-type-conforming)))
+(def json-transformer
+ (st/type-transformer
+ {:name :json
+ :decoders (merge
+ stt/json-type-decoders
+ stt/strip-extra-keys-type-decoders)
+ :encoders stt/json-type-encoders
+ :default-encoder stt/any->any}))
-(def default-conforming
- ::default)
+(def no-op-transformer
+ st/no-op-transformer)
(defprotocol IntoSpec
(into-spec [this name]))
@@ -58,12 +64,12 @@
(def default-options
{:coerce-response? coerce-response?
- :conforming {:body {:default default-conforming
- :formats {"application/json" json-conforming}}
- :string {:default string-conforming}
- :response {:default default-conforming}}})
+ :transformers {:body {:default no-op-transformer
+ :formats {"application/json" json-transformer}}
+ :string {:default string-transformer}
+ :response {:default no-op-transformer}}})
-(defn create [{:keys [conforming coerce-response?] :as opts}]
+(defn create [{:keys [transformers coerce-response?] :as opts}]
^{:type ::coercion/coercion}
(reify coercion/Coercion
(-get-name [_] :spec)
@@ -98,16 +104,16 @@
(update :problems (partial mapv #(update % :pred stringify-pred)))))
(-request-coercer [this type spec]
(let [spec (coercion/-compile-model this spec nil)
- {:keys [formats default]} (conforming type)]
+ {:keys [formats default]} (transformers type)]
(fn [value format]
- (if-let [conforming (or (get formats format) default)]
- (let [conformed (st/conform spec value conforming)]
- (if (s/invalid? conformed)
- (let [problems (st/explain-data spec value conforming)]
+ (if-let [transformer (or (get formats format) default)]
+ (let [transformed (st/conform spec value transformer)]
+ (if (s/invalid? transformed)
+ (let [problems (st/explain-data spec value transformer)]
(coercion/map->CoercionError
{:spec spec
:problems (::s/problems problems)}))
- (s/unform spec conformed)))
+ (s/unform spec transformed)))
value))))
(-response-coercer [this spec]
(if (coerce-response? spec)
diff --git a/modules/reitit-swagger-ui/project.clj b/modules/reitit-swagger-ui/project.clj
new file mode 100644
index 00000000..fa43ea9e
--- /dev/null
+++ b/modules/reitit-swagger-ui/project.clj
@@ -0,0 +1,11 @@
+(defproject metosin/reitit-swagger-ui "0.1.1-SNAPSHOT"
+ :description "Reitit: Swagger-ui support"
+ :url "https://github.com/metosin/reitit"
+ :license {:name "Eclipse Public License"
+ :url "http://www.eclipse.org/legal/epl-v10.html"}
+ :plugins [[lein-parent "0.3.2"]]
+ :parent-project {:path "../../project.clj"
+ :inherit [:deploy-repositories :managed-dependencies]}
+ :dependencies [[metosin/reitit-ring]
+ [metosin/jsonista]
+ [metosin/ring-swagger-ui]])
diff --git a/modules/reitit-swagger-ui/src/reitit/swagger_ui.cljc b/modules/reitit-swagger-ui/src/reitit/swagger_ui.cljc
new file mode 100644
index 00000000..202556a8
--- /dev/null
+++ b/modules/reitit-swagger-ui/src/reitit/swagger_ui.cljc
@@ -0,0 +1,52 @@
+(ns reitit.swagger-ui
+ (:require [clojure.string :as str]
+ [reitit.ring :as ring]
+ #?@(:clj [
+ [jsonista.core :as j]])))
+
+#?(:clj
+ (defn create-swagger-ui-handler
+ "Creates a ring handler which can be used to serve swagger-ui.
+
+ | key | description |
+ | -----------------|-------------|
+ | :parameter | optional name of the wildcard parameter, defaults to unnamed keyword `:`
+ | :root | optional resource root, defaults to `\"swagger-ui\"`
+ | :url | path to swagger endpoint, defaults to `/swagger.json`
+ | :path | optional path to mount the handler to. Works only if mounted outside of a router.
+ | :config | parameters passed to swaggger-ui, keys transformed into camelCase.
+
+ See https://github.com/swagger-api/swagger-ui/blob/master/docs/usage/configuration.md
+ for all available :config options
+
+ Examples:
+
+ ;; with defaults
+ (create-swagger-ui-handler)
+
+ ;; with path and url set, swagger validator disabled
+ (swagger-ui/create-swagger-ui-handler
+ {:path \"\"
+ :url \"/api/swagger.json\"
+ :config {:validator-url nil})"
+ ([]
+ (create-swagger-ui-handler nil))
+ ([options]
+ (let [mixed-case (fn [k]
+ (let [[f & rest] (str/split (name k) #"-")]
+ (apply str (str/lower-case f) (map str/capitalize rest))))
+ mixed-case-key (fn [[k v]] [(mixed-case k) v])
+ config-json (fn [{:keys [url config]}] (j/write-value-as-string (merge config {:url url})))
+ conf-js (fn [opts] (str "window.API_CONF = " (config-json opts) ";"))
+ options (as-> options $
+ (update $ :root (fnil identity "swagger-ui"))
+ (update $ :url (fnil identity "/swagger.json"))
+ (update $ :config #(->> % (map mixed-case-key) (into {})))
+ (assoc $ :paths {"conf.js" {:headers {"Content-Type" "application/javascript"}
+ :status 200
+ :body (conf-js $)}
+ "config.json" {:headers {"Content-Type" "application/json"}
+ :status 200
+ :body (config-json $)}}))]
+ (ring/routes
+ (ring/create-resource-handler options))))))
diff --git a/modules/reitit-swagger/src/reitit/swagger.cljc b/modules/reitit-swagger/src/reitit/swagger.cljc
index 0a296dac..1679a061 100644
--- a/modules/reitit-swagger/src/reitit/swagger.cljc
+++ b/modules/reitit-swagger/src/reitit/swagger.cljc
@@ -5,7 +5,7 @@
[clojure.set :as set]
[reitit.coercion :as coercion]))
-(s/def ::id keyword?)
+(s/def ::id (s/or :keyword keyword? :set (s/coll-of keyword? :into #{})))
(s/def ::no-doc boolean?)
(s/def ::tags (s/coll-of (s/or :keyword keyword? :string string?) :kind #{}))
(s/def ::summary string?)
@@ -21,16 +21,11 @@
documentation keys for the route data. Should be accompanied by a
[[swagger-spec-handler]] to expose the swagger spec.
- Swagger-specific keys:
-
- | key | description |
- | --------------|-------------|
- | :swagger | map of any swagger-data. Must have `:id` to identify the api
-
- The following common keys also contribute to swagger spec:
+ New route data keys contributing to swagger docs:
| key | description |
| --------------|-------------|
+ | :swagger | map of any swagger-data. Must have `:id` (keyword or sequence of keywords) to identify the api
| :no-doc | optional boolean to exclude endpoint from api docs
| :tags | optional set of strings of keywords tags for an endpoint api docs
| :summary | optional short string summary of an endpoint
@@ -38,6 +33,8 @@
Also the coercion keys contribute to swagger spec:
+ | key | description |
+ | --------------|-------------|
| :parameters | optional input parameters for a route, in a format defined by the coercion
| :responses | optional descriptions of responess, in a format defined by coercion
@@ -66,11 +63,16 @@
:spec ::spec})
(defn create-swagger-handler []
- "Create a ring handler to emit swagger spec."
+ "Create a ring handler to emit swagger spec. Collects all routes from router which have
+ an intersecting `[:swagger :id]` and which are not marked with `:no-doc` route data."
(fn [{:keys [::r/router ::r/match :request-method]}]
(let [{:keys [id] :as swagger} (-> match :result request-method :data :swagger)
- swagger (set/rename-keys swagger {:id :x-id})
- accept-route #(-> % second :swagger :id (= id))
+ ->set (fn [x] (if (or (set? x) (sequential? x)) (set x) (conj #{} x)))
+ ids (->set id)
+ swagger (->> (dissoc swagger :id)
+ (merge {:swagger "2.0"
+ :x-id ids}))
+ accept-route #(-> % second :swagger :id ->set (set/intersection ids) seq)
transform-endpoint (fn [[method {{:keys [coercion no-doc swagger] :as data} :data}]]
(if (and data (not no-doc))
[method
diff --git a/modules/reitit/project.clj b/modules/reitit/project.clj
index 9263d98b..21b5b664 100644
--- a/modules/reitit/project.clj
+++ b/modules/reitit/project.clj
@@ -10,4 +10,5 @@
[metosin/reitit-ring]
[metosin/reitit-spec]
[metosin/reitit-schema]
- [metosin/reitit-swagger]])
+ [metosin/reitit-swagger]
+ [metosin/reitit-swagger-ui]])
diff --git a/project.clj b/project.clj
index 257c8415..7a284a27 100644
--- a/project.clj
+++ b/project.clj
@@ -15,11 +15,14 @@
[metosin/reitit-spec "0.1.1-SNAPSHOT"]
[metosin/reitit-schema "0.1.1-SNAPSHOT"]
[metosin/reitit-swagger "0.1.1-SNAPSHOT"]
+ [metosin/reitit-swagger-ui "0.1.1-SNAPSHOT"]
[meta-merge "1.0.0"]
[ring/ring-core "1.6.3"]
- [metosin/spec-tools "0.6.2-SNAPSHOT"]
- [metosin/schema-tools "0.10.2-SNAPSHOT"]]
+ [metosin/spec-tools "0.7.0"]
+ [metosin/schema-tools "0.10.2"]
+ [metosin/ring-swagger-ui "2.2.10"]
+ [metosin/jsonista "0.2.0"]]
:plugins [[jonase/eastwood "0.2.5"]
[lein-doo "0.1.10"]
@@ -36,10 +39,11 @@
"modules/reitit-ring/src"
"modules/reitit-spec/src"
"modules/reitit-schema/src"
- "modules/reitit-swagger/src"]
+ "modules/reitit-swagger/src"
+ "modules/reitit-swagger-ui/src"]
:dependencies [[org.clojure/clojure "1.9.0"]
- [org.clojure/clojurescript "1.9.946"]
+ [org.clojure/clojurescript "1.10.238"]
;; modules dependencies
[metosin/reitit]
@@ -50,12 +54,13 @@
[ring "1.6.3"]
[metosin/muuntaja "0.5.0"]
- [metosin/jsonista "0.1.1"]
+ [metosin/jsonista "0.2.0"]
+ [metosin/ring-swagger-ui "2.2.10"]
[criterium "0.4.4"]
[org.clojure/test.check "0.9.0"]
[org.clojure/tools.namespace "0.2.11"]
- [com.gfredericks/test.chuck "0.2.8"]]}
+ [com.gfredericks/test.chuck "0.2.9"]]}
:perf {:jvm-opts ^:replace ["-server"
"-Xmx4096m"
"-Dclojure.compiler.direct-linking=true"]
diff --git a/scripts/lein-modules b/scripts/lein-modules
index e93284b9..daf815fe 100755
--- a/scripts/lein-modules
+++ b/scripts/lein-modules
@@ -3,6 +3,6 @@
set -e
# Modules
-for ext in reitit-core reitit-ring reitit-spec reitit-schema reitit-swagger reitit; do
+for ext in reitit-core reitit-ring reitit-spec reitit-schema reitit-swagger reitit-swagger-ui reitit; do
cd modules/$ext; lein "$@"; cd ../..;
done
diff --git a/test/cljc/reitit/ring_coercion_test.cljc b/test/cljc/reitit/ring_coercion_test.cljc
index a3e708ea..11eb5141 100644
--- a/test/cljc/reitit/ring_coercion_test.cljc
+++ b/test/cljc/reitit/ring_coercion_test.cljc
@@ -84,11 +84,11 @@
(app valid-request))))
(testing "invalid request"
- (let [{:keys [status body]} (app invalid-request)]
+ (let [{:keys [status]} (app invalid-request)]
(is (= 400 status))))
(testing "invalid response"
- (let [{:keys [status body]} (app invalid-request2)]
+ (let [{:keys [status]} (app invalid-request2)]
(is (= 500 status))))))))
(deftest schema-coercion-test
diff --git a/test/cljc/reitit/ring_test.cljc b/test/cljc/reitit/ring_test.cljc
index eb7d265c..c803a320 100644
--- a/test/cljc/reitit/ring_test.cljc
+++ b/test/cljc/reitit/ring_test.cljc
@@ -271,7 +271,9 @@
(ring/ring-handler
(ring/router
[["/ping" (constantly {:status 200, :body "pong"})]
- ["/files/*" (ring/create-resource-handler)]])
+ ["/files/*" (ring/create-resource-handler)]
+ ["/*" (ring/create-resource-handler)]]
+ {:conflicts (constantly nil)})
(ring/create-default-handler))]
["outside of a router"
@@ -280,34 +282,37 @@
["/ping" (constantly {:status 200, :body "pong"})])
(ring/routes
(ring/create-resource-handler {:path "/files"})
- (ring/create-default-handler)))]]]
+ (ring/create-resource-handler {:path "/"})
+ (ring/create-default-handler)))]]
+ prefix ["/" "/files"]
+ :let [request (fn [uri] {:uri (str prefix uri), :request-method :get})]]
(testing test
(testing "different file-types"
- (let [response (app {:uri "/files/hello.json", :request-method :get})]
+ (let [response (app (request "/hello.json"))]
(is (= "application/json" (get-in response [:headers "Content-Type"])))
(is (get-in response [:headers "Last-Modified"]))
(is (= "{\"hello\": \"file\"}" (slurp (:body response)))))
- (let [response (app {:uri "/files/hello.xml", :request-method :get})]
+ (let [response (app (request "/hello.xml"))]
(is (= "text/xml" (get-in response [:headers "Content-Type"])))
(is (get-in response [:headers "Last-Modified"]))
(is (= "