mirror of
https://github.com/metosin/reitit.git
synced 2025-12-16 16:01:11 +00:00
Add stuff
* router, partially from Pedestal * sample perf tests * kws expand to :name * fns expand to :handler
This commit is contained in:
parent
4e18963d8c
commit
faa3c08bf0
6 changed files with 338 additions and 35 deletions
54
README.md
54
README.md
|
|
@ -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
|
||||
|
||||
[](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.
|
||||
|
|
|
|||
106
perf-test/clj/reitit/perf_test.clj
Normal file
106
perf-test/clj/reitit/perf_test.clj
Normal 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))
|
||||
|
|
@ -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"]]
|
||||
|
|
|
|||
|
|
@ -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
108
src/reitit/regex.cljc
Normal 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 %) {})))
|
||||
|
|
@ -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"))))))
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue