Add stuff

* router, partially from Pedestal
* sample perf tests
* kws expand to :name
* fns expand to :handler
This commit is contained in:
Tommi Reiman 2017-08-08 15:31:00 +03:00
parent 4e18963d8c
commit faa3c08bf0
6 changed files with 338 additions and 35 deletions

View file

@ -2,16 +2,66 @@
Snappy data-driven router for Clojure(Script).
* Simple data-driven route syntax
* Generic, not tied to HTTP
* Extendable
* Fast
## Latest version
[![Clojars Project](http://clojars.org/metosin/reitit/latest-version.svg)](http://clojars.org/metosin/reitit)
## Usage
TODO
Named routes (example from [bide](https://github.com/funcool/bide#why-another-routing-library)).
```clj
(require '[reitit.core :as reitit])
(def router
(reitit/router
[["/auth/login" :auth/login]
["/auth/recovery/token/:token" :auth/recovery]
["/workspace/:project-uuid/:page-uuid" :workspace/page]]))
(reitit/match-route router "/workspace/1/2")
; {:name :workspace/page
; :route-params {:project-uuid "1", :page-uuid "2"}}
```
Nested routes with meta-data:
```clj
(def handler (constantly "ok"))
(def ring-router
(reitit/router
["/api" {:middleware [:api]}
["/ping" handler]
["/public/*path" handler]
["/user/:id" {:parameters {:id String}
:handler handler}]
["/admin" {:middleware [:admin] :roles #{:admin}}
["/root" {:roles ^:replace #{:root}
:handler handler}]
["/db" {:middleware [:db]
:handler handler}]]]))
(reitit/match-route ring-router "/api/admin/db")
; {:middleware [:api :admin :db]
; :roles #{:admin}
; :handler #object[...]
; :route-params {}}
```
## Special thanks
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 Route](https://github.com/pedestal/pedestal/tree/master/route),
## License
Copyright © 2016-2017 [Metosin Oy](http://www.metosin.fi)
Copyright © 2017 [Metosin Oy](http://www.metosin.fi)
Distributed under the Eclipse Public License, the same as Clojure.

View file

@ -0,0 +1,106 @@
(ns reitit.perf-test
(:require [criterium.core :as cc]
[reitit.core :as reitit]
[bidi.bidi :as bidi]
[compojure.api.core :refer [routes GET]]
[ataraxy.core :as ataraxy]
[io.pedestal.http.route.definition.table :as table]
[io.pedestal.http.route.map-tree :as map-tree]
[io.pedestal.http.route.router :as pedestal]))
;;
;; start repl with `lein perf repl`
;; perf measured with the following setup:
;;
;; Model Name: MacBook Pro
;; Model Identifier: MacBookPro11,3
;; Processor Name: Intel Core i7
;; Processor Speed: 2,5 GHz
;; Number of Processors: 1
;; Total Number of Cores: 4
;; L2 Cache (per Core): 256 KB
;; L3 Cache: 6 MB
;; Memory: 16 GB
;;
(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"))
(def bidi-routes
["/" [["auth/login" :auth/login]
[["auth/recovery/token/" :token] :auth/recovery]
["workspace/" [[[:project "/" :page] :workspace/page]]]]])
(def compojure-api-routes
(routes
(GET "/auth/login" [] (constantly ""))
(GET "/auth/recovery/token/:token" [] (constantly ""))
(GET "/workspace/:project/:page" [] (constantly ""))))
(def ataraxy-routes
(ataraxy/compile
'{["/auth/login"] [:auth/login]
["/auth/recovery/token/" token] [:auth/recovery token]
["/workspace/" project "/" token] [:workspace/page project token]}))
(def pedestal-routes
(map-tree/router
(table/table-routes
[["/auth/login" :get (constantly "") :route-name :auth/login]
["/auth/recovery/token/:token" :get (constantly "") :route-name :auth/recovery]
["/workspace/:project/:page" :get (constantly "") :route-name :workspace/page]])))
(def reitit-routes
(reitit/router
[["/auth/login" :auth/login]
["/auth/recovery/token/:token" :auth/recovery]
["/workspace/:project/:page" :workspace/page]]))
(defn routing-test []
(suite "simple routing")
;; 15.4µs
(title "bidi")
(let [call #(bidi/match-route bidi-routes "/workspace/1/1")]
(assert (call))
(cc/quick-bench
(call)))
;; 2.9µs (-81%)
(title "ataraxy")
(let [call #(ataraxy/matches ataraxy-routes {:uri "/workspace/1/1"})]
(assert (call))
(cc/quick-bench
(call)))
;; 2.4µs (-84%)
(title "pedestal - map-tree => prefix-tree")
(let [call #(pedestal/find-route pedestal-routes {:path-info "/workspace/1/1" :request-method :get})]
(assert (call))
(cc/quick-bench
(call)))
;; 3.8µs (-75%)
(title "compojure-api")
(let [call #(compojure-api-routes {:uri "/workspace/1/1", :request-method :get})]
(assert (call))
(cc/quick-bench
(call)))
;; 1.0µs (-94%)
(title "reitit")
(let [call #(reitit/match-route reitit-routes "/workspace/1/1")]
(assert (call))
(cc/quick-bench
(call))))
(comment
(routing-test))

View file

@ -26,7 +26,13 @@
[org.clojure/test.check "0.9.0"]
[org.clojure/tools.namespace "0.2.11"]
[com.gfredericks/test.chuck "0.2.7"]]}
:perf {:jvm-opts ^:replace ["-server"]}}
:perf {:jvm-opts ^:replace ["-server"]
:test-paths ["perf-test/clj"]
:dependencies [[metosin/compojure-api "2.0.0-alpha7"]
[io.pedestal/pedestal.route "0.5.2"]
[org.clojure/core.async "0.3.443"]
[ataraxy "0.4.0"]
[bidi "2.0.9"]]}}
:aliases {"all" ["with-profile" "dev"]
"perf" ["with-profile" "default,dev,perf"]
"test-clj" ["all" "do" ["test"] ["check"]]

View file

@ -1,14 +1,15 @@
(ns reitit.core
(:require [meta-merge.core :refer [meta-merge]]))
(:require [meta-merge.core :refer [meta-merge]]
[reitit.regex :as regex]))
(defprotocol ExpandArgs
(defprotocol Expand
(expand [this]))
(extend-protocol ExpandArgs
(extend-protocol Expand
#?(:clj clojure.lang.Keyword
:cljs cljs.core.Keyword)
(expand [this] {:handler this})
(expand [this] {:name this})
#?(:clj clojure.lang.PersistentArrayMap
:cljs cljs.core.PersistentArrayMap)
@ -18,25 +19,30 @@
:cljs cljs.core.PersistentHashMap)
(expand [this] this)
#?(:clj clojure.lang.Fn
:cljs function)
(expand [this] {:handler this})
nil
(expand [_]))
(defn walk
([routes]
(walk ["" []] routes))
([[pacc macc] routes]
(letfn [(subwalk [p m r]
(reduce #(into %1 (walk [p m] %2)) [] r))]
(if (vector? (first routes))
(subwalk pacc macc routes)
(let [[path & [maybe-meta :as args]] routes]
(let [[meta childs] (if (vector? maybe-meta)
[{} args]
[maybe-meta (rest args)])
macc (into macc (expand meta))]
(if (seq childs)
(subwalk (str pacc path) macc childs)
[[(str pacc path) macc]])))))))
(defn walk [data {:keys [path meta routes expand]
:or {path "", meta [], routes [], expand expand}}]
(letfn
[(walk-many [p m r]
(reduce #(into %1 (walk-one p m %2)) [] r))
(walk-one [pacc macc routes]
(if (vector? (first routes))
(walk-many pacc macc routes)
(let [[path & [maybe-meta :as args]] routes]
(let [[meta childs] (if (vector? maybe-meta)
[{} args]
[maybe-meta (rest args)])
macc (into macc (expand meta))]
(if (seq childs)
(walk-many (str pacc path) macc childs)
[[(str pacc path) macc]])))))]
(walk-one path meta data)))
(defn map-meta [f routes]
(mapv #(update % 1 f) routes))
@ -47,5 +53,26 @@
(meta-merge acc {k v}))
{} x))
(defn resolve-routes [x]
(->> x (walk) (map-meta merge-meta)))
(defn resolve-routes [data opts]
(->> (walk data opts) (map-meta merge-meta)))
(defprotocol Routing
(match-route [this path])
(path-for [this name] [this name parameters]))
(defrecord LinearRouter [routes]
Routing
(match-route [_ path]
(reduce
(fn [acc [p m matcher]]
(if-let [params (matcher path)]
(reduced (assoc m :route-params params))))
nil routes)))
(defn router
([data]
(router data {}))
([data opts]
(->LinearRouter
(for [[p m] (resolve-routes data opts)]
[p m (regex/matcher p)]))))

108
src/reitit/regex.cljc Normal file
View file

@ -0,0 +1,108 @@
; Copyright 2013 Relevance, Inc.
; Copyright 2014-2016 Cognitect, Inc.
; The use and distribution terms for this software are covered by the
; Eclipse Public License 1.0 (http://opensource.org/licenses/eclipse-1.0)
; which can be found in the file epl-v10.html at the root of this distribution.
;
; By using this software in any fashion, you are agreeing to be bound by
; the terms of this license.
;
; You must not remove this notice, or any other, from this software.
(ns reitit.regex
(:require [clojure.string :as str])
(:import #?(:clj (java.util.regex Pattern))))
;;
;; https://github.com/pedestal/pedestal/blob/master/route/src/io/pedestal/http/route/path.clj
;;
(defn- parse-path-token [out string]
(condp re-matches string
#"^:(.+)$" :>> (fn [[_ token]]
(let [key (keyword token)]
(-> out
(update-in [:path-parts] conj key)
(update-in [:path-params] conj key)
(assoc-in [:path-constraints key] "([^/]+)"))))
#"^\*(.+)$" :>> (fn [[_ token]]
(let [key (keyword token)]
(-> out
(update-in [:path-parts] conj key)
(update-in [:path-params] conj key)
(assoc-in [:path-constraints key] "(.*)"))))
(update-in out [:path-parts] conj string)))
(defn- parse-path
([pattern] (parse-path {:path-parts [] :path-params [] :path-constraints {}} pattern))
([accumulated-info pattern]
(if-let [m (re-matches #"/(.*)" pattern)]
(let [[_ path] m]
(reduce parse-path-token
accumulated-info
(str/split path #"/")))
(throw (ex-info "Routes must start from the root, so they must begin with a '/'" {:pattern pattern})))))
;; TODO: is this correct?
(defn- re-quote [x]
#?(:clj (Pattern/quote x)
:cljs (str/replace-all x #"([.?*+^$[\\]\\\\(){}|-])" "\\$1")))
(defn- path-regex [{:keys [path-parts path-constraints] :as route}]
(let [[pp & pps] path-parts
path-parts (if (and (seq pps) (string? pp) (empty? pp)) pps path-parts)]
(re-pattern
(apply str
(interleave (repeat "/")
(map #(or (get path-constraints %) (re-quote %))
path-parts))))))
(defn- path-matcher [route]
(let [{:keys [path-re path-params]} route]
(fn [path]
(when-let [m (re-matches path-re path)]
(zipmap path-params (rest m))))))
;;
;; (c) https://github.com/pedestal/pedestal/blob/master/route/src/io/pedestal/http/route/prefix_tree.clj
;;
(defn- wild? [s]
(contains? #{\: \*} (first s)))
(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."
[path-spec]
(let [groups (partition-by wild? (str/split path-spec #"/"))
first-groups (butlast groups)
last-group (last groups)]
(flatten
(conj (mapv #(if (wild? (first %))
%
(str (str/join "/" %) "/"))
first-groups)
(if (wild? (first last-group))
last-group
(str/join "/" last-group))))))
(defn contains-wilds?
"Return true if the given path-spec contains any wildcard params or
catch-alls."
[path-spec]
(let [parts (partition-wilds path-spec)]
(or (> (count parts) 1)
(wild? (first parts)))))
;;
;; Routing
;;
(defn matcher [path]
(if (contains-wilds? path)
(as-> (parse-path path) $
(assoc $ :path-re (path-regex $))
(path-matcher $))
#(if (= path %) {})))

View file

@ -8,23 +8,29 @@
(let [routes [["/auth/login" :auth/login]
["/auth/recovery/token/:token" :auth/recovery]
["/workspace/:project-uuid/:page-uuid" :workspace/page]]
expected [["/auth/login" {:handler :auth/login}]
["/auth/recovery/token/:token" {:handler :auth/recovery}]
["/workspace/:project-uuid/:page-uuid" {:handler :workspace/page}]]]
(is (= expected (reitit/resolve-routes routes)))))
expected [["/auth/login" {:name :auth/login}]
["/auth/recovery/token/:token" {:name :auth/recovery}]
["/workspace/:project-uuid/:page-uuid" {:name :workspace/page}]]]
(is (= expected (reitit/resolve-routes routes {})))))
(testing "ring sample"
(let [routes ["/api" {:mw [:api]}
(let [pong (constantly "ok")
routes ["/api" {:mw [:api]}
["/ping" :kikka]
["/user/:id" {:parameters {:id String}}
["/:sub-id" {:parameters {:sub-id String}}]]
["/pong"]
["/pong" pong]
["/admin" {:mw [:admin] :roles #{:admin}}
["/user" {:roles ^:replace #{:user}}]
["/db" {:mw [:db]}]]]
expected [["/api/ping" {:mw [:api], :handler :kikka}]
expected [["/api/ping" {:mw [:api], :name :kikka}]
["/api/user/:id/:sub-id" {:mw [:api], :parameters {:id String, :sub-id String}}]
["/api/pong" {:mw [:api]}]
["/api/pong" {:mw [:api], :handler pong}]
["/api/admin/user" {:mw [:api :admin], :roles #{:user}}]
["/api/admin/db" {:mw [:api :admin :db], :roles #{:admin}}]]]
(is (= expected (reitit/resolve-routes routes))))))
["/api/admin/db" {:mw [:api :admin :db], :roles #{:admin}}]]
router (reitit/router routes)]
(is (= expected (reitit/resolve-routes routes {})))
(is (= {:mw [:api], :parameters {:id String, :sub-id String}
:route-params {:id "1", :sub-id "2"}}
(reitit/match-route router "/api/user/1/2"))))))