From 60ec0e9e81c1912db435130f75ef18628c94f93b Mon Sep 17 00:00:00 2001 From: Miikka Koskinen Date: Thu, 14 Sep 2017 16:33:36 +0300 Subject: [PATCH] Move documentation to GitBook --- .gitignore | 4 +- README.md | 708 +-------------------------------- book.json | 13 + doc/README.md | 16 + doc/SUMMARY.md | 16 + doc/compiling_middleware.md | 72 ++++ doc/configuring_routers.md | 15 + doc/parameter_coercion.md | 92 +++++ doc/ring.md | 205 ++++++++++ doc/routing/route_conflicts.md | 24 ++ doc/routing/route_metadata.md | 55 +++ doc/routing/route_syntax.md | 48 +++ doc/routing/routers.md | 85 ++++ doc/validating.md | 92 +++++ 14 files changed, 738 insertions(+), 707 deletions(-) create mode 100644 book.json create mode 100644 doc/README.md create mode 100644 doc/SUMMARY.md create mode 100644 doc/compiling_middleware.md create mode 100644 doc/configuring_routers.md create mode 100644 doc/parameter_coercion.md create mode 100644 doc/ring.md create mode 100644 doc/routing/route_conflicts.md create mode 100644 doc/routing/route_metadata.md create mode 100644 doc/routing/route_syntax.md create mode 100644 doc/routing/routers.md create mode 100644 doc/validating.md diff --git a/.gitignore b/.gitignore index 6a25983e..baf4b6ae 100644 --- a/.gitignore +++ b/.gitignore @@ -8,5 +8,7 @@ pom.xml.asc /.lein-* /.nrepl-port /.nrepl-history -/doc /gh-pages +/node_modules +/package-lock.json +/_book diff --git a/README.md b/README.md index 84cc2dfd..c00e9d60 100644 --- a/README.md +++ b/README.md @@ -17,713 +17,9 @@ Ships with example router for [Ring](#ring). See [Issues](https://github.com/met [![Clojars Project](http://clojars.org/metosin/reitit/latest-version.svg)](http://clojars.org/metosin/reitit) -## Route Syntax +## Documentation -Routes are defined as vectors, which String path, optional (non-vector) route argument and optional child routes. Routes can be wrapped in vectors. - -Simple route: - -```clj -["/ping"] -``` - -Two routes: - -```clj -[["/ping"] - ["/pong"]] -``` - -Routes with meta-data: - -```clj -[["/ping" ::ping] - ["/pong" {:name ::pong}]] -``` - -Routes with path and catch-all parameters: - -```clj -[["/users/:user-id"] - ["/public/*path"]] -``` - -Nested routes with meta-data: - -```clj -["/api" - ["/admin" {:middleware [::admin]} - ["/user" ::user] - ["/db" ::db] - ["/ping" ::ping]] -``` - -Same routes flattened: - -```clj -[["/api/admin/user" {:middleware [::admin], :name ::user} - ["/api/admin/db" {:middleware [::admin], :name ::db} - ["/api/ping" ::ping]] -``` - -## Routing - -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. 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: - -```clj -(require '[reitit.core :as reitit]) - -(def router - (reitit/router - [["/api" - ["/ping" ::ping] - ["/user/:id" ::user]]])) -``` - -`:mixed-router` is created (both static & wild routes are found): - -```clj -(reitit/router-name router) -; :mixed-router -``` - -The expanded routes: - -```clj -(reitit/routes router) -; [["/api/ping" {:name :user/ping}] -; ["/api/user/:id" {:name :user/user}]] -``` - -Route names: - -```clj -(reitit/route-names router) -; [:user/ping :user/user] -``` - -### Path-based routing - -```clj -(reitit/match-by-path router "/hello") -; nil - -(reitit/match-by-path router "/api/user/1") -; #Match{:template "/api/user/:id" -; :meta {:name :user/user} -; :path "/api/user/1" -; :result nil -; :params {:id "1"}} -``` - -### Name-based (reverse) routing - -```clj -(reitit/match-by-name router ::user) -; #PartialMatch{:template "/api/user/:id", -; :meta {:name :user/user}, -; :result nil, -; :params nil, -; :required #{:id}} - -(reitit/partial-match? (reitit/match-by-name router ::user)) -; true -``` - -Only a partial match. Let's provide the path-parameters: - -```clj -(reitit/match-by-name router ::user {:id "1"}) -; #Match{:template "/api/user/:id" -; :meta {:name :user/user} -; :path "/api/user/1" -; :result nil -; :params {:id "1"}} -``` - -There is also a exception throwing version: - -```clj -(reitit/match-by-name! router ::user) -; ExceptionInfo missing path-params for route /api/user/:id: #{:id} -``` - -## Route meta-data - -Routes can have arbitrary meta-data. For nested routes, the meta-data is accumulated from root towards leafs using [meta-merge](https://github.com/weavejester/meta-merge). - -A router based on nested route tree: - -```clj -(def router - (reitit/router - ["/api" {:interceptors [::api]} - ["/ping" ::ping] - ["/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" {: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/users") -; #Match{:template "/api/admin/users" -; :meta {:interceptors [::api] -; :roles #{:admin} -; :name ::users} -; :result nil -; :params {} -; :path "/api/admin/users"} -``` - -On match, route meta-data is returned and can interpreted by the application. - -Routers also support meta-data compilation enabling things like fast [Ring](https://github.com/ring-clojure/ring) or [Pedestal](http://pedestal.io/) -style handlers. Compilation results are found under `:result` in the match. See [configuring routers](#configuring-routers) for details. - -## Route conflicts - -Route trees should not have multiple routes that match to a single (request) path. `router` checks the route tree at creation for conflicts and calls a registered `:conflicts` option callback with the found conflicts. Default implementation throws `ex-info` with a descriptive message. - -```clj -(reitit/router - [["/ping"] - ["/:user-id/orders"] - ["/bulk/:bulk-id"] - ["/public/*path"] - ["/:version/status"]]) -; CompilerException clojure.lang.ExceptionInfo: router contains conflicting routes: -; -; /:user-id/orders -; -> /public/*path -; -> /bulk/:bulk-id -; -; /bulk/:bulk-id -; -> /:version/status -; -; /public/*path -; -> /:version/status -; -``` - -## Ring - -[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: - -```clj -(require '[reitit.ring :as ring]) - -(defn handler [_] - {:status 200, :body "ok"}) - -(def app - (ring/ring-handler - (ring/router - ["/ping" handler]))) -``` - -Applying the handler: - -```clj -(app {:request-method :get, :uri "/favicon.ico"}) -; nil - -(app {:request-method :get, :uri "/ping"}) -; {: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 - -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 -(def app - (ring/ring-handler - (ring/router - ["/ping" {:name ::ping - :get handler - :post handler}]))) - -(app {:request-method :get, :uri "/ping"}) -; {:status 200, :body "ok"} - -(app {:request-method :put, :uri "/ping"}) -; nil -``` - -Reverse routing: - -```clj -(-> app - (ring/get-router) - (reitit/match-by-name ::ping) - :path) -; "/ping" -``` - -### Middleware - -Middleware can be added with a `:middleware` key, with a vector value of the following: - -1. 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: - -```clj -(defn wrap [handler id] - (fn [request] - (handler (update request ::acc (fnil conj []) id)))) - -(defn handler [{:keys [::acc]}] - {:status 200, :body (conj acc :handler)}) -``` - -App with nested middleware: - -```clj -(def app - (ring/ring-handler - (ring/router - ["/api" {:middleware [#(wrap % :api)]} - ["/ping" handler] - ["/admin" {:middleware [[wrap :admin]]} - ["/db" {:middleware [[wrap :db]] - :delete {:middleware [#(wrap % :delete)] - :handler handler}}]]]))) -``` - -Middleware is applied correctly: - -```clj -(app {:request-method :delete, :uri "/api/ping"}) -; {:status 200, :body [:api :handler]} -``` - -```clj -(app {:request-method :delete, :uri "/api/admin/db"}) -; {:status 200, :body [:api :admin :db :delete :handler]} -``` - -### Middleware Records - -Reitit supports first-class data-driven middleware via `reitit.middleware/Middleware` records, created with `reitit.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). - -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 -(require '[reitit.middleware :as middleware]) - -(def wrap2 - (middleware/create - {:name ::wrap2 - :description "a nice little mw, takes 1 arg." - :wrap wrap})) -``` - -As plain map: - -```clj -;; plain map -(def wrap3 - {:name ::wrap3 - :description "a nice little mw, :api as arg" - :wrap (fn [handler] - (wrap handler :api))}) -``` - -### Async Ring - -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 - -`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. - -Example middleware to guard routes based on user roles: - -```clj -(require '[clojure.set :as set]) - -(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))) - {:status 403, :body "forbidden"} - (handler request))))) -``` - -Mounted to an app via router meta-data (effecting all routes): - -```clj -(def handler (constantly {:status 200, :body "ok"})) - -(def app - (ring/ring-handler - (ring/router - [["/api" - ["/ping" handler] - ["/admin" {::roles #{:admin}} - ["/ping" handler]]]] - {:meta {:middleware [wrap-enforce-roles]}}))) -``` - -Anonymous access to public route: - -```clj -(app {:request-method :get, :uri "/api/ping"}) -; {:status 200, :body "ok"} -``` - -Anonymous access to guarded route: - -```clj -(app {:request-method :get, :uri "/api/admin/ping"}) -; {:status 403, :body "forbidden"} -``` - -Authorized access to guarded route: - -```clj -(app {:request-method :get, :uri "/api/admin/ping", ::roles #{:admin}}) -; {:status 200, :body "ok"} -``` - -## 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). - -**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/spec.alpha "0.1.123"] -[metosin/spec-tools "0.3.3"] -``` - -### 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 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 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`. 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`. These are the actual codes are from [`reitit.coercion`](https://github.com/metosin/reitit/blob/master/src/reitit/coercion.cljc): - -### 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 :responses 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/create - {:name ::coerce-response - :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)))))))})) -``` - -The `:gen` -version has 50% less code, is easier to reason about and is 2-4x faster on basic perf tests. - -## Merging route-trees - -*TODO* - -## Validating route-trees - -Namespace `reitit.spec` contains [specs](https://clojure.org/about/spec) for routes, router and router options. - -To enable spec-validation of `router` inputs & outputs at development time, one can do the following: - -```clj -; add to dependencies: -; [expound "0.3.0"] - -(require '[clojure.spec.test.alpha :as st]) -(require '[expound.alpha :as expound]) -(require '[clojure.spec.alpha :as s]) -(require '[reitit.spec]) - -(st/instrument `reitit/router) -(set! s/*explain-out* expound/printer) - -(reitit/router - ["/api" - ["/publuc" - ["/ping"] - ["pong"]]]) -; -- Spec failed -------------------- -; -; ["/api" ...] -; ^^^^^^ -; -; should satisfy -; -; (clojure.spec.alpha/cat -; :path -; :reitit.spec/path -; :arg -; (clojure.spec.alpha/? :reitit.spec/arg) -; :childs -; (clojure.spec.alpha/* (clojure.spec.alpha/and :reitit.spec/raw-route))) -; -; -- Relevant specs ------- -; -; :reitit.spec/raw-route: -; (clojure.spec.alpha/cat -; :path -; :reitit.spec/path -; :arg -; (clojure.spec.alpha/? :reitit.spec/arg) -; :childs -; (clojure.spec.alpha/* (clojure.spec.alpha/and :reitit.spec/raw-route))) -; :reitit.spec/raw-routes: -; (clojure.spec.alpha/or -; :route -; :reitit.spec/raw-route -; :routes -; (clojure.spec.alpha/coll-of :reitit.spec/raw-route :into [])) -; -; -- Spec failed -------------------- -; -; [... [... ... ["pong"]]] -; ^^^^^^ -; -; should satisfy -; -; (fn [%] (clojure.string/starts-with? % "/")) -; -; -- Relevant specs ------- -; -; :reitit.spec/path: -; (clojure.spec.alpha/and -; clojure.core/string? -; (clojure.core/fn [%] (clojure.string/starts-with? % "/"))) -; :reitit.spec/raw-route: -; (clojure.spec.alpha/cat -; :path -; :reitit.spec/path -; :arg -; (clojure.spec.alpha/? :reitit.spec/arg) -; :childs -; (clojure.spec.alpha/* (clojure.spec.alpha/and :reitit.spec/raw-route))) -; :reitit.spec/raw-routes: -; (clojure.spec.alpha/or -; :route -; :reitit.spec/raw-route -; :routes -; (clojure.spec.alpha/coll-of :reitit.spec/raw-route :into [])) -; -; ------------------------- -; Detected 2 errors -``` - -### Validating meta-data - -*TODO* - -## Swagger & Openapi - -*TODO* - -## Interceptors - -*TODO* - -## Configuring Routers - -Routers can be configured via options. Options allow things like [`clojure.spec`](https://clojure.org/about/spec) validation for meta-data and fast, compiled handlers. The following options are available for the `reitit.core/router`: - - | key | description | - | -------------|-------------| - | `:path` | Base-path for routes - | `:routes` | Initial resolved routes (default `[]`) - | `:meta` | Initial expanded route-meta vector (default `[]`) - | `:expand` | Function of `arg opts => meta` to expand route arg to route meta-data (default `reitit.core/expand`) - | `:coerce` | Function of `route opts => route` to coerce resolved route, can throw or return `nil` - | `:compile` | Function of `route opts => result` to compile a route handler - | `:conflicts` | Function of `{route #{route}} => side-effect` to handle conflicting routes (default `reitit.core/throw-on-conflicts!`) - | `:router` | Function of `routes opts => router` to override the actual router implementation +[Check out the full documentation!](https://metosin.github.io/reitit/) ## Special thanks diff --git a/book.json b/book.json new file mode 100644 index 00000000..699430ea --- /dev/null +++ b/book.json @@ -0,0 +1,13 @@ +{ + "root": "doc", + "plugins": ["editlink", "github"], + "pluginsConfig": { + "editlink": { + "base": "https://github.com/metosin/reitit/tree/master/doc", + "label": "Edit This Page" + }, + "github": { + "url": "https://github.com/metosin/reitit" + } + } +} diff --git a/doc/README.md b/doc/README.md new file mode 100644 index 00000000..128c34a0 --- /dev/null +++ b/doc/README.md @@ -0,0 +1,16 @@ +# reitit + +[reitit](https://github.com/metosin/reitit) is a friendly data-driven router for Clojure(Script). + +* Simple data-driven [route syntax](./routing/route_syntax.md) +* First-class [route meta-data](./routing/route_metadata.md) +* Generic, not tied to HTTP +* [Route conflict resolution](./routing/route_conflicts.md) +* [Pluggable coercion](./parameter-coercion.md) ([clojure.spec](https://clojure.org/about/spec)) +* both [Middleware](./ring.md#middleware) & Interceptors +* Extendable +* Fast + +## Latest version + +[![Clojars Project](http://clojars.org/metosin/reitit/latest-version.svg)](http://clojars.org/metosin/reitit) diff --git a/doc/SUMMARY.md b/doc/SUMMARY.md new file mode 100644 index 00000000..8a4d5e40 --- /dev/null +++ b/doc/SUMMARY.md @@ -0,0 +1,16 @@ +# Summary + +* [Introduction](README.md) +* Routing + * [Route syntax](routing/route_syntax.md) + * [Routers](routing/routers.md) + * [Route metadata](routing/route_metadata.md) + * [Route conflicts](routing/route_conflicts.md) +* [Ring support](ring.md) +* [Parameter coercion](parameter_coercion.md) +* [Compiling middleware](compiling_middleware.md) +* [Validating route-trees](validating.md) +* [Configuring routers](configuring_routers.md) +* TODO: Merging route-trees +* TODO: Swagger & OpenAPI +* TODO: Interceptors diff --git a/doc/compiling_middleware.md b/doc/compiling_middleware.md new file mode 100644 index 00000000..fc83bddf --- /dev/null +++ b/doc/compiling_middleware.md @@ -0,0 +1,72 @@ +## Compiling Middleware + +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 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`. 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`. These are the actual codes are from [`reitit.coercion`](https://github.com/metosin/reitit/blob/master/src/reitit/coercion.cljc): + +### 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 :responses 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/create + {:name ::coerce-response + :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)))))))})) +``` + +The `:gen` -version has 50% less code, is easier to reason about and is 2-4x faster on basic perf tests. diff --git a/doc/configuring_routers.md b/doc/configuring_routers.md new file mode 100644 index 00000000..af3fbfa7 --- /dev/null +++ b/doc/configuring_routers.md @@ -0,0 +1,15 @@ +## Configuring Routers + +Routers can be configured via options. Options allow things like [`clojure.spec`](https://clojure.org/about/spec) validation for meta-data and fast, compiled handlers. The following options are available for the `reitit.core/router`: + + | key | description | + | -------------|-------------| + | `:path` | Base-path for routes + | `:routes` | Initial resolved routes (default `[]`) + | `:meta` | Initial expanded route-meta vector (default `[]`) + | `:expand` | Function of `arg opts => meta` to expand route arg to route meta-data (default `reitit.core/expand`) + | `:coerce` | Function of `route opts => route` to coerce resolved route, can throw or return `nil` + | `:compile` | Function of `route opts => result` to compile a route handler + | `:conflicts` | Function of `{route #{route}} => side-effect` to handle conflicting routes (default `reitit.core/throw-on-conflicts!`) + | `:router` | Function of `routes opts => router` to override the actual router implementation + diff --git a/doc/parameter_coercion.md b/doc/parameter_coercion.md new file mode 100644 index 00000000..6bd92452 --- /dev/null +++ b/doc/parameter_coercion.md @@ -0,0 +1,92 @@ +# 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). + +**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/spec.alpha "0.1.123"] +[metosin/spec-tools "0.3.3"] +``` + +### 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}} +``` diff --git a/doc/ring.md b/doc/ring.md new file mode 100644 index 00000000..d361d4fa --- /dev/null +++ b/doc/ring.md @@ -0,0 +1,205 @@ +# Ring support + +[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: + +```clj +(require '[reitit.ring :as ring]) + +(defn handler [_] + {:status 200, :body "ok"}) + +(def app + (ring/ring-handler + (ring/router + ["/ping" handler]))) +``` + +Applying the handler: + +```clj +(app {:request-method :get, :uri "/favicon.ico"}) +; nil + +(app {:request-method :get, :uri "/ping"}) +; {: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 + +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 +(def app + (ring/ring-handler + (ring/router + ["/ping" {:name ::ping + :get handler + :post handler}]))) + +(app {:request-method :get, :uri "/ping"}) +; {:status 200, :body "ok"} + +(app {:request-method :put, :uri "/ping"}) +; nil +``` + +Reverse routing: + +```clj +(-> app + (ring/get-router) + (reitit/match-by-name ::ping) + :path) +; "/ping" +``` + +### Middleware + +Middleware can be added with a `:middleware` key, with a vector value of the following: + +1. 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: + +```clj +(defn wrap [handler id] + (fn [request] + (handler (update request ::acc (fnil conj []) id)))) + +(defn handler [{:keys [::acc]}] + {:status 200, :body (conj acc :handler)}) +``` + +App with nested middleware: + +```clj +(def app + (ring/ring-handler + (ring/router + ["/api" {:middleware [#(wrap % :api)]} + ["/ping" handler] + ["/admin" {:middleware [[wrap :admin]]} + ["/db" {:middleware [[wrap :db]] + :delete {:middleware [#(wrap % :delete)] + :handler handler}}]]]))) +``` + +Middleware is applied correctly: + +```clj +(app {:request-method :delete, :uri "/api/ping"}) +; {:status 200, :body [:api :handler]} +``` + +```clj +(app {:request-method :delete, :uri "/api/admin/db"}) +; {:status 200, :body [:api :admin :db :delete :handler]} +``` + +### Middleware Records + +Reitit supports first-class data-driven middleware via `reitit.middleware/Middleware` records, created with `reitit.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). + +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 +(require '[reitit.middleware :as middleware]) + +(def wrap2 + (middleware/create + {:name ::wrap2 + :description "a nice little mw, takes 1 arg." + :wrap wrap})) +``` + +As plain map: + +```clj +;; plain map +(def wrap3 + {:name ::wrap3 + :description "a nice little mw, :api as arg" + :wrap (fn [handler] + (wrap handler :api))}) +``` + +### Async Ring + +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 + +`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. + +Example middleware to guard routes based on user roles: + +```clj +(require '[clojure.set :as set]) + +(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))) + {:status 403, :body "forbidden"} + (handler request))))) +``` + +Mounted to an app via router meta-data (effecting all routes): + +```clj +(def handler (constantly {:status 200, :body "ok"})) + +(def app + (ring/ring-handler + (ring/router + [["/api" + ["/ping" handler] + ["/admin" {::roles #{:admin}} + ["/ping" handler]]]] + {:meta {:middleware [wrap-enforce-roles]}}))) +``` + +Anonymous access to public route: + +```clj +(app {:request-method :get, :uri "/api/ping"}) +; {:status 200, :body "ok"} +``` + +Anonymous access to guarded route: + +```clj +(app {:request-method :get, :uri "/api/admin/ping"}) +; {:status 403, :body "forbidden"} +``` + +Authorized access to guarded route: + +```clj +(app {:request-method :get, :uri "/api/admin/ping", ::roles #{:admin}}) +; {:status 200, :body "ok"} +``` diff --git a/doc/routing/route_conflicts.md b/doc/routing/route_conflicts.md new file mode 100644 index 00000000..535827be --- /dev/null +++ b/doc/routing/route_conflicts.md @@ -0,0 +1,24 @@ +## Route conflicts + +Route trees should not have multiple routes that match to a single (request) path. `router` checks the route tree at creation for conflicts and calls a registered `:conflicts` option callback with the found conflicts. Default implementation throws `ex-info` with a descriptive message. + +```clj +(reitit/router + [["/ping"] + ["/:user-id/orders"] + ["/bulk/:bulk-id"] + ["/public/*path"] + ["/:version/status"]]) +; CompilerException clojure.lang.ExceptionInfo: router contains conflicting routes: +; +; /:user-id/orders +; -> /public/*path +; -> /bulk/:bulk-id +; +; /bulk/:bulk-id +; -> /:version/status +; +; /public/*path +; -> /:version/status +; +``` diff --git a/doc/routing/route_metadata.md b/doc/routing/route_metadata.md new file mode 100644 index 00000000..621eba25 --- /dev/null +++ b/doc/routing/route_metadata.md @@ -0,0 +1,55 @@ +# Route meta-data + +Routes can have arbitrary meta-data. For nested routes, the meta-data is accumulated from root towards leafs using [meta-merge](https://github.com/weavejester/meta-merge). + +A router based on nested route tree: + +```clj +(def router + (reitit/router + ["/api" {:interceptors [::api]} + ["/ping" ::ping] + ["/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" {: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/users") +; #Match{:template "/api/admin/users" +; :meta {:interceptors [::api] +; :roles #{:admin} +; :name ::users} +; :result nil +; :params {} +; :path "/api/admin/users"} +``` + +On match, route meta-data is returned and can interpreted by the application. + +Routers also support meta-data compilation enabling things like fast [Ring](https://github.com/ring-clojure/ring) or [Pedestal](http://pedestal.io/) -style handlers. Compilation results are found under `:result` in the match. See [configuring routers](#configuring-routers) for details. diff --git a/doc/routing/route_syntax.md b/doc/routing/route_syntax.md new file mode 100644 index 00000000..29cfc5fb --- /dev/null +++ b/doc/routing/route_syntax.md @@ -0,0 +1,48 @@ +# Route Syntax + +Routes are defined as vectors, which String path, optional (non-vector) route argument and optional child routes. Routes can be wrapped in vectors. + +Simple route: + +```clj +["/ping"] +``` + +Two routes: + +```clj +[["/ping"] + ["/pong"]] +``` + +Routes with meta-data: + +```clj +[["/ping" ::ping] + ["/pong" {:name ::pong}]] +``` + +Routes with path and catch-all parameters: + +```clj +[["/users/:user-id"] + ["/public/*path"]] +``` + +Nested routes with meta-data: + +```clj +["/api" + ["/admin" {:middleware [::admin]} + ["/user" ::user] + ["/db" ::db] + ["/ping" ::ping]] +``` + +Same routes flattened: + +```clj +[["/api/admin/user" {:middleware [::admin], :name ::user} + ["/api/admin/db" {:middleware [::admin], :name ::db} + ["/api/ping" ::ping]] +``` diff --git a/doc/routing/routers.md b/doc/routing/routers.md new file mode 100644 index 00000000..4d4c224b --- /dev/null +++ b/doc/routing/routers.md @@ -0,0 +1,85 @@ +# Routers + +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. 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: + +```clj +(require '[reitit.core :as reitit]) + +(def router + (reitit/router + [["/api" + ["/ping" ::ping] + ["/user/:id" ::user]]])) +``` + +`:mixed-router` is created (both static & wild routes are found): + +```clj +(reitit/router-name router) +; :mixed-router +``` + +The expanded routes: + +```clj +(reitit/routes router) +; [["/api/ping" {:name :user/ping}] +; ["/api/user/:id" {:name :user/user}]] +``` + +Route names: + +```clj +(reitit/route-names router) +; [:user/ping :user/user] +``` + +### Path-based routing + +```clj +(reitit/match-by-path router "/hello") +; nil + +(reitit/match-by-path router "/api/user/1") +; #Match{:template "/api/user/:id" +; :meta {:name :user/user} +; :path "/api/user/1" +; :result nil +; :params {:id "1"}} +``` + +### Name-based (reverse) routing + +```clj +(reitit/match-by-name router ::user) +; #PartialMatch{:template "/api/user/:id", +; :meta {:name :user/user}, +; :result nil, +; :params nil, +; :required #{:id}} + +(reitit/partial-match? (reitit/match-by-name router ::user)) +; true +``` + +Only a partial match. Let's provide the path-parameters: + +```clj +(reitit/match-by-name router ::user {:id "1"}) +; #Match{:template "/api/user/:id" +; :meta {:name :user/user} +; :path "/api/user/1" +; :result nil +; :params {:id "1"}} +``` + +There is also a exception throwing version: + +```clj +(reitit/match-by-name! router ::user) +; ExceptionInfo missing path-params for route /api/user/:id: #{:id} +``` diff --git a/doc/validating.md b/doc/validating.md new file mode 100644 index 00000000..f73e28ca --- /dev/null +++ b/doc/validating.md @@ -0,0 +1,92 @@ +# Validating route-trees + +Namespace `reitit.spec` contains [specs](https://clojure.org/about/spec) for routes, router and router options. + +To enable spec-validation of `router` inputs & outputs at development time, one can do the following: + +```clj +; add to dependencies: +; [expound "0.3.0"] + +(require '[clojure.spec.test.alpha :as st]) +(require '[expound.alpha :as expound]) +(require '[clojure.spec.alpha :as s]) +(require '[reitit.spec]) + +(st/instrument `reitit/router) +(set! s/*explain-out* expound/printer) + +(reitit/router + ["/api" + ["/publuc" + ["/ping"] + ["pong"]]]) +; -- Spec failed -------------------- +; +; ["/api" ...] +; ^^^^^^ +; +; should satisfy +; +; (clojure.spec.alpha/cat +; :path +; :reitit.spec/path +; :arg +; (clojure.spec.alpha/? :reitit.spec/arg) +; :childs +; (clojure.spec.alpha/* (clojure.spec.alpha/and :reitit.spec/raw-route))) +; +; -- Relevant specs ------- +; +; :reitit.spec/raw-route: +; (clojure.spec.alpha/cat +; :path +; :reitit.spec/path +; :arg +; (clojure.spec.alpha/? :reitit.spec/arg) +; :childs +; (clojure.spec.alpha/* (clojure.spec.alpha/and :reitit.spec/raw-route))) +; :reitit.spec/raw-routes: +; (clojure.spec.alpha/or +; :route +; :reitit.spec/raw-route +; :routes +; (clojure.spec.alpha/coll-of :reitit.spec/raw-route :into [])) +; +; -- Spec failed -------------------- +; +; [... [... ... ["pong"]]] +; ^^^^^^ +; +; should satisfy +; +; (fn [%] (clojure.string/starts-with? % "/")) +; +; -- Relevant specs ------- +; +; :reitit.spec/path: +; (clojure.spec.alpha/and +; clojure.core/string? +; (clojure.core/fn [%] (clojure.string/starts-with? % "/"))) +; :reitit.spec/raw-route: +; (clojure.spec.alpha/cat +; :path +; :reitit.spec/path +; :arg +; (clojure.spec.alpha/? :reitit.spec/arg) +; :childs +; (clojure.spec.alpha/* (clojure.spec.alpha/and :reitit.spec/raw-route))) +; :reitit.spec/raw-routes: +; (clojure.spec.alpha/or +; :route +; :reitit.spec/raw-route +; :routes +; (clojure.spec.alpha/coll-of :reitit.spec/raw-route :into [])) +; +; ------------------------- +; Detected 2 errors +``` + +### Validating meta-data + +*TODO*