mirror of
https://github.com/metosin/reitit.git
synced 2025-12-18 08:51:12 +00:00
Merge master into url-decode-path-params
This commit is contained in:
commit
c81dad4f94
57 changed files with 2088 additions and 360 deletions
51
CHANGELOG.md
51
CHANGELOG.md
|
|
@ -1,11 +1,55 @@
|
||||||
## UNRELEASED
|
## 0.2.0-SNAPSHOT
|
||||||
|
|
||||||
## `reitit-core`
|
## `reitit-core`
|
||||||
|
|
||||||
* **BREAKING**: the router option key to extract body format has been renamed: `:extract-request-format` => `:reitit.coercion/extract-request-format`
|
* **BREAKING**: the router option key to extract body format has been renamed: `:extract-request-format` => `:reitit.coercion/extract-request-format`
|
||||||
* should only concern you if you are not using [Muuntaja](https://github.com/metosin/muuntaja).
|
* should only concern you if you are not using [Muuntaja](https://github.com/metosin/muuntaja).
|
||||||
* the `r/routes` returns just the path + data tuples as documented, not the compiled route results. To get the compiled results, use `r/compiled-routes` instead.
|
* the `r/routes` returns just the path + data tuples as documented, not the compiled route results. To get the compiled results, use `r/compiled-routes` instead.
|
||||||
|
* new [faster](https://github.com/metosin/reitit/blob/master/perf-test/clj/reitit/impl_perf_test.clj) and more correct encoders and decoders for query & path params.
|
||||||
|
* query-parameters are encoded with `reitit.impl/form-encode`, so spaces are `+` instead of `%20`.
|
||||||
|
* correctly read `:header` params from request `:headers`, not `:header-params`
|
||||||
* welcome route name conflict resolution! If router has routes with same names, router can't be created. fix 'em.
|
* welcome route name conflict resolution! If router has routes with same names, router can't be created. fix 'em.
|
||||||
|
* sequential child routes are allowed, enabling this:
|
||||||
|
|
||||||
|
```clj
|
||||||
|
(-> ["/api"
|
||||||
|
(for [i (range 4)]
|
||||||
|
[(str "/" i)])]
|
||||||
|
(r/router)
|
||||||
|
(r/routes))
|
||||||
|
;[["/api/0" {}]
|
||||||
|
; ["/api/1" {}]
|
||||||
|
; ["/api/2" {}]
|
||||||
|
; ["/api/3" {}]]
|
||||||
|
```
|
||||||
|
|
||||||
|
* A [Guide to compose routers](https://metosin.github.io/reitit/advanced/composing_routers.html)
|
||||||
|
* Welcome Middleware and Intercetor Registries!
|
||||||
|
* when Keywords are used in place of middleware / interceptor, a lookup is done into Router option `::middleware/registry` (or `::interceptor/registry`) with the key. Fails fast with missing registry entries.
|
||||||
|
* fixes [#32](https://github.com/metosin/reitit/issues/32).
|
||||||
|
* full documentation [here](https://metosin.github.io/reitit/ring/middleware_registry.html).
|
||||||
|
|
||||||
|
```clj
|
||||||
|
(require '[reitit.ring :as ring])
|
||||||
|
(require '[reitit.middleware :as middleware])
|
||||||
|
|
||||||
|
(defn wrap-bonus [handler value]
|
||||||
|
(fn [request]
|
||||||
|
(handler (update request :bonus (fnil + 0) value))))
|
||||||
|
|
||||||
|
(def app
|
||||||
|
(ring/ring-handler
|
||||||
|
(ring/router
|
||||||
|
["/api" {:middleware [[:bonus 20]]}
|
||||||
|
["/bonus" {:middleware [:bonus10]
|
||||||
|
:get (fn [{:keys [bonus]}]
|
||||||
|
{:status 200, :body {:bonus bonus}})}]]
|
||||||
|
{::middleware/registry {:bonus wrap-bonus
|
||||||
|
:bonus10 [:bonus 10]}})))
|
||||||
|
|
||||||
|
(app {:request-method :get, :uri "/api/bonus"})
|
||||||
|
; {:status 200, :body {:bonus 30}}
|
||||||
|
```
|
||||||
|
|
||||||
## `reitit-swagger`
|
## `reitit-swagger`
|
||||||
|
|
||||||
|
|
@ -26,6 +70,11 @@
|
||||||
(swagger-ui/create-swagger-ui-handler {:path "/"}))
|
(swagger-ui/create-swagger-ui-handler {:path "/"}))
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## `reitit-middleware`
|
||||||
|
|
||||||
|
* A new module with common data-driven middleware: exception handling, content negotiation & multipart requests. See [the docs](https://metosin.github.io/reitit/ring/default_middleware.html).
|
||||||
|
|
||||||
|
|
||||||
## `reitit-swagger-ui`
|
## `reitit-swagger-ui`
|
||||||
|
|
||||||
* **BREAKING**: pass swagger-ui `:config` as-is (instead of mixed-casing keys) to swagger-ui, fixes [#109](https://github.com/metosin/reitit/issues/109):
|
* **BREAKING**: pass swagger-ui `:config` as-is (instead of mixed-casing keys) to swagger-ui, fixes [#109](https://github.com/metosin/reitit/issues/109):
|
||||||
|
|
|
||||||
19
README.md
19
README.md
|
|
@ -20,11 +20,12 @@ See the [full documentation](https://metosin.github.io/reitit/) for details.
|
||||||
## Modules
|
## Modules
|
||||||
|
|
||||||
* `reitit-core` - the routing core
|
* `reitit-core` - the routing core
|
||||||
* [`reitit-ring`](https://metosin.github.io/reitit/ring/ring.html) with [data-driven middleware](https://metosin.github.io/reitit/ring/data_driven_middleware.html)
|
* `reitit-ring` - a [ring router](https://metosin.github.io/reitit/ring/ring.html)
|
||||||
|
* `reitit-middleware` - [common data-driven middleware](https://metosin.github.io/reitit/ring/default_middleware.html) for `reitit-ring`
|
||||||
* `reitit-spec` [clojure.spec](https://clojure.org/about/spec) coercion
|
* `reitit-spec` [clojure.spec](https://clojure.org/about/spec) coercion
|
||||||
* `reitit-schema` [Schema](https://github.com/plumatic/schema) coercion
|
* `reitit-schema` [Schema](https://github.com/plumatic/schema) coercion
|
||||||
* `reitit-swagger` [Swagger2](https://swagger.io/) apidocs
|
* `reitit-swagger` [Swagger2](https://swagger.io/) apidocs
|
||||||
* `reitit-swagger-ui` Integrated [Swagger UI](https://github.com/swagger-api/swagger-ui).
|
* `reitit-swagger-ui` Integrated [Swagger UI](https://github.com/swagger-api/swagger-ui)
|
||||||
|
|
||||||
Bubblin' under:
|
Bubblin' under:
|
||||||
|
|
||||||
|
|
@ -36,18 +37,18 @@ Bubblin' under:
|
||||||
All bundled:
|
All bundled:
|
||||||
|
|
||||||
```clj
|
```clj
|
||||||
[metosin/reitit "0.1.4-SNAPSHOT"]
|
[metosin/reitit "0.2.0-SNAPSHOT"]
|
||||||
```
|
```
|
||||||
|
|
||||||
Optionally, the parts can be required separately:
|
Optionally, the parts can be required separately:
|
||||||
|
|
||||||
```clj
|
```clj
|
||||||
[metosin/reitit-core "0.1.4-SNAPSHOT"]
|
[metosin/reitit-core "0.2.0-SNAPSHOT"]
|
||||||
[metosin/reitit-ring "0.1.4-SNAPSHOT"]
|
[metosin/reitit-ring "0.2.0-SNAPSHOT"]
|
||||||
[metosin/reitit-spec "0.1.4-SNAPSHOT"]
|
[metosin/reitit-spec "0.2.0-SNAPSHOT"]
|
||||||
[metosin/reitit-schema "0.1.4-SNAPSHOT"]
|
[metosin/reitit-schema "0.2.0-SNAPSHOT"]
|
||||||
[metosin/reitit-swagger "0.1.4-SNAPSHOT"]
|
[metosin/reitit-swagger "0.2.0-SNAPSHOT"]
|
||||||
[metosin/reitit-swagger-ui "0.1.4-SNAPSHOT"]
|
[metosin/reitit-swagger-ui "0.2.0-SNAPSHOT"]
|
||||||
```
|
```
|
||||||
|
|
||||||
## Quick start
|
## Quick start
|
||||||
|
|
|
||||||
|
|
@ -25,18 +25,18 @@ Modules:
|
||||||
To use Reitit, add the following dependency to your project:
|
To use Reitit, add the following dependency to your project:
|
||||||
|
|
||||||
```clj
|
```clj
|
||||||
[metosin/reitit "0.1.4-SNAPSHOT"]
|
[metosin/reitit "0.2.0-SNAPSHOT"]
|
||||||
```
|
```
|
||||||
|
|
||||||
Optionally, the parts can be required separately:
|
Optionally, the parts can be required separately:
|
||||||
|
|
||||||
```clj
|
```clj
|
||||||
[metosin/reitit-core "0.1.4-SNAPSHOT"]
|
[metosin/reitit-core "0.2.0-SNAPSHOT"]
|
||||||
[metosin/reitit-ring "0.1.4-SNAPSHOT"]
|
[metosin/reitit-ring "0.2.0-SNAPSHOT"]
|
||||||
[metosin/reitit-spec "0.1.4-SNAPSHOT"]
|
[metosin/reitit-spec "0.2.0-SNAPSHOT"]
|
||||||
[metosin/reitit-schema "0.1.4-SNAPSHOT"]
|
[metosin/reitit-schema "0.2.0-SNAPSHOT"]
|
||||||
[metosin/reitit-swagger "0.1.4-SNAPSHOT"]
|
[metosin/reitit-swagger "0.2.0-SNAPSHOT"]
|
||||||
[metosin/reitit-swagger-ui "0.1.4-SNAPSHOT"]
|
[metosin/reitit-swagger-ui "0.2.0-SNAPSHOT"]
|
||||||
[metosin/frontend "0.1.4-SNAPSHOT"]
|
[metosin/frontend "0.1.4-SNAPSHOT"]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,8 @@
|
||||||
* [Static Resources](ring/static.md)
|
* [Static Resources](ring/static.md)
|
||||||
* [Dynamic Extensions](ring/dynamic_extensions.md)
|
* [Dynamic Extensions](ring/dynamic_extensions.md)
|
||||||
* [Data-driven Middleware](ring/data_driven_middleware.md)
|
* [Data-driven Middleware](ring/data_driven_middleware.md)
|
||||||
|
* [Middleware Registry](ring/middleware_registry.md)
|
||||||
|
* [Default Middleware](ring/default_middleware.md)
|
||||||
* [Pluggable Coercion](ring/coercion.md)
|
* [Pluggable Coercion](ring/coercion.md)
|
||||||
* [Route Data Validation](ring/route_data_validation.md)
|
* [Route Data Validation](ring/route_data_validation.md)
|
||||||
* [Compiling Middleware](ring/compiling_middleware.md)
|
* [Compiling Middleware](ring/compiling_middleware.md)
|
||||||
|
|
|
||||||
|
|
@ -1,79 +1,413 @@
|
||||||
# Composing Routers
|
# Composing Routers
|
||||||
|
|
||||||
Routers expose both their routes and options via the `Router` protocol, enabling one to create new routers from existing ones.
|
Data-driven approach in `reitit` allows us to compose routes, route data, route specs, middleware and interceptors chains. We can compose routers too. This is needed to achieve dynamic routing like in [Compojure](https://github.com/weavejester/compojure).
|
||||||
|
|
||||||
## Adding routes to an existing routers
|
## Immutatability
|
||||||
|
|
||||||
Let's define a router in an `Atom`:
|
Once a router is created, the routing tree is immutable and cannot be changed. To change the routing, we need to create a new router with changed routes and/or options. For this, the `Router` protocol exposes it's resolved routes via `r/routes` and options via `r/options`.
|
||||||
|
|
||||||
|
## Adding routes
|
||||||
|
|
||||||
|
Let's create a router:
|
||||||
|
|
||||||
```clj
|
```clj
|
||||||
(require '[reitit.core :as r])
|
(require '[reitit.core :as r])
|
||||||
|
|
||||||
(def router (atom (r/router
|
(def router
|
||||||
[["/foo/bar" identity]
|
(r/router
|
||||||
["/foo/bar/:id" identity]])))
|
[["/foo" ::foo]
|
||||||
|
["/bar/:id" ::bar]]))
|
||||||
(r/routes @router)
|
|
||||||
;[["/foo/bar" {:handler #object[clojure.core$identity]}]
|
|
||||||
; ["/foo/bar/:id" {:handler #object[clojure.core$identity]}]]
|
|
||||||
```
|
```
|
||||||
|
|
||||||
A helper to add new route to a router:
|
We can query the resolved routes and options:
|
||||||
|
|
||||||
```clj
|
```clj
|
||||||
(defn add-route [router route]
|
(r/routes router)
|
||||||
|
;[["/foo" {:name :user/foo}]
|
||||||
|
; ["/bar/:id" {:name :user/bar}]]
|
||||||
|
|
||||||
|
(r/options router)
|
||||||
|
;{:lookup #object[...]
|
||||||
|
; :expand #object[...]
|
||||||
|
; :coerce #object[...]
|
||||||
|
; :compile #object[...]
|
||||||
|
; :conflicts #object[...]}
|
||||||
|
```
|
||||||
|
|
||||||
|
Let's add a helper function to create a new router with extra routes:
|
||||||
|
|
||||||
|
```clj
|
||||||
|
(defn add-routes [router routes]
|
||||||
(r/router
|
(r/router
|
||||||
(conj (r/routes router) route)
|
(into (r/routes router) routes)
|
||||||
(r/options router)))
|
(r/options router)))
|
||||||
```
|
```
|
||||||
|
|
||||||
Now, we can add routers to the router:
|
We can now create a new router with an extra routes:
|
||||||
|
|
||||||
```clj
|
```clj
|
||||||
(swap! router add-route ["/foo/bar/:id/:subid" identity])
|
(def router2
|
||||||
|
(add-routes
|
||||||
|
router
|
||||||
|
[["/baz/:id/:subid" ::baz]]))
|
||||||
|
|
||||||
(r/routes @router)
|
(r/routes router2)
|
||||||
;[["/foo/bar" {:handler #object[clojure.core$identity]}]
|
;[["/foo" {:name :user/foo}]
|
||||||
; ["/foo/bar/:id" {:handler #object[clojure.core$identity]}]
|
; ["/bar/:id" {:name :user/bar}]
|
||||||
; ["/foo/bar/:id/:subid" {:handler #object[clojure.core$identity]}]]
|
; ["/baz/:id/:subid" {:name :user/baz}]]
|
||||||
```
|
```
|
||||||
|
|
||||||
Router is recreated, so all the rules are fired:
|
The original router was not changed:
|
||||||
|
|
||||||
```clj
|
```clj
|
||||||
(swap! router add-route ["/foo/:fail" identity])
|
(r/routes router)
|
||||||
;CompilerException clojure.lang.ExceptionInfo: Router contains conflicting routes:
|
;[["/foo" {:name :user/foo}]
|
||||||
|
; ["/bar/:id" {:name :user/bar}]]
|
||||||
|
```
|
||||||
|
|
||||||
|
When a new router is created, all rules are applied, including the conflict resolution:
|
||||||
|
|
||||||
|
```clj
|
||||||
|
(add-routes
|
||||||
|
router2
|
||||||
|
[["/:this/should/:fail" ::fail]])
|
||||||
|
;CompilerException clojure.lang.ExceptionInfo: Router contains conflicting route paths:
|
||||||
;
|
;
|
||||||
; /foo/bar
|
; /baz/:id/:subid
|
||||||
;-> /foo/:fail
|
;-> /:this/should/:fail
|
||||||
```
|
```
|
||||||
|
|
||||||
## Merging routers
|
## Merging routers
|
||||||
|
|
||||||
Given we have two routers:
|
Let's create a helper function to merge routers:
|
||||||
|
|
||||||
```clj
|
```clj
|
||||||
(def r1 (r/router ["/route1" identity]))
|
(defn merge-routers [& routers]
|
||||||
(def r2 (r/router ["/route2" identity]))
|
(r/router
|
||||||
|
(apply merge (map r/routes routers))
|
||||||
|
(apply merge (map r/options routers))))
|
||||||
```
|
```
|
||||||
|
|
||||||
We can create a new router, with merged routes and options:
|
We can now merge multiple routers into one:
|
||||||
|
|
||||||
```clj
|
```clj
|
||||||
(def r12 (r/router
|
(def router
|
||||||
(merge
|
(merge-routers
|
||||||
(r/routes r1)
|
(r/router ["/route1" ::route1])
|
||||||
(r/routes r2))
|
(r/router ["/route2" ::route2])
|
||||||
(merge
|
(r/router ["/route3" ::route3])))
|
||||||
(r/options r1)
|
|
||||||
(r/options r2))))
|
|
||||||
|
|
||||||
(r/routes r12)
|
(r/routes router)
|
||||||
;[["/route1" {:handler #object[clojure.core$identity]}]
|
;[["/route1" {:name :user/route1}]
|
||||||
; ["/route2" {:handler #object[clojure.core$identity]}]]
|
; ["/route2" {:name :user/route2}]
|
||||||
|
; ["/route3" {:name :user/route3}]]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Nesting routers
|
||||||
|
|
||||||
|
Routers can be nested using the catch-all parameter.
|
||||||
|
|
||||||
|
Here's a router with deeply nested routers under a `:router` key in the route data:
|
||||||
|
|
||||||
|
```clj
|
||||||
|
(def router
|
||||||
|
(r/router
|
||||||
|
[["/ping" :ping]
|
||||||
|
["/olipa/*" {:name :olipa
|
||||||
|
:router (r/router
|
||||||
|
[["/olut" :olut]
|
||||||
|
["/makkara" :makkara]
|
||||||
|
["/kerran/*" {:name :kerran
|
||||||
|
:router (r/router
|
||||||
|
[["/avaruus" :avaruus]
|
||||||
|
["/ihminen" :ihminen]])}]])}]]))
|
||||||
|
```
|
||||||
|
|
||||||
|
Matching by path:
|
||||||
|
|
||||||
|
```clj
|
||||||
|
(r/match-by-path router "/olipa/kerran/iso/kala")
|
||||||
|
;#Match{:template "/olipa/*"
|
||||||
|
; :data {:name :olipa
|
||||||
|
; :router #object[reitit.core$mixed_router]}
|
||||||
|
; :result nil
|
||||||
|
; :path-params {: "kerran/iso/kala"}
|
||||||
|
; :path "/olipa/iso/kala"}
|
||||||
|
```
|
||||||
|
|
||||||
|
That didn't work as we wanted, as the nested routers don't have such a route. The core routing doesn't understand anything the `:router` key, so it only matched against the top-level router, which gave a match for the catch-all path.
|
||||||
|
|
||||||
|
As the `Match` contains all the route data, we can create a new matching function that understands the `:router` key. Below is a function that does recursive matching using the subrouters. It returns either `nil` or a vector of mathces.
|
||||||
|
|
||||||
|
```clj
|
||||||
|
(require '[clojure.string :as str])
|
||||||
|
|
||||||
|
(defn recursive-match-by-path [router path]
|
||||||
|
(if-let [match (r/match-by-path router path)]
|
||||||
|
(if-let [subrouter (-> match :data :router)]
|
||||||
|
(let [subpath (subs path (str/last-index-of (:template match) "/"))]
|
||||||
|
(if-let [submatch (recursive-match-by-path subrouter subpath)]
|
||||||
|
(cons match submatch)))
|
||||||
|
(list match))))
|
||||||
|
```
|
||||||
|
|
||||||
|
With invalid nested path we get now `nil` as expected:
|
||||||
|
|
||||||
|
```clj
|
||||||
|
(recursive-match-by-path router "/olipa/kerran/iso/kala")
|
||||||
|
; nil
|
||||||
|
```
|
||||||
|
|
||||||
|
With valid path we get all the nested matches:
|
||||||
|
|
||||||
|
```clj
|
||||||
|
(recursive-match-by-path router "/olipa/kerran/avaruus")
|
||||||
|
;[#reitit.core.Match{:template "/olipa/*"
|
||||||
|
; :data {:name :olipa
|
||||||
|
; :router #object[reitit.core$mixed_router]}
|
||||||
|
; :result nil
|
||||||
|
; :path-params {: "kerran/avaruus"}
|
||||||
|
; :path "/olipa/kerran/avaruus"}
|
||||||
|
; #reitit.core.Match{:template "/kerran/*"
|
||||||
|
; :data {:name :kerran
|
||||||
|
; :router #object[reitit.core$lookup_router]}
|
||||||
|
; :result nil
|
||||||
|
; :path-params {: "avaruus"}
|
||||||
|
; :path "/kerran/avaruus"}
|
||||||
|
; #reitit.core.Match{:template "/avaruus"
|
||||||
|
; :data {:name :avaruus}
|
||||||
|
; :result nil
|
||||||
|
; :path-params {}
|
||||||
|
; :path "/avaruus"}]
|
||||||
|
```
|
||||||
|
|
||||||
|
Let's create a helper to get only the route names for matches:
|
||||||
|
|
||||||
|
```clj
|
||||||
|
(defn name-path [router path]
|
||||||
|
(some->> (recursive-match-by-path router path)
|
||||||
|
(mapv (comp :name :data))))
|
||||||
|
|
||||||
|
(name-path router "/olipa/kerran/avaruus")
|
||||||
|
; [:olipa :kerran :avaruus]
|
||||||
|
```
|
||||||
|
|
||||||
|
So, we can nest routers, but why would we do that?
|
||||||
|
|
||||||
|
## Dynamic routing
|
||||||
|
|
||||||
|
In all the examples above, the routers were created ahead of time, making the whole route tree effective static. To have more dynamic routing, we can use router references allowing the router to be swapped over time. We can also create fully dynamic routers where the router is re-created for each request. Let's walk through both cases.
|
||||||
|
|
||||||
|
First, we need to modify our matching function to support router references:
|
||||||
|
|
||||||
|
```clj
|
||||||
|
(defn- << [x]
|
||||||
|
(if (instance? clojure.lang.IDeref x)
|
||||||
|
(deref x) x))
|
||||||
|
|
||||||
|
(defn recursive-match-by-path [router path]
|
||||||
|
(if-let [match (r/match-by-path (<< router) path)]
|
||||||
|
(if-let [subrouter (-> match :data :router <<)]
|
||||||
|
(let [subpath (subs path (str/last-index-of (:template match) "/"))]
|
||||||
|
(if-let [submatch (recursive-match-by-path subrouter subpath)]
|
||||||
|
(cons match submatch)))
|
||||||
|
(list match))))
|
||||||
|
```
|
||||||
|
|
||||||
|
Then, we need some routers.
|
||||||
|
|
||||||
|
First, a reference to a router that can be updated on background, for example when a new entry in inserted into a database. We'll wrap the router into a `atom`:
|
||||||
|
|
||||||
|
```clj
|
||||||
|
(def beer-router
|
||||||
|
(atom
|
||||||
|
(r/router
|
||||||
|
[["/lager" :lager]])))
|
||||||
|
```
|
||||||
|
|
||||||
|
Second, a reference to router, which is re-created on each routing request:
|
||||||
|
|
||||||
|
```clj
|
||||||
|
(def dynamic-router
|
||||||
|
(reify clojure.lang.IDeref
|
||||||
|
(deref [_]
|
||||||
|
(r/router
|
||||||
|
["/duo" (keyword (str "duo" (rand-int 100)))]))))
|
||||||
|
```
|
||||||
|
|
||||||
|
We can compose the routers into a system-level static root router:
|
||||||
|
|
||||||
|
```clj
|
||||||
|
(def router
|
||||||
|
(r/router
|
||||||
|
[["/gin/napue" :napue]
|
||||||
|
["/ciders/*" :ciders]
|
||||||
|
["/beers/*" {:name :beers
|
||||||
|
:router beer-router}]
|
||||||
|
["/dynamic/*" {:name :dynamic
|
||||||
|
:router dynamic-router}]]))
|
||||||
|
```
|
||||||
|
|
||||||
|
Matching root routes:
|
||||||
|
|
||||||
|
```clj
|
||||||
|
(name-path router "/vodka/russian")
|
||||||
|
; nil
|
||||||
|
|
||||||
|
(name-path router "/gin/napue")
|
||||||
|
; [:napue]
|
||||||
|
```
|
||||||
|
|
||||||
|
Matching (nested) beer routes:
|
||||||
|
|
||||||
|
```clj
|
||||||
|
(name-path router "/beers/lager")
|
||||||
|
; [:beers :lager]
|
||||||
|
|
||||||
|
(name-path router "/beers/saison")
|
||||||
|
; nil
|
||||||
|
```
|
||||||
|
|
||||||
|
No saison!? Let's add the route:
|
||||||
|
|
||||||
|
```clj
|
||||||
|
(swap! beer-router add-routes [["/saison" :saison]])
|
||||||
|
```
|
||||||
|
|
||||||
|
There we have it:
|
||||||
|
|
||||||
|
```clj
|
||||||
|
(name-path router "/beers/saison")
|
||||||
|
; [:beers :saison]
|
||||||
|
```
|
||||||
|
|
||||||
|
We can't add conflicting routes:
|
||||||
|
|
||||||
|
```clj
|
||||||
|
(swap! beer-router add-routes [["/saison" :saison]])
|
||||||
|
;CompilerException clojure.lang.ExceptionInfo: Router contains conflicting route paths:
|
||||||
|
;
|
||||||
|
; /saison
|
||||||
|
;-> /saison
|
||||||
|
```
|
||||||
|
|
||||||
|
The dynamic routes are re-created on every request:
|
||||||
|
|
||||||
|
```clj
|
||||||
|
(name-path router "/dynamic/duo")
|
||||||
|
; [:dynamic :duo71]
|
||||||
|
|
||||||
|
(name-path router "/dynamic/duo")
|
||||||
|
; [:dynamic :duo55]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Performance
|
||||||
|
|
||||||
|
With nested routers, instead of having to do just one route match, matching is recursive, which adds a small cost. All nested routers need to be of type catch-all at top-level, which is order of magnitude slower than fully static routes. Dynamic routes are the slowest ones, at least two orders of magnitude slower, as the router needs to be recreated for each request.
|
||||||
|
|
||||||
|
A quick benchmark on the recursive lookups:
|
||||||
|
|
||||||
|
| path | time | type
|
||||||
|
|------------------|---------|-----------------------
|
||||||
|
| `/gin/napue` | 40ns | static
|
||||||
|
| `/ciders/weston` | 440ns | catch-all
|
||||||
|
| `/beers/saison` | 600ns | catch-all + static
|
||||||
|
| `/dynamic/duo` | 12000ns | catch-all + dynamic
|
||||||
|
|
||||||
|
The non-recursive lookup for `/gin/napue` is around 23ns.
|
||||||
|
|
||||||
|
Comparing the dynamic routing performance with Compojure:
|
||||||
|
|
||||||
|
```clj
|
||||||
|
(require '[compojure.core :refer [context])
|
||||||
|
|
||||||
|
(def app
|
||||||
|
(context "/dynamic" [] (constantly :duo)))
|
||||||
|
|
||||||
|
(app {:uri "/dynamic/duo" :request-method :get})
|
||||||
|
; :duo
|
||||||
|
```
|
||||||
|
|
||||||
|
| path | time | type
|
||||||
|
|------------------|---------|-----------------------
|
||||||
|
| `/dynamic/duo` | 20000ns | compojure
|
||||||
|
|
||||||
|
Can we make the nester routing faster? Sure. We could use the Router `:compile` hook to compile the nested routers for better performance. We could also allow router creation rules to be disabled, to get the dynamic routing much faster.
|
||||||
|
|
||||||
|
### When to use nested routers?
|
||||||
|
|
||||||
|
Nesting routers is not trivial and because of that, should be avoided. For dynamic (request-time) route generation, it's the only choise. For other cases, nested routes are most likely a better option.
|
||||||
|
|
||||||
|
Let's re-create the previous example with normal route nesting/composition.
|
||||||
|
|
||||||
|
A helper to the root router:
|
||||||
|
|
||||||
|
```clj
|
||||||
|
(defn create-router [beers]
|
||||||
|
(r/router
|
||||||
|
[["/gin/napue" :napue]
|
||||||
|
["/ciders/*" :ciders]
|
||||||
|
["/beers" (for [beer beers]
|
||||||
|
[(str "/" beer) (keyword "beer" beer)])]
|
||||||
|
["/dynamic/*" {:name :dynamic
|
||||||
|
:router dynamic-router}]]))
|
||||||
|
```
|
||||||
|
|
||||||
|
New new root router *reference* and a helper to reset it:
|
||||||
|
|
||||||
|
```clj
|
||||||
|
(def router
|
||||||
|
(atom (create-router nil)))
|
||||||
|
|
||||||
|
(defn reset-router! [beers]
|
||||||
|
(reset! router (create-router beers)))
|
||||||
|
```
|
||||||
|
|
||||||
|
The routing tree:
|
||||||
|
|
||||||
|
```clj
|
||||||
|
(r/routes @router)
|
||||||
|
;[["/gin/napue" {:name :napue}]
|
||||||
|
; ["/ciders/*" {:name :ciders}]
|
||||||
|
; ["/dynamic/*" {:name :dynamic,
|
||||||
|
; :router #object[user$reify__24359]}]]
|
||||||
|
```
|
||||||
|
|
||||||
|
Let's reset the router with some beers:
|
||||||
|
|
||||||
|
```clj
|
||||||
|
(reset-router! ["lager" "sahti" "bock"])
|
||||||
|
```
|
||||||
|
|
||||||
|
We can see that the beer routes are now embedded into the core router:
|
||||||
|
|
||||||
|
```clj
|
||||||
|
(r/routes @router)
|
||||||
|
;[["/gin/napue" {:name :napue}]
|
||||||
|
; ["/ciders/*" {:name :ciders}]
|
||||||
|
; ["/beers/lager" {:name :beer/lager}]
|
||||||
|
; ["/beers/sahti" {:name :beer/sahti}]
|
||||||
|
; ["/beers/bock" {:name :beer/bock}]
|
||||||
|
; ["/dynamic/*" {:name :dynamic,
|
||||||
|
; :router #object[user$reify__24359]}]]
|
||||||
|
```
|
||||||
|
|
||||||
|
And the routing works:
|
||||||
|
|
||||||
|
```clj
|
||||||
|
(name-path @router "/beers/sahti")
|
||||||
|
;[:beer/sahti]
|
||||||
|
```
|
||||||
|
|
||||||
|
All the beer-routes now match in constant time.
|
||||||
|
|
||||||
|
| path | time | type
|
||||||
|
|-----------------|---------|-----------------------
|
||||||
|
| `/beers/sahti` | 40ns | static
|
||||||
|
|
||||||
## TODO
|
## TODO
|
||||||
|
|
||||||
* `reitit.core/merge-routes` to effectively merge routes with route data
|
* add an example how to do dynamic routing with `reitit-ring`
|
||||||
|
* maybe create a `recursive-router` into a separate ns with all `Router` functions implemented correctly? maybe not...
|
||||||
|
* add `reitit.core/merge-routes` to effectively merge routes with route data
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@
|
||||||
{:file "doc/advanced/README.md"}
|
{:file "doc/advanced/README.md"}
|
||||||
["Configuring Routers"
|
["Configuring Routers"
|
||||||
{:file "doc/advanced/configuring_routers.md"}]
|
{:file "doc/advanced/configuring_routers.md"}]
|
||||||
|
["Composing Routers" {:file "doc/advanced/composing_routers.md"}]
|
||||||
["Different Routers" {:file "doc/advanced/different_routers.md"}]
|
["Different Routers" {:file "doc/advanced/different_routers.md"}]
|
||||||
["Route Validation" {:file "doc/advanced/route_validation.md"}]
|
["Route Validation" {:file "doc/advanced/route_validation.md"}]
|
||||||
["Dev Workflow" {:file "doc/advanced/dev_workflow.md"}]]
|
["Dev Workflow" {:file "doc/advanced/dev_workflow.md"}]]
|
||||||
|
|
@ -40,6 +41,7 @@
|
||||||
["Dynamic Extensions" {:file "doc/ring/dynamic_extensions.md"}]
|
["Dynamic Extensions" {:file "doc/ring/dynamic_extensions.md"}]
|
||||||
["Data-driven Middleware"
|
["Data-driven Middleware"
|
||||||
{:file "doc/ring/data_driven_middleware.md"}]
|
{:file "doc/ring/data_driven_middleware.md"}]
|
||||||
|
["Middleware Registry" {:file "doc/ring/middleware_registry.md"}]
|
||||||
["Pluggable Coercion" {:file "doc/ring/coercion.md"}]
|
["Pluggable Coercion" {:file "doc/ring/coercion.md"}]
|
||||||
["Route Data Validation"
|
["Route Data Validation"
|
||||||
{:file "doc/ring/route_data_validation.md"}]
|
{:file "doc/ring/route_data_validation.md"}]
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,8 @@
|
||||||
* [Static Resources](static.md)
|
* [Static Resources](static.md)
|
||||||
* [Dynamic Extensions](dynamic_extensions.md)
|
* [Dynamic Extensions](dynamic_extensions.md)
|
||||||
* [Data-driven Middleware](data_driven_middleware.md)
|
* [Data-driven Middleware](data_driven_middleware.md)
|
||||||
|
* [Middleware Registry](middleware_registry.md)
|
||||||
|
* [Default Middleware](default_middleware.md)
|
||||||
* [Pluggable Coercion](coercion.md)
|
* [Pluggable Coercion](coercion.md)
|
||||||
* [Route Data Validation](route_data_validation.md)
|
* [Route Data Validation](route_data_validation.md)
|
||||||
* [Compiling Middleware](compiling_middleware.md)
|
* [Compiling Middleware](compiling_middleware.md)
|
||||||
|
|
|
||||||
131
doc/ring/default_middleware.md
Normal file
131
doc/ring/default_middleware.md
Normal file
|
|
@ -0,0 +1,131 @@
|
||||||
|
# Default Middleware
|
||||||
|
|
||||||
|
```clj
|
||||||
|
[metosin/reitit-middleware "0.2.0-SNAPSHOT"]
|
||||||
|
```
|
||||||
|
|
||||||
|
Any Ring middleware can be used with `reitit-ring`, but using data-driven middleware is preferred as they are easier to manage and in many cases, yield better performance. `reitit-middleware` contains a set of common ring middleware, lifted into data-driven middleware.
|
||||||
|
|
||||||
|
* [Exception handling](#exception-handling)
|
||||||
|
* [Content negotiation](#content-negotiation)
|
||||||
|
* [Multipart request handling](#multipart-request-handling)
|
||||||
|
|
||||||
|
## Exception handling
|
||||||
|
|
||||||
|
A polished version of [compojure-api](https://github.com/metosin/compojure-api) exception handling. Catches all exceptions and invokes configured exception handler.
|
||||||
|
|
||||||
|
```clj
|
||||||
|
(require '[reitit.ring.middleware.exception :as exception])
|
||||||
|
```
|
||||||
|
|
||||||
|
### `exception/exception-middleware`
|
||||||
|
|
||||||
|
A preconfigured middleware using `exception/default-handlers`. Catches:
|
||||||
|
|
||||||
|
* Request & response [Coercion](coercion.md) exceptions
|
||||||
|
* [Muuntaja](https://github.com/metosin/muuntaja) decode exceptions
|
||||||
|
* Exceptions with `:type` of `:reitit.ring/response`, returning `:response` key from `ex-data`.
|
||||||
|
* Safely all other exceptions
|
||||||
|
|
||||||
|
```clj
|
||||||
|
(require '[reitit.ring :as ring])
|
||||||
|
|
||||||
|
(def app
|
||||||
|
(ring/ring-handler
|
||||||
|
(ring/router
|
||||||
|
["/fail" (fn [_] (throw (Exception. "fail")))]
|
||||||
|
{:data {:middleware [exception/exception-middleware]}})))
|
||||||
|
|
||||||
|
(app {:request-method :get, :uri "/fail"})
|
||||||
|
;{:status 500
|
||||||
|
; :body {:type "exception"
|
||||||
|
; :class "java.lang.Exception"}}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `exception/create-exception-middleware`
|
||||||
|
|
||||||
|
Creates the exception-middleware with custom options. Takes a map of `identifier => exception request => response` that is used to select the exception handler for the thown/raised exception identifier. Exception idenfier is either a `Keyword` or a Exception Class.
|
||||||
|
|
||||||
|
The following handlers special keys are available:
|
||||||
|
|
||||||
|
| key | description
|
||||||
|
|------------------------|-------------
|
||||||
|
| `::exception/default` | a default exception handler if nothing else mathced (default `exception/default-handler`).
|
||||||
|
| `::exception/wrap` | a 3-arity handler to wrap the actual handler `handler exception request => response` (no default).
|
||||||
|
|
||||||
|
The handler is selected from the options map by exception idenfitifier in the following lookup order:
|
||||||
|
|
||||||
|
1) `:type` of exception ex-data
|
||||||
|
2) Class of exception
|
||||||
|
3) `:type` ancestors of exception ex-data
|
||||||
|
4) Super Classes of exception
|
||||||
|
5) The ::default handler
|
||||||
|
|
||||||
|
```clj
|
||||||
|
;; type hierarchy
|
||||||
|
(derive ::error ::exception)
|
||||||
|
(derive ::failure ::exception)
|
||||||
|
(derive ::horror ::exception)
|
||||||
|
|
||||||
|
(defn handler [message exception request]
|
||||||
|
{:status 500
|
||||||
|
:body {:message message
|
||||||
|
:exception (.getClass exception)
|
||||||
|
:data (ex-data exception)
|
||||||
|
:uri (:uri request)}})
|
||||||
|
|
||||||
|
(def exception-middleware
|
||||||
|
(exception/create-exception-middleware
|
||||||
|
(merge
|
||||||
|
exception/default-handlers
|
||||||
|
{;; ex-data with :type ::error
|
||||||
|
::error (partial handler "error")
|
||||||
|
|
||||||
|
;; ex-data with ::exception or ::failure
|
||||||
|
::exception (partial handler "exception")
|
||||||
|
|
||||||
|
;; SQLException and all it's child classes
|
||||||
|
java.sql.SQLException (partial handler "sql-exception")
|
||||||
|
|
||||||
|
;; override the default handler
|
||||||
|
::exception/default (partial handler "default")
|
||||||
|
|
||||||
|
;; print stack-traces for all exceptions
|
||||||
|
::exception/wrap (fn [handler e request]
|
||||||
|
(println "ERROR" (pr-str (:uri request)))
|
||||||
|
(handler e request))})))
|
||||||
|
|
||||||
|
(def app
|
||||||
|
(ring/ring-handler
|
||||||
|
(ring/router
|
||||||
|
["/fail" (fn [_] (throw (ex-info "fail" {:type ::failue})))]
|
||||||
|
{:data {:middleware [exception-middleware]}})))
|
||||||
|
|
||||||
|
(app {:request-method :get, :uri "/fail"})
|
||||||
|
; ERROR "/fail"
|
||||||
|
; => {:status 500,
|
||||||
|
; :body {:message "default"
|
||||||
|
; :exception clojure.lang.ExceptionInfo
|
||||||
|
; :data {:type :user/failue}
|
||||||
|
; :uri "/fail"}}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Content Negotiation
|
||||||
|
|
||||||
|
Wrapper for [Muuntaja](https://github.com/metosin/muuntaja) middleware for content-negotiation, request decoding and response encoding. Reads configuration from route data and emit's [swagger](swagger.md) `:produces` and `:consumes` definitions automatically.
|
||||||
|
|
||||||
|
```clj
|
||||||
|
(require '[reitit.ring.middleware.muuntaja :as muuntaja])
|
||||||
|
```
|
||||||
|
|
||||||
|
## Multipart request handling
|
||||||
|
|
||||||
|
Wrapper for [Ring Multipart Middleware](https://github.com/ring-clojure/ring/blob/master/ring-core/src/ring/middleware/multipart_params.clj). Conditionally mounts to an endpoint only if it has `:multipart` params defined. Emits swagger `:consumes` definitions automatically.
|
||||||
|
|
||||||
|
```clj
|
||||||
|
(require '[reitit.ring.middleware.multipart :as multipart])
|
||||||
|
```
|
||||||
|
|
||||||
|
## Example app
|
||||||
|
|
||||||
|
See an example app with the default middleware in action: https://github.com/metosin/reitit/blob/master/examples/ring-swagger/src/example/server.clj.
|
||||||
63
doc/ring/middleware_registry.md
Normal file
63
doc/ring/middleware_registry.md
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
# Middleware Registry
|
||||||
|
|
||||||
|
The `:middleware` syntax in `reitit-ring` supports also Keywords. Keywords are looked from Middleware Registry, which is a map of `keyword => IntoMiddleware`. Middleware registry should be stored under key `:reitit.middleware/registry` in the router options. If a middleware keyword isn't found in the registry, router creation fails fast with descriptive error message.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
Application using middleware defined in the Middleware Registry:
|
||||||
|
|
||||||
|
```clj
|
||||||
|
(require '[reitit.ring :as ring])
|
||||||
|
(require '[reitit.middleware :as middleware])
|
||||||
|
|
||||||
|
(defn wrap-bonus [handler value]
|
||||||
|
(fn [request]
|
||||||
|
(handler (update request :bonus (fnil + 0) value))))
|
||||||
|
|
||||||
|
(def app
|
||||||
|
(ring/ring-handler
|
||||||
|
(ring/router
|
||||||
|
["/api" {:middleware [[:bonus 20]]}
|
||||||
|
["/bonus" {:middleware [:bonus10]
|
||||||
|
:get (fn [{:keys [bonus]}]
|
||||||
|
{:status 200, :body {:bonus bonus}})}]]
|
||||||
|
{::middleware/registry {:bonus wrap-bonus
|
||||||
|
:bonus10 [:bonus 10]}})))
|
||||||
|
```
|
||||||
|
|
||||||
|
Works as expected:
|
||||||
|
|
||||||
|
```clj
|
||||||
|
(app {:request-method :get, :uri "/api/bonus"})
|
||||||
|
; {:status 200, :body {:bonus 30}}
|
||||||
|
```
|
||||||
|
|
||||||
|
Router creation fails fast if registry doesn't contain the Middleware:
|
||||||
|
|
||||||
|
```clj
|
||||||
|
(def app
|
||||||
|
(ring/ring-handler
|
||||||
|
(ring/router
|
||||||
|
["/api" {:middleware [[:bonus 20]]}
|
||||||
|
["/bonus" {:middleware [:bonus10]
|
||||||
|
:get (fn [{:keys [bonus]}]
|
||||||
|
{:status 200, :body {:bonus bonus}})}]]
|
||||||
|
{::middleware/registry {:bonus wrap-bonus}})))
|
||||||
|
;CompilerException clojure.lang.ExceptionInfo: Middleware :bonus10 not found in registry.
|
||||||
|
;
|
||||||
|
;Available middleware in registry:
|
||||||
|
;
|
||||||
|
;| :id | :description |
|
||||||
|
;|--------+--------------------------------------|
|
||||||
|
;| :bonus | reitit.ring_test$wrap_bonus@59fddabb |
|
||||||
|
```
|
||||||
|
|
||||||
|
## When to use the registry?
|
||||||
|
|
||||||
|
Middleware as Keywords helps to keep the routes (all but handlers) as literal data (e.g. data that evaluates to itself) enabling the routes to be persisted in external formats like EDN-files and databases. Duct is a good example where the [middleware can be referenced from EDN-files](https://github.com/duct-framework/duct/wiki/Configuration). It should be easy to make Duct configuration a Middleware Registry in `reitit-ring`.
|
||||||
|
|
||||||
|
The bad thing it's an extra indirection, making things more complex and removed the default IDE-support of "goto definition" or "look source".
|
||||||
|
|
||||||
|
## TODO
|
||||||
|
|
||||||
|
* a prefilled registry of common middleware in the `reitit-middleware`
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
[Ring](https://github.com/ring-clojure/ring) is a Clojure web applications library inspired by Python's WSGI and Ruby's Rack. By abstracting the details of HTTP into a simple, unified API, Ring allows web applications to be constructed of modular components that can be shared among a variety of applications, web servers, and web frameworks.
|
[Ring](https://github.com/ring-clojure/ring) is a Clojure web applications library inspired by Python's WSGI and Ruby's Rack. By abstracting the details of HTTP into a simple, unified API, Ring allows web applications to be constructed of modular components that can be shared among a variety of applications, web servers, and web frameworks.
|
||||||
|
|
||||||
```clj
|
```clj
|
||||||
[metosin/reitit-ring "0.1.4-SNAPSHOT"]
|
[metosin/reitit-ring "0.2.0-SNAPSHOT"]
|
||||||
```
|
```
|
||||||
|
|
||||||
Ring-router adds support for [handlers](https://github.com/ring-clojure/ring/wiki/Concepts#handlers), [middleware](https://github.com/ring-clojure/ring/wiki/Concepts#middleware) and routing based on `:request-method`. Ring-router is created with `reitit.ring/router` function. It uses a custom route compiler, creating a optimized data structure for handling route matches, with compiled middleware chain & handlers for all request methods. It also ensures that all routes have a `:handler` defined. `reitit.ring/ring-handler` is used to create a Ring handler out of ring-router.
|
Ring-router adds support for [handlers](https://github.com/ring-clojure/ring/wiki/Concepts#handlers), [middleware](https://github.com/ring-clojure/ring/wiki/Concepts#middleware) and routing based on `:request-method`. Ring-router is created with `reitit.ring/router` function. It uses a custom route compiler, creating a optimized data structure for handling route matches, with compiled middleware chain & handlers for all request methods. It also ensures that all routes have a `:handler` defined. `reitit.ring/ring-handler` is used to create a Ring handler out of ring-router.
|
||||||
|
|
@ -80,10 +80,12 @@ Name-based reverse routing:
|
||||||
|
|
||||||
# Middleware
|
# Middleware
|
||||||
|
|
||||||
Middleware can be added with a `:middleware` key, either to top-level or under `:request-method` submap. It's value should be a vector value of the following:
|
Middleware can be added with a `:middleware` key, either to top-level or under `:request-method` submap. It's value should be a vector of any the following:
|
||||||
|
|
||||||
1. normal ring middleware function `handler -> request -> response`
|
1. normal ring middleware function `handler -> request -> response`
|
||||||
2. vector of middleware function `handler ?args -> request -> response` and optinally it's args.
|
2. vector of middleware function `[handler args*] -> request -> response` and it's arguments
|
||||||
|
3. a [data-driven middleware](data_driven_middleware.md) record or a map
|
||||||
|
4. a Keyword name, to lookup the middleware from a [Middleware Registry](middleware_registry.md)
|
||||||
|
|
||||||
A middleware and a handler:
|
A middleware and a handler:
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
# Swagger Support
|
# Swagger Support
|
||||||
|
|
||||||
```
|
```
|
||||||
[metosin/reitit-swagger "0.1.4-SNAPSHOT"]
|
[metosin/reitit-swagger "0.2.0-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.
|
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.
|
||||||
|
|
@ -44,7 +44,7 @@ If you need to post-process the generated spec, just wrap the handler with a cus
|
||||||
[Swagger-ui](https://github.com/swagger-api/swagger-ui) is a user interface to visualize and interact with the Swagger specification. To make things easy, there is a pre-integrated version of the swagger-ui as a separate module.
|
[Swagger-ui](https://github.com/swagger-api/swagger-ui) is a user interface to visualize and interact with the Swagger specification. To make things easy, there is a pre-integrated version of the swagger-ui as a separate module.
|
||||||
|
|
||||||
```
|
```
|
||||||
[metosin/reitit-swagger-ui "0.1.4-SNAPSHOT"]
|
[metosin/reitit-swagger-ui "0.2.0-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:
|
`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:
|
||||||
|
|
|
||||||
|
|
@ -11,9 +11,9 @@
|
||||||
[compojure "1.6.1"]
|
[compojure "1.6.1"]
|
||||||
[hiccup "1.0.5"]
|
[hiccup "1.0.5"]
|
||||||
[org.clojure/clojurescript "1.10.339"]
|
[org.clojure/clojurescript "1.10.339"]
|
||||||
[metosin/reitit "0.1.4-SNAPSHOT"]
|
[metosin/reitit "0.2.0-SNAPSHOT"]
|
||||||
[metosin/reitit-schema "0.1.4-SNAPSHOT"]
|
[metosin/reitit-schema "0.2.0-SNAPSHOT"]
|
||||||
[metosin/reitit-frontend "0.1.4-SNAPSHOT"]
|
[metosin/reitit-frontend "0.2.0-SNAPSHOT"]
|
||||||
;; Just for pretty printting the match
|
;; Just for pretty printting the match
|
||||||
[fipp "0.6.12"]]
|
[fipp "0.6.12"]]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -84,8 +84,7 @@
|
||||||
(swap! match (fn [old-match]
|
(swap! match (fn [old-match]
|
||||||
(if new-match
|
(if new-match
|
||||||
(assoc new-match :controllers (rfc/apply-controllers (:controllers old-match) new-match))))))
|
(assoc new-match :controllers (rfc/apply-controllers (:controllers old-match) new-match))))))
|
||||||
{:use-fragment true
|
{:use-fragment true})
|
||||||
:path-prefix "/"})
|
|
||||||
(r/render [current-page] (.getElementById js/document "app")))
|
(r/render [current-page] (.getElementById js/document "app")))
|
||||||
|
|
||||||
(init!)
|
(init!)
|
||||||
|
|
|
||||||
|
|
@ -11,9 +11,9 @@
|
||||||
[compojure "1.6.1"]
|
[compojure "1.6.1"]
|
||||||
[hiccup "1.0.5"]
|
[hiccup "1.0.5"]
|
||||||
[org.clojure/clojurescript "1.10.339"]
|
[org.clojure/clojurescript "1.10.339"]
|
||||||
[metosin/reitit "0.1.4-SNAPSHOT"]
|
[metosin/reitit "0.2.0-SNAPSHOT"]
|
||||||
[metosin/reitit-schema "0.1.4-SNAPSHOT"]
|
[metosin/reitit-schema "0.2.0-SNAPSHOT"]
|
||||||
[metosin/reitit-frontend "0.1.4-SNAPSHOT"]
|
[metosin/reitit-frontend "0.2.0-SNAPSHOT"]
|
||||||
;; Just for pretty printting the match
|
;; Just for pretty printting the match
|
||||||
[fipp "0.6.12"]]
|
[fipp "0.6.12"]]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,17 @@
|
||||||
|
|
||||||
(defn home-page []
|
(defn home-page []
|
||||||
[:div
|
[:div
|
||||||
[:h2 "Welcome to frontend"]])
|
[:h2 "Welcome to frontend"]
|
||||||
|
|
||||||
|
[:button
|
||||||
|
{:type "button"
|
||||||
|
:on-click #(rfe/push-state ::item {:id 3})}
|
||||||
|
"Item 3"]
|
||||||
|
|
||||||
|
[:button
|
||||||
|
{:type "button"
|
||||||
|
:on-click #(rfe/replace-state ::item {:id 4})}
|
||||||
|
"Replace State Item 4"] ])
|
||||||
|
|
||||||
(defn about-page []
|
(defn about-page []
|
||||||
[:div
|
[:div
|
||||||
|
|
@ -62,8 +72,7 @@
|
||||||
(defn init! []
|
(defn init! []
|
||||||
(rfe/start! routes
|
(rfe/start! routes
|
||||||
(fn [m] (reset! match m))
|
(fn [m] (reset! match m))
|
||||||
{:use-fragment true
|
{:use-fragment true})
|
||||||
:path-prefix "/"})
|
|
||||||
(r/render [current-page] (.getElementById js/document "app")))
|
(r/render [current-page] (.getElementById js/document "app")))
|
||||||
|
|
||||||
(init!)
|
(init!)
|
||||||
|
|
|
||||||
|
|
@ -3,4 +3,4 @@
|
||||||
:dependencies [[org.clojure/clojure "1.9.0"]
|
:dependencies [[org.clojure/clojure "1.9.0"]
|
||||||
[ring "1.6.3"]
|
[ring "1.6.3"]
|
||||||
[metosin/muuntaja "0.4.1"]
|
[metosin/muuntaja "0.4.1"]
|
||||||
[metosin/reitit "0.1.4-SNAPSHOT"]])
|
[metosin/reitit "0.2.0-SNAPSHOT"]])
|
||||||
|
|
|
||||||
|
|
@ -3,4 +3,4 @@
|
||||||
:dependencies [[org.clojure/clojure "1.9.0"]
|
:dependencies [[org.clojure/clojure "1.9.0"]
|
||||||
[ring "1.6.3"]
|
[ring "1.6.3"]
|
||||||
[metosin/muuntaja "0.4.1"]
|
[metosin/muuntaja "0.4.1"]
|
||||||
[metosin/reitit "0.1.4-SNAPSHOT"]])
|
[metosin/reitit "0.2.0-SNAPSHOT"]])
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
[ring.middleware.params]
|
[ring.middleware.params]
|
||||||
[muuntaja.middleware]
|
[muuntaja.middleware]
|
||||||
[reitit.ring :as ring]
|
[reitit.ring :as ring]
|
||||||
[reitit.ring.coercion :as rrc]
|
[reitit.ring.coercion :as coercion]
|
||||||
[example.dspec]
|
[example.dspec]
|
||||||
[example.schema]
|
[example.schema]
|
||||||
[example.spec]))
|
[example.spec]))
|
||||||
|
|
@ -18,9 +18,9 @@
|
||||||
example.spec/routes]
|
example.spec/routes]
|
||||||
{:data {:middleware [ring.middleware.params/wrap-params
|
{:data {:middleware [ring.middleware.params/wrap-params
|
||||||
muuntaja.middleware/wrap-format
|
muuntaja.middleware/wrap-format
|
||||||
rrc/coerce-exceptions-middleware
|
coercion/coerce-exceptions-middleware
|
||||||
rrc/coerce-request-middleware
|
coercion/coerce-request-middleware
|
||||||
rrc/coerce-response-middleware]}})))
|
coercion/coerce-response-middleware]}})))
|
||||||
|
|
||||||
(defn restart []
|
(defn restart []
|
||||||
(swap! server (fn [x]
|
(swap! server (fn [x]
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,5 @@
|
||||||
:description "Reitit Ring App with Swagger"
|
:description "Reitit Ring App with Swagger"
|
||||||
:dependencies [[org.clojure/clojure "1.9.0"]
|
:dependencies [[org.clojure/clojure "1.9.0"]
|
||||||
[ring "1.6.3"]
|
[ring "1.6.3"]
|
||||||
[metosin/muuntaja "0.5.0"]
|
[metosin/reitit "0.2.0-SNAPSHOT"]]
|
||||||
[metosin/reitit "0.1.4-SNAPSHOT"]]
|
|
||||||
:repl-options {:init-ns example.server})
|
:repl-options {:init-ns example.server})
|
||||||
|
|
|
||||||
BIN
examples/ring-swagger/resources/reitit.png
Normal file
BIN
examples/ring-swagger/resources/reitit.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 494 KiB |
|
|
@ -2,28 +2,45 @@
|
||||||
(:require [reitit.ring :as ring]
|
(:require [reitit.ring :as ring]
|
||||||
[reitit.swagger :as swagger]
|
[reitit.swagger :as swagger]
|
||||||
[reitit.swagger-ui :as swagger-ui]
|
[reitit.swagger-ui :as swagger-ui]
|
||||||
[reitit.ring.coercion :as rrc]
|
[reitit.ring.coercion :as coercion]
|
||||||
[reitit.coercion.spec :as spec]
|
[reitit.coercion.spec]
|
||||||
[reitit.coercion.schema :as schema]
|
[reitit.ring.middleware.muuntaja :as muuntaja]
|
||||||
[schema.core :refer [Int]]
|
[reitit.ring.middleware.exception :as exception]
|
||||||
|
[reitit.ring.middleware.multipart :as multipart]
|
||||||
|
[ring.middleware.params :as params]
|
||||||
[ring.adapter.jetty :as jetty]
|
[ring.adapter.jetty :as jetty]
|
||||||
[ring.middleware.params]
|
[muuntaja.core :as m]
|
||||||
[muuntaja.middleware]))
|
[clojure.java.io :as io]))
|
||||||
|
|
||||||
(def app
|
(def app
|
||||||
(ring/ring-handler
|
(ring/ring-handler
|
||||||
(ring/router
|
(ring/router
|
||||||
["/api"
|
[["/swagger.json"
|
||||||
|
|
||||||
["/swagger.json"
|
|
||||||
{:get {:no-doc true
|
{:get {:no-doc true
|
||||||
:swagger {:info {:title "my-api"}}
|
:swagger {:info {:title "my-api"}}
|
||||||
:handler (swagger/create-swagger-handler)}}]
|
:handler (swagger/create-swagger-handler)}}]
|
||||||
|
|
||||||
["/spec"
|
["/files"
|
||||||
{:coercion spec/coercion
|
{:swagger {:tags ["files"]}}
|
||||||
:swagger {:tags ["spec"]}}
|
|
||||||
|
["/upload"
|
||||||
|
{:post {:summary "upload a file"
|
||||||
|
:parameters {:multipart {:file multipart/temp-file-part}}
|
||||||
|
:responses {200 {:body {:file multipart/temp-file-part}}}
|
||||||
|
:handler (fn [{{{:keys [file]} :multipart} :parameters}]
|
||||||
|
{:status 200
|
||||||
|
:body {:file file}})}}]
|
||||||
|
|
||||||
|
["/download"
|
||||||
|
{:get {:summary "downloads a file"
|
||||||
|
:swagger {:produces ["image/png"]}
|
||||||
|
:handler (fn [_]
|
||||||
|
{:status 200
|
||||||
|
:headers {"Content-Type" "image/png"}
|
||||||
|
:body (io/input-stream (io/resource "reitit.png"))})}}]]
|
||||||
|
|
||||||
|
["/math"
|
||||||
|
{:swagger {:tags ["math"]}}
|
||||||
|
|
||||||
["/plus"
|
["/plus"
|
||||||
{:get {:summary "plus with spec query parameters"
|
{:get {:summary "plus with spec query parameters"
|
||||||
|
|
@ -35,43 +52,30 @@
|
||||||
:post {:summary "plus with spec body parameters"
|
:post {:summary "plus with spec body parameters"
|
||||||
:parameters {:body {:x int?, :y int?}}
|
:parameters {:body {:x int?, :y int?}}
|
||||||
:responses {200 {:body {:total int?}}}
|
:responses {200 {:body {:total int?}}}
|
||||||
:handler (fn [{{{:keys [x y]} :body} :parameters}]
|
|
||||||
{:status 200
|
|
||||||
:body {:total (+ x y)}})}}]]
|
|
||||||
|
|
||||||
["/schema"
|
|
||||||
{:coercion schema/coercion
|
|
||||||
:swagger {:tags ["schema"]}}
|
|
||||||
|
|
||||||
["/plus"
|
|
||||||
{:get {:summary "plus with schema query parameters"
|
|
||||||
:parameters {:query {:x Int, :y Int}}
|
|
||||||
:responses {200 {:body {:total Int}}}
|
|
||||||
:handler (fn [{{{:keys [x y]} :query} :parameters}]
|
|
||||||
{:status 200
|
|
||||||
:body {:total (+ x y)}})}
|
|
||||||
:post {:summary "plus with schema body parameters"
|
|
||||||
:parameters {:body {:x Int, :y Int}}
|
|
||||||
:responses {200 {:body {:total Int}}}
|
|
||||||
:handler (fn [{{{:keys [x y]} :body} :parameters}]
|
:handler (fn [{{{:keys [x y]} :body} :parameters}]
|
||||||
{:status 200
|
{:status 200
|
||||||
:body {:total (+ x y)}})}}]]]
|
:body {:total (+ x y)}})}}]]]
|
||||||
|
|
||||||
{:data {:middleware [ring.middleware.params/wrap-params
|
{:data {:coercion reitit.coercion.spec/coercion
|
||||||
muuntaja.middleware/wrap-format
|
:muuntaja m/instance
|
||||||
swagger/swagger-feature
|
:middleware [;; query-params & form-params
|
||||||
rrc/coerce-exceptions-middleware
|
params/wrap-params
|
||||||
rrc/coerce-request-middleware
|
;; content-negotiation
|
||||||
rrc/coerce-response-middleware]
|
muuntaja/format-negotiate-middleware
|
||||||
:swagger {:produces #{"application/json"
|
;; encoding response body
|
||||||
"application/edn"
|
muuntaja/format-response-middleware
|
||||||
"application/transit+json"}
|
;; exception handling
|
||||||
:consumes #{"application/json"
|
exception/exception-middleware
|
||||||
"application/edn"
|
;; decoding request body
|
||||||
"application/transit+json"}}}})
|
muuntaja/format-request-middleware
|
||||||
|
;; coercing response bodys
|
||||||
|
coercion/coerce-response-middleware
|
||||||
|
;; coercing request parameters
|
||||||
|
coercion/coerce-request-middleware
|
||||||
|
;; multipart
|
||||||
|
multipart/multipart-middleware]}})
|
||||||
(ring/routes
|
(ring/routes
|
||||||
(swagger-ui/create-swagger-ui-handler
|
(swagger-ui/create-swagger-ui-handler {:path "/"})
|
||||||
{:path "/", :url "/api/swagger.json"})
|
|
||||||
(ring/create-default-handler))))
|
(ring/create-default-handler))))
|
||||||
|
|
||||||
(defn start []
|
(defn start []
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
(defproject metosin/reitit-core "0.1.4-SNAPSHOT"
|
(defproject metosin/reitit-core "0.2.0-SNAPSHOT"
|
||||||
:description "Snappy data-driven router for Clojure(Script)"
|
:description "Snappy data-driven router for Clojure(Script)"
|
||||||
:url "https://github.com/metosin/reitit"
|
:url "https://github.com/metosin/reitit"
|
||||||
:license {:name "Eclipse Public License"
|
:license {:name "Eclipse Public License"
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@
|
||||||
{:query (->ParameterCoercion :query-params :string true true)
|
{:query (->ParameterCoercion :query-params :string true true)
|
||||||
:body (->ParameterCoercion :body-params :body false false)
|
:body (->ParameterCoercion :body-params :body false false)
|
||||||
:form (->ParameterCoercion :form-params :string true true)
|
:form (->ParameterCoercion :form-params :string true true)
|
||||||
:header (->ParameterCoercion :header-params :string true true)
|
:header (->ParameterCoercion :headers :string true true)
|
||||||
:path (->ParameterCoercion :path-params :string true true)})
|
:path (->ParameterCoercion :path-params :string true true)})
|
||||||
|
|
||||||
(defn ^:no-doc request-coercion-failed! [result coercion value in request]
|
(defn ^:no-doc request-coercion-failed! [result coercion value in request]
|
||||||
|
|
@ -74,19 +74,19 @@
|
||||||
:or {extract-request-format extract-request-format-default
|
:or {extract-request-format extract-request-format-default
|
||||||
parameter-coercion default-parameter-coercion}}]
|
parameter-coercion default-parameter-coercion}}]
|
||||||
(if coercion
|
(if coercion
|
||||||
(let [{:keys [keywordize? open? in style]} (parameter-coercion type)
|
(if-let [{:keys [keywordize? open? in style]} (parameter-coercion type)]
|
||||||
transform (comp (if keywordize? walk/keywordize-keys identity) in)
|
(let [transform (comp (if keywordize? walk/keywordize-keys identity) in)
|
||||||
model (if open? (-open-model coercion model) model)
|
model (if open? (-open-model coercion model) model)
|
||||||
coercer (-request-coercer coercion style model)]
|
coercer (-request-coercer coercion style model)]
|
||||||
(fn [request]
|
(fn [request]
|
||||||
(let [value (transform request)
|
(let [value (transform request)
|
||||||
format (extract-request-format request)
|
format (extract-request-format request)
|
||||||
result (coercer value format)]
|
result (coercer value format)]
|
||||||
(if (error? result)
|
(if (error? result)
|
||||||
(request-coercion-failed! result coercion value in request)
|
(request-coercion-failed! result coercion value in request)
|
||||||
result))))))
|
result)))))))
|
||||||
|
|
||||||
(defn extract-response-format-default [request response]
|
(defn extract-response-format-default [request _]
|
||||||
(-> request :muuntaja/response :format))
|
(-> request :muuntaja/response :format))
|
||||||
|
|
||||||
(defn response-coercer [coercion body {:keys [extract-response-format]
|
(defn response-coercer [coercion body {:keys [extract-response-format]
|
||||||
|
|
@ -124,6 +124,7 @@
|
||||||
(->> (for [[k v] parameters
|
(->> (for [[k v] parameters
|
||||||
:when v]
|
:when v]
|
||||||
[k (request-coercer coercion k v opts)])
|
[k (request-coercer coercion k v opts)])
|
||||||
|
(filter second)
|
||||||
(into {})))
|
(into {})))
|
||||||
|
|
||||||
(defn response-coercers [coercion responses opts]
|
(defn response-coercers [coercion responses opts]
|
||||||
|
|
@ -140,6 +141,28 @@
|
||||||
"{:compile reitit.coercion/compile-request-coercers}\n")
|
"{:compile reitit.coercion/compile-request-coercers}\n")
|
||||||
{:match match})))
|
{:match match})))
|
||||||
|
|
||||||
|
;;
|
||||||
|
;; api-docs
|
||||||
|
;;
|
||||||
|
|
||||||
|
(defn get-apidocs [this spesification data]
|
||||||
|
(let [swagger-parameter {:query :query
|
||||||
|
:body :body
|
||||||
|
:form :formData
|
||||||
|
:header :header
|
||||||
|
:path :path
|
||||||
|
:multipart :formData}]
|
||||||
|
(case spesification
|
||||||
|
:swagger (->> (update
|
||||||
|
data
|
||||||
|
:parameters
|
||||||
|
(fn [parameters]
|
||||||
|
(->> parameters
|
||||||
|
(map (fn [[k v]] [(swagger-parameter k) v]))
|
||||||
|
(filter first)
|
||||||
|
(into {}))))
|
||||||
|
(-get-apidocs this spesification)))))
|
||||||
|
|
||||||
;;
|
;;
|
||||||
;; integration
|
;; integration
|
||||||
;;
|
;;
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,10 @@
|
||||||
(walk-many pacc macc routes)
|
(walk-many pacc macc routes)
|
||||||
(when (string? (first routes))
|
(when (string? (first routes))
|
||||||
(let [[path & [maybe-arg :as args]] routes
|
(let [[path & [maybe-arg :as args]] routes
|
||||||
[data childs] (if (or (vector? maybe-arg) (nil? maybe-arg))
|
[data childs] (if (or (vector? maybe-arg)
|
||||||
|
(and (sequential? maybe-arg)
|
||||||
|
(sequential? (first maybe-arg)))
|
||||||
|
(nil? maybe-arg))
|
||||||
[{} args]
|
[{} args]
|
||||||
[maybe-arg (rest args)])
|
[maybe-arg (rest args)])
|
||||||
macc (into macc (expand data opts))
|
macc (into macc (expand data opts))
|
||||||
|
|
|
||||||
|
|
@ -45,17 +45,6 @@
|
||||||
(defn contains-wilds? [path]
|
(defn contains-wilds? [path]
|
||||||
(boolean (some wild-or-catch-all-param? (segments path))))
|
(boolean (some wild-or-catch-all-param? (segments path))))
|
||||||
|
|
||||||
(defn url-encode [s]
|
|
||||||
(some-> s
|
|
||||||
#?(:clj (URLEncoder/encode "UTF-8")
|
|
||||||
:cljs (js/encodeURIComponent))
|
|
||||||
#?(:clj (.replace "+" "%20"))))
|
|
||||||
|
|
||||||
(defn url-decode [s]
|
|
||||||
(some-> s #?(:clj (URLDecoder/decode "UTF-8")
|
|
||||||
:cljs (js/decodeURIComponent))))
|
|
||||||
|
|
||||||
|
|
||||||
;;
|
;;
|
||||||
;; https://github.com/pedestal/pedestal/blob/master/route/src/io/pedestal/http/route/path.clj
|
;; https://github.com/pedestal/pedestal/blob/master/route/src/io/pedestal/http/route/path.clj
|
||||||
;;
|
;;
|
||||||
|
|
@ -173,10 +162,47 @@
|
||||||
(defn strip-nils [m]
|
(defn strip-nils [m]
|
||||||
(->> m (remove (comp nil? second)) (into {})))
|
(->> m (remove (comp nil? second)) (into {})))
|
||||||
|
|
||||||
|
#?(:clj (def +percents+ (into [] (map #(format "%%%02X" %) (range 0 256)))))
|
||||||
|
|
||||||
|
#?(:clj (defn byte->percent [byte]
|
||||||
|
(nth +percents+ (if (< byte 0) (+ 256 byte) byte))))
|
||||||
|
|
||||||
|
#?(:clj (defn percent-encode [^String s]
|
||||||
|
(->> (.getBytes s "UTF-8") (map byte->percent) (str/join))))
|
||||||
|
|
||||||
;;
|
;;
|
||||||
;; Path-parameters, see https://github.com/metosin/reitit/issues/75
|
;; encoding & decoding
|
||||||
;;
|
;;
|
||||||
|
|
||||||
|
;; + is safe, but removed so it would work the same as with js
|
||||||
|
(defn url-encode [s]
|
||||||
|
(if s
|
||||||
|
#?(:clj (str/replace s #"[^A-Za-z0-9\!'\(\)\*_~.-]+" percent-encode)
|
||||||
|
:cljs (js/encodeURIComponent s))))
|
||||||
|
|
||||||
|
(defn url-decode [s]
|
||||||
|
(if s
|
||||||
|
#?(:clj (if (.contains ^String s "%")
|
||||||
|
(URLDecoder/decode
|
||||||
|
(if (.contains ^String s "+")
|
||||||
|
(.replace ^String s "+" "%2B")
|
||||||
|
s)
|
||||||
|
"UTF-8")
|
||||||
|
s)
|
||||||
|
:cljs (js/decodeURIComponent s))))
|
||||||
|
|
||||||
|
(defn form-encode [s]
|
||||||
|
(if s
|
||||||
|
#?(:clj (URLEncoder/encode ^String s "UTF-8")
|
||||||
|
:cljs (str/replace (js/encodeURIComponent s) "%20" "+"))))
|
||||||
|
|
||||||
|
(defn form-decode [s]
|
||||||
|
(if s
|
||||||
|
#?(:clj (if (or (.contains ^String s "%") (.contains ^String s "+"))
|
||||||
|
(URLDecoder/decode ^String s "UTF-8")
|
||||||
|
s)
|
||||||
|
:cljs (js/decodeURIComponent (str/replace s "+" " ")))))
|
||||||
|
|
||||||
(defprotocol IntoString
|
(defprotocol IntoString
|
||||||
(into-string [_]))
|
(into-string [_]))
|
||||||
|
|
||||||
|
|
@ -204,7 +230,7 @@
|
||||||
(into-string [this] (str this))
|
(into-string [this] (str this))
|
||||||
|
|
||||||
nil
|
nil
|
||||||
(into-string [this]))
|
(into-string [_]))
|
||||||
|
|
||||||
(defn path-params
|
(defn path-params
|
||||||
"shallow transform of the path parameters values into strings"
|
"shallow transform of the path parameters values into strings"
|
||||||
|
|
@ -220,9 +246,9 @@
|
||||||
[params]
|
[params]
|
||||||
(->> params
|
(->> params
|
||||||
(map (fn [[k v]]
|
(map (fn [[k v]]
|
||||||
(str (url-encode (into-string k))
|
(str (form-encode (into-string k))
|
||||||
"="
|
"="
|
||||||
(url-encode (into-string v)))))
|
(form-encode (into-string v)))))
|
||||||
(str/join "&")))
|
(str/join "&")))
|
||||||
|
|
||||||
(defmacro goog-extend [type base-type ctor & methods]
|
(defmacro goog-extend [type base-type ctor & methods]
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
(ns reitit.interceptor
|
(ns reitit.interceptor
|
||||||
(:require [meta-merge.core :refer [meta-merge]]
|
(:require [meta-merge.core :refer [meta-merge]]
|
||||||
|
[clojure.pprint :as pprint]
|
||||||
[reitit.core :as r]
|
[reitit.core :as r]
|
||||||
[reitit.impl :as impl]))
|
[reitit.impl :as impl]))
|
||||||
|
|
||||||
|
|
@ -13,6 +14,24 @@
|
||||||
|
|
||||||
(extend-protocol IntoInterceptor
|
(extend-protocol IntoInterceptor
|
||||||
|
|
||||||
|
#?(:clj clojure.lang.Keyword
|
||||||
|
:cljs cljs.core.Keyword)
|
||||||
|
(into-interceptor [this data {:keys [::registry] :as opts}]
|
||||||
|
(or (if-let [interceptor (if registry (registry this))]
|
||||||
|
(into-interceptor interceptor data opts))
|
||||||
|
(throw
|
||||||
|
(ex-info
|
||||||
|
(str
|
||||||
|
"Interceptor " this " not found in registry.\n\n"
|
||||||
|
(if (seq registry)
|
||||||
|
(str
|
||||||
|
"Available interceptors in registry:\n"
|
||||||
|
(with-out-str
|
||||||
|
(pprint/print-table [:id :description] (for [[k v] registry] {:id k :description v}))))
|
||||||
|
"see [reitit.interceptor/router] on how to add interceptor to the registry.\n") "\n")
|
||||||
|
{:id this
|
||||||
|
:registry registry}))))
|
||||||
|
|
||||||
#?(:clj clojure.lang.APersistentVector
|
#?(:clj clojure.lang.APersistentVector
|
||||||
:cljs cljs.core.PersistentVector)
|
:cljs cljs.core.PersistentVector)
|
||||||
(into-interceptor [[f & args :as form] data opts]
|
(into-interceptor [[f & args :as form] data opts]
|
||||||
|
|
@ -115,6 +134,7 @@
|
||||||
| key | description |
|
| key | description |
|
||||||
| --------------------------------|-------------|
|
| --------------------------------|-------------|
|
||||||
| `:reitit.interceptor/transform` | Function of [Interceptor] => [Interceptor] to transform the expanded Interceptors (default: identity).
|
| `:reitit.interceptor/transform` | Function of [Interceptor] => [Interceptor] to transform the expanded Interceptors (default: identity).
|
||||||
|
| `:reitit.interceptor/registry` | Map of `keyword => IntoInterceptor` to replace keyword references into Interceptor
|
||||||
|
|
||||||
See router options from [[reitit.core/router]]."
|
See router options from [[reitit.core/router]]."
|
||||||
([data]
|
([data]
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
(ns reitit.middleware
|
(ns reitit.middleware
|
||||||
(:require [meta-merge.core :refer [meta-merge]]
|
(:require [meta-merge.core :refer [meta-merge]]
|
||||||
|
[clojure.pprint :as pprint]
|
||||||
[reitit.core :as r]
|
[reitit.core :as r]
|
||||||
[reitit.impl :as impl]))
|
[reitit.impl :as impl]))
|
||||||
|
|
||||||
|
|
@ -13,6 +14,24 @@
|
||||||
|
|
||||||
(extend-protocol IntoMiddleware
|
(extend-protocol IntoMiddleware
|
||||||
|
|
||||||
|
#?(:clj clojure.lang.Keyword
|
||||||
|
:cljs cljs.core.Keyword)
|
||||||
|
(into-middleware [this data {:keys [::registry] :as opts}]
|
||||||
|
(or (if-let [middleware (if registry (registry this))]
|
||||||
|
(into-middleware middleware data opts))
|
||||||
|
(throw
|
||||||
|
(ex-info
|
||||||
|
(str
|
||||||
|
"Middleware " this " not found in registry.\n\n"
|
||||||
|
(if (seq registry)
|
||||||
|
(str
|
||||||
|
"Available middleware in registry:\n"
|
||||||
|
(with-out-str
|
||||||
|
(pprint/print-table [:id :description] (for [[k v] registry] {:id k :description v}))))
|
||||||
|
"see [reitit.middleware/router] on how to add middleware to the registry.\n") "\n")
|
||||||
|
{:id this
|
||||||
|
:registry registry}))))
|
||||||
|
|
||||||
#?(:clj clojure.lang.APersistentVector
|
#?(:clj clojure.lang.APersistentVector
|
||||||
:cljs cljs.core.PersistentVector)
|
:cljs cljs.core.PersistentVector)
|
||||||
(into-middleware [[f & args] data opts]
|
(into-middleware [[f & args] data opts]
|
||||||
|
|
@ -116,6 +135,7 @@
|
||||||
| key | description |
|
| key | description |
|
||||||
| -------------------------------|-------------|
|
| -------------------------------|-------------|
|
||||||
| `:reitit.middleware/transform` | Function of `[Middleware] => [Middleware]` to transform the expanded Middleware (default: identity).
|
| `:reitit.middleware/transform` | Function of `[Middleware] => [Middleware]` to transform the expanded Middleware (default: identity).
|
||||||
|
| `:reitit.middleware/registry` | Map of `keyword => IntoMiddleware` to replace keyword references into Middleware
|
||||||
|
|
||||||
See other router options from [[reitit.core/router]]."
|
See other router options from [[reitit.core/router]]."
|
||||||
([data]
|
([data]
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@
|
||||||
|
|
||||||
(s/def ::path (s/with-gen string? #(gen/fmap (fn [s] (str "/" s)) (s/gen string?))))
|
(s/def ::path (s/with-gen string? #(gen/fmap (fn [s] (str "/" s)) (s/gen string?))))
|
||||||
|
|
||||||
(s/def ::arg (s/and some? (complement vector?)))
|
(s/def ::arg (s/and some? (complement sequential?)))
|
||||||
(s/def ::data (s/map-of keyword? any?))
|
(s/def ::data (s/map-of keyword? any?))
|
||||||
(s/def ::result any?)
|
(s/def ::result any?)
|
||||||
|
|
||||||
|
|
@ -74,7 +74,7 @@
|
||||||
;; coercion
|
;; coercion
|
||||||
;;
|
;;
|
||||||
|
|
||||||
(s/def :reitit.core.coercion/kw-map (s/map-of keyword? any?))
|
(s/def :reitit.core.coercion/kw-map (s/or :map (s/map-of keyword? any?) :spec s/spec?))
|
||||||
|
|
||||||
(s/def :reitit.core.coercion/query :reitit.core.coercion/kw-map)
|
(s/def :reitit.core.coercion/query :reitit.core.coercion/kw-map)
|
||||||
(s/def :reitit.core.coercion/body :reitit.core.coercion/kw-map)
|
(s/def :reitit.core.coercion/body :reitit.core.coercion/kw-map)
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
(defproject metosin/reitit-frontend "0.1.4-SNAPSHOT"
|
(defproject metosin/reitit-frontend "0.2.0-SNAPSHOT"
|
||||||
:description "Reitit: Clojurescript frontend routing core"
|
:description "Reitit: Clojurescript frontend routing core"
|
||||||
:url "https://github.com/metosin/reitit"
|
:url "https://github.com/metosin/reitit"
|
||||||
:license {:name "Eclipse Public License"
|
:license {:name "Eclipse Public License"
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,20 @@
|
||||||
|
|
||||||
(defonce history (atom nil))
|
(defonce history (atom nil))
|
||||||
|
|
||||||
|
;; Doc-strings from reitit.frontend.history
|
||||||
|
;; remember to update both!
|
||||||
|
|
||||||
(defn start!
|
(defn start!
|
||||||
|
"This registers event listeners on HTML5 history and hashchange events.
|
||||||
|
When using with development workflow like Figwheel, rememeber to
|
||||||
|
remove listeners using stop! call before calling start! again.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
- router The Reitit routing tree.
|
||||||
|
- on-navigate Function to be called when route changes. Takes two parameters, ´token´ and ´history´ object.
|
||||||
|
|
||||||
|
Options:
|
||||||
|
- :use-fragment (default true) If true, onhashchange and location hash are used to store the token."
|
||||||
[routes on-navigate opts]
|
[routes on-navigate opts]
|
||||||
(swap! history (fn [old-history]
|
(swap! history (fn [old-history]
|
||||||
(rfh/stop! old-history)
|
(rfh/stop! old-history)
|
||||||
|
|
@ -14,26 +27,26 @@
|
||||||
|
|
||||||
(defn href
|
(defn href
|
||||||
([k]
|
([k]
|
||||||
(rfh/href @history k))
|
(rfh/href @history k nil nil))
|
||||||
([k params]
|
([k params]
|
||||||
(rfh/href @history k params))
|
(rfh/href @history k params nil))
|
||||||
([k params query]
|
([k params query]
|
||||||
(rfh/href @history k params query)))
|
(rfh/href @history k params query)))
|
||||||
|
|
||||||
(defn set-token
|
(defn push-state
|
||||||
"Sets the new route, leaving previous route in history."
|
"Sets the new route, leaving previous route in history."
|
||||||
([k]
|
([k]
|
||||||
(rfh/set-token @history k))
|
(rfh/push-state @history k nil nil))
|
||||||
([k params]
|
([k params]
|
||||||
(rfh/set-token @history k params))
|
(rfh/push-state @history k params nil))
|
||||||
([k params query]
|
([k params query]
|
||||||
(rfh/set-token @history k params query)))
|
(rfh/push-state @history k params query)))
|
||||||
|
|
||||||
(defn replace-token
|
(defn replace-state
|
||||||
"Replaces current route. I.e. current route is not left on history."
|
"Replaces current route. I.e. current route is not left on history."
|
||||||
([k]
|
([k]
|
||||||
(rfh/replace-token @history k))
|
(rfh/replace-state @history k nil nil))
|
||||||
([k params]
|
([k params]
|
||||||
(rfh/replace-token @history k params))
|
(rfh/replace-state @history k params nil))
|
||||||
([k params query]
|
([k params query]
|
||||||
(rfh/replace-token @history k params query)))
|
(rfh/replace-state @history k params query)))
|
||||||
|
|
|
||||||
|
|
@ -4,154 +4,159 @@
|
||||||
[goog.events :as e]
|
[goog.events :as e]
|
||||||
[reitit.core :as r]
|
[reitit.core :as r]
|
||||||
[reitit.frontend :as rf]
|
[reitit.frontend :as rf]
|
||||||
[reitit.impl :as impl])
|
[reitit.impl :as impl]
|
||||||
(:import goog.history.Html5History
|
[goog.events :as gevents])
|
||||||
goog.Uri))
|
(:import goog.Uri))
|
||||||
|
|
||||||
;; Token is for Closure HtmlHistory
|
(defprotocol History
|
||||||
;; Path is for reitit
|
(-init [this] "Create event listeners")
|
||||||
|
(-stop [this] "Remove event listeners")
|
||||||
|
(-on-navigate [this path])
|
||||||
|
(-get-path [this])
|
||||||
|
(-href [this path]))
|
||||||
|
|
||||||
(defn- token->path [history token]
|
;; This version listens for both pop-state and hash-change for
|
||||||
(if (.-useFragment_ history)
|
;; compatibility for old browsers not supporting History API.
|
||||||
;; If no fragment at all, default to "/"
|
(defrecord FragmentHistory [on-navigate router listen-key last-fragment]
|
||||||
;; If fragment is present, the token already is prefixed with "/"
|
History
|
||||||
(if (= "" token)
|
(-init [this]
|
||||||
(.getPathPrefix history)
|
;; Link clicks and e.g. back button trigger both events, if fragment is same as previous ignore second event.
|
||||||
token)
|
;; For old browsers only the hash-change event is triggered.
|
||||||
(str (.getPathPrefix history) token)))
|
(let [last-fragment (atom nil)
|
||||||
|
this (assoc this :last-fragment last-fragment)
|
||||||
|
handler (fn [e]
|
||||||
|
(let [path (-get-path this)]
|
||||||
|
(when (or (= goog.events.EventType.POPSTATE (.-type e))
|
||||||
|
(not= @last-fragment path))
|
||||||
|
(-on-navigate this path))))]
|
||||||
|
(-on-navigate this (-get-path this))
|
||||||
|
(assoc this
|
||||||
|
:listen-key (gevents/listen js/window
|
||||||
|
#js [goog.events.EventType.POPSTATE goog.events.EventType.HASHCHANGE]
|
||||||
|
handler
|
||||||
|
false))))
|
||||||
|
(-stop [this]
|
||||||
|
(gevents/unlistenByKey listen-key))
|
||||||
|
(-on-navigate [this path]
|
||||||
|
(reset! last-fragment path)
|
||||||
|
(on-navigate (rf/match-by-path router path) this))
|
||||||
|
(-get-path [this]
|
||||||
|
;; Remove #
|
||||||
|
;; "" or "#" should be same as "#/"
|
||||||
|
(let [fragment (subs (.. js/window -location -hash) 1)]
|
||||||
|
(if (= "" fragment)
|
||||||
|
"/"
|
||||||
|
fragment)))
|
||||||
|
(-href [this path]
|
||||||
|
(if path
|
||||||
|
(str "#" path))))
|
||||||
|
|
||||||
(defn- path->token [history path]
|
(defrecord Html5History [on-navigate router listen-key click-listen-key]
|
||||||
(subs path (if (.-useFragment_ history)
|
History
|
||||||
1
|
(-init [this]
|
||||||
(count (.getPathPrefix history)))))
|
(let [handler
|
||||||
|
(fn [e]
|
||||||
|
(-on-navigate this (-get-path this)))
|
||||||
|
|
||||||
(defn- token->href [history token]
|
current-domain
|
||||||
(if token
|
(if (exists? js/location)
|
||||||
(str (if (.-useFragment_ history)
|
(.getDomain (.parse Uri js/location)))
|
||||||
(str "#"))
|
|
||||||
(.getPathPrefix history)
|
|
||||||
token)))
|
|
||||||
|
|
||||||
(def ^:private current-domain (if (exists? js/location)
|
;; Prevent document load when clicking a elements, if the href points to URL that is part
|
||||||
(.getDomain (.parse Uri js/location))))
|
;; of the routing tree."
|
||||||
|
ignore-anchor-click
|
||||||
(defn ignore-anchor-click
|
(fn ignore-anchor-click
|
||||||
"Ignore click events from a elements, if the href points to URL that is part
|
[e]
|
||||||
of the routing tree."
|
;; Returns the next matching anchestor of event target
|
||||||
[router history e]
|
(when-let [el (.closest (.-target e) "a")]
|
||||||
;; Returns the next matching anchestor of event target
|
(let [uri (.parse Uri (.-href el))]
|
||||||
(when-let [el (.closest (.-target e) "a")]
|
(when (and (or (and (not (.hasScheme uri)) (not (.hasDomain uri)))
|
||||||
(let [uri (.parse Uri (.-href el))]
|
(= current-domain (.getDomain uri)))
|
||||||
(when (and (or (and (not (.hasScheme uri)) (not (.hasDomain uri)))
|
(not (.-altKey e))
|
||||||
(= current-domain (.getDomain uri)))
|
(not (.-ctrlKey e))
|
||||||
(not (.-altKey e))
|
(not (.-metaKey e))
|
||||||
(not (.-ctrlKey e))
|
(not (.-shiftKey e))
|
||||||
(not (.-metaKey e))
|
(not (contains? #{"_blank" "self"} (.getAttribute el "target")))
|
||||||
(not (.-shiftKey e))
|
;; Left button
|
||||||
(not (contains? #{"_blank" "self"} (.getAttribute el "target")))
|
(= 0 (.-button e))
|
||||||
;; Left button
|
(reitit/match-by-path router (.getPath uri)))
|
||||||
(= 0 (.-button e))
|
(.preventDefault e)
|
||||||
(reitit/match-by-path router (.getPath uri)))
|
(let [path (str (.getPath uri)
|
||||||
(.preventDefault e)
|
(if (seq (.getQuery uri))
|
||||||
(.setToken history (path->token history (str (.getPath uri)
|
(str "?" (.getQuery uri))))]
|
||||||
(if (seq (.getQuery uri))
|
(.pushState js/window.history nil "" path)
|
||||||
(str "?" (.getQuery uri))))))))))
|
(-on-navigate this path))))))]
|
||||||
|
(-on-navigate this (-get-path this))
|
||||||
(impl/goog-extend
|
(assoc this
|
||||||
^{:jsdoc ["@constructor"
|
:listen-key (gevents/listen js/window goog.events.EventType.POPSTATE handler false)
|
||||||
"@extends {Html5History.TokenTransformer}"]}
|
:click-listen-key (e/listen js/document e/EventType.CLICK ignore-anchor-click))))
|
||||||
TokenTransformer
|
(-on-navigate [this path]
|
||||||
Html5History.TokenTransformer
|
(on-navigate (rf/match-by-path router path) this))
|
||||||
([]
|
(-stop [this]
|
||||||
(this-as this
|
(gevents/unlistenByKey listen-key)
|
||||||
(.call Html5History.TokenTransformer this)))
|
(gevents/unlistenByKey click-listen-key))
|
||||||
(retrieveToken [path-prefix location]
|
(-get-path [this]
|
||||||
(subs (.-pathname location) (count path-prefix)))
|
(.. js/window -location -pathname))
|
||||||
(createUrl [token path-prefix location]
|
(-href [this path]
|
||||||
;; Code in Closure also adds current query params
|
path))
|
||||||
;; from location.
|
|
||||||
(str path-prefix token)))
|
|
||||||
|
|
||||||
(defn start!
|
(defn start!
|
||||||
"This registers event listeners on either haschange or HTML5 history.
|
"This registers event listeners on HTML5 history and hashchange events.
|
||||||
When using with development workflow like Figwheel, rememeber to
|
When using with development workflow like Figwheel, rememeber to
|
||||||
remove listeners using stop! call before calling start! again.
|
remove listeners using stop! call before calling start! again.
|
||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
- router The reitit routing tree.
|
- router The Reitit routing tree.
|
||||||
- on-navigate Function to be called when route changes.
|
- on-navigate Function to be called when route changes. Takes two parameters, ´match´ and ´history´ object.
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
- :use-fragment (default true) If true, onhashchange and location hash are used to store the token.
|
- :use-fragment (default true) If true, onhashchange and location hash are used to store current route."
|
||||||
- :path-prefix (default \"/\") If :use-fragment is false, this is prepended to all tokens, and is
|
([router on-navigate]
|
||||||
removed from start of the token before matching the route."
|
(start! router on-navigate nil))
|
||||||
[router
|
([router
|
||||||
on-navigate
|
on-navigate
|
||||||
{:keys [path-prefix use-fragment]
|
{:keys [use-fragment]
|
||||||
:or {path-prefix "/"
|
:or {use-fragment true}}]
|
||||||
use-fragment true}}]
|
(let [opts {:router router
|
||||||
(let [history
|
:on-navigate on-navigate}]
|
||||||
(doto (Html5History. nil (TokenTransformer.))
|
(-init (if use-fragment
|
||||||
(.setEnabled true)
|
(map->FragmentHistory opts)
|
||||||
(.setPathPrefix path-prefix)
|
(map->Html5History opts))))))
|
||||||
(.setUseFragment use-fragment))
|
|
||||||
|
|
||||||
event-key
|
(defn stop! [history]
|
||||||
(e/listen history goog.history.EventType.NAVIGATE
|
(if history
|
||||||
(fn [e]
|
(-stop history)))
|
||||||
(on-navigate (rf/match-by-path router (token->path history (.getToken history))))))
|
|
||||||
|
|
||||||
click-listen-key
|
|
||||||
(if-not use-fragment
|
|
||||||
(e/listen js/document e/EventType.CLICK
|
|
||||||
(partial ignore-anchor-click router history)))]
|
|
||||||
|
|
||||||
;; Trigger navigate event for current route
|
|
||||||
(on-navigate (rf/match-by-path router (token->path history (.getToken history))))
|
|
||||||
|
|
||||||
{:router router
|
|
||||||
:history history
|
|
||||||
:close-fn (fn []
|
|
||||||
(e/unlistenByKey event-key)
|
|
||||||
(e/unlistenByKey click-listen-key)
|
|
||||||
(.dispose history))}))
|
|
||||||
|
|
||||||
(defn stop! [{:keys [close-fn]}]
|
|
||||||
(if close-fn
|
|
||||||
(close-fn)))
|
|
||||||
|
|
||||||
(defn- match->token [history match k params query]
|
|
||||||
(some->> (r/match->path match query)
|
|
||||||
(path->token history)))
|
|
||||||
|
|
||||||
(defn href
|
(defn href
|
||||||
([state k]
|
([history k]
|
||||||
(href state k nil))
|
(href history k nil))
|
||||||
([state k params]
|
([history k params]
|
||||||
(href state k params nil))
|
(href history k params nil))
|
||||||
([{:keys [router history]} k params query]
|
([history k params query]
|
||||||
(let [match (rf/match-by-name! router k params)
|
(let [match (rf/match-by-name! (:router history) k params)]
|
||||||
token (match->token history match k params query)]
|
(-href history (r/match->path match query)))))
|
||||||
(token->href history token))))
|
|
||||||
|
|
||||||
(defn set-token
|
(defn push-state
|
||||||
"Sets the new route, leaving previous route in history."
|
"Sets the new route, leaving previous route in history."
|
||||||
([state k]
|
([history k]
|
||||||
(set-token state k nil))
|
(push-state history k nil nil))
|
||||||
([state k params]
|
([history k params]
|
||||||
(set-token state k params nil))
|
(push-state history k params nil))
|
||||||
([{:keys [router history]} k params query]
|
([history k params query]
|
||||||
(let [match (rf/match-by-name! router k params)
|
(let [match (rf/match-by-name! (:router history) k params)
|
||||||
token (match->token history match k params query)]
|
path (r/match->path match query)]
|
||||||
(.setToken history token))))
|
;; pushState and replaceState don't trigger popstate event so call on-navigate manually
|
||||||
|
(.pushState js/window.history nil "" (-href history path))
|
||||||
|
(-on-navigate history path))))
|
||||||
|
|
||||||
(defn replace-token
|
(defn replace-state
|
||||||
"Replaces current route. I.e. current route is not left on history."
|
"Replaces current route. I.e. current route is not left on history."
|
||||||
([state k]
|
([history k]
|
||||||
(replace-token state k nil))
|
(replace-state history k nil nil))
|
||||||
([state k params]
|
([history k params]
|
||||||
(replace-token state k params nil))
|
(replace-state history k params nil))
|
||||||
([{:keys [router history]} k params query]
|
([history k params query]
|
||||||
(let [match (rf/match-by-name! router k params)
|
(let [match (rf/match-by-name! (:router history) k params)
|
||||||
token (match->token history match k params query)]
|
path (r/match->path match query)]
|
||||||
(.replaceToken history token))))
|
(.replaceState js/window.history nil "" (-href history path))
|
||||||
|
(-on-navigate history path))))
|
||||||
|
|
|
||||||
10
modules/reitit-middleware/project.clj
Normal file
10
modules/reitit-middleware/project.clj
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
(defproject metosin/reitit-middleware "0.2.0-SNAPSHOT"
|
||||||
|
:description "Reitit, common middleware bundled"
|
||||||
|
: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/muuntaja]])
|
||||||
|
|
@ -0,0 +1,177 @@
|
||||||
|
(ns reitit.ring.middleware.exception
|
||||||
|
(:require [reitit.coercion :as coercion]
|
||||||
|
[reitit.ring :as ring]
|
||||||
|
[clojure.spec.alpha :as s]
|
||||||
|
[clojure.string :as str])
|
||||||
|
(:import (java.time Instant)
|
||||||
|
(java.io PrintWriter)))
|
||||||
|
|
||||||
|
(s/def ::handlers (s/map-of any? fn?))
|
||||||
|
(s/def ::spec (s/keys :opt-un [::handlers]))
|
||||||
|
|
||||||
|
;;
|
||||||
|
;; helpers
|
||||||
|
;;
|
||||||
|
|
||||||
|
(defn- super-classes [^Class k]
|
||||||
|
(loop [sk (.getSuperclass k), ks []]
|
||||||
|
(if-not (= sk Object)
|
||||||
|
(recur (.getSuperclass sk) (conj ks sk))
|
||||||
|
ks)))
|
||||||
|
|
||||||
|
(defn- call-error-handler [handlers error request]
|
||||||
|
(let [type (:type (ex-data error))
|
||||||
|
ex-class (class error)
|
||||||
|
error-handler (or (get handlers type)
|
||||||
|
(get handlers ex-class)
|
||||||
|
(some
|
||||||
|
(partial get handlers)
|
||||||
|
(descendants type))
|
||||||
|
(some
|
||||||
|
(partial get handlers)
|
||||||
|
(super-classes ex-class))
|
||||||
|
(get handlers ::default))]
|
||||||
|
(if-let [wrap (get handlers ::wrap)]
|
||||||
|
(wrap error-handler error request)
|
||||||
|
(error-handler error request))))
|
||||||
|
|
||||||
|
(defn- on-exception [handlers e request respond raise]
|
||||||
|
(try
|
||||||
|
(respond (call-error-handler handlers e request))
|
||||||
|
(catch Exception e
|
||||||
|
(raise e))))
|
||||||
|
|
||||||
|
(defn- wrap [handlers]
|
||||||
|
(fn [handler]
|
||||||
|
(fn
|
||||||
|
([request]
|
||||||
|
(try
|
||||||
|
(handler request)
|
||||||
|
(catch Throwable e
|
||||||
|
(on-exception handlers e request identity #(throw %)))))
|
||||||
|
([request respond raise]
|
||||||
|
(try
|
||||||
|
(handler request respond (fn [e] (on-exception handlers e request respond raise)))
|
||||||
|
(catch Throwable e
|
||||||
|
(on-exception handlers e request respond raise)))))))
|
||||||
|
|
||||||
|
(defn print! [^PrintWriter writer & more]
|
||||||
|
(.write writer (str (str/join " " more) "\n")))
|
||||||
|
|
||||||
|
;;
|
||||||
|
;; handlers
|
||||||
|
;;
|
||||||
|
|
||||||
|
(defn default-handler
|
||||||
|
"Default safe handler for any exception."
|
||||||
|
[^Exception e _]
|
||||||
|
{:status 500
|
||||||
|
:body {:type "exception"
|
||||||
|
:class (.getName (.getClass e))}})
|
||||||
|
|
||||||
|
(defn create-coercion-handler
|
||||||
|
"Creates a coercion exception handler."
|
||||||
|
[status]
|
||||||
|
(fn [e _]
|
||||||
|
{:status status
|
||||||
|
:body (coercion/encode-error (ex-data e))}))
|
||||||
|
|
||||||
|
(defn http-response-handler
|
||||||
|
"Reads response from Exception ex-data :response"
|
||||||
|
[e _]
|
||||||
|
(-> e ex-data :response))
|
||||||
|
|
||||||
|
(defn request-parsing-handler [e _]
|
||||||
|
{:status 400
|
||||||
|
:headers {"Content-Type" "text/plain"}
|
||||||
|
:body (str "Malformed " (-> e ex-data :format pr-str) " request.")})
|
||||||
|
|
||||||
|
(defn wrap-log-to-console [handler e {:keys [uri request-method] :as req}]
|
||||||
|
(print! *out* (Instant/now) request-method (pr-str uri) "=>" (.getMessage e))
|
||||||
|
(.printStackTrace e *out*)
|
||||||
|
(handler e req))
|
||||||
|
|
||||||
|
;;
|
||||||
|
;; public api
|
||||||
|
;;
|
||||||
|
|
||||||
|
(def default-handlers
|
||||||
|
{::default default-handler
|
||||||
|
::ring/response http-response-handler
|
||||||
|
:muuntaja/decode request-parsing-handler
|
||||||
|
::coercion/request-coercion (create-coercion-handler 400)
|
||||||
|
::coercion/response-coercion (create-coercion-handler 500)})
|
||||||
|
|
||||||
|
(defn wrap-exception
|
||||||
|
([handler]
|
||||||
|
(handler default-handlers))
|
||||||
|
([handler options]
|
||||||
|
(-> options wrap handler)))
|
||||||
|
|
||||||
|
(def exception-middleware
|
||||||
|
{:name ::exception
|
||||||
|
:spec ::spec
|
||||||
|
:wrap (wrap default-handlers)})
|
||||||
|
|
||||||
|
(defn create-exception-middleware
|
||||||
|
"Creates a reitit middleware that catches all exceptions. Takes a map
|
||||||
|
of `identifier => exception request => response` that is used to select
|
||||||
|
the exception handler for the thown/raised exception identifier. Exception
|
||||||
|
idenfier is either a `Keyword` or a Exception Class.
|
||||||
|
|
||||||
|
The following handlers special handlers are available:
|
||||||
|
|
||||||
|
| key | description
|
||||||
|
|------------------------|-------------
|
||||||
|
| `::exception/default` | a default exception handler if nothing else mathced (default [[default-handler]]).
|
||||||
|
| `::exception/wrap` | a 3-arity handler to wrap the actual handler `handler exception request => response`
|
||||||
|
|
||||||
|
The handler is selected from the options map by exception idenfiter
|
||||||
|
in the following lookup order:
|
||||||
|
|
||||||
|
1) `:type` of exception ex-data
|
||||||
|
2) Class of exception
|
||||||
|
3) `:type` ancestors of exception ex-data
|
||||||
|
4) Super Classes of exception
|
||||||
|
5) The ::default handler
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
(require '[reitit.ring.middleware.exception :as exception])
|
||||||
|
|
||||||
|
;; type hierarchy
|
||||||
|
(derive ::error ::exception)
|
||||||
|
(derive ::failure ::exception)
|
||||||
|
(derive ::horror ::exception)
|
||||||
|
|
||||||
|
(defn handler [message exception request]
|
||||||
|
{:status 500
|
||||||
|
:body {:message message
|
||||||
|
:exception (str exception)
|
||||||
|
:uri (:uri request)}})
|
||||||
|
|
||||||
|
(exception/create-exception-middleware
|
||||||
|
(merge
|
||||||
|
exception/default-handlers
|
||||||
|
{;; ex-data with :type ::error
|
||||||
|
::error (partial handler \"error\")
|
||||||
|
|
||||||
|
;; ex-data with ::exception or ::failure
|
||||||
|
::exception (partial handler \"exception\")
|
||||||
|
|
||||||
|
;; SQLException and all it's child classes
|
||||||
|
java.sql.SQLException (partial handler \"sql-exception\")
|
||||||
|
|
||||||
|
;; override the default handler
|
||||||
|
::exception/default (partial handler \"default\")
|
||||||
|
|
||||||
|
;; print stack-traces for all exceptions
|
||||||
|
::exception/wrap (fn [handler e request]
|
||||||
|
(.printStackTrace e)
|
||||||
|
(handler e request))}))"
|
||||||
|
([]
|
||||||
|
(create-exception-middleware default-handlers))
|
||||||
|
([handlers]
|
||||||
|
{:name ::exception
|
||||||
|
:spec ::spec
|
||||||
|
:wrap (wrap handlers)}))
|
||||||
|
|
@ -0,0 +1,77 @@
|
||||||
|
(ns ^:no-doc reitit.ring.middleware.multipart
|
||||||
|
(:refer-clojure :exclude [compile])
|
||||||
|
(:require [reitit.coercion :as coercion]
|
||||||
|
[ring.middleware.multipart-params :as multipart-params]
|
||||||
|
[clojure.spec.alpha :as s]
|
||||||
|
[spec-tools.core :as st])
|
||||||
|
(:import (java.io File)))
|
||||||
|
|
||||||
|
(s/def ::filename string?)
|
||||||
|
(s/def ::content-type string?)
|
||||||
|
(s/def ::tempfile (partial instance? File))
|
||||||
|
(s/def ::bytes bytes?)
|
||||||
|
(s/def ::size int?)
|
||||||
|
|
||||||
|
(def temp-file-part
|
||||||
|
"Spec for file param created by ring.middleware.multipart-params.temp-file store."
|
||||||
|
(st/spec
|
||||||
|
{:spec (s/keys :req-un [::filename ::content-type ::tempfile ::size])
|
||||||
|
:swagger/type "file"}))
|
||||||
|
|
||||||
|
(def bytes-part
|
||||||
|
"Spec for file param created by ring.middleware.multipart-params.byte-array store."
|
||||||
|
(st/spec
|
||||||
|
{:spec (s/keys :req-un [::filename ::content-type ::bytes])
|
||||||
|
:swagger/type "file"}))
|
||||||
|
|
||||||
|
(defn- coerced-request [request coercers]
|
||||||
|
(if-let [coerced (if coercers (coercion/coerce-request coercers request))]
|
||||||
|
(update request :parameters merge coerced)
|
||||||
|
request))
|
||||||
|
|
||||||
|
(defn- compile [options]
|
||||||
|
(fn [{:keys [parameters coercion]} opts]
|
||||||
|
(if-let [multipart (:multipart parameters)]
|
||||||
|
(let [parameter-coercion {:multipart (coercion/->ParameterCoercion
|
||||||
|
:multipart-params :string true true)}
|
||||||
|
opts (assoc opts ::coercion/parameter-coercion parameter-coercion)
|
||||||
|
coercers (if multipart (coercion/request-coercers coercion parameters opts))]
|
||||||
|
{:data {:swagger {:consumes ^:replace #{"multipart/form-data"}}}
|
||||||
|
:wrap (fn [handler]
|
||||||
|
(fn
|
||||||
|
([request]
|
||||||
|
(try
|
||||||
|
(-> request
|
||||||
|
(multipart-params/multipart-params-request options)
|
||||||
|
(coerced-request coercers)
|
||||||
|
(handler))
|
||||||
|
(catch Exception e
|
||||||
|
(.printStackTrace e)
|
||||||
|
(throw e))))
|
||||||
|
([request respond raise]
|
||||||
|
(-> request
|
||||||
|
(multipart-params/multipart-params-request options)
|
||||||
|
(coerced-request coercers)
|
||||||
|
(handler respond raise)))))}))))
|
||||||
|
|
||||||
|
;;
|
||||||
|
;; public api
|
||||||
|
;;
|
||||||
|
|
||||||
|
(defn create-multipart-middleware
|
||||||
|
"Creates a Middleware to handle the multipart params, based on
|
||||||
|
ring.middleware.multipart-params, taking same options. Mounts only
|
||||||
|
if endpoint has `[:parameters :multipart]` defined. Publishes coerced
|
||||||
|
parameters into `[:parameters :multipart]` under request."
|
||||||
|
([]
|
||||||
|
(create-multipart-middleware nil))
|
||||||
|
([options]
|
||||||
|
{:name ::multipart
|
||||||
|
:compile (compile options)}))
|
||||||
|
|
||||||
|
(def multipart-middleware
|
||||||
|
"Middleware to handle the multipart params, based on
|
||||||
|
ring.middleware.multipart-params, taking same options. Mounts only
|
||||||
|
if endpoint has `[:parameters :multipart]` defined. Publishes coerced
|
||||||
|
parameters into `[:parameters :multipart]` under request."
|
||||||
|
(create-multipart-middleware))
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
(ns reitit.ring.middleware.muuntaja
|
||||||
|
(:require [muuntaja.core :as m]
|
||||||
|
[muuntaja.middleware]
|
||||||
|
[clojure.spec.alpha :as s]))
|
||||||
|
|
||||||
|
(s/def ::muuntaja (partial instance? m/Muuntaja))
|
||||||
|
(s/def ::spec (s/keys :opt-un [::muuntaja]))
|
||||||
|
|
||||||
|
(defn- displace [x] (with-meta x {:displace true}))
|
||||||
|
|
||||||
|
(def format-middleware
|
||||||
|
{:name ::format
|
||||||
|
:spec ::spec
|
||||||
|
:compile (fn [{:keys [muuntaja]} _]
|
||||||
|
(if muuntaja
|
||||||
|
{:data {:swagger {:produces (displace (m/encodes muuntaja))
|
||||||
|
:consumes (displace (m/decodes muuntaja))}}
|
||||||
|
:wrap #(muuntaja.middleware/wrap-format % muuntaja)}))})
|
||||||
|
|
||||||
|
(def format-negotiate-middleware
|
||||||
|
{:name ::format-negotiate
|
||||||
|
:spec ::spec
|
||||||
|
:compile (fn [{:keys [muuntaja]} _]
|
||||||
|
(if muuntaja
|
||||||
|
{:wrap #(muuntaja.middleware/wrap-format-negotiate % muuntaja)}))})
|
||||||
|
|
||||||
|
(def format-request-middleware
|
||||||
|
{:name ::format-request
|
||||||
|
:spec ::spec
|
||||||
|
:compile (fn [{:keys [muuntaja]} _]
|
||||||
|
(if muuntaja
|
||||||
|
{:data {:swagger {:consumes (displace (m/decodes muuntaja))}}
|
||||||
|
:wrap #(muuntaja.middleware/wrap-format-request % muuntaja)}))})
|
||||||
|
|
||||||
|
(def format-response-middleware
|
||||||
|
{:name ::format-response
|
||||||
|
:spec ::spec
|
||||||
|
:compile (fn [{:keys [muuntaja]} _]
|
||||||
|
(if muuntaja
|
||||||
|
{:data {:swagger {:produces (displace (m/encodes muuntaja))}}
|
||||||
|
:wrap #(muuntaja.middleware/wrap-format-response % muuntaja)}))})
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
(defproject metosin/reitit-ring "0.1.4-SNAPSHOT"
|
(defproject metosin/reitit-ring "0.2.0-SNAPSHOT"
|
||||||
:description "Reitit: Ring routing"
|
:description "Reitit: Ring routing"
|
||||||
:url "https://github.com/metosin/reitit"
|
:url "https://github.com/metosin/reitit"
|
||||||
:license {:name "Eclipse Public License"
|
:license {:name "Eclipse Public License"
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
(defproject metosin/reitit-schema "0.1.4-SNAPSHOT"
|
(defproject metosin/reitit-schema "0.2.0-SNAPSHOT"
|
||||||
:description "Reitit: Plumatic Schema coercion"
|
:description "Reitit: Plumatic Schema coercion"
|
||||||
:url "https://github.com/metosin/reitit"
|
:url "https://github.com/metosin/reitit"
|
||||||
:license {:name "Eclipse Public License"
|
:license {:name "Eclipse Public License"
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,7 @@
|
||||||
(-get-options [_] opts)
|
(-get-options [_] opts)
|
||||||
(-get-apidocs [this spesification {:keys [parameters responses]}]
|
(-get-apidocs [this spesification {:keys [parameters responses]}]
|
||||||
;; TODO: this looks identical to spec, refactor when schema is done.
|
;; TODO: this looks identical to spec, refactor when schema is done.
|
||||||
(condp = spesification
|
(case spesification
|
||||||
:swagger (swagger/swagger-spec
|
:swagger (swagger/swagger-spec
|
||||||
(merge
|
(merge
|
||||||
(if parameters
|
(if parameters
|
||||||
|
|
|
||||||
30
modules/reitit-schema/src/reitit/ring/schema.cljc
Normal file
30
modules/reitit-schema/src/reitit/ring/schema.cljc
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
(ns reitit.ring.schema
|
||||||
|
(:require [schema.core :as s]
|
||||||
|
[schema-tools.swagger.core :as swagger])
|
||||||
|
#?(:clj (:import (java.io File))))
|
||||||
|
|
||||||
|
(defrecord Upload [m]
|
||||||
|
s/Schema
|
||||||
|
(spec [_]
|
||||||
|
(s/spec m))
|
||||||
|
(explain [_]
|
||||||
|
(cons 'file m))
|
||||||
|
|
||||||
|
swagger/SwaggerSchema
|
||||||
|
(-transform [_ _]
|
||||||
|
{:type "file"}))
|
||||||
|
|
||||||
|
#?(:clj
|
||||||
|
(def TempFilePart
|
||||||
|
"Schema for file param created by ring.middleware.multipart-params.temp-file store."
|
||||||
|
(->Upload {:filename s/Str
|
||||||
|
:content-type s/Str
|
||||||
|
:size s/Int
|
||||||
|
:tempfile File})))
|
||||||
|
|
||||||
|
#?(:clj
|
||||||
|
(def BytesPart
|
||||||
|
"Schema for file param created by ring.middleware.multipart-params.byte-array store."
|
||||||
|
(->Upload {:filename s/Str
|
||||||
|
:content-type s/Str
|
||||||
|
:bytes s/Any})))
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
(defproject metosin/reitit-spec "0.1.4-SNAPSHOT"
|
(defproject metosin/reitit-spec "0.2.0-SNAPSHOT"
|
||||||
:description "Reitit: clojure.spec coercion"
|
:description "Reitit: clojure.spec coercion"
|
||||||
:url "https://github.com/metosin/reitit"
|
:url "https://github.com/metosin/reitit"
|
||||||
:license {:name "Eclipse Public License"
|
:license {:name "Eclipse Public License"
|
||||||
|
|
|
||||||
|
|
@ -87,7 +87,7 @@
|
||||||
(-get-name [_] :spec)
|
(-get-name [_] :spec)
|
||||||
(-get-options [_] opts)
|
(-get-options [_] opts)
|
||||||
(-get-apidocs [this spesification {:keys [parameters responses]}]
|
(-get-apidocs [this spesification {:keys [parameters responses]}]
|
||||||
(condp = spesification
|
(case spesification
|
||||||
:swagger (swagger/swagger-spec
|
:swagger (swagger/swagger-spec
|
||||||
(merge
|
(merge
|
||||||
(if parameters
|
(if parameters
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
(defproject metosin/reitit-swagger-ui "0.1.4-SNAPSHOT"
|
(defproject metosin/reitit-swagger-ui "0.2.0-SNAPSHOT"
|
||||||
:description "Reitit: Swagger-ui support"
|
:description "Reitit: Swagger-ui support"
|
||||||
:url "https://github.com/metosin/reitit"
|
:url "https://github.com/metosin/reitit"
|
||||||
:license {:name "Eclipse Public License"
|
:license {:name "Eclipse Public License"
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
(defproject metosin/reitit-swagger "0.1.4-SNAPSHOT"
|
(defproject metosin/reitit-swagger "0.2.0-SNAPSHOT"
|
||||||
:description "Reitit: Swagger-support"
|
:description "Reitit: Swagger-support"
|
||||||
:url "https://github.com/metosin/reitit"
|
:url "https://github.com/metosin/reitit"
|
||||||
:license {:name "Eclipse Public License"
|
:license {:name "Eclipse Public License"
|
||||||
|
|
|
||||||
|
|
@ -77,18 +77,22 @@
|
||||||
(let [{:keys [id] :or {id ::default} :as swagger} (-> match :result request-method :data :swagger)
|
(let [{:keys [id] :or {id ::default} :as swagger} (-> match :result request-method :data :swagger)
|
||||||
->set (fn [x] (if (or (set? x) (sequential? x)) (set x) (conj #{} x)))
|
->set (fn [x] (if (or (set? x) (sequential? x)) (set x) (conj #{} x)))
|
||||||
ids (->set id)
|
ids (->set id)
|
||||||
swagger (->> (dissoc swagger :id)
|
strip-top-level-keys #(dissoc % :id :info :host :basePath :definitions :securityDefinitions)
|
||||||
|
strip-endpoint-keys #(dissoc % :id :parameters :responses :summary :description)
|
||||||
|
swagger (->> (strip-endpoint-keys swagger)
|
||||||
(merge {:swagger "2.0"
|
(merge {:swagger "2.0"
|
||||||
:x-id ids}))
|
:x-id ids}))
|
||||||
accept-route #(-> % second :swagger :id (or ::default) ->set (set/intersection ids) seq)
|
accept-route (fn [route]
|
||||||
transform-endpoint (fn [[method {{:keys [coercion no-doc swagger] :as data} :data}]]
|
(-> route second :swagger :id (or ::default) ->set (set/intersection ids) seq))
|
||||||
|
transform-endpoint (fn [[method {{:keys [coercion no-doc swagger] :as data} :data middleware :middleware}]]
|
||||||
(if (and data (not no-doc))
|
(if (and data (not no-doc))
|
||||||
[method
|
[method
|
||||||
(meta-merge
|
(meta-merge
|
||||||
|
(apply meta-merge (keep (comp :swagger :data) middleware))
|
||||||
(if coercion
|
(if coercion
|
||||||
(coercion/-get-apidocs coercion :swagger data))
|
(coercion/get-apidocs coercion :swagger data))
|
||||||
(select-keys data [:tags :summary :description])
|
(select-keys data [:tags :summary :description])
|
||||||
(dissoc swagger :id))]))
|
(strip-top-level-keys swagger))]))
|
||||||
transform-path (fn [[p _ c]]
|
transform-path (fn [[p _ c]]
|
||||||
(if-let [endpoint (some->> c (keep transform-endpoint) (seq) (into {}))]
|
(if-let [endpoint (some->> c (keep transform-endpoint) (seq) (into {}))]
|
||||||
[(path->template p) endpoint]))]
|
[(path->template p) endpoint]))]
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
(defproject metosin/reitit "0.1.4-SNAPSHOT"
|
(defproject metosin/reitit "0.2.0-SNAPSHOT"
|
||||||
:description "Snappy data-driven router for Clojure(Script)"
|
:description "Snappy data-driven router for Clojure(Script)"
|
||||||
:url "https://github.com/metosin/reitit"
|
:url "https://github.com/metosin/reitit"
|
||||||
:license {:name "Eclipse Public License"
|
:license {:name "Eclipse Public License"
|
||||||
|
|
@ -8,6 +8,7 @@
|
||||||
:inherit [:deploy-repositories :managed-dependencies]}
|
:inherit [:deploy-repositories :managed-dependencies]}
|
||||||
:dependencies [[metosin/reitit-core]
|
:dependencies [[metosin/reitit-core]
|
||||||
[metosin/reitit-ring]
|
[metosin/reitit-ring]
|
||||||
|
[metosin/reitit-middleware]
|
||||||
[metosin/reitit-spec]
|
[metosin/reitit-spec]
|
||||||
[metosin/reitit-schema]
|
[metosin/reitit-schema]
|
||||||
[metosin/reitit-swagger]
|
[metosin/reitit-swagger]
|
||||||
|
|
|
||||||
170
perf-test/clj/reitit/impl_perf_test.clj
Normal file
170
perf-test/clj/reitit/impl_perf_test.clj
Normal file
|
|
@ -0,0 +1,170 @@
|
||||||
|
(ns reitit.impl-perf-test
|
||||||
|
(:require [criterium.core :as cc]
|
||||||
|
[reitit.perf-utils :refer :all]
|
||||||
|
[ring.util.codec]
|
||||||
|
[reitit.impl])
|
||||||
|
(:import (java.net URLDecoder URLEncoder)))
|
||||||
|
|
||||||
|
;;
|
||||||
|
;; start repl with `lein perf repl`
|
||||||
|
;; perf measured with the following setup:
|
||||||
|
;;
|
||||||
|
;; Model Name: MacBook Pro
|
||||||
|
;; Model Identifier: MacBookPro11,3
|
||||||
|
;; Processor Name: Intel Core i7
|
||||||
|
;; Processor Speed: 2,5 GHz
|
||||||
|
;; Number of Processors: 1
|
||||||
|
;; Total Number of Cores: 4
|
||||||
|
;; L2 Cache (per Core): 256 KB
|
||||||
|
;; L3 Cache: 6 MB
|
||||||
|
;; Memory: 16 GB
|
||||||
|
;;
|
||||||
|
|
||||||
|
|
||||||
|
(defn test! [f input]
|
||||||
|
(do
|
||||||
|
(println "\u001B[33m")
|
||||||
|
(println (pr-str input) "=>" (pr-str (f input)))
|
||||||
|
(println "\u001B[0m")
|
||||||
|
(cc/quick-bench (f input))))
|
||||||
|
|
||||||
|
(defn url-decode-naive [s]
|
||||||
|
(URLDecoder/decode
|
||||||
|
(.replace ^String s "+" "%2B")
|
||||||
|
"UTF-8"))
|
||||||
|
|
||||||
|
(defn url-decode! []
|
||||||
|
|
||||||
|
;; ring
|
||||||
|
|
||||||
|
;; 890ns
|
||||||
|
;; 190ns
|
||||||
|
;; 90ns
|
||||||
|
;; 80ns
|
||||||
|
|
||||||
|
;; naive
|
||||||
|
|
||||||
|
;; 750ns
|
||||||
|
;; 340ns
|
||||||
|
;; 420ns
|
||||||
|
;; 200ns
|
||||||
|
|
||||||
|
;; reitit
|
||||||
|
|
||||||
|
;; 630ns (-29%)
|
||||||
|
;; 12ns (-94%)
|
||||||
|
;; 8ns (-91%)
|
||||||
|
;; 8ns (-90%)
|
||||||
|
|
||||||
|
(doseq [fs ['ring.util.codec/url-decode
|
||||||
|
'url-decode-naive
|
||||||
|
'reitit.impl/url-decode]
|
||||||
|
:let [f (deref (resolve fs))]]
|
||||||
|
(suite (str fs))
|
||||||
|
(doseq [s ["aja%20hiljaa+sillalla"
|
||||||
|
"aja_hiljaa_sillalla"
|
||||||
|
"1+1"
|
||||||
|
"1"]]
|
||||||
|
(test! f s))))
|
||||||
|
|
||||||
|
(defn url-encode-naive [^String s]
|
||||||
|
(cond-> (.replace (URLEncoder/encode s "UTF-8") "+" "%20")
|
||||||
|
(.contains s "+") (.replace "%2B" "+")
|
||||||
|
(.contains s "~") (.replace "%7E" "~")
|
||||||
|
(.contains s "=") (.replace "%3D" "=")
|
||||||
|
(.contains s "!") (.replace "%21" "!")
|
||||||
|
(.contains s "'") (.replace "%27" "'")
|
||||||
|
(.contains s "(") (.replace "%28" "(")
|
||||||
|
(.contains s ")") (.replace "%29" ")")))
|
||||||
|
|
||||||
|
(defn url-encode! []
|
||||||
|
|
||||||
|
;; ring
|
||||||
|
|
||||||
|
;; 2500ns
|
||||||
|
;; 610ns
|
||||||
|
;; 160ns
|
||||||
|
;; 120ns
|
||||||
|
|
||||||
|
;; naive
|
||||||
|
|
||||||
|
;; 1000ns
|
||||||
|
;; 440ns
|
||||||
|
;; 570ns
|
||||||
|
;; 200ns
|
||||||
|
|
||||||
|
;; reitit
|
||||||
|
|
||||||
|
;; 1400ns
|
||||||
|
;; 740ns
|
||||||
|
;; 180ns
|
||||||
|
;; 130ns
|
||||||
|
|
||||||
|
(doseq [fs ['ring.util.codec/url-encode
|
||||||
|
'url-encode-naive
|
||||||
|
'reitit.impl/url-encode]
|
||||||
|
:let [f (deref (resolve fs))]]
|
||||||
|
(suite (str fs))
|
||||||
|
(doseq [s ["aja hiljaa+sillalla"
|
||||||
|
"aja_hiljaa_sillalla"
|
||||||
|
"1+1"
|
||||||
|
"1"]]
|
||||||
|
(test! f s))))
|
||||||
|
|
||||||
|
(defn form-decode! []
|
||||||
|
|
||||||
|
;; ring
|
||||||
|
|
||||||
|
;; 280ns
|
||||||
|
;; 130ns
|
||||||
|
;; 43ns
|
||||||
|
;; 25ns
|
||||||
|
|
||||||
|
;; reitit
|
||||||
|
|
||||||
|
;; 270ns (-4%)
|
||||||
|
;; 20ns (-84%)
|
||||||
|
;; 47ns (+8%)
|
||||||
|
;; 12ns (-52%)
|
||||||
|
|
||||||
|
(doseq [fs ['ring.util.codec/form-decode-str
|
||||||
|
'reitit.impl/form-decode]
|
||||||
|
:let [f (deref (resolve fs))]]
|
||||||
|
(suite (str fs))
|
||||||
|
(doseq [s ["%2Baja%20hiljaa+sillalla"
|
||||||
|
"aja_hiljaa_sillalla"
|
||||||
|
"1+1"
|
||||||
|
"1"]]
|
||||||
|
(test! f s))))
|
||||||
|
|
||||||
|
(defn form-encode! []
|
||||||
|
|
||||||
|
;; ring
|
||||||
|
|
||||||
|
;; 240ns
|
||||||
|
;; 120ns
|
||||||
|
;; 130ns
|
||||||
|
;; 31ns
|
||||||
|
|
||||||
|
;; reitit
|
||||||
|
|
||||||
|
;; 210ns
|
||||||
|
;; 120ns
|
||||||
|
;; 130ns
|
||||||
|
;; 30ns
|
||||||
|
|
||||||
|
(doseq [fs ['ring.util.codec/form-encode
|
||||||
|
'reitit.impl/form-encode]
|
||||||
|
:let [f (deref (resolve fs))]]
|
||||||
|
(suite (str fs))
|
||||||
|
(doseq [s ["aja hiljaa+sillalla"
|
||||||
|
"aja_hiljaa_sillalla"
|
||||||
|
"1+1"
|
||||||
|
"1"]]
|
||||||
|
(test! f s))))
|
||||||
|
|
||||||
|
(comment
|
||||||
|
(url-decode!)
|
||||||
|
(url-encode!)
|
||||||
|
(form-decode!)
|
||||||
|
(form-encode!))
|
||||||
24
project.clj
24
project.clj
|
|
@ -1,4 +1,4 @@
|
||||||
(defproject metosin/reitit-parent "0.1.4-SNAPSHOT"
|
(defproject metosin/reitit-parent "0.2.0-SNAPSHOT"
|
||||||
:description "Snappy data-driven router for Clojure(Script)"
|
:description "Snappy data-driven router for Clojure(Script)"
|
||||||
:url "https://github.com/metosin/reitit"
|
:url "https://github.com/metosin/reitit"
|
||||||
:license {:name "Eclipse Public License"
|
:license {:name "Eclipse Public License"
|
||||||
|
|
@ -9,20 +9,21 @@
|
||||||
:source-uri "https://github.com/metosin/reitit/{version}/{filepath}#L{line}"
|
:source-uri "https://github.com/metosin/reitit/{version}/{filepath}#L{line}"
|
||||||
:metadata {:doc/format :markdown}}
|
:metadata {:doc/format :markdown}}
|
||||||
|
|
||||||
:managed-dependencies [[metosin/reitit "0.1.4-SNAPSHOT"]
|
:managed-dependencies [[metosin/reitit "0.2.0-SNAPSHOT"]
|
||||||
[metosin/reitit-core "0.1.4-SNAPSHOT"]
|
[metosin/reitit-core "0.2.0-SNAPSHOT"]
|
||||||
[metosin/reitit-ring "0.1.4-SNAPSHOT"]
|
[metosin/reitit-ring "0.2.0-SNAPSHOT"]
|
||||||
[metosin/reitit-spec "0.1.4-SNAPSHOT"]
|
[metosin/reitit-middleware "0.2.0-SNAPSHOT"]
|
||||||
[metosin/reitit-schema "0.1.4-SNAPSHOT"]
|
[metosin/reitit-spec "0.2.0-SNAPSHOT"]
|
||||||
[metosin/reitit-swagger "0.1.4-SNAPSHOT"]
|
[metosin/reitit-schema "0.2.0-SNAPSHOT"]
|
||||||
[metosin/reitit-swagger-ui "0.1.4-SNAPSHOT"]
|
[metosin/reitit-swagger "0.2.0-SNAPSHOT"]
|
||||||
[metosin/reitit-frontend "0.1.4-SNAPSHOT"]
|
[metosin/reitit-swagger-ui "0.2.0-SNAPSHOT"]
|
||||||
|
[metosin/reitit-frontend "0.2.0-SNAPSHOT"]
|
||||||
[meta-merge "1.0.0"]
|
[meta-merge "1.0.0"]
|
||||||
[ring/ring-core "1.6.3"]
|
[ring/ring-core "1.6.3"]
|
||||||
[metosin/spec-tools "0.7.1"]
|
[metosin/spec-tools "0.7.1"]
|
||||||
[metosin/schema-tools "0.10.3"]
|
[metosin/schema-tools "0.10.3"]
|
||||||
[metosin/ring-swagger-ui "2.2.10"]
|
[metosin/ring-swagger-ui "2.2.10"]
|
||||||
|
[metosin/muuntaja "0.6.0-alpha1"]
|
||||||
[metosin/jsonista "0.2.1"]]
|
[metosin/jsonista "0.2.1"]]
|
||||||
|
|
||||||
:plugins [[jonase/eastwood "0.2.6"]
|
:plugins [[jonase/eastwood "0.2.6"]
|
||||||
|
|
@ -38,6 +39,7 @@
|
||||||
:source-paths ["modules/reitit/src"
|
:source-paths ["modules/reitit/src"
|
||||||
"modules/reitit-core/src"
|
"modules/reitit-core/src"
|
||||||
"modules/reitit-ring/src"
|
"modules/reitit-ring/src"
|
||||||
|
"modules/reitit-middleware/src"
|
||||||
"modules/reitit-spec/src"
|
"modules/reitit-spec/src"
|
||||||
"modules/reitit-schema/src"
|
"modules/reitit-schema/src"
|
||||||
"modules/reitit-swagger/src"
|
"modules/reitit-swagger/src"
|
||||||
|
|
@ -55,7 +57,7 @@
|
||||||
|
|
||||||
[ring "1.6.3"]
|
[ring "1.6.3"]
|
||||||
[ikitommi/immutant-web "3.0.0-alpha1"]
|
[ikitommi/immutant-web "3.0.0-alpha1"]
|
||||||
[metosin/muuntaja "0.6.0-SNAPSHOT"]
|
[metosin/muuntaja "0.6.0-alpha1"]
|
||||||
[metosin/ring-swagger-ui "2.2.10"]
|
[metosin/ring-swagger-ui "2.2.10"]
|
||||||
[metosin/jsonista "0.2.1"]
|
[metosin/jsonista "0.2.1"]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,6 @@
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
# Modules
|
# Modules
|
||||||
for ext in reitit-core reitit-ring reitit-spec reitit-schema reitit-swagger reitit-swagger-ui reitit-frontend reitit; do
|
for ext in reitit-core reitit-ring reitit-middleware reitit-spec reitit-schema reitit-swagger reitit-swagger-ui reitit-frontend reitit; do
|
||||||
cd modules/$ext; lein "$@"; cd ../..;
|
cd modules/$ext; lein "$@"; cd ../..;
|
||||||
done
|
done
|
||||||
|
|
|
||||||
116
test/clj/reitit/ring/middleware/exception_test.clj
Normal file
116
test/clj/reitit/ring/middleware/exception_test.clj
Normal file
|
|
@ -0,0 +1,116 @@
|
||||||
|
(ns reitit.ring.middleware.exception-test
|
||||||
|
(:require [clojure.test :refer [deftest testing is]]
|
||||||
|
[reitit.ring :as ring]
|
||||||
|
[reitit.ring.middleware.exception :as exception]
|
||||||
|
[reitit.coercion.spec]
|
||||||
|
[reitit.ring.coercion]
|
||||||
|
[muuntaja.core :as m])
|
||||||
|
(:import (java.sql SQLException SQLWarning)))
|
||||||
|
|
||||||
|
(derive ::kikka ::kukka)
|
||||||
|
|
||||||
|
(deftest exception-test
|
||||||
|
(letfn [(create
|
||||||
|
([f]
|
||||||
|
(create f nil))
|
||||||
|
([f wrap]
|
||||||
|
(ring/ring-handler
|
||||||
|
(ring/router
|
||||||
|
[["/defaults"
|
||||||
|
{:handler f}]
|
||||||
|
["/coercion"
|
||||||
|
{:middleware [reitit.ring.coercion/coerce-request-middleware
|
||||||
|
reitit.ring.coercion/coerce-response-middleware]
|
||||||
|
:coercion reitit.coercion.spec/coercion
|
||||||
|
:parameters {:query {:x int?, :y int?}}
|
||||||
|
:responses {200 {:body {:total pos-int?}}}
|
||||||
|
:handler f}]]
|
||||||
|
{:data {:middleware [(exception/create-exception-middleware
|
||||||
|
(merge
|
||||||
|
exception/default-handlers
|
||||||
|
{::kikka (constantly {:status 400, :body "kikka"})
|
||||||
|
SQLException (constantly {:status 400, :body "sql"})
|
||||||
|
::exception/wrap wrap}))]}}))))]
|
||||||
|
|
||||||
|
(testing "normal calls work ok"
|
||||||
|
(let [response {:status 200, :body "ok"}
|
||||||
|
app (create (fn [_] response))]
|
||||||
|
(is (= response (app {:request-method :get, :uri "/defaults"})))))
|
||||||
|
|
||||||
|
(testing "unknown exception"
|
||||||
|
(let [app (create (fn [_] (throw (NullPointerException.))))]
|
||||||
|
(is (= {:status 500
|
||||||
|
:body {:type "exception"
|
||||||
|
:class "java.lang.NullPointerException"}}
|
||||||
|
(app {:request-method :get, :uri "/defaults"}))))
|
||||||
|
(let [app (create (fn [_] (throw (ex-info "fail" {:type ::invalid}))))]
|
||||||
|
(is (= {:status 500
|
||||||
|
:body {:type "exception"
|
||||||
|
:class "clojure.lang.ExceptionInfo"}}
|
||||||
|
(app {:request-method :get, :uri "/defaults"})))))
|
||||||
|
|
||||||
|
(testing "::ring/response"
|
||||||
|
(let [response {:status 200, :body "ok"}
|
||||||
|
app (create (fn [_] (throw (ex-info "fail" {:type ::ring/response, :response response}))))]
|
||||||
|
(is (= response (app {:request-method :get, :uri "/defaults"})))))
|
||||||
|
|
||||||
|
(testing ":muuntaja/decode"
|
||||||
|
(let [app (create (fn [_] (m/decode m/instance "application/json" "{:so \"invalid\"}")))]
|
||||||
|
(is (= {:body "Malformed \"application/json\" request."
|
||||||
|
:headers {"Content-Type" "text/plain"}
|
||||||
|
:status 400}
|
||||||
|
(app {:request-method :get, :uri "/defaults"}))))
|
||||||
|
|
||||||
|
(testing "::coercion/request-coercion"
|
||||||
|
(let [app (create (fn [{{{:keys [x y]} :query} :parameters}]
|
||||||
|
{:status 200, :body {:total (+ x y)}}))]
|
||||||
|
|
||||||
|
(let [{:keys [status body]} (app {:request-method :get
|
||||||
|
:uri "/coercion"
|
||||||
|
:query-params {"x" "1", "y" "2"}})]
|
||||||
|
(is (= 200 status))
|
||||||
|
(is (= {:total 3} body)))
|
||||||
|
|
||||||
|
(let [{:keys [status body]} (app {:request-method :get
|
||||||
|
:uri "/coercion"
|
||||||
|
:query-params {"x" "abba", "y" "2"}})]
|
||||||
|
(is (= 400 status))
|
||||||
|
(is (= :reitit.coercion/request-coercion (:type body))))
|
||||||
|
|
||||||
|
(let [{:keys [status body]} (app {:request-method :get
|
||||||
|
:uri "/coercion"
|
||||||
|
:query-params {"x" "-10", "y" "2"}})]
|
||||||
|
(is (= 500 status))
|
||||||
|
(is (= :reitit.coercion/response-coercion (:type body)))))))
|
||||||
|
|
||||||
|
(testing "exact :type"
|
||||||
|
(let [app (create (fn [_] (throw (ex-info "fail" {:type ::kikka}))))]
|
||||||
|
(is (= {:status 400, :body "kikka"}
|
||||||
|
(app {:request-method :get, :uri "/defaults"})))))
|
||||||
|
|
||||||
|
(testing "parent :type"
|
||||||
|
(let [app (create (fn [_] (throw (ex-info "fail" {:type ::kukka}))))]
|
||||||
|
(is (= {:status 400, :body "kikka"}
|
||||||
|
(app {:request-method :get, :uri "/defaults"})))))
|
||||||
|
|
||||||
|
(testing "exact Exception"
|
||||||
|
(let [app (create (fn [_] (throw (SQLException.))))]
|
||||||
|
(is (= {:status 400, :body "sql"}
|
||||||
|
(app {:request-method :get, :uri "/defaults"})))))
|
||||||
|
|
||||||
|
(testing "Exception SuperClass"
|
||||||
|
(let [app (create (fn [_] (throw (SQLWarning.))))]
|
||||||
|
(is (= {:status 400, :body "sql"}
|
||||||
|
(app {:request-method :get, :uri "/defaults"})))))
|
||||||
|
|
||||||
|
(testing "::exception/wrap"
|
||||||
|
(let [calls (atom 0)
|
||||||
|
app (create (fn [_] (throw (SQLWarning.)))
|
||||||
|
(fn [handler exception request]
|
||||||
|
(if (< (swap! calls inc) 2)
|
||||||
|
(handler exception request)
|
||||||
|
{:status 500, :body "too many tries"})))]
|
||||||
|
(is (= {:status 400, :body "sql"}
|
||||||
|
(app {:request-method :get, :uri "/defaults"})))
|
||||||
|
(is (= {:status 500, :body "too many tries"}
|
||||||
|
(app {:request-method :get, :uri "/defaults"})))))))
|
||||||
143
test/clj/reitit/ring/middleware/muuntaja_test.clj
Normal file
143
test/clj/reitit/ring/middleware/muuntaja_test.clj
Normal file
|
|
@ -0,0 +1,143 @@
|
||||||
|
(ns reitit.ring.middleware.muuntaja-test
|
||||||
|
(:require [clojure.test :refer [deftest testing is]]
|
||||||
|
[reitit.ring :as ring]
|
||||||
|
[reitit.ring.middleware.muuntaja :as muuntaja]
|
||||||
|
[reitit.swagger :as swagger]
|
||||||
|
[muuntaja.core :as m]))
|
||||||
|
|
||||||
|
(deftest muuntaja-test
|
||||||
|
(let [data {:kikka "kukka"}
|
||||||
|
app (ring/ring-handler
|
||||||
|
(ring/router
|
||||||
|
["/ping" {:get (constantly {:status 200, :body data})}]
|
||||||
|
{:data {:muuntaja m/instance
|
||||||
|
:middleware [muuntaja/format-middleware]}}))]
|
||||||
|
(is (= data (->> {:request-method :get, :uri "/ping"}
|
||||||
|
(app)
|
||||||
|
:body
|
||||||
|
(m/decode m/instance "application/json"))))))
|
||||||
|
|
||||||
|
(deftest muuntaja-swagger-test
|
||||||
|
(let [with-defaults m/instance
|
||||||
|
no-edn-decode (m/create (-> m/default-options (update-in [:formats "application/edn"] dissoc :decoder)))
|
||||||
|
just-edn (m/create (-> m/default-options (m/select-formats ["application/edn"])))
|
||||||
|
app (ring/ring-handler
|
||||||
|
(ring/router
|
||||||
|
[["/defaults"
|
||||||
|
{:get identity}]
|
||||||
|
["/explicit-defaults"
|
||||||
|
{:muuntaja with-defaults
|
||||||
|
:get identity}]
|
||||||
|
["/no-edn-decode"
|
||||||
|
{:muuntaja no-edn-decode
|
||||||
|
:get identity}]
|
||||||
|
["/just-edn"
|
||||||
|
{:muuntaja just-edn
|
||||||
|
:get identity}]
|
||||||
|
["/swagger.json"
|
||||||
|
{:get {:no-doc true
|
||||||
|
:handler (swagger/create-swagger-handler)}}]]
|
||||||
|
{:data {:muuntaja m/instance
|
||||||
|
:middleware [muuntaja/format-middleware]}}))
|
||||||
|
spec (fn [path]
|
||||||
|
(let [path (keyword path)]
|
||||||
|
(-> {:request-method :get :uri "/swagger.json"}
|
||||||
|
(app) :body
|
||||||
|
(->> (m/decode m/instance "application/json"))
|
||||||
|
:paths path :get)))
|
||||||
|
produces (comp set :produces spec)
|
||||||
|
consumes (comp set :consumes spec)]
|
||||||
|
|
||||||
|
(testing "with defaults"
|
||||||
|
(let [path "/defaults"]
|
||||||
|
(is (= #{"application/json"
|
||||||
|
"application/transit+msgpack"
|
||||||
|
"application/transit+json"
|
||||||
|
"application/edn"}
|
||||||
|
(produces path)
|
||||||
|
(consumes path)))))
|
||||||
|
|
||||||
|
(testing "with explicit muuntaja defaults"
|
||||||
|
(let [path "/explicit-defaults"]
|
||||||
|
(is (= #{"application/json"
|
||||||
|
"application/transit+msgpack"
|
||||||
|
"application/transit+json"
|
||||||
|
"application/edn"}
|
||||||
|
(produces path)
|
||||||
|
(consumes path)))))
|
||||||
|
|
||||||
|
(testing "without edn decode"
|
||||||
|
(let [path "/no-edn-decode"]
|
||||||
|
(is (= #{"application/json"
|
||||||
|
"application/transit+msgpack"
|
||||||
|
"application/transit+json"
|
||||||
|
"application/edn"}
|
||||||
|
(produces path)))
|
||||||
|
(is (= #{"application/json"
|
||||||
|
"application/transit+msgpack"
|
||||||
|
"application/transit+json"}
|
||||||
|
(consumes path)))))
|
||||||
|
|
||||||
|
(testing "just edn"
|
||||||
|
(let [path "/just-edn"]
|
||||||
|
(is (= #{"application/edn"}
|
||||||
|
(produces path)
|
||||||
|
(consumes path)))))))
|
||||||
|
|
||||||
|
(deftest muuntaja-swagger-parts-test
|
||||||
|
(let [app (ring/ring-handler
|
||||||
|
(ring/router
|
||||||
|
[["/request"
|
||||||
|
{:middleware [muuntaja/format-negotiate-middleware
|
||||||
|
muuntaja/format-request-middleware]
|
||||||
|
:get identity}]
|
||||||
|
["/response"
|
||||||
|
{:middleware [muuntaja/format-negotiate-middleware
|
||||||
|
muuntaja/format-response-middleware]
|
||||||
|
:get identity}]
|
||||||
|
["/both"
|
||||||
|
{:middleware [muuntaja/format-negotiate-middleware
|
||||||
|
muuntaja/format-response-middleware
|
||||||
|
muuntaja/format-request-middleware]
|
||||||
|
:get identity}]
|
||||||
|
|
||||||
|
["/swagger.json"
|
||||||
|
{:get {:no-doc true
|
||||||
|
:handler (swagger/create-swagger-handler)}}]]
|
||||||
|
{:data {:muuntaja m/instance}}))
|
||||||
|
spec (fn [path]
|
||||||
|
(-> {:request-method :get :uri "/swagger.json"}
|
||||||
|
(app) :body :paths (get path) :get))
|
||||||
|
produces (comp :produces spec)
|
||||||
|
consumes (comp :consumes spec)]
|
||||||
|
|
||||||
|
(testing "just request formatting"
|
||||||
|
(let [path "/request"]
|
||||||
|
(is (nil? (produces path)))
|
||||||
|
(is (= #{"application/json"
|
||||||
|
"application/transit+msgpack"
|
||||||
|
"application/transit+json"
|
||||||
|
"application/edn"}
|
||||||
|
(consumes path)))))
|
||||||
|
|
||||||
|
(testing "just response formatting"
|
||||||
|
(let [path "/response"]
|
||||||
|
(is (= #{"application/json"
|
||||||
|
"application/transit+msgpack"
|
||||||
|
"application/transit+json"
|
||||||
|
"application/edn"}
|
||||||
|
(produces path)))
|
||||||
|
(is (nil? (consumes path)))))
|
||||||
|
|
||||||
|
(testing "just response formatting"
|
||||||
|
(let [path "/both"]
|
||||||
|
(is (= #{"application/json"
|
||||||
|
"application/transit+msgpack"
|
||||||
|
"application/transit+json"
|
||||||
|
"application/edn"}
|
||||||
|
(produces path)))
|
||||||
|
(is (= #{"application/json"
|
||||||
|
"application/transit+msgpack"
|
||||||
|
"application/transit+json"
|
||||||
|
"application/edn"}
|
||||||
|
(consumes path)))))))
|
||||||
|
|
@ -295,3 +295,19 @@
|
||||||
(-> router
|
(-> router
|
||||||
(r/match-by-name! ::route {:a "olipa", :b "kerran"})
|
(r/match-by-name! ::route {:a "olipa", :b "kerran"})
|
||||||
(r/match->path {:iso "pöriläinen"}))))))
|
(r/match->path {:iso "pöriläinen"}))))))
|
||||||
|
|
||||||
|
(deftest sequential-routes
|
||||||
|
(testing "sequential child routes work"
|
||||||
|
(is (= [["/api/0" {}]
|
||||||
|
["/api/1" {}]]
|
||||||
|
(-> ["/api"
|
||||||
|
(for [i (range 2)]
|
||||||
|
[(str "/" i)])]
|
||||||
|
(r/router)
|
||||||
|
(r/routes)))))
|
||||||
|
(testing "sequential route definition fails"
|
||||||
|
(is (thrown?
|
||||||
|
#?(:clj Exception, :cljs js/Error)
|
||||||
|
(-> ["/api"
|
||||||
|
(list "/ipa")]
|
||||||
|
(r/router))))))
|
||||||
|
|
|
||||||
|
|
@ -57,10 +57,117 @@
|
||||||
{:a 1} "a=1"
|
{:a 1} "a=1"
|
||||||
{:a nil} "a="
|
{:a nil} "a="
|
||||||
{:a :b :c "d"} "a=b&c=d"
|
{:a :b :c "d"} "a=b&c=d"
|
||||||
{:a "b c"} "a=b%20c"))
|
{:a "b c"} "a=b+c"))
|
||||||
|
|
||||||
; TODO: support seq values?
|
; TODO: support seq values?
|
||||||
;{:a ["b" "c"]} "a=b&a=c"
|
;{:a ["b" "c"]} "a=b&a=c"
|
||||||
;{:a ["c" "b"]} "a=c&a=b"
|
;{:a ["c" "b"]} "a=c&a=b"
|
||||||
;{:a (seq [1 2])} "a=1&a=2"
|
;{:a (seq [1 2])} "a=1&a=2"
|
||||||
;{:a #{"c" "b"}} "a=b&a=c"
|
;{:a #{"c" "b"}} "a=b&a=c"
|
||||||
|
|
||||||
|
;; test from https://github.com/playframework/playframework -> UriEncodingSpec.scala
|
||||||
|
|
||||||
|
(deftest url-encode-test
|
||||||
|
(are [in out]
|
||||||
|
(= out (impl/url-encode in))
|
||||||
|
|
||||||
|
"/" "%2F"
|
||||||
|
"?" "%3F"
|
||||||
|
"#" "%23"
|
||||||
|
"[" "%5B"
|
||||||
|
"]" "%5D"
|
||||||
|
"!" "!"
|
||||||
|
#_#_"$" "$"
|
||||||
|
#_#_"&" "&"
|
||||||
|
"'" "'"
|
||||||
|
"(" "("
|
||||||
|
")" ")"
|
||||||
|
"*" "*"
|
||||||
|
#_#_"+" "+"
|
||||||
|
#_#_"," ","
|
||||||
|
#_#_";" ";"
|
||||||
|
#_#_"=" "="
|
||||||
|
#_#_":" ":"
|
||||||
|
#_#_"@" "@"
|
||||||
|
"a" "a"
|
||||||
|
"z" "z"
|
||||||
|
"A" "A"
|
||||||
|
"Z" "Z"
|
||||||
|
"0" "0"
|
||||||
|
"9" "9"
|
||||||
|
"-" "-"
|
||||||
|
"." "."
|
||||||
|
"_" "_"
|
||||||
|
"~" "~"
|
||||||
|
"\000" "%00"
|
||||||
|
"\037" "%1F"
|
||||||
|
" " "%20"
|
||||||
|
"\"" "%22"
|
||||||
|
"%" "%25"
|
||||||
|
"<" "%3C"
|
||||||
|
">" "%3E"
|
||||||
|
"\\" "%5C"
|
||||||
|
"^" "%5E"
|
||||||
|
"`" "%60"
|
||||||
|
"{" "%7B"
|
||||||
|
"|" "%7C"
|
||||||
|
"}" "%7D"
|
||||||
|
"\177" "%7F"
|
||||||
|
#_#_"\377" "%FF"
|
||||||
|
|
||||||
|
"£0.25" "%C2%A30.25"
|
||||||
|
"€100" "%E2%82%AC100"
|
||||||
|
"«küßî»" "%C2%ABk%C3%BC%C3%9F%C3%AE%C2%BB"
|
||||||
|
"“ЌύБЇ”" "%E2%80%9C%D0%8C%CF%8D%D0%91%D0%87%E2%80%9D"
|
||||||
|
|
||||||
|
"\000" "%00"
|
||||||
|
#_#_"\231" "%99"
|
||||||
|
#_#_"\252" "%AA"
|
||||||
|
#_#_"\377" "%FF"
|
||||||
|
|
||||||
|
"" ""
|
||||||
|
"1" "1"
|
||||||
|
"12" "12"
|
||||||
|
"123" "123"
|
||||||
|
"1234567890" "1234567890"
|
||||||
|
|
||||||
|
"Hello world" "Hello%20world"
|
||||||
|
"/home/foo" "%2Fhome%2Ffoo"
|
||||||
|
|
||||||
|
" " "%20"
|
||||||
|
"+" "%2B" #_"+"
|
||||||
|
" +" "%20%2B" #_"%20+"
|
||||||
|
#_#_"1+2=3" "1+2=3"
|
||||||
|
#_#_"1 + 2 = 3" "1%20+%202%20=%203"))
|
||||||
|
|
||||||
|
(deftest url-decode-test
|
||||||
|
(are [in out]
|
||||||
|
(= out (impl/url-decode in))
|
||||||
|
|
||||||
|
"1+1" "1+1"
|
||||||
|
"%21" "!"
|
||||||
|
"%61" "a"
|
||||||
|
"%31%32%33" "123"
|
||||||
|
"%2b" "+"
|
||||||
|
"%7e" "~"
|
||||||
|
"hello%20world" "hello world"
|
||||||
|
"a%2fb" "a/b"
|
||||||
|
"a/.." "a/.."
|
||||||
|
"a/." "a/."
|
||||||
|
"//a" "//a"
|
||||||
|
"a//b" "a//b"
|
||||||
|
"a//" "a//"
|
||||||
|
"/path/%C2%ABk%C3%BC%C3%9F%C3%AE%C2%BB" "/path/«küßî»"
|
||||||
|
"/path/%E2%80%9C%D0%8C%CF%8D%D0%91%D0%87%E2%80%9D" "/path/“ЌύБЇ”"))
|
||||||
|
|
||||||
|
(deftest form-encode-test
|
||||||
|
(are [in out]
|
||||||
|
(= out (impl/form-encode in))
|
||||||
|
|
||||||
|
"+632 905 123 4567" "%2B632+905+123+4567"))
|
||||||
|
|
||||||
|
(deftest form-decode-test
|
||||||
|
(are [in out]
|
||||||
|
(= out (impl/form-decode in))
|
||||||
|
|
||||||
|
"%2B632+905+123+4567" "+632 905 123 4567"))
|
||||||
|
|
|
||||||
|
|
@ -24,11 +24,14 @@
|
||||||
(defn handler [ctx]
|
(defn handler [ctx]
|
||||||
(conj ctx :ok))
|
(conj ctx :ok))
|
||||||
|
|
||||||
(defn create [interceptors]
|
(defn create
|
||||||
(let [chain (interceptor/chain
|
([interceptors]
|
||||||
interceptors
|
(create interceptors nil))
|
||||||
handler :data nil)]
|
([interceptors opts]
|
||||||
(partial execute chain)))
|
(let [chain (interceptor/chain
|
||||||
|
interceptors
|
||||||
|
handler :data opts)]
|
||||||
|
(partial execute chain))))
|
||||||
|
|
||||||
(deftest expand-interceptor-test
|
(deftest expand-interceptor-test
|
||||||
|
|
||||||
|
|
@ -55,6 +58,19 @@
|
||||||
(is (= [:value :ok] (app ctx)))
|
(is (= [:value :ok] (app ctx)))
|
||||||
(is (= 1 @calls)))))
|
(is (= 1 @calls)))))
|
||||||
|
|
||||||
|
(testing "as keyword"
|
||||||
|
(reset! calls 0)
|
||||||
|
(let [app (create [:enter] {::interceptor/registry {:enter (enter :value)}})]
|
||||||
|
(dotimes [_ 10]
|
||||||
|
(is (= [:value :ok] (app ctx)))
|
||||||
|
(is (= 1 @calls)))))
|
||||||
|
|
||||||
|
(testing "missing keyword"
|
||||||
|
(is (thrown-with-msg?
|
||||||
|
ExceptionInfo
|
||||||
|
#"Interceptor :enter not found in registry"
|
||||||
|
(create [:enter]))))
|
||||||
|
|
||||||
(testing "as map"
|
(testing "as map"
|
||||||
(reset! calls 0)
|
(reset! calls 0)
|
||||||
(let [app (create [{:enter (enter :value)}])]
|
(let [app (create [{:enter (enter :value)}])]
|
||||||
|
|
|
||||||
|
|
@ -10,12 +10,15 @@
|
||||||
(defn handler [request]
|
(defn handler [request]
|
||||||
(conj request :ok))
|
(conj request :ok))
|
||||||
|
|
||||||
(defn create [middleware]
|
(defn create
|
||||||
(middleware/chain
|
([middleware]
|
||||||
middleware
|
(create middleware nil))
|
||||||
handler
|
([middleware opts]
|
||||||
:data
|
(middleware/chain
|
||||||
nil))
|
middleware
|
||||||
|
handler
|
||||||
|
:data
|
||||||
|
opts)))
|
||||||
|
|
||||||
(deftest expand-middleware-test
|
(deftest expand-middleware-test
|
||||||
|
|
||||||
|
|
@ -42,6 +45,26 @@
|
||||||
(is (= [:value :ok] (app request)))
|
(is (= [:value :ok] (app request)))
|
||||||
(is (= 1 @calls)))))
|
(is (= 1 @calls)))))
|
||||||
|
|
||||||
|
(testing "as keyword"
|
||||||
|
(reset! calls 0)
|
||||||
|
(let [app (create [:wrap] {::middleware/registry {:wrap #(wrap % :value)}})]
|
||||||
|
(dotimes [_ 10]
|
||||||
|
(is (= [:value :ok] (app request)))
|
||||||
|
(is (= 1 @calls)))))
|
||||||
|
|
||||||
|
(testing "as keyword vector"
|
||||||
|
(reset! calls 0)
|
||||||
|
(let [app (create [[:wrap :value]] {::middleware/registry {:wrap wrap}})]
|
||||||
|
(dotimes [_ 10]
|
||||||
|
(is (= [:value :ok] (app request)))
|
||||||
|
(is (= 1 @calls)))))
|
||||||
|
|
||||||
|
(testing "missing keyword"
|
||||||
|
(is (thrown-with-msg?
|
||||||
|
ExceptionInfo
|
||||||
|
#"Middleware :wrap not found in registry"
|
||||||
|
(create [:wrap]))))
|
||||||
|
|
||||||
(testing "as function vector with value(s)"
|
(testing "as function vector with value(s)"
|
||||||
(reset! calls 0)
|
(reset! calls 0)
|
||||||
(let [app (create [[wrap :value]])]
|
(let [app (create [[wrap :value]])]
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@
|
||||||
:query-params {"a" "1"}
|
:query-params {"a" "1"}
|
||||||
:body-params {:b 2}
|
:body-params {:b 2}
|
||||||
:form-params {:c 3}
|
:form-params {:c 3}
|
||||||
:header-params {:d 4}})
|
:headers {"d" "4"}})
|
||||||
|
|
||||||
(def invalid-request
|
(def invalid-request
|
||||||
{:uri "/api/plus/5"
|
{:uri "/api/plus/5"
|
||||||
|
|
@ -40,7 +40,7 @@
|
||||||
:query-params {"a" "1"}
|
:query-params {"a" "1"}
|
||||||
:body-params {:b 2}
|
:body-params {:b 2}
|
||||||
:form-params {:c 3}
|
:form-params {:c 3}
|
||||||
:header-params {:d -40}})
|
:headers {"d" "-40"}})
|
||||||
|
|
||||||
(deftest spec-coercion-test
|
(deftest spec-coercion-test
|
||||||
(let [create (fn [middleware]
|
(let [create (fn [middleware]
|
||||||
|
|
|
||||||
|
|
@ -180,5 +180,23 @@
|
||||||
:handler (swagger/create-swagger-handler)}}]]))]
|
:handler (swagger/create-swagger-handler)}}]]))]
|
||||||
(is (= ["/ping"] (spec-paths app "/swagger.json")))
|
(is (= ["/ping"] (spec-paths app "/swagger.json")))
|
||||||
(is (= #{::swagger/default}
|
(is (= #{::swagger/default}
|
||||||
(-> {:request-method :get :uri "/swagger.json"}
|
(-> {:request-method :get :uri "/swagger.json"}
|
||||||
(app) :body :x-id)))))
|
(app) :body :x-id)))))
|
||||||
|
|
||||||
|
(deftest all-parameter-types-test
|
||||||
|
(let [app (ring/ring-handler
|
||||||
|
(ring/router
|
||||||
|
[["/parameters"
|
||||||
|
{:post {:coercion spec/coercion
|
||||||
|
:parameters {:query {:q string?}
|
||||||
|
:body {:b string?}
|
||||||
|
:form {:f string?}
|
||||||
|
:header {:h string?}
|
||||||
|
:path {:p string?}}
|
||||||
|
:handler identity}}]
|
||||||
|
["/swagger.json"
|
||||||
|
{:get {:no-doc true
|
||||||
|
:handler (swagger/create-swagger-handler)}}]]))
|
||||||
|
spec (:body (app {:request-method :get, :uri "/swagger.json"}))]
|
||||||
|
(is (= ["query" "body" "formData" "header" "path"]
|
||||||
|
(map :in (get-in spec [:paths "/parameters" :post :parameters]))))))
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,24 @@
|
||||||
(ns reitit.frontend.history-test
|
(ns reitit.frontend.history-test
|
||||||
(:require [clojure.test :refer [deftest testing is are]]
|
(:require [clojure.test :refer [deftest testing is are async]]
|
||||||
[reitit.core :as r]
|
[reitit.core :as r]
|
||||||
|
[reitit.frontend :as rf]
|
||||||
[reitit.frontend.history :as rfh]
|
[reitit.frontend.history :as rfh]
|
||||||
[reitit.frontend.test-utils :refer [capture-console]]))
|
[reitit.frontend.test-utils :refer [capture-console]]
|
||||||
|
[goog.events :as gevents]))
|
||||||
|
|
||||||
(def browser (exists? js/window))
|
(def browser (exists? js/window))
|
||||||
|
|
||||||
|
(def router (r/router ["/"
|
||||||
|
["" ::frontpage]
|
||||||
|
["foo" ::foo]
|
||||||
|
["bar/:id" ::bar]]))
|
||||||
|
|
||||||
(deftest fragment-history-test
|
(deftest fragment-history-test
|
||||||
(when browser
|
(when browser
|
||||||
(let [router (r/router ["/"
|
(gevents/removeAll js/window goog.events.EventType.POPSTATE)
|
||||||
["" ::frontpage]
|
(gevents/removeAll js/window goog.events.EventType.HASHCHANGE)
|
||||||
["foo" ::foo]
|
|
||||||
["bar/:id" ::bar]])
|
(let [history (rfh/start! router (fn [_]) {:use-fragment true})]
|
||||||
history (rfh/start! router
|
|
||||||
(fn [_])
|
|
||||||
{:use-fragment true
|
|
||||||
:path-prefix "/"})]
|
|
||||||
|
|
||||||
(testing "creating urls"
|
(testing "creating urls"
|
||||||
(is (= "#/foo"
|
(is (= "#/foo"
|
||||||
|
|
@ -30,18 +33,49 @@
|
||||||
(is (= nil value))
|
(is (= nil value))
|
||||||
(is (= [{:type :warn
|
(is (= [{:type :warn
|
||||||
:message ["missing route" ::asd]}]
|
:message ["missing route" ::asd]}]
|
||||||
messages)))))))
|
messages))))
|
||||||
|
|
||||||
|
(rfh/stop! history))))
|
||||||
|
|
||||||
|
(deftest fragment-history-routing-test
|
||||||
|
(when browser
|
||||||
|
(gevents/removeAll js/window goog.events.EventType.POPSTATE)
|
||||||
|
(gevents/removeAll js/window goog.events.EventType.HASHCHANGE)
|
||||||
|
|
||||||
|
(async done
|
||||||
|
(let [n (atom 0)
|
||||||
|
history (rfh/start! router
|
||||||
|
(fn [match history]
|
||||||
|
(let [url (rfh/-get-path history)]
|
||||||
|
(case (swap! n inc)
|
||||||
|
1 (do (is (= "/" url)
|
||||||
|
"start at root")
|
||||||
|
(rfh/push-state history ::foo))
|
||||||
|
2 (do (is (= "/foo" url)
|
||||||
|
"push-state")
|
||||||
|
(.back js/window.history))
|
||||||
|
3 (do (is (= "/" url)
|
||||||
|
"go back")
|
||||||
|
(rfh/push-state history ::bar {:id 1}))
|
||||||
|
4 (do (is (= "/bar/1" url)
|
||||||
|
"push-state 2")
|
||||||
|
(rfh/replace-state history ::bar {:id 2}))
|
||||||
|
5 (do (is (= "/bar/2" url)
|
||||||
|
"replace-state")
|
||||||
|
(.back js/window.history))
|
||||||
|
6 (do (is (= "/" url)
|
||||||
|
"go back after replace state")
|
||||||
|
(rfh/stop! history)
|
||||||
|
(done))
|
||||||
|
(do (is false "extra event")))))
|
||||||
|
{:use-fragment true})]))))
|
||||||
|
|
||||||
(deftest html5-history-test
|
(deftest html5-history-test
|
||||||
(when browser
|
(when browser
|
||||||
(let [router (r/router ["/"
|
(gevents/removeAll js/window goog.events.EventType.POPSTATE)
|
||||||
["" ::frontpage]
|
(gevents/removeAll js/window goog.events.EventType.HASHCHANGE)
|
||||||
["foo" ::foo]
|
|
||||||
["bar/:id" ::bar]])
|
(let [history (rfh/start! router (fn [_]) {:use-fragment false})]
|
||||||
history (rfh/start! router
|
|
||||||
(fn [_])
|
|
||||||
{:use-fragment false
|
|
||||||
:path-prefix "/"})]
|
|
||||||
|
|
||||||
(testing "creating urls"
|
(testing "creating urls"
|
||||||
(is (= "/foo"
|
(is (= "/foo"
|
||||||
|
|
@ -56,4 +90,40 @@
|
||||||
(is (= nil value))
|
(is (= nil value))
|
||||||
(is (= [{:type :warn
|
(is (= [{:type :warn
|
||||||
:message ["missing route" ::asd]}]
|
:message ["missing route" ::asd]}]
|
||||||
messages)))))))
|
messages))))
|
||||||
|
|
||||||
|
(rfh/stop! history))))
|
||||||
|
|
||||||
|
(deftest html5-history-routing-test
|
||||||
|
(when browser
|
||||||
|
(gevents/removeAll js/window goog.events.EventType.POPSTATE)
|
||||||
|
(gevents/removeAll js/window goog.events.EventType.HASHCHANGE)
|
||||||
|
|
||||||
|
(async done
|
||||||
|
(let [n (atom 0)
|
||||||
|
history (rfh/start! router
|
||||||
|
(fn [match history]
|
||||||
|
(let [url (rfh/-get-path history)]
|
||||||
|
(case (swap! n inc)
|
||||||
|
1 (do (rfh/push-state history ::frontpage))
|
||||||
|
2 (do (is (= "/" url)
|
||||||
|
"start at root")
|
||||||
|
(rfh/push-state history ::foo))
|
||||||
|
3 (do (is (= "/foo" url)
|
||||||
|
"push-state")
|
||||||
|
(.back js/window.history))
|
||||||
|
4 (do (is (= "/" url)
|
||||||
|
"go back")
|
||||||
|
(rfh/push-state history ::bar {:id 1}))
|
||||||
|
5 (do (is (= "/bar/1" url)
|
||||||
|
"push-state 2")
|
||||||
|
(rfh/replace-state history ::bar {:id 2}))
|
||||||
|
6 (do (is (= "/bar/2" url)
|
||||||
|
"replace-state")
|
||||||
|
(.back js/window.history))
|
||||||
|
7 (do (is (= "/" url)
|
||||||
|
"go back after replace state")
|
||||||
|
(rfh/stop! history)
|
||||||
|
(done))
|
||||||
|
(do (is false "extra event")))))
|
||||||
|
{:use-fragment false})]))))
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue