mirror of
https://github.com/metosin/reitit.git
synced 2025-12-21 10:01:11 +00:00
Merge pull request #30 from metosin/FirstClassMiddleware
First class middleware
This commit is contained in:
commit
3df6f11c2b
12 changed files with 275 additions and 237 deletions
114
README.md
114
README.md
|
|
@ -2,12 +2,12 @@
|
||||||
|
|
||||||
A friendly data-driven router for Clojure(Script).
|
A friendly data-driven router for Clojure(Script).
|
||||||
|
|
||||||
* Simple data-driven route syntax
|
* Simple data-driven [route syntax](#route-syntax)
|
||||||
* First-class route meta-data
|
* First-class [route meta-data](#route-meta-data)
|
||||||
* Generic, not tied to HTTP
|
* Generic, not tied to HTTP
|
||||||
* [Route conflict resolution](#route-conflicts)
|
* [Route conflict resolution](#route-conflicts)
|
||||||
* [Pluggable coercion](#parameter-coercion) ([clojure.spec](https://clojure.org/about/spec))
|
* [Pluggable coercion](#parameter-coercion) ([clojure.spec](https://clojure.org/about/spec))
|
||||||
* both Middleware & Interceptors
|
* both [Middleware](#middleware) & Interceptors
|
||||||
* Extendable
|
* Extendable
|
||||||
* Fast
|
* Fast
|
||||||
|
|
||||||
|
|
@ -87,7 +87,7 @@ Creating a router:
|
||||||
`:mixed-router` is created (both static & wild routes are found):
|
`:mixed-router` is created (both static & wild routes are found):
|
||||||
|
|
||||||
```clj
|
```clj
|
||||||
(reitit/router-type router)
|
(reitit/router-name router)
|
||||||
; :mixed-router
|
; :mixed-router
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -147,7 +147,7 @@ Only a partial match. Let's provide the path-parameters:
|
||||||
|
|
||||||
There is also a exception throwing version:
|
There is also a exception throwing version:
|
||||||
|
|
||||||
```
|
```clj
|
||||||
(reitit/match-by-name! router ::user)
|
(reitit/match-by-name! router ::user)
|
||||||
; ExceptionInfo missing path-params for route /api/user/:id: #{:id}
|
; ExceptionInfo missing path-params for route /api/user/:id: #{:id}
|
||||||
```
|
```
|
||||||
|
|
@ -159,12 +159,14 @@ Routes can have arbitrary meta-data. For nested routes, the meta-data is accumul
|
||||||
A router based on nested route tree:
|
A router based on nested route tree:
|
||||||
|
|
||||||
```clj
|
```clj
|
||||||
|
(def router
|
||||||
(reitit/router
|
(reitit/router
|
||||||
["/api" {:interceptors [::api]}
|
["/api" {:interceptors [::api]}
|
||||||
["/ping" ::ping]
|
["/ping" ::ping]
|
||||||
["/admin" {:roles #{:admin}}
|
["/admin" {:roles #{:admin}}
|
||||||
["/users" ::users]
|
["/users" ::users]
|
||||||
["/db" {:interceptors [::db], :roles ^:replace #{:db-admin}}
|
["/db" {:interceptors [::db]
|
||||||
|
:roles ^:replace #{:db-admin}}
|
||||||
["/:db" {:parameters {:db String}}
|
["/:db" {:parameters {:db String}}
|
||||||
["/drop" ::drop-db]
|
["/drop" ::drop-db]
|
||||||
["/stats" ::db-stats]]]]]))
|
["/stats" ::db-stats]]]]]))
|
||||||
|
|
@ -233,7 +235,7 @@ Route trees should not have multiple routes that match to a single (request) pat
|
||||||
|
|
||||||
## Ring
|
## Ring
|
||||||
|
|
||||||
[Ring](https://github.com/ring-clojure/ring)-router adds support for ring [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 validates that all paths have a `:handler` defined and expands `:middleware` to create accumulated handlers for all request-methods. `reitit.ring/ring-handler` creates an actual ring handler out of a ring-router.
|
[Ring](https://github.com/ring-clojure/ring)-router adds support for ring concepts like [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 runs a custom route compiler, creating a optimized stucture for handling route matches, with compiled middleware chain & handlers for all request methods. It also ensures that all routes have a `:handler` defined.
|
||||||
|
|
||||||
Simple Ring app:
|
Simple Ring app:
|
||||||
|
|
||||||
|
|
@ -249,13 +251,6 @@ Simple Ring app:
|
||||||
["/ping" handler])))
|
["/ping" handler])))
|
||||||
```
|
```
|
||||||
|
|
||||||
The expanded routes:
|
|
||||||
|
|
||||||
```clj
|
|
||||||
(-> app (ring/get-router) (reitit/routes))
|
|
||||||
; [["/ping" {:handler #object[...]}]]
|
|
||||||
```
|
|
||||||
|
|
||||||
Applying the handler:
|
Applying the handler:
|
||||||
|
|
||||||
```clj
|
```clj
|
||||||
|
|
@ -266,8 +261,23 @@ Applying the handler:
|
||||||
; {:status 200, :body "ok"}
|
; {:status 200, :body "ok"}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The expanded routes:
|
||||||
|
|
||||||
|
```clj
|
||||||
|
(-> app (ring/get-router) (reitit/routes))
|
||||||
|
; [["/ping"
|
||||||
|
; {:handler #object[...]}
|
||||||
|
; #Methods{:any #Endpoint{:meta {:handler #object[...]},
|
||||||
|
; :handler #object[...],
|
||||||
|
; :middleware []}}]]
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that the compiled resuts as third element in the route vector.
|
||||||
|
|
||||||
### Request-method based routing
|
### Request-method based routing
|
||||||
|
|
||||||
|
Handler are also looked under request-method keys: `:get`, `:head`, `:patch`, `:delete`, `:options`, `:post` or `:put`. Top-level handler is used if request-method based handler is not found.
|
||||||
|
|
||||||
```clj
|
```clj
|
||||||
(def app
|
(def app
|
||||||
(ring/ring-handler
|
(ring/ring-handler
|
||||||
|
|
@ -295,21 +305,18 @@ Reverse routing:
|
||||||
|
|
||||||
### Middleware
|
### Middleware
|
||||||
|
|
||||||
`:middleware` should be a vector of either of the following (expanded via the `reitit.middleware/ExpandMiddleware`:
|
Middleware can be added with a `:middleware` key, with a vector value of the following:
|
||||||
|
|
||||||
1. a ring middleware function of `handler -> request -> response`
|
1. ring middleware function `handler -> request -> response`
|
||||||
2. a vector of middleware function (`handler args -> request -> response`) and it's args - actial middleware is created by applying function with handler and args
|
2. vector of middleware function `handler ?args -> request -> response` and optinally it's args.
|
||||||
|
|
||||||
Let's define some middleware and a handler:
|
A middleware and a handler:
|
||||||
|
|
||||||
```clj
|
```clj
|
||||||
(defn wrap [handler id]
|
(defn wrap [handler id]
|
||||||
(fn [request]
|
(fn [request]
|
||||||
(handler (update request ::acc (fnil conj []) id))))
|
(handler (update request ::acc (fnil conj []) id))))
|
||||||
|
|
||||||
(defn wrap-api [handler]
|
|
||||||
(wrap handler :api))
|
|
||||||
|
|
||||||
(defn handler [{:keys [::acc]}]
|
(defn handler [{:keys [::acc]}]
|
||||||
{:status 200, :body (conj acc :handler)})
|
{:status 200, :body (conj acc :handler)})
|
||||||
```
|
```
|
||||||
|
|
@ -320,7 +327,7 @@ App with nested middleware:
|
||||||
(def app
|
(def app
|
||||||
(ring/ring-handler
|
(ring/ring-handler
|
||||||
(ring/router
|
(ring/router
|
||||||
["/api" {:middleware [wrap-api]}
|
["/api" {:middleware [#(wrap % :api)]}
|
||||||
["/ping" handler]
|
["/ping" handler]
|
||||||
["/admin" {:middleware [[wrap :admin]]}
|
["/admin" {:middleware [[wrap :admin]]}
|
||||||
["/db" {:middleware [[wrap :db]]
|
["/db" {:middleware [[wrap :db]]
|
||||||
|
|
@ -342,57 +349,48 @@ Middleware is applied correctly:
|
||||||
|
|
||||||
### Middleware Records
|
### Middleware Records
|
||||||
|
|
||||||
Besides just being opaque functions, middleware can be presented as first-class data entries, `reitit.middleware/Middleware` records. They are created with `reitit.middleware/create` function and must have a `:name` and either `:wrap` or `:gen` key with the actual middleware function or a [middleware generator function](#compiling-middleware).
|
Reitit supports first-class data-driven middleware via `reitit.middleware/Middleware` records, created with `reitit.middleware/create` function. The following keys have special purpose:
|
||||||
|
|
||||||
When routes are compiled, middleware records are unwrapped into normal middleware functions producing no runtime performance penalty. Thanks to the `ExpandMiddleware` protocol, plain clojure(script) maps can also be used - they get expanded into middleware records.
|
| key | description |
|
||||||
|
| -----------|-------------|
|
||||||
|
| `:name` | Name of the middleware as qualified keyword (optional,recommended for libs)
|
||||||
|
| `:wrap` | The actual middleware function of `handler args? => request => response`
|
||||||
|
| `:gen` | Middleware compile function, see [compiling middleware](#compiling-middleware).
|
||||||
|
|
||||||
The previous middleware re-written as records:
|
When routes are compiled, all middleware are expanded (and optionally compiled) into `Middleware` and stored in compilation results for later use (api-docs etc). For actual request processing, they are unwrapped into normal middleware functions producing zero runtime performance penalty. Middleware expansion is backed by `reitit.middleware/IntoMiddleware` protocol, enabling plain clojure(script) maps to be used.
|
||||||
|
|
||||||
|
A Record:
|
||||||
|
|
||||||
```clj
|
```clj
|
||||||
(require '[reitit.middleware :as middleware])
|
(require '[reitit.middleware :as middleware])
|
||||||
|
|
||||||
(def wrap2
|
(def wrap2
|
||||||
(middleware/create
|
(middleware/create
|
||||||
{:name ::wrap
|
{:name ::wrap2
|
||||||
:description "a nice little mw, takes 1 arg."
|
:description "a nice little mw, takes 1 arg."
|
||||||
:wrap wrap}))
|
:wrap wrap}))
|
||||||
|
|
||||||
(def wrap2-api
|
|
||||||
{:name ::wrap-api
|
|
||||||
:description "a nice little mw, :api as arg"
|
|
||||||
:wrap (fn [handler]
|
|
||||||
(wrap handler :api))})
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Or as maps:
|
As plain map:
|
||||||
|
|
||||||
```clj
|
```clj
|
||||||
(require '[reitit.middleware :as middleware])
|
;; plain map
|
||||||
|
|
||||||
(def wrap3
|
(def wrap3
|
||||||
{:name ::wrap
|
{:name ::wrap3
|
||||||
:description "a nice little mw, takes 1 arg."
|
|
||||||
:wrap wrap})
|
|
||||||
|
|
||||||
(def wrap3-api
|
|
||||||
{:name ::wrap-api
|
|
||||||
:description "a nice little mw, :api as arg"
|
:description "a nice little mw, :api as arg"
|
||||||
:wrap (fn [handler]
|
:wrap (fn [handler]
|
||||||
(wrap handler :api))})
|
(wrap handler :api))})
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### Async Ring
|
### Async Ring
|
||||||
|
|
||||||
All built-in middleware provide both the 2 and 3-arity, so they work with [Async Ring](https://www.booleanknot.com/blog/2016/07/15/asynchronous-ring.html) too.
|
All built-in middleware provide both 2 and 3-arity and are compiled for both Clojure & ClojureScript, so they work with [Async Ring](https://www.booleanknot.com/blog/2016/07/15/asynchronous-ring.html) and [Node.js](https://nodejs.org) too.
|
||||||
|
|
||||||
### Meta-data based extensions
|
### Meta-data based extensions
|
||||||
|
|
||||||
The routing `Match` is injected into a request and can be extracted with `reitit.ring/get-match`. It can be used to build dynamic extensions to the system.
|
`ring-handler` injects the `Match` into a request and it can be extracted at runtime with `reitit.ring/get-match`. This can be used to build dynamic extensions to the system.
|
||||||
|
|
||||||
A middleware to guard routes:
|
Example middleware to guard routes based on user roles:
|
||||||
|
|
||||||
```clj
|
```clj
|
||||||
(require '[clojure.set :as set])
|
(require '[clojure.set :as set])
|
||||||
|
|
@ -443,12 +441,12 @@ Authorized access to guarded route:
|
||||||
|
|
||||||
## Parameter coercion
|
## Parameter coercion
|
||||||
|
|
||||||
Reitit ships with pluggable parameter coercion via `reitit.coercion.protocol/Coercion` protocol. `reitit.coercion.spec/SpecCoercion` provides implements it for [clojure.spec](https://clojure.org/about/spec) & [data-specs](https://github.com/metosin/spec-tools#data-specs).
|
Reitit provides pluggable parameter coercion via `reitit.coercion.protocol/Coercion` protocol, originally introduced in [compojure-api](https://clojars.org/metosin/compojure-api). Reitit ships with `reitit.coercion.spec/SpecCoercion` providing implemenation for [clojure.spec](https://clojure.org/about/spec) and [data-specs](https://github.com/metosin/spec-tools#data-specs).
|
||||||
|
|
||||||
**NOTE**: to use the spec-coercion, one needs to add the following dependencies manually to the project:
|
**NOTE**: Before Clojure 1.9.0 is shipped, to use the spec-coercion, one needs to add the following dependencies manually to the project:
|
||||||
|
|
||||||
```clj
|
```clj
|
||||||
[org.clojure/clojure "1.9.0-alpha19"]
|
[org.clojure/clojure "1.9.0-alpha20"]
|
||||||
[org.clojure/spec.alpha "0.1.123"]
|
[org.clojure/spec.alpha "0.1.123"]
|
||||||
[metosin/spec-tools "0.3.3"]
|
[metosin/spec-tools "0.3.3"]
|
||||||
```
|
```
|
||||||
|
|
@ -536,13 +534,13 @@ If either request or response coercion fails, an descriptive error is thrown.
|
||||||
|
|
||||||
## Compiling Middleware
|
## Compiling Middleware
|
||||||
|
|
||||||
The [meta-data extensions](#meta-data-based-extensions) are a easy way to extend the system. Routes meta-data can be trasnformed into any shape (records, functions etc.) in route compilation, enabling easy access at request-time.
|
The [meta-data extensions](#meta-data-based-extensions) are a easy way to extend the system. Routes meta-data can be transformed into any shape (records, functions etc.) in route compilation, enabling fast access at request-time.
|
||||||
|
|
||||||
Still, we can do better. As we know the exact route interceptor/middleware is linked to, we can pass the (compiled) route information into the interceptor/middleware at creation-time. It can extract and transform relevant data just for it and pass it into the actual request-handler via a closure. We can do all the static local computations forehand, yielding faster runtime processing.
|
Still, we can do better. As we know the exact route that interceptor/middleware is linked to, we can pass the (compiled) route information into the interceptor/middleware at creation-time. It can extract and transform relevant data just for it and pass it into the actual request-handler via a closure - yielding faster runtime processing.
|
||||||
|
|
||||||
To do this we use [middleware records](#middleware-records) `:gen` hook instead of the normal `:wrap`. `:gen` expects a function of `route-meta router-opts => wrap`. Instead of returning the actual middleware function, the middleware record can also decide no to mount itsef byt returning `nil`. Why mount `wrap-enforce-roles` for a route if there are no roles required for it?
|
To do this we use [middleware records](#middleware-records) `:gen` hook instead of the normal `:wrap`. `:gen` expects a function of `route-meta router-opts => wrap`. Middleware can also return `nil`, which effective unmounts the middleware. Why mount a `wrap-enforce-roles` middleware for a route if there are no roles required for it?
|
||||||
|
|
||||||
To demonstrate the two approaches, below are response coercion middleware written as normal ring middleware function and as middleware record with `:gen`. The actual codes are from `reitit.coercion`:
|
To demonstrate the two approaches, below are response coercion middleware written as normal ring middleware function and as middleware record with `:gen`. These are the actual codes are from [`reitit.coercion`](https://github.com/metosin/reitit/blob/master/src/reitit/coercion.cljc):
|
||||||
|
|
||||||
### Naive
|
### Naive
|
||||||
|
|
||||||
|
|
@ -552,7 +550,7 @@ To demonstrate the two approaches, below are response coercion middleware writte
|
||||||
(defn wrap-coerce-response
|
(defn wrap-coerce-response
|
||||||
"Pluggable response coercion middleware.
|
"Pluggable response coercion middleware.
|
||||||
Expects a :coercion of type `reitit.coercion.protocol/Coercion`
|
Expects a :coercion of type `reitit.coercion.protocol/Coercion`
|
||||||
and :responeses from route meta, otherwise does not mount."
|
and :responses from route meta, otherwise does not mount."
|
||||||
[handler]
|
[handler]
|
||||||
(fn
|
(fn
|
||||||
([request]
|
([request]
|
||||||
|
|
@ -605,7 +603,7 @@ To demonstrate the two approaches, below are response coercion middleware writte
|
||||||
(handler request #(respond (coerce-response coercers request %)) raise)))))))}))
|
(handler request #(respond (coerce-response coercers request %)) raise)))))))}))
|
||||||
```
|
```
|
||||||
|
|
||||||
The `:gen` -version is both much easier to understand but also 2-4x faster on basic perf tests.
|
The `:gen` -version has 50% less code, is easier to reason about and is 2-4x faster on basic perf tests.
|
||||||
|
|
||||||
## Merging route-trees
|
## Merging route-trees
|
||||||
|
|
||||||
|
|
@ -644,6 +642,10 @@ To all Clojure(Script) routing libs out there, expecially to
|
||||||
[Ataraxy](https://github.com/weavejester/ataraxy), [Bide](https://github.com/funcool/bide), [Bidi](https://github.com/juxt/bidi), [Compojure](https://github.com/weavejester/compojure) and
|
[Ataraxy](https://github.com/weavejester/ataraxy), [Bide](https://github.com/funcool/bide), [Bidi](https://github.com/juxt/bidi), [Compojure](https://github.com/weavejester/compojure) and
|
||||||
[Pedestal](https://github.com/pedestal/pedestal/tree/master/route).
|
[Pedestal](https://github.com/pedestal/pedestal/tree/master/route).
|
||||||
|
|
||||||
|
Also to [Compojure-api](https://github.com/metosin/compojure-api), [Kekkonen](https://github.com/metosin/kekkonen) and [Ring-swagger](https://github.com/metosin/ring-swagger) and for the data-driven syntax, coercion & stuff.
|
||||||
|
|
||||||
|
And some [Yada](https://github.com/juxt/yada) too.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
Copyright © 2017 [Metosin Oy](http://www.metosin.fi)
|
Copyright © 2017 [Metosin Oy](http://www.metosin.fi)
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@
|
||||||
(s/def ::y (s/and (s/conformer #(if (string? %) (Long/parseLong %) %) identity) int?))
|
(s/def ::y (s/and (s/conformer #(if (string? %) (Long/parseLong %) %) identity) int?))
|
||||||
(s/def ::k (s/keys :req-un [::x ::y]))
|
(s/def ::k (s/keys :req-un [::x ::y]))
|
||||||
|
|
||||||
(let [spec (spec/specify {:x int?, :y int?} ::jeah)
|
(let [spec (spec/into-spec {:x int?, :y int?} ::jeah)
|
||||||
coercers (#'coercion/request-coercers spec/coercion {:body spec})
|
coercers (#'coercion/request-coercers spec/coercion {:body spec})
|
||||||
params {:x "1", :y "2"}
|
params {:x "1", :y "2"}
|
||||||
request {:body-params {:x "1", :y "2"}}]
|
request {:body-params {:x "1", :y "2"}}]
|
||||||
|
|
|
||||||
|
|
@ -115,7 +115,7 @@
|
||||||
match (ring/get-match request)
|
match (ring/get-match request)
|
||||||
parameters (-> match :result method :meta :parameters)
|
parameters (-> match :result method :meta :parameters)
|
||||||
coercion (-> match :meta :coercion)]
|
coercion (-> match :meta :coercion)]
|
||||||
(if coercion
|
(if (and coercion parameters)
|
||||||
(let [coercers (request-coercers coercion parameters)
|
(let [coercers (request-coercers coercion parameters)
|
||||||
coerced (coerce-parameters coercers request)]
|
coerced (coerce-parameters coercers request)]
|
||||||
(handler (impl/fast-assoc request :parameters coerced)))
|
(handler (impl/fast-assoc request :parameters coerced)))
|
||||||
|
|
@ -125,7 +125,7 @@
|
||||||
match (ring/get-match request)
|
match (ring/get-match request)
|
||||||
parameters (-> match :result method :meta :parameters)
|
parameters (-> match :result method :meta :parameters)
|
||||||
coercion (-> match :meta :coercion)]
|
coercion (-> match :meta :coercion)]
|
||||||
(if coercion
|
(if (and coercion parameters)
|
||||||
(let [coercers (request-coercers coercion parameters)
|
(let [coercers (request-coercers coercion parameters)
|
||||||
coerced (coerce-parameters coercers request)]
|
coerced (coerce-parameters coercers request)]
|
||||||
(handler (impl/fast-assoc request :parameters coerced) respond raise)))))))
|
(handler (impl/fast-assoc request :parameters coerced) respond raise)))))))
|
||||||
|
|
@ -161,7 +161,7 @@
|
||||||
responses (-> match :result method :meta :responses)
|
responses (-> match :result method :meta :responses)
|
||||||
coercion (-> match :meta :coercion)
|
coercion (-> match :meta :coercion)
|
||||||
opts (-> match :meta :opts)]
|
opts (-> match :meta :opts)]
|
||||||
(if coercion
|
(if (and coercion responses)
|
||||||
(let [coercers (response-coercers coercion responses opts)
|
(let [coercers (response-coercers coercion responses opts)
|
||||||
coerced (coerce-response coercers request response)]
|
coerced (coerce-response coercers request response)]
|
||||||
(coerce-response coercers request (handler request)))
|
(coerce-response coercers request (handler request)))
|
||||||
|
|
@ -173,7 +173,7 @@
|
||||||
responses (-> match :result method :meta :responses)
|
responses (-> match :result method :meta :responses)
|
||||||
coercion (-> match :meta :coercion)
|
coercion (-> match :meta :coercion)
|
||||||
opts (-> match :meta :opts)]
|
opts (-> match :meta :opts)]
|
||||||
(if coercion
|
(if (and coercion responses)
|
||||||
(let [coercers (response-coercers coercion responses opts)
|
(let [coercers (response-coercers coercion responses opts)
|
||||||
coerced (coerce-response coercers request response)]
|
coerced (coerce-response coercers request response)]
|
||||||
(handler request #(respond (coerce-response coercers request %))))
|
(handler request #(respond (coerce-response coercers request %))))
|
||||||
|
|
|
||||||
|
|
@ -23,31 +23,32 @@
|
||||||
(def default-conforming
|
(def default-conforming
|
||||||
::default)
|
::default)
|
||||||
|
|
||||||
(defprotocol Specify
|
(defprotocol IntoSpec
|
||||||
(specify [this name]))
|
(into-spec [this name]))
|
||||||
|
|
||||||
(extend-protocol Specify
|
(extend-protocol IntoSpec
|
||||||
|
|
||||||
#?(:clj clojure.lang.PersistentArrayMap
|
#?(:clj clojure.lang.PersistentArrayMap
|
||||||
:cljs cljs.core.PersistentArrayMap)
|
:cljs cljs.core.PersistentArrayMap)
|
||||||
(specify [this name]
|
(into-spec [this name]
|
||||||
(ds/spec name this))
|
(ds/spec name this))
|
||||||
|
|
||||||
#?(:clj clojure.lang.PersistentHashMap
|
#?(:clj clojure.lang.PersistentHashMap
|
||||||
:cljs cljs.core.PersistentHashMap)
|
:cljs cljs.core.PersistentHashMap)
|
||||||
(specify [this name]
|
(into-spec [this name]
|
||||||
(ds/spec name this))
|
(ds/spec name this))
|
||||||
|
|
||||||
Spec
|
Spec
|
||||||
(specify [this _] this)
|
(into-spec [this _] this)
|
||||||
|
|
||||||
Object
|
#?(:clj Object
|
||||||
(specify [this _]
|
:cljs default)
|
||||||
|
(into-spec [this _]
|
||||||
(st/create-spec {:spec this})))
|
(st/create-spec {:spec this})))
|
||||||
|
|
||||||
;; TODO: proper name!
|
;; TODO: proper name!
|
||||||
(def memoized-specify
|
(def memoized-into-spec
|
||||||
(memoize #(specify %1 (gensym "spec"))))
|
(memoize #(into-spec %1 (gensym "spec"))))
|
||||||
|
|
||||||
(defmulti coerce-response? identity :default ::default)
|
(defmulti coerce-response? identity :default ::default)
|
||||||
(defmethod coerce-response? ::default [_] true)
|
(defmethod coerce-response? ::default [_] true)
|
||||||
|
|
@ -58,7 +59,7 @@
|
||||||
(get-name [_] name)
|
(get-name [_] name)
|
||||||
|
|
||||||
(compile [_ model _]
|
(compile [_ model _]
|
||||||
(memoized-specify model))
|
(memoized-into-spec model))
|
||||||
|
|
||||||
(get-apidocs [_ _ {:keys [parameters responses] :as info}]
|
(get-apidocs [_ _ {:keys [parameters responses] :as info}]
|
||||||
(cond-> (dissoc info :parameters :responses)
|
(cond-> (dissoc info :parameters :responses)
|
||||||
|
|
@ -67,13 +68,13 @@
|
||||||
(into
|
(into
|
||||||
(empty parameters)
|
(empty parameters)
|
||||||
(for [[k v] parameters]
|
(for [[k v] parameters]
|
||||||
[k memoized-specify])))
|
[k memoized-into-spec])))
|
||||||
responses (assoc
|
responses (assoc
|
||||||
::swagger/responses
|
::swagger/responses
|
||||||
(into
|
(into
|
||||||
(empty responses)
|
(empty responses)
|
||||||
(for [[k response] responses]
|
(for [[k response] responses]
|
||||||
[k (update response :schema memoized-specify)])))))
|
[k (update response :schema memoized-into-spec)])))))
|
||||||
|
|
||||||
(make-open [_ spec] spec)
|
(make-open [_ spec] spec)
|
||||||
|
|
||||||
|
|
@ -81,7 +82,7 @@
|
||||||
(update error :spec (comp str s/form)))
|
(update error :spec (comp str s/form)))
|
||||||
|
|
||||||
(request-coercer [_ type spec]
|
(request-coercer [_ type spec]
|
||||||
(let [spec (memoized-specify spec)
|
(let [spec (memoized-into-spec spec)
|
||||||
{:keys [formats default]} (conforming type)]
|
{:keys [formats default]} (conforming type)]
|
||||||
(fn [value format]
|
(fn [value format]
|
||||||
(if-let [conforming (or (get formats format) default)]
|
(if-let [conforming (or (get formats format) default)]
|
||||||
|
|
@ -101,9 +102,7 @@
|
||||||
(def default-options
|
(def default-options
|
||||||
{:coerce-response? coerce-response?
|
{:coerce-response? coerce-response?
|
||||||
:conforming {:body {:default default-conforming
|
:conforming {:body {:default default-conforming
|
||||||
:formats {"application/json" json-conforming
|
:formats {"application/json" json-conforming}}
|
||||||
"application/msgpack" json-conforming
|
|
||||||
"application/x-yaml" json-conforming}}
|
|
||||||
:string {:default string-conforming}
|
:string {:default string-conforming}
|
||||||
:response {:default default-conforming}}})
|
:response {:default default-conforming}}})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -89,11 +89,14 @@
|
||||||
(defn find-names [routes opts]
|
(defn find-names [routes opts]
|
||||||
(into [] (keep #(-> % second :name)) routes))
|
(into [] (keep #(-> % second :name)) routes))
|
||||||
|
|
||||||
(defn compile-route [[p m :as route] {:keys [compile] :as opts}]
|
(defn- compile-route [[p m :as route] {:keys [compile] :as opts}]
|
||||||
[p m (if compile (compile route opts))])
|
[p m (if compile (compile route opts))])
|
||||||
|
|
||||||
|
(defn- compile-routes [routes opts]
|
||||||
|
(into [] (keep #(compile-route % opts) routes)))
|
||||||
|
|
||||||
(defprotocol Router
|
(defprotocol Router
|
||||||
(router-type [this])
|
(router-name [this])
|
||||||
(routes [this])
|
(routes [this])
|
||||||
(options [this])
|
(options [this])
|
||||||
(route-names [this])
|
(route-names [this])
|
||||||
|
|
@ -132,7 +135,7 @@
|
||||||
([routes]
|
([routes]
|
||||||
(linear-router routes {}))
|
(linear-router routes {}))
|
||||||
([routes opts]
|
([routes opts]
|
||||||
(let [compiled (map #(compile-route % opts) routes)
|
(let [compiled (compile-routes routes opts)
|
||||||
names (find-names routes opts)
|
names (find-names routes opts)
|
||||||
[data lookup] (reduce
|
[data lookup] (reduce
|
||||||
(fn [[data lookup] [p {:keys [name] :as meta} result]]
|
(fn [[data lookup] [p {:keys [name] :as meta} result]]
|
||||||
|
|
@ -146,10 +149,10 @@
|
||||||
lookup (impl/fast-map lookup)]
|
lookup (impl/fast-map lookup)]
|
||||||
(reify
|
(reify
|
||||||
Router
|
Router
|
||||||
(router-type [_]
|
(router-name [_]
|
||||||
:linear-router)
|
:linear-router)
|
||||||
(routes [_]
|
(routes [_]
|
||||||
routes)
|
compiled)
|
||||||
(options [_]
|
(options [_]
|
||||||
opts)
|
opts)
|
||||||
(route-names [_]
|
(route-names [_]
|
||||||
|
|
@ -179,7 +182,7 @@
|
||||||
(str "can't create LookupRouter with wildcard routes: " wilds)
|
(str "can't create LookupRouter with wildcard routes: " wilds)
|
||||||
{:wilds wilds
|
{:wilds wilds
|
||||||
:routes routes})))
|
:routes routes})))
|
||||||
(let [compiled (map #(compile-route % opts) routes)
|
(let [compiled (compile-routes routes opts)
|
||||||
names (find-names routes opts)
|
names (find-names routes opts)
|
||||||
[data lookup] (reduce
|
[data lookup] (reduce
|
||||||
(fn [[data lookup] [p {:keys [name] :as meta} result]]
|
(fn [[data lookup] [p {:keys [name] :as meta} result]]
|
||||||
|
|
@ -190,10 +193,10 @@
|
||||||
data (impl/fast-map data)
|
data (impl/fast-map data)
|
||||||
lookup (impl/fast-map lookup)]
|
lookup (impl/fast-map lookup)]
|
||||||
(reify Router
|
(reify Router
|
||||||
(router-type [_]
|
(router-name [_]
|
||||||
:lookup-router)
|
:lookup-router)
|
||||||
(routes [_]
|
(routes [_]
|
||||||
routes)
|
compiled)
|
||||||
(options [_]
|
(options [_]
|
||||||
opts)
|
opts)
|
||||||
(route-names [_]
|
(route-names [_]
|
||||||
|
|
@ -220,7 +223,7 @@
|
||||||
lookup-router (lookup-router lookup opts)
|
lookup-router (lookup-router lookup opts)
|
||||||
names (find-names routes opts)]
|
names (find-names routes opts)]
|
||||||
(reify Router
|
(reify Router
|
||||||
(router-type [_]
|
(router-name [_]
|
||||||
:mixed-router)
|
:mixed-router)
|
||||||
(routes [_]
|
(routes [_]
|
||||||
routes)
|
routes)
|
||||||
|
|
|
||||||
|
|
@ -2,56 +2,55 @@
|
||||||
(:require [meta-merge.core :refer [meta-merge]]
|
(:require [meta-merge.core :refer [meta-merge]]
|
||||||
[reitit.core :as reitit]))
|
[reitit.core :as reitit]))
|
||||||
|
|
||||||
(defprotocol ExpandMiddleware
|
(defprotocol IntoMiddleware
|
||||||
(expand-middleware [this meta opts]))
|
(into-middleware [this meta opts]))
|
||||||
|
|
||||||
(defrecord Middleware [name wrap create])
|
(defrecord Middleware [name wrap])
|
||||||
|
(defrecord Endpoint [meta handler middleware])
|
||||||
|
|
||||||
(defn create [{:keys [name gen wrap] :as m}]
|
(defn create [{:keys [name gen wrap] :as m}]
|
||||||
(when-not name
|
|
||||||
(throw
|
|
||||||
(ex-info
|
|
||||||
(str "Middleware must have :name defined " m) m)))
|
|
||||||
(when (and gen wrap)
|
(when (and gen wrap)
|
||||||
(throw
|
(throw
|
||||||
(ex-info
|
(ex-info
|
||||||
(str "Middleware can't both :wrap and :gen defined " m) m)))
|
(str "Middleware can't both :wrap and :gen defined " m) m)))
|
||||||
(map->Middleware m))
|
(map->Middleware m))
|
||||||
|
|
||||||
(extend-protocol ExpandMiddleware
|
(extend-protocol IntoMiddleware
|
||||||
|
|
||||||
#?(:clj clojure.lang.APersistentVector
|
#?(:clj clojure.lang.APersistentVector
|
||||||
:cljs cljs.core.PersistentVector)
|
:cljs cljs.core.PersistentVector)
|
||||||
(expand-middleware [[f & args] meta opts]
|
(into-middleware [[f & args] meta opts]
|
||||||
(if-let [mw (expand-middleware f meta opts)]
|
(if-let [{:keys [wrap] :as mw} (into-middleware f meta opts)]
|
||||||
(fn [handler]
|
(assoc mw :wrap #(apply wrap % args))))
|
||||||
(apply mw handler args))))
|
|
||||||
|
|
||||||
#?(:clj clojure.lang.Fn
|
#?(:clj clojure.lang.Fn
|
||||||
:cljs function)
|
:cljs function)
|
||||||
(expand-middleware [this _ _] this)
|
(into-middleware [this _ _]
|
||||||
|
(map->Middleware
|
||||||
|
{:wrap this}))
|
||||||
|
|
||||||
#?(:clj clojure.lang.PersistentArrayMap
|
#?(:clj clojure.lang.PersistentArrayMap
|
||||||
:cljs cljs.core.PersistentArrayMap)
|
:cljs cljs.core.PersistentArrayMap)
|
||||||
(expand-middleware [this meta opts]
|
(into-middleware [this meta opts]
|
||||||
(expand-middleware (create this) meta opts))
|
(into-middleware (create this) meta opts))
|
||||||
|
|
||||||
#?(:clj clojure.lang.PersistentHashMap
|
#?(:clj clojure.lang.PersistentHashMap
|
||||||
:cljs cljs.core.PersistentHashMap)
|
:cljs cljs.core.PersistentHashMap)
|
||||||
(expand-middleware [this meta opts]
|
(into-middleware [this meta opts]
|
||||||
(expand-middleware (create this) meta opts))
|
(into-middleware (create this) meta opts))
|
||||||
|
|
||||||
Middleware
|
Middleware
|
||||||
(expand-middleware [{:keys [wrap gen]} meta opts]
|
(into-middleware [{:keys [wrap gen] :as this} meta opts]
|
||||||
(if gen
|
(if-not gen
|
||||||
|
this
|
||||||
(if-let [wrap (gen meta opts)]
|
(if-let [wrap (gen meta opts)]
|
||||||
(fn [handler & args]
|
(map->Middleware
|
||||||
(apply wrap handler args)))
|
(-> this
|
||||||
(fn [handler & args]
|
(dissoc :gen)
|
||||||
(apply wrap handler args))))
|
(assoc :wrap wrap))))))
|
||||||
|
|
||||||
nil
|
nil
|
||||||
(expand-middleware [_ _ _]))
|
(into-middleware [_ _ _]))
|
||||||
|
|
||||||
(defn- ensure-handler! [path meta scope]
|
(defn- ensure-handler! [path meta scope]
|
||||||
(when-not (:handler meta)
|
(when-not (:handler meta)
|
||||||
|
|
@ -61,23 +60,44 @@
|
||||||
(merge {:path path, :meta meta}
|
(merge {:path path, :meta meta}
|
||||||
(if scope {:scope scope}))))))
|
(if scope {:scope scope}))))))
|
||||||
|
|
||||||
(defn compose-middleware [middleware meta opts]
|
(defn expand [middleware meta opts]
|
||||||
(->> middleware
|
(->> middleware
|
||||||
(keep identity)
|
(keep #(into-middleware % meta opts))
|
||||||
(map #(expand-middleware % meta opts))
|
(into [])))
|
||||||
(keep identity)
|
|
||||||
(apply comp identity)))
|
|
||||||
|
|
||||||
(defn compile-handler
|
(defn compile-handler [middleware handler]
|
||||||
|
((apply comp identity (keep :wrap middleware)) handler))
|
||||||
|
|
||||||
|
(compile-handler
|
||||||
|
[(map->Middleware
|
||||||
|
{:wrap
|
||||||
|
(fn [handler]
|
||||||
|
(fn [request]
|
||||||
|
(handler request)))})] identity)
|
||||||
|
|
||||||
|
(defn compile-result
|
||||||
([route opts]
|
([route opts]
|
||||||
(compile-handler route opts nil))
|
(compile-result route opts nil))
|
||||||
([[path {:keys [middleware handler] :as meta}] opts scope]
|
([[path {:keys [middleware handler] :as meta}] opts scope]
|
||||||
(ensure-handler! path meta scope)
|
(ensure-handler! path meta scope)
|
||||||
((compose-middleware middleware meta opts) handler)))
|
(let [middleware (expand middleware meta opts)]
|
||||||
|
(map->Endpoint
|
||||||
|
{:handler (compile-handler middleware handler)
|
||||||
|
:middleware middleware
|
||||||
|
:meta meta}))))
|
||||||
|
|
||||||
(defn router
|
(defn router
|
||||||
([data]
|
([data]
|
||||||
(router data nil))
|
(router data nil))
|
||||||
([data opts]
|
([data opts]
|
||||||
(let [opts (meta-merge {:compile compile-handler} opts)]
|
(let [opts (meta-merge {:compile compile-result} opts)]
|
||||||
(reitit/router data opts))))
|
(reitit/router data opts))))
|
||||||
|
|
||||||
|
(defn middleware-handler [router]
|
||||||
|
(with-meta
|
||||||
|
(fn [path]
|
||||||
|
(some->> path
|
||||||
|
(reitit/match-by-path router)
|
||||||
|
:result
|
||||||
|
:handler))
|
||||||
|
{::router router}))
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@
|
||||||
|
|
||||||
(def http-methods #{:get :head :patch :delete :options :post :put})
|
(def http-methods #{:get :head :patch :delete :options :post :put})
|
||||||
(defrecord Methods [get head post put delete trace options connect patch any])
|
(defrecord Methods [get head post put delete trace options connect patch any])
|
||||||
(defrecord Endpoint [meta handler])
|
|
||||||
|
|
||||||
(defn- group-keys [meta]
|
(defn- group-keys [meta]
|
||||||
(reduce-kv
|
(reduce-kv
|
||||||
|
|
@ -28,7 +27,7 @@
|
||||||
(if handler
|
(if handler
|
||||||
(handler
|
(handler
|
||||||
(cond-> (impl/fast-assoc request ::match match)
|
(cond-> (impl/fast-assoc request ::match match)
|
||||||
params (impl/fast-assoc :path-params params)))))))
|
(seq params) (impl/fast-assoc :path-params params)))))))
|
||||||
([request respond raise]
|
([request respond raise]
|
||||||
(if-let [match (reitit/match-by-path router (:uri request))]
|
(if-let [match (reitit/match-by-path router (:uri request))]
|
||||||
(let [method (:request-method request :any)
|
(let [method (:request-method request :any)
|
||||||
|
|
@ -39,7 +38,7 @@
|
||||||
(if handler
|
(if handler
|
||||||
(handler
|
(handler
|
||||||
(cond-> (impl/fast-assoc request ::match match)
|
(cond-> (impl/fast-assoc request ::match match)
|
||||||
params (impl/fast-assoc :path-params params))
|
(seq params) (impl/fast-assoc :path-params params))
|
||||||
respond raise))))))
|
respond raise))))))
|
||||||
{::router router}))
|
{::router router}))
|
||||||
|
|
||||||
|
|
@ -56,30 +55,22 @@
|
||||||
(update acc method expand opts)
|
(update acc method expand opts)
|
||||||
acc)) meta http-methods)])
|
acc)) meta http-methods)])
|
||||||
|
|
||||||
(defn compile-handler [[path meta] opts]
|
(defn compile-result [[path meta] opts]
|
||||||
(let [[top childs] (group-keys meta)]
|
(let [[top childs] (group-keys meta)]
|
||||||
(if-not (seq childs)
|
(if-not (seq childs)
|
||||||
(map->Methods
|
(let [middleware (middleware/compile-result [path top] opts)]
|
||||||
{:any (map->Endpoint
|
(map->Methods {:any (middleware/compile-result [path top] opts)}))
|
||||||
{:handler (middleware/compile-handler [path top] opts)
|
(let [any-handler (if (:handler top) (middleware/compile-result [path meta] opts))]
|
||||||
:meta top})})
|
|
||||||
(let [any-handler (if (:handler top) (middleware/compile-handler [path meta] opts))]
|
|
||||||
(reduce-kv
|
(reduce-kv
|
||||||
(fn [acc method meta]
|
(fn [acc method meta]
|
||||||
(let [meta (meta-merge top meta)
|
(let [meta (meta-merge top meta)]
|
||||||
handler (middleware/compile-handler [path meta] opts method)]
|
(assoc acc method (middleware/compile-result [path meta] opts method))))
|
||||||
(assoc acc method (map->Endpoint
|
(map->Methods {:any any-handler})
|
||||||
{:handler handler
|
|
||||||
:meta meta}))))
|
|
||||||
(map->Methods
|
|
||||||
{:any (map->Endpoint
|
|
||||||
{:handler (if (:handler top) (middleware/compile-handler [path meta] opts))
|
|
||||||
:meta top})})
|
|
||||||
childs)))))
|
childs)))))
|
||||||
|
|
||||||
(defn router
|
(defn router
|
||||||
([data]
|
([data]
|
||||||
(router data nil))
|
(router data nil))
|
||||||
([data opts]
|
([data opts]
|
||||||
(let [opts (meta-merge {:coerce coerce-handler, :compile compile-handler} opts)]
|
(let [opts (meta-merge {:coerce coerce-handler, :compile compile-result} opts)]
|
||||||
(reitit/router data opts))))
|
(reitit/router data opts))))
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,8 @@
|
||||||
|
|
||||||
(s/def ::route
|
(s/def ::route
|
||||||
(s/cat :path ::path
|
(s/cat :path ::path
|
||||||
:meta ::meta))
|
:meta ::meta
|
||||||
|
:result (s/? any?)))
|
||||||
|
|
||||||
(s/def ::routes
|
(s/def ::routes
|
||||||
(s/or :route ::route
|
(s/or :route ::route
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,8 @@
|
||||||
|
|
||||||
(testing "linear-router"
|
(testing "linear-router"
|
||||||
(let [router (reitit/router ["/api" ["/ipa" ["/:size" ::beer]]])]
|
(let [router (reitit/router ["/api" ["/ipa" ["/:size" ::beer]]])]
|
||||||
(is (= :linear-router (reitit/router-type router)))
|
(is (= :linear-router (reitit/router-name router)))
|
||||||
(is (= [["/api/ipa/:size" {:name ::beer}]]
|
(is (= [["/api/ipa/:size" {:name ::beer} nil]]
|
||||||
(reitit/routes router)))
|
(reitit/routes router)))
|
||||||
(is (= true (map? (reitit/options router))))
|
(is (= true (map? (reitit/options router))))
|
||||||
(is (= (reitit/map->Match
|
(is (= (reitit/map->Match
|
||||||
|
|
@ -27,6 +27,7 @@
|
||||||
(reitit/match-by-name router ::beer {:size "large"})))
|
(reitit/match-by-name router ::beer {:size "large"})))
|
||||||
(is (= nil (reitit/match-by-name router "ILLEGAL")))
|
(is (= nil (reitit/match-by-name router "ILLEGAL")))
|
||||||
(is (= [::beer] (reitit/route-names router)))
|
(is (= [::beer] (reitit/route-names router)))
|
||||||
|
|
||||||
(testing "name-based routing with missing parameters"
|
(testing "name-based routing with missing parameters"
|
||||||
(is (= (reitit/map->PartialMatch
|
(is (= (reitit/map->PartialMatch
|
||||||
{:template "/api/ipa/:size"
|
{:template "/api/ipa/:size"
|
||||||
|
|
@ -42,8 +43,8 @@
|
||||||
|
|
||||||
(testing "lookup-router"
|
(testing "lookup-router"
|
||||||
(let [router (reitit/router ["/api" ["/ipa" ["/large" ::beer]]])]
|
(let [router (reitit/router ["/api" ["/ipa" ["/large" ::beer]]])]
|
||||||
(is (= :lookup-router (reitit/router-type router)))
|
(is (= :lookup-router (reitit/router-name router)))
|
||||||
(is (= [["/api/ipa/large" {:name ::beer}]]
|
(is (= [["/api/ipa/large" {:name ::beer} nil]]
|
||||||
(reitit/routes router)))
|
(reitit/routes router)))
|
||||||
(is (= true (map? (reitit/options router))))
|
(is (= true (map? (reitit/options router))))
|
||||||
(is (= (reitit/map->Match
|
(is (= (reitit/map->Match
|
||||||
|
|
@ -60,6 +61,7 @@
|
||||||
(reitit/match-by-name router ::beer {:size "large"})))
|
(reitit/match-by-name router ::beer {:size "large"})))
|
||||||
(is (= nil (reitit/match-by-name router "ILLEGAL")))
|
(is (= nil (reitit/match-by-name router "ILLEGAL")))
|
||||||
(is (= [::beer] (reitit/route-names router)))
|
(is (= [::beer] (reitit/route-names router)))
|
||||||
|
|
||||||
(testing "can't be created with wildcard routes"
|
(testing "can't be created with wildcard routes"
|
||||||
(is (thrown-with-msg?
|
(is (thrown-with-msg?
|
||||||
ExceptionInfo
|
ExceptionInfo
|
||||||
|
|
@ -69,6 +71,7 @@
|
||||||
["/api/:version/ping"] {})))))))
|
["/api/:version/ping"] {})))))))
|
||||||
|
|
||||||
(testing "route coercion & compilation"
|
(testing "route coercion & compilation"
|
||||||
|
|
||||||
(testing "custom compile"
|
(testing "custom compile"
|
||||||
(let [compile-times (atom 0)
|
(let [compile-times (atom 0)
|
||||||
coerce (fn [[path meta] _]
|
coerce (fn [[path meta] _]
|
||||||
|
|
@ -86,6 +89,7 @@
|
||||||
["/crap"]]]
|
["/crap"]]]
|
||||||
{:coerce coerce
|
{:coerce coerce
|
||||||
:compile compile})]
|
:compile compile})]
|
||||||
|
|
||||||
(testing "routes are coerced"
|
(testing "routes are coerced"
|
||||||
(is (= [["/api/ping" {:name ::ping
|
(is (= [["/api/ping" {:name ::ping
|
||||||
:path "/api/ping",
|
:path "/api/ping",
|
||||||
|
|
@ -93,13 +97,15 @@
|
||||||
["/api/pong" {:name ::pong
|
["/api/pong" {:name ::pong
|
||||||
:path "/api/pong",
|
:path "/api/pong",
|
||||||
:roles #{:admin}}]]
|
:roles #{:admin}}]]
|
||||||
(reitit/routes router))))
|
(map butlast (reitit/routes router)))))
|
||||||
|
|
||||||
(testing "route match contains compiled handler"
|
(testing "route match contains compiled handler"
|
||||||
(is (= 2 @compile-times))
|
(is (= 2 @compile-times))
|
||||||
(let [{:keys [result]} (reitit/match-by-path router "/api/pong")]
|
(let [{:keys [result]} (reitit/match-by-path router "/api/pong")]
|
||||||
(is result)
|
(is result)
|
||||||
(is (= "/api/pong" (result)))
|
(is (= "/api/pong" (result)))
|
||||||
(is (= 2 @compile-times))))))
|
(is (= 2 @compile-times))))))
|
||||||
|
|
||||||
(testing "default compile"
|
(testing "default compile"
|
||||||
(let [router (reitit/router ["/ping" (constantly "ok")])]
|
(let [router (reitit/router ["/ping" (constantly "ok")])]
|
||||||
(let [{:keys [result]} (reitit/match-by-path router "/ping")]
|
(let [{:keys [result]} (reitit/match-by-path router "/ping")]
|
||||||
|
|
@ -109,9 +115,9 @@
|
||||||
(testing "custom router"
|
(testing "custom router"
|
||||||
(let [router (reitit/router ["/ping"] {:router (fn [_ _]
|
(let [router (reitit/router ["/ping"] {:router (fn [_ _]
|
||||||
(reify Router
|
(reify Router
|
||||||
(reitit/router-type [_]
|
(reitit/router-name [_]
|
||||||
::custom)))})]
|
::custom)))})]
|
||||||
(is (= ::custom (reitit/router-type router)))))
|
(is (= ::custom (reitit/router-name router)))))
|
||||||
|
|
||||||
(testing "bide sample"
|
(testing "bide sample"
|
||||||
(let [routes [["/auth/login" :auth/login]
|
(let [routes [["/auth/login" :auth/login]
|
||||||
|
|
|
||||||
|
|
@ -6,37 +6,10 @@
|
||||||
#?(:clj
|
#?(:clj
|
||||||
(:import (clojure.lang ExceptionInfo))))
|
(:import (clojure.lang ExceptionInfo))))
|
||||||
|
|
||||||
(defn mw [handler name]
|
|
||||||
(fn
|
|
||||||
([request]
|
|
||||||
(-> request
|
|
||||||
(update ::mw (fnil conj []) name)
|
|
||||||
(handler)
|
|
||||||
(update :body (fnil conj []) name)))
|
|
||||||
([request respond raise]
|
|
||||||
(handler
|
|
||||||
(update request ::mw (fnil conj []) name)
|
|
||||||
#(respond (update % :body (fnil conj []) name))
|
|
||||||
raise))))
|
|
||||||
|
|
||||||
(defn handler
|
|
||||||
([{:keys [::mw]}]
|
|
||||||
{:status 200 :body (conj mw :ok)})
|
|
||||||
([request respond raise]
|
|
||||||
(respond (handler request))))
|
|
||||||
|
|
||||||
(deftest expand-middleware-test
|
(deftest expand-middleware-test
|
||||||
|
|
||||||
(testing "middleware records"
|
(testing "middleware records"
|
||||||
|
|
||||||
(testing ":name is mandatory"
|
|
||||||
(is (thrown-with-msg?
|
|
||||||
ExceptionInfo
|
|
||||||
#"Middleware must have :name defined"
|
|
||||||
(middleware/create
|
|
||||||
{:wrap identity
|
|
||||||
:gen (constantly identity)}))))
|
|
||||||
|
|
||||||
(testing ":wrap & :gen are exclusive"
|
(testing ":wrap & :gen are exclusive"
|
||||||
(is (thrown-with-msg?
|
(is (thrown-with-msg?
|
||||||
ExceptionInfo
|
ExceptionInfo
|
||||||
|
|
@ -46,78 +19,93 @@
|
||||||
:wrap identity
|
:wrap identity
|
||||||
:gen (constantly identity)}))))
|
:gen (constantly identity)}))))
|
||||||
|
|
||||||
(testing ":wrap"
|
(testing "middleware"
|
||||||
(let [calls (atom 0)
|
(let [calls (atom 0)
|
||||||
data {:name ::test
|
wrap (fn [handler value]
|
||||||
:wrap (fn [handler value]
|
|
||||||
(swap! calls inc)
|
(swap! calls inc)
|
||||||
(fn [request]
|
(fn [request]
|
||||||
[value request]))}]
|
[value request]))
|
||||||
|
->app (fn [ast handler]
|
||||||
|
(middleware/compile-handler
|
||||||
|
(middleware/expand ast :meta {})
|
||||||
|
handler))]
|
||||||
|
|
||||||
|
(testing "as middleware function"
|
||||||
|
(reset! calls 0)
|
||||||
|
(let [app (->app [[#(wrap % :value)]] identity)]
|
||||||
|
(dotimes [_ 10]
|
||||||
|
(is (= [:value :request] (app :request)))
|
||||||
|
(is (= 1 @calls)))))
|
||||||
|
|
||||||
|
(testing "as middleware vector"
|
||||||
|
(reset! calls 0)
|
||||||
|
(let [app (->app [[wrap :value]] identity)]
|
||||||
|
(dotimes [_ 10]
|
||||||
|
(is (= [:value :request] (app :request)))
|
||||||
|
(is (= 1 @calls)))))
|
||||||
|
|
||||||
(testing "as map"
|
(testing "as map"
|
||||||
(reset! calls 0)
|
(reset! calls 0)
|
||||||
(let [app ((middleware/compose-middleware [data] :meta {}) identity :value)]
|
(let [app (->app [[{:wrap #(wrap % :value)}]] identity)]
|
||||||
(dotimes [_ 10]
|
(dotimes [_ 10]
|
||||||
(is (= [:value :request] (app :request)))
|
(is (= [:value :request] (app :request)))
|
||||||
(is (= 1 @calls)))))
|
(is (= 1 @calls)))))
|
||||||
|
|
||||||
(testing "direct"
|
(testing "as map vector"
|
||||||
(reset! calls 0)
|
(reset! calls 0)
|
||||||
(let [app ((middleware/compose-middleware [(middleware/create data)] :meta {}) identity :value)]
|
(let [app (->app [[{:wrap wrap} :value]] identity)]
|
||||||
(dotimes [_ 10]
|
(dotimes [_ 10]
|
||||||
(is (= [:value :request] (app :request)))
|
(is (= [:value :request] (app :request)))
|
||||||
(is (= 1 @calls)))))
|
(is (= 1 @calls)))))
|
||||||
|
|
||||||
(testing "vector"
|
(testing "as Middleware"
|
||||||
(reset! calls 0)
|
(reset! calls 0)
|
||||||
(let [app ((middleware/compose-middleware [[(middleware/create data) :value]] :meta {}) identity)]
|
(let [app (->app [[(middleware/create {:wrap #(wrap % :value)})]] identity)]
|
||||||
|
(dotimes [_ 10]
|
||||||
|
(is (= [:value :request] (app :request)))
|
||||||
|
(is (= 1 @calls)))))
|
||||||
|
|
||||||
|
(testing "as Middleware vector"
|
||||||
|
(reset! calls 0)
|
||||||
|
(let [app (->app [[(middleware/create {:wrap wrap}) :value]] identity)]
|
||||||
(dotimes [_ 10]
|
(dotimes [_ 10]
|
||||||
(is (= [:value :request] (app :request)))
|
(is (= [:value :request] (app :request)))
|
||||||
(is (= 1 @calls)))))))
|
(is (= 1 @calls)))))))
|
||||||
|
|
||||||
(testing ":gen"
|
(testing "compiled Middleware"
|
||||||
(let [calls (atom 0)
|
(let [calls (atom 0)
|
||||||
data {:name ::test
|
mw {:gen (fn [meta _]
|
||||||
:gen (fn [meta _]
|
|
||||||
(swap! calls inc)
|
(swap! calls inc)
|
||||||
(fn [handler value]
|
(fn [handler value]
|
||||||
(swap! calls inc)
|
(swap! calls inc)
|
||||||
(fn [request]
|
(fn [request]
|
||||||
[meta value request])))}]
|
[meta value request])))}
|
||||||
|
->app (fn [ast handler]
|
||||||
|
(middleware/compile-handler
|
||||||
|
(middleware/expand ast :meta {})
|
||||||
|
handler))]
|
||||||
|
|
||||||
(testing "as map"
|
(testing "as map"
|
||||||
(reset! calls 0)
|
(reset! calls 0)
|
||||||
(let [app ((middleware/compose-middleware [data] :meta {}) identity :value)]
|
(let [app (->app [[mw :value]] identity)]
|
||||||
(dotimes [_ 10]
|
(dotimes [_ 10]
|
||||||
(is (= [:meta :value :request] (app :request)))
|
(is (= [:meta :value :request] (app :request)))
|
||||||
(is (= 2 @calls)))))
|
(is (= 2 @calls)))))
|
||||||
|
|
||||||
(testing "direct"
|
(testing "as Middleware"
|
||||||
(reset! calls 0)
|
(reset! calls 0)
|
||||||
(let [app ((middleware/compose-middleware [(middleware/create data)] :meta {}) identity :value)]
|
(let [app (->app [[(middleware/create mw) :value]] identity)]
|
||||||
(dotimes [_ 10]
|
|
||||||
(is (= [:meta :value :request] (app :request)))
|
|
||||||
(is (= 2 @calls)))))
|
|
||||||
|
|
||||||
(testing "vector"
|
|
||||||
(reset! calls 0)
|
|
||||||
(let [app ((middleware/compose-middleware [[(middleware/create data) :value]] :meta {}) identity)]
|
|
||||||
(is (= [:meta :value :request] (app :request)))
|
|
||||||
(dotimes [_ 10]
|
(dotimes [_ 10]
|
||||||
(is (= [:meta :value :request] (app :request)))
|
(is (= [:meta :value :request] (app :request)))
|
||||||
(is (= 2 @calls)))))
|
(is (= 2 @calls)))))
|
||||||
|
|
||||||
(testing "nil unmounts the middleware"
|
(testing "nil unmounts the middleware"
|
||||||
(reset! calls 0)
|
(let [app (->app [{:gen (constantly nil)}
|
||||||
(let [syntax [[(middleware/create
|
{:gen (constantly nil)}] identity)]
|
||||||
{:name ::test
|
|
||||||
:gen (fn [meta _])}) :value]]
|
|
||||||
app ((middleware/compose-middleware syntax :meta {}) identity)]
|
|
||||||
(is (= :request (app :request)))
|
|
||||||
(dotimes [_ 10]
|
(dotimes [_ 10]
|
||||||
(is (= :request (app :request))))))))))
|
(is (= :request (app :request))))))))))
|
||||||
|
|
||||||
(deftest middleware-router-test
|
(deftest middleware-handler-test
|
||||||
|
|
||||||
(testing "all paths should have a handler"
|
(testing "all paths should have a handler"
|
||||||
(is (thrown-with-msg?
|
(is (thrown-with-msg?
|
||||||
|
|
@ -125,40 +113,59 @@
|
||||||
#"path \"/ping\" doesn't have a :handler defined"
|
#"path \"/ping\" doesn't have a :handler defined"
|
||||||
(middleware/router ["/ping"]))))
|
(middleware/router ["/ping"]))))
|
||||||
|
|
||||||
(testing "ring-handler"
|
(testing "middleware-handler"
|
||||||
(let [api-mw #(mw % :api)
|
(let [mw (fn [handler value]
|
||||||
|
(fn [request]
|
||||||
|
(conj (handler (conj request value)) value)))
|
||||||
|
api-mw #(mw % :api)
|
||||||
|
handler #(conj % :ok)
|
||||||
router (middleware/router
|
router (middleware/router
|
||||||
[["/ping" handler]
|
[["/ping" handler]
|
||||||
["/api" {:middleware [api-mw]}
|
["/api" {:middleware [api-mw]}
|
||||||
["/ping" handler]
|
["/ping" handler]
|
||||||
["/admin" {:middleware [[mw :admin]]}
|
["/admin" {:middleware [[mw :admin]]}
|
||||||
["/ping" handler]]]])
|
["/ping" handler]]]])
|
||||||
app (fn
|
->app (fn [router]
|
||||||
([{:keys [uri] :as request}]
|
(let [h (middleware/middleware-handler router)]
|
||||||
(if-let [handler (:result (reitit/match-by-path router uri))]
|
(fn [path]
|
||||||
(handler request)))
|
(if-let [f (h path)]
|
||||||
([{:keys [uri] :as request} respond raise]
|
(f [])))))
|
||||||
(if-let [handler (:result (reitit/match-by-path router uri))]
|
app (->app router)]
|
||||||
(handler request respond raise))))]
|
|
||||||
|
|
||||||
(testing "not found"
|
(testing "not found"
|
||||||
(is (= nil (app {:uri "/favicon.ico"}))))
|
(is (= nil (app "/favicon.ico"))))
|
||||||
|
|
||||||
(testing "normal handler"
|
(testing "normal handler"
|
||||||
(is (= {:status 200, :body [:ok]}
|
(is (= [:ok] (app "/ping"))))
|
||||||
(app {:uri "/ping"}))))
|
|
||||||
|
|
||||||
(testing "with middleware"
|
(testing "with middleware"
|
||||||
(is (= {:status 200, :body [:api :ok :api]}
|
(is (= [:api :ok :api] (app "/api/ping"))))
|
||||||
(app {:uri "/api/ping"}))))
|
|
||||||
|
|
||||||
(testing "with nested middleware"
|
(testing "with nested middleware"
|
||||||
(is (= {:status 200, :body [:api :admin :ok :admin :api]}
|
(is (= [:api :admin :ok :admin :api] (app "/api/admin/ping"))))
|
||||||
(app {:uri "/api/admin/ping"}))))
|
|
||||||
|
|
||||||
(testing "3-arity"
|
(testing ":gen middleware can be unmounted at creation-time"
|
||||||
(let [result (atom nil)
|
(let [mw1 {:name ::mw1, :gen (constantly #(mw % ::mw1))}
|
||||||
respond (partial reset! result), raise ::not-called]
|
mw2 {:name ::mw2, :gen (constantly nil)}
|
||||||
(app {:uri "/api/admin/ping"} respond raise)
|
mw3 {:name ::mw3, :wrap #(mw % ::mw3)}
|
||||||
(is (= {:status 200, :body [:api :admin :ok :admin :api]}
|
router (middleware/router
|
||||||
@result)))))))
|
["/api" {:name ::api
|
||||||
|
:middleware [mw1 mw2 mw3 mw2]
|
||||||
|
:handler handler}])
|
||||||
|
app (->app router)]
|
||||||
|
|
||||||
|
(is (= [::mw1 ::mw3 :ok ::mw3 ::mw1] (app "/api")))
|
||||||
|
|
||||||
|
(testing "routes contain list of actually applied mw"
|
||||||
|
(is (= [::mw1 ::mw3] (->> (reitit/routes router)
|
||||||
|
first
|
||||||
|
last
|
||||||
|
:middleware
|
||||||
|
(map :name)))))
|
||||||
|
|
||||||
|
(testing "match contains list of actually applied mw"
|
||||||
|
(is (= [::mw1 ::mw3] (->> "/api"
|
||||||
|
(reitit/match-by-path router)
|
||||||
|
:result
|
||||||
|
:middleware
|
||||||
|
(map :name))))))))))
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
(ns reitit.spec-test
|
(ns reitit.spec-test
|
||||||
(:require [clojure.test :refer [deftest testing is are]]
|
(:require [clojure.test :refer [deftest testing is are]]
|
||||||
[clojure.spec.test.alpha :as stest]
|
[#?(:clj clojure.spec.test.alpha :cljs cljs.spec.test.alpha) :as stest]
|
||||||
[clojure.spec.alpha :as s]
|
[clojure.spec.alpha :as s]
|
||||||
[reitit.core :as reitit]
|
[reitit.core :as reitit]
|
||||||
[reitit.spec :as spec])
|
[reitit.spec :as spec])
|
||||||
#?(:clj
|
#?(:clj
|
||||||
(:import (clojure.lang ExceptionInfo))))
|
(:import (clojure.lang ExceptionInfo))))
|
||||||
|
|
||||||
(stest/instrument `reitit/router)
|
(stest/instrument `reitit/router `reitit/routes)
|
||||||
|
|
||||||
(deftest router-spec-test
|
(deftest router-spec-test
|
||||||
|
|
||||||
|
|
@ -44,6 +44,9 @@
|
||||||
["/api" []
|
["/api" []
|
||||||
["/ipa"]])))
|
["/ipa"]])))
|
||||||
|
|
||||||
|
(testing "routes conform to spec (can't spec protocol functions)"
|
||||||
|
(is (= true (s/valid? ::spec/routes (reitit/routes (reitit/router ["/ping"]))))))
|
||||||
|
|
||||||
(testing "options"
|
(testing "options"
|
||||||
|
|
||||||
(are [opts]
|
(are [opts]
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,15 @@
|
||||||
(ns reitit.doo-runner
|
(ns reitit.doo-runner
|
||||||
(:require [doo.runner :refer-macros [doo-tests]]
|
(:require [doo.runner :refer-macros [doo-tests]]
|
||||||
|
reitit.coercion-test
|
||||||
reitit.core-test
|
reitit.core-test
|
||||||
reitit.ring-test))
|
reitit.middleware-test
|
||||||
|
reitit.ring-test
|
||||||
|
#_reitit.spec-test))
|
||||||
|
|
||||||
(enable-console-print!)
|
(enable-console-print!)
|
||||||
|
|
||||||
(doo-tests 'reitit.core-test
|
(doo-tests 'reitit.coercion-test
|
||||||
'reitit.ring-test)
|
'reitit.core-test
|
||||||
|
'reitit.middleware-test
|
||||||
|
'reitit.ring-test
|
||||||
|
#_'reitit.spec-test)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue