Merge pull request #203 from metosin/JavaSegment

Java SegmentTrie
This commit is contained in:
Tommi Reiman 2019-01-16 09:36:56 +02:00 committed by GitHub
commit 4e7963be91
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 832 additions and 93 deletions

View file

@ -4,6 +4,15 @@
### `reitit-core`
* `reitit.core/Expand` can be extended, fixes [#201](https://github.com/metosin/reitit/issues/201).
* new snappy Java-backed `SegmentTrie` routing algorithm and data structure, making wildcard routing ~2x faster on the JVM
### `reitit-ring`
* new options `:inject-match?` and `:inject-router?` on `reitit.ring/ring-handler` to optionally not to inject `Router` and `Match` into the request. See [performance guide](https://metosin.github.io/reitit/performance.html#faster!) for details.
### `reitit-http`
* new options `:inject-match?` and `:inject-router?` on `reitit.http/ring-handler` and `reitit.http/routing-interceptor` to optionally not to inject `Router` and `Match` into the request. See [performance guide](https://metosin.github.io/reitit/performance.html#faster!) for details.
## 0.2.10 (2018-12-30)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 36 KiB

View file

@ -1,6 +1,6 @@
# Performance
Reitit tries to be both great in features and be really, really fast. Originally the routing was ported from [Pedestal](http://pedestal.io/), but has been mostly rewritten.
Besides having great features, goal of reitit is to be really, really fast. The routing was originally exported from Pedestal, but since rewritten.
![Opensensors perf test](images/opensensors.png)
@ -63,13 +63,13 @@ The routing sample taken from [bide](https://github.com/funcool/bide) README:
(dotimes [_ 1000]
(r/match-by-path routes "/auth/login")))
;; Execution time mean (per 1000): 530 µs -> 1.9M ops/sec
;; Execution time mean (per 1000): 315 µs -> 3.2M ops/sec
(cc/quick-bench
(dotimes [_ 1000]
(r/match-by-path routes "/workspace/1/1")))
```
Based on the [perf tests](https://github.com/metosin/reitit/tree/master/perf-test/clj/reitit/perf/bide_perf_test.clj), the first (static path) lookup is 300-500x faster and the second (wildcard path) lookup is 4-24x faster that the other tested routing libs (Ataraxy, Bidi, Compojure and Pedestal).
Based on the [perf tests](https://github.com/metosin/reitit/tree/master/perf-test/clj/reitit/perf/bide_perf_test.clj), the first (static path) lookup is 300-500x faster and the second (wildcard path) lookup is 6-40x faster that the other tested routing libs (Ataraxy, Bidi, Compojure and Pedestal).
But, the example is too simple for any real benchmark. Also, some of the libraries always match on the `:request-method` too and by doing so, do more work than just match by path. Compojure does most work also by invoking the handler.
@ -79,13 +79,13 @@ So, we need to test something more realistic.
To get better view on the real life routing performance, there is [test](https://github.com/metosin/reitit/blob/master/perf-test/clj/reitit/opensensors_perf_test.clj) of a mid-size rest(ish) http api with 50+ routes, having a lot of path parameters. The route definitions are pulled off from the [OpenSensors](https://opensensors.io/) swagger definitions.
Thanks to the snappy new [segment-tree](https://github.com/metosin/reitit/blob/master/modules/reitit-core/src/reitit/segment.cljc) algorithm, `reitit-ring` is fastest here. Pedestal is also fast with it's [prefix-tree](https://en.wikipedia.org/wiki/Radix_tree) implementation.
Thanks to the snappy [SegmentTrie](https://github.com/metosin/reitit/blob/master/modules/reitit-core/java-src/reitit/SegmentTrie.java) (a modification of [Radix Tree](https://en.wikipedia.org/wiki/Radix_tree)), `reitit-ring` is fastest here. [Calfpath](https://github.com/kumarshantanu/calfpath) and [Pedestal](https://github.com/pedestal/pedestal) are also quite fast.
![Opensensors perf](images/opensensors.png)
### CQRS apis
Another real-life [test scenario](https://github.com/metosin/reitit/blob/master/perf-test/clj/reitit/lupapiste_perf_test.clj) is a [CQRS](https://martinfowler.com/bliki/CQRS.html)-style route tree, where all the paths are static, e.g. `/api/command/add-order`. The 300 route definitions are pulled out from [Lupapiste](https://github.com/lupapiste/lupapiste).
Another real-life [test scenario](https://github.com/metosin/reitit/blob/master/perf-test/clj/reitit/lupapiste_perf_test.clj) is a [CQRS](https://martinfowler.com/bliki/CQRS.html) style route tree, where all the paths are static, e.g. `/api/command/add-order`. The 300 route definitions are pulled out from [Lupapiste](https://github.com/lupapiste/lupapiste).
Both `reitit-ring` and Pedestal shine in this test, thanks to the fast lookup-routers. On average, they are **two** and on best case, **three orders of magnitude faster** than the other tested libs. Ataraxy failed this test on `Method code too large!` error.
@ -99,13 +99,42 @@ The reitit routing perf is measured to get an internal baseline to optimize agai
### Looking out of the box
A quick poke to [routers in Go](https://github.com/julienschmidt/go-http-routing-benchmark) indicates that the reitit is only few times slower than the fastest routers in Go. Which is really awesome (if true).
A quick poke to [routers in Go](https://github.com/julienschmidt/go-http-routing-benchmark) indicates that the reitit is only few times slower than the fastest routers in Go. Which is kinda awesome.
### Faster!
By default, `reitit.ring/ring-router`, `reitit.http/ring-router` and `reitit.http/routing-interceptor` inject both `Match` and `Router` into the request. You can remove the injections setting options `:inject-match?` and `:inject-router?` to `false`. This saves some tens of nanos (with the hw described above).
```clj
(require '[reitit.ring :as ring])
(require '[criterium.core :as cc])
(defn create [options]
(ring/ring-handler
(ring/router
["/ping" (constantly {:status 200, :body "ok"})])
(ring/create-default-handler)
options))
;; 130ns
(let [app (create nil)]
(cc/quick-bench
(app {:request-method :get, :uri "/ping"})))
;; 80ns
(let [app (create {:inject-router? false, :inject-match? false})]
(cc/quick-bench
(app {:request-method :get, :uri "/ping"})))
```
**NOTE**: Without `Router`, you can't to do [reverse routing](ring/reverse_routing.md) and without `Match` you can't write [dynamic extensions](ring/dynamic_extensions.md).
### Performance tips
Few things that have an effect on performance:
* Wildcard-routes are an order of magnitude slower than static routes
* It's ok to mix non-wildcard and wildcard routes in a same routing tree as long as you don't disable the [conflict resolution](basics/route_conflicts.md) => if no conflicting routes are found, a `:mixed-router` can be created, which internally has a fast static path router and a separate wildcard-router. So, the static paths are still fast.
* Move computation from request processing time into creation time, using by compiling [middleware](ring/compiling_middleware.md) & [route data](advanced/configuring_routers.md).
* Conflicting routes are served with LinearRouter, which is the slowest implementation.
* It's ok to mix non-wildcard, wildcard or even conflicting routes in a same routing tree. Reitit will create an hierarchy of routers to serve all the routes with best possible implementation.
* Move computation from request processing time into creation time, using by compiling [middleware](ring/compiling_middleware.md), [interceptors](http/interceptors.md) and [route data](advanced/configuring_routers.md).
* Unmounted middleware (or interceptor) is infinitely faster than a mounted one effectively doing nothing.

View file

@ -51,9 +51,11 @@ Match contains `:result` compiled by the `ring-router`:
Given a `ring-router`, optional default-handler & options, `ring-handler` function will return a valid ring handler supporting both synchronous and [asynchronous](https://www.booleanknot.com/blog/2016/07/15/asynchronous-ring.html) request handling. The following options are available:
| key | description |
| --------------|-------------|
| `:middleware` | Optional sequence of middleware that wrap the ring-handler"
| key | description |
| ------------------|-------------|
| `:middleware` | Optional sequence of middleware that wrap the ring-handler"
| `:inject-match?` | Boolean to inject `match` into request under `:reitit.core/match` key (default true)
| `:inject-router?` | Boolean to inject `router` into request under `:reitit.core/router` key (default true)
Simple Ring app:

View file

@ -0,0 +1,313 @@
package reitit;
import clojure.lang.Keyword;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.*;
public class SegmentTrie {
public static ArrayList<String> split(final String path) {
final ArrayList<String> segments = new ArrayList<>(4);
final int size = path.length();
int start = 1;
for (int i = start; i < size; i++) {
final char c = path.charAt(i);
if (c == '/') {
segments.add(path.substring(start, i));
start = i + 1;
}
}
segments.add(path.substring(start, size));
return segments;
}
private static String decode(String s) {
try {
if (s.contains("%")) {
String _s = s;
if (s.contains("+")) {
_s = s.replace("+", "%2B");
}
return URLDecoder.decode(_s, "UTF-8");
}
} catch (UnsupportedEncodingException ignored) {
}
return s;
}
public static class Match {
public final Map<Keyword, String> params = new HashMap<>();
public Object data;
@Override
public String toString() {
Map<Object, Object> m = new HashMap<>();
m.put(Keyword.intern("data"), data);
m.put(Keyword.intern("params"), params);
return m.toString();
}
}
private Map<String, SegmentTrie> childs = new HashMap<>();
private Map<Keyword, SegmentTrie> wilds = new HashMap<>();
private Map<Keyword, SegmentTrie> catchAll = new HashMap<>();
private Object data;
public SegmentTrie add(String path, Object data) {
List<String> paths = split(path);
SegmentTrie pointer = this;
for (String p : paths) {
if (p.startsWith(":")) {
Keyword k = Keyword.intern(p.substring(1));
SegmentTrie s = pointer.wilds.get(k);
if (s == null) {
s = new SegmentTrie();
pointer.wilds.put(k, s);
}
pointer = s;
} else if (p.startsWith("*")) {
Keyword k = Keyword.intern(p.substring(1));
SegmentTrie s = pointer.catchAll.get(k);
if (s == null) {
s = new SegmentTrie();
pointer.catchAll.put(k, s);
}
pointer = s;
break;
} else {
SegmentTrie s = pointer.childs.get(p);
if (s == null) {
s = new SegmentTrie();
pointer.childs.put(p, s);
}
pointer = s;
}
}
pointer.data = data;
return this;
}
private Matcher staticMatcher() {
if (childs.size() == 1) {
return new StaticMatcher(childs.keySet().iterator().next(), childs.values().iterator().next().matcher());
} else {
Map<String, Matcher> m = new HashMap<>();
for (Map.Entry<String, SegmentTrie> e : childs.entrySet()) {
m.put(e.getKey(), e.getValue().matcher());
}
return new StaticMapMatcher(m);
}
}
public Matcher matcher() {
Matcher m;
if (!catchAll.isEmpty()) {
m = new CatchAllMatcher(catchAll.keySet().iterator().next(), catchAll.values().iterator().next().data);
if (data != null) {
m = new LinearMatcher(Arrays.asList(new DataMatcher(data), m));
}
} else if (!wilds.isEmpty()) {
if (wilds.size() == 1 && data == null && childs.isEmpty()) {
m = new WildMatcher(wilds.keySet().iterator().next(), wilds.values().iterator().next().matcher());
} else {
List<Matcher> matchers = new ArrayList<>();
if (data != null) {
matchers.add(new DataMatcher(data));
}
if (!childs.isEmpty()) {
matchers.add(staticMatcher());
}
for (Map.Entry<Keyword, SegmentTrie> e : wilds.entrySet()) {
matchers.add(new WildMatcher(e.getKey(), e.getValue().matcher()));
}
m = new LinearMatcher(matchers);
}
} else if (!childs.isEmpty()) {
m = staticMatcher();
if (data != null) {
m = new LinearMatcher(Arrays.asList(new DataMatcher(data), m));
}
} else {
return new DataMatcher(data);
}
return m;
}
public interface Matcher {
Match match(int i, List<String> segments, Match match);
}
public static final class StaticMatcher implements Matcher {
private final String segment;
private final Matcher child;
StaticMatcher(String segment, Matcher child) {
this.segment = segment;
this.child = child;
}
@Override
public Match match(int i, List<String> segments, Match match) {
if (i < segments.size() && segment.equals(segments.get(i))) {
return child.match(i + 1, segments, match);
}
return null;
}
@Override
public String toString() {
return "[\"" + segment + "\" " + child + "]";
}
}
public static final class WildMatcher implements Matcher {
private final Keyword parameter;
private final Matcher child;
WildMatcher(Keyword parameter, Matcher child) {
this.parameter = parameter;
this.child = child;
}
@Override
public Match match(int i, List<String> segments, Match match) {
if (i < segments.size() && !segments.get(i).isEmpty()) {
final Match m = child.match(i + 1, segments, match);
if (m != null) {
m.params.put(parameter, decode(segments.get(i)));
return m;
}
}
return null;
}
@Override
public String toString() {
return "[" + parameter + " " + child + "]";
}
}
public static final class CatchAllMatcher implements Matcher {
private final Keyword parameter;
private final Object data;
CatchAllMatcher(Keyword parameter, Object data) {
this.parameter = parameter;
this.data = data;
}
@Override
public Match match(int i, List<String> segments, Match match) {
if (i < segments.size()) {
match.params.put(parameter, decode(String.join("/", segments.subList(i, segments.size()))));
match.data = data;
return match;
}
return null;
}
@Override
public String toString() {
return "[" + parameter + " " + new DataMatcher(data) + "]";
}
}
public static final class StaticMapMatcher implements Matcher {
private final Map<String, Matcher> map;
StaticMapMatcher(Map<String, Matcher> map) {
this.map = map;
}
@Override
public Match match(int i, List<String> segments, Match match) {
if (i < segments.size()) {
final Matcher child = map.get(segments.get(i));
if (child != null) {
return child.match(i + 1, segments, match);
}
}
return null;
}
@Override
public String toString() {
StringBuilder b = new StringBuilder();
b.append("{");
List<String> keys = new ArrayList<>(map.keySet());
for (int i = 0; i < keys.size(); i++) {
String path = keys.get(i);
Matcher value = map.get(path);
b.append("\"").append(path).append("\" ").append(value);
if (i < keys.size() - 1) {
b.append(", ");
}
}
b.append("}");
return b.toString();
}
}
public static final class LinearMatcher implements Matcher {
private final List<Matcher> childs;
LinearMatcher(List<Matcher> childs) {
this.childs = childs;
}
@Override
public Match match(int i, List<String> segments, Match match) {
for (Matcher child : childs) {
final Match m = child.match(i, segments, match);
if (m != null) {
return m;
}
}
return null;
}
@Override
public String toString() {
return childs.toString();
}
}
public static final class DataMatcher implements Matcher {
private final Object data;
DataMatcher(Object data) {
this.data = data;
}
@Override
public Match match(int i, List<String> segments, Match match) {
if (i == segments.size()) {
match.data = data;
return match;
}
return null;
}
@Override
public String toString() {
return (data != null ? data.toString() : "nil");
}
}
public static Match lookup(Matcher matcher, String path) {
return matcher.match(0, split(path), new Match());
}
public static void main(String[] args) {
SegmentTrie trie = new SegmentTrie();
trie.add("/repos/:owner/:repo/stargazers", 1);
Matcher m = trie.matcher();
System.err.println(m);
System.err.println(m.getClass());
System.out.println(lookup(m, "/repos/metosin/reitit/stargazers"));
}
}

View file

@ -8,4 +8,5 @@
:plugins [[lein-parent "0.3.2"]]
:parent-project {:path "../../project.clj"
:inherit [:deploy-repositories :managed-dependencies]}
:java-source-paths ["java-src"]
:dependencies [[meta-merge]])

View file

@ -271,6 +271,7 @@
(if name (assoc nl name f) nl)]))
[nil {}]
compiled-routes)
pl (segment/compile pl)
lookup (impl/fast-map nl)
routes (uncompile-routes compiled-routes)]
^{:type ::router}
@ -289,7 +290,7 @@
(match-by-path [_ path]
(if-let [match (segment/lookup pl path)]
(-> (:data match)
(assoc :path-params (impl/url-decode-coll (:path-params match)))
(assoc :path-params (:path-params match))
(assoc :path path))))
(match-by-name [_ name]
(if-let [match (impl/fast-get lookup name)]

View file

@ -5,7 +5,8 @@
#?(:clj
(:import (java.util.regex Pattern)
(java.util HashMap Map)
(java.net URLEncoder URLDecoder))))
(java.net URLEncoder URLDecoder)
(reitit SegmentTrie))))
(defn maybe-map-values
"Applies a function to every value of a map, updates the value if not nil.
@ -19,6 +20,16 @@
coll
coll))
(defn segments
"Splits the path into sequence of segments, using `/` char. Assumes that the
path starts with `/`, stripping the first empty segment. e.g.
(segments \"/a/b/c\") ; => (\"a\" \"b\" \"c\")
(segments \"/a/) ; => (\"a\" \"\")"
[path]
#?(:clj (SegmentTrie/split ^String path)
:cljs (rest (.split path #"/" 666))))
;;
;; https://github.com/pedestal/pedestal/blob/master/route/src/io/pedestal/http/route/prefix_tree.clj
;;
@ -42,10 +53,6 @@
(defn wild-or-catch-all-param? [x]
(boolean (or (wild-param x) (catch-all-param x))))
(defn segments [path]
#?(:clj (.split ^String path "/" 666)
:cljs (.split path #"/" 666)))
(defn contains-wilds? [path]
(boolean (some wild-or-catch-all-param? (segments path))))

View file

@ -1,7 +1,8 @@
(ns reitit.segment
(:refer-clojure :exclude [-lookup])
(:refer-clojure :exclude [-lookup compile])
(:require [reitit.impl :as impl]
[clojure.string :as str]))
[clojure.string :as str])
#?(:clj (:import (reitit SegmentTrie SegmentTrie$Match))))
(defrecord Match [data path-params])
@ -43,14 +44,24 @@
(if (and wilds? (not (str/blank? p))) (some #(-lookup (impl/fast-get children' %) ps (assoc path-params % p)) wilds))
(if catch-all (-catch-all children' catch-all path-params p ps)))))))))
(defn insert [root path data]
(-insert (or root (segment)) (impl/segments path) (map->Match {:data data})))
;;
;; public api
;;
(defn create [paths]
(reduce
(fn [segment [p data]]
(insert segment p data))
nil paths))
(defn insert
"Returns a Segment Trie with path with data inserted into it. Creates the trie if `nil`."
[trie path data]
#?(:cljs (-insert (or trie (segment)) (impl/segments path) (map->Match {:data data}))
:clj (.add (or ^SegmentTrie trie ^SegmentTrie (SegmentTrie.)) ^String path data)))
(defn lookup [segment path]
(-lookup segment (impl/segments path) {}))
(defn compile [trie]
"Compiles the Trie so that [[lookup]] can be used."
#?(:cljs trie
:clj (.matcher ^SegmentTrie (or trie (SegmentTrie.)))))
(defn lookup [trie path]
"Looks the path from a Segment Trie. Returns a [[Match]] or `nil`."
#?(:cljs (if-let [match (-lookup trie (impl/segments path) {})]
(assoc match :path-params (impl/url-decode-coll (:path-params match))))
:clj (if-let [match ^SegmentTrie$Match (SegmentTrie/lookup trie path)]
(->Match (.data match) (clojure.lang.PersistentHashMap/create (.params match))))))

View file

@ -73,12 +73,22 @@
(r/router data opts))))
(defn routing-interceptor
"A Pedestal-style routing interceptor that enqueus the interceptors into context."
[router default-handler {:keys [interceptors executor]}]
"Creates a Pedestal-style routing interceptor that enqueus the interceptors into context.
Takes http-router, default ring-handler and and options map, with the following keys:
| key | description |
| ------------------|-------------|
| `:executor` | `reitit.interceptor.Executor` for the interceptor chain
| `:interceptors` | Optional sequence of interceptors that are always run before any other interceptors, even for the default handler
| `:inject-match?` | Boolean to inject `match` into request under `:reitit.core/match` key (default true)
| `:inject-router?` | Boolean to inject `router` into request under `:reitit.core/router` key (default true)"
[router default-handler {:keys [interceptors executor inject-match? inject-router?]
:or {inject-match? true, inject-router? true}}]
(let [default-handler (or default-handler (fn ([_])))
default-interceptors (->> interceptors
(map #(interceptor/into-interceptor % nil (r/options router))))
default-queue (interceptor/queue executor default-interceptors)]
default-queue (interceptor/queue executor default-interceptors)
enrich-request (ring/create-enrich-request inject-match? inject-router?)]
{:name ::router
:enter (fn [{:keys [request] :as context}]
(if-let [match (r/match-by-path router (:uri request))]
@ -86,10 +96,7 @@
path-params (:path-params match)
endpoint (-> match :result method)
interceptors (or (:queue endpoint) (:interceptors endpoint))
request (-> request
(impl/fast-assoc :path-params path-params)
(impl/fast-assoc ::r/match match)
(impl/fast-assoc ::r/router router))
request (enrich-request request path-params match router)
context (assoc context :request request)
queue (interceptor/queue executor (concat default-interceptors interceptors))]
(interceptor/enqueue executor context queue))
@ -103,13 +110,16 @@
"Creates a ring-handler out of a http-router, optional default ring-handler
and options map, with the following keys:
| key | description |
| ----------------|-------------|
| `:executor` | `reitit.interceptor.Executor` for the interceptor chain
| `:interceptors` | Optional sequence of interceptors that are always run before any other interceptors, even for the default handler"
| key | description |
| ------------------|-------------|
| `:executor` | `reitit.interceptor.Executor` for the interceptor chain
| `:interceptors` | Optional sequence of interceptors that are always run before any other interceptors, even for the default handler
| `:inject-match?` | Boolean to inject `match` into request under `:reitit.core/match` key (default true)
| `:inject-router?` | Boolean to inject `router` into request under `:reitit.core/router` key (default true)"
([router opts]
(ring-handler router nil opts))
([router default-handler {:keys [executor interceptors]}]
([router default-handler {:keys [executor interceptors inject-match? inject-router?]
:or {inject-match? true, inject-router? true}}]
(let [default-handler (or default-handler (fn ([_]) ([_ respond _] (respond nil))))
default-queue (->> [default-handler]
(concat interceptors)
@ -120,7 +130,9 @@
(dissoc :data) ; data is already merged into routes
(cond-> (seq interceptors)
(update-in [:data :interceptors] (partial into (vec interceptors)))))
router (reitit.http/router (r/routes router) router-opts)]
router (reitit.http/router (r/routes router) router-opts)
enrich-request (ring/create-enrich-request inject-match? inject-router?)
enrich-default-request (ring/create-enrich-default-request inject-router?)]
(with-meta
(fn
([request]
@ -129,13 +141,10 @@
path-params (:path-params match)
endpoint (-> match :result method)
interceptors (or (:queue endpoint) (:interceptors endpoint))
request (-> request
(impl/fast-assoc :path-params path-params)
(impl/fast-assoc ::r/match match)
(impl/fast-assoc ::r/router router))]
request (enrich-request request path-params match router)]
(or (interceptor/execute executor interceptors request)
(interceptor/execute executor default-queue request)))
(interceptor/execute executor default-queue (impl/fast-assoc request ::r/router router))))
(interceptor/execute executor default-queue (enrich-default-request request router))))
([request respond raise]
(let [default #(interceptor/execute executor default-queue % respond raise)]
(if-let [match (r/match-by-path router (:uri request))]
@ -143,10 +152,7 @@
path-params (:path-params match)
endpoint (-> match :result method)
interceptors (or (:queue endpoint) (:interceptors endpoint))
request (-> request
(impl/fast-assoc :path-params path-params)
(impl/fast-assoc ::r/match match)
(impl/fast-assoc ::r/router router))
request (enrich-request request path-params match router)
respond' (fn [response]
(if response
(respond response)
@ -154,7 +160,7 @@
(if interceptors
(interceptor/execute executor interceptors request respond' raise)
(default request)))
(default (impl/fast-assoc request ::r/router router))))
(default (enrich-default-request request router))))
nil))
{::r/router router}))))

View file

@ -226,20 +226,54 @@
(not-found-handler request)))))]
(create handler)))))
(defn create-enrich-request [inject-match? inject-router?]
(cond
(and inject-match? inject-router?)
(fn enrich-request [request path-params match router]
(-> request
(impl/fast-assoc :path-params path-params)
(impl/fast-assoc ::r/match match)
(impl/fast-assoc ::r/router router)))
inject-router?
(fn enrich-request [request path-params _ router]
(-> request
(impl/fast-assoc :path-params path-params)
(impl/fast-assoc ::r/router router)))
inject-match?
(fn enrich-request [request path-params match _]
(-> request
(impl/fast-assoc :path-params path-params)
(impl/fast-assoc ::r/match match)))
:else
(fn enrich-request [request path-params _ _]
(-> request
(impl/fast-assoc :path-params path-params)))))
(defn create-enrich-default-request [inject-router?]
(if inject-router?
(fn enrich-request [request router]
(impl/fast-assoc request ::r/router router))
identity))
(defn ring-handler
"Creates a ring-handler out of a router, optional default ring-handler
and options map, with the following keys:
| key | description |
| --------------|-------------|
| `:middleware` | Optional sequence of middleware that wrap the ring-handler"
| key | description |
| ------------------|-------------|
| `:middleware` | Optional sequence of middleware that wrap the ring-handler
| `:inject-match?` | Boolean to inject `match` into request under `:reitit.core/match` key (default true)
| `:inject-router?` | Boolean to inject `router` into request under `:reitit.core/router` key (default true)"
([router]
(ring-handler router nil))
([router default-handler]
(ring-handler router default-handler nil))
([router default-handler {:keys [middleware]}]
([router default-handler {:keys [middleware inject-match? inject-router?]
:or {inject-match? true, inject-router? true}}]
(let [default-handler (or default-handler (fn ([_]) ([_ respond _] (respond nil))))
wrap (if middleware (partial middleware/chain middleware) identity)]
wrap (if middleware (partial middleware/chain middleware) identity)
enrich-request (create-enrich-request inject-match? inject-router?)
enrich-default-request (create-enrich-default-request inject-router?)]
(with-meta
(wrap
(fn
@ -249,24 +283,18 @@
path-params (:path-params match)
result (:result match)
handler (-> result method :handler (or default-handler))
request (-> request
(impl/fast-assoc :path-params path-params)
(impl/fast-assoc ::r/match match)
(impl/fast-assoc ::r/router router))]
request (enrich-request request path-params match router)]
(or (handler request) (default-handler request)))
(default-handler (impl/fast-assoc request ::r/router router))))
(default-handler (enrich-default-request request router))))
([request respond raise]
(if-let [match (r/match-by-path router (:uri request))]
(let [method (:request-method request)
path-params (:path-params match)
result (:result match)
handler (-> result method :handler (or default-handler))
request (-> request
(impl/fast-assoc :path-params path-params)
(impl/fast-assoc ::r/match match)
(impl/fast-assoc ::r/router router))]
request (enrich-request request path-params match router)]
((routes handler default-handler) request respond raise))
(default-handler (impl/fast-assoc request ::r/router router) respond raise))
(default-handler (enrich-default-request request router) respond raise))
nil)))
{::r/router router}))))

View file

@ -68,7 +68,8 @@
(->> (impl/segments path)
(map #(if (impl/wild-or-catch-all-param? %)
(str "{" (subs % 1) "}") %))
(str/join "/")))
(str/join "/")
(str "/")))
(defn create-swagger-handler []
"Create a ring handler to emit swagger spec. Collects all routes from router which have

View file

@ -89,10 +89,11 @@
;; 1600 µs
(title "bidi")
(assert (bidi/match-route bidi-routes "/auth/login"))
(cc/quick-bench
(dotimes [_ 1000]
(bidi/match-route bidi-routes "/auth/login")))
(let [request "/auth/login"]
(assert (bidi/match-route bidi-routes request))
(cc/quick-bench
(dotimes [_ 1000]
(bidi/match-route bidi-routes request))))
;; 1400 µs
(title "ataraxy")
@ -105,10 +106,10 @@
;; 1000 µs
(title "pedestal - map-tree => prefix-tree")
(let [request {:path-info "/auth/login" :request-method :get}]
(assert (pedestal/find-route pedestal-router {:path-info "/auth/login" :request-method :get}))
(assert (pedestal/find-route pedestal-router request))
(cc/quick-bench
(dotimes [_ 1000]
(pedestal/find-route pedestal-router {:path-info "/auth/login" :request-method :get}))))
(pedestal/find-route pedestal-router request))))
;; 1400 µs
(title "compojure")
@ -163,6 +164,7 @@
;; 710 µs (3-18x)
;; 530 µs (4-24x) -25% prefix-tree-router
;; 710 µs (3-18x) segment-router
;; 320 µs (6-40x) java-segment-router
(title "reitit")
(assert (reitit/match-by-path reitit-routes "/workspace/1/1"))
(cc/quick-bench

View file

@ -0,0 +1,120 @@
(ns reitit.calf-perf-test
(:require [criterium.core :as cc]
[reitit.perf-utils :refer :all]
[ring.util.codec]
[reitit.impl]
[clojure.edn :as edn]
[reitit.ring :as ring]
[reitit.core :as r])
(:import (reitit SegmentTrie)))
;;
;; 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 test! [f input]
(do
(println "\u001B[33m")
(println (pr-str input) "=>" (pr-str (f input)))
(println "\u001B[0m")
(cc/quick-bench (f input))))
(defn h11 [id type] {:status 200
:headers {"Content-Type" "text/plain"}
:body (str id ".11." type)})
(defn h12 [id type] {:status 200
:headers {"Content-Type" "text/plain"}
:body (str id ".12." type)})
(defn h1x [] {:status 405
:headers {"Allow" "GET, PUT"
"Content-Type" "text/plain"}
:body "405 Method not supported. Supported methods are: GET, PUT"})
(defn h21 [id] {:status 200
:headers {"Content-Type" "text/plain"}
:body (str id ".21")})
(defn h22 [id] {:status 200
:headers {"Content-Type" "text/plain"}
:body (str id ".22")})
(defn h2x [] {:status 405
:headers {"Allow" "GET, PUT"
"Content-Type" "text/plain"}
:body "405 Method not supported. Supported methods are: GET, PUT"})
(defn h30 [cid did] {:status 200
:headers {"Content-Type" "text/plain"}
:body (str cid ".3." did)})
(defn h3x [] {:status 405
:headers {"Allow" "PUT"
"Content-Type" "text/plain"}
:body "405 Method not supported. Only PUT is supported."})
(defn h40 [] {:status 200
:headers {"Content-Type" "text/plain"}
:body "4"})
(defn h4x [] {:status 405
:headers {"Allow" "PUT"
"Content-Type" "text/plain"}
:body "405 Method not supported. Only PUT is supported."})
(defn hxx [] {:status 400
:headers {"Content-Type" "text/plain"}
:body "400 Bad request. URI does not match any available uri-template."})
(def handler-reitit
(ring/ring-handler
(ring/router
[["/user/:id/profile/:type/" {:get (fn [{{:keys [id type]} :path-params}] (h11 id type))
:put (fn [{{:keys [id type]} :path-params}] (h12 id type))
:handler (fn [_] (h1x))}]
["/user/:id/permissions/" {:get (fn [{{:keys [id]} :path-params}] (h21 id))
:put (fn [{{:keys [id]} :path-params}] (h22 id))
:handler (fn [_] (h2x))}]
["/company/:cid/dept/:did/" {:put (fn [{{:keys [cid did]} :path-params}] (h30 cid did))
:handler (fn [_] (h3x))}]
["/this/is/a/static/route" {:put (fn [_] (h40))
:handler (fn [_] (h4x))}]])
(fn [_] (hxx))
{:inject-match? false, :inject-router? false}))
(comment
(let [request {:request-method :get
:uri "/user/1234/profile/compact/"}]
;; OLD: 1338ns
;; NEW: 981ns
;; JAVA: 805ns
;; NO-INJECT: 704ns
#_(cc/quick-bench
(handler-reitit request))
(handler-reitit request)))
(comment
;; 281ns
(let [router (r/router [["/user/:id/profile/:type" ::1]
["/user/:id/permissions" ::2]
["/company/:cid/dept/:did" ::3]
["/this/is/a/static/route" ::4]])]
#_(cc/quick-bench
(r/match-by-path router "/user/1234/profile/compact"))
(r/match-by-path router "/user/1234/profile/compact")))
(comment
(edn/read-string
(str
(.matcher
(doto (SegmentTrie.)
(.add "/user" 1)
(.add "/user/:id" 2)
(.add "/user/:id/orders" 3)
(.add "/user/id/permissions" 4))))))

View file

@ -296,7 +296,9 @@
(def app
(ring/ring-handler
(ring/router
(reduce (partial add h) [] routes))))
(reduce (partial add h) [] routes))
(ring/create-default-handler)
{:inject-match? false, :inject-router? false}))
(defrecord Req [uri request-method])
@ -313,6 +315,8 @@
;; 40ns (httprouter)
;; 140ns
;; 120ns (faster decode params)
;; 140µs (java-segment-router)
;; 60ns (java-segment-router, no injects)
(let [req (map->Req {:request-method :get, :uri "/user/repos"})]
(title "static")
(assert (= {:status 200, :body "/user/repos"} (app req)))
@ -321,6 +325,9 @@
;; 160ns (httprouter)
;; 990ns
;; 830ns (faster decode params)
;; 560µs (java-segment-router)
;; 490ns (java-segment-router, no injects)
;; 440ns (java-segment-router, no injects, single-wild-optimization)
(let [req (map->Req {:request-method :get, :uri "/repos/julienschmidt/httprouter/stargazers"})]
(title "param")
(assert (= {:status 200, :body "/repos/:owner/:repo/stargazers"} (app req)))
@ -329,6 +336,9 @@
;; 30µs (httprouter)
;; 190µs
;; 160µs (faster decode params)
;; 120µs (java-segment-router)
;; 100µs (java-segment-router, no injects)
;; 90µs (java-segment-router, no injects, single-wild-optimization)
(let [requests (mapv route->req routes)]
(title "all")
(cc/quick-bench

View file

@ -185,9 +185,19 @@
:c "1+1"
:d "1"}))
(defn split! []
(suite "split")
;; 114ns (String/split)
;; 82ns (SegmentTrie/split)
(test "Splitting a String")
(test! impl/segments "/olipa/kerran/:avaruus"))
(comment
(url-decode!)
(url-encode!)
(form-decode!)
(form-encode!)
(url-encode-coll!))
(url-encode-coll!)
(split!))

View file

@ -8,6 +8,8 @@
[bidi.bidi :as bidi]
[ataraxy.core :as ataraxy]
[compojure.core :refer [routes context ANY]]
[calfpath.core :as cp]
[calfpath.route :as cr]
[io.pedestal.http.route.definition.table :as table]
[io.pedestal.http.route.map-tree :as map-tree]
@ -366,6 +368,133 @@
["/v1/users/:user-id/bookmarks" :get handler :route-name :test/route56]
["/v1/orgs/:org-id/topics" :get handler :route-name :test/route57]])))
(defn opensensors-calfpath-macro-handler [request]
(cp/->uri
request
"/v2/whoami" [] (cp/->get request (handler request) nil)
"/v2/users/:user-id/datasets" [] (cp/->get request (handler request) nil)
"/v2/public/projects/:project-id/datasets" [] (cp/->get request (handler request) nil)
"/v1/public/topics/:topic" [] (cp/->get request (handler request) nil)
"/v1/users/:user-id/orgs/:org-id" [] (cp/->get request (handler request) nil)
"/v1/search/topics/:term" [] (cp/->get request (handler request) nil)
"/v1/users/:user-id/invitations" [] (cp/->get request (handler request) nil)
"/v1/orgs/:org-id/devices/:batch/:type" [] (cp/->get request (handler request) nil)
"/v1/users/:user-id/topics" [] (cp/->get request (handler request) nil)
"/v1/users/:user-id/bookmarks/followers" [] (cp/->get request (handler request) nil)
"/v2/datasets/:dataset-id" [] (cp/->get request (handler request) nil)
"/v1/orgs/:org-id/usage-stats" [] (cp/->get request (handler request) nil)
"/v1/orgs/:org-id/devices/:client-id" [] (cp/->get request (handler request) nil)
"/v1/messages/user/:user-id" [] (cp/->get request (handler request) nil)
"/v1/users/:user-id/devices" [] (cp/->get request (handler request) nil)
"/v1/public/users/:user-id" [] (cp/->get request (handler request) nil)
"/v1/orgs/:org-id/errors" [] (cp/->get request (handler request) nil)
"/v1/public/orgs/:org-id" [] (cp/->get request (handler request) nil)
"/v1/orgs/:org-id/invitations" [] (cp/->get request (handler request) nil)
"/v2/public/messages/dataset/bulk" [] (cp/->get request (handler request) nil)
"/v1/users/:user-id/devices/bulk" [] (cp/->get request (handler request) nil)
"/v1/users/:user-id/device-errors" [] (cp/->get request (handler request) nil)
"/v2/login" [] (cp/->get request (handler request) nil)
"/v1/users/:user-id/usage-stats" [] (cp/->get request (handler request) nil)
"/v2/users/:user-id/devices" [] (cp/->get request (handler request) nil)
"/v1/users/:user-id/claim-device/:client-id" [] (cp/->get request (handler request) nil)
"/v2/public/projects/:project-id" [] (cp/->get request (handler request) nil)
"/v2/public/datasets/:dataset-id" [] (cp/->get request (handler request) nil)
"/v2/users/:user-id/topics/bulk" [] (cp/->get request (handler request) nil)
"/v1/messages/device/:client-id" [] (cp/->get request (handler request) nil)
"/v1/users/:user-id/owned-orgs" [] (cp/->get request (handler request) nil)
"/v1/topics/:topic" [] (cp/->get request (handler request) nil)
"/v1/users/:user-id/bookmark/:topic" [] (cp/->get request (handler request) nil)
"/v1/orgs/:org-id/members/:user-id" [] (cp/->get request (handler request) nil)
"/v1/users/:user-id/devices/:client-id" [] (cp/->get request (handler request) nil)
"/v1/users/:user-id" [] (cp/->get request (handler request) nil)
"/v1/orgs/:org-id/devices" [] (cp/->get request (handler request) nil)
"/v1/orgs/:org-id/members" [] (cp/->get request (handler request) nil)
"/v1/orgs/:org-id/members/invitation-data/:user-id" [] (cp/->get request (handler request) nil)
"/v2/orgs/:org-id/topics" [] (cp/->get request (handler request) nil)
"/v1/whoami" [] (cp/->get request (handler request) nil)
"/v1/orgs/:org-id" [] (cp/->get request (handler request) nil)
"/v1/users/:user-id/api-key" [] (cp/->get request (handler request) nil)
"/v2/schemas" [] (cp/->get request (handler request) nil)
"/v2/users/:user-id/topics" [] (cp/->get request (handler request) nil)
"/v1/orgs/:org-id/confirm-membership/:token" [] (cp/->get request (handler request) nil)
"/v2/topics/:topic" [] (cp/->get request (handler request) nil)
"/v1/messages/topic/:topic" [] (cp/->get request (handler request) nil)
"/v1/users/:user-id/devices/:client-id/reset-password" [] (cp/->get request (handler request) nil)
"/v2/topics" [] (cp/->get request (handler request) nil)
"/v1/login" [] (cp/->get request (handler request) nil)
"/v1/users/:user-id/orgs" [] (cp/->get request (handler request) nil)
"/v2/public/messages/dataset/:dataset-id" [] (cp/->get request (handler request) nil)
"/v1/topics" [] (cp/->get request (handler request) nil)
"/v1/orgs" [] (cp/->get request (handler request) nil)
"/v1/users/:user-id/bookmarks" [] (cp/->get request (handler request) nil)
"/v1/orgs/:org-id/topics" [] (cp/->get request (handler request) nil)
nil))
(def opensensors-calfpath-routes
[{:uri "/v2/whoami" :nested [{:method :get, :handler handler}]}
{:uri "/v2/users/:user-id/datasets" :nested [{:method :get, :handler handler}]}
{:uri "/v2/public/projects/:project-id/datasets" :nested [{:method :get, :handler handler}]}
{:uri "/v1/public/topics/:topic" :nested [{:method :get, :handler handler}]}
{:uri "/v1/users/:user-id/orgs/:org-id" :nested [{:method :get, :handler handler}]}
{:uri "/v1/search/topics/:term" :nested [{:method :get, :handler handler}]}
{:uri "/v1/users/:user-id/invitations" :nested [{:method :get, :handler handler}]}
{:uri "/v1/orgs/:org-id/devices/:batch/:type" :nested [{:method :get, :handler handler}]}
{:uri "/v1/users/:user-id/topics" :nested [{:method :get, :handler handler}]}
{:uri "/v1/users/:user-id/bookmarks/followers" :nested [{:method :get, :handler handler}]}
{:uri "/v2/datasets/:dataset-id" :nested [{:method :get, :handler handler}]}
{:uri "/v1/orgs/:org-id/usage-stats" :nested [{:method :get, :handler handler}]}
{:uri "/v1/orgs/:org-id/devices/:client-id" :nested [{:method :get, :handler handler}]}
{:uri "/v1/messages/user/:user-id" :nested [{:method :get, :handler handler}]}
{:uri "/v1/users/:user-id/devices" :nested [{:method :get, :handler handler}]}
{:uri "/v1/public/users/:user-id" :nested [{:method :get, :handler handler}]}
{:uri "/v1/orgs/:org-id/errors" :nested [{:method :get, :handler handler}]}
{:uri "/v1/public/orgs/:org-id" :nested [{:method :get, :handler handler}]}
{:uri "/v1/orgs/:org-id/invitations" :nested [{:method :get, :handler handler}]}
{:uri "/v2/public/messages/dataset/bulk" :nested [{:method :get, :handler handler}]}
{:uri "/v1/users/:user-id/devices/bulk" :nested [{:method :get, :handler handler}]}
{:uri "/v1/users/:user-id/device-errors" :nested [{:method :get, :handler handler}]}
{:uri "/v2/login" :nested [{:method :get, :handler handler}]}
{:uri "/v1/users/:user-id/usage-stats" :nested [{:method :get, :handler handler}]}
{:uri "/v2/users/:user-id/devices" :nested [{:method :get, :handler handler}]}
{:uri "/v1/users/:user-id/claim-device/:client-id" :nested [{:method :get, :handler handler}]}
{:uri "/v2/public/projects/:project-id" :nested [{:method :get, :handler handler}]}
{:uri "/v2/public/datasets/:dataset-id" :nested [{:method :get, :handler handler}]}
{:uri "/v2/users/:user-id/topics/bulk" :nested [{:method :get, :handler handler}]}
{:uri "/v1/messages/device/:client-id" :nested [{:method :get, :handler handler}]}
{:uri "/v1/users/:user-id/owned-orgs" :nested [{:method :get, :handler handler}]}
{:uri "/v1/topics/:topic" :nested [{:method :get, :handler handler}]}
{:uri "/v1/users/:user-id/bookmark/:topic" :nested [{:method :get, :handler handler}]}
{:uri "/v1/orgs/:org-id/members/:user-id" :nested [{:method :get, :handler handler}]}
{:uri "/v1/users/:user-id/devices/:client-id" :nested [{:method :get, :handler handler}]}
{:uri "/v1/users/:user-id" :nested [{:method :get, :handler handler}]}
{:uri "/v1/orgs/:org-id/devices" :nested [{:method :get, :handler handler}]}
{:uri "/v1/orgs/:org-id/members" :nested [{:method :get, :handler handler}]}
{:uri "/v1/orgs/:org-id/members/invitation-data/:user-id" :nested [{:method :get, :handler handler}]}
{:uri "/v2/orgs/:org-id/topics" :nested [{:method :get, :handler handler}]}
{:uri "/v1/whoami" :nested [{:method :get, :handler handler}]}
{:uri "/v1/orgs/:org-id" :nested [{:method :get, :handler handler}]}
{:uri "/v1/users/:user-id/api-key" :nested [{:method :get, :handler handler}]}
{:uri "/v2/schemas" :nested [{:method :get, :handler handler}]}
{:uri "/v2/users/:user-id/topics" :nested [{:method :get, :handler handler}]}
{:uri "/v1/orgs/:org-id/confirm-membership/:token" :nested [{:method :get, :handler handler}]}
{:uri "/v2/topics/:topic" :nested [{:method :get, :handler handler}]}
{:uri "/v1/messages/topic/:topic" :nested [{:method :get, :handler handler}]}
{:uri "/v1/users/:user-id/devices/:client-id/reset-password" :nested [{:method :get, :handler handler}]}
{:uri "/v2/topics" :nested [{:method :get, :handler handler}]}
{:uri "/v1/login" :nested [{:method :get, :handler handler}]}
{:uri "/v1/users/:user-id/orgs" :nested [{:method :get, :handler handler}]}
{:uri "/v2/public/messages/dataset/:dataset-id" :nested [{:method :get, :handler handler}]}
{:uri "/v1/topics" :nested [{:method :get, :handler handler}]}
{:uri "/v1/orgs" :nested [{:method :get, :handler handler}]}
{:uri "/v1/users/:user-id/bookmarks" :nested [{:method :get, :handler handler}]}
{:uri "/v1/orgs/:org-id/topics" :nested [{:method :get, :handler handler}]}])
(def opensensors-calfpath-walker-handler
(partial cr/dispatch (cr/compile-routes opensensors-calfpath-routes {:show-uris-400? false})))
(def opensensors-calfpath-unroll-handler
(cr/make-dispatcher (cr/compile-routes opensensors-calfpath-routes {:show-uris-400? false})))
(comment
(pedestal/find-route
(map-tree/router
@ -422,7 +551,11 @@
router (reitit/router routes)
reitit-f #(reitit/match-by-path router (:uri %))
reitit-ring-f (ring/ring-handler (ring/router opensensors-routes))
reitit-ring-fast-f (ring/ring-handler (ring/router opensensors-routes) nil {:inject-router? false, :inject-match? false})
bidi-f #(bidi/match-route opensensors-bidi-routes (:uri %))
calfpath-macros-f opensensors-calfpath-macro-handler
calfpath-walker-f opensensors-calfpath-walker-handler
calfpath-unroll-f opensensors-calfpath-unroll-handler
ataraxy-f (partial ataraxy/matches opensensors-ataraxy-routes)
compojure-f opensensors-compojure-routes
pedestal-f (partial pedestal/find-route opensensors-pedestal-routes)
@ -432,6 +565,7 @@
;; 2065ns
;; 662ns (prefix-tree-router)
;; 567ns (segment-router)
;; 326ns (java-segment-router)
(b! "reitit" reitit-f)
;; 2845ns
@ -441,11 +575,24 @@
;; 702ns (before path-parameters)
;; 806ns (decode path-parameters)
;; 735ns (maybe-map-values)
;; 474ns (java-segment-router)
(b! "reitit-ring" reitit-ring-f)
;; 385ns (java-segment-router, no injects)
(b! "reitit-ring-fast" reitit-ring-fast-f)
;; 2137ns
(b! "calfpath-walker" calfpath-walker-f)
;; 4774ns
(b! "calfpath-unroll" calfpath-unroll-f)
;; 2821ns
(b! "pedestal" pedestal-f)
;; 4803ns
(b! "calfpath-macros" calfpath-macros-f)
;; 11615ns
(b! "compojure" compojure-f)

View file

@ -2,7 +2,8 @@
(:require [clojure.test :refer :all]
[io.pedestal.http.route.prefix-tree :as p]
[reitit.segment :as segment]
[criterium.core :as cc]))
[criterium.core :as cc])
(:import (reitit SegmentTrie)))
;;
;; testing
@ -69,14 +70,13 @@
(p/insert acc p d))
nil routes))
#_(def reitit-tree
(def matcher
(.matcher
^SegmentTrie
(reduce
(fn [acc [p d]]
(trie/insert acc p d))
nil routes))
(def reitit-segment
(segment/create routes))
(segment/insert acc p d))
nil routes)))
(defn bench! []
@ -108,8 +108,9 @@
;; 1.0µs (Match records)
;; 0.63µs (Single sweep path paraµs)
;; 0.51µs (Cleanup)
;; 0.30µs (Java)
(cc/quick-bench
(segment/lookup reitit-segment "/v1/orgs/1/topics")))
(segment/lookup matcher "/v1/orgs/1/topics")))
(comment
(bench!))
@ -117,4 +118,4 @@
(comment
(p/lookup pedestal-tree "/v1/orgs/1/topics")
#_(trie/lookup reitit-tree "/v1/orgs/1/topics" {})
(segment/lookup reitit-segment "/v1/orgs/1/topics"))
(segment/lookup matcher "/v1/orgs/1/topics"))

View file

@ -18,21 +18,31 @@
;; Memory: 16 GB
;;
(def app
(defn create-app [options]
(ring/ring-handler
(ring/router
[["/auth/login" identity]
["/auth/recovery/token/:token" identity]
["/workspace/:project/:page" identity]])))
["/workspace/:project/:page" identity]])
(ring/create-default-handler)
options))
(comment
(let [request {:request-method :post, :uri "/auth/login"}]
(defn bench-app []
(let [request {:request-method :post, :uri "/auth/login"}
app1 (create-app nil)
app2 (create-app {:inject-match? false, :inject-router? false})]
;; 192ns (initial)
;; 163ns (always assoc path params)
;; 132ns (expand methods)
;; 111ns (java-segment-router)
(cc/quick-bench
(app request))
(app1 request))
;; 113ns (don't inject router)
;; 89ns (don't inject router & match)
))
;; 77ns (java-segment-router)
(cc/quick-bench
(app2 request))))
(comment
(bench-app))

View file

@ -60,6 +60,8 @@
"modules/reitit-sieppari/src"
"modules/reitit-pedestal/src"]
:java-source-paths ["modules/reitit-core/java-src"]
:dependencies [[org.clojure/clojure "1.10.0"]
[org.clojure/clojurescript "1.10.439"]
@ -99,6 +101,7 @@
[ikitommi/immutant-web "3.0.0-alpha1"]
[io.pedestal/pedestal.service "0.5.5"]
[io.pedestal/pedestal.jetty "0.5.5"]
[calfpath "0.7.1"]
[org.clojure/core.async "0.4.490"]
[manifold "0.1.8"]
[funcool/promesa "1.9.0"]

View file

@ -17,6 +17,8 @@
(is (= [["/api/ipa/:size" {:name ::beer}]]
(r/routes router)))
(is (map? (r/options router)))
(is (= nil
(r/match-by-path router "/api")))
(is (= (r/map->Match
{:template "/api/ipa/:size"
:data {:name ::beer}
@ -72,8 +74,9 @@
["/abba/1" ::abba2]
["/:jabba/2" ::jabba2]
["/:abba/:dabba/doo" ::doo]
["/abba/dabba/boo/baa" ::baa]
["/abba/:dabba/boo" ::boo]
["/:jabba/:dabba/:doo/*foo" ::wild]]
["/:jabba/:dabba/:doo/:daa/*foo" ::wild]]
{:router r})
matches #(-> router (r/match-by-path %) :data :name)]
(is (= ::abba (matches "/abba")))
@ -81,6 +84,8 @@
(is (= ::jabba2 (matches "/abba/2")))
(is (= ::doo (matches "/abba/1/doo")))
(is (= ::boo (matches "/abba/1/boo")))
(is (= ::baa (matches "/abba/dabba/boo/baa")))
(is (= ::boo (matches "/abba/dabba/boo")))
(is (= ::wild (matches "/olipa/kerran/avaruus/vaan/ei/toista/kertaa")))))
(testing "empty path segments"
@ -108,6 +113,8 @@
(is (= [["/api/ipa/large" {:name ::beer}]]
(r/routes router)))
(is (map? (r/options router)))
(is (= nil
(r/match-by-path router "/api")))
(is (= (r/map->Match
{:template "/api/ipa/large"
:data {:name ::beer}

View file

@ -3,9 +3,9 @@
[reitit.impl :as impl]))
(deftest segments-test
(is (= ["" "api" "ipa" "beer" "craft" "bisse"]
(is (= ["api" "ipa" "beer" "craft" "bisse"]
(into [] (impl/segments "/api/ipa/beer/craft/bisse"))))
(is (= ["" "a" "" "b" "" "c" ""]
(is (= ["a" "" "b" "" "c" ""]
(into [] (impl/segments "/a//b//c/")))))
(deftest strip-nils-test

View file

@ -0,0 +1,21 @@
(ns reitit.segment-test
(:require [clojure.test :refer [deftest testing is are]]
[reitit.segment :as s]))
(deftest tests
(is (= (s/->Match {:a 1} {})
(-> (s/insert nil "/foo" {:a 1})
(s/compile)
(s/lookup "/foo"))))
(is (= (s/->Match {:a 1} {})
(-> (s/insert nil "/foo" {:a 1})
(s/insert "/foo/*bar" {:b 1})
(s/compile)
(s/lookup "/foo"))))
(is (= (s/->Match {:b 1} {:bar "bar"})
(-> (s/insert nil "/foo" {:a 1})
(s/insert "/foo/*bar" {:b 1})
(s/compile)
(s/lookup "/foo/bar")))))