From eb22bae047a76208eb7b96f4aff202a0fee84bcd Mon Sep 17 00:00:00 2001 From: Tommi Reiman Date: Mon, 18 Sep 2017 08:30:03 +0300 Subject: [PATCH] Re-organize docs --- doc/README.md | 129 ++++++++++++++++- doc/SUMMARY.md | 27 ++-- doc/basics.md | 254 +++++++++++++++++++++++++++++++++ doc/compiling_middleware.md | 6 +- doc/dynamic_extensions.md | 52 +++++++ doc/middleware_records.md | 34 +++++ doc/ring.md | 98 +------------ doc/route_conflicts.md | 56 ++++++++ doc/route_validation.md | 172 ++++++++++++++++++++++ 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, 715 insertions(+), 417 deletions(-) create mode 100644 doc/basics.md create mode 100644 doc/dynamic_extensions.md create mode 100644 doc/middleware_records.md create mode 100644 doc/route_conflicts.md create mode 100644 doc/route_validation.md delete mode 100644 doc/routing/route_conflicts.md delete mode 100644 doc/routing/route_metadata.md delete mode 100644 doc/routing/route_syntax.md delete mode 100644 doc/routing/routers.md delete mode 100644 doc/validating.md diff --git a/doc/README.md b/doc/README.md index 128c34a0..9b9e1b02 100644 --- a/doc/README.md +++ b/doc/README.md @@ -1,16 +1,133 @@ -# reitit +# Introduction -[reitit](https://github.com/metosin/reitit) is a friendly data-driven router for Clojure(Script). +[Reitit](https://github.com/metosin/reitit) is a small Clojure(Script) library for data-driven routing. * Simple data-driven [route syntax](./routing/route_syntax.md) * First-class [route meta-data](./routing/route_metadata.md) -* Generic, not tied to HTTP +* Bi-directional-routing * [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 +* Both [Middleware](./ring.md#middleware) & Interceptors * Extendable * Fast -## Latest version +To use Reitit, add the following dependecy to your project: -[![Clojars Project](http://clojars.org/metosin/reitit/latest-version.svg)](http://clojars.org/metosin/reitit) +```clj +[metosin/reitit "0.1.0-SNAPSHOT"] +``` + +# Examples + +## Simple router + +```clj +(require '[reitit.core :as r]) + +(def router + (r/router + [["/api/ping" ::ping] + ["/api/orders/:id" ::order-by-id]])) +``` + +Routing: + +```clj +(r/match-by-path router "/api/ipa") +; nil + +(r/match-by-path router "/api/ping") +; #Match{:template "/api/ping" +; :meta {:name ::ping} +; :result nil +; :params {} +; :path "/api/ping"} + +(r/match-by-path router "/api/orders/1") +; #Match{:template "/api/orders/:id" +; :meta {:name ::order-by-id} +; :result nil +; :params {:id "1"} +; :path "/api/orders/1"} +``` + +Reverse-routing: + +```clj +(r/match-by-name router ::ipa) +; nil + +(r/match-by-name router ::ping) +; #Match{:template "/api/ping" +; :meta {:name ::ping} +; :result nil +; :params {} +; :path "/api/ping"} + +(r/match-by-name router ::order-by-id) +; #PartialMatch{:template "/api/orders/:id" +; :meta {:name :user/order-by-id} +; :result nil +; :params nil +; :required #{:id}} + +(r/partial-match? (r/match-by-name router ::order-by-id)) +; true + +(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"} +``` + +## Ring-router + +Ring-router adds support for `:handler` functions, `:middleware` and routing based on `:request-method`. It also supports pluggable parameter coercion (`clojure.spec`), data-driven middleware, route and middleware compilation, dynamic extensions and more. + +```clj +(require '[reitit.ring :as ring]) + +(def handler [_] + {:status 200, :body "ok"}) + +(defn wrap [handler id] + (fn [request] + (update (handler request) :wrap (fnil conj '()) id))) + +(def app + (ring/ring-handler + (ring/router + ["/api" {:middleware [[wrap :api]]} + ["/ping" {:get handler + :name ::ping}] + ["/admin" {:middleware [[wrap :admin]]} + ["/users" {:get handler + :post handler}]]]))) +``` + +Routing: + +```clj +(app {:request-method :get, :uri "/api/admin/users"}) +; {:status 200, :body "ok", :wrap (:api :admin} + +(app {:request-method :put, :uri "/api/admin/users"}) +; nil +``` + +Reverse-routing: + +```clj +(require '[reitit.core :as r]) + +(-> app (ring/get-router) (r/match-by-name ::ping)) +; #Match{:template "/api/ping" +; :meta {:middleware [[#object[user$wrap] :api]] +; :get {:handler #object[user$handler]} +; :name ::ping} +; :result #Methods{...} +; :params nil +; :path "/api/ping"} +``` diff --git a/doc/SUMMARY.md b/doc/SUMMARY.md index 8a4d5e40..1bdc0159 100644 --- a/doc/SUMMARY.md +++ b/doc/SUMMARY.md @@ -1,16 +1,21 @@ # 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 +* Basics + * [Route syntax](basics.md#route-syntax) + * [Router](basics.md#router) + * [Path-based Routing](basics.md#path-based-routing) + * [Name-based Routing](basics.md#name-based-routing) + * [Route data](basics.md#route-data) + * [Different Routers](basics.md#different-routers) +* Advanced + * [Route conflicts](route_conflicts.md) + * [Route Validation](route_validation.md) + * [Configuring routers](configuring_routers.md) +* Ring + * [Ring-router](ring.md) + * [Dynamic extensions](dynamic_extensions.md) + * [Parameter coercion](parameter_coercion.md) + * [Compiling middleware](compiling_middleware.md) * TODO: Swagger & OpenAPI * TODO: Interceptors diff --git a/doc/basics.md b/doc/basics.md new file mode 100644 index 00000000..3a7ae421 --- /dev/null +++ b/doc/basics.md @@ -0,0 +1,254 @@ +# 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`). + +Simple route: + +```clj +["/ping"] +``` + +Two routes: + +```clj +[["/ping"] + ["/pong"]] +``` + +Routes with route arguments: + +```clj +[["/ping" ::ping] + ["/pong" {:name ::pong}]] +``` + +Routes with path parameters: + +```clj +[["/users/:user-id"] + ["/api/:version/ping"]] +``` + +Route with catch-all parameter: + +```clj +["/public/*path"] +``` + +Nested routes: + +```clj +["/api" + ["/admin" {:middleware [::admin]} + ["" ::admin] + ["/db" ::db]] + ["/ping" ::ping]] +``` + +Same routes flattened: + +```clj +[["/api/admin" {:middleware [::admin], :name ::admin}] + ["/api/admin/db" {:middleware [::admin], :name ::db}] + ["/api/ping" {:name ::ping}]] +``` + +As routes are just data, it's easy to create them programamtically: + +```clj +(defn cqrs-routes [actions dev-mode?] + ["/api" {:interceptors [::api ::db]} + (for [[type interceptor] actions + :let [path (str "/" (name interceptor)) + method (condp = type + :query :get + :command :post)]] + [path {method {:interceptors [interceptor]}}]) + (if dev-mode? ["/dev-tools" ::dev-tools])]) +``` + +```clj +(cqrs-routes + [[:query 'get-user] + [:command 'add-user] + [:command 'add-order]] + false) +; ["/api" {:interceptors [::api ::db]} +; (["/get-user" {:get {:interceptors [get-user]}}] +; ["/add-user" {:post {:interceptors [add-user]}}] +; ["/add-order" {:post {:interceptors [add-order]}}]) +; nil] +``` + + +# 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. + +`Router` protocol: + +```clj +(defprotocol Router + (router-name [this]) + (routes [this]) + (options [this]) + (route-names [this]) + (match-by-path [this path]) + (match-by-name [this name] [this name params])) +``` + +Creating a router: + +```clj +(require '[reitit.core :as r]) + +(def router + (r/router + [["/api" + ["/ping" ::ping] + ["/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: + +```clj +(r/routes router) +; [["/api/ping" {:name :user/ping}] +; ["/api/user/:id" {:name :user/user}]] +``` + +## Path-based routing + +Path-based routing is done using the `reitit.core/match-by-path` function. It takes the router and path as arguments and returns one of the following: + +* `nil`, no match +* `PartialMatch`, path matched, missing path-parameters (only in reverse-routing) +* `Match`, exact match + +```clj +(r/match-by-path router "/hello") +; nil +``` + +```clj +(r/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 routing + +All routes which `:name` route data defined, can be matched by name. + +Listing all route names: + +```clj +(r/route-names router) +; [:user/ping :user/user] +``` + +Matching by name: + +```clj +(r/match-by-name router ::user) +; #PartialMatch{:template "/api/user/:id", +; :meta {:name :user/user}, +; :result nil, +; :params nil, +; :required #{:id}} + +(r/partial-match? (r/match-by-name router ::user)) +; true +``` + +We only got a partial match as we didn't provide the needed path-parameters. Let's provide the them too: + +```clj +(r/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 +(r/match-by-name! router ::user) +; ExceptionInfo missing path-params for route /api/user/:id: #{:id} +``` + +# 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`. + +An example router with nested data: + +```clj +(def router + (r/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}]] +``` + +Route data is returned with `Match` and the application can act based on it. + +```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} +; :result nil +; :params {:db "users"} +; :path "/api/admin/db/users/drop"} +``` + +# 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/compiling_middleware.md b/doc/compiling_middleware.md index fd64a103..da6d442d 100644 --- a/doc/compiling_middleware.md +++ b/doc/compiling_middleware.md @@ -1,4 +1,4 @@ -## Compiling Middleware +# 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. @@ -8,7 +8,7 @@ To do this we use [middleware records](ring.md#middleware-records) `:gen` hook i To demonstrate the two approaches, below are response coercion middleware written as normal ring middleware function and as middleware record with `:gen`. These are the actual codes are from [`reitit.coercion`](https://github.com/metosin/reitit/blob/master/src/reitit/coercion.cljc): -### Naive +## Naive * Extracts the compiled route information on every request. @@ -45,7 +45,7 @@ To demonstrate the two approaches, below are response coercion middleware writte (handler request respond raise)))))) ``` -### Compiled +## Compiled * Route information is provided via a closure * Pre-compiled coercers diff --git a/doc/dynamic_extensions.md b/doc/dynamic_extensions.md new file mode 100644 index 00000000..755a382c --- /dev/null +++ b/doc/dynamic_extensions.md @@ -0,0 +1,52 @@ +# 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. + +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/middleware_records.md b/doc/middleware_records.md new file mode 100644 index 00000000..784d0485 --- /dev/null +++ b/doc/middleware_records.md @@ -0,0 +1,34 @@ +# 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))}) +``` diff --git a/doc/ring.md b/doc/ring.md index d361d4fa..8e32c094 100644 --- a/doc/ring.md +++ b/doc/ring.md @@ -1,6 +1,6 @@ -# Ring support +# Ring Router -[Ring](https://github.com/ring-clojure/ring)-router adds support for ring concepts like [handlers](https://github.com/ring-clojure/ring/wiki/Concepts#handlers), [middleware](https://github.com/ring-clojure/ring/wiki/Concepts#middleware) and routing based on `:request-method`. Ring-router is created with `reitit.ring/router` function. It runs a custom route compiler, creating a optimized stucture for handling route matches, with compiled middleware chain & handlers for all request methods. It also ensures that all routes have a `:handler` defined. +[Ring](https://github.com/ring-clojure/ring)-router adds support for [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: @@ -39,7 +39,7 @@ The expanded routes: Note that the compiled resuts as third element in the route vector. -### Request-method based routing +# Request-method based routing Handler are also looked under request-method keys: `:get`, `:head`, `:patch`, `:delete`, `:options`, `:post` or `:put`. Top-level handler is used if request-method based handler is not found. @@ -68,7 +68,7 @@ Reverse routing: ; "/ping" ``` -### Middleware +# Middleware Middleware can be added with a `:middleware` key, with a vector value of the following: @@ -112,94 +112,6 @@ Middleware is applied correctly: ; {: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 +# 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/route_conflicts.md b/doc/route_conflicts.md new file mode 100644 index 00000000..41ebab19 --- /dev/null +++ b/doc/route_conflicts.md @@ -0,0 +1,56 @@ +# 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. + +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. + +Examples routes with conflicts: + +```clj +(require '[reitit.core :as reitit]) + +(def routes + [["/ping"] + ["/:user-id/orders"] + ["/bulk/:bulk-id"] + ["/public/*path"] + ["/:version/status"]]) +``` + +By default, `ExceptionInfo` is thrown: + +```clj +(reitit/router routes) +; 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 +; +``` + +Just logging the conflicts: + +```clj +(reitit/router + routes + {:conflicts (comp println reitit/conflicts-str)}) +; 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/route_validation.md b/doc/route_validation.md new file mode 100644 index 00000000..991d5c83 --- /dev/null +++ b/doc/route_validation.md @@ -0,0 +1,172 @@ +# Route validation + +Namespace `reitit.spec` contains [clojure.spec](https://clojure.org/about/spec) definitions for raw-routes, routes, router and router options. + +**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) + +## 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. + +```clj +(require '[clojure.spec.alpha :as s]) +(require '[reitit.spec :as spec]) + +(def routes-from-db + ["tenant1" ::tenant1]) + +(s/valid? ::spec/raw-routes routes-from-db) +; false + +(s/explain ::spec/raw-routes routes-from-db) +; In: [0] val: "tenant1" fails spec: :reitit.spec/path at: [:route :path] predicate: (or (blank? %) (starts-with? % "/")) +; In: [0] val: "tenant1" fails spec: :reitit.spec/raw-route at: [:routes] predicate: (cat :path :reitit.spec/path :arg (? :reitit.spec/arg) :childs (* (and (nilable :reitit.spec/raw-route)))) +; In: [1] val: :user/tenant1 fails spec: :reitit.spec/raw-route at: [:routes] predicate: (cat :path :reitit.spec/path :arg (? :reitit.spec/arg) :childs (* (and (nilable :reitit.spec/raw-route)))) +; :clojure.spec.alpha/spec :reitit.spec/raw-routes +; :clojure.spec.alpha/value ["tenant1" :user/tenant1] +``` + +## At development time + +`reitit.core/router` can be instrumented and use something like [expound](https://github.com/bhb/expound) to pretty-print the spec problems. + +First add a `:dev` dependency to: + +```clj +[expound "0.3.0"] +``` + +Some bootstrapping: + +```clj +(require '[clojure.spec.test.alpha :as stest]) +(require '[expound.alpha :as expound]) +(require '[clojure.spec.alpha :as s]) +(require '[reitit.spec]) + +(stest/instrument `reitit/router) +(set! s/*explain-out* expound/printer) +``` + +And we are ready to go: + +```clj + +(reitit/router + ["/api" + ["/public" + ["/ping"] + ["pong"]]]) + +; CompilerException clojure.lang.ExceptionInfo: Call to #'reitit.core/router did not conform to spec: +; +; -- Spec failed -------------------- +; +; Function arguments +; +; (["/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 +; (clojure.spec.alpha/nilable :reitit.spec/raw-route)))) +; +; or +; +; (clojure.spec.alpha/cat +; :path +; :reitit.spec/path +; :arg +; (clojure.spec.alpha/? :reitit.spec/arg) +; :childs +; (clojure.spec.alpha/* +; (clojure.spec.alpha/and +; (clojure.spec.alpha/nilable :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 +; (clojure.spec.alpha/nilable :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 -------------------- +; +; Function arguments +; +; ([... [... ... ["pong"]]]) +; ^^^^^^ +; +; should satisfy +; +; (fn +; [%] +; (or +; (clojure.string/blank? %) +; (clojure.string/starts-with? % "/"))) +; +; or +; +; (fn +; [%] +; (or +; (clojure.string/blank? %) +; (clojure.string/starts-with? % "/"))) +; +; -- Relevant specs ------- +; +; :reitit.spec/path: +; (clojure.spec.alpha/and +; clojure.core/string? +; (clojure.core/fn +; [%] +; (clojure.core/or +; (clojure.string/blank? %) +; (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 +; (clojure.spec.alpha/nilable :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 route data + +*TODO* diff --git a/doc/routing/route_conflicts.md b/doc/routing/route_conflicts.md deleted file mode 100644 index 535827be..00000000 --- a/doc/routing/route_conflicts.md +++ /dev/null @@ -1,24 +0,0 @@ -## 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 deleted file mode 100644 index cab1f994..00000000 --- a/doc/routing/route_metadata.md +++ /dev/null @@ -1,55 +0,0 @@ -# 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.md) for details. diff --git a/doc/routing/route_syntax.md b/doc/routing/route_syntax.md deleted file mode 100644 index 29cfc5fb..00000000 --- a/doc/routing/route_syntax.md +++ /dev/null @@ -1,48 +0,0 @@ -# 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 deleted file mode 100644 index 4d4c224b..00000000 --- a/doc/routing/routers.md +++ /dev/null @@ -1,85 +0,0 @@ -# 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 deleted file mode 100644 index f73e28ca..00000000 --- a/doc/validating.md +++ /dev/null @@ -1,92 +0,0 @@ -# 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*