mirror of
https://github.com/metosin/reitit.git
synced 2026-02-20 17:29:08 +00:00
Remove segment-rouiter code
This commit is contained in:
parent
ce80f83319
commit
8628f0cec6
5 changed files with 29 additions and 433 deletions
|
|
@ -1,319 +0,0 @@
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (start <= size) {
|
|
||||||
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 Matcher scanner(List<Matcher> matchers) {
|
|
||||||
return new LinearMatcher(matchers);
|
|
||||||
}
|
|
||||||
|
|
||||||
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"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,75 +0,0 @@
|
||||||
(ns reitit.segment
|
|
||||||
(:refer-clojure :exclude [-lookup compile])
|
|
||||||
(:require [reitit.impl :as impl]
|
|
||||||
[clojure.string :as str])
|
|
||||||
#?(:clj (:import (reitit SegmentTrie SegmentTrie$Match))))
|
|
||||||
|
|
||||||
(defrecord Match [data path-params])
|
|
||||||
|
|
||||||
(defprotocol Segment
|
|
||||||
(-insert [this ps data])
|
|
||||||
(-lookup [this ps path-params]))
|
|
||||||
|
|
||||||
(extend-protocol Segment
|
|
||||||
nil
|
|
||||||
(-insert [_ _ _])
|
|
||||||
(-lookup [_ _ _]))
|
|
||||||
|
|
||||||
(defn- -catch-all [children catch-all path-params p ps]
|
|
||||||
(-lookup
|
|
||||||
(impl/fast-get children catch-all)
|
|
||||||
nil
|
|
||||||
(assoc path-params catch-all (str/join "/" (cons p ps)))))
|
|
||||||
|
|
||||||
(defn- segment
|
|
||||||
([] (segment {} #{} nil nil))
|
|
||||||
([children wilds catch-all match]
|
|
||||||
(let [children' (impl/fast-map children)
|
|
||||||
wilds? (seq wilds)]
|
|
||||||
^{:type ::segment}
|
|
||||||
(reify
|
|
||||||
Segment
|
|
||||||
(-insert [_ [p & ps] d]
|
|
||||||
(if-not p
|
|
||||||
(segment children wilds catch-all d)
|
|
||||||
(let [[w c] ((juxt impl/wild-param impl/catch-all-param) p)
|
|
||||||
wilds (if w (conj wilds w) wilds)
|
|
||||||
catch-all (or c catch-all)
|
|
||||||
children (update children (or w c p) #(-insert (or % (segment)) ps d))]
|
|
||||||
(segment children wilds catch-all match))))
|
|
||||||
(-lookup [_ [p & ps] path-params]
|
|
||||||
(if (nil? p)
|
|
||||||
(when match (assoc match :path-params path-params))
|
|
||||||
(or (-lookup (impl/fast-get children' p) ps path-params)
|
|
||||||
(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)))))))))
|
|
||||||
|
|
||||||
;;
|
|
||||||
;; public api
|
|
||||||
;;
|
|
||||||
|
|
||||||
(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 compile [trie]
|
|
||||||
"Compiles the Trie so that [[lookup]] can be used."
|
|
||||||
#?(:cljs trie
|
|
||||||
:clj (.matcher (or ^SegmentTrie trie (SegmentTrie.)))))
|
|
||||||
|
|
||||||
(defn scanner [compiled-tries]
|
|
||||||
"Returns a new compiled trie that does linear scan on the given compiled tries on [[lookup]]."
|
|
||||||
#?(:cljs (reify
|
|
||||||
Segment
|
|
||||||
(-lookup [_ ps params]
|
|
||||||
(some (fn [trie] (-lookup trie ps params)) compiled-tries)))
|
|
||||||
:clj (SegmentTrie/scanner compiled-tries)))
|
|
||||||
|
|
||||||
(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))))))
|
|
||||||
|
|
@ -1,10 +1,8 @@
|
||||||
(ns reitit.prefix-tree-perf-test
|
(ns reitit.prefix-tree-perf-test
|
||||||
(:require [clojure.test :refer :all]
|
(:require [clojure.test :refer :all]
|
||||||
[io.pedestal.http.route.prefix-tree :as p]
|
[io.pedestal.http.route.prefix-tree :as p]
|
||||||
[reitit.segment :as segment]
|
|
||||||
[reitit.trie :as trie]
|
[reitit.trie :as trie]
|
||||||
[criterium.core :as cc])
|
[criterium.core :as cc]))
|
||||||
(:import (reitit SegmentTrie)))
|
|
||||||
|
|
||||||
;;
|
;;
|
||||||
;; testing
|
;; testing
|
||||||
|
|
@ -71,14 +69,6 @@
|
||||||
(p/insert acc p d))
|
(p/insert acc p d))
|
||||||
nil routes))
|
nil routes))
|
||||||
|
|
||||||
(def segment-matcher
|
|
||||||
(.matcher
|
|
||||||
^SegmentTrie
|
|
||||||
(reduce
|
|
||||||
(fn [acc [p d]]
|
|
||||||
(segment/insert acc p d))
|
|
||||||
nil routes)))
|
|
||||||
|
|
||||||
(def trie-matcher
|
(def trie-matcher
|
||||||
(trie/compile
|
(trie/compile
|
||||||
(reduce
|
(reduce
|
||||||
|
|
@ -117,7 +107,7 @@
|
||||||
;; 0.63µs (Single sweep path paraµs)
|
;; 0.63µs (Single sweep path paraµs)
|
||||||
;; 0.51µs (Cleanup)
|
;; 0.51µs (Cleanup)
|
||||||
;; 0.30µs (Java)
|
;; 0.30µs (Java)
|
||||||
(cc/quick-bench
|
#_(cc/quick-bench
|
||||||
(segment/lookup segment-matcher "/v1/orgs/1/topics"))
|
(segment/lookup segment-matcher "/v1/orgs/1/topics"))
|
||||||
|
|
||||||
;; 0.32µs (initial)
|
;; 0.32µs (initial)
|
||||||
|
|
@ -134,5 +124,5 @@
|
||||||
(comment
|
(comment
|
||||||
(p/lookup pedestal-tree "/v1/orgs/1/topics")
|
(p/lookup pedestal-tree "/v1/orgs/1/topics")
|
||||||
(trie/lookup trie-matcher "/v1/orgs/1/topics")
|
(trie/lookup trie-matcher "/v1/orgs/1/topics")
|
||||||
(segment/lookup segment-matcher "/v1/orgs/1/topics"))
|
#_(segment/lookup segment-matcher "/v1/orgs/1/topics"))
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,26 +0,0 @@
|
||||||
(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"))))
|
|
||||||
|
|
||||||
(is (= (s/->Match {:a 1} {})
|
|
||||||
(-> (s/insert nil "" {:a 1})
|
|
||||||
(s/compile)
|
|
||||||
(s/lookup "")))))
|
|
||||||
26
test/cljc/reitit/trie_test.cljc
Normal file
26
test/cljc/reitit/trie_test.cljc
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
(ns reitit.trie-test
|
||||||
|
(:require [clojure.test :refer [deftest testing is are]]
|
||||||
|
[reitit.trie :as rt]))
|
||||||
|
|
||||||
|
(deftest tests
|
||||||
|
(is (= (rt/->Match {:a 1} {})
|
||||||
|
(-> (rt/insert nil "/foo" {:a 1})
|
||||||
|
(rt/compile)
|
||||||
|
(rt/lookup "/foo"))))
|
||||||
|
|
||||||
|
(is (= (rt/->Match {:a 1} {})
|
||||||
|
(-> (rt/insert nil "/foo" {:a 1})
|
||||||
|
(rt/insert "/foo/*bar" {:b 1})
|
||||||
|
(rt/compile)
|
||||||
|
(rt/lookup "/foo"))))
|
||||||
|
|
||||||
|
(is (= (rt/->Match {:b 1} {:bar "bar"})
|
||||||
|
(-> (rt/insert nil "/foo" {:a 1})
|
||||||
|
(rt/insert "/foo/*bar" {:b 1})
|
||||||
|
(rt/compile)
|
||||||
|
(rt/lookup "/foo/bar"))))
|
||||||
|
|
||||||
|
(is (= (rt/->Match {:a 1} {})
|
||||||
|
(-> (rt/insert nil "" {:a 1})
|
||||||
|
(rt/compile)
|
||||||
|
(rt/lookup "")))))
|
||||||
Loading…
Reference in a new issue