diff --git a/README.md b/README.md index 8627966e..589291a7 100644 --- a/README.md +++ b/README.md @@ -2,12 +2,12 @@ A friendly data-driven router for Clojure(Script). -* Simple data-driven [route syntax](https://metosin.github.io/reitit/basics.html#route-syntax) -* First-class [route meta-data](https://metosin.github.io/reitit/basics.html#route-data) -* Generic, not tied to HTTP -* [Route conflict resolution](https://metosin.github.io/reitit/route_conflicts.html) -* [Pluggable coercion](https://metosin.github.io/reitit/parameter_coercion.html) ([clojure.spec](https://clojure.org/about/spec)) -* both [Middleware](https://metosin.github.io/reitit/compiling_middleware.html) & Interceptors +* 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) +* 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 * Extendable * Fast diff --git a/doc/README.md b/doc/README.md index 438dbfa5..9be63ffe 100644 --- a/doc/README.md +++ b/doc/README.md @@ -2,12 +2,12 @@ [Reitit](https://github.com/metosin/reitit) is a small Clojure(Script) library for data-driven routing. -* Simple data-driven [route syntax](./basics.md#route-syntax) -* First-class [route meta-data](./basics.md#route-data) -* Bi-directional-routing -* [Route conflict resolution](./route_conflicts.md) -* [Pluggable coercion](./parameter_coercion.md) ([clojure.spec](https://clojure.org/about/spec)) -* Both [Middleware](./ring.md#middleware) & Interceptors +* Simple data-driven [route syntax](./basics/route_syntax.md) +* [Route conflict resolution](./advanced/route_conflicts.md) +* First-class [route meta-data](./basics/route_data.md) +* Bi-directional routing +* [Pluggable coercion](./ring/parameter_coercion.md) ([clojure.spec](https://clojure.org/about/spec)) +* supports both [Middleware](./ring/compiling_middleware.md) & Interceptors * Extendable * Fast diff --git a/doc/SUMMARY.md b/doc/SUMMARY.md index 1bdc0159..feb44aba 100644 --- a/doc/SUMMARY.md +++ b/doc/SUMMARY.md @@ -1,21 +1,21 @@ # Summary * [Introduction](README.md) -* 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) +* [Basics](basics/README.md) + * [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) +* [Advanced](advanced/README.md) + * [Route conflicts](advanced/route_conflicts.md) + * [Route Validation](advanced/route_validation.md) + * [Configuring routers](advanced/configuring_routers.md) +* [Ring](ring/README.md) + * [Ring-router](ring/ring.md) + * [Dynamic extensions](ring/dynamic_extensions.md) + * [Parameter coercion](ring/parameter_coercion.md) + * [Compiling middleware](ring/compiling_middleware.md) * TODO: Swagger & OpenAPI * TODO: Interceptors diff --git a/doc/advanced/README.md b/doc/advanced/README.md new file mode 100644 index 00000000..6dccc442 --- /dev/null +++ b/doc/advanced/README.md @@ -0,0 +1,5 @@ +# Advanced + +* [Route conflicts](advanced/route_conflicts.md) +* [Route Validation](advanced/route_validation.md) +* [Configuring routers](advanced/configuring_routers.md) diff --git a/doc/configuring_routers.md b/doc/advanced/configuring_routers.md similarity index 100% rename from doc/configuring_routers.md rename to doc/advanced/configuring_routers.md diff --git a/doc/route_conflicts.md b/doc/advanced/route_conflicts.md similarity index 100% rename from doc/route_conflicts.md rename to doc/advanced/route_conflicts.md diff --git a/doc/route_validation.md b/doc/advanced/route_validation.md similarity index 100% rename from doc/route_validation.md rename to doc/advanced/route_validation.md diff --git a/doc/basics.md b/doc/basics.md deleted file mode 100644 index 3a7ae421..00000000 --- a/doc/basics.md +++ /dev/null @@ -1,254 +0,0 @@ -# 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/basics/README.md b/doc/basics/README.md new file mode 100644 index 00000000..b92e5f81 --- /dev/null +++ b/doc/basics/README.md @@ -0,0 +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) diff --git a/doc/basics/different_routers.md b/doc/basics/different_routers.md new file mode 100644 index 00000000..d5a8ca1a --- /dev/null +++ b/doc/basics/different_routers.md @@ -0,0 +1,17 @@ +# 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 new file mode 100644 index 00000000..7dcbe60f --- /dev/null +++ b/doc/basics/name_based_routing.md @@ -0,0 +1,42 @@ +## 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} +``` diff --git a/doc/basics/path_based_routing.md b/doc/basics/path_based_routing.md new file mode 100644 index 00000000..aeccea00 --- /dev/null +++ b/doc/basics/path_based_routing.md @@ -0,0 +1,21 @@ +## 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"}} +``` diff --git a/doc/basics/route_data.md b/doc/basics/route_data.md new file mode 100644 index 00000000..3fc4bd20 --- /dev/null +++ b/doc/basics/route_data.md @@ -0,0 +1,52 @@ +# 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"} +``` diff --git a/doc/basics/route_syntax.md b/doc/basics/route_syntax.md new file mode 100644 index 00000000..7c3a26e1 --- /dev/null +++ b/doc/basics/route_syntax.md @@ -0,0 +1,81 @@ +# 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] +``` diff --git a/doc/basics/router.md b/doc/basics/router.md new file mode 100644 index 00000000..55bfce4d --- /dev/null +++ b/doc/basics/router.md @@ -0,0 +1,35 @@ +# 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}]] +``` diff --git a/doc/ring/README.md b/doc/ring/README.md new file mode 100644 index 00000000..69ae9e80 --- /dev/null +++ b/doc/ring/README.md @@ -0,0 +1,6 @@ +# Ring + +* [Ring-router](ring.md) +* [Dynamic extensions](dynamic_extensions.md) +* [Parameter coercion](parameter_coercion.md) +* [Compiling middleware](compiling_middleware.md) diff --git a/doc/compiling_middleware.md b/doc/ring/compiling_middleware.md similarity index 100% rename from doc/compiling_middleware.md rename to doc/ring/compiling_middleware.md diff --git a/doc/dynamic_extensions.md b/doc/ring/dynamic_extensions.md similarity index 100% rename from doc/dynamic_extensions.md rename to doc/ring/dynamic_extensions.md diff --git a/doc/parameter_coercion.md b/doc/ring/parameter_coercion.md similarity index 100% rename from doc/parameter_coercion.md rename to doc/ring/parameter_coercion.md diff --git a/doc/ring.md b/doc/ring/ring.md similarity index 100% rename from doc/ring.md rename to doc/ring/ring.md