README & small fixes

This commit is contained in:
Tommi Reiman 2017-08-30 13:24:01 +03:00
parent dcd559bf27
commit c7c4013f97
6 changed files with 227 additions and 65 deletions

232
README.md
View file

@ -6,6 +6,8 @@ A friendly data-driven router for Clojure(Script).
* First-class route meta-data
* Generic, not tied to HTTP
* [Route conflict resolution](#route-conflicts)
* [Pluggable coercion](#parameter-coercion) ([clojure.spec](https://clojure.org/about/spec))
* Middleware & Interceptors
* Extendable
* Fast
@ -68,7 +70,7 @@ Same routes flattened:
For routing, a `Router` is needed. Reitit ships with several different router implementations: `:linear-router`, `:lookup-router` and `:mixed-router`, based on the awesome [Pedestal](https://github.com/pedestal/pedestal/tree/master/route) implementation.
`Router` is created with `reitit.core/router`, which takes routes and optional options map as arguments. The route tree gets expanded, optionally coerced and compiled. The actual `Router` implementation is selected based on the route tree or can be selected with the `:router` option. `Router` support both fast path- and name-based lookups.
`Router` is created with `reitit.core/router`, which takes routes and optional options map as arguments. The route tree gets expanded, optionally coerced and compiled. Actual `Router` implementation is selected automatically but can be defined with a `:router` option. `Router` support both path- and name-based lookups.
Creating a router:
@ -82,7 +84,7 @@ Creating a router:
["/user/:id" ::user]]]))
```
`:mixed-router` is created (both static & wild routes are used):
`:mixed-router` is created (both static & wild routes are found):
```clj
(reitit/router-type router)
@ -157,52 +159,47 @@ Routes can have arbitrary meta-data. For nested routes, the meta-data is accumul
A router based on nested route tree:
```clj
(def router
(reitit/router
["/api" {:interceptors [::api]}
["/ping" ::ping]
["/public/*path" ::resources]
["/user/:id" {:name ::get-user
:parameters {:id String}}
["/orders" ::user-orders]]
["/admin" {:interceptors [::admin]
:roles #{:admin}}
["/root" {:name ::root
:roles ^:replace #{:root}}]
["/db" {:name ::db
:interceptors [::db]}]]]))
["/admin" {:roles #{:admin}}
["/users" ::users]
["/db" {:interceptors [::db], :roles ^:replace #{:db-admin}}
["/:db" {:parameters {:db String}}
["/drop" ::drop-db]
["/stats" ::db-stats]]]]]))
```
Resolved route tree:
```clj
(reitit/routes router)
; [["/api/ping" {:name :user/ping
; :interceptors [::api]}]
; ["/api/public/*path" {:name :user/resources
; :interceptors [::api]}]
; ["/api/user/:id/orders" {:name :user/user-orders
; :interceptors [::api]
; :parameters {:id String}}]
; ["/api/admin/root" {:name :user/root
; :interceptors [::api ::admin]
; :roles #{:root}}]
; ["/api/admin/db" {:name :user/db
; :interceptors [::api ::admin ::db]
; :roles #{:admin}}]]
; [["/api/ping" {:interceptors [::api]
; :name ::ping}]
; ["/api/admin/users" {:interceptors [::api]
; :roles #{:admin}
; :name ::users}]
; ["/api/admin/db/:db/drop" {:interceptors [::api ::db]
; :roles #{:db-admin}
; :parameters {:db String}
; :name ::drop-db}]
; ["/api/admin/db/:db/stats" {:interceptors [::api ::db]
; :roles #{:db-admin}
; :parameters {:db String}
; :name ::db-stats}]]
```
Path-based routing:
```clj
(reitit/match-by-path router "/api/admin/root")
; #Match{:template "/api/admin/root"
; :meta {:name :user/root
; :interceptors [::api ::admin]
; :roles #{:root}}
; :path "/api/admin/root"
(reitit/match-by-path router "/api/admin/users")
; #Match{:template "/api/admin/users"
; :meta {:interceptors [::api]
; :roles #{:admin}
; :name ::users}
; :result nil
; :params {}}
; :params {}
; :path "/api/admin/users"}
```
On match, route meta-data is returned and can interpreted by the application.
@ -333,8 +330,6 @@ Middleware is applied correctly:
; {:status 200, :body [:api :handler]}
```
Nested middleware works too:
```clj
(app {:request-method :delete, :uri "/api/admin/db"})
; {:status 200, :body [:api :admin :db :delete :handler]}
@ -342,7 +337,7 @@ Nested middleware works too:
### Async Ring
Ring-router supports also 3-arity [Async Ring](https://www.booleanknot.com/blog/2016/07/15/asynchronous-ring.html), so it can be used on [Node.js](https://nodejs.org/en/) too.
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.
### Meta-data based extensions
@ -397,6 +392,169 @@ Authorized access to guarded route:
; {:status 200, :body "ok"}
```
## 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).
**NOTE**: to use the spec-coercion, one needs to add the following dependencies manually to the project:
```clj
[org.clojure/clojure "1.9.0-alpha17"]
[org.clojure/spec.alpha "0.1.123"]
[metosin/spec-tools "0.3.2"]
```
### Ring request and response coercion
To use `Coercion` with Ring, one needs to do the following:
1. Define parameters and responses as data into route meta-data, in format adopted from [ring-swagger](https://github.com/metosin/ring-swagger#more-complete-example):
* `:parameters` map, with submaps for different parameters: `:query`, `:body`, `:form`, `:header` and `:path`. Parameters are defined in the format understood by the `Coercion`.
* `:responses` map, with response status codes as keys (or `:default` for "everything else") with maps with `:schema` and optionally `:description` as values.
2. Define a `Coercion` to route meta-data under `:coercion`
3. Mount request & response coercion middleware to the routes.
If the request coercion succeeds, the coerced parameters are injected into request under `:parameters`.
If either request or response coercion fails, an descriptive error is thrown.
#### Example with data-specs
```clj
(require '[reitit.ring :as ring])
(require '[reitit.coercion :as coercion])
(require '[reitit.coercion.spec :as spec])
(def app
(ring/ring-handler
(ring/router
["/api"
["/ping" {:parameters {:body {:x int?, :y int?}}
:responses {200 {:schema {:total pos-int?}}}
:get {:handler (fn [{{{:keys [x y]} :body} :parameters}]
{:status 200
:body {:total (+ x y)}})}}]]
{:meta {:middleware [coercion/gen-wrap-coerce-parameters
coercion/gen-wrap-coerce-response]
:coercion spec/coercion}})))
```
```clj
(app
{:request-method :get
:uri "/api/ping"
:body-params {:x 1, :y 2}})
; {:status 200, :body {:total 3}}
```
#### Example with specs
```clj
(require '[reitit.ring :as ring])
(require '[reitit.coercion :as coercion])
(require '[reitit.coercion.spec :as spec])
(require '[clojure.spec.alpha :as s])
(require '[spec-tools.core :as st])
(s/def ::x (st/spec int?))
(s/def ::y (st/spec int?))
(s/def ::total int?)
(s/def ::request (s/keys :req-un [::x ::y]))
(s/def ::response (s/keys :req-un [::total]))
(def app
(ring/ring-handler
(ring/router
["/api"
["/ping" {:parameters {:body ::request}
:responses {200 {:schema ::response}}
:get {:handler (fn [{{{:keys [x y]} :body} :parameters}]
{:status 200
:body {:total (+ x y)}})}}]]
{:meta {:middleware [coercion/gen-wrap-coerce-parameters
coercion/gen-wrap-coerce-response]
:coercion spec/coercion}})))
```
```clj
(app
{:request-method :get
:uri "/api/ping"
:body-params {:x 1, :y 2}})
; {:status 200, :body {:total 3}}
```
## 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.
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 much lighter runtime processing.
For middleware, there is a helper `reitit.middleware/gen` for this. It takes a function of `route-meta router-opts => middleware` and returns a special record extending the internal middleware protocols so it can be mounted as normal middleware. The compiled middleware can also decide no to mount itsef byt returning `nil`. Why mount `wrap-enforce-roles` if there are no roles required for that route?
To demonstrate the two approaches, below are response coercion middleware written in both ways (found in `reitit.coercion`):
### Naive
* Extracts the compiled route information on every request.
```clj
(defn wrap-coerce-response
"Pluggable response coercion middleware.
Expects a :coercion of type `reitit.coercion.protocol/Coercion`
and :responeses from route meta, otherwise does not mount."
[handler]
(fn
([request]
(let [response (handler request)
method (:request-method request)
match (ring/get-match request)
responses (-> match :result method :meta :responses)
coercion (-> match :meta :coercion)
opts (-> match :meta :opts)]
(if (and coercion responses)
(let [coercers (response-coercers coercion responses opts)
coerced (coerce-response coercers request response)]
(coerce-response coercers request (handler request)))
(handler request))))
([request respond raise]
(let [response (handler request)
method (:request-method request)
match (ring/get-match request)
responses (-> match :result method :meta :responses)
coercion (-> match :meta :coercion)
opts (-> match :meta :opts)]
(if (and coercion responses)
(let [coercers (response-coercers coercion responses opts)
coerced (coerce-response coercers request response)]
(handler request #(respond (coerce-response coercers request %))))
(handler request respond raise))))))
```
### Compiled
* Route information is provided via a closure
* Pre-compiled coercers
* Mounts only if `:coercion` and `:responses` are defined for the route
```clj
(def gen-wrap-coerce-response
"Generator for pluggable response coercion middleware.
Expects a :coercion of type `reitit.coercion.protocol/Coercion`
and :responses from route meta, otherwise does not mount."
(middleware/gen
(fn [{:keys [responses coercion opts]} _]
(if (and coercion responses)
(let [coercers (response-coercers coercion responses opts)]
(fn [handler]
(fn
([request]
(coerce-response coercers request (handler request)))
([request respond raise]
(handler request #(respond (coerce-response coercers request %)) raise)))))))))
```
## Merging route-trees
*TODO*
@ -405,7 +563,7 @@ Authorized access to guarded route:
*TODO*
## Schema, Spec, Swagger & Openapi
## Swagger & Openapi
*TODO*

View file

@ -80,7 +80,7 @@
(defrecord NoOpCoercion []
protocol/Coercion
(get-name [_] :no-op)
(compile [_ model] model)
(compile [_ model _] model)
(get-apidocs [_ _ {:keys [parameters responses] :as info}])
(make-open [_ spec] spec)
(encode-error [_ error] error)

View file

@ -6,7 +6,7 @@
[reitit.ring :as ring]
[reitit.impl :as impl]))
(defn get-apidocs [coercion spec info]
#_(defn get-apidocs [coercion spec info]
(protocol/get-apidocs coercion spec info))
;;
@ -61,15 +61,16 @@
(request-coercion-failed! result coercion value in request)
result))))))
(defn- response-format [request response]
#_(defn muuntaja-response-format [request response]
(or (-> response :muuntaja/content-type)
(some-> request :muuntaja/response :format)))
(defn response-coercer [coercion model]
(defn response-coercer [coercion model {:keys [extract-response-format]
:or {extract-response-format (constantly nil)}}]
(if coercion
(let [coercer (protocol/response-coercer coercion model)]
(fn [request response]
(let [format (response-format request response)
(let [format (extract-response-format request response)
value (:body response)
result (coercer value format)]
(if (protocol/error? result)
@ -98,15 +99,15 @@
[k (request-coercer coercion k v)])
(into {})))
(defn ^:no-doc response-coercers [coercion responses]
(defn ^:no-doc response-coercers [coercion responses opts]
(->> (for [[status {:keys [schema]}] responses :when schema]
[status (response-coercer coercion schema)])
[status (response-coercer coercion schema opts)])
(into {})))
(defn wrap-coerce-parameters
"Pluggable request coercion middleware.
Expects a :coercion of type `reitit.coercion.protocol/Coercion`
from injected route meta, otherwise does not mount."
and :parameters from route meta, otherwise does not mount."
[handler]
(fn
([request]
@ -132,10 +133,10 @@
(def gen-wrap-coerce-parameters
"Generator for pluggable request coercion middleware.
Expects a :coercion of type `reitit.coercion.protocol/Coercion`
from injected route meta, otherwise does not mount."
and :parameters from route meta, otherwise does not mount."
(middleware/gen
(fn [{:keys [parameters coercion]} _]
(if coercion
(if (and coercion parameters)
(let [coercers (request-coercers coercion parameters)]
(fn [handler]
(fn
@ -149,7 +150,7 @@
(defn wrap-coerce-response
"Pluggable response coercion middleware.
Expects a :coercion of type `reitit.coercion.protocol/Coercion`
from injected route meta, otherwise does not mount."
and :responses from route meta, otherwise does not mount."
[handler]
(fn
([request]
@ -157,9 +158,10 @@
method (:request-method request)
match (ring/get-match request)
responses (-> match :result method :meta :responses)
coercion (-> match :meta :coercion)]
coercion (-> match :meta :coercion)
opts (-> match :meta :opts)]
(if coercion
(let [coercers (response-coercers coercion responses)
(let [coercers (response-coercers coercion responses opts)
coerced (coerce-response coercers request response)]
(coerce-response coercers request (handler request)))
(handler request))))
@ -168,9 +170,10 @@
method (:request-method request)
match (ring/get-match request)
responses (-> match :result method :meta :responses)
coercion (-> match :meta :coercion)]
coercion (-> match :meta :coercion)
opts (-> match :meta :opts)]
(if coercion
(let [coercers (response-coercers coercion responses)
(let [coercers (response-coercers coercion responses opts)
coerced (coerce-response coercers request response)]
(handler request #(respond (coerce-response coercers request %))))
(handler request respond raise))))))
@ -178,11 +181,11 @@
(def gen-wrap-coerce-response
"Generator for pluggable response coercion middleware.
Expects a :coercion of type `reitit.coercion.protocol/Coercion`
from injected route meta, otherwise does not mount."
and :responses from route meta, otherwise does not mount."
(middleware/gen
(fn [{:keys [responses coercion]} _]
(if coercion
(let [coercers (response-coercers coercion responses)]
(fn [{:keys [responses coercion opts]} _]
(if (and coercion responses)
(let [coercers (response-coercers coercion responses opts)]
(fn [handler]
(fn
([request]

View file

@ -2,13 +2,14 @@
(:refer-clojure :exclude [compile]))
(defprotocol Coercion
(get-name [this])
(compile [this model])
(get-apidocs [this model data])
(make-open [this model])
(encode-error [this error])
(request-coercer [this type model])
(response-coercer [this model]))
"Pluggable coercion protocol"
(get-name [this] "Keyword name for the coercion")
(compile [this model name] "Compiles a coercion model")
(get-apidocs [this model data] "???")
(make-open [this model] "Returns a new map model which doesn't fail on extra keys")
(encode-error [this error] "Converts error in to a serializable format")
(request-coercer [this type model] "Returns a `value format => value` request coercion function")
(response-coercer [this model] "Returns a `value format => value` response coercion function"))
(defrecord CoercionError [])

View file

@ -57,7 +57,7 @@
protocol/Coercion
(get-name [_] name)
(compile [_ model]
(compile [_ model _]
(memoized-specify model))
(get-apidocs [_ _ {:keys [parameters responses] :as info}]

View file

@ -1,4 +1,4 @@
(ns reitit.middleware-test
(ns reitit.coercion-test
(:require [clojure.test :refer [deftest testing is]]
[reitit.ring :as ring]
[reitit.coercion :as coercion]