diff --git a/README.md b/README.md index a7757fe2..d5b5e9d6 100644 --- a/README.md +++ b/README.md @@ -6,9 +6,10 @@ A friendly data-driven router for Clojure(Script). * Route [conflict resolution](https://metosin.github.io/reitit/basics/route_conflicts.html) * First-class [route data](https://metosin.github.io/reitit/basics/route_data.html) * Bi-directional routing -* [Ring-router](https://metosin.github.io/reitit/ring.html) with data-driven [middleware](https://metosin.github.io/reitit/ring/compiling_middleware.html) -* [Pluggable coercion](https://metosin.github.io/reitit/ring/parameter_coercion.html) ([clojure.spec](https://clojure.org/about/spec)) +* [Ring-router](https://metosin.github.io/reitit/ring/ring.html) with [data-driven middleware](https://metosin.github.io/reitit/ring/data_driven_middleware.html) +* [Pluggable coercion](https://metosin.github.io/reitit/ring/coercion.html) ([schema](https://github.com/plumatic/schema) & [clojure.spec](https://clojure.org/about/spec)) * Extendable +* Modular * [Fast](https://metosin.github.io/reitit/performance.html) See the [full documentation](https://metosin.github.io/reitit/) for details. @@ -26,7 +27,8 @@ Optionally, the parts can be required separately: ```clj [metosin/reitit-core "0.1.0-SNAPSHOT"] ; just the router [metosin/reitit-ring "0.1.0-SNAPSHOT"] ; ring-router -[metosin/reitit-spec "0.1.0-SNAPSHOT"] ; spec-coercion +[metosin/reitit-spec "0.1.0-SNAPSHOT"] ; spec coercion +[metosin/reitit-schema "0.1.0-SNAPSHOT"] ; schema coercion ``` ## Quick start diff --git a/doc/README.md b/doc/README.md index 4dfd2318..bde4bab0 100644 --- a/doc/README.md +++ b/doc/README.md @@ -3,12 +3,13 @@ [Reitit](https://github.com/metosin/reitit) is a small Clojure(Script) library for data-driven routing. * Simple data-driven [route syntax](./basics/route_syntax.md) -* [Route conflict resolution](./advanced/route_conflicts.md) +* [Route conflict resolution](./basics/route_conflicts.md) * First-class [route data](./basics/route_data.md) * Bi-directional routing -* [Pluggable coercion](./ring/parameter_coercion.md) ([clojure.spec](https://clojure.org/about/spec)) -* supports both [Middleware](./ring/compiling_middleware.md) & Interceptors +* [Ring-router](./ring/ring.html) with [data-driven middleware](./ring/data_driven_middleware.html) +* [Pluggable coercion](./ring/coercion.html) ([schema](https://github.com/plumatic/schema) & [clojure.spec](https://clojure.org/about/spec)) * Extendable +* Modular * [Fast](performance.md) To use Reitit, add the following dependecy to your project: @@ -23,6 +24,7 @@ Optionally, the parts can be required separately: [metosin/reitit-core "0.1.0-SNAPSHOT"] ; just the router [metosin/reitit-ring "0.1.0-SNAPSHOT"] ; ring-router [metosin/reitit-spec "0.1.0-SNAPSHOT"] ; spec-coercion +[metosin/reitit-schema "0.1.0-SNAPSHOT"] ; schema coercion ``` For discussions, there is a [#reitit](https://clojurians.slack.com/messages/reitit/) channel in [Clojurians slack](http://clojurians.net/). diff --git a/doc/SUMMARY.md b/doc/SUMMARY.md index f221c216..8930d756 100644 --- a/doc/SUMMARY.md +++ b/doc/SUMMARY.md @@ -16,8 +16,9 @@ * [Ring-router](ring/ring.md) * [Dynamic extensions](ring/dynamic_extensions.md) * [Data-driven Middleware](ring/data_driven_middleware.md) - * [Parameter coercion](ring/parameter_coercion.md) + * [Pluggable Coercion](ring/coercion.md) * [Compiling middleware](ring/compiling_middleware.md) * [Performance](performance.md) +* [FAQ](faq.md) * TODO: Swagger & OpenAPI * TODO: Interceptors diff --git a/doc/faq.md b/doc/faq.md new file mode 100644 index 00000000..318b08dc --- /dev/null +++ b/doc/faq.md @@ -0,0 +1 @@ +# Frequently Asked Questions diff --git a/doc/ring/README.md b/doc/ring/README.md index 9bbe9b59..7db18acf 100644 --- a/doc/ring/README.md +++ b/doc/ring/README.md @@ -3,5 +3,5 @@ * [Ring-router](ring.md) * [Dynamic extensions](dynamic_extensions.md) * [Data-driven Middleware](data_driven_middleware.md) -* [Parameter coercion](parameter_coercion.md) +* [Pluggable Coercion](coercion.md) * [Compiling middleware](compiling_middleware.md) diff --git a/doc/ring/coercion.md b/doc/ring/coercion.md new file mode 100644 index 00000000..a2a730a0 --- /dev/null +++ b/doc/ring/coercion.md @@ -0,0 +1,209 @@ +# Pluggable Coercion + +Reitit provides pluggable parameter coercion via `reitit.ring.coercion.protocol/Coercion` protocol, originally introduced in [compojure-api](https://clojars.org/metosin/compojure-api). + +Reitit ships with the following coercion modules: + +* `reitit.ring.coercion.schema/SchemaCoercion` for [plumatic schema](https://github.com/plumatic/schema). +* `reitit.ring.coercion.spec/SpecCoercion` for both [clojure.spec](https://clojure.org/about/spec) and [data-specs](https://github.com/metosin/spec-tools#data-specs). + +### 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 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. Set a `Coercion` implementation to route data under `:coercion` +3. Mount request & response coercion middleware to the routes (can be done for all routes as the middleware are only mounted to routes which have the parameters &/ responses defined): + * `reitit.ring.coercion/gen-wrap-coerce-parameters` + * `reitit.ring.coercion/gen-wrap-coerce-response` + +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. To turn the exceptions into http responses, one can also mount the `reitit.ring.coercion/gen-wrap-coerce-exceptions` middleware + +### Example with Schema + +```clj +(require '[reitit.ring :as ring]) +(require '[reitit.ring.coercion :as coercion]) +(require '[reitit.ring.coercion.schema :as schema]) +(require '[schema.core :as s]) + +(def app + (ring/ring-handler + (ring/router + ["/api" + ["/ping" {:parameters {:body {:x s/Int, :y s/Int}} + :responses {200 {:schema {:total (s/constrained s/Int pos?}}} + :get {:handler (fn [{{{:keys [x y]} :body} :parameters}] + {:status 200 + :body {:total (+ x y)}})}}]] + {:data {:middleware [coercion/gen-wrap-coerce-exceptions + coercion/gen-wrap-coerce-parameters + coercion/gen-wrap-coerce-response] + :coercion schema/coercion}}))) +``` + +Valid request: + +```clj +(app + {:request-method :get + :uri "/api/ping" + :body-params {:x 1, :y 2}}) +; {:status 200 +; :body {:total 3}} +``` + +Invalid request: + +```clj +(app + {:request-method :get + :uri "/api/ping" + :body-params {:x 1, :y "2"}}) +; {:status 400, +; :body {:type :reitit.ring.coercion/request-coercion +; :coercion :schema +; :in [:request :body-params] +; :value {:x 1, :y "2"} +; :schema {:x "Int", :y "Int"} +; :errors {:y "(not (integer? \"2\"))"}}} +``` + +### Example with data-specs + +```clj +(require '[reitit.ring :as ring]) +(require '[reitit.ring.coercion :as coercion]) +(require '[reitit.ring.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)}})}}]] + {:data {:middleware [coercion/gen-wrap-coerce-exceptions + coercion/gen-wrap-coerce-parameters + coercion/gen-wrap-coerce-response] + :coercion spec/coercion}}))) +``` + +Valid request: + +```clj +(app + {:request-method :get + :uri "/api/ping" + :body-params {:x 1, :y 2}}) +; {:status 200 +; :body {:total 3}} +``` + +Invalid request: + +```clj +(app + {:request-method :get + :uri "/api/ping" + :body-params {:x 1, :y "2"}}) +; {:status 400, +; :body {:type ::coercion/request-coercion +; :coercion :spec +; :in [:request :body-params] +; :value {:x 1, :y "2"} +; :spec "(spec-tools.core/spec {:spec (clojure.spec.alpha/keys :req-un [:$spec37747/x :$spec37747/y]), :type :map, :keys #{:y :x}, :keys/req #{:y :x}})" +; :problems [{:path [:y] +; :pred "clojure.core/int?" +; :val "2" +; :via [:$spec37747/y] +; :in [:y]}]}} +``` + +### Example with clojure.spec + +Currently, `clojure.spec` [doesn't support runtime transformations via conforming](https://dev.clojure.org/jira/browse/CLJ-2116), so one needs to wrap all specs with `spec-tools.core/spec`. + +```clj +(require '[reitit.ring :as ring]) +(require '[reitit.ring.coercion :as coercion]) +(require '[reitit.ring.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)}})}}]] + {:data {:middleware [coercion/gen-wrap-coerce-exceptions + coercion/gen-wrap-coerce-parameters + coercion/gen-wrap-coerce-response] + :coercion spec/coercion}}))) +``` + +Valid request: + +```clj +(app + {:request-method :get + :uri "/api/ping" + :body-params {:x 1, :y 2}}) +; {:status 200 +; :body {:total 3}} +``` + +Invalid request: + +```clj +(app + {:request-method :get + :uri "/api/ping" + :body-params {:x 1, :y "2"}}) +; {:status 400, +; :body {:type ::coercion/request-coercion +; :coercion :spec +; :in [:request :body-params] +; :value {:x 1, :y "2"} +; :spec "(spec-tools.core/spec {:spec (clojure.spec.alpha/keys :req-un [:reitit.coercion-test/x :reitit.coercion-test/y]), :type :map, :keys #{:y :x}, :keys/req #{:y :x}})" +; :problems [{:path [:y] +; :pred "clojure.core/int?" +; :val "2" +; :via [::request ::y] +; :in [:y]}]}} +``` + +### Custom coercion + +Both Schema and Spec Coercion can be configured via options, see the source code for details. + +To plug in new validation engine, see the +`reitit.ring.coercion.protocol/Coercion` protocol. + +```clj +(defprotocol Coercion + "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")) +``` diff --git a/doc/ring/compiling_middleware.md b/doc/ring/compiling_middleware.md index 19afd272..c522e8e8 100644 --- a/doc/ring/compiling_middleware.md +++ b/doc/ring/compiling_middleware.md @@ -2,13 +2,13 @@ The [dynamic extensions](dynamic_extensions.md) is a easy way to extend the system. To enable fast lookups into route data, we can compile them into any shape (records, functions etc.) we want, enabling fast access at request-time. -Still, we can do much better. As we know the exact route that middleware/interceptor is linked to, we can pass the (compiled) route information into the middleware/interceptor at creation-time. It can do local reasoning: extract and transform relevant data just for it and pass it into the actual request-handler via a closure - yielding much faster runtime processing. It can also decide not to mount itself by returning `nil`. Why mount a `wrap-enforce-roles` middleware for a route if there are no roles required for it? +But, we can do much better. As we know the exact route that middleware/interceptor is linked to, we can pass the (compiled) route information into the middleware/interceptor at creation-time. It can do local reasoning: extract and transform relevant data just for it and pass it into the actual request-handler via a closure - yielding much faster runtime processing. It can also decide not to mount itself by returning `nil`. Why mount a `wrap-enforce-roles` middleware for a route if there are no roles required for it? To enable this we use [middleware records](data_driven_middleware.md) `:gen-wrap` key instead of the normal `:wrap`. `:gen-wrap` expects a function of `route-data router-opts => ?wrap`. -To demonstrate the two approaches, below are response coercion middleware written as normal ring middleware function and as middleware record with `:gen-wrap`. Actual codes can be found in [`reitit.ring.coercion`](https://github.com/metosin/reitit/blob/master/src/reitit/ring/coercion.cljc): +To demonstrate the two approaches, below are response coercion middleware written as normal ring middleware function and as middleware record with `:gen-wrap`. -## Naive +## Normal Middleware * Reads the compiled route information on every request. @@ -42,7 +42,7 @@ To demonstrate the two approaches, below are response coercion middleware writte (handler request respond raise)))))) ``` -## Compiled +## Compiled Middleware * Route information is provided via a closure * Pre-compiled coercers @@ -52,20 +52,20 @@ To demonstrate the two approaches, below are response coercion middleware writte (require '[reitit.ring.middleware :as middleware]) (def gen-wrap-coerce-response - "Generator for pluggable response coercion middleware. + "Middleware for pluggable response coercion. Expects a :coercion of type `reitit.coercion.protocol/Coercion` and :responses from route data, otherwise does not mount." (middleware/create {:name ::coerce-response - :gen-wrap (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)))))))})) + :gen-wrap (fn [{:keys [coercion responses 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)))))))})) ``` The latter has 50% less code, is easier to reason about and is much faster. diff --git a/doc/ring/parameter_coercion.md b/doc/ring/parameter_coercion.md deleted file mode 100644 index 10b20d6b..00000000 --- a/doc/ring/parameter_coercion.md +++ /dev/null @@ -1,96 +0,0 @@ -# Parameter coercion - -Reitit provides pluggable parameter coercion via `reitit.ring.coercion.protocol/Coercion` protocol, originally introduced in [compojure-api](https://clojars.org/metosin/compojure-api). Reitit ships with `reitit.ring.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**: Before Clojure 1.9.0 is shipped, to use the spec-coercion, one needs to add the following dependencies manually to the project: - -```clj -[org.clojure/clojure "1.9.0-beta2"] -[org.clojure/spec.alpha "0.1.123"] -[metosin/spec-tools "0.4.0"] -``` - -### 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 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 data under `:coercion` -3. Mount request & response coercion middleware to the routes (recommended to mount to all routes under router as they mounted only to routes which have the parameters / responses defined): - * `reitit.ring.coercion/gen-wrap-coerce-parameters` - * `gen-wrap-coerce-parameters/gen-wrap-coerce-responses` - -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.ring.coercion :as coercion]) -(require '[reitit.ring.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)}})}}]] - {:data {: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 - -Currently, `clojure.spec` [doesn't support runtime transformations via conforming](https://dev.clojure.org/jira/browse/CLJ-2116), so one needs to wrap all specs with `spec-tools.core/spec`. - -```clj -(require '[reitit.ring :as ring]) -(require '[reitit.ring.coercion :as coercion]) -(require '[reitit.ring.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)}})}}]] - {:data {: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}} -```