diff --git a/README.md b/README.md index c73eba40..7cad9a20 100644 --- a/README.md +++ b/README.md @@ -63,9 +63,9 @@ Same routes flattened: ["/api/ping" ::ping]] ``` -## Routers +## Routing -For routing, a `Router` is needed. Reitit ships with 2 different router implementations: `LinearRouter` and `LookupRouter`, both based on the awesome [Pedestal](https://github.com/pedestal/pedestal/tree/master/route) implementation. +For routing, a `Router` is needed. Reitit ships with 2 different router implementations: `:linear-router` and `:lookup-router`, both 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. `Router` support both fast path- and name-based lookups. @@ -78,14 +78,14 @@ Creating a router: (reitit/router [["/api" ["/ping" ::ping] - ["/user/:id" ::user]])) + ["/user/:id" ::user]]])) ``` -`LinearRouter` is created (as there are wildcard): +`:linear-router` is created (as there are wildcard): ```clj -(class router) -; reitit.core.LinearRouter +(reitit/router-type router) +; :linear-router ``` The expanded routes: @@ -96,6 +96,13 @@ The expanded routes: ; ["/api/user/:id" {:name :user/user}]] ``` +Route names: + +```clj +(reitit/route-names router) +; [:user/ping :user/user] +``` + Path-based routing: ```clj @@ -114,8 +121,19 @@ Name-based (reverse) routing: ```clj (reitit/match-by-name router ::user) -; ExceptionInfo missing path-params for route '/api/user/:id': #{:id} +; #PartialMatch{:template "/api/user/:id", +; :meta {:name :user/user}, +; :handler nil, +; :params nil, +; :required #{:id}} +(reitit/partial-match? (reitit/match-by-name router ::user)) +; true +``` + +Only a partial match. Let's provide path-parameters: + +```clj (reitit/match-by-name router ::user {:id "1"}) ; #Match{:template "/api/user/:id" ; :meta {:name :user/user} @@ -124,6 +142,13 @@ Name-based (reverse) routing: ; :params {:id "1"}} ``` +There is also a exception throwing version: + +``` +(reitit/match-by-name! router ::user) +; ExceptionInfo missing path-params for route /api/user/:id: #{:id} +``` + ## Route meta-data Routes can have arbitrary meta-data. For nested routes, the meta-data is accumulated from root towards leafs using [meta-merge](https://github.com/weavejester/meta-merge). @@ -199,11 +224,11 @@ Simple [Ring](https://github.com/ring-clojure/ring)-based routing app: ["/ping" handler]))) ``` -Backed by a `LookupRouter` (as no wildcards found): +Backed by a `:lookup-router` (as no wildcards found): ```clj -(-> app (ring/get-router) class) -; reitit.core.LookupRouter +(-> app (ring/get-router) (reitit/router-type)) +; :lookup-router ``` The expanded routes: @@ -229,7 +254,8 @@ Routing based on `:request-method`: (def app (ring/ring-handler (ring/router - ["/ping" {:get handler + ["/ping" {:name ::ping + :get handler :post handler}]))) (app {:request-method :get, :uri "/ping"}) @@ -239,6 +265,16 @@ Routing based on `:request-method`: ; nil ``` +Reverse routing: + +```clj +(-> app + (ring/get-router) + (reitit/match-by-name ::ping) + :path) +; "/ping" +``` + Some middleware and a new handler: ```clj @@ -362,8 +398,8 @@ Routers can be configured via options. Options allow things like [`clojure.spec` | `:routes` | Initial resolved routes (default `[]`) | `:meta` | Initial expanded route-meta vector (default `[]`) | `:expand` | Function of `arg opts => meta` to expand route arg to route meta-data (default `reitit.core/expand`) - | `:coerce` | Function of `[path meta] opts => [path meta]` to coerce resolved route, can throw or return `nil` - | `:compile` | Function of `[path meta] opts => handler` to compile a route handler + | `:coerce` | Function of `route opts => route` to coerce resolved route, can throw or return `nil` + | `:compile` | Function of `route opts => handler` to compile a route handler ## Special thanks diff --git a/perf-test/clj/reitit/opensensors_routing_test.clj b/perf-test/clj/reitit/opensensors_routing_test.clj new file mode 100644 index 00000000..d25b7942 --- /dev/null +++ b/perf-test/clj/reitit/opensensors_routing_test.clj @@ -0,0 +1,563 @@ +(ns reitit.opensensors-routing-test + (:require [clojure.test :refer [deftest testing is]] + [cheshire.core :as json] + [clojure.string :as str] + [reitit.core :as reitit] + [reitit.ring :as ring] + + [bidi.bidi :as bidi] + + [ataraxy.core :as ataraxy] + + [compojure.api.sweet :refer [api routes context ANY]] + + [io.pedestal.http.route.definition.table :as table] + [io.pedestal.http.route.map-tree :as map-tree] + [io.pedestal.http.route.router :as pedestal] + [io.pedestal.http.route :as route])) + +(defn raw-title [color s] + (println (str color (apply str (repeat (count s) "#")) "\u001B[0m")) + (println (str color s "\u001B[0m")) + (println (str color (apply str (repeat (count s) "#")) "\u001B[0m"))) + +(def title (partial raw-title "\u001B[35m")) +(def suite (partial raw-title "\u001B[32m")) + +;; +;; extract sample routes +;; + +(defn swagger->routes [url ring?] + (let [route-number (atom 0) + ->route-name #(keyword "test" (str "route" (swap! route-number inc))) + ->endpoint (fn [m] + (if ring? + (reduce-kv + (fn [acc k v] + (assoc acc k {:handler #'identity, :name (->route-name)})) + {} (select-keys m #{:get :head :patch :delete :options :post :put})) + (->route-name)))] + (-> (slurp url) + (json/parse-string true) + (->> :paths + (mapv (fn [[p v]] [(-> p name (str/replace #"\{(.*?)\}" ":$1") (->> (str "/"))) (->endpoint v)])))))) + +(defn valid-urls [router] + (->> + (for [name (reitit/route-names router) + :let [match (reitit/match-by-name router name) + params (if (reitit/partial-match? match) + (-> match :required (zipmap (range))))]] + (:path (reitit/match-by-name router name params))) + (into []))) + +(comment + (swagger->routes "https://api.opensensors.io/doc" false)) + +(defn bench-routes [routes f] + (let [router (reitit/router routes) + urls (valid-urls router) + random-url #(rand-nth urls) + log-time #(let [now (System/nanoTime)] (%) (- (System/nanoTime) now)) + total 10000 + dropped (int (* total 0.45))] + (mapv + #(let [times (->> (range total) + (mapv + (fn [_] + (let [now (System/nanoTime) + result (f %) + total (- (System/nanoTime) now)] + (assert result) + total))) + (sort) + (drop dropped) + (drop-last dropped)) + avg (int (/ (reduce + times) (count times)))] + [% avg]) urls))) + +(defn bench [routes no-paths?] + (let [routes (mapv (fn [[path name]] + (if no-paths? + [(str/replace path #"\:" "") name] + [path name])) routes) + router (reitit/router routes)] + (doseq [[path time] (bench-routes routes #(reitit/match-by-path router %))] + (println path "\t" time)))) + +(defn bench [routes no-paths?] + (let [routes (mapv (fn [[path name]] + (if no-paths? + [(str/replace path #"\:" "") name] + [path name])) routes) + router (reitit/router routes)] + (doseq [[path time] (bench-routes routes #(reitit/match-by-path router %))] + (println path "\t" time)))) +;; +;; Perf tests +;; + +(def handler (constantly {:status 200, :body "ok"})) + +(def opensensors-routes + [["/v2/whoami" {:handler handler, :name :test/route1}] + ["/v2/users/:user-id/datasets" {:handler handler, :name :test/route2}] + ["/v2/public/projects/:project-id/datasets" {:handler handler, :name :test/route3}] + ["/v1/public/topics/:topic" {:handler handler, :name :test/route4}] + ["/v1/users/:user-id/orgs/:org-id" {:handler handler, :name :test/route5}] + ["/v1/search/topics/:term" {:handler handler, :name :test/route6}] + ["/v1/users/:user-id/invitations" {:handler handler, :name :test/route7}] + #_["/v1/orgs/:org-id/devices/:batch/:type" {:handler handler, :name :test/route8}] + ["/v1/users/:user-id/topics" {:handler handler, :name :test/route9}] + ["/v1/users/:user-id/bookmarks/followers" {:handler handler, :name :test/route10}] + ["/v2/datasets/:dataset-id" {:handler handler, :name :test/route11}] + ["/v1/orgs/:org-id/usage-stats" {:handler handler, :name :test/route12}] + ["/v1/orgs/:org-id/devices/:client-id" {:handler handler, :name :test/route13}] + ["/v1/messages/user/:user-id" {:handler handler, :name :test/route14}] + ["/v1/users/:user-id/devices" {:handler handler, :name :test/route15}] + ["/v1/public/users/:user-id" {:handler handler, :name :test/route16}] + ["/v1/orgs/:org-id/errors" {:handler handler, :name :test/route17}] + ["/v1/public/orgs/:org-id" {:handler handler, :name :test/route18}] + ["/v1/orgs/:org-id/invitations" {:handler handler, :name :test/route19}] + ["/v2/public/messages/dataset/bulk" {:handler handler, :name :test/route20}] + #_["/v1/users/:user-id/devices/bulk" {:handler handler, :name :test/route21}] + ["/v1/users/:user-id/device-errors" {:handler handler, :name :test/route22}] + ["/v2/login" {:handler handler, :name :test/route23}] + ["/v1/users/:user-id/usage-stats" {:handler handler, :name :test/route24}] + ["/v2/users/:user-id/devices" {:handler handler, :name :test/route25}] + ["/v1/users/:user-id/claim-device/:client-id" {:handler handler, :name :test/route26}] + ["/v2/public/projects/:project-id" {:handler handler, :name :test/route27}] + ["/v2/public/datasets/:dataset-id" {:handler handler, :name :test/route28}] + ["/v2/users/:user-id/topics/bulk" {:handler handler, :name :test/route29}] + ["/v1/messages/device/:client-id" {:handler handler, :name :test/route30}] + ["/v1/users/:user-id/owned-orgs" {:handler handler, :name :test/route31}] + ["/v1/topics/:topic" {:handler handler, :name :test/route32}] + ["/v1/users/:user-id/bookmark/:topic" {:handler handler, :name :test/route33}] + ["/v1/orgs/:org-id/members/:user-id" {:handler handler, :name :test/route34}] + ["/v1/users/:user-id/devices/:client-id" {:handler handler, :name :test/route35}] + ["/v1/users/:user-id" {:handler handler, :name :test/route36}] + ["/v1/orgs/:org-id/devices" {:handler handler, :name :test/route37}] + ["/v1/orgs/:org-id/members" {:handler handler, :name :test/route38}] + #_["/v1/orgs/:org-id/members/invitation-data/:user-id" {:handler handler, :name :test/route39}] + ["/v2/orgs/:org-id/topics" {:handler handler, :name :test/route40}] + ["/v1/whoami" {:handler handler, :name :test/route41}] + ["/v1/orgs/:org-id" {:handler handler, :name :test/route42}] + ["/v1/users/:user-id/api-key" {:handler handler, :name :test/route43}] + ["/v2/schemas" {:handler handler, :name :test/route44}] + ["/v2/users/:user-id/topics" {:handler handler, :name :test/route45}] + ["/v1/orgs/:org-id/confirm-membership/:token" {:handler handler, :name :test/route46}] + ["/v2/topics/:topic" {:handler handler, :name :test/route47}] + ["/v1/messages/topic/:topic" {:handler handler, :name :test/route48}] + ["/v1/users/:user-id/devices/:client-id/reset-password" {:handler handler, :name :test/route49}] + ["/v2/topics" {:handler handler, :name :test/route50}] + ["/v1/login" {:handler handler, :name :test/route51}] + ["/v1/users/:user-id/orgs" {:handler handler, :name :test/route52}] + ["/v2/public/messages/dataset/:dataset-id" {:handler handler, :name :test/route53}] + ["/v1/topics" {:handler handler, :name :test/route54}] + ["/v1/orgs" {:handler handler, :name :test/route55}] + ["/v1/users/:user-id/bookmarks" {:handler handler, :name :test/route56}] + ["/v1/orgs/:org-id/topics" {:handler handler, :name :test/route57}]]) + +(def opensensors-bidi-routes + ["/" {"v1/" {"public/" {["topics/" :topic] :test/route4 + ["users/" :user-id] :test/route16 + ["orgs/" :org-id] :test/route18} + ["users/" :user-id] {["/orgs/" :org-id] :test/route5 + "/invitations" :test/route7 + "/topics" :route9 + "/bookmarks/followers" :test/route10 + "/devices" {"" :route15 + #_#_"/bulk" :test/route21 + ["/" :client-id] :test/route35 + ["/" :client-id "/reset-password"] :test/route49} + "/device-errors" :test/route22 + "/usage-stats" :test/route24 + ["/claim-device/" :client-id] :test/route26 + "/owned-orgs" :test/route31 + ["/bookmark/" :topic] :test/route33 + "" :test/route36 + "/orgs" :test/route52 + "/api-key" :test/route43 + "/bookmarks" :test/route56} + ["search/topics/" :term] :test/route6 + "orgs" {"" :test/route55 + ["/" :org-id] {"/devices" {"" :test/route37 + ["/" :device-id] :test/route13 + #_#_["/" :batch "/" :type] :test/route8} + "/usage-stats" :test/route12 + "/invitations" :test/route19 + "/members" {["/" :user-id] :test/route34 + "" :test/route38 + #_#_["/invitation-data/" :user-id] :test/route39} + "/errors" :test/route17 + "" :test/route42 + ["/confirm-membership/" :token] :test/route46 + "/topics" :test/route57}} + "messages/" {["user/" :user-id] :test/route14 + ["device/" :client-id] :test/route30 + ["topic/" :topic] :test/route48} + "topics" {["/" :topic] :test/route32 + "" :test/route54} + "whoami" :test/route41 + "login" :test/route51} + "v2/" {"whoami" :test/route1 + ["users/" :user-id "/"] {"datasets" :test/route2 + "devices" :test/route25 + "topics" {"/bulk" :test/route29 + "" :test/route45}} + "public/" {["projects/" :project-id] {"/datasets" :test/route3 + "" :test/route27} + "messages/dataset/bulk" :test/route20 + ["datasets/" :dataset-id] :test/route28 + ["messages/dataset/" :dataset-id] :test/route53} + ["datasets/" :dataset-id] :test/route11 + "login" :test/route23 + ["orgs/" :org-id "/topics"] :test/route40 + "schemas" :test/route44 + ["topics/" :topic] :test/route47 + "topics" :test/route50}}]) + +(def opensensors-ataraxy-routes + (ataraxy/compile + '{"/v1/" {"public/" {["topics/" topic] [:test/route4 topic] + ["users/" user-id] [:test/route16 user-id] + ["orgs/" org-id] [:test/route18 org-id]} + ["users/" user-id] {["/orgs/" org-id] [:test/route5 user-id org-id] + "/invitations" [:test/route7 user-id] + "/topics" [:route9 user-id] + "/bookmarks/followers" [:test/route10 user-id] + "/devices" {"" [:route15 user-id] + #_#_"/bulk" [:test/route21 user-id] + ["/" client-id] [:test/route35 user-id client-id] + ["/" client-id "/reset-password"] [:test/route49 user-id client-id]} + "/device-errors" [:test/route22 user-id] + "/usage-stats" [:test/route24 user-id] + ["/claim-device/" client-id] [:test/route26 user-id client-id] + "/owned-orgs" [:test/route31 user-id] + ["/bookmark/" topic] [:test/route33 user-id topic] + "" [:test/route36 user-id] + "/orgs" [:test/route52 user-id] + "/api-key" [:test/route43 user-id] + "/bookmarks" [:test/route56 user-id]} + ["search/topics/" term] [:test/route6 term] + "orgs" {"" [:test/route55] + ["/" org-id] {"/devices" {"" [:test/route37 org-id] + ["/" device-id] [:test/route13 org-id device-id] + #_#_["/" batch "/" type] [:test/route8 org-id batch type]} + "/usage-stats" [:test/route12 org-id] + "/invitations" [:test/route19 org-id] + "/members" {["/" user-id] [:test/route34 org-id user-id] + "" [:test/route38 org-id] + #_#_["/invitation-data/" user-id] [:test/route39 org-id user-id]} + "/errors" [:test/route17 org-id] + "" [:test/route42 org-id] + ["/confirm-membership/" token] [:test/route46 org-id token] + "/topics" [:test/route57 org-id]}} + "messages/" {["user/" user-id] [:test/route14 user-id] + ["device/" client-id] [:test/route30 client-id] + ["topic/" topic] [:test/route48 topic]} + "topics" {["/" topic] [:test/route32 topic] + "" [:test/route54]} + "whoami" [:test/route41] + "login" [:test/route51]} + "/v2/" {"whoami" [:test/route1] + ["users/" user-id] {"/datasets" [:test/route2 user-id] + "/devices" [:test/route25 user-id] + "topics/" {"bulk" [:test/route29 user-id] + "" [:test/route45 user-id]}} + "public/" {["projects/" project-id] {"/datasets" [:test/route3 project-id] + "" [:test/route27 project-id]} + "messages/dataset/bulk" [:test/route20] + ["datasets/" dataset-id] [:test/route28 dataset-id] + ["messages/dataset/" dataset-id] [:test/route53 dataset-id]} + ["datasets/" dataset-id] [:test/route11 dataset-id] + "login" [:test/route23] + ["orgs/" org-id "/topics"] [:test/route40 org-id] + "schemas" [:test/route44] + ["topics/" topic] [:test/route47 topic] + "topics" [:test/route50]}})) + +(def opensensors-compojure-api-routes + (routes + (context "/v1" [] + (context "/public" [] + (ANY "/topics/:topic" [] {:name :test/route4} handler) + (ANY "/users/:user-id" [] {:name :test/route16} handler) + (ANY "/orgs/:org-id" [] {:name :test/route18} handler)) + (context "/users/:user-id" [] + (ANY "/orgs/:org-id" [] {:name :test/route5} handler) + (ANY "/invitations" [] {:name :test/route7} handler) + (ANY "/topics" [] {:name :test/route9} handler) + (ANY "/bookmarks/followers" [] {:name :test/route10} handler) + (context "/devices" [] + (ANY "/" [] {:name :test/route15} handler) + #_(ANY "/bulk" [] {:name :test/route21} handler) + (ANY "/:client-id" [] {:name :test/route35} handler) + (ANY "/:client-id/reset-password" [] {:name :test/route49} handler)) + (ANY "/device-errors" [] {:name :test/route22} handler) + (ANY "/usage-stats" [] {:name :test/route24} handler) + (ANY "/claim-device/:client-id" [] {:name :test/route26} handler) + (ANY "/owned-orgs" [] {:name :test/route31} handler) + (ANY "/bookmark/:topic" [] {:name :test/route33} handler) + (ANY "/" [] {:name :test/route36} handler) + (ANY "/orgs" [] {:name :test/route52} handler) + (ANY "/api-key" [] {:name :test/route43} handler) + (ANY "/bookmarks" [] {:name :test/route56} handler)) + (ANY "/search/topics/:term" [] {:name :test/route6} handler) + (context "/orgs" [] + (ANY "/" [] {:name :test/route55} handler) + (context "/:org-id" [] + (context "/devices" [] + (ANY "/" [] {:name :test/route37} handler) + (ANY "/:device-id" [] {:name :test/route13} handler) + #_(ANY "/:batch/:type" [] {:name :test/route8} handler)) + (ANY "/usage-stats" [] {:name :test/route12} handler) + (ANY "/invitations" [] {:name :test/route19} handler) + (context "/members" [] + (ANY "/:user-id" [] {:name :test/route34} handler) + (ANY "/" [] {:name :test/route38} handler) + #_(ANY "/invitation-data/:user-id" [] {:name :test/route39} handler)) + (ANY "/errors" [] {:name :test/route17} handler) + (ANY "/" [] {:name :test/route42} handler) + (ANY "/confirm-membership/:token" [] {:name :test/route46} handler) + (ANY "/topics" [] {:name :test/route57} handler))) + (context "/messages" [] + (ANY "/user/:user-id" [] {:name :test/route14} handler) + (ANY "/device/:client-id" [] {:name :test/route30} handler) + (ANY "/topic/:topic" [] {:name :test/route48} handler)) + (context "/topics" [] + (ANY "/:topic" [] {:name :test/route32} handler) + (ANY "/" [] {:name :test/route54} handler)) + (ANY "/whoami" [] {:name :test/route41} handler) + (ANY "/login" [] {:name :test/route51} handler)) + (context "/v2" [] + (ANY "/whoami" [] {:name :test/route1} handler) + (context "/users/:user-id" [] + (ANY "/datasets" [] {:name :test/route2} handler) + (ANY "/devices" [] {:name :test/route25} handler) + (context "/topics" [] + (ANY "/bulk" [] {:name :test/route29} handler) + (ANY "/" [] {:name :test/route54} handler)) + (ANY "/" [] {:name :test/route45} handler)) + (context "/public" [] + (context "/projects/:project-id" [] + (ANY "/datasets" [] {:name :test/route3} handler) + (ANY "/" [] {:name :test/route27} handler)) + (ANY "/messages/dataset/bulk" [] {:name :test/route20} handler) + (ANY "/datasets/:dataset-id" [] {:name :test/route28} handler) + (ANY "/messages/dataset/:dataset-id" [] {:name :test/route53} handler)) + (ANY "/datasets/:dataset-id" [] {:name :test/route11} handler) + (ANY "/login" [] {:name :test/route23} handler) + (ANY "/orgs/:org-id/topics" [] {:name :test/route40} handler) + (ANY "/schemas" [] {:name :test/route44} handler) + (ANY "/topics/:topic" [] {:name :test/route47} handler) + (ANY "/topics" [] {:name :test/route50} handler)))) + +(def opensensors-pedestal-routes + (map-tree/router + (table/table-routes + [["/v2/whoami" :get handler :route-name :test/route1] + ["/v2/users/:user-id/datasets" :get handler :route-name :test/route2] + ["/v2/public/projects/:project-id/datasets" :get handler :route-name :test/route3] + ["/v1/public/topics/:topic" :get handler :route-name :test/route4] + ["/v1/users/:user-id/orgs/:org-id" :get handler :route-name :test/route5] + ["/v1/search/topics/:term" :get handler :route-name :test/route6] + ["/v1/users/:user-id/invitations" :get handler :route-name :test/route7] + #_["/v1/orgs/:org-id/devices/:batch/:type" :get handler :route-name :test/route8] + ["/v1/users/:user-id/topics" :get handler :route-name :test/route9] + ["/v1/users/:user-id/bookmarks/followers" :get handler :route-name :test/route10] + ["/v2/datasets/:dataset-id" :get handler :route-name :test/route11] + ["/v1/orgs/:org-id/usage-stats" :get handler :route-name :test/route12] + ["/v1/orgs/:org-id/devices/:client-id" :get handler :route-name :test/route13] + ["/v1/messages/user/:user-id" :get handler :route-name :test/route14] + ["/v1/users/:user-id/devices" :get handler :route-name :test/route15] + ["/v1/public/users/:user-id" :get handler :route-name :test/route16] + ["/v1/orgs/:org-id/errors" :get handler :route-name :test/route17] + ["/v1/public/orgs/:org-id" :get handler :route-name :test/route18] + ["/v1/orgs/:org-id/invitations" :get handler :route-name :test/route19] + ["/v2/public/messages/dataset/bulk" :get handler :route-name :test/route20] + #_["/v1/users/:user-id/devices/bulk" :get handler :route-name :test/route21] + ["/v1/users/:user-id/device-errors" :get handler :route-name :test/route22] + ["/v2/login" :get handler :route-name :test/route23] + ["/v1/users/:user-id/usage-stats" :get handler :route-name :test/route24] + ["/v2/users/:user-id/devices" :get handler :route-name :test/route25] + ["/v1/users/:user-id/claim-device/:client-id" :get handler :route-name :test/route26] + ["/v2/public/projects/:project-id" :get handler :route-name :test/route27] + ["/v2/public/datasets/:dataset-id" :get handler :route-name :test/route28] + ["/v2/users/:user-id/topics/bulk" :get handler :route-name :test/route29] + ["/v1/messages/device/:client-id" :get handler :route-name :test/route30] + ["/v1/users/:user-id/owned-orgs" :get handler :route-name :test/route31] + ["/v1/topics/:topic" :get handler :route-name :test/route32] + ["/v1/users/:user-id/bookmark/:topic" :get handler :route-name :test/route33] + ["/v1/orgs/:org-id/members/:user-id" :get handler :route-name :test/route34] + ["/v1/users/:user-id/devices/:client-id" :get handler :route-name :test/route35] + ["/v1/users/:user-id" :get handler :route-name :test/route36] + ["/v1/orgs/:org-id/devices" :get handler :route-name :test/route37] + ["/v1/orgs/:org-id/members" :get handler :route-name :test/route38] + #_["/v1/orgs/:org-id/members/invitation-data/:user-id" :get handler :route-name :test/route39] + ["/v2/orgs/:org-id/topics" :get handler :route-name :test/route40] + ["/v1/whoami" :get handler :route-name :test/route41] + ["/v1/orgs/:org-id" :get handler :route-name :test/route42] + ["/v1/users/:user-id/api-key" :get handler :route-name :test/route43] + ["/v2/schemas" :get handler :route-name :test/route44] + ["/v2/users/:user-id/topics" :get handler :route-name :test/route45] + ["/v1/orgs/:org-id/confirm-membership/:token" :get handler :route-name :test/route46] + ["/v2/topics/:topic" :get handler :route-name :test/route47] + ["/v1/messages/topic/:topic" :get handler :route-name :test/route48] + ["/v1/users/:user-id/devices/:client-id/reset-password" :get handler :route-name :test/route49] + ["/v2/topics" :get handler :route-name :test/route50] + ["/v1/login" :get handler :route-name :test/route51] + ["/v1/users/:user-id/orgs" :get handler :route-name :test/route52] + ["/v2/public/messages/dataset/:dataset-id" :get handler :route-name :test/route53] + ["/v1/topics" :get handler :route-name :test/route54] + ["/v1/orgs" :get handler :route-name :test/route55] + ["/v1/users/:user-id/bookmarks" :get handler :route-name :test/route56] + ["/v1/orgs/:org-id/topics" :get handler :route-name :test/route57]]))) + +(comment + (pedestal/find-route + (map-tree/router + (table/table-routes + [["/v1/orgs/:org-id/members/:user-id" :get (constantly "") :route-name :test/route34] + ["/v1/orgs/:org-id/members/invitation-data/:user-id" :get (constantly "") :route-name :test/route39]])) + {:path-info "/v1/orgs/0/members/invitation-data/1" :request-method :get}) + + (require '[io.pedestal.http.route.definition.table :as table]) + (require '[io.pedestal.http.route.map-tree :as map-tree]) + (require '[io.pedestal.http.route.router :as pedestal]) + + (pedestal/find-route + (map-tree/router + (table/table-routes + [["/:a" :get (constantly "") :route-name ::ping] + ["/evil/ping" :get (constantly "") :route-name ::evil-ping]])) + {:path-info "/evil/ping" :request-method :get})) + +(doseq [route (valid-urls (reitit/router opensensors-routes))] + (let [match (pedestal/find-route opensensors-pedestal-routes {:path-info route :request-method :get})] + (if-not match + (println route)))) + +(comment + (bench opensensors-routes false) + (bench opensensors-routes true)) + +(comment + + (doseq [route (valid-urls (reitit/router opensensors-routes))] + (let [app (ring/ring-handler (ring/router opensensors-routes)) + match (app {:uri route :request-method :get})] + (if-not match + (println route)))) + + (doseq [route (valid-urls (reitit/router opensensors-routes))] + (let [match (bidi/match-route opensensors-bidi-routes route)] + (if-not match + (println route)))) + + (doseq [route (valid-urls (reitit/router opensensors-routes))] + (let [match (ataraxy/matches opensensors-ataraxy-routes {:uri route})] + (if-not match + (println route)))) + + (doseq [route (valid-urls (reitit/router opensensors-routes))] + (let [match (opensensors-compojure-api-routes {:uri route :request-method :get})] + (if-not match + (println route)))) + + (doseq [route (valid-urls (reitit/router opensensors-routes))] + (let [match (pedestal/find-route opensensors-pedestal-routes {:path-info route :request-method :get})] + (if-not match + (println route))))) + +(defn bench! [routes verbose? name f] + (System/gc) + (println) + (suite name) + (println) + (let [times (for [[path time] (bench-routes routes f)] + (do + (when verbose? (println (format "%7s" time) "\t" path)) + time))] + (title (str "average: " (int (/ (reduce + times) (count times))))))) + +(defn bench-rest! [] + (let [routes opensensors-routes + router (reitit/router routes) + reitit-f #(reitit/match-by-path router %) + reitit-ring-f (let [app (ring/ring-handler (ring/router opensensors-routes))] + #(app {:uri % :request-method :get})) + bidi-f #(bidi/match-route opensensors-bidi-routes %) + ataraxy-f #(ataraxy/matches opensensors-ataraxy-routes {:uri %}) + compojure-api-f #(opensensors-compojure-api-routes {:uri % :request-method :get}) + pedestal-f #(pedestal/find-route opensensors-pedestal-routes {:path-info % :request-method :get})] + + (bench! routes true "reitit" reitit-f) ;; 2538ns 10% + (bench! routes true "pedestal" pedestal-f) ;; 2737ns 11% + (bench! routes true "reitit-ring" reitit-ring-f) ;; 2845ns 11% + (bench! routes true "compojure-api" compojure-api-f) ;; 10215ns 41% + (bench! routes true "bidi" bidi-f) ;; 19298ns 77% + (bench! routes true "ataraxy" ataraxy-f) ;; 24950ns 100% + + )) + +(comment + (bench-rest!)) + +;; +;; CQRSish +;; + +(def commands #{:upsert-appeal :upsert-appeal-verdict :delete-appeal :delete-appeal-verdict :mark-seen :mark-everything-seen :upsert-application-handler :remove-application-handler :cancel-inforequest :cancel-application :cancel-application-authority :undo-cancellation :request-for-complement :cleanup-krysp :submit-application :refresh-ktj :save-application-drawings :create-application :add-operation :update-op-description :change-primary-operation :change-permit-sub-type :change-location :change-application-state :return-to-draft :change-warranty-start-date :change-warranty-end-date :add-link-permit :remove-link-permit-by-app-id :create-change-permit :create-continuation-period-permit :convert-to-application :add-bulletin-comment :move-to-proclaimed :move-to-verdict-given :move-to-final :save-proclaimed-bulletin :save-verdict-given-bulletin :set-municipality-hears-neighbors :archive-documents :mark-pre-verdict-phase-archived :save-asianhallinta-config :create-assignment :update-assignment :complete-assignment :bind-attachment :bind-attachments :set-attachment-type :approve-attachment :reject-attachment :reject-attachment-note :create-attachments :create-ram-attachment :delete-attachment :delete-attachment-version :upload-attachment :rotate-pdf :upsert-stamp-template :delete-stamp-template :stamp-attachments :sign-attachments :set-attachment-meta :set-attachment-not-needed :set-attachments-as-verdict-attachment :set-attachment-as-construction-time :set-attachment-visibility :convert-to-pdfa :invite-with-role :approve-invite :decline-invitation :remove-auth :change-auth :unsubscribe-notifications :subscribe-notifications :set-calendar-enabled-for-authority :create-calendar-slots :update-calendar-slot :delete-calendar-slot :add-reservation-type-for-organization :update-reservation-type :delete-reservation-type :reserve-calendar-slot :accept-reservation :decline-reservation :cancel-reservation :mark-reservation-update-seen :add-campaign :delete-campaign :change-email-init :change-email :can-target-comment-to-authority :can-mark-answered :add-comment :company-update :company-lock :company-user-update :company-user-delete :company-user-delete-all :company-invite-user :company-add-user :company-invite :company-cancel-invite :save-company-tags :update-application-company-notes :inform-construction-started :inform-construction-ready :copy-application :update-3d-map-server-details :set-3d-map-enabled :redirect-to-3d-map :create-archiving-project :submit-archiving-project :create-doc :remove-doc :set-doc-status :update-doc :update-task :remove-document-data :approve-doc :reject-doc :reject-doc-note :set-user-to-document :set-current-user-to-document :set-company-to-document :set-feature :remove-uploaded-file :create-foreman-application :update-foreman-other-applications :link-foreman-task :update-guest-authority-organization :remove-guest-authority-organization :invite-guest :toggle-guest-subscription :delete-guest-application :info-link-delete :info-link-reorder :info-link-upsert :mark-seen-organization-links :create-inspection-summary-template :delete-inspection-summary-template :modify-inspection-summary-template :set-inspection-summary-template-for-operation :create-inspection-summary :delete-inspection-summary :toggle-inspection-summary-locking :add-target-to-inspection-summary :edit-inspection-summary-target :remove-target-from-inspection-summary :set-target-status :set-inspection-date :approve-application :move-attachments-to-backing-system :parties-as-krysp :merge-details-from-krysp :application-to-asianhallinta :attachments-to-asianhallinta :order-verdict-attachment-prints :frontend-log :reset-frontend-log :new-verdict-template :set-verdict-template-name :save-verdict-template-draft-value :publish-verdict-template :toggle-delete-verdict-template :copy-verdict-template :save-verdict-template-settings-value :add-verdict-template-review :update-verdict-template-review :add-verdict-template-plan :update-verdict-template-plan :set-default-operation-verdict-template :upsert-phrase :delete-phrase :neighbor-add :neighbor-add-owners :neighbor-update :neighbor-remove :neighbor-send-invite :neighbor-mark-done :neighbor-response :change-urgency :add-authority-notice :add-application-tags :init-sign :cancel-sign :convert-to-normal-inforequests :update-organization :add-scope :create-organization :add-organization-link :update-organization-link :remove-organization-link :update-allowed-autologin-ips :set-organization-selected-operations :organization-operations-attachments :set-organization-app-required-fields-filling-obligatory :set-automatic-ok-for-attachments :set-organization-assignments :set-organization-inspection-summaries :set-organization-extended-construction-waste-report :set-organization-validate-verdict-given-date :set-organization-use-attachment-links-integration :set-organization-calendars-enabled :set-organization-boolean-attribute :set-organization-permanent-archive-start-date :set-organization-neighbor-order-email :set-organization-submit-notification-email :set-organization-inforequest-notification-email :set-organization-default-reservation-location :set-krysp-endpoint :set-kopiolaitos-info :save-vendor-backend-redirect-config :update-organization-name :save-organization-tags :update-map-server-details :update-user-layers :update-suti-server-details :section-toggle-enabled :section-toggle-operation :upsert-handler-role :toggle-handler-role :upsert-assignment-trigger :remove-assignment-trigger :update-docstore-info :browser-timing :create-application-from-previous-permit :screenmessages-add :screenmessages-reset :add-single-sign-on-key :update-single-sign-on-key :remove-single-sign-on-key :create-statement-giver :delete-statement-giver :request-for-statement :ely-statement-request :delete-statement :save-statement-as-draft :give-statement :request-for-statement-reply :save-statement-reply-as-draft :reply-statement :suti-toggle-enabled :suti-toggle-operation :suti-www :suti-update-id :suti-update-added :create-task :delete-task :approve-task :reject-task :review-done :mark-review-faulty :resend-review-to-backing-system :set-tos-function-for-operation :remove-tos-function-from-operation :set-tos-function-for-application :force-fix-tos-function-for-application :store-tos-metadata-for-attachment :store-tos-metadata-for-application :store-tos-metadata-for-process :set-myyntipalvelu-for-attachment :create-user :create-rest-api-user :update-user :applicant-to-authority :update-default-application-filter :save-application-filter :remove-application-filter :update-user-organization :remove-user-organization :update-user-roles :check-password :change-passwd :reset-password :admin-reset-password :set-user-enabled :login :impersonate-authority :register-user :confirm-account-link :retry-rakentajafi :remove-user-attachment :copy-user-attachments-to-application :remove-user-notification :notifications-update :check-for-verdict :new-verdict-draft :save-verdict-draft :publish-verdict :delete-verdict :sign-verdict :create-digging-permit}) + +(def queries #{:comments :actions :allowed-actions :allowed-actions-for-category :admin-attachment-report :appeals :application :application-authorities :application-commenters :enable-accordions :party-document-names :application-submittable :inforequest-markers :change-application-state-targets :link-permit-required :app-matches-for-link-permits :all-operations-in :application-handlers :application-organization-handler-roles :application-organization-archive-enabled :application-bulletins :application-bulletin-municipalities :application-bulletin-states :bulletin :bulletin-versions :bulletin-comments :publish-bulletin-enabled :municipality-hears-neighbors-visible :applications-search :applications-search-default :applications-for-new-appointment-page :get-application-operations :applications :latest-applications :event-search :tasks-tab-visible :application-info-tab-visible :application-summary-tab-visible :application-verdict-tab-visible :document-states :archiving-operations-enabled :permanent-archive-enabled :application-in-final-archiving-state :asianhallinta-config :assignments-for-application :assignment-targets :assignments-search :assignment-count :assignments :assignment :bind-attachments-job :attachments :attachment :attachment-groups :attachments-filters :attachments-tag-groups :attachment-types :ram-linked-attachments :attachment-operations :stamp-templates :custom-stamps :stamp-attachments-job :signing-possible :set-attachment-group-enabled :invites :my-calendars :calendar :calendars-for-authority-admin :calendar-slots :reservation-types-for-organization :available-calendar-slots :application-calendar-config :calendar-actions-required :applications-with-appointments :my-reserved-slots :campaigns :campaign :company :company-users-for-person-selector :company-tags :companies :user-company-locked :company-search-user :remove-company-tag-ok :company-notes :enable-company-search :info-construction-status :copy-application-invite-candidates :application-copyable-to-location :application-copyable :source-application :user-is-pure-digitizer :digitizing-enabled :document :validate-doc :fetch-validation-errors :schemas :features :apply-fixture :foreman-history :foreman-applications :resolve-guest-authority-candidate :guest-authorities-organization :application-guests :guest-authorities-application-organization :get-link-account-token :info-links :organization-links :organization-inspection-summary-settings :inspection-summaries-for-application :get-building-info-from-wfs :external-api-enabled :integration-messages :ely-statement-types :frontend-log-entries :newest-version :verdict-templates :verdict-template-categories :verdict-template :verdict-template-settings :verdict-template-reviews :verdict-template-plans :default-operation-verdict-templates :organization-phrases :application-phrases :owners :application-property-owners :municipality-borders :active-municipalities :municipality-active :neighbor-application :authority-notice :find-sign-process :organization-by-user :all-attachment-types-by-user :organization-name-by-user :user-organizations-for-permit-type :user-organizations-for-archiving-project :organizations :allowed-autologin-ips-for-organization :organization-by-id :permit-types :municipalities-with-organization :municipalities :all-operations-for-organization :selected-operations-for-municipality :addable-operations :organization-details :krysp-config :kopiolaitos-config :get-organization-names :vendor-backend-redirect-config :remove-tag-ok :get-organization-tags :get-organization-areas :get-map-layers-data :municipality-for-property :property-borders :screenmessages :get-single-sign-on-keys :get-organizations-statement-givers :get-possible-statement-statuses :get-statement-givers :statement-replies-enabled :statement-is-replyable :authorized-for-requesting-statement-reply :statement-attachment-allowed :statements-after-approve-allowed :neighbors-statement-enabled :suti-admin-details :suti-operations :suti-application-data :suti-application-products :suti-pre-sent-state :task-types-for-application :review-can-be-marked-done :is-end-review :available-tos-functions :tos-metadata-schema :case-file-data :tos-operations-enabled :common-area-application :user :users :users-in-same-organizations :user-by-email :users-for-datatables :saved-application-filters :redirect-after-login :user-attachments :add-user-attachment-allowed :email-in-use :enable-foreman-search :calendars-enabled :verdict-attachment-type :selected-digging-operations-for-organization :ya-extensions :approve-ya-extension}) + +(def cqrs-routes + (mapv (fn [command] [(str "/command/" (name command)) {:post handler :name command}]) commands)) + +(def cqrs-routes-pedestal + (map-tree/router + (table/table-routes + (mapv (fn [command] [(str "/command/" (name command)) :post handler :route-name command]) commands)))) + +(class (:tree-map cqrs-routes-pedestal)) + +(class (:data (ring/router cqrs-routes))) + +(comment + + (doseq [route (valid-urls (reitit/router cqrs-routes))] + (let [app (ring/ring-handler (ring/router cqrs-routes)) + match (app {:uri route :request-method :post})] + (if-not match + (println route)))) + + (doseq [route (valid-urls (reitit/router cqrs-routes))] + (let [match (pedestal/find-route cqrs-routes-pedestal {:path-info route :request-method :post})] + (if-not match + (println route))))) + +(defn bench-cqrs! [] + (let [routes cqrs-routes + router (reitit/router cqrs-routes) + reitit-f #(reitit/match-by-path router %) + reitit-ring-f (let [app (ring/ring-handler (ring/router routes))] + #(app {:uri % :request-method :post})) + pedestal-f #(pedestal/find-route cqrs-routes-pedestal {:path-info % :request-method :post})] + + ;; 125ns + ;; 62ns (fast-map) + (bench! routes false "reitit" reitit-f) + + ;; 272ns + ;; 219ns (fast-assoc) + ;; 171ns (fast-map) + (bench! routes false "reitit-ring" reitit-ring-f) + + ;; 172ns + (bench! routes false "pedestal" pedestal-f))) + +(comment + (bench-cqrs!)) + diff --git a/src/reitit/core.cljc b/src/reitit/core.cljc index cdd36186..6ee1c63a 100644 --- a/src/reitit/core.cljc +++ b/src/reitit/core.cljc @@ -60,38 +60,45 @@ (cond->> (->> (walk data opts) (map-meta merge-meta)) coerce (into [] (keep #(coerce % opts))))) +(defn name-lookup [[_ {:keys [name]}] opts] + (if name #{name})) + +(defn find-names [routes opts] + (into [] (keep #(-> % second :name) routes))) + (defn compile-route [[p m :as route] {:keys [compile] :as opts}] [p m (if compile (compile route opts))]) (defprotocol Routing + (router-type [this]) (routes [this]) + (options [this]) + (route-names [this]) (match-by-path [this path]) - (match-by-name [this name] [this name parameters])) + (match-by-name [this name] [this name params])) -(defrecord Match [template meta path handler params]) +(defrecord Match [template meta handler params path]) +(defrecord PartialMatch [template meta handler params required]) + +(defn partial-match? [x] + (instance? PartialMatch x)) + +(defn match-by-name! + ([this name] + (match-by-name! this name nil)) + ([this name params] + (if-let [match (match-by-name this name params)] + (if-not (partial-match? match) + match + (impl/throw-on-missing-path-params + (:template match) (:required match) params))))) (def default-router-options - {:expand expand + {:lookup name-lookup + :expand expand :coerce (fn [route _] route) :compile (fn [[_ {:keys [handler]}] _] handler)}) -(defrecord LinearRouter [routes data lookup] - Routing - (routes [_] - routes) - (match-by-path [_ path] - (reduce - (fn [acc ^Route route] - (if-let [params ((:matcher route) path)] - (reduced (->Match (:path route) (:meta route) path (:handler route) params)))) - nil data)) - (match-by-name [_ name] - (if-let [match (lookup name)] - (match nil))) - (match-by-name [_ name params] - (if-let [match (lookup name)] - (match params)))) - (defn linear-router "Creates a [[LinearRouter]] from resolved routes and optional expanded options. See [[router]] for available options" @@ -99,30 +106,42 @@ (linear-router routes {})) ([routes opts] (let [compiled (map #(compile-route % opts) routes) + names (find-names routes opts) [data lookup] (reduce (fn [[data lookup] [p {:keys [name] :as meta} handler]] - (let [route (impl/create [p meta handler])] + (let [{:keys [params] :as route} (impl/create [p meta handler]) + f #(if-let [path (impl/path-for route %)] + (->Match p meta handler % path) + (->PartialMatch p meta handler % params))] [(conj data route) - (if name - (assoc lookup name #(->Match p meta (impl/path-for route %) handler %)) - lookup)])) [[] {}] compiled)] - (->LinearRouter routes data lookup)))) - -(defrecord LookupRouter [routes data lookup] - Routing - (routes [_] - routes) - (match-by-path [_ path] - (data path)) - (match-by-name [_ name] - (if-let [match (lookup name)] - (match nil))) - (match-by-name [_ name params] - (if-let [match (lookup name)] - (match params)))) + (if name (assoc lookup name f) lookup)])) + [[] {}] compiled) + lookup (impl/fast-map lookup)] + (reify + Routing + (router-type [_] + :linear-router) + (routes [_] + routes) + (options [_] + opts) + (route-names [_] + names) + (match-by-path [_ path] + (reduce + (fn [acc ^Route route] + (if-let [params ((:matcher route) path)] + (reduced (->Match (:path route) (:meta route) (:handler route) params path)))) + nil data)) + (match-by-name [_ name] + (if-let [match (impl/fast-get lookup name)] + (match nil))) + (match-by-name [_ name params] + (if-let [match (impl/fast-get lookup name)] + (match params))))))) (defn lookup-router - "Creates a [[LookupRouter]] from resolved routes and optional + "Creates a LookupRouter from resolved routes and optional expanded options. See [[router]] for available options" ([routes] (lookup-router routes {})) @@ -134,13 +153,32 @@ {:route route :routes routes}))) (let [compiled (map #(compile-route % opts) routes) + names (find-names routes opts) [data lookup] (reduce (fn [[data lookup] [p {:keys [name] :as meta} handler]] - [(assoc data p (->Match p meta p handler {})) + [(assoc data p (->Match p meta handler {} p)) (if name - (assoc lookup name #(->Match p meta p handler %)) - lookup)]) [{} {}] compiled)] - (->LookupRouter routes data lookup)))) + (assoc lookup name #(->Match p meta handler % p)) + lookup)]) [{} {}] compiled) + data (impl/fast-map data) + lookup (impl/fast-map lookup)] + (reify Routing + (router-type [_] + :lookup-router) + (routes [_] + routes) + (options [_] + opts) + (route-names [_] + names) + (match-by-path [_ path] + (impl/fast-get data path)) + (match-by-name [_ name] + (if-let [match (impl/fast-get lookup name)] + (match nil))) + (match-by-name [_ name params] + (if-let [match (impl/fast-get lookup name)] + (match params))))))) (defn router "Create a [[Router]] from raw route data and optionally an options map. @@ -153,8 +191,8 @@ | `:routes` | Initial resolved routes (default `[]`) | `:meta` | Initial expanded route-meta vector (default `[]`) | `:expand` | Function of `arg opts => meta` to expand route arg to route meta-data (default `reitit.core/expand`) - | `:coerce` | Function of `[path meta] opts => [path meta]` to coerce resolved route, can throw or return `nil` - | `:compile` | Function of `[path meta] opts => handler` to compile a route handler" + | `:coerce` | Function of `route opts => route` to coerce resolved route, can throw or return `nil` + | `:compile` | Function of `route opts => handler` to compile a route handler" ([data] (router data {})) ([data opts] diff --git a/src/reitit/impl.cljc b/src/reitit/impl.cljc index 38fb5577..0e95c253 100644 --- a/src/reitit/impl.cljc +++ b/src/reitit/impl.cljc @@ -122,12 +122,28 @@ :handler handler}))) (defn path-for [^Route route params] - (when-not (every? #(contains? params %) (:params route)) + (if-let [required (:params route)] + (if (every? #(contains? params %) required) + (str "/" (str/join \/ (map #(get (or params {}) % %) (:parts route))))) + (:path route))) + +(defn throw-on-missing-path-params [template required params] + (when-not (every? #(contains? params %) required) (let [defined (-> params keys set) - required (:params route) missing (clojure.set/difference required defined)] (throw (ex-info - (str "missing path-params for route '" (:path route) "': " missing) - {:params params, :required required})))) - (str "/" (str/join \/ (map #(get params % %) (:parts route))))) + (str "missing path-params for route " template ": " missing) + {:params params, :required required}))))) + +(defn fast-assoc + #?@(:clj [[^clojure.lang.Associative a k v] (.assoc a k v)] + :cljs [[a k v] (assoc a k v)])) + +(defn fast-map [m] + #?@(:clj [(java.util.HashMap. m)] + :cljs [m])) + +(defn fast-get + #?@(:clj [[^java.util.HashMap m k] (.get m k)] + :cljs [[m k] (m k)])) diff --git a/src/reitit/ring.cljc b/src/reitit/ring.cljc index 98b52e9a..2badd934 100644 --- a/src/reitit/ring.cljc +++ b/src/reitit/ring.cljc @@ -1,7 +1,8 @@ (ns reitit.ring (:require [meta-merge.core :refer [meta-merge]] [reitit.middleware :as middleware] - [reitit.core :as reitit])) + [reitit.core :as reitit] + [reitit.impl :as impl])) (def http-methods #{:get :head :patch :delete :options :post :put}) (defrecord MethodHandlers [get head patch delete options post put]) @@ -18,10 +19,10 @@ (fn ([request] (if-let [match (reitit/match-by-path router (:uri request))] - ((:handler match) (assoc request ::match match)))) + ((:handler match) (impl/fast-assoc request ::match match)))) ([request respond raise] (if-let [match (reitit/match-by-path router (:uri request))] - ((:handler match) (assoc request ::match match) respond raise)))) + ((:handler match) (impl/fast-assoc request ::match match) respond raise)))) {::router router})) (defn get-router [handler] @@ -46,14 +47,13 @@ #(assoc %1 %2 (middleware/compile-handler [path (meta-merge top %3)] opts %2)) {} childs)) - default-handler (if (:handler top) (middleware/compile-handler [path meta] opts)) - resolved-handler #(or (% handlers) default-handler)] + default-handler (if (:handler top) (middleware/compile-handler [path meta] opts))] (fn ([request] - (if-let [handler (resolved-handler (:request-method request))] + (if-let [handler (or ((:request-method request) handlers) default-handler)] (handler request))) ([request respond raise] - (if-let [handler (resolved-handler (:request-method request))] + (if-let [handler (or ((:request-method request) handlers) default-handler)] (handler request respond raise)))))))) (defn router diff --git a/test/cljc/reitit/core_test.cljc b/test/cljc/reitit/core_test.cljc index 6e28e101..ba8c0dbf 100644 --- a/test/cljc/reitit/core_test.cljc +++ b/test/cljc/reitit/core_test.cljc @@ -1,17 +1,18 @@ (ns reitit.core-test (:require [clojure.test :refer [deftest testing is]] - [reitit.core :as reitit #?@(:cljs [:refer [Match LinearRouter LookupRouter]])]) + [reitit.core :as reitit #?@(:cljs [:refer [Match]])]) #?(:clj - (:import (reitit.core Match LinearRouter LookupRouter) + (:import (reitit.core Match) (clojure.lang ExceptionInfo)))) (deftest reitit-test (testing "linear router" (let [router (reitit/router ["/api" ["/ipa" ["/:size" ::beer]]])] - (is (instance? LinearRouter router)) + (is (= :linear-router (reitit/router-type router))) (is (= [["/api/ipa/:size" {:name ::beer}]] (reitit/routes router))) + (is (= true (map? (reitit/options router)))) (is (= (reitit/map->Match {:template "/api/ipa/:size" :meta {:name ::beer} @@ -25,17 +26,26 @@ :params {:size "large"}}) (reitit/match-by-name router ::beer {:size "large"}))) (is (= nil (reitit/match-by-name router "ILLEGAL"))) - (testing "name-based routing at runtime for missing parameters" + (is (= [::beer] (reitit/route-names router))) + (testing "name-based routing with missing parameters" + (is (= (reitit/map->PartialMatch + {:template "/api/ipa/:size" + :meta {:name ::beer} + :required #{:size} + :params nil}) + (reitit/match-by-name router ::beer))) + (is (= true (reitit/partial-match? (reitit/match-by-name router ::beer)))) (is (thrown-with-msg? ExceptionInfo - #"^missing path-params for route '/api/ipa/:size': \#\{:size\}$" - (reitit/match-by-name router ::beer)))))) + #"^missing path-params for route /api/ipa/:size: \#\{:size\}$" + (reitit/match-by-name! router ::beer)))))) (testing "lookup router" (let [router (reitit/router ["/api" ["/ipa" ["/large" ::beer]]])] - (is (instance? LookupRouter router)) + (is (= :lookup-router (reitit/router-type router))) (is (= [["/api/ipa/large" {:name ::beer}]] (reitit/routes router))) + (is (= true (map? (reitit/options router)))) (is (= (reitit/map->Match {:template "/api/ipa/large" :meta {:name ::beer} @@ -49,6 +59,7 @@ :params {:size "large"}}) (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 @@ -108,14 +119,14 @@ (let [pong (constantly "ok") routes ["/api" {:mw [:api]} ["/ping" :kikka] - ["/user/:id" {:parameters {:id String}} - ["/:sub-id" {:parameters {:sub-id String}}]] + ["/user/:id" {:parameters {:id "String"}} + ["/:sub-id" {:parameters {:sub-id "String"}}]] ["/pong" pong] ["/admin" {:mw [:admin] :roles #{:admin}} ["/user" {:roles ^:replace #{:user}}] ["/db" {:mw [:db]}]]] expected [["/api/ping" {:mw [:api], :name :kikka}] - ["/api/user/:id/:sub-id" {:mw [:api], :parameters {:id String, :sub-id String}}] + ["/api/user/:id/:sub-id" {:mw [:api], :parameters {:id "String", :sub-id "String"}}] ["/api/pong" {:mw [:api], :handler pong}] ["/api/admin/user" {:mw [:api :admin], :roles #{:user}}] ["/api/admin/db" {:mw [:api :admin :db], :roles #{:admin}}]] @@ -123,7 +134,7 @@ (is (= expected (reitit/resolve-routes routes {}))) (is (= (reitit/map->Match {:template "/api/user/:id/:sub-id" - :meta {:mw [:api], :parameters {:id String, :sub-id String}} + :meta {:mw [:api], :parameters {:id "String", :sub-id "String"}} :path "/api/user/1/2" :params {:id "1", :sub-id "2"}}) (reitit/match-by-path router "/api/user/1/2")))))) diff --git a/test/cljc/reitit/ring_test.cljc b/test/cljc/reitit/ring_test.cljc index bbd63e0b..641c40a7 100644 --- a/test/cljc/reitit/ring_test.cljc +++ b/test/cljc/reitit/ring_test.cljc @@ -2,7 +2,8 @@ (:require [clojure.test :refer [deftest testing is]] [reitit.middleware :as middleware] [reitit.ring :as ring] - [clojure.set :as set]) + [clojure.set :as set] + [reitit.core :as reitit]) #?(:clj (:import (clojure.lang ExceptionInfo)))) @@ -122,7 +123,30 @@ respond (partial reset! result), raise ::not-called] (app {:uri "/api/users" :request-method :post} respond raise) (is (= {:status 200, :body [:api :users :post :ok :post :users :api]} - @result))))))) + @result)))))) + + (testing "named routes" + (let [router (ring/router + [["/api" + ["/all" {:handler handler :name ::all}] + ["/get" {:get {:handler handler :name ::HIDDEN} + :name ::get}] + ["/users" {:get handler + :post handler + :handler handler + :name ::users}]]]) + app (ring/ring-handler router)] + + (testing "router can be extracted" + (is (= router (ring/get-router app)))) + + (testing "only top-level route names are matched" + (is (= [::all ::get ::users] + (reitit/route-names router)))) + + (testing "all named routes can be matched" + (doseq [name (reitit/route-names router)] + (is (= name (-> (reitit/match-by-name router name) :meta :name)))))))) (defn wrap-enforce-roles [handler] (fn [{:keys [::roles] :as request}]