diff --git a/CHANGELOG.md b/CHANGELOG.md index acfa86c7..22c439f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,7 @@ * `"/file-:id/topics"` (free start, ends at slash) * `"/file-{name}.html"` (free start & end) * backed by a new `:trie-router`, replacing `:segment-router` - * [over 40% faster](https://metosin.github.io/reitit/performance.html) on the JVM + * [up to 2x faster](https://metosin.github.io/reitit/performance.html) on the JVM * **BREAKING**: `reitit.spec/validate-spec!` has been renamed to `validate` * With `clojure.spec` coercion, values flow through both `st/coerce` & `st/conform` yielding better error messages. Original issue in [compojure-api](https://github.com/metosin/compojure-api/issues/409). @@ -59,7 +59,7 @@ ### `reitit-frontend` -* **BREAKING**: Frontend controllers redesigned +* Frontend controllers redesigned * Controller `:params` function has been deprecated * Controller `:identity` function works the same as `:params` * New `:parameters` option can be used to declare which parameters diff --git a/modules/reitit-core/java-src/reitit/Trie.java b/modules/reitit-core/java-src/reitit/Trie.java index 65f98a42..953a70ad 100644 --- a/modules/reitit-core/java-src/reitit/Trie.java +++ b/modules/reitit-core/java-src/reitit/Trie.java @@ -160,10 +160,14 @@ public class Trie { @Override public Match match(int i, int max, char[] path) { + boolean hasPercent = false; + boolean hasPlus = false; if (i < max && path[i] != end) { int stop = max; for (int j = i; j < max; j++) { final char c = path[j]; + hasPercent = hasPercent || c == '%'; + hasPlus = hasPlus || c == '+'; if (c == end) { stop = j; break; @@ -171,7 +175,7 @@ public class Trie { } final Match m = child.match(stop, max, path); if (m != null) { - m.params = m.params.assoc(key, decode(path, i, stop)); + m.params = m.params.assoc(key, decode(new String(path, i, stop - i), hasPercent, hasPlus)); } return m; } diff --git a/modules/reitit-core/src/reitit/core.cljc b/modules/reitit-core/src/reitit/core.cljc index 4b121eda..1025618a 100644 --- a/modules/reitit-core/src/reitit/core.cljc +++ b/modules/reitit-core/src/reitit/core.cljc @@ -79,11 +79,12 @@ | key | description | | -----------------------------|-------------| - | `:reitit.core/trie-compiler` | Optional trie-compiler." + | `:reitit.trie/trie-compiler` | Optional trie-compiler. + | `:reitit.trie/parameters` | Optional function to transform path-parameters at creation time (default `identity`)." ([compiled-routes] (linear-router compiled-routes {})) ([compiled-routes opts] - (let [compiler (::trie-compiler opts (trie/compiler)) + (let [compiler (::trie/trie-compiler opts (trie/compiler)) names (impl/find-names compiled-routes opts) [pl nl] (reduce (fn [[pl nl] [p {:keys [name] :as data} result]] @@ -91,7 +92,7 @@ f #(if-let [path (impl/path-for route %)] (->Match p data result (impl/url-decode-coll %) path) (->PartialMatch p data result (impl/url-decode-coll %) path-params))] - [(conj pl (-> (trie/insert nil p (->Match p data result nil nil)) (trie/compile))) + [(conj pl (-> (trie/insert nil p (->Match p data result nil nil) opts) (trie/compile))) (if name (assoc nl name f) nl)])) [[] {}] compiled-routes) @@ -174,11 +175,12 @@ | key | description | | -----------------------------|-------------| - | `:reitit.core/trie-compiler` | Optional trie-compiler." + | `:reitit.trie/trie-compiler` | Optional trie-compiler. + | `:reitit.trie/parameters` | Optional function to transform path-parameters at creation time (default `identity`)." ([compiled-routes] (trie-router compiled-routes {})) ([compiled-routes opts] - (let [compiler (::trie-compiler opts (trie/compiler)) + (let [compiler (::trie/trie-compiler opts (trie/compiler)) names (impl/find-names compiled-routes opts) [pl nl] (reduce (fn [[pl nl] [p {:keys [name] :as data} result]] @@ -186,7 +188,7 @@ f #(if-let [path (impl/path-for route %)] (->Match p data result (impl/url-decode-coll %) path) (->PartialMatch p data result (impl/url-decode-coll %) path-params))] - [(trie/insert pl p (->Match p data result nil nil)) + [(trie/insert pl p (->Match p data result nil nil) opts) (if name (assoc nl name f) nl)])) [nil {}] compiled-routes) diff --git a/modules/reitit-core/src/reitit/trie.cljc b/modules/reitit-core/src/reitit/trie.cljc index a6ba6251..8af5877d 100644 --- a/modules/reitit-core/src/reitit/trie.cljc +++ b/modules/reitit-core/src/reitit/trie.cljc @@ -288,6 +288,18 @@ ;; Managing Tries ;; +#?(:clj + (def record-parameters + "Memoized function to transform parameters into runtime generated Record." + (memoize + (fn [params] + (let [fields (keys params)] + (if (some qualified-keyword? fields) + params + (let [sym (gensym "PathParams") + ctor (symbol (str "map->" sym))] + (eval `(do (defrecord ~sym ~(mapv (comp symbol name) fields)) (~ctor {})))))))))) + (defn insert "Returns a trie with routes added to it." ([routes] @@ -298,8 +310,10 @@ (insert acc p d)) node routes)) ([node path data] + (insert node path data nil)) + ([node path data {::keys [parameters] :or {parameters identity}}] (let [parts (split-path path) - params (zipmap (->> parts (remove string?) (map :value)) (repeat nil))] + params (parameters (zipmap (->> parts (remove string?) (map :value)) (repeat nil)))] (-insert (or node (-node {})) (split-path path) path params data)))) (defn compiler diff --git a/perf-test/clj/reitit/go_perf_test.clj b/perf-test/clj/reitit/go_perf_test.clj index 59838449..4e432c2e 100644 --- a/perf-test/clj/reitit/go_perf_test.clj +++ b/perf-test/clj/reitit/go_perf_test.clj @@ -2,6 +2,7 @@ (:require [criterium.core :as cc] [reitit.perf-utils :refer :all] [reitit.ring :as ring] + [reitit.trie :as trie] [clojure.string :as str])) ;; @@ -295,7 +296,8 @@ (def app (ring/ring-handler (ring/router - (reduce (partial add h) [] routes)) + (reduce (partial add h) [] routes) + {::trie/parameters trie/record-parameters}) (ring/create-default-handler) {:inject-match? false, :inject-router? false})) @@ -334,6 +336,8 @@ ;; 277ns (trie-router, no injects, switch-case) - 690ns clojure ;; 273ns (trie-router, no injects, direct-data) ;; 256ns (trie-router, pre-defined parameters) + ;; 237ns (trie-router, single-sweep wild-params) + ;; 191ns (trie-router, record parameters) (let [req (map->Req {:request-method :get, :uri "/repos/julienschmidt/httprouter/stargazers"})] (title "param") (assert (= {:status 200, :body "/repos/:owner/:repo/stargazers"} (app req))) @@ -365,6 +369,6 @@ (do (require '[clj-async-profiler.core :as prof]) (prof/profile - (dotimes [_ 1000000] + (dotimes [_ 10000000] (app {:request-method :get, :uri "/repos/julienschmidt/httprouter/stargazers"}))) (prof/serve-files 8080))) diff --git a/perf-test/clj/reitit/prefix_tree_perf_test.clj b/perf-test/clj/reitit/prefix_tree_perf_test.clj index b35a102a..0e6e2091 100644 --- a/perf-test/clj/reitit/prefix_tree_perf_test.clj +++ b/perf-test/clj/reitit/prefix_tree_perf_test.clj @@ -74,7 +74,7 @@ (trie/compile (reduce (fn [acc [p d]] - (trie/insert acc p d)) + (trie/insert acc p d {::trie/parameters trie/record-parameters})) nil routes)))) (defn bench! [] @@ -117,6 +117,7 @@ ;; 0.300µs (iterate arrays) ;; 0.280µs (list-params) ;; 0.096µs (trie) + ;; 0.083µs (trie, record-params, faster decode) (cc/with-progress-reporting (cc/bench (trie-matcher "/v1/orgs/1/topics")))) diff --git a/test/cljc/reitit/trie_test.cljc b/test/cljc/reitit/trie_test.cljc index 90286d77..1592123c 100644 --- a/test/cljc/reitit/trie_test.cljc +++ b/test/cljc/reitit/trie_test.cljc @@ -34,4 +34,11 @@ (is (= (trie/->Match {} {:a 1}) ((-> (trie/insert nil "" {:a 1}) (trie/compile) - (trie/path-matcher)) "")))) + (trie/path-matcher)) ""))) + + #?(:clj + (let [match ((-> (trie/insert nil "/:a" {:a 1} {::trie/parameters trie/record-parameters}) + (trie/compile) + (trie/path-matcher)) "/a")] + (is (record? (:params match))) + (is (= (trie/->Match {:a "a"} {:a 1}) (update match :params (partial into {})))))))