diff --git a/README.md b/README.md index 12090b7e..ff89d226 100644 --- a/README.md +++ b/README.md @@ -2,12 +2,12 @@ A friendly data-driven router for Clojure(Script). -* Simple data-driven route syntax -* First-class route meta-data +* Simple data-driven [route syntax](#route-syntax) +* First-class [route meta-data](#route-meta-data) * Generic, not tied to HTTP * [Route conflict resolution](#route-conflicts) * [Pluggable coercion](#parameter-coercion) ([clojure.spec](https://clojure.org/about/spec)) -* both Middleware & Interceptors +* both [Middleware](#middleware) & Interceptors * Extendable * Fast @@ -87,7 +87,7 @@ Creating a router: `:mixed-router` is created (both static & wild routes are found): ```clj -(reitit/router-type router) +(reitit/router-name router) ; :mixed-router ``` @@ -147,7 +147,7 @@ Only a partial match. Let's provide the path-parameters: There is also a exception throwing version: -``` +```clj (reitit/match-by-name! router ::user) ; ExceptionInfo missing path-params for route /api/user/:id: #{:id} ``` @@ -159,12 +159,14 @@ Routes can have arbitrary meta-data. For nested routes, the meta-data is accumul 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" {:interceptors [::db] + :roles ^:replace #{:db-admin}} ["/:db" {:parameters {:db String}} ["/drop" ::drop-db] ["/stats" ::db-stats]]]]])) @@ -233,7 +235,7 @@ Route trees should not have multiple routes that match to a single (request) pat ## Ring -[Ring](https://github.com/ring-clojure/ring)-router adds support for ring [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 validates that all paths have a `:handler` defined and expands `:middleware` to create accumulated handlers for all request-methods. `reitit.ring/ring-handler` creates an actual ring handler out of a 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. Simple Ring app: @@ -249,13 +251,6 @@ Simple Ring app: ["/ping" handler]))) ``` -The expanded routes: - -```clj -(-> app (ring/get-router) (reitit/routes)) -; [["/ping" {:handler #object[...]}]] -``` - Applying the handler: ```clj @@ -266,8 +261,23 @@ Applying the handler: ; {: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 @@ -295,21 +305,18 @@ Reverse routing: ### Middleware -`:middleware` should be a vector of either of the following (expanded via the `reitit.middleware/ExpandMiddleware`: +Middleware can be added with a `:middleware` key, with a vector value of the following: -1. a ring middleware function of `handler -> request -> response` -2. a vector of middleware function (`handler args -> request -> response`) and it's args - actial middleware is created by applying function with handler and args +1. ring middleware function `handler -> request -> response` +2. vector of middleware function `handler ?args -> request -> response` and optinally it's args. -Let's define some middleware and a handler: +A middleware and a handler: ```clj (defn wrap [handler id] (fn [request] (handler (update request ::acc (fnil conj []) id)))) -(defn wrap-api [handler] - (wrap handler :api)) - (defn handler [{:keys [::acc]}] {:status 200, :body (conj acc :handler)}) ``` @@ -320,7 +327,7 @@ App with nested middleware: (def app (ring/ring-handler (ring/router - ["/api" {:middleware [wrap-api]} + ["/api" {:middleware [#(wrap % :api)]} ["/ping" handler] ["/admin" {:middleware [[wrap :admin]]} ["/db" {:middleware [[wrap :db]] @@ -342,57 +349,48 @@ Middleware is applied correctly: ### Middleware Records -Besides just being opaque functions, middleware can be presented as first-class data entries, `reitit.middleware/Middleware` records. They are created with `reitit.middleware/create` function and must have a `:name` and either `:wrap` or `:gen` key with the actual middleware function or a [middleware generator function](#compiling-middleware). +Reitit supports first-class data-driven middleware via `reitit.middleware/Middleware` records, created with `reitit.middleware/create` function. The following keys have special purpose: -When routes are compiled, middleware records are unwrapped into normal middleware functions producing no runtime performance penalty. Thanks to the `ExpandMiddleware` protocol, plain clojure(script) maps can also be used - they get expanded into middleware records. +| 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). -The previous middleware re-written as records: +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 ::wrap + {:name ::wrap2 :description "a nice little mw, takes 1 arg." :wrap wrap})) - -(def wrap2-api - {:name ::wrap-api - :description "a nice little mw, :api as arg" - :wrap (fn [handler] - (wrap handler :api))}) ``` -Or as maps: +As plain map: ```clj -(require '[reitit.middleware :as middleware]) - +;; plain map (def wrap3 - {:name ::wrap - :description "a nice little mw, takes 1 arg." - :wrap wrap}) - -(def wrap3-api - {:name ::wrap-api + {:name ::wrap3 :description "a nice little mw, :api as arg" :wrap (fn [handler] (wrap handler :api))}) ``` - - - ### Async Ring -All built-in middleware provide both the 2 and 3-arity, so they work with [Async Ring](https://www.booleanknot.com/blog/2016/07/15/asynchronous-ring.html) too. +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 -The routing `Match` is injected into a request and can be extracted with `reitit.ring/get-match`. It can be used to build dynamic extensions to the system. +`ring-handler` injects the `Match` into a request and it can be extracted at runtime with `reitit.ring/get-match`. This can be used to build dynamic extensions to the system. -A middleware to guard routes: +Example middleware to guard routes based on user roles: ```clj (require '[clojure.set :as set]) @@ -443,12 +441,12 @@ Authorized access to guarded route: ## Parameter coercion -Reitit ships with pluggable parameter coercion via `reitit.coercion.protocol/Coercion` protocol. `reitit.coercion.spec/SpecCoercion` provides implements it for [clojure.spec](https://clojure.org/about/spec) & [data-specs](https://github.com/metosin/spec-tools#data-specs). +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**: to use the spec-coercion, one needs to add the following dependencies manually to the project: +**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-alpha19"] +[org.clojure/clojure "1.9.0-alpha20"] [org.clojure/spec.alpha "0.1.123"] [metosin/spec-tools "0.3.3"] ``` @@ -536,13 +534,13 @@ If either request or response coercion fails, an descriptive error is thrown. ## Compiling Middleware -The [meta-data extensions](#meta-data-based-extensions) are a easy way to extend the system. Routes meta-data can be trasnformed into any shape (records, functions etc.) in route compilation, enabling easy access at request-time. +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 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. We can do all the static local computations forehand, yielding faster runtime processing. +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`. Instead of returning the actual middleware function, the middleware record can also decide no to mount itsef byt returning `nil`. Why mount `wrap-enforce-roles` for a route if there are no roles required for it? +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`. The actual codes are from `reitit.coercion`: +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 @@ -552,7 +550,7 @@ To demonstrate the two approaches, below are response coercion middleware writte (defn wrap-coerce-response "Pluggable response coercion middleware. Expects a :coercion of type `reitit.coercion.protocol/Coercion` - and :responeses from route meta, otherwise does not mount." + and :responses from route meta, otherwise does not mount." [handler] (fn ([request] @@ -605,7 +603,7 @@ To demonstrate the two approaches, below are response coercion middleware writte (handler request #(respond (coerce-response coercers request %)) raise)))))))})) ``` -The `:gen` -version is both much easier to understand but also 2-4x faster on basic perf tests. +The `:gen` -version has 50% less code, is easier to reason about and is 2-4x faster on basic perf tests. ## Merging route-trees @@ -644,6 +642,10 @@ To all Clojure(Script) routing libs out there, expecially to [Ataraxy](https://github.com/weavejester/ataraxy), [Bide](https://github.com/funcool/bide), [Bidi](https://github.com/juxt/bidi), [Compojure](https://github.com/weavejester/compojure) and [Pedestal](https://github.com/pedestal/pedestal/tree/master/route). +Also to [Compojure-api](https://github.com/metosin/compojure-api), [Kekkonen](https://github.com/metosin/kekkonen) and [Ring-swagger](https://github.com/metosin/ring-swagger) and for the data-driven syntax, coercion & stuff. + +And some [Yada](https://github.com/juxt/yada) too. + ## License Copyright © 2017 [Metosin Oy](http://www.metosin.fi) diff --git a/perf-test/clj/reitit/coercion_perf_test.clj b/perf-test/clj/reitit/coercion_perf_test.clj index 61adcb1b..d556736d 100644 --- a/perf-test/clj/reitit/coercion_perf_test.clj +++ b/perf-test/clj/reitit/coercion_perf_test.clj @@ -33,7 +33,7 @@ (s/def ::y (s/and (s/conformer #(if (string? %) (Long/parseLong %) %) identity) int?)) (s/def ::k (s/keys :req-un [::x ::y])) - (let [spec (spec/specify {:x int?, :y int?} ::jeah) + (let [spec (spec/into-spec {:x int?, :y int?} ::jeah) coercers (#'coercion/request-coercers spec/coercion {:body spec}) params {:x "1", :y "2"} request {:body-params {:x "1", :y "2"}}] diff --git a/src/reitit/coercion.cljc b/src/reitit/coercion.cljc index c2c0c901..4879f50a 100644 --- a/src/reitit/coercion.cljc +++ b/src/reitit/coercion.cljc @@ -115,7 +115,7 @@ match (ring/get-match request) parameters (-> match :result method :meta :parameters) coercion (-> match :meta :coercion)] - (if coercion + (if (and coercion parameters) (let [coercers (request-coercers coercion parameters) coerced (coerce-parameters coercers request)] (handler (impl/fast-assoc request :parameters coerced))) @@ -125,7 +125,7 @@ match (ring/get-match request) parameters (-> match :result method :meta :parameters) coercion (-> match :meta :coercion)] - (if coercion + (if (and coercion parameters) (let [coercers (request-coercers coercion parameters) coerced (coerce-parameters coercers request)] (handler (impl/fast-assoc request :parameters coerced) respond raise))))))) @@ -161,7 +161,7 @@ responses (-> match :result method :meta :responses) coercion (-> match :meta :coercion) opts (-> match :meta :opts)] - (if coercion + (if (and coercion responses) (let [coercers (response-coercers coercion responses opts) coerced (coerce-response coercers request response)] (coerce-response coercers request (handler request))) @@ -173,7 +173,7 @@ responses (-> match :result method :meta :responses) coercion (-> match :meta :coercion) opts (-> match :meta :opts)] - (if coercion + (if (and coercion responses) (let [coercers (response-coercers coercion responses opts) coerced (coerce-response coercers request response)] (handler request #(respond (coerce-response coercers request %)))) diff --git a/src/reitit/coercion/spec.cljc b/src/reitit/coercion/spec.cljc index 4d8603e3..f1f2fb00 100644 --- a/src/reitit/coercion/spec.cljc +++ b/src/reitit/coercion/spec.cljc @@ -23,31 +23,32 @@ (def default-conforming ::default) -(defprotocol Specify - (specify [this name])) +(defprotocol IntoSpec + (into-spec [this name])) -(extend-protocol Specify +(extend-protocol IntoSpec #?(:clj clojure.lang.PersistentArrayMap :cljs cljs.core.PersistentArrayMap) - (specify [this name] + (into-spec [this name] (ds/spec name this)) #?(:clj clojure.lang.PersistentHashMap :cljs cljs.core.PersistentHashMap) - (specify [this name] + (into-spec [this name] (ds/spec name this)) Spec - (specify [this _] this) + (into-spec [this _] this) - Object - (specify [this _] + #?(:clj Object + :cljs default) + (into-spec [this _] (st/create-spec {:spec this}))) ;; TODO: proper name! -(def memoized-specify - (memoize #(specify %1 (gensym "spec")))) +(def memoized-into-spec + (memoize #(into-spec %1 (gensym "spec")))) (defmulti coerce-response? identity :default ::default) (defmethod coerce-response? ::default [_] true) @@ -58,7 +59,7 @@ (get-name [_] name) (compile [_ model _] - (memoized-specify model)) + (memoized-into-spec model)) (get-apidocs [_ _ {:keys [parameters responses] :as info}] (cond-> (dissoc info :parameters :responses) @@ -67,13 +68,13 @@ (into (empty parameters) (for [[k v] parameters] - [k memoized-specify]))) + [k memoized-into-spec]))) responses (assoc ::swagger/responses (into (empty responses) (for [[k response] responses] - [k (update response :schema memoized-specify)]))))) + [k (update response :schema memoized-into-spec)]))))) (make-open [_ spec] spec) @@ -81,7 +82,7 @@ (update error :spec (comp str s/form))) (request-coercer [_ type spec] - (let [spec (memoized-specify spec) + (let [spec (memoized-into-spec spec) {:keys [formats default]} (conforming type)] (fn [value format] (if-let [conforming (or (get formats format) default)] @@ -101,9 +102,7 @@ (def default-options {:coerce-response? coerce-response? :conforming {:body {:default default-conforming - :formats {"application/json" json-conforming - "application/msgpack" json-conforming - "application/x-yaml" json-conforming}} + :formats {"application/json" json-conforming}} :string {:default string-conforming} :response {:default default-conforming}}}) diff --git a/src/reitit/core.cljc b/src/reitit/core.cljc index bd1aa9da..faf0e60f 100644 --- a/src/reitit/core.cljc +++ b/src/reitit/core.cljc @@ -89,11 +89,14 @@ (defn find-names [routes opts] (into [] (keep #(-> % second :name)) routes)) -(defn compile-route [[p m :as route] {:keys [compile] :as opts}] +(defn- compile-route [[p m :as route] {:keys [compile] :as opts}] [p m (if compile (compile route opts))]) +(defn- compile-routes [routes opts] + (into [] (keep #(compile-route % opts) routes))) + (defprotocol Router - (router-type [this]) + (router-name [this]) (routes [this]) (options [this]) (route-names [this]) @@ -132,7 +135,7 @@ ([routes] (linear-router routes {})) ([routes opts] - (let [compiled (map #(compile-route % opts) routes) + (let [compiled (compile-routes routes opts) names (find-names routes opts) [data lookup] (reduce (fn [[data lookup] [p {:keys [name] :as meta} result]] @@ -146,10 +149,10 @@ lookup (impl/fast-map lookup)] (reify Router - (router-type [_] + (router-name [_] :linear-router) (routes [_] - routes) + compiled) (options [_] opts) (route-names [_] @@ -179,7 +182,7 @@ (str "can't create LookupRouter with wildcard routes: " wilds) {:wilds wilds :routes routes}))) - (let [compiled (map #(compile-route % opts) routes) + (let [compiled (compile-routes routes opts) names (find-names routes opts) [data lookup] (reduce (fn [[data lookup] [p {:keys [name] :as meta} result]] @@ -190,10 +193,10 @@ data (impl/fast-map data) lookup (impl/fast-map lookup)] (reify Router - (router-type [_] + (router-name [_] :lookup-router) (routes [_] - routes) + compiled) (options [_] opts) (route-names [_] @@ -220,7 +223,7 @@ lookup-router (lookup-router lookup opts) names (find-names routes opts)] (reify Router - (router-type [_] + (router-name [_] :mixed-router) (routes [_] routes) diff --git a/src/reitit/middleware.cljc b/src/reitit/middleware.cljc index a817b2f8..18bceeb1 100644 --- a/src/reitit/middleware.cljc +++ b/src/reitit/middleware.cljc @@ -2,56 +2,55 @@ (:require [meta-merge.core :refer [meta-merge]] [reitit.core :as reitit])) -(defprotocol ExpandMiddleware - (expand-middleware [this meta opts])) +(defprotocol IntoMiddleware + (into-middleware [this meta opts])) -(defrecord Middleware [name wrap create]) +(defrecord Middleware [name wrap]) +(defrecord Endpoint [meta handler middleware]) (defn create [{:keys [name gen wrap] :as m}] - (when-not name - (throw - (ex-info - (str "Middleware must have :name defined " m) m))) (when (and gen wrap) (throw (ex-info (str "Middleware can't both :wrap and :gen defined " m) m))) (map->Middleware m)) -(extend-protocol ExpandMiddleware +(extend-protocol IntoMiddleware #?(:clj clojure.lang.APersistentVector :cljs cljs.core.PersistentVector) - (expand-middleware [[f & args] meta opts] - (if-let [mw (expand-middleware f meta opts)] - (fn [handler] - (apply mw handler args)))) + (into-middleware [[f & args] meta opts] + (if-let [{:keys [wrap] :as mw} (into-middleware f meta opts)] + (assoc mw :wrap #(apply wrap % args)))) #?(:clj clojure.lang.Fn :cljs function) - (expand-middleware [this _ _] this) + (into-middleware [this _ _] + (map->Middleware + {:wrap this})) #?(:clj clojure.lang.PersistentArrayMap :cljs cljs.core.PersistentArrayMap) - (expand-middleware [this meta opts] - (expand-middleware (create this) meta opts)) + (into-middleware [this meta opts] + (into-middleware (create this) meta opts)) #?(:clj clojure.lang.PersistentHashMap :cljs cljs.core.PersistentHashMap) - (expand-middleware [this meta opts] - (expand-middleware (create this) meta opts)) + (into-middleware [this meta opts] + (into-middleware (create this) meta opts)) Middleware - (expand-middleware [{:keys [wrap gen]} meta opts] - (if gen + (into-middleware [{:keys [wrap gen] :as this} meta opts] + (if-not gen + this (if-let [wrap (gen meta opts)] - (fn [handler & args] - (apply wrap handler args))) - (fn [handler & args] - (apply wrap handler args)))) + (map->Middleware + (-> this + (dissoc :gen) + (assoc :wrap wrap)))))) nil - (expand-middleware [_ _ _])) + (into-middleware [_ _ _])) (defn- ensure-handler! [path meta scope] (when-not (:handler meta) @@ -61,23 +60,44 @@ (merge {:path path, :meta meta} (if scope {:scope scope})))))) -(defn compose-middleware [middleware meta opts] +(defn expand [middleware meta opts] (->> middleware - (keep identity) - (map #(expand-middleware % meta opts)) - (keep identity) - (apply comp identity))) + (keep #(into-middleware % meta opts)) + (into []))) -(defn compile-handler +(defn compile-handler [middleware handler] + ((apply comp identity (keep :wrap middleware)) handler)) + +(compile-handler + [(map->Middleware + {:wrap + (fn [handler] + (fn [request] + (handler request)))})] identity) + +(defn compile-result ([route opts] - (compile-handler route opts nil)) + (compile-result route opts nil)) ([[path {:keys [middleware handler] :as meta}] opts scope] (ensure-handler! path meta scope) - ((compose-middleware middleware meta opts) handler))) + (let [middleware (expand middleware meta opts)] + (map->Endpoint + {:handler (compile-handler middleware handler) + :middleware middleware + :meta meta})))) (defn router ([data] (router data nil)) ([data opts] - (let [opts (meta-merge {:compile compile-handler} opts)] + (let [opts (meta-merge {:compile compile-result} opts)] (reitit/router data opts)))) + +(defn middleware-handler [router] + (with-meta + (fn [path] + (some->> path + (reitit/match-by-path router) + :result + :handler)) + {::router router})) diff --git a/src/reitit/ring.cljc b/src/reitit/ring.cljc index 9a6b2019..f866b705 100644 --- a/src/reitit/ring.cljc +++ b/src/reitit/ring.cljc @@ -6,7 +6,6 @@ (def http-methods #{:get :head :patch :delete :options :post :put}) (defrecord Methods [get head post put delete trace options connect patch any]) -(defrecord Endpoint [meta handler]) (defn- group-keys [meta] (reduce-kv @@ -28,7 +27,7 @@ (if handler (handler (cond-> (impl/fast-assoc request ::match match) - params (impl/fast-assoc :path-params params))))))) + (seq params) (impl/fast-assoc :path-params params))))))) ([request respond raise] (if-let [match (reitit/match-by-path router (:uri request))] (let [method (:request-method request :any) @@ -39,7 +38,7 @@ (if handler (handler (cond-> (impl/fast-assoc request ::match match) - params (impl/fast-assoc :path-params params)) + (seq params) (impl/fast-assoc :path-params params)) respond raise)))))) {::router router})) @@ -56,30 +55,22 @@ (update acc method expand opts) acc)) meta http-methods)]) -(defn compile-handler [[path meta] opts] +(defn compile-result [[path meta] opts] (let [[top childs] (group-keys meta)] (if-not (seq childs) - (map->Methods - {:any (map->Endpoint - {:handler (middleware/compile-handler [path top] opts) - :meta top})}) - (let [any-handler (if (:handler top) (middleware/compile-handler [path meta] opts))] + (let [middleware (middleware/compile-result [path top] opts)] + (map->Methods {:any (middleware/compile-result [path top] opts)})) + (let [any-handler (if (:handler top) (middleware/compile-result [path meta] opts))] (reduce-kv (fn [acc method meta] - (let [meta (meta-merge top meta) - handler (middleware/compile-handler [path meta] opts method)] - (assoc acc method (map->Endpoint - {:handler handler - :meta meta})))) - (map->Methods - {:any (map->Endpoint - {:handler (if (:handler top) (middleware/compile-handler [path meta] opts)) - :meta top})}) + (let [meta (meta-merge top meta)] + (assoc acc method (middleware/compile-result [path meta] opts method)))) + (map->Methods {:any any-handler}) childs))))) (defn router ([data] (router data nil)) ([data opts] - (let [opts (meta-merge {:coerce coerce-handler, :compile compile-handler} opts)] + (let [opts (meta-merge {:coerce coerce-handler, :compile compile-result} opts)] (reitit/router data opts)))) diff --git a/src/reitit/spec.cljc b/src/reitit/spec.cljc index 8953f3f7..32e956ff 100644 --- a/src/reitit/spec.cljc +++ b/src/reitit/spec.cljc @@ -26,7 +26,8 @@ (s/def ::route (s/cat :path ::path - :meta ::meta)) + :meta ::meta + :result (s/? any?))) (s/def ::routes (s/or :route ::route diff --git a/test/cljc/reitit/core_test.cljc b/test/cljc/reitit/core_test.cljc index 5dfe38f2..0540d68c 100644 --- a/test/cljc/reitit/core_test.cljc +++ b/test/cljc/reitit/core_test.cljc @@ -9,8 +9,8 @@ (testing "linear-router" (let [router (reitit/router ["/api" ["/ipa" ["/:size" ::beer]]])] - (is (= :linear-router (reitit/router-type router))) - (is (= [["/api/ipa/:size" {:name ::beer}]] + (is (= :linear-router (reitit/router-name router))) + (is (= [["/api/ipa/:size" {:name ::beer} nil]] (reitit/routes router))) (is (= true (map? (reitit/options router)))) (is (= (reitit/map->Match @@ -27,6 +27,7 @@ (reitit/match-by-name router ::beer {:size "large"}))) (is (= nil (reitit/match-by-name router "ILLEGAL"))) (is (= [::beer] (reitit/route-names router))) + (testing "name-based routing with missing parameters" (is (= (reitit/map->PartialMatch {:template "/api/ipa/:size" @@ -42,8 +43,8 @@ (testing "lookup-router" (let [router (reitit/router ["/api" ["/ipa" ["/large" ::beer]]])] - (is (= :lookup-router (reitit/router-type router))) - (is (= [["/api/ipa/large" {:name ::beer}]] + (is (= :lookup-router (reitit/router-name router))) + (is (= [["/api/ipa/large" {:name ::beer} nil]] (reitit/routes router))) (is (= true (map? (reitit/options router)))) (is (= (reitit/map->Match @@ -60,6 +61,7 @@ (reitit/match-by-name router ::beer {:size "large"}))) (is (= nil (reitit/match-by-name router "ILLEGAL"))) (is (= [::beer] (reitit/route-names router))) + (testing "can't be created with wildcard routes" (is (thrown-with-msg? ExceptionInfo @@ -69,6 +71,7 @@ ["/api/:version/ping"] {}))))))) (testing "route coercion & compilation" + (testing "custom compile" (let [compile-times (atom 0) coerce (fn [[path meta] _] @@ -86,6 +89,7 @@ ["/crap"]]] {:coerce coerce :compile compile})] + (testing "routes are coerced" (is (= [["/api/ping" {:name ::ping :path "/api/ping", @@ -93,13 +97,15 @@ ["/api/pong" {:name ::pong :path "/api/pong", :roles #{:admin}}]] - (reitit/routes router)))) + (map butlast (reitit/routes router))))) + (testing "route match contains compiled handler" (is (= 2 @compile-times)) (let [{:keys [result]} (reitit/match-by-path router "/api/pong")] (is result) (is (= "/api/pong" (result))) (is (= 2 @compile-times)))))) + (testing "default compile" (let [router (reitit/router ["/ping" (constantly "ok")])] (let [{:keys [result]} (reitit/match-by-path router "/ping")] @@ -109,9 +115,9 @@ (testing "custom router" (let [router (reitit/router ["/ping"] {:router (fn [_ _] (reify Router - (reitit/router-type [_] + (reitit/router-name [_] ::custom)))})] - (is (= ::custom (reitit/router-type router))))) + (is (= ::custom (reitit/router-name router))))) (testing "bide sample" (let [routes [["/auth/login" :auth/login] diff --git a/test/cljc/reitit/middleware_test.cljc b/test/cljc/reitit/middleware_test.cljc index 6c24f5c3..dea830ea 100644 --- a/test/cljc/reitit/middleware_test.cljc +++ b/test/cljc/reitit/middleware_test.cljc @@ -6,37 +6,10 @@ #?(:clj (:import (clojure.lang ExceptionInfo)))) -(defn mw [handler name] - (fn - ([request] - (-> request - (update ::mw (fnil conj []) name) - (handler) - (update :body (fnil conj []) name))) - ([request respond raise] - (handler - (update request ::mw (fnil conj []) name) - #(respond (update % :body (fnil conj []) name)) - raise)))) - -(defn handler - ([{:keys [::mw]}] - {:status 200 :body (conj mw :ok)}) - ([request respond raise] - (respond (handler request)))) - (deftest expand-middleware-test (testing "middleware records" - (testing ":name is mandatory" - (is (thrown-with-msg? - ExceptionInfo - #"Middleware must have :name defined" - (middleware/create - {:wrap identity - :gen (constantly identity)})))) - (testing ":wrap & :gen are exclusive" (is (thrown-with-msg? ExceptionInfo @@ -46,78 +19,93 @@ :wrap identity :gen (constantly identity)})))) - (testing ":wrap" + (testing "middleware" (let [calls (atom 0) - data {:name ::test - :wrap (fn [handler value] - (swap! calls inc) - (fn [request] - [value request]))}] + wrap (fn [handler value] + (swap! calls inc) + (fn [request] + [value request])) + ->app (fn [ast handler] + (middleware/compile-handler + (middleware/expand ast :meta {}) + handler))] + + (testing "as middleware function" + (reset! calls 0) + (let [app (->app [[#(wrap % :value)]] identity)] + (dotimes [_ 10] + (is (= [:value :request] (app :request))) + (is (= 1 @calls))))) + + (testing "as middleware vector" + (reset! calls 0) + (let [app (->app [[wrap :value]] identity)] + (dotimes [_ 10] + (is (= [:value :request] (app :request))) + (is (= 1 @calls))))) (testing "as map" (reset! calls 0) - (let [app ((middleware/compose-middleware [data] :meta {}) identity :value)] + (let [app (->app [[{:wrap #(wrap % :value)}]] identity)] (dotimes [_ 10] (is (= [:value :request] (app :request))) (is (= 1 @calls))))) - (testing "direct" + (testing "as map vector" (reset! calls 0) - (let [app ((middleware/compose-middleware [(middleware/create data)] :meta {}) identity :value)] + (let [app (->app [[{:wrap wrap} :value]] identity)] (dotimes [_ 10] (is (= [:value :request] (app :request))) (is (= 1 @calls))))) - (testing "vector" + (testing "as Middleware" (reset! calls 0) - (let [app ((middleware/compose-middleware [[(middleware/create data) :value]] :meta {}) identity)] + (let [app (->app [[(middleware/create {:wrap #(wrap % :value)})]] identity)] + (dotimes [_ 10] + (is (= [:value :request] (app :request))) + (is (= 1 @calls))))) + + (testing "as Middleware vector" + (reset! calls 0) + (let [app (->app [[(middleware/create {:wrap wrap}) :value]] identity)] (dotimes [_ 10] (is (= [:value :request] (app :request))) (is (= 1 @calls))))))) - (testing ":gen" + (testing "compiled Middleware" (let [calls (atom 0) - data {:name ::test - :gen (fn [meta _] + mw {:gen (fn [meta _] + (swap! calls inc) + (fn [handler value] (swap! calls inc) - (fn [handler value] - (swap! calls inc) - (fn [request] - [meta value request])))}] + (fn [request] + [meta value request])))} + ->app (fn [ast handler] + (middleware/compile-handler + (middleware/expand ast :meta {}) + handler))] (testing "as map" (reset! calls 0) - (let [app ((middleware/compose-middleware [data] :meta {}) identity :value)] + (let [app (->app [[mw :value]] identity)] (dotimes [_ 10] (is (= [:meta :value :request] (app :request))) (is (= 2 @calls))))) - (testing "direct" + (testing "as Middleware" (reset! calls 0) - (let [app ((middleware/compose-middleware [(middleware/create data)] :meta {}) identity :value)] - (dotimes [_ 10] - (is (= [:meta :value :request] (app :request))) - (is (= 2 @calls))))) - - (testing "vector" - (reset! calls 0) - (let [app ((middleware/compose-middleware [[(middleware/create data) :value]] :meta {}) identity)] - (is (= [:meta :value :request] (app :request))) + (let [app (->app [[(middleware/create mw) :value]] identity)] (dotimes [_ 10] (is (= [:meta :value :request] (app :request))) (is (= 2 @calls))))) (testing "nil unmounts the middleware" - (reset! calls 0) - (let [syntax [[(middleware/create - {:name ::test - :gen (fn [meta _])}) :value]] - app ((middleware/compose-middleware syntax :meta {}) identity)] - (is (= :request (app :request))) + (let [app (->app [{:gen (constantly nil)} + {:gen (constantly nil)}] identity)] (dotimes [_ 10] (is (= :request (app :request)))))))))) -(deftest middleware-router-test +(deftest middleware-handler-test (testing "all paths should have a handler" (is (thrown-with-msg? @@ -125,40 +113,59 @@ #"path \"/ping\" doesn't have a :handler defined" (middleware/router ["/ping"])))) - (testing "ring-handler" - (let [api-mw #(mw % :api) + (testing "middleware-handler" + (let [mw (fn [handler value] + (fn [request] + (conj (handler (conj request value)) value))) + api-mw #(mw % :api) + handler #(conj % :ok) router (middleware/router [["/ping" handler] ["/api" {:middleware [api-mw]} ["/ping" handler] ["/admin" {:middleware [[mw :admin]]} ["/ping" handler]]]]) - app (fn - ([{:keys [uri] :as request}] - (if-let [handler (:result (reitit/match-by-path router uri))] - (handler request))) - ([{:keys [uri] :as request} respond raise] - (if-let [handler (:result (reitit/match-by-path router uri))] - (handler request respond raise))))] + ->app (fn [router] + (let [h (middleware/middleware-handler router)] + (fn [path] + (if-let [f (h path)] + (f []))))) + app (->app router)] (testing "not found" - (is (= nil (app {:uri "/favicon.ico"})))) + (is (= nil (app "/favicon.ico")))) (testing "normal handler" - (is (= {:status 200, :body [:ok]} - (app {:uri "/ping"})))) + (is (= [:ok] (app "/ping")))) (testing "with middleware" - (is (= {:status 200, :body [:api :ok :api]} - (app {:uri "/api/ping"})))) + (is (= [:api :ok :api] (app "/api/ping")))) (testing "with nested middleware" - (is (= {:status 200, :body [:api :admin :ok :admin :api]} - (app {:uri "/api/admin/ping"})))) + (is (= [:api :admin :ok :admin :api] (app "/api/admin/ping")))) - (testing "3-arity" - (let [result (atom nil) - respond (partial reset! result), raise ::not-called] - (app {:uri "/api/admin/ping"} respond raise) - (is (= {:status 200, :body [:api :admin :ok :admin :api]} - @result))))))) + (testing ":gen middleware can be unmounted at creation-time" + (let [mw1 {:name ::mw1, :gen (constantly #(mw % ::mw1))} + mw2 {:name ::mw2, :gen (constantly nil)} + mw3 {:name ::mw3, :wrap #(mw % ::mw3)} + router (middleware/router + ["/api" {:name ::api + :middleware [mw1 mw2 mw3 mw2] + :handler handler}]) + app (->app router)] + + (is (= [::mw1 ::mw3 :ok ::mw3 ::mw1] (app "/api"))) + + (testing "routes contain list of actually applied mw" + (is (= [::mw1 ::mw3] (->> (reitit/routes router) + first + last + :middleware + (map :name))))) + + (testing "match contains list of actually applied mw" + (is (= [::mw1 ::mw3] (->> "/api" + (reitit/match-by-path router) + :result + :middleware + (map :name)))))))))) diff --git a/test/cljc/reitit/spec_test.cljc b/test/cljc/reitit/spec_test.cljc index cb3f18a6..fd3334cb 100644 --- a/test/cljc/reitit/spec_test.cljc +++ b/test/cljc/reitit/spec_test.cljc @@ -1,13 +1,13 @@ (ns reitit.spec-test (:require [clojure.test :refer [deftest testing is are]] - [clojure.spec.test.alpha :as stest] + [#?(:clj clojure.spec.test.alpha :cljs cljs.spec.test.alpha) :as stest] [clojure.spec.alpha :as s] [reitit.core :as reitit] [reitit.spec :as spec]) #?(:clj (:import (clojure.lang ExceptionInfo)))) -(stest/instrument `reitit/router) +(stest/instrument `reitit/router `reitit/routes) (deftest router-spec-test @@ -44,6 +44,9 @@ ["/api" [] ["/ipa"]]))) + (testing "routes conform to spec (can't spec protocol functions)" + (is (= true (s/valid? ::spec/routes (reitit/routes (reitit/router ["/ping"])))))) + (testing "options" (are [opts] diff --git a/test/cljs/reitit/doo_runner.cljs b/test/cljs/reitit/doo_runner.cljs index ff3bd70e..aaa2fc1b 100644 --- a/test/cljs/reitit/doo_runner.cljs +++ b/test/cljs/reitit/doo_runner.cljs @@ -1,9 +1,15 @@ (ns reitit.doo-runner (:require [doo.runner :refer-macros [doo-tests]] + reitit.coercion-test reitit.core-test - reitit.ring-test)) + reitit.middleware-test + reitit.ring-test + #_reitit.spec-test)) (enable-console-print!) -(doo-tests 'reitit.core-test - 'reitit.ring-test) +(doo-tests 'reitit.coercion-test + 'reitit.core-test + 'reitit.middleware-test + 'reitit.ring-test + #_'reitit.spec-test)