mirror of
https://github.com/metosin/reitit.git
synced 2025-12-17 00:11:11 +00:00
commit
32598f0e56
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).
|
Snappy data-driven router for Clojure(Script).
|
||||||
|
|
||||||
|
* Simple data-driven route syntax
|
||||||
|
* Generic, not tied to HTTP
|
||||||
|
* Extendable
|
||||||
|
* Fast
|
||||||
|
|
||||||
## Latest version
|
## Latest version
|
||||||
|
|
||||||
[](http://clojars.org/metosin/reitit)
|
[](http://clojars.org/metosin/reitit)
|
||||||
|
|
||||||
## Usage
|
## 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
|
## 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.
|
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/test.check "0.9.0"]
|
||||||
[org.clojure/tools.namespace "0.2.11"]
|
[org.clojure/tools.namespace "0.2.11"]
|
||||||
[com.gfredericks/test.chuck "0.2.7"]]}
|
[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"]
|
:aliases {"all" ["with-profile" "dev"]
|
||||||
"perf" ["with-profile" "default,dev,perf"]
|
"perf" ["with-profile" "default,dev,perf"]
|
||||||
"test-clj" ["all" "do" ["test"] ["check"]]
|
"test-clj" ["all" "do" ["test"] ["check"]]
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,15 @@
|
||||||
(ns reitit.core
|
(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]))
|
(expand [this]))
|
||||||
|
|
||||||
(extend-protocol ExpandArgs
|
(extend-protocol Expand
|
||||||
|
|
||||||
#?(:clj clojure.lang.Keyword
|
#?(:clj clojure.lang.Keyword
|
||||||
:cljs cljs.core.Keyword)
|
:cljs cljs.core.Keyword)
|
||||||
(expand [this] {:handler this})
|
(expand [this] {:name this})
|
||||||
|
|
||||||
#?(:clj clojure.lang.PersistentArrayMap
|
#?(:clj clojure.lang.PersistentArrayMap
|
||||||
:cljs cljs.core.PersistentArrayMap)
|
:cljs cljs.core.PersistentArrayMap)
|
||||||
|
|
@ -18,25 +19,30 @@
|
||||||
:cljs cljs.core.PersistentHashMap)
|
:cljs cljs.core.PersistentHashMap)
|
||||||
(expand [this] this)
|
(expand [this] this)
|
||||||
|
|
||||||
|
#?(:clj clojure.lang.Fn
|
||||||
|
:cljs function)
|
||||||
|
(expand [this] {:handler this})
|
||||||
|
|
||||||
nil
|
nil
|
||||||
(expand [_]))
|
(expand [_]))
|
||||||
|
|
||||||
(defn walk
|
(defn walk [data {:keys [path meta routes expand]
|
||||||
([routes]
|
:or {path "", meta [], routes [], expand expand}}]
|
||||||
(walk ["" []] routes))
|
(letfn
|
||||||
([[pacc macc] routes]
|
[(walk-many [p m r]
|
||||||
(letfn [(subwalk [p m r]
|
(reduce #(into %1 (walk-one p m %2)) [] r))
|
||||||
(reduce #(into %1 (walk [p m] %2)) [] r))]
|
(walk-one [pacc macc routes]
|
||||||
(if (vector? (first routes))
|
(if (vector? (first routes))
|
||||||
(subwalk pacc macc routes)
|
(walk-many pacc macc routes)
|
||||||
(let [[path & [maybe-meta :as args]] routes]
|
(let [[path & [maybe-meta :as args]] routes]
|
||||||
(let [[meta childs] (if (vector? maybe-meta)
|
(let [[meta childs] (if (vector? maybe-meta)
|
||||||
[{} args]
|
[{} args]
|
||||||
[maybe-meta (rest args)])
|
[maybe-meta (rest args)])
|
||||||
macc (into macc (expand meta))]
|
macc (into macc (expand meta))]
|
||||||
(if (seq childs)
|
(if (seq childs)
|
||||||
(subwalk (str pacc path) macc childs)
|
(walk-many (str pacc path) macc childs)
|
||||||
[[(str pacc path) macc]])))))))
|
[[(str pacc path) macc]])))))]
|
||||||
|
(walk-one path meta data)))
|
||||||
|
|
||||||
(defn map-meta [f routes]
|
(defn map-meta [f routes]
|
||||||
(mapv #(update % 1 f) routes))
|
(mapv #(update % 1 f) routes))
|
||||||
|
|
@ -47,5 +53,26 @@
|
||||||
(meta-merge acc {k v}))
|
(meta-merge acc {k v}))
|
||||||
{} x))
|
{} x))
|
||||||
|
|
||||||
(defn resolve-routes [x]
|
(defn resolve-routes [data opts]
|
||||||
(->> x (walk) (map-meta merge-meta)))
|
(->> (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]
|
(let [routes [["/auth/login" :auth/login]
|
||||||
["/auth/recovery/token/:token" :auth/recovery]
|
["/auth/recovery/token/:token" :auth/recovery]
|
||||||
["/workspace/:project-uuid/:page-uuid" :workspace/page]]
|
["/workspace/:project-uuid/:page-uuid" :workspace/page]]
|
||||||
expected [["/auth/login" {:handler :auth/login}]
|
expected [["/auth/login" {:name :auth/login}]
|
||||||
["/auth/recovery/token/:token" {:handler :auth/recovery}]
|
["/auth/recovery/token/:token" {:name :auth/recovery}]
|
||||||
["/workspace/:project-uuid/:page-uuid" {:handler :workspace/page}]]]
|
["/workspace/:project-uuid/:page-uuid" {:name :workspace/page}]]]
|
||||||
(is (= expected (reitit/resolve-routes routes)))))
|
(is (= expected (reitit/resolve-routes routes {})))))
|
||||||
|
|
||||||
(testing "ring sample"
|
(testing "ring sample"
|
||||||
(let [routes ["/api" {:mw [:api]}
|
(let [pong (constantly "ok")
|
||||||
|
routes ["/api" {:mw [:api]}
|
||||||
["/ping" :kikka]
|
["/ping" :kikka]
|
||||||
["/user/:id" {:parameters {:id String}}
|
["/user/:id" {:parameters {:id String}}
|
||||||
["/:sub-id" {:parameters {:sub-id String}}]]
|
["/:sub-id" {:parameters {:sub-id String}}]]
|
||||||
["/pong"]
|
["/pong" pong]
|
||||||
["/admin" {:mw [:admin] :roles #{:admin}}
|
["/admin" {:mw [:admin] :roles #{:admin}}
|
||||||
["/user" {:roles ^:replace #{:user}}]
|
["/user" {:roles ^:replace #{:user}}]
|
||||||
["/db" {:mw [:db]}]]]
|
["/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/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/user" {:mw [:api :admin], :roles #{:user}}]
|
||||||
["/api/admin/db" {:mw [:api :admin :db], :roles #{:admin}}]]]
|
["/api/admin/db" {:mw [:api :admin :db], :roles #{:admin}}]]
|
||||||
(is (= expected (reitit/resolve-routes routes))))))
|
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