diff --git a/README.md b/README.md index b1132ccc..1f07ac1f 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,47 @@ -# reitit [![Build Status](https://travis-ci.org/metosin/reitit.svg?branch=master)](https://travis-ci.org/metosin/reitit) [![Dependencies Status](https://jarkeeper.com/metosin/reitit/status.svg)](https://jarkeeper.com/metosin/reitit) +# reitit [![Build Status](https://travis-ci.org/metosin/reitit.svg?branch=master)](https://travis-ci.org/metosin/reitit) A friendly data-driven router for Clojure(Script). -* Simple data-driven [route syntax](https://metosin.github.io/reitit/basics/route_syntax.md) -* [Route conflict resolution](https://metosin.github.io/reitit/advanced/route_conflicts.md) -* First-class [route meta-data](https://metosin.github.io/reitit/basics/route_data.md) +* Simple data-driven [route syntax](https://metosin.github.io/reitit/basics/route_syntax.html) +* Route [conflict resolution](https://metosin.github.io/reitit/basics/route_conflicts.html) +* First-class [route meta-data](https://metosin.github.io/reitit/basics/route_data.html) * Bi-directional routing -* [Pluggable coercion](https://metosin.github.io/reitit/ring/parameter_coercion.md) ([clojure.spec](https://clojure.org/about/spec)) -* supports both [Middleware](https://metosin.github.io/reitit/ring/compiling_middleware.md) & Interceptors +* [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)) * Extendable * Fast -Ships with example router for [Ring](#ring). See [Issues](https://github.com/metosin/reitit/issues) for roadmap. +See [Issues](https://github.com/metosin/reitit/issues) for roadmap. ## Latest version [![Clojars Project](http://clojars.org/metosin/reitit/latest-version.svg)](http://clojars.org/metosin/reitit) +## Quick start + +```clj +(require '[reitit.core :as r]) + +(def router + (r/router + [["/api/ping" ::ping] + ["/api/orders/:id" ::order-by-id]])) + +(r/match-by-path router "/api/ping") +; #Match{:template "/api/ping" +; :meta {:name ::ping} +; :result nil +; :params {} +; :path "/api/ping"} + +(r/match-by-name router ::order-by-id {:id 2}) +; #Match{:template "/api/orders/:id", +; :meta {:name ::order-by-id}, +; :result nil, +; :params {:id 2}, +; :path "/api/orders/2"} +``` + ## Documentation [Check out the full documentation!](https://metosin.github.io/reitit/) @@ -41,7 +66,7 @@ gitbook install gitbook serve ``` -To raise the version: +To bump up version: ```bash # new version diff --git a/doc/SUMMARY.md b/doc/SUMMARY.md index feb44aba..5cbcdd34 100644 --- a/doc/SUMMARY.md +++ b/doc/SUMMARY.md @@ -7,14 +7,15 @@ * [Path-based Routing](basics/path_based_routing.md) * [Name-based Routing](basics/name_based_routing.md) * [Route data](basics/route_data.md) - * [Different Routers](basics/different_routers.md) + * [Route conflicts](basics/route_conflicts.md) * [Advanced](advanced/README.md) - * [Route conflicts](advanced/route_conflicts.md) * [Route Validation](advanced/route_validation.md) + * [Different Routers](advanced/different_routers.md) * [Configuring routers](advanced/configuring_routers.md) * [Ring](ring/README.md) * [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) * [Compiling middleware](ring/compiling_middleware.md) * TODO: Swagger & OpenAPI diff --git a/doc/advanced/README.md b/doc/advanced/README.md index 6dccc442..cbea0749 100644 --- a/doc/advanced/README.md +++ b/doc/advanced/README.md @@ -1,5 +1,5 @@ # Advanced -* [Route conflicts](advanced/route_conflicts.md) -* [Route Validation](advanced/route_validation.md) -* [Configuring routers](advanced/configuring_routers.md) +* [Route Validation](route_validation.md) +* [Different Routers](different_routers.md) +* [Configuring routers](configuring_routers.md) diff --git a/doc/advanced/different_routers.md b/doc/advanced/different_routers.md new file mode 100644 index 00000000..ae742760 --- /dev/null +++ b/doc/advanced/different_routers.md @@ -0,0 +1,18 @@ +# Different Routers + +Reitit ships with several different implementations for the `Router` protocol, originally based on the [Pedestal](https://github.com/pedestal/pedestal/tree/master/route) implementation. `router` selects the most suitable implementation by inspecting the expanded routes. The implementation can be set manually using `:router` option, see [configuring routers](advanced/configuring_routers.md). + +| router | description | +| ------------------------------|-------------| +| `:linear-router` | Matches the routes one-by-one starting from the top until a match is found. Works with any kind of routes. +| `:lookup-router` | Fast router, uses hash-lookup to resolve the route. Valid if no paths have path or catch-all parameters. +| `:mixed-router` | Creates internally a `:linear-router` and a `:lookup-router` and used them to effectively get best-of-both-worlds. Valid if there are no [Route conflicts](../basics/route_conflicts.md). +| `::single-static-path-router` | Fastest possible router: valid only if there is one static route. +| `:prefix-tree-router` | TODO: https://github.com/julienschmidt/httprouter#how-does-it-work + +The router name can be asked from the router + +```clj +(r/router-name router) +; :mixed-router +``` diff --git a/doc/advanced/route_validation.md b/doc/advanced/route_validation.md index 991d5c83..80ef1bcd 100644 --- a/doc/advanced/route_validation.md +++ b/doc/advanced/route_validation.md @@ -4,12 +4,11 @@ Namespace `reitit.spec` contains [clojure.spec](https://clojure.org/about/spec) **NOTE:** Use of specs requires to use one of the following: -* `[org.clojure/clojurescript "1.9.660"]` -* `[org.clojure/clojure "1.9.0-alpha19"]` -* `[clojure-future-spec "1.9.0-alpha17"]` (Clojure 1.8) +* `[org.clojure/clojurescript "1.9.660"]` (or higher) +* `[org.clojure/clojure "1.9.0-beta2"]` (or higher) +* `[clojure-future-spec "1.9.0-alpha17"]` (if Clojure 1.8 is used) -## At runtime -If route trees are generated at runtime (e.g. from external source like the database), one can use directly the `clojure.spec` functions. +## Example ```clj (require '[clojure.spec.alpha :as s]) @@ -36,7 +35,7 @@ If route trees are generated at runtime (e.g. from external source like the data First add a `:dev` dependency to: ```clj -[expound "0.3.0"] +[expound "0.3.0"] ; or higher ``` Some bootstrapping: @@ -54,8 +53,9 @@ Some bootstrapping: And we are ready to go: ```clj +(require '[reitit.core :as r]) -(reitit/router +(r/router ["/api" ["/public" ["/ping"] diff --git a/doc/basics/README.md b/doc/basics/README.md index b92e5f81..1dec603b 100644 --- a/doc/basics/README.md +++ b/doc/basics/README.md @@ -1,8 +1,8 @@ # Basics -* [Route syntax](basics/route_syntax.md) -* [Router](basics/router.md) -* [Path-based Routing](basics/path_based_routing.md) -* [Name-based Routing](basics/name_based_routing.md) -* [Route data](basics/route_data.md) -* [Different Routers](basics/different_routers.md) +* [Route syntax](route_syntax.md) +* [Router](router.md) +* [Path-based Routing](path_based_routing.md) +* [Name-based Routing](name_based_routing.md) +* [Route data](route_data.md) +* [Route conflicts](route_conflicts.md) diff --git a/doc/basics/different_routers.md b/doc/basics/different_routers.md deleted file mode 100644 index d5a8ca1a..00000000 --- a/doc/basics/different_routers.md +++ /dev/null @@ -1,17 +0,0 @@ -# Different Routers - -Reitit ships with several different implementations for the `Router` protocol, originally based on the awesome [Pedestal](https://github.com/pedestal/pedestal/tree/master/route) implementation. `router` selects the most suitable implementation by inspecting the expanded routes. The implementation can be set manually using `:router` ROUTER OPTION. - -| router | description | -| ----------------------|-------------| -| `:linear-router` | Matches the routes one-by-one starting from the top until a match is found. Works with any kind of routes. -| `:lookup-router` | Fastest router, uses hash-lookup to resolve the route. Valid if no paths have path or catch-all parameters. -| `:mixed-router` | Creates internally a `:linear-router` and a `:lookup-router` and used them to effectively get best-of-both-worlds. Valid if there are no CONFLICTING ROUTES. -| `:prefix-tree-router` | [TODO](https://github.com/julienschmidt/httprouter#how-does-it-work) - -The router name can be asked from the router - -```clj -(r/router-name router) -; :mixed-router -``` diff --git a/doc/basics/name_based_routing.md b/doc/basics/name_based_routing.md index 7dcbe60f..68950869 100644 --- a/doc/basics/name_based_routing.md +++ b/doc/basics/name_based_routing.md @@ -1,6 +1,18 @@ -## Name-based routing +## Name-based (reverse) routing -All routes which `:name` route data defined, can be matched by name. +All routes which have `:name` route data defined, can also be matched by name. + +Given a router: + +```clj +(require '[reitit.core :as r]) + +(def router + (r/router + [["/api" + ["/ping" ::ping] + ["/user/:id" ::user]]])) +``` Listing all route names: @@ -9,7 +21,25 @@ Listing all route names: ; [:user/ping :user/user] ``` -Matching by name: +No match returns `nil`: + +```clj +(r/match-by-name router ::kikka) +nil +``` + +Matching a route: + +```clj +(r/match-by-name router ::ping) +; #Match{:template "/api/ping" +; :meta {:name :user/ping} +; :result nil +; :params {} +; :path "/api/ping"} +``` + +If not all path-parameters are set, a `PartialMatch` is returned: ```clj (r/match-by-name router ::user) @@ -23,7 +53,7 @@ Matching by name: ; true ``` -We only got a partial match as we didn't provide the needed path-parameters. Let's provide the them too: +With provided path-parameters: ```clj (r/match-by-name router ::user {:id "1"}) diff --git a/doc/basics/path_based_routing.md b/doc/basics/path_based_routing.md index aeccea00..7f61ef35 100644 --- a/doc/basics/path_based_routing.md +++ b/doc/basics/path_based_routing.md @@ -6,11 +6,27 @@ Path-based routing is done using the `reitit.core/match-by-path` function. It ta * `PartialMatch`, path matched, missing path-parameters (only in reverse-routing) * `Match`, exact match +Given a router: + +```clj +(require '[reitit.core :as r]) + +(def router + (r/router + [["/api" + ["/ping" ::ping] + ["/user/:id" ::user]]])) +``` + +No match returns `nil`: + ```clj (r/match-by-path router "/hello") ; nil ``` +Match provides the route information: + ```clj (r/match-by-path router "/api/user/1") ; #Match{:template "/api/user/:id" diff --git a/doc/advanced/route_conflicts.md b/doc/basics/route_conflicts.md similarity index 61% rename from doc/advanced/route_conflicts.md rename to doc/basics/route_conflicts.md index 41ebab19..d38e2e51 100644 --- a/doc/advanced/route_conflicts.md +++ b/doc/basics/route_conflicts.md @@ -1,13 +1,13 @@ # Route conflicts -Many routing libraries allow single path lookup could match multiple routes. Usually, first match is used. This is not good, especially if route tree is merged from multiple sources - routes might regress to be unreachable without a warning. +Many routing libraries allow multiple matches for a single path lookup. Usually, the first match is used and the rest are effecively unreachanle. This is not good, especially if route tree is merged from multiple sources. -Reitit resolves this by running explicit conflicit resolution when a `Router` is created. Conflicting routes are passed into a `:conflicts` callback. Default implementation throws `ex-info` with a descriptive message. +Reitit resolves this by running explicit conflicit resolution when a `router` is called. Conflicting routes are passed into a `:conflicts` callback. Default implementation throws `ex-info` with a descriptive message. -Examples routes with conflicts: +Examples router with conflicting routes: ```clj -(require '[reitit.core :as reitit]) +(require '[reitit.core :as r]) (def routes [["/ping"] @@ -20,7 +20,7 @@ Examples routes with conflicts: By default, `ExceptionInfo` is thrown: ```clj -(reitit/router routes) +(r/router routes) ; CompilerException clojure.lang.ExceptionInfo: Router contains conflicting routes: ; ; /:user-id/orders @@ -38,7 +38,7 @@ By default, `ExceptionInfo` is thrown: Just logging the conflicts: ```clj -(reitit/router +(r/router routes {:conflicts (comp println reitit/conflicts-str)}) ; Router contains conflicting routes: diff --git a/doc/basics/route_data.md b/doc/basics/route_data.md index 3fc4bd20..9a013956 100644 --- a/doc/basics/route_data.md +++ b/doc/basics/route_data.md @@ -1,6 +1,47 @@ # Route data -Routes can have arbitrary meta-data, interpreted by the router (via it's `:compile` hook) or the application itself. For nested routes, route data is accumulated recursively using [meta-merge](https://github.com/weavejester/meta-merge). By default, it appends collections, but it can be overridden to do `:prepend`, `:replace` or `:displace`. +Route data is the heart of this library. Routes can have any data attachted to them. Data is interpeted either by the client application or the `Router` via it's `:coerce` and `:compile` hooks. This enables co-existence of both [adaptive and principled](https://youtu.be/x9pxbnFC4aQ?t=1907) components. + +Routes can have a non-sequential route argument that is expanded into route data map when a router is created. + +```clj +(require '[reitit.core :as r]) + +(def router + (r/router + [["/ping" ::ping] + ["/pong" identity] + ["/users" {:get {:roles #{:admin} + :handler identity}}]])) +``` + +The expanded route data can be retrieved from a router with `routes` and is returned with `match-by-path` and `match-by-name` in case of a route match. + +```clj +(r/routes router) +; [["/ping" {:name :user/ping}] +; ["/pong" {:handler identity]} +; ["/users" {:get {:roles #{:admin} +; :handler identity}}]] + +(r/match-by-path router "/ping") +; #Match{:template "/ping" +; :meta {:name :user/ping} +; :result nil +; :params {} +; :path "/ping"} + +(r/match-by-name router ::ping) +; #Match{:template "/ping" +; :meta {:name :user/ping} +; :result nil +; :params {} +; :path "/ping"} +``` + +## Nested route data + +For nested route trees, route data is accumulated recursively from root towards leafs using [meta-merge](https://github.com/weavejester/meta-merge). Default behavior for colections is `:append`, but this can be overridden to `:prepend`, `:replace` or `:displace` using the target meta-data. An example router with nested data: @@ -12,41 +53,47 @@ An example router with nested data: ["/admin" {:roles #{:admin}} ["/users" ::users] ["/db" {:interceptors [::db] - :roles ^:replace #{:db-admin}} - ["/:db" {:parameters {:db String}} - ["/drop" ::drop-db] - ["/stats" ::db-stats]]]]])) + :roles ^:replace #{:db-admin}}]]])) ``` Resolved route tree: ```clj -(reitit/routes router) +(r/routes router) ; [["/api/ping" {:interceptors [::api] -; :name ::ping}] +; :name :user/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}]] +; :name ::users} nil] +; ["/api/admin/db" {:interceptors [::api ::db] +; :roles #{:db-admin}}]] ``` -Route data is returned with `Match` and the application can act based on it. + +## Expansion + +By default, `reitit/Expand` protocol is used to expand the route arguments. It expands keywords into `:name` and functions into `:handler` key in the route data map. It's easy to add custom expanders and one can chenge the whole expand implementation via [router options](../advanced/configuring_routers.md). ```clj -(r/match-by-path router "/api/admin/db/users/drop") -; #Match{:template "/api/admin/db/:db/drop" -; :meta {:interceptors [::api ::db] -; :roles #{:db-admin} -; :parameters {:db String} -; :name ::drop-db} +(require '[reitit.core :as r]) + +(def router + (r/router + [["/ping" ::ping] + ["/pong" identity] + ["/users" {:get {:roles #{:admin} + :handler identity}}]])) + +(r/routes router) +; [["/ping" {:name :user/ping}] +; ["/pong" {:handler identity]} +; ["/users" {:get {:roles #{:admin} +; :handler identity}}]] + +(r/match-by-path router "/ping") +; #Match{:template "/ping" +; :meta {:name :user/ping} ; :result nil -; :params {:db "users"} -; :path "/api/admin/db/users/drop"} +; :params {} +; :path "/ping"} ``` diff --git a/doc/basics/route_syntax.md b/doc/basics/route_syntax.md index 7c3a26e1..24b2c23b 100644 --- a/doc/basics/route_syntax.md +++ b/doc/basics/route_syntax.md @@ -1,6 +1,12 @@ # Route Syntax -Raw routes are defined as vectors, which have a String path, optional (non-sequential) route argument and optional child routes. Routes can be wrapped in vectors and lists and `nil` routes are ignored. Paths can have path-parameters (`:id`) or catch-all-parameters (`*path`). +Routes are defined as vectors of String path and optional (non-sequential) route argument child routes. + +Routes can be wrapped in vectors and lists and `nil` routes are ignored. + +Paths can have path-parameters (`:id`) or catch-all-parameters (`*path`). + +### Examples Simple route: @@ -53,7 +59,8 @@ Same routes flattened: ["/api/ping" {:name ::ping}]] ``` -As routes are just data, it's easy to create them programamtically: +### Generating routes +As routes are just data, it's easy to create them programmatically: ```clj (defn cqrs-routes [actions dev-mode?] diff --git a/doc/basics/router.md b/doc/basics/router.md index 55bfce4d..38ecbdda 100644 --- a/doc/basics/router.md +++ b/doc/basics/router.md @@ -1,8 +1,8 @@ # Router -Routes are just data and to do actual routing, we need a Router satisfying the `reitit.core/Router` protocol. Routers are created with `reitit.core/router` function, taking the raw routes and optionally an options map. Raw routes gets expanded and optionally coerced and compiled. +Routes are just data and for routing, we need a router instance satisfying the `reitit.core/Router` protocol. Routers are created with `reitit.core/router` function, taking the raw routes and optionally an options map. -`Router` protocol: +The `Router` protocol: ```clj (defprotocol Router @@ -26,10 +26,25 @@ Creating a router: ["/user/:id" ::user]]])) ``` -Router flattens the raw routes and expands the route arguments using `reitit.core/Expand` protocol. By default, `Keyword`s are expanded to `:name` and functions are expaned to `:handler`. `nil` routes are removed. The expanded routes can be retrieved with router: +Name of the created router: + +```clj +(r/router-name router) +; :mixed-router +``` + +The flattened route tree: ```clj (r/routes router) ; [["/api/ping" {:name :user/ping}] ; ["/api/user/:id" {:name :user/user}]] ``` + +### Behind the scenes +When router is created, the following steps are done: +* route tree is flattened +* route arguments are expanded (via `reitit.core/Expand` protocol) and optionally coerced +* [route conflicts](advanced/route_conflicts.md) are resolved +* actual [router implementation](../advanced/different_routers.md) is selected and created +* optionally route meta-data gets compiled diff --git a/doc/ring/README.md b/doc/ring/README.md index 69ae9e80..9bbe9b59 100644 --- a/doc/ring/README.md +++ b/doc/ring/README.md @@ -2,5 +2,6 @@ * [Ring-router](ring.md) * [Dynamic extensions](dynamic_extensions.md) +* [Data-driven Middleware](data_driven_middleware.md) * [Parameter coercion](parameter_coercion.md) * [Compiling middleware](compiling_middleware.md) diff --git a/doc/ring/compiling_middleware.md b/doc/ring/compiling_middleware.md index da6d442d..13d82756 100644 --- a/doc/ring/compiling_middleware.md +++ b/doc/ring/compiling_middleware.md @@ -1,12 +1,12 @@ # Compiling Middleware -The [meta-data extensions](ring.md#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. +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 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. +Still, we can do 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. Middleware/interceptor can also decide not to mount itself. Why mount a `wrap-enforce-roles` middleware for a route if there are no roles required for it? -To do this we use [middleware records](ring.md#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 enable this we use [middleware records](data_driven_middleware.md) `: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. -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): +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.ring.coercion`](https://github.com/metosin/reitit/blob/master/src/reitit/ring/coercion.cljc): ## Naive @@ -16,7 +16,7 @@ To demonstrate the two approaches, below are response coercion middleware writte (defn wrap-coerce-response "Pluggable response coercion middleware. Expects a :coercion of type `reitit.coercion.protocol/Coercion` - and :responses from route meta, otherwise does not mount." + and :responses from route meta, otherwise will do nothing." [handler] (fn ([request] @@ -27,20 +27,17 @@ To demonstrate the two approaches, below are response coercion middleware writte 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)))) + (let [coercers (response-coercers coercion responses opts)] + (coerce-response coercers request response)) + response))) ([request respond raise] - (let [response (handler request) - method (:request-method request) + (let [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)] + (let [coercers (response-coercers coercion responses opts)] (handler request #(respond (coerce-response coercers request %)))) (handler request respond raise)))))) ``` @@ -69,4 +66,4 @@ To demonstrate the two approaches, below are response coercion middleware writte (handler request #(respond (coerce-response coercers request %)) raise)))))))})) ``` -The `:gen` -version has 50% less code, is easier to reason about and is 2-4x faster on basic perf tests. +The `:gen` -version has 50% less code, is easier to reason about and is twice as faster on basic perf tests. diff --git a/doc/middleware_records.md b/doc/ring/data_driven_middleware.md similarity index 52% rename from doc/middleware_records.md rename to doc/ring/data_driven_middleware.md index 784d0485..572e96ff 100644 --- a/doc/middleware_records.md +++ b/doc/ring/data_driven_middleware.md @@ -1,14 +1,14 @@ -# Middleware Records +# Data-driven 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: +Reitit supports first-class data-driven middleware via `reitit.ring.middleware/Middleware` records, created with `reitit.ring.middleware/create` function. The following keys have special purpose: | 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). +| `:gen` | Middleware compile function, see [compiling middleware](compiling_middleware.md). -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. +When routes are compiled, all middleware are expanded (and optionally compiled) into `Middleware` Records and stored in compilation results for later use (api-docs etc). For actual request processing, they are unwrapped into normal middleware functions and composed together producing zero runtime performance penalty. Middleware expansion is backed by `reitit.middleware/IntoMiddleware` protocol, enabling plain clojure(script) maps to be used. A Record: @@ -32,3 +32,7 @@ As plain map: :wrap (fn [handler] (wrap handler :api))}) ``` + +### TODO + +more! diff --git a/doc/ring/dynamic_extensions.md b/doc/ring/dynamic_extensions.md index 755a382c..4aa7d4c2 100644 --- a/doc/ring/dynamic_extensions.md +++ b/doc/ring/dynamic_extensions.md @@ -1,6 +1,6 @@ # Dynamic extensions -`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. +`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 ad-hoc extensions to the system. Example middleware to guard routes based on user roles: @@ -10,7 +10,7 @@ Example middleware to guard routes based on user roles: (defn wrap-enforce-roles [handler] (fn [{:keys [::roles] :as request}] (let [required (some-> request (ring/get-match) :meta ::roles)] - (if (and (seq required) (not (set/intersection required roles))) + (if (and (seq required) (not (set/subset? required roles))) {:status 403, :body "forbidden"} (handler request))))) ``` diff --git a/doc/ring/parameter_coercion.md b/doc/ring/parameter_coercion.md index 6bd92452..73b73f84 100644 --- a/doc/ring/parameter_coercion.md +++ b/doc/ring/parameter_coercion.md @@ -1,13 +1,13 @@ # Parameter coercion -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). +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-alpha20"] +[org.clojure/clojure "1.9.0-beta2"] [org.clojure/spec.alpha "0.1.123"] -[metosin/spec-tools "0.3.3"] +[metosin/spec-tools "0.4.0"] ``` ### Ring request and response coercion @@ -18,7 +18,9 @@ To use `Coercion` with Ring, one needs to do the following: * `: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. +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`. @@ -28,8 +30,8 @@ If either request or response coercion fails, an descriptive error is thrown. ```clj (require '[reitit.ring :as ring]) -(require '[reitit.coercion :as coercion]) -(require '[reitit.coercion.spec :as spec]) +(require '[reitit.ring.coercion :as coercion]) +(require '[reitit.ring.coercion.spec :as spec]) (def app (ring/ring-handler @@ -56,10 +58,12 @@ If either request or response coercion fails, an descriptive error is thrown. #### 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.coercion :as coercion]) -(require '[reitit.coercion.spec :as spec]) +(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]) diff --git a/doc/ring/ring.md b/doc/ring/ring.md index 8e32c094..0ac7aea7 100644 --- a/doc/ring/ring.md +++ b/doc/ring/ring.md @@ -26,7 +26,7 @@ Applying the handler: ; {:status 200, :body "ok"} ``` -The expanded routes: +The expanded routes shows the compilation results: ```clj (-> app (ring/get-router) (reitit/routes)) @@ -58,7 +58,7 @@ Handler are also looked under request-method keys: `:get`, `:head`, `:patch`, `: ; nil ``` -Reverse routing: +Name-based reverse routing: ```clj (-> app @@ -70,9 +70,9 @@ Reverse routing: # Middleware -Middleware can be added with a `:middleware` key, with 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 value of the following: -1. 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. A middleware and a handler: @@ -96,7 +96,7 @@ App with nested middleware: ["/ping" handler] ["/admin" {:middleware [[wrap :admin]]} ["/db" {:middleware [[wrap :db]] - :delete {:middleware [#(wrap % :delete)] + :delete {:middleware [[wrap :delete]] :handler handler}}]]]))) ``` diff --git a/modules/reitit-ring/src/reitit/ring/coercion.cljc b/modules/reitit-ring/src/reitit/ring/coercion.cljc index 39245b12..e015486f 100644 --- a/modules/reitit-ring/src/reitit/ring/coercion.cljc +++ b/modules/reitit-ring/src/reitit/ring/coercion.cljc @@ -107,7 +107,7 @@ (defn wrap-coerce-parameters "Pluggable request coercion middleware. Expects a :coercion of type `reitit.coercion.protocol/Coercion` - and :parameters from route meta, otherwise does not mount." + and :parameters from route meta, otherwise will do nothing." [handler] (fn ([request]