Merge pull request #44 from metosin/trie

Prefix-tree router
This commit is contained in:
Tommi Reiman 2017-11-11 15:27:26 +00:00 committed by GitHub
commit 95b796e94c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 568 additions and 130 deletions

View file

@ -1,18 +1,25 @@
# Different Routers
Reitit ships with several different implementations for the `Router` protocol, originally based on the [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` option, see [configuring routers](advanced/configuring_routers.md).
Reitit ships with several different implementations for the `Router` protocol, originally based on the [Pedestal](https://github.com/pedestal/pedestal/tree/master/route) implementation. `router` function selects the most suitable implementation by inspecting the expanded routes. The implementation can be set manually using `:router` option, see [configuring routers](advanced/configuring_routers.md).
| 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` | Fast 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 [Route conflicts](../basics/route_conflicts.md).
| `::single-static-path-router` | Fastest possible router: valid only if there is one static route.
| `:prefix-tree-router` | TODO: https://github.com/julienschmidt/httprouter#how-does-it-work
| `:linear-router` | Matches the routes one-by-one starting from the top until a match is found. Works with any kind of routes. Slow, but works with all route trees.
| `:lookup-router` | Fast router, uses hash-lookup to resolve the route. Valid if no paths have path or catch-all parameters and there are no [Route conflicts](../basics/route_conflicts.md).
| `:mixed-router` | Creates internally a `:prefix-tree-router` and a `:lookup-router` and used them to effectively get best-of-both-worlds. Valid only if there are no [Route conflicts](../basics/route_conflicts.md).
| `::single-static-path-router` | Super fast router: sting-matches the route. Valid only if there is one static route.
| `:prefix-tree-router` | Router that creates a [prefix-tree](https://en.wikipedia.org/wiki/Radix_tree) out of an route table. Much faster than `:linear-router`. Valid only if there are no [Route conflicts](../basics/route_conflicts.md).
The router name can be asked from the router
The router name can be asked from the router:
```clj
(require '[reitit.core :as r])
(def router
(r/router
[["/ping" ::ping]
["/api/:users" ::users]]))
(r/router-name router)
; :mixed-router
```

View file

@ -5,7 +5,7 @@ Namespace `reitit.spec` contains [clojure.spec](https://clojure.org/about/spec)
**NOTE:** Use of specs requires to use one of the following:
* `[org.clojure/clojurescript "1.9.660"]` (or higher)
* `[org.clojure/clojure "1.9.0-beta2"]` (or higher)
* `[org.clojure/clojure "1.9.0-RC1"]` (or higher)
* `[clojure-future-spec "1.9.0-alpha17"]` (if Clojure 1.8 is used)
## Example

View file

@ -1,6 +1,7 @@
(ns reitit.core
(:require [meta-merge.core :refer [meta-merge]]
[clojure.string :as str]
[reitit.trie :as trie]
[reitit.impl :as impl #?@(:cljs [:refer [Route]])])
#?(:clj
(:import (reitit.impl Route))))
@ -216,6 +217,46 @@
(if-let [match (impl/fast-get lookup name)]
(match params)))))))
(defn prefix-tree-router
"Creates a prefix-tree router from resolved routes and optional
expanded options. See [[router]] for available options"
([routes]
(prefix-tree-router routes {}))
([routes opts]
(let [compiled (compile-routes routes opts)
names (find-names routes opts)
[node lookup] (reduce
(fn [[node lookup] [p {:keys [name] :as meta} result]]
(let [{:keys [params] :as route} (impl/create [p meta result])
f #(if-let [path (impl/path-for route %)]
(->Match p meta result % path)
(->PartialMatch p meta result % params))]
[(trie/insert node p (->Match p meta result nil nil))
(if name (assoc lookup name f) lookup)]))
[nil {}] compiled)
lookup (impl/fast-map lookup)]
(reify
Router
(router-name [_]
:prefix-tree-router)
(routes [_]
compiled)
(options [_]
opts)
(route-names [_]
names)
(match-by-path [_ path]
(if-let [match (trie/lookup node path {})]
(-> (:data match)
(assoc :params (:params match))
(assoc :path 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 single-static-path-router
"Creates a fast router of 1 static route(s) and optional
expanded options. See [[router]] for available options"
@ -252,16 +293,16 @@
(defn mixed-router
"Creates two routers: [[lookup-router]] or [[single-static-path-router]] for
static routes and [[linear-router]] for wildcard routes. All
static routes and [[prefix-tree-router]] for wildcard routes. All
routes should be non-conflicting. Takes resolved routes and optional
expanded options. See [[router]] for options."
([routes]
(mixed-router routes {}))
([routes opts]
(let [{linear true, lookup false} (group-by impl/wild-route? routes)
(let [{wild true, lookup false} (group-by impl/wild-route? routes)
compiled (compile-routes routes opts)
->static-router (if (= 1 (count lookup)) single-static-path-router lookup-router)
wildcard-router (linear-router linear opts)
wildcard-router (prefix-tree-router wild opts)
static-router (->static-router lookup opts)
names (find-names routes opts)]
(reify Router
@ -309,10 +350,10 @@
router (cond
router router
(and (= 1 (count routes)) (not wilds?)) single-static-path-router
conflicting linear-router
(not wilds?) lookup-router
all-wilds? linear-router
(not conflicting) mixed-router
:else linear-router)]
all-wilds? prefix-tree-router
:else mixed-router)]
(when-let [conflicts (:conflicts opts)]
(when conflicting (conflicts conflicting)))

View file

@ -69,10 +69,20 @@
;; (c) https://github.com/pedestal/pedestal/blob/master/route/src/io/pedestal/http/route/prefix_tree.clj
;;
(defn- wild? [s]
(defn wild? [s]
(contains? #{\: \*} (first s)))
(defn- partition-wilds
(defn wild-param?
"Return true if a string segment starts with a wildcard string."
[segment]
(= \: (first segment)))
(defn catch-all-param?
"Return true if a string segment starts with a catch-all string."
[segment]
(= \* (first segment)))
(defn partition-wilds
"Given a path-spec string, return a seq of strings with wildcards
and catch-alls separated into their own strings. Eats the forward
slash following a wildcard."
@ -161,7 +171,7 @@
:cljs [[a k v] (assoc a k v)]))
(defn fast-map [m]
#?@(:clj [(java.util.HashMap. m)]
#?@(:clj [(java.util.HashMap. (or m {}))]
:cljs [m]))
(defn fast-get

View file

@ -0,0 +1,223 @@
(ns reitit.trie
(:require [reitit.impl :as impl]))
;;
;; original https://github.com/pedestal/pedestal/blob/master/route/src/io/pedestal/http/route/prefix_tree.clj
;;
(declare insert)
(defn- char-key [s i]
(if (< i (count s))
(subs s i (inc i))))
(defn- maybe-wild-node [children]
(get children ":"))
(defn- maybe-catch-all-node [children]
(get children "*"))
(defprotocol Node
(lookup [this path params])
(get-segment [this])
(update-segment [this subs lcs])
(get-data [this])
(set-data [this data])
(get-chidren [this])
(add-child [this key child])
(insert-child [this key path-spec data]))
(extend-protocol Node
nil
(lookup [_ _ _])
(get-segment [_]))
(defrecord Match [data params])
(defn- wild-node [segment param children data]
(let [?wild (maybe-wild-node children)
?catch (maybe-catch-all-node children)
children' (impl/fast-map children)]
^{:type ::node}
(reify
Node
(lookup [_ path params]
(let [i (.indexOf ^String path "/")]
(if (pos? i)
(let [value (subs path 0 i)]
(let [child (impl/fast-get children' (char-key path (inc i)))
path' (subs path (inc i))
params (assoc params param value)]
(or (lookup child path' params)
(lookup ?wild path' params)
(lookup ?catch path' params))))
(->Match data (assoc params param path)))))
(get-segment [_]
segment)
(get-data [_]
data)
(set-data [_ data]
(wild-node segment param children data))
(get-chidren [_]
children)
(add-child [_ key child]
(wild-node segment param (assoc children key child) data))
(insert-child [_ key path-spec child-data]
(wild-node segment param (update children key insert path-spec child-data) data)))))
(defn- catch-all-node [segment children param data]
^{:type ::node}
(reify
Node
(lookup [_ path params]
(->Match data (assoc params param path)))
(get-segment [_]
segment)
(get-data [_]
data)
(get-chidren [_]
children)))
(defn- static-node [^String segment children data]
(let [size (count segment)
?wild (maybe-wild-node children)
?catch (maybe-catch-all-node children)
children' (impl/fast-map children)]
^{:type ::node}
(reify
Node
(lookup [_ path params]
(if (#?(:clj .equals, :cljs =) segment path)
(->Match data params)
(let [p (if (>= (count path) size) (subs path 0 size))]
(if (#?(:clj .equals, :cljs =) segment p)
(let [child (impl/fast-get children' (char-key path size))
path (subs path size)]
(or (lookup child path params)
(lookup ?wild path params)
(lookup ?catch path params)))))))
(get-segment [_]
segment)
(update-segment [_ subs lcs]
(static-node (subs segment lcs) children data))
(get-data [_]
data)
(set-data [_ data]
(static-node segment children data))
(get-chidren [_]
children)
(add-child [_ key child]
(static-node segment (assoc children key child) data))
(insert-child [_ key path-spec child-data]
(static-node segment (update children key insert path-spec child-data) data)))))
(defn- make-node
"Given a path-spec segment string and a payload object, return a new
tree node."
[segment data]
(cond
(impl/wild-param? segment)
(wild-node segment (keyword (subs segment 1)) nil data)
(impl/catch-all-param? segment)
(catch-all-node segment (keyword (subs segment 1)) nil data)
:else
(static-node segment nil data)))
(defn- new-node
"Given a path-spec and a payload object, return a new tree node. If
the path-spec contains wildcards or catch-alls, will return parent
node of a tree (linked list)."
[path-spec data]
(if (impl/contains-wilds? path-spec)
(let [parts (impl/partition-wilds path-spec)]
(reduce (fn [child segment]
(when (impl/catch-all-param? segment)
(throw (ex-info "catch-all may only appear at the end of a path spec"
{:patch-spec path-spec})))
(-> (make-node segment nil)
(add-child (subs (get-segment child) 0 1) child)))
(let [segment (last parts)]
(make-node segment data))
(reverse (butlast parts))))
(make-node path-spec data)))
(defn- calc-lcs
"Given two strings, return the end index of the longest common
prefix string."
[s1 s2]
(loop [i 1]
(cond (or (< (count s1) i)
(< (count s2) i))
(dec i)
(= (subs s1 0 i)
(subs s2 0 i))
(recur (inc i))
:else (dec i))))
(defn- split
"Given a node, a path-spec, a payload object to insert into the tree
and the lcs, split the node and return a new parent node with the
old contents of node and the new item as children.
lcs is the index of the longest common string in path-spec and the
segment of node."
[node path-spec data lcs]
(let [segment (get-segment node)
common (subs path-spec 0 lcs)
parent (new-node common nil)]
(if (= common path-spec)
(-> (set-data parent data)
(add-child (char-key segment lcs) (update-segment node subs lcs)))
(-> parent
(add-child (char-key segment lcs) (update-segment node subs lcs))
(insert-child (char-key path-spec lcs) (subs path-spec lcs) data)))))
(defn insert
"Given a tree node, a path-spec and a payload object, return a new
tree with payload inserted."
[node path-spec data]
(let [segment (get-segment node)]
(cond (nil? node)
(new-node path-spec data)
(= segment path-spec)
(set-data node data)
;; handle case where path-spec is a wildcard param
(impl/wild-param? path-spec)
(let [lcs (calc-lcs segment path-spec)
common (subs path-spec 0 lcs)]
(if (= common segment)
(let [path-spec (subs path-spec (inc lcs))]
(insert-child node (subs path-spec 0 1) path-spec data))
(throw (ex-info "route conflict"
{:node node
:path-spec path-spec
:segment segment}))))
;; in the case where path-spec is a catch-all, node should always be nil.
;; getting here means we have an invalid route specification
(impl/catch-all-param? path-spec)
(throw (ex-info "route conflict"
{:node node
:path-spec path-spec
:segment segment}))
:else
(let [lcs (calc-lcs segment path-spec)]
(cond (= lcs (count segment))
(insert-child node (char-key path-spec lcs) (subs path-spec lcs) data)
:else
(split node path-spec data lcs))))))
(defn view
"Returns a view representation of a prefix-tree."
[x]
(vec (concat
[(get-segment x)]
(some->> (get-chidren x) vals seq (map view))
(some->> (get-data x) vector))))

View file

@ -5,13 +5,14 @@
[cheshire.core :as json]
[clojure.string :as str]
[reitit.core :as reitit]
[reitit.core :as ring]
[reitit.ring :as ring]
[bidi.bidi :as bidi]
[ataraxy.core :as ataraxy]
[compojure.api.sweet :refer [api routes context ANY]]
[compojure.core :as compojure]
[io.pedestal.http.route.definition.table :as table]
[io.pedestal.http.route.map-tree :as map-tree]
@ -72,19 +73,11 @@
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)))
(fn [path]
(let [time (int (* (first (:sample-mean (cc/quick-benchmark (dotimes [_ 1000] (f path)) {}))) 1e6))]
(println path "=>" time "ns")
[path time]))
urls)))
(defn bench [routes no-paths?]
(let [routes (mapv (fn [[path name]]
@ -278,6 +271,82 @@
["topics/" topic] [:test/route47 topic]
"topics" [:test/route50]}}))
(def opensensors-compojure-routes
(compojure/routes
(compojure/context "/v1" []
(compojure/context "/public" []
(compojure/ANY "/topics/:topic" [] {:name :test/route4} handler)
(compojure/ANY "/users/:user-id" [] {:name :test/route16} handler)
(compojure/ANY "/orgs/:org-id" [] {:name :test/route18} handler))
(compojure/context "/users/:user-id" []
(compojure/ANY "/orgs/:org-id" [] {:name :test/route5} handler)
(compojure/ANY "/invitations" [] {:name :test/route7} handler)
(compojure/ANY "/topics" [] {:name :test/route9} handler)
(compojure/ANY "/bookmarks/followers" [] {:name :test/route10} handler)
(compojure/context "/devices" []
(compojure/ANY "/" [] {:name :test/route15} handler)
#_(compojure/ANY "/bulk" [] {:name :test/route21} handler)
(compojure/ANY "/:client-id" [] {:name :test/route35} handler)
(compojure/ANY "/:client-id/reset-password" [] {:name :test/route49} handler))
(compojure/ANY "/device-errors" [] {:name :test/route22} handler)
(compojure/ANY "/usage-stats" [] {:name :test/route24} handler)
(compojure/ANY "/claim-device/:client-id" [] {:name :test/route26} handler)
(compojure/ANY "/owned-orgs" [] {:name :test/route31} handler)
(compojure/ANY "/bookmark/:topic" [] {:name :test/route33} handler)
(compojure/ANY "/" [] {:name :test/route36} handler)
(compojure/ANY "/orgs" [] {:name :test/route52} handler)
(compojure/ANY "/api-key" [] {:name :test/route43} handler)
(compojure/ANY "/bookmarks" [] {:name :test/route56} handler))
(compojure/ANY "/search/topics/:term" [] {:name :test/route6} handler)
(compojure/context "/orgs" []
(compojure/ANY "/" [] {:name :test/route55} handler)
(compojure/context "/:org-id" []
(compojure/context "/devices" []
(compojure/ANY "/" [] {:name :test/route37} handler)
(compojure/ANY "/:device-id" [] {:name :test/route13} handler)
#_(compojure/ANY "/:batch/:type" [] {:name :test/route8} handler))
(compojure/ANY "/usage-stats" [] {:name :test/route12} handler)
(compojure/ANY "/invitations" [] {:name :test/route19} handler)
(compojure/context "/members" []
(compojure/ANY "/:user-id" [] {:name :test/route34} handler)
(compojure/ANY "/" [] {:name :test/route38} handler)
#_(compojure/ANY "/invitation-data/:user-id" [] {:name :test/route39} handler))
(compojure/ANY "/errors" [] {:name :test/route17} handler)
(compojure/ANY "/" [] {:name :test/route42} handler)
(compojure/ANY "/confirm-membership/:token" [] {:name :test/route46} handler)
(compojure/ANY "/topics" [] {:name :test/route57} handler)))
(compojure/context "/messages" []
(compojure/ANY "/user/:user-id" [] {:name :test/route14} handler)
(compojure/ANY "/device/:client-id" [] {:name :test/route30} handler)
(compojure/ANY "/topic/:topic" [] {:name :test/route48} handler))
(compojure/context "/topics" []
(compojure/ANY "/:topic" [] {:name :test/route32} handler)
(compojure/ANY "/" [] {:name :test/route54} handler))
(compojure/ANY "/whoami" [] {:name :test/route41} handler)
(compojure/ANY "/login" [] {:name :test/route51} handler))
(compojure/context "/v2" []
(compojure/ANY "/whoami" [] {:name :test/route1} handler)
(compojure/context "/users/:user-id" []
(compojure/ANY "/datasets" [] {:name :test/route2} handler)
(compojure/ANY "/devices" [] {:name :test/route25} handler)
(compojure/context "/topics" []
(compojure/ANY "/bulk" [] {:name :test/route29} handler)
(compojure/ANY "/" [] {:name :test/route54} handler))
(compojure/ANY "/" [] {:name :test/route45} handler))
(compojure/context "/public" []
(compojure/context "/projects/:project-id" []
(compojure/ANY "/datasets" [] {:name :test/route3} handler)
(compojure/ANY "/" [] {:name :test/route27} handler))
#_(compojure/ANY "/messages/dataset/bulk" [] {:name :test/route20} handler)
(compojure/ANY "/datasets/:dataset-id" [] {:name :test/route28} handler)
(compojure/ANY "/messages/dataset/:dataset-id" [] {:name :test/route53} handler))
(compojure/ANY "/datasets/:dataset-id" [] {:name :test/route11} handler)
(compojure/ANY "/login" [] {:name :test/route23} handler)
(compojure/ANY "/orgs/:org-id/topics" [] {:name :test/route40} handler)
(compojure/ANY "/schemas" [] {:name :test/route44} handler)
(compojure/ANY "/topics/:topic" [] {:name :test/route47} handler)
(compojure/ANY "/topics" [] {:name :test/route50} handler))))
(def opensensors-compojure-api-routes
(routes
(context "/v1" []
@ -490,25 +559,33 @@
#(app {:uri % :request-method :get}))
bidi-f #(bidi/match-route opensensors-bidi-routes %)
ataraxy-f #(ataraxy/matches opensensors-ataraxy-routes {:uri %})
compojure-f #(opensensors-compojure-routes {:uri % :request-method :get})
compojure-api-f #(opensensors-compojure-api-routes {:uri % :request-method :get})
pedestal-f #(pedestal/find-route opensensors-pedestal-routes {:path-info % :request-method :get})]
;; 2538ns -> 2028ns
;; 2538ns
;; 2065ns
;; 680ns (prefix-tree-router)
(bench!! routes true "reitit" reitit-f)
;; 2845ns -> 2299ns
;; 2845ns
;; 2316ns
;; 947ns (prefix-tree-router)
(bench!! routes true "reitit-ring" reitit-ring-f)
;; 2737ns
;; 2541ns
(bench!! routes true "pedestal" pedestal-f)
;; 9823ns
;; 9462ns
(bench!! routes true "compojure-api" compojure-api-f)
;; 16716ns
;; 11041ns
(bench!! routes true "compojure" compojure-f)
;; 16820ns
(bench!! routes true "bidi" bidi-f)
;; 24467ns
;; 24134ns
(bench!! routes true "ataraxy" ataraxy-f)))
(comment

View file

@ -148,6 +148,7 @@
(call))))
;; 710 µs (3-18x)
;; 540 µs (4-23x) -23% prefix-tree-router
(title "reitit")
(let [call #(reitit/match-by-path reitit-routes "/workspace/1/1")]
(assert (call))

View file

@ -0,0 +1,105 @@
(ns reitit.prefix-tree-perf-test
(:require [clojure.test :refer :all]
[io.pedestal.http.route.prefix-tree :as p]
[reitit.trie :as trie]
[criterium.core :as cc]))
;;
;; testing
;;
(def routes
[["/v2/whoami" {:name :test/route1}]
["/v2/users/:user-id/datasets" {:name :test/route2}]
["/v2/public/projects/:project-id/datasets" {:name :test/route3}]
["/v1/public/topics/:topic" {:name :test/route4}]
["/v1/users/:user-id/orgs/:org-id" {:name :test/route5}]
["/v1/search/topics/:term" {:name :test/route6}]
["/v1/users/:user-id/invitations" {:name :test/route7}]
["/v1/users/:user-id/topics" {:name :test/route9}]
["/v1/users/:user-id/bookmarks/followers" {:name :test/route10}]
["/v2/datasets/:dataset-id" {:name :test/route11}]
["/v1/orgs/:org-id/usage-stats" {:name :test/route12}]
["/v1/orgs/:org-id/devices/:client-id" {:name :test/route13}]
["/v1/messages/user/:user-id" {:name :test/route14}]
["/v1/users/:user-id/devices" {:name :test/route15}]
["/v1/public/users/:user-id" {:name :test/route16}]
["/v1/orgs/:org-id/errors" {:name :test/route17}]
["/v1/public/orgs/:org-id" {:name :test/route18}]
["/v1/orgs/:org-id/invitations" {:name :test/route19}]
["/v1/users/:user-id/device-errors" {:name :test/route22}]
["/v2/login" {:name :test/route23}]
["/v1/users/:user-id/usage-stats" {:name :test/route24}]
["/v2/users/:user-id/devices" {:name :test/route25}]
["/v1/users/:user-id/claim-device/:client-id" {:name :test/route26}]
["/v2/public/projects/:project-id" {:name :test/route27}]
["/v2/public/datasets/:dataset-id" {:name :test/route28}]
["/v2/users/:user-id/topics/bulk" {:name :test/route29}]
["/v1/messages/device/:client-id" {:name :test/route30}]
["/v1/users/:user-id/owned-orgs" {:name :test/route31}]
["/v1/topics/:topic" {:name :test/route32}]
["/v1/users/:user-id/bookmark/:topic" {:name :test/route33}]
["/v1/orgs/:org-id/members/:user-id" {:name :test/route34}]
["/v1/users/:user-id/devices/:client-id" {:name :test/route35}]
["/v1/users/:user-id" {:name :test/route36}]
["/v1/orgs/:org-id/devices" {:name :test/route37}]
["/v1/orgs/:org-id/members" {:name :test/route38}]
["/v2/orgs/:org-id/topics" {:name :test/route40}]
["/v1/whoami" {:name :test/route41}]
["/v1/orgs/:org-id" {:name :test/route42}]
["/v1/users/:user-id/api-key" {:name :test/route43}]
["/v2/schemas" {:name :test/route44}]
["/v2/users/:user-id/topics" {:name :test/route45}]
["/v1/orgs/:org-id/confirm-membership/:token" {:name :test/route46}]
["/v2/topics/:topic" {:name :test/route47}]
["/v1/messages/topic/:topic" {:name :test/route48}]
["/v1/users/:user-id/devices/:client-id/reset-password" {:name :test/route49}]
["/v2/topics" {:name :test/route50}]
["/v1/login" {:name :test/route51}]
["/v1/users/:user-id/orgs" {:name :test/route52}]
["/v2/public/messages/dataset/:dataset-id" {:name :test/route53}]
["/v1/topics" {:name :test/route54}]
["/v1/orgs" {:name :test/route55}]
["/v1/users/:user-id/bookmarks" {:name :test/route56}]
["/v1/orgs/:org-id/topics" {:name :test/route57}]])
(def pedestal-tree
(reduce
(fn [acc [p d]]
(p/insert acc p d))
nil routes))
(def reitit-tree
(reduce
(fn [acc [p d]]
(trie/insert acc p d))
nil routes))
(defn bench! []
;; 2.3ms
(cc/quick-bench
(dotimes [_ 1000]
(p/lookup pedestal-tree "/v1/orgs/1/topics")))
;; 3.1ms
;; 2.5ms (string equals)
;; 2.5ms (protocol)
;; 2.3ms (nil childs)
;; 2.0ms (rando impros)
;; 1.9ms (wild & catch shortcuts)
;; 1.5ms (inline child fetching)
;; 1.5ms (WildNode also backtracks)
;; 1.4ms (precalculate segment-size)
;; 1.3ms (fast-map)
;; 1.3ms (dissoc wild & catch-all from children)
;; 1.3ms (reified protocols)
;; 0.8ms (flattened matching)
;; 0.8ms (return route-data)
;; 0.8ms (fix payloads)
(cc/quick-bench
(dotimes [_ 1000]
(trie/lookup reitit-tree "/v1/orgs/1/topics" {}))))
(comment
(bench!))

View file

@ -7,9 +7,10 @@
(deftest reitit-test
(testing "linear-router"
(let [router (r/router ["/api" ["/ipa" ["/:size" ::beer]]])]
(is (= :linear-router (r/router-name router)))
(testing "routers handling wildcard paths"
(are [r name]
(let [router (r/router ["/api" ["/ipa" ["/:size" ::beer]]] {:router r})]
(is (= name (r/router-name router)))
(is (= [["/api/ipa/:size" {:name ::beer} nil]]
(r/routes router)))
(is (= true (map? (r/options router))))
@ -39,11 +40,16 @@
(is (thrown-with-msg?
ExceptionInfo
#"^missing path-params for route /api/ipa/:size -> \#\{:size\}$"
(r/match-by-name! router ::beer))))))
(r/match-by-name! router ::beer)))))
(testing "lookup-router"
(let [router (r/router ["/api" ["/ipa" ["/large" ::beer]]] {:router r/lookup-router})]
(is (= :lookup-router (r/router-name router)))
r/linear-router :linear-router
r/prefix-tree-router :prefix-tree-router
r/mixed-router :mixed-router))
(testing "routers handling static paths"
(are [r name]
(let [router (r/router ["/api" ["/ipa" ["/large" ::beer]]] {:router r})]
(is (= name (r/router-name router)))
(is (= [["/api/ipa/large" {:name ::beer} nil]]
(r/routes router)))
(is (= true (map? (r/options router))))
@ -68,45 +74,13 @@
#"can't create :lookup-router with wildcard routes"
(r/lookup-router
(r/resolve-routes
["/api/:version/ping"] {})))))))
["/api/:version/ping"] {}))))))
(testing "single-static-path-router"
(let [router (r/router ["/api" ["/ipa" ["/large" ::beer]]])]
(is (= :single-static-path-router (r/router-name router)))
(is (= [["/api/ipa/large" {:name ::beer} nil]]
(r/routes router)))
(is (= true (map? (r/options router))))
(is (= (r/map->Match
{:template "/api/ipa/large"
:meta {:name ::beer}
:path "/api/ipa/large"
:params {}})
(r/match-by-path router "/api/ipa/large")))
(is (= (r/map->Match
{:template "/api/ipa/large"
:meta {:name ::beer}
:path "/api/ipa/large"
:params {:size "large"}})
(r/match-by-name router ::beer {:size "large"})))
(is (= nil (r/match-by-name router "ILLEGAL")))
(is (= [::beer] (r/route-names router)))
(testing "can't be created with wildcard routes"
(is (thrown-with-msg?
ExceptionInfo
#":single-static-path-router requires exactly 1 static route"
(r/single-static-path-router
(r/resolve-routes
["/api/:version/ping"] {})))))
(testing "can't be created with multiple routes"
(is (thrown-with-msg?
ExceptionInfo
#":single-static-path-router requires exactly 1 static route"
(r/single-static-path-router
(r/resolve-routes
[["/ping"]
["/pong"]] {})))))))
r/lookup-router :lookup-router
r/single-static-path-router :single-static-path-router
r/linear-router :linear-router
r/prefix-tree-router :prefix-tree-router
r/mixed-router :mixed-router))
(testing "route coercion & compilation"