mirror of
https://github.com/metosin/reitit.git
synced 2026-01-10 09:09:50 +00:00
commit
6ef78b6238
40 changed files with 1571 additions and 1265 deletions
14
CHANGELOG.md
14
CHANGELOG.md
|
|
@ -1,5 +1,19 @@
|
|||
## 0.3.0-SNAPSHOT
|
||||
|
||||
### `reitit.core`
|
||||
|
||||
* welcome new wildcard routing!
|
||||
* optional bracket-syntax with parameters
|
||||
* `"/user/:user-id"` = `"/user/{user-id}"`
|
||||
* `"/assets/*asset"` = `"/assets/{*asset}`
|
||||
* enabling qualified parameters
|
||||
* `"/user/{my.user/id}/{my.order/id}"`
|
||||
* parameters don't have to span whole segments
|
||||
* `"/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
|
||||
|
||||
## `reitit-frontend`
|
||||
|
||||
* **BREAKING** New frontend controllers:
|
||||
|
|
|
|||
|
|
@ -5,10 +5,10 @@ Reitit ships with several different implementations for the `Router` protocol, o
|
|||
| router | description |
|
||||
| ------------------------------|-------------|
|
||||
| `:linear-router` | Matches the routes one-by-one starting from the top until a match is found. Slow, but works with all route trees.
|
||||
| `:segment-router` | Router that creates a optimized [search trie](https://en.wikipedia.org/wiki/Trie) out of an route table. Much faster than `:linear-router` for wildcard routes. Valid only if there are no [Route conflicts](../basics/route_conflicts.md).
|
||||
| `:trie-router` | Router that creates a optimized [search trie](https://en.wikipedia.org/wiki/Trie) out of an route table. Much faster than `:linear-router` for wildcard routes. Valid only if there are no [Route conflicts](../basics/route_conflicts.md).
|
||||
| `:lookup-router` | Fast router, uses hash-lookup to resolve the route. Valid if no paths have path or catch-all parameters and there are no [Route conflicts](../basics/route_conflicts.md).
|
||||
| `:single-static-path-router` | Super fast router: string-matches a route. Valid only if there is one static route.
|
||||
| `:mixed-router` | Contains two routers: `:segment-router` for wildcard routes and a `:lookup-router` or `:single-static-path-router` for static routes. Valid only if there are no [Route conflicts](../basics/route_conflicts.md).
|
||||
| `:mixed-router` | Contains two routers: `:trie-router` for wildcard routes and a `:lookup-router` or `:single-static-path-router` for static routes. Valid only if there are no [Route conflicts](../basics/route_conflicts.md).
|
||||
| `:quarantine-router` | Contains two routers: `:mixed-router` for non-conflicting routes and a `:linear-router` for conflicting routes.
|
||||
|
||||
The router name can be asked from the router:
|
||||
|
|
|
|||
|
|
@ -1,8 +1,17 @@
|
|||
# Route Data
|
||||
|
||||
Route data is the core feature of reitit. Routes can have any map-like data attached to them. This data is interpreted either by the client application or the `Router` via its `:coerce` and `:compile` hooks. Route data format can be defined and validated with `clojure.spec` enabling an architecture of both [adaptive and principled](https://youtu.be/x9pxbnFC4aQ?t=1907) components.
|
||||
Route data is the key feature of reitit. Routes can have any map-like data attached to them, to be interpreted by the client application, `Router` or routing components like `Middleware` or `Interceptors`.
|
||||
|
||||
Raw routes can have a non-sequential route argument that is expanded (via router `:expand` hook) into route data at router creation time. By default, Keywords are expanded into `:name` and functions into `:handler` keys.
|
||||
```clj
|
||||
[["/ping" {:name ::ping}]
|
||||
["/pong" {:handler identity}]
|
||||
["/users" {:get {:roles #{:admin}
|
||||
:handler identity}}]]
|
||||
```
|
||||
|
||||
Besides map-like data, raw routes can have any non-sequential route argument after the path. This argument is expanded by `Router` (via `:expand` option) into route data at router creation time.
|
||||
|
||||
By default, Keywords are expanded into `:name` and functions into `:handler` keys.
|
||||
|
||||
```clj
|
||||
(require '[reitit.core :as r])
|
||||
|
|
@ -15,11 +24,13 @@ Raw routes can have a non-sequential route argument that is expanded (via router
|
|||
:handler identity}}]]))
|
||||
```
|
||||
|
||||
The expanded route data can be retrieved from a router with `routes` and is returned with `match-by-path` and `match-by-name` in case of a route match.
|
||||
## Using Route Data
|
||||
|
||||
Expanded route data can be retrieved from a router with `routes` and is returned with `match-by-path` and `match-by-name` in case of a route match.
|
||||
|
||||
```clj
|
||||
(r/routes router)
|
||||
; [["/ping" {:name :user/ping}]
|
||||
; [["/ping" {:name ::ping}]
|
||||
; ["/pong" {:handler identity]}
|
||||
; ["/users" {:get {:roles #{:admin}
|
||||
; :handler identity}}]]
|
||||
|
|
@ -43,7 +54,7 @@ The expanded route data can be retrieved from a router with `routes` and is retu
|
|||
; :path "/ping"}
|
||||
```
|
||||
|
||||
## Nested route data
|
||||
## Nested Route Data
|
||||
|
||||
For nested route trees, route data is accumulated recursively from root towards leafs using [meta-merge](https://github.com/weavejester/meta-merge). Default behavior for collections is `:append`, but this can be overridden to `:prepend`, `:replace` or `:displace` using the target meta-data.
|
||||
|
||||
|
|
@ -73,9 +84,72 @@ Resolved route tree:
|
|||
; :roles #{:db-admin}}]]
|
||||
```
|
||||
|
||||
## Expansion
|
||||
## Route Data Fragments
|
||||
|
||||
By default, router `:expand` hook maps to `reitit.core/expand` function, backed by a `reitit.core/Expand` protocol. One can provide either a totally different function or add new implementations to that protocol. Expand implementations can be recursive.
|
||||
Just like [fragments in React.js](https://reactjs.org/docs/fragments.html), we can create routing tree fragments by using empty path `""`. This allows us to add route data without accumulating to path.
|
||||
|
||||
Given a route tree:
|
||||
|
||||
```clj
|
||||
[["/swagger.json" ::swagger]
|
||||
["/api-docs" ::api-docs]
|
||||
["/api/ping" ::ping]
|
||||
["/api/pong" ::pong]]
|
||||
```
|
||||
|
||||
Adding `:no-doc` route data to exclude the first routes from generated [Swagger documentation](../ring/swagger.md):
|
||||
|
||||
```clj
|
||||
[["" {:no-doc true}
|
||||
["/swagger.json" ::swagger]
|
||||
["/api-docs" ::api-docs]]
|
||||
["/api/ping" ::ping]
|
||||
["/api/pong" ::pong]]
|
||||
```
|
||||
|
||||
Accumulated route data:
|
||||
|
||||
```clj
|
||||
(def router
|
||||
(r/router
|
||||
[["" {:no-doc true}
|
||||
["/swagger.json" ::swagger]
|
||||
["/api-docs" ::api-docs]]
|
||||
["/api/ping" ::ping]
|
||||
["/api/pong" ::pong]]))
|
||||
|
||||
(r/routes router)
|
||||
; [["/swagger.json" {:no-doc true, :name ::swagger}]
|
||||
; ["/api-docs" {:no-doc true, :name ::api-docs}]
|
||||
; ["/api/ping" {:name ::ping}]
|
||||
; ["/api/pong" {:name ::pong}]]
|
||||
```
|
||||
|
||||
## Top-level Route Data
|
||||
|
||||
Route data can be introduced also via `Router` option `:data`:
|
||||
|
||||
```clj
|
||||
(def router
|
||||
(r/router
|
||||
["/api"
|
||||
{:middleware [::api]}
|
||||
["/ping" ::ping]
|
||||
["/pong" ::pong]]
|
||||
{:data {:middleware [::session]}}))
|
||||
```
|
||||
|
||||
Expanded routes:
|
||||
|
||||
```clj
|
||||
[["/api/ping" {:middleware [::session ::api], :name ::ping}]
|
||||
["/api/pong" {:middleware [::session ::api], :name ::pong}]]
|
||||
```
|
||||
|
||||
|
||||
## Customizing Expansion
|
||||
|
||||
By default, router `:expand` option has value `r/expand` function, backed by a `r/Expand` protocol. Expansion can be customized either by swapping the `:expand` implementation or by extending the Protocol. `r/Expand` implementations can be recursive.
|
||||
|
||||
Naive example to add direct support for `java.io.File` route argument:
|
||||
|
||||
|
|
@ -91,4 +165,8 @@ Naive example to add direct support for `java.io.File` route argument:
|
|||
["/" (java.io.File. "index.html")])
|
||||
```
|
||||
|
||||
See [router options](../advanced/configuring_routers.md) for all available options.
|
||||
Page [shared routes](../advanced/shared_routes.md#using-custom-expander) has an example of an custom `:expand` implementation.
|
||||
|
||||
## Route data validation
|
||||
|
||||
See [Route data validation](route_data_validation.md).
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ Routes are defined as vectors of String path and optional (non-sequential) route
|
|||
|
||||
Routes can be wrapped in vectors and lists and `nil` routes are ignored.
|
||||
|
||||
Paths can have path-parameters (`:id`) or catch-all-parameters (`*path`).
|
||||
Paths can have path-parameters (`:id`) or catch-all-parameters (`*path`). Since version `0.4.0`, parameters can also be wrapped in brackets, enabling use of qualified keywords `{user/id}`, `{*user/path}`. The non-bracket syntax might be deprecated later.
|
||||
|
||||
### Examples
|
||||
|
||||
|
|
@ -35,12 +35,21 @@ Routes with path parameters:
|
|||
["/api/:version/ping"]]
|
||||
```
|
||||
|
||||
```clj
|
||||
[["/users/{user-id}"]
|
||||
["/files/file-{number}.pdf"]]
|
||||
```
|
||||
|
||||
Route with catch-all parameter:
|
||||
|
||||
```clj
|
||||
["/public/*path"]
|
||||
```
|
||||
|
||||
```clj
|
||||
["/public/{*path}"]
|
||||
```
|
||||
|
||||
Nested routes:
|
||||
|
||||
```clj
|
||||
|
|
@ -59,6 +68,42 @@ Same routes flattened:
|
|||
["/api/ping" {:name ::ping}]]
|
||||
```
|
||||
|
||||
### Encoding
|
||||
|
||||
Reitit does not apply any encoding to your paths. If you need that, you must encode them yourself. E.g., `/foo bar` should be `/foo%20bar`.
|
||||
|
||||
### Wildcards
|
||||
|
||||
Normal path-parameters (`:id`) can start anywhere in the path string, but have to end either to slash `/` (currently hardcoded) or to en end of path string:
|
||||
|
||||
```clj
|
||||
[["/api/:version"]
|
||||
["/files/file-:number"]
|
||||
["/user/:user-id/orders"]]
|
||||
```
|
||||
|
||||
Bracket path-parameters can start and stop anywhere in the path-string, the following character is used as a terminator.
|
||||
|
||||
```clj
|
||||
[["/api/{version}"]
|
||||
["/files/{name}.{extension}"]
|
||||
["/user/{user-id}/orders"]]
|
||||
```
|
||||
|
||||
Having multiple terminators after a bracket path-path parameter with identical path prefix will cause a compile-time error at router creation:
|
||||
|
||||
```clj
|
||||
[["/files/file-{name}.pdf"] ;; terminator \.
|
||||
["/files/file-{name}-{version}.pdf"]] ;; terminator \-
|
||||
```
|
||||
|
||||
### Slash Free Routing
|
||||
|
||||
```clj
|
||||
[["broker.{customer}.{device}.{*data}"]
|
||||
["events.{target}.{type}"]]
|
||||
```
|
||||
|
||||
### Generating routes
|
||||
|
||||
Routes are just data, so it's easy to create them programmatically:
|
||||
|
|
@ -68,7 +113,7 @@ Routes are just data, so it's easy to create them programmatically:
|
|||
["/api" {:interceptors [::api ::db]}
|
||||
(for [[type interceptor] actions
|
||||
:let [path (str "/" (name interceptor))
|
||||
method (condp = type
|
||||
method (case type
|
||||
:query :get
|
||||
:command :post)]]
|
||||
[path {method {:interceptors [interceptor]}}])])
|
||||
|
|
@ -84,8 +129,3 @@ Routes are just data, so it's easy to create them programmatically:
|
|||
; ["/add-user" {:post {:interceptors [add-user]}}]
|
||||
; ["/add-order" {:post {:interceptors [add-order]}}])]
|
||||
```
|
||||
|
||||
### Encoding
|
||||
|
||||
Reitit does not apply any encoding to your paths. If you need that, you must encode them yourself.
|
||||
E.g., `/foo bar` should be `/foo%20bar`.
|
||||
|
|
|
|||
|
|
@ -41,10 +41,74 @@ The flattened route tree:
|
|||
; ["/api/user/:id" {:name :user/user}]]
|
||||
```
|
||||
|
||||
With a router instance, we can do [Path-based routing](path_based_routing.md) or [Name-based (Reverse) routing](name_based_routing.md).
|
||||
|
||||
## More details
|
||||
|
||||
Router options:
|
||||
|
||||
```clj
|
||||
(r/options router)
|
||||
{:lookup #object[...]
|
||||
:expand #object[...]
|
||||
:coerce #object[...]
|
||||
:compile #object[...]
|
||||
:conflicts #object[...]}
|
||||
```
|
||||
|
||||
Route names:
|
||||
|
||||
```clj
|
||||
(r/route-names router)
|
||||
; [:user/ping :user/user]
|
||||
```
|
||||
|
||||
The compiled route tree:
|
||||
|
||||
```clj
|
||||
(r/routes router)
|
||||
; [["/api/ping" {:name :user/ping} nil]
|
||||
; ["/api/user/:id" {:name :user/user} nil]]
|
||||
```
|
||||
|
||||
### Composing
|
||||
|
||||
As routes are defined as plain data, it's easy to merge multiple route trees into a single router
|
||||
|
||||
```clj
|
||||
(def user-routes
|
||||
[["/users" ::users]
|
||||
["/users/:id" ::user]])
|
||||
|
||||
(def admin-routes
|
||||
["/admin"
|
||||
["/ping" ::ping]
|
||||
["/users" ::users]])
|
||||
|
||||
(r/router
|
||||
[admin-routes
|
||||
user-routes])
|
||||
```
|
||||
|
||||
Merged route tree:
|
||||
|
||||
```clj
|
||||
(r/routes router)
|
||||
; [["/admin/ping" {:name :user/ping}]
|
||||
; ["/admin/db" {:name :user/db}]
|
||||
; ["/users" {:name :user/users}]
|
||||
; ["/users/:id" {:name :user/user}]]
|
||||
```
|
||||
|
||||
More details on [composing routers](../advanced/composing_routers.md).
|
||||
|
||||
### Behind the scenes
|
||||
|
||||
When router is created, the following steps are done:
|
||||
* route tree is flattened
|
||||
* route arguments are expanded (via `reitit.core/Expand` protocol) and optionally coerced
|
||||
* [route conflicts](advanced/route_conflicts.md) are resolved
|
||||
* route tree is compiled
|
||||
* actual [router implementation](../advanced/different_routers.md) is selected and created
|
||||
* route arguments are expanded (via `:expand` option)
|
||||
* routes are coerced (via `:coerce` options)
|
||||
* route tree is compiled (via `:compile` options)
|
||||
* [route conflicts](advanced/route_conflicts.md) are resolved (via `:conflicts` options)
|
||||
* optionally, route data is validated (via `:validate` options)
|
||||
* [router implementation](../advanced/different_routers.md) is automatically selected (or forced via `:router` options) and created
|
||||
|
|
|
|||
Binary file not shown.
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 30 KiB |
|
|
@ -1,6 +1,6 @@
|
|||
# Performance
|
||||
|
||||
Besides having great features, goal of reitit is to be really, really fast. The routing was originally exported from Pedestal, but since rewritten.
|
||||
Reitit tries to be really, really fast.
|
||||
|
||||

|
||||
|
||||
|
|
@ -9,9 +9,10 @@ Besides having great features, goal of reitit is to be really, really fast. The
|
|||
* Multiple routing algorithms, chosen based on the route tree
|
||||
* Route flattening and re-ordering
|
||||
* Managed mutability over immutability
|
||||
* Precompute/compile as much as possible (matches, middleware, interceptors, routes)
|
||||
* Precompute/compile as much as possible (matches, middleware, interceptors, routes, path-parameter sets)
|
||||
* Use abstractions that enable JVM optimizations
|
||||
* Use small functions to enable JVM Inlining
|
||||
* Use Java where needed
|
||||
* Protocols over Multimethods
|
||||
* Records over Maps
|
||||
* Always be measuring
|
||||
|
|
@ -63,13 +64,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): 315 µs -> 3.2M ops/sec
|
||||
;; Execution time mean (per 1000): 115 µs -> 8.7M 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 6-40x 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 18-110x 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,7 +80,7 @@ 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 [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.
|
||||
Thanks to the snappy [Wildcard Trie](https://github.com/metosin/reitit/blob/master/modules/reitit-core/java-src/reitit/Trie.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.
|
||||
|
||||

|
||||
|
||||
|
|
@ -93,13 +94,17 @@ Both `reitit-ring` and Pedestal shine in this test, thanks to the fast lookup-ro
|
|||
|
||||
**NOTE**: in real life, there are usually always also wild-card routes present. In this case, Pedestal would fallback from lookup-router to the prefix-tree router, which is order of magnitude slower (30x in this test). Reitit would handle this nicely thanks to it's `:mixed-router`: all static routes would still be served with `:lookup-router`, just the wildcard routes with `:segment-tree`. The performance would not notably degrade.
|
||||
|
||||
### Path conflicts
|
||||
|
||||
**TODO**
|
||||
|
||||
### Why measure?
|
||||
|
||||
The reitit routing perf is measured to get an internal baseline to optimize against. We also want to ensure that new features don't regress the performance. Perf tests should be run in a stable CI environment. Help welcome!
|
||||
|
||||
### 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 kinda awesome.
|
||||
A quick poke to [the fast routers in Go](https://github.com/julienschmidt/go-http-routing-benchmark) indicates that reitit is less 50% slower than the fastest routers in Go. Which is kinda awesome.
|
||||
|
||||
### Faster!
|
||||
|
||||
|
|
|
|||
|
|
@ -83,7 +83,8 @@
|
|||
(ring/routes
|
||||
(swagger-ui/create-swagger-ui-handler
|
||||
{:path "/"
|
||||
:config {:validatorUrl nil}})
|
||||
:config {:validatorUrl nil
|
||||
:operationsSorter "alpha"}})
|
||||
(ring/create-default-handler))))
|
||||
|
||||
(defn start []
|
||||
|
|
|
|||
|
|
@ -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"));
|
||||
}
|
||||
}
|
||||
295
modules/reitit-core/java-src/reitit/Trie.java
Normal file
295
modules/reitit-core/java-src/reitit/Trie.java
Normal file
|
|
@ -0,0 +1,295 @@
|
|||
package reitit;
|
||||
|
||||
// https://www.codeproject.com/Tips/1190293/Iteration-Over-Java-Collections-with-High-Performa
|
||||
|
||||
import clojure.lang.IPersistentMap;
|
||||
import clojure.lang.Keyword;
|
||||
import clojure.lang.PersistentArrayMap;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.URLDecoder;
|
||||
import java.util.*;
|
||||
|
||||
public class Trie {
|
||||
|
||||
private static String decode(String s, boolean hasPercent, boolean hasPlus) {
|
||||
try {
|
||||
if (hasPercent) {
|
||||
return URLDecoder.decode(hasPlus ? s.replace("+", "%2B") : s, "UTF-8");
|
||||
}
|
||||
} catch (UnsupportedEncodingException ignored) {
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
private static String decode(char[] chars, int begin, int end) {
|
||||
boolean hasPercent = false;
|
||||
boolean hasPlus = false;
|
||||
for (int j = begin; j < end; j++) {
|
||||
switch (chars[j]) {
|
||||
case '%':
|
||||
hasPercent = true;
|
||||
break;
|
||||
case '+':
|
||||
hasPlus = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return decode(new String(chars, begin, end - begin), hasPercent, hasPlus);
|
||||
}
|
||||
|
||||
public static class Match {
|
||||
public IPersistentMap params;
|
||||
public Object data;
|
||||
|
||||
public Match(IPersistentMap params, Object data) {
|
||||
this.params = params;
|
||||
this.data = 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();
|
||||
}
|
||||
}
|
||||
|
||||
public interface Matcher {
|
||||
Match match(int i, int max, char[] path);
|
||||
|
||||
int depth();
|
||||
|
||||
int length();
|
||||
}
|
||||
|
||||
public static StaticMatcher staticMatcher(String path, Matcher child) {
|
||||
return new StaticMatcher(path, child);
|
||||
}
|
||||
|
||||
static class StaticMatcher implements Matcher {
|
||||
private final Matcher child;
|
||||
private final char[] path;
|
||||
private final int size;
|
||||
|
||||
StaticMatcher(String path, Matcher child) {
|
||||
this.path = path.toCharArray();
|
||||
this.size = path.length();
|
||||
this.child = child;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Match match(int i, int max, char[] path) {
|
||||
if (max < i + size) {
|
||||
return null;
|
||||
}
|
||||
for (int j = 0; j < size; j++) {
|
||||
if (path[j + i] != this.path[j]) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return child.match(i + size, max, path);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int depth() {
|
||||
return child.depth() + 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int length() {
|
||||
return path.length;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "[\"" + new String(path) + "\" " + child + "]";
|
||||
}
|
||||
}
|
||||
|
||||
public static DataMatcher dataMatcher(IPersistentMap params, Object data) {
|
||||
return new DataMatcher(params, data);
|
||||
}
|
||||
|
||||
static final class DataMatcher implements Matcher {
|
||||
private final Match match;
|
||||
|
||||
DataMatcher(IPersistentMap params, Object data) {
|
||||
this.match = new Match(params, data);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Match match(int i, int max, char[] path) {
|
||||
if (i == max) {
|
||||
return match;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int depth() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int length() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return (match.data != null ? match.data.toString() : "nil");
|
||||
}
|
||||
}
|
||||
|
||||
public static WildMatcher wildMatcher(Keyword parameter, char end, Matcher child) {
|
||||
return new WildMatcher(parameter, end, child);
|
||||
}
|
||||
|
||||
static final class WildMatcher implements Matcher {
|
||||
private final Keyword key;
|
||||
private final char end;
|
||||
private final Matcher child;
|
||||
|
||||
WildMatcher(Keyword key, char end, Matcher child) {
|
||||
this.key = key;
|
||||
this.end = end;
|
||||
this.child = child;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Match match(int i, int max, char[] path) {
|
||||
if (i < max && path[i] != end) {
|
||||
int stop = max;
|
||||
for (int j = i; j < max; j++) {
|
||||
final char c = path[j];
|
||||
if (c == end) {
|
||||
stop = j;
|
||||
break;
|
||||
}
|
||||
}
|
||||
final Match m = child.match(stop, max, path);
|
||||
if (m != null) {
|
||||
m.params = m.params.assoc(key, decode(path, i, stop));
|
||||
}
|
||||
return m;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int depth() {
|
||||
return child.depth() + 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int length() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "[" + key + " " + child + "]";
|
||||
}
|
||||
}
|
||||
|
||||
public static CatchAllMatcher catchAllMatcher(Keyword parameter, IPersistentMap params, Object data) {
|
||||
return new CatchAllMatcher(parameter, params, data);
|
||||
}
|
||||
|
||||
static final class CatchAllMatcher implements Matcher {
|
||||
private final Keyword parameter;
|
||||
private final IPersistentMap params;
|
||||
private final Object data;
|
||||
|
||||
CatchAllMatcher(Keyword parameter, IPersistentMap params, Object data) {
|
||||
this.parameter = parameter;
|
||||
this.params = params;
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Match match(int i, int max, char[] path) {
|
||||
if (i < max) {
|
||||
return new Match(params.assoc(parameter, decode(path, i, max)), data);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int depth() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int length() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "[" + parameter + " " + new DataMatcher(null, data) + "]";
|
||||
}
|
||||
}
|
||||
|
||||
public static LinearMatcher linearMatcher(List<Matcher> childs) {
|
||||
return new LinearMatcher(childs);
|
||||
}
|
||||
|
||||
static final class LinearMatcher implements Matcher {
|
||||
|
||||
private final Matcher[] childs;
|
||||
private final int size;
|
||||
|
||||
LinearMatcher(List<Matcher> childs) {
|
||||
this.childs = childs.toArray(new Matcher[0]);
|
||||
Arrays.sort(this.childs, Comparator.comparing(Matcher::depth).thenComparing(Matcher::length).reversed());
|
||||
this.size = childs.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Match match(int i, int max, char[] path) {
|
||||
for (int j = 0; j < size; j++) {
|
||||
final Match m = childs[j].match(i, max, path);
|
||||
if (m != null) {
|
||||
return m;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int depth() {
|
||||
return Arrays.stream(childs).mapToInt(Matcher::depth).max().orElseThrow(NoSuchElementException::new) + 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int length() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return Arrays.toString(childs);
|
||||
}
|
||||
}
|
||||
|
||||
public static Object lookup(Matcher matcher, String path) {
|
||||
return matcher.match(0, path.length(), path.toCharArray());
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
Matcher matcher =
|
||||
linearMatcher(
|
||||
Arrays.asList(
|
||||
staticMatcher("/auth/",
|
||||
linearMatcher(
|
||||
Arrays.asList(
|
||||
staticMatcher("login", dataMatcher(null, 1)),
|
||||
staticMatcher("recovery", dataMatcher(null, 2)))))));
|
||||
System.err.println(matcher);
|
||||
System.out.println(lookup(matcher, "/auth/login"));
|
||||
System.out.println(lookup(matcher, "/auth/recovery"));
|
||||
}
|
||||
}
|
||||
|
|
@ -12,7 +12,7 @@
|
|||
"Pluggable coercion protocol"
|
||||
(-get-name [this] "Keyword name for the coercion")
|
||||
(-get-options [this] "Coercion options")
|
||||
(-get-apidocs [this spesification data] "Returns api documentation")
|
||||
(-get-apidocs [this specification data] "Returns api documentation")
|
||||
(-compile-model [this model name] "Compiles a model")
|
||||
(-open-model [this model] "Returns a new model which allows extra keys in maps")
|
||||
(-encode-error [this error] "Converts error in to a serializable format")
|
||||
|
|
@ -132,27 +132,18 @@
|
|||
[status (response-coercer coercion body opts)])
|
||||
(into {})))
|
||||
|
||||
(defn- coercers-not-compiled! [match]
|
||||
(throw
|
||||
(ex-info
|
||||
(str
|
||||
"Match didn't have a compiled coercion attached.\n"
|
||||
"Maybe you should have defined a router option:\n"
|
||||
"{:compile reitit.coercion/compile-request-coercers}\n")
|
||||
{:match match})))
|
||||
|
||||
;;
|
||||
;; api-docs
|
||||
;;
|
||||
|
||||
(defn get-apidocs [this spesification data]
|
||||
(defn get-apidocs [this specification data]
|
||||
(let [swagger-parameter {:query :query
|
||||
:body :body
|
||||
:form :formData
|
||||
:header :header
|
||||
:path :path
|
||||
:multipart :formData}]
|
||||
(case spesification
|
||||
(case specification
|
||||
:swagger (->> (update
|
||||
data
|
||||
:parameters
|
||||
|
|
@ -161,7 +152,7 @@
|
|||
(map (fn [[k v]] [(swagger-parameter k) v]))
|
||||
(filter first)
|
||||
(into {}))))
|
||||
(-get-apidocs this spesification)))))
|
||||
(-get-apidocs this specification)))))
|
||||
|
||||
;;
|
||||
;; integration
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
(ns reitit.core
|
||||
(:require [meta-merge.core :refer [meta-merge]]
|
||||
[clojure.string :as str]
|
||||
[reitit.segment :as segment]
|
||||
[reitit.impl :as impl #?@(:cljs [:refer [Route]])])
|
||||
#?(:clj
|
||||
(:import (reitit.impl Route))))
|
||||
(:require [clojure.string :as str]
|
||||
[reitit.impl :as impl]
|
||||
[reitit.exception :as exception]
|
||||
[reitit.trie :as trie]))
|
||||
|
||||
;;
|
||||
;; Expand
|
||||
;;
|
||||
|
||||
(defprotocol Expand
|
||||
(expand [this opts]))
|
||||
|
|
@ -30,56 +32,9 @@
|
|||
nil
|
||||
(expand [_ _]))
|
||||
|
||||
(defn walk [raw-routes {:keys [path data routes expand]
|
||||
:or {data [], routes [], expand expand}
|
||||
:as opts}]
|
||||
(letfn
|
||||
[(walk-many [p m r]
|
||||
(reduce #(into %1 (walk-one p m %2)) [] r))
|
||||
(walk-one [pacc macc routes]
|
||||
(if (vector? (first routes))
|
||||
(walk-many pacc macc routes)
|
||||
(when (string? (first routes))
|
||||
(let [[path & [maybe-arg :as args]] routes
|
||||
[data childs] (if (or (vector? maybe-arg)
|
||||
(and (sequential? maybe-arg)
|
||||
(sequential? (first maybe-arg)))
|
||||
(nil? maybe-arg))
|
||||
[{} args]
|
||||
[maybe-arg (rest args)])
|
||||
macc (into macc (expand data opts))
|
||||
child-routes (walk-many (str pacc path) macc (keep identity childs))]
|
||||
(if (seq childs) (seq child-routes) [[(str pacc path) macc]])))))]
|
||||
(walk-one path (mapv identity data) raw-routes)))
|
||||
|
||||
(defn map-data [f routes]
|
||||
(mapv #(update % 1 f) routes))
|
||||
|
||||
(defn merge-data [x]
|
||||
(reduce
|
||||
(fn [acc [k v]]
|
||||
(meta-merge acc {k v}))
|
||||
{} x))
|
||||
|
||||
(defn resolve-routes [raw-routes {:keys [coerce] :as opts}]
|
||||
(cond->> (->> (walk raw-routes opts) (map-data merge-data))
|
||||
coerce (into [] (keep #(coerce % opts)))))
|
||||
|
||||
(defn path-conflicting-routes [routes]
|
||||
(-> (into {}
|
||||
(comp (map-indexed (fn [index route]
|
||||
[route (into #{}
|
||||
(filter #(impl/conflicting-routes? route %))
|
||||
(subvec routes (inc index)))]))
|
||||
(filter (comp seq second)))
|
||||
routes)
|
||||
(not-empty)))
|
||||
|
||||
(defn conflicting-paths [conflicts]
|
||||
(->> (for [[p pc] conflicts]
|
||||
(conj (map first pc) (first p)))
|
||||
(apply concat)
|
||||
(set)))
|
||||
;;
|
||||
;; Conflicts
|
||||
;;
|
||||
|
||||
(defn path-conflicts-str [conflicts]
|
||||
(apply str "Router contains conflicting route paths:\n\n"
|
||||
|
|
@ -88,15 +43,6 @@
|
|||
(str " " path "\n-> " (str/join "\n-> " (mapv first vals)) "\n\n"))
|
||||
conflicts)))
|
||||
|
||||
(defn name-conflicting-routes [routes]
|
||||
(some->> routes
|
||||
(group-by (comp :name second))
|
||||
(remove (comp nil? first))
|
||||
(filter (comp pos? count butlast second))
|
||||
(seq)
|
||||
(map (fn [[k v]] [k (set v)]))
|
||||
(into {})))
|
||||
|
||||
(defn name-conflicts-str [conflicts]
|
||||
(apply str "Router contains conflicting route names:\n\n"
|
||||
(mapv
|
||||
|
|
@ -105,28 +51,11 @@
|
|||
conflicts)))
|
||||
|
||||
(defn throw-on-conflicts! [f conflicts]
|
||||
(throw
|
||||
(ex-info
|
||||
(f conflicts)
|
||||
{:conflicts conflicts})))
|
||||
(exception/fail! (f conflicts) {:conflicts conflicts}))
|
||||
|
||||
(defn- name-lookup [[_ {:keys [name]}] _]
|
||||
(if name #{name}))
|
||||
|
||||
(defn- find-names [routes _]
|
||||
(into [] (keep #(-> % second :name)) routes))
|
||||
|
||||
(defn- compile-route [[p m :as route] {:keys [compile] :as opts}]
|
||||
[p m (if compile (compile route opts))])
|
||||
|
||||
(defn- compile-routes [routes opts]
|
||||
(into [] (keep #(compile-route % opts) routes)))
|
||||
|
||||
(defn- uncompile-routes [routes]
|
||||
(mapv (comp vec (partial take 2)) routes))
|
||||
|
||||
(defn route-info [route]
|
||||
(impl/create route))
|
||||
;;
|
||||
;; Router
|
||||
;;
|
||||
|
||||
(defprotocol Router
|
||||
(router-name [this])
|
||||
|
|
@ -162,26 +91,36 @@
|
|||
([match query-params]
|
||||
(some-> match :path (cond-> query-params (str "?" (impl/query-string query-params))))))
|
||||
|
||||
;;
|
||||
;; Different routers
|
||||
;;
|
||||
|
||||
(defn linear-router
|
||||
"Creates a linear-router from resolved routes and optional
|
||||
expanded options. See [[router]] for available options."
|
||||
expanded options. See [[router]] for available options, plus the following:
|
||||
|
||||
| key | description |
|
||||
| -----------------------------|-------------|
|
||||
| `:reitit.core/trie-compiler` | Optional trie-compiler."
|
||||
([compiled-routes]
|
||||
(linear-router compiled-routes {}))
|
||||
([compiled-routes opts]
|
||||
(let [names (find-names compiled-routes opts)
|
||||
(let [compiler (::trie-compiler opts (trie/compiler))
|
||||
names (impl/find-names compiled-routes opts)
|
||||
[pl nl] (reduce
|
||||
(fn [[pl nl] [p {:keys [name] :as data} result]]
|
||||
(let [{:keys [path-params] :as route} (impl/create [p data result])
|
||||
(let [{:keys [path-params] :as route} (impl/parse p)
|
||||
f #(if-let [path (impl/path-for route %)]
|
||||
(->Match p data result (impl/url-decode-coll %) path)
|
||||
(->PartialMatch p data result % path-params))]
|
||||
[(conj pl (-> (segment/insert nil p (->Match p data result nil nil)) (segment/compile)))
|
||||
(->PartialMatch p data result (impl/url-decode-coll %) path-params))]
|
||||
[(conj pl (-> (trie/insert nil p (->Match p data result nil nil)) (trie/compile)))
|
||||
(if name (assoc nl name f) nl)]))
|
||||
[[] {}]
|
||||
compiled-routes)
|
||||
lookup (impl/fast-map nl)
|
||||
scanner (segment/scanner pl)
|
||||
routes (uncompile-routes compiled-routes)]
|
||||
matcher (trie/linear-matcher compiler pl)
|
||||
match-by-path (trie/path-matcher matcher compiler)
|
||||
routes (impl/uncompile-routes compiled-routes)]
|
||||
^{:type ::router}
|
||||
(reify
|
||||
Router
|
||||
|
|
@ -196,9 +135,9 @@
|
|||
(route-names [_]
|
||||
names)
|
||||
(match-by-path [_ path]
|
||||
(if-let [match (segment/lookup scanner path)]
|
||||
(if-let [match (match-by-path path)]
|
||||
(-> (:data match)
|
||||
(assoc :path-params (:path-params match))
|
||||
(assoc :path-params (:params match))
|
||||
(assoc :path path))))
|
||||
(match-by-name [_ name]
|
||||
(if-let [match (impl/fast-get lookup name)]
|
||||
|
|
@ -214,12 +153,11 @@
|
|||
(lookup-router compiled-routes {}))
|
||||
([compiled-routes opts]
|
||||
(when-let [wilds (seq (filter impl/wild-route? compiled-routes))]
|
||||
(throw
|
||||
(ex-info
|
||||
(str "can't create :lookup-router with wildcard routes: " wilds)
|
||||
{:wilds wilds
|
||||
:routes compiled-routes})))
|
||||
(let [names (find-names compiled-routes opts)
|
||||
(exception/fail!
|
||||
(str "can't create :lookup-router with wildcard routes: " wilds)
|
||||
{:wilds wilds
|
||||
:routes compiled-routes}))
|
||||
(let [names (impl/find-names compiled-routes opts)
|
||||
[pl nl] (reduce
|
||||
(fn [[pl nl] [p {:keys [name] :as data} result]]
|
||||
[(assoc pl p (->Match p data result {} p))
|
||||
|
|
@ -230,7 +168,7 @@
|
|||
compiled-routes)
|
||||
data (impl/fast-map pl)
|
||||
lookup (impl/fast-map nl)
|
||||
routes (uncompile-routes compiled-routes)]
|
||||
routes (impl/uncompile-routes compiled-routes)]
|
||||
^{:type ::router}
|
||||
(reify Router
|
||||
(router-name [_]
|
||||
|
|
@ -252,31 +190,37 @@
|
|||
(if-let [match (impl/fast-get lookup name)]
|
||||
(match (impl/path-params path-params))))))))
|
||||
|
||||
(defn segment-router
|
||||
"Creates a special prefix-tree style segment router from resolved routes and optional
|
||||
expanded options. See [[router]] for available options."
|
||||
(defn trie-router
|
||||
"Creates a special prefix-tree router from resolved routes and optional
|
||||
expanded options. See [[router]] for available options, plus the following:
|
||||
|
||||
| key | description |
|
||||
| -----------------------------|-------------|
|
||||
| `:reitit.core/trie-compiler` | Optional trie-compiler."
|
||||
([compiled-routes]
|
||||
(segment-router compiled-routes {}))
|
||||
(trie-router compiled-routes {}))
|
||||
([compiled-routes opts]
|
||||
(let [names (find-names compiled-routes opts)
|
||||
(let [compiler (::trie-compiler opts (trie/compiler))
|
||||
names (impl/find-names compiled-routes opts)
|
||||
[pl nl] (reduce
|
||||
(fn [[pl nl] [p {:keys [name] :as data} result]]
|
||||
(let [{:keys [path-params] :as route} (impl/create [p data result])
|
||||
(let [{:keys [path-params] :as route} (impl/parse p)
|
||||
f #(if-let [path (impl/path-for route %)]
|
||||
(->Match p data result (impl/url-decode-coll %) path)
|
||||
(->PartialMatch p data result % path-params))]
|
||||
[(segment/insert pl p (->Match p data result nil nil))
|
||||
(->PartialMatch p data result (impl/url-decode-coll %) path-params))]
|
||||
[(trie/insert pl p (->Match p data result nil nil))
|
||||
(if name (assoc nl name f) nl)]))
|
||||
[nil {}]
|
||||
compiled-routes)
|
||||
pl (segment/compile pl)
|
||||
matcher (trie/compile pl compiler)
|
||||
match-by-path (trie/path-matcher matcher compiler)
|
||||
lookup (impl/fast-map nl)
|
||||
routes (uncompile-routes compiled-routes)]
|
||||
routes (impl/uncompile-routes compiled-routes)]
|
||||
^{:type ::router}
|
||||
(reify
|
||||
Router
|
||||
(router-name [_]
|
||||
:segment-router)
|
||||
:trie-router)
|
||||
(routes [_]
|
||||
routes)
|
||||
(compiled-routes [_]
|
||||
|
|
@ -286,9 +230,9 @@
|
|||
(route-names [_]
|
||||
names)
|
||||
(match-by-path [_ path]
|
||||
(if-let [match (segment/lookup pl path)]
|
||||
(if-let [match (match-by-path path)]
|
||||
(-> (:data match)
|
||||
(assoc :path-params (:path-params match))
|
||||
(assoc :path-params (:params match))
|
||||
(assoc :path path))))
|
||||
(match-by-name [_ name]
|
||||
(if-let [match (impl/fast-get lookup name)]
|
||||
|
|
@ -304,15 +248,14 @@
|
|||
(single-static-path-router compiled-routes {}))
|
||||
([compiled-routes opts]
|
||||
(when (or (not= (count compiled-routes) 1) (some impl/wild-route? compiled-routes))
|
||||
(throw
|
||||
(ex-info
|
||||
(str ":single-static-path-router requires exactly 1 static route: " compiled-routes)
|
||||
{:routes compiled-routes})))
|
||||
(let [[n :as names] (find-names compiled-routes opts)
|
||||
(exception/fail!
|
||||
(str ":single-static-path-router requires exactly 1 static route: " compiled-routes)
|
||||
{:routes compiled-routes}))
|
||||
(let [[n :as names] (impl/find-names compiled-routes opts)
|
||||
[[p data result]] compiled-routes
|
||||
p #?(:clj (.intern ^String p) :cljs p)
|
||||
match (->Match p data result {} p)
|
||||
routes (uncompile-routes compiled-routes)]
|
||||
routes (impl/uncompile-routes compiled-routes)]
|
||||
^{:type ::router}
|
||||
(reify Router
|
||||
(router-name [_]
|
||||
|
|
@ -345,10 +288,10 @@
|
|||
([compiled-routes opts]
|
||||
(let [{wild true, lookup false} (group-by impl/wild-route? compiled-routes)
|
||||
->static-router (if (= 1 (count lookup)) single-static-path-router lookup-router)
|
||||
wildcard-router (segment-router wild opts)
|
||||
wildcard-router (trie-router wild opts)
|
||||
static-router (->static-router lookup opts)
|
||||
names (find-names compiled-routes opts)
|
||||
routes (uncompile-routes compiled-routes)]
|
||||
names (impl/find-names compiled-routes opts)
|
||||
routes (impl/uncompile-routes compiled-routes)]
|
||||
^{:type ::router}
|
||||
(reify Router
|
||||
(router-name [_]
|
||||
|
|
@ -378,13 +321,13 @@
|
|||
([compiled-routes]
|
||||
(quarantine-router compiled-routes {}))
|
||||
([compiled-routes opts]
|
||||
(let [conflicting-paths (-> compiled-routes path-conflicting-routes conflicting-paths)
|
||||
(let [conflicting-paths (-> compiled-routes impl/path-conflicting-routes impl/conflicting-paths)
|
||||
conflicting? #(contains? conflicting-paths (first %))
|
||||
{conflicting true, non-conflicting false} (group-by conflicting? compiled-routes)
|
||||
linear-router (linear-router conflicting opts)
|
||||
mixed-router (mixed-router non-conflicting opts)
|
||||
names (find-names compiled-routes opts)
|
||||
routes (uncompile-routes compiled-routes)]
|
||||
names (impl/find-names compiled-routes opts)
|
||||
routes (impl/uncompile-routes compiled-routes)]
|
||||
^{:type ::router}
|
||||
(reify Router
|
||||
(router-name [_]
|
||||
|
|
@ -407,12 +350,16 @@
|
|||
(or (match-by-name mixed-router name path-params)
|
||||
(match-by-name linear-router name path-params)))))))
|
||||
|
||||
;;
|
||||
;; Creating Routers
|
||||
;;
|
||||
|
||||
(defn ^:no-doc default-router-options []
|
||||
{:lookup name-lookup
|
||||
{:lookup (fn lookup [[_ {:keys [name]}] _] (if name #{name}))
|
||||
:expand expand
|
||||
:coerce (fn [route _] route)
|
||||
:compile (fn [[_ {:keys [handler]}] _] handler)
|
||||
:conflicts (partial throw-on-conflicts! path-conflicts-str)})
|
||||
:coerce (fn coerce [route _] route)
|
||||
:compile (fn compile [[_ {:keys [handler]}] _] handler)
|
||||
:conflicts (fn throw! [conflicts] (throw-on-conflicts! path-conflicts-str conflicts))})
|
||||
|
||||
(defn router
|
||||
"Create a [[Router]] from raw route data and optionally an options map.
|
||||
|
|
@ -435,10 +382,10 @@
|
|||
(router raw-routes {}))
|
||||
([raw-routes opts]
|
||||
(let [{:keys [router] :as opts} (merge (default-router-options) opts)
|
||||
routes (resolve-routes raw-routes opts)
|
||||
path-conflicting (path-conflicting-routes routes)
|
||||
name-conflicting (name-conflicting-routes routes)
|
||||
compiled-routes (compile-routes routes opts)
|
||||
routes (impl/resolve-routes raw-routes opts)
|
||||
path-conflicting (impl/path-conflicting-routes routes)
|
||||
name-conflicting (impl/name-conflicting-routes routes)
|
||||
compiled-routes (impl/compile-routes routes opts)
|
||||
wilds? (boolean (some impl/wild-route? compiled-routes))
|
||||
all-wilds? (every? impl/wild-route? compiled-routes)
|
||||
router (cond
|
||||
|
|
@ -446,16 +393,16 @@
|
|||
(and (= 1 (count compiled-routes)) (not wilds?)) single-static-path-router
|
||||
path-conflicting quarantine-router
|
||||
(not wilds?) lookup-router
|
||||
all-wilds? segment-router
|
||||
all-wilds? trie-router
|
||||
:else mixed-router)]
|
||||
|
||||
(when-let [validate (:validate opts)]
|
||||
(validate compiled-routes opts))
|
||||
|
||||
(when-let [conflicts (:conflicts opts)]
|
||||
(when path-conflicting (conflicts path-conflicting)))
|
||||
|
||||
(when name-conflicting
|
||||
(throw-on-conflicts! name-conflicts-str name-conflicting))
|
||||
|
||||
(when-let [validate (:validate opts)]
|
||||
(validate compiled-routes opts))
|
||||
|
||||
(router compiled-routes opts))))
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
(ns reitit.dependency
|
||||
"Dependency resolution for middleware/interceptors.")
|
||||
"Dependency resolution for middleware/interceptors."
|
||||
(:require [reitit.exception :as exception]))
|
||||
|
||||
(defn- providers
|
||||
"Map from provision key to provider. `get-provides` should return the provision keys of a dependent."
|
||||
|
|
@ -8,8 +9,9 @@
|
|||
(into acc
|
||||
(map (fn [provide]
|
||||
(when (contains? acc provide)
|
||||
(throw (ex-info (str "multiple providers for: " provide)
|
||||
{::multiple-providers provide})))
|
||||
(exception/fail!
|
||||
(str "multiple providers for: " provide)
|
||||
{::multiple-providers provide}))
|
||||
[provide dependent]))
|
||||
(get-provides dependent)))
|
||||
{} nodes))
|
||||
|
|
@ -19,8 +21,9 @@
|
|||
[providers k]
|
||||
(if (contains? providers k)
|
||||
(get providers k)
|
||||
(throw (ex-info (str "provider missing for dependency: " k)
|
||||
{::missing-provider k}))))
|
||||
(exception/fail!
|
||||
(str "provider missing for dependency: " k)
|
||||
{::missing-provider k})))
|
||||
|
||||
(defn post-order
|
||||
"Put `nodes` in post-order. Can also be described as a reverse topological sort.
|
||||
|
|
@ -37,8 +40,7 @@
|
|||
(assoc colors node :grey))]
|
||||
[(conj nodes* node)
|
||||
(assoc colors node :black)])
|
||||
:grey (throw (ex-info "circular dependency"
|
||||
{:cycle (drop-while #(not= % node) (conj path node))}))
|
||||
:grey (exception/fail! "circular dependency" {:cycle (drop-while #(not= % node) (conj path node))})
|
||||
:black [() colors]))
|
||||
|
||||
(toposort-seq [nodes path colors]
|
||||
|
|
|
|||
7
modules/reitit-core/src/reitit/exception.cljc
Normal file
7
modules/reitit-core/src/reitit/exception.cljc
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
(ns reitit.exception)
|
||||
|
||||
(defn fail!
|
||||
([message]
|
||||
(throw (ex-info message {::type :exeption})))
|
||||
([message data]
|
||||
(throw (ex-info message (merge {::type ::exeption} data)))))
|
||||
|
|
@ -1,12 +1,27 @@
|
|||
(ns ^:no-doc reitit.impl
|
||||
#?(:cljs (:require-macros [reitit.impl]))
|
||||
(:require [clojure.string :as str]
|
||||
[clojure.set :as set])
|
||||
[clojure.set :as set]
|
||||
[meta-merge.core :as mm]
|
||||
[reitit.trie :as trie]
|
||||
[reitit.exception :as exception])
|
||||
#?(:clj
|
||||
(:import (java.util.regex Pattern)
|
||||
(java.util HashMap Map)
|
||||
(java.net URLEncoder URLDecoder)
|
||||
(reitit SegmentTrie))))
|
||||
(java.net URLEncoder URLDecoder))))
|
||||
|
||||
(defrecord Route [path path-parts path-params])
|
||||
|
||||
(defn parse [path]
|
||||
(let [path #?(:clj (.intern ^String (trie/normalize path)) :cljs (trie/normalize path))
|
||||
path-parts (trie/split-path path)
|
||||
path-params (->> path-parts (remove string?) (map :value) set)]
|
||||
(map->Route {:path-params path-params
|
||||
:path-parts path-parts
|
||||
:path path})))
|
||||
|
||||
(defn wild-route? [[path]]
|
||||
(-> path parse :path-params seq boolean))
|
||||
|
||||
(defn maybe-map-values
|
||||
"Applies a function to every value of a map, updates the value if not nil.
|
||||
|
|
@ -20,117 +35,101 @@
|
|||
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.
|
||||
(defn walk [raw-routes {:keys [path data routes expand]
|
||||
:or {data [], routes []}
|
||||
:as opts}]
|
||||
(letfn
|
||||
[(walk-many [p m r]
|
||||
(reduce #(into %1 (walk-one p m %2)) [] r))
|
||||
(walk-one [pacc macc routes]
|
||||
(if (vector? (first routes))
|
||||
(walk-many pacc macc routes)
|
||||
(when (string? (first routes))
|
||||
(let [[path & [maybe-arg :as args]] routes
|
||||
[data childs] (if (or (vector? maybe-arg)
|
||||
(and (sequential? maybe-arg)
|
||||
(sequential? (first maybe-arg)))
|
||||
(nil? maybe-arg))
|
||||
[{} args]
|
||||
[maybe-arg (rest args)])
|
||||
macc (into macc (expand data opts))
|
||||
child-routes (walk-many (str pacc path) macc (keep identity childs))]
|
||||
(if (seq childs) (seq child-routes) [[(str pacc path) macc]])))))]
|
||||
(walk-one path (mapv identity data) raw-routes)))
|
||||
|
||||
(segments \"/a/b/c\") ; => (\"a\" \"b\" \"c\")
|
||||
(segments \"/a/) ; => (\"a\" \"\")"
|
||||
[path]
|
||||
#?(:clj (SegmentTrie/split ^String path)
|
||||
:cljs (rest (.split path #"/" 666))))
|
||||
(defn map-data [f routes]
|
||||
(mapv #(update % 1 f) routes))
|
||||
|
||||
;;
|
||||
;; https://github.com/pedestal/pedestal/blob/master/route/src/io/pedestal/http/route/prefix_tree.clj
|
||||
;;
|
||||
(defn merge-data [x]
|
||||
(reduce
|
||||
(fn [acc [k v]]
|
||||
(mm/meta-merge acc {k v}))
|
||||
{} x))
|
||||
|
||||
(defn wild? [s]
|
||||
(contains? #{\: \*} (first (str s))))
|
||||
(defn resolve-routes [raw-routes {:keys [coerce] :as opts}]
|
||||
(cond->> (->> (walk raw-routes opts) (map-data merge-data))
|
||||
coerce (into [] (keep #(coerce % opts)))))
|
||||
|
||||
(defn catch-all? [s]
|
||||
(= \* (first (str s))))
|
||||
(defn conflicting-routes? [route1 route2]
|
||||
(trie/conflicting-paths? (first route1) (first route2)))
|
||||
|
||||
(defn wild-param [s]
|
||||
(let [ss (str s)]
|
||||
(if (= \: (first ss))
|
||||
(keyword (subs ss 1)))))
|
||||
(defn path-conflicting-routes [routes]
|
||||
(-> (into {}
|
||||
(comp (map-indexed (fn [index route]
|
||||
[route (into #{}
|
||||
(filter (partial conflicting-routes? route))
|
||||
(subvec routes (inc index)))]))
|
||||
(filter (comp seq second)))
|
||||
routes)
|
||||
(not-empty)))
|
||||
|
||||
(defn catch-all-param [s]
|
||||
(let [ss (str s)]
|
||||
(if (= \* (first ss))
|
||||
(keyword (subs ss 1)))))
|
||||
(defn conflicting-paths [conflicts]
|
||||
(->> (for [[p pc] conflicts]
|
||||
(conj (map first pc) (first p)))
|
||||
(apply concat)
|
||||
(set)))
|
||||
|
||||
(defn wild-or-catch-all-param? [x]
|
||||
(boolean (or (wild-param x) (catch-all-param x))))
|
||||
(defn name-conflicting-routes [routes]
|
||||
(some->> routes
|
||||
(group-by (comp :name second))
|
||||
(remove (comp nil? first))
|
||||
(filter (comp pos? count butlast second))
|
||||
(seq)
|
||||
(map (fn [[k v]] [k (set v)]))
|
||||
(into {})))
|
||||
|
||||
(defn contains-wilds? [path]
|
||||
(boolean (some wild-or-catch-all-param? (segments path))))
|
||||
(defn find-names [routes _]
|
||||
(into [] (keep #(-> % second :name)) routes))
|
||||
|
||||
;;
|
||||
;; https://github.com/pedestal/pedestal/blob/master/route/src/io/pedestal/http/route/path.clj
|
||||
;;
|
||||
(defn compile-route [[p m :as route] {:keys [compile] :as opts}]
|
||||
[p m (if compile (compile route opts))])
|
||||
|
||||
(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))))
|
||||
#"^\*(.*)$" :>> (fn [[_ token]]
|
||||
(let [key (keyword token)]
|
||||
(-> out
|
||||
(update-in [:path-parts] conj key)
|
||||
(update-in [:path-params] conj key))))
|
||||
(update-in out [:path-parts] conj string)))
|
||||
(defn compile-routes [routes opts]
|
||||
(into [] (keep #(compile-route % opts) routes)))
|
||||
|
||||
(defn- parse-path
|
||||
([pattern] (parse-path {:path-parts [] :path-params #{}} 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})))))
|
||||
|
||||
;;
|
||||
;; Routing (c) Metosin
|
||||
;;
|
||||
|
||||
(defrecord Route [path path-parts path-params data result])
|
||||
|
||||
(defn create [[path data result]]
|
||||
(let [path #?(:clj (.intern ^String path) :cljs path)
|
||||
{:keys [path-parts path-params]} (parse-path path)]
|
||||
(map->Route
|
||||
{:path-params path-params
|
||||
:path-parts path-parts
|
||||
:path path
|
||||
:result result
|
||||
:data data})))
|
||||
|
||||
(defn wild-route? [[path]]
|
||||
(contains-wilds? path))
|
||||
|
||||
(defn conflicting-routes? [[p1] [p2]]
|
||||
(loop [[s1 & ss1] (segments p1)
|
||||
[s2 & ss2] (segments p2)]
|
||||
(cond
|
||||
(= s1 s2 nil) true
|
||||
(or (nil? s1) (nil? s2)) false
|
||||
(or (catch-all? s1) (catch-all? s2)) true
|
||||
(or (wild? s1) (wild? s2)) (recur ss1 ss2)
|
||||
(not= s1 s2) false
|
||||
:else (recur ss1 ss2))))
|
||||
(defn uncompile-routes [routes]
|
||||
(mapv (comp vec (partial take 2)) routes))
|
||||
|
||||
(defn path-for [^Route route path-params]
|
||||
(if-let [required (:path-params route)]
|
||||
(if (every? #(contains? path-params %) required)
|
||||
(->> (:path-parts route)
|
||||
(map #(get (or path-params {}) % %))
|
||||
(str/join \/)
|
||||
(str "/")))
|
||||
(if (:path-params route)
|
||||
(if-let [parts (reduce
|
||||
(fn [acc part]
|
||||
(if (string? part)
|
||||
(conj acc part)
|
||||
(if-let [p (get path-params (:value part))]
|
||||
(conj acc p)
|
||||
(reduced nil))))
|
||||
[] (:path-parts route))]
|
||||
(apply str parts))
|
||||
(:path route)))
|
||||
|
||||
(defn throw-on-missing-path-params [template required path-params]
|
||||
(when-not (every? #(contains? path-params %) required)
|
||||
(let [defined (-> path-params keys set)
|
||||
missing (set/difference required defined)]
|
||||
(throw
|
||||
(ex-info
|
||||
(str "missing path-params for route " template " -> " missing)
|
||||
{:path-params path-params, :required required})))))
|
||||
(exception/fail!
|
||||
(str "missing path-params for route " template " -> " missing)
|
||||
{:path-params path-params, :required required}))))
|
||||
|
||||
(defn fast-assoc
|
||||
#?@(:clj [[^clojure.lang.Associative a k v] (.assoc a k v)]
|
||||
|
|
|
|||
|
|
@ -2,7 +2,8 @@
|
|||
(:require [meta-merge.core :refer [meta-merge]]
|
||||
[clojure.pprint :as pprint]
|
||||
[reitit.core :as r]
|
||||
[reitit.impl :as impl]))
|
||||
[reitit.impl :as impl]
|
||||
[reitit.exception :as exception]))
|
||||
|
||||
(defprotocol IntoInterceptor
|
||||
(into-interceptor [this data opts]))
|
||||
|
|
@ -52,10 +53,9 @@
|
|||
:cljs cljs.core.PersistentVector)
|
||||
(into-interceptor [[f & args :as form] data opts]
|
||||
(when (and (seq args) (not (fn? f)))
|
||||
(throw
|
||||
(ex-info
|
||||
(str "Invalid Interceptor form: " form "")
|
||||
{:form form})))
|
||||
(exception/fail!
|
||||
(str "Invalid Interceptor form: " form "")
|
||||
{:form form}))
|
||||
(into-interceptor (apply f args) data opts))
|
||||
|
||||
#?(:clj clojure.lang.Fn
|
||||
|
|
@ -85,10 +85,9 @@
|
|||
(let [compiled (::compiled opts 0)
|
||||
opts (assoc opts ::compiled (inc ^long compiled))]
|
||||
(when (>= ^long compiled ^long *max-compile-depth*)
|
||||
(throw
|
||||
(ex-info
|
||||
(str "Too deep Interceptor compilation - " compiled)
|
||||
{:this this, :data data, :opts opts})))
|
||||
(exception/fail!
|
||||
(str "Too deep Interceptor compilation - " compiled)
|
||||
{:this this, :data data, :opts opts}))
|
||||
(if-let [interceptor (into-interceptor (compile data opts) data opts)]
|
||||
(map->Interceptor
|
||||
(merge
|
||||
|
|
|
|||
|
|
@ -2,7 +2,8 @@
|
|||
(:require [meta-merge.core :refer [meta-merge]]
|
||||
[clojure.pprint :as pprint]
|
||||
[reitit.core :as r]
|
||||
[reitit.impl :as impl]))
|
||||
[reitit.impl :as impl]
|
||||
[reitit.exception :as exception]))
|
||||
|
||||
(defprotocol IntoMiddleware
|
||||
(into-middleware [this data opts]))
|
||||
|
|
@ -61,10 +62,9 @@
|
|||
(let [compiled (::compiled opts 0)
|
||||
opts (assoc opts ::compiled (inc ^long compiled))]
|
||||
(when (>= ^long compiled ^long *max-compile-depth*)
|
||||
(throw
|
||||
(ex-info
|
||||
(str "Too deep Middleware compilation - " compiled)
|
||||
{:this this, :data data, :opts opts})))
|
||||
(exception/fail!
|
||||
(str "Too deep Middleware compilation - " compiled)
|
||||
{:this this, :data data, :opts opts}))
|
||||
(if-let [middeware (into-middleware (compile data opts) data opts)]
|
||||
(map->Middleware
|
||||
(merge
|
||||
|
|
@ -76,11 +76,11 @@
|
|||
|
||||
(defn- ensure-handler! [path data scope]
|
||||
(when-not (:handler data)
|
||||
(throw (ex-info
|
||||
(str "path \"" path "\" doesn't have a :handler defined"
|
||||
(if scope (str " for " scope)))
|
||||
(merge {:path path, :data data}
|
||||
(if scope {:scope scope}))))))
|
||||
(exception/fail!
|
||||
(str "path \"" path "\" doesn't have a :handler defined"
|
||||
(if scope (str " for " scope)))
|
||||
(merge {:path path, :data data}
|
||||
(if scope {:scope scope})))))
|
||||
|
||||
(defn- expand-and-transform
|
||||
[middleware data {:keys [::transform] :or {transform identity} :as opts}]
|
||||
|
|
|
|||
|
|
@ -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,7 +1,8 @@
|
|||
(ns reitit.spec
|
||||
(:require [clojure.spec.alpha :as s]
|
||||
[clojure.spec.gen.alpha :as gen]
|
||||
[reitit.core :as reitit]))
|
||||
[reitit.core :as reitit]
|
||||
[reitit.exception :as exception]))
|
||||
|
||||
;;
|
||||
;; routes
|
||||
|
|
@ -119,10 +120,9 @@
|
|||
problems)))
|
||||
|
||||
(defn throw-on-problems! [problems explain]
|
||||
(throw
|
||||
(ex-info
|
||||
(problems-str problems explain)
|
||||
{:problems problems})))
|
||||
(exception/fail!
|
||||
(problems-str problems explain)
|
||||
{:problems problems}))
|
||||
|
||||
(defn validate-route-data [routes spec]
|
||||
(->> (for [[p d _] routes]
|
||||
|
|
|
|||
423
modules/reitit-core/src/reitit/trie.cljc
Normal file
423
modules/reitit-core/src/reitit/trie.cljc
Normal file
|
|
@ -0,0 +1,423 @@
|
|||
(ns reitit.trie
|
||||
(:refer-clojure :exclude [compile])
|
||||
(:require [clojure.string :as str]
|
||||
[reitit.exception :as ex])
|
||||
#?(:clj (:import [reitit Trie Trie$Match Trie$Matcher]
|
||||
(java.net URLDecoder))))
|
||||
|
||||
(defrecord Wild [value])
|
||||
(defrecord CatchAll [value])
|
||||
(defrecord Match [params data])
|
||||
(defrecord Node [children wilds catch-all params data])
|
||||
|
||||
(defn wild? [x] (instance? Wild x))
|
||||
(defn catch-all? [x] (instance? CatchAll x))
|
||||
|
||||
(defprotocol Matcher
|
||||
(match [this i max path])
|
||||
(view [this])
|
||||
(depth [this])
|
||||
(length [this]))
|
||||
|
||||
(defprotocol TrieCompiler
|
||||
(data-matcher [this params data])
|
||||
(static-matcher [this path matcher])
|
||||
(wild-matcher [this key end matcher])
|
||||
(catch-all-matcher [this key params data])
|
||||
(linear-matcher [this matchers])
|
||||
(-pretty [this matcher])
|
||||
(-path-matcher [this matcher]))
|
||||
|
||||
(defn- assoc-param [match k v]
|
||||
(let [params (:params match)]
|
||||
(assoc match :params (assoc params k v))))
|
||||
|
||||
;; https://stackoverflow.com/questions/8033655/find-longest-common-prefix
|
||||
(defn- common-prefix [s1 s2]
|
||||
(let [max (min (count s1) (count s2))]
|
||||
(loop [i 0]
|
||||
(cond
|
||||
;; full match
|
||||
(> i max)
|
||||
(subs s1 0 max)
|
||||
;; partial match
|
||||
(not= (get s1 i) (get s2 i))
|
||||
(if-not (zero? i) (subs s1 0 i))
|
||||
;; recur
|
||||
:else (recur (inc i))))))
|
||||
|
||||
(defn- -keyword [s]
|
||||
(if-let [i (str/index-of s "/")]
|
||||
(keyword (subs s 0 i) (subs s (inc i)))
|
||||
(keyword s)))
|
||||
|
||||
(defn split-path [s]
|
||||
(let [-static (fn [from to] (if-not (= from to) [(subs s from to)]))
|
||||
-wild (fn [from to] [(->Wild (-keyword (subs s (inc from) to)))])
|
||||
-catch-all (fn [from to] [(->CatchAll (keyword (subs s (inc from) to)))])]
|
||||
(loop [ss nil, from 0, to 0]
|
||||
(if (= to (count s))
|
||||
(concat ss (-static from to))
|
||||
(case (get s to)
|
||||
\{ (let [to' (or (str/index-of s "}" to) (ex/fail! (str "Unclosed brackets: " (pr-str s))))]
|
||||
(if (= \* (get s (inc to)))
|
||||
(recur (concat ss (-static from to) (-catch-all (inc to) to')) (inc to') (inc to'))
|
||||
(recur (concat ss (-static from to) (-wild to to')) (inc to') (inc to'))))
|
||||
\: (let [to' (or (str/index-of s "/" to) (count s))]
|
||||
(recur (concat ss (-static from to) (-wild to to')) to' to'))
|
||||
\* (let [to' (count s)]
|
||||
(recur (concat ss (-static from to) (-catch-all to to')) to' to'))
|
||||
(recur ss from (inc to)))))))
|
||||
|
||||
(defn join-path [xs]
|
||||
(reduce
|
||||
(fn [s x]
|
||||
(str s (cond
|
||||
(string? x) x
|
||||
(instance? Wild x) (str "{" (-> x :value str (subs 1)) "}")
|
||||
(instance? CatchAll x) (str "{*" (-> x :value str (subs 1)) "}"))))
|
||||
"" xs))
|
||||
|
||||
(defn normalize [s]
|
||||
(-> s (split-path) (join-path)))
|
||||
|
||||
;;
|
||||
;; Conflict Resolution
|
||||
;;
|
||||
|
||||
(defn- -slice-start [[p1 :as p1s] [p2 :as p2s]]
|
||||
(let [-split (fn [p]
|
||||
(if-let [i (and p (str/index-of p "/"))]
|
||||
[(subs p 0 i) (subs p i)]
|
||||
[p]))
|
||||
-slash (fn [cp p]
|
||||
(cond
|
||||
(not (string? cp)) [cp]
|
||||
(and (string? cp) (not= (count cp) (count p))) [(subs p (count cp))]
|
||||
(and (string? p) (not cp)) (-split p)))
|
||||
-postcut (fn [[p :as pps]]
|
||||
(let [i (and p (str/index-of p "/"))]
|
||||
(if (and i (pos? i))
|
||||
(concat [(subs p 0 i) (subs p i)] (rest pps))
|
||||
pps)))
|
||||
-tailcut (fn [cp [p :as ps]] (concat (-slash cp p) (rest ps)))]
|
||||
(if (or (nil? p1) (nil? p2))
|
||||
[(-postcut p1s) (-postcut p2s)]
|
||||
(if-let [cp (and (string? p1) (string? p2) (common-prefix p1 p2))]
|
||||
[(-tailcut cp p1s) (-tailcut cp p2s)]
|
||||
[p1s p2s]))))
|
||||
|
||||
(defn- -slice-end [x xs]
|
||||
(let [i (if (string? x) (str/index-of x "/"))]
|
||||
(if (and (number? i) (pos? i))
|
||||
(concat [(subs x i)] xs)
|
||||
xs)))
|
||||
|
||||
(defn conflicting-paths? [path1 path2]
|
||||
(loop [parts1 (split-path path1)
|
||||
parts2 (split-path path2)]
|
||||
(let [[[s1 & ss1] [s2 & ss2]] (-slice-start parts1 parts2)]
|
||||
(cond
|
||||
(= s1 s2 nil) true
|
||||
(or (nil? s1) (nil? s2)) false
|
||||
(or (catch-all? s1) (catch-all? s2)) true
|
||||
(or (wild? s1) (wild? s2)) (recur (-slice-end s1 ss1) (-slice-end s2 ss2))
|
||||
(not= s1 s2) false
|
||||
:else (recur ss1 ss2)))))
|
||||
|
||||
;;
|
||||
;; Creating Tries
|
||||
;;
|
||||
|
||||
(defn- -node [m]
|
||||
(map->Node (merge {:children {}, :wilds {}, :catch-all {}, :params {}} m)))
|
||||
|
||||
(defn- -insert [node [path & ps] params data]
|
||||
(let [node' (cond
|
||||
|
||||
(nil? path)
|
||||
(assoc node :data data :params params)
|
||||
|
||||
(instance? Wild path)
|
||||
(let [next (first ps)]
|
||||
(if (or (instance? Wild next) (instance? CatchAll next))
|
||||
(ex/fail! (str "Two following wilds: " path ", " next))
|
||||
(update-in node [:wilds path] (fn [n] (-insert (or n (-node {})) ps params data)))))
|
||||
|
||||
(instance? CatchAll path)
|
||||
(assoc-in node [:catch-all path] (-node {:params params, :data data}))
|
||||
|
||||
(str/blank? path)
|
||||
(-insert node ps params data)
|
||||
|
||||
:else
|
||||
(or
|
||||
(reduce
|
||||
(fn [_ [p n]]
|
||||
(if-let [cp (common-prefix p path)]
|
||||
(if (= cp p)
|
||||
;; insert into child node
|
||||
(let [n' (-insert n (conj ps (subs path (count p))) params data)]
|
||||
(reduced (assoc-in node [:children p] n')))
|
||||
;; split child node
|
||||
(let [rp (subs p (count cp))
|
||||
rp' (subs path (count cp))
|
||||
n' (-insert (-node {}) ps params data)
|
||||
n'' (-insert (-node {:children {rp n, rp' n'}}) nil nil nil)]
|
||||
(reduced (update node :children (fn [children]
|
||||
(-> children
|
||||
(dissoc p)
|
||||
(assoc cp n'')))))))))
|
||||
nil (:children node))
|
||||
;; new child node
|
||||
(assoc-in node [:children path] (-insert (-node {}) ps params data))))]
|
||||
(if-let [child (get-in node' [:children ""])]
|
||||
;; optimize by removing empty paths
|
||||
(-> (merge-with merge (dissoc node' :data) child)
|
||||
(update :children dissoc ""))
|
||||
node')))
|
||||
|
||||
(defn- decode [path start end percent?]
|
||||
(let [param (subs path start end)]
|
||||
(if percent?
|
||||
#?(:cljs (js/decodeURIComponent param)
|
||||
:clj (URLDecoder/decode
|
||||
(if (.contains ^String param "+")
|
||||
(.replace ^String param "+" "%2B")
|
||||
param)
|
||||
"UTF-8"))
|
||||
param)))
|
||||
|
||||
;;
|
||||
;; Compilers
|
||||
;;
|
||||
|
||||
(defn clojure-trie-compiler []
|
||||
(reify
|
||||
TrieCompiler
|
||||
(data-matcher [_ params data]
|
||||
(let [match (->Match params data)]
|
||||
(reify Matcher
|
||||
(match [_ i max _]
|
||||
(if (= i max)
|
||||
match))
|
||||
(view [_] data)
|
||||
(depth [_] 1)
|
||||
(length [_]))))
|
||||
(static-matcher [_ path matcher]
|
||||
(let [size (count path)]
|
||||
(reify Matcher
|
||||
(match [_ i max p]
|
||||
(if-not (< max (+ i size))
|
||||
(loop [j 0]
|
||||
(if (= j size)
|
||||
(match matcher (+ i size) max p)
|
||||
(if (= (get p (+ i j)) (get path j))
|
||||
(recur (inc j)))))))
|
||||
(view [_] [path (view matcher)])
|
||||
(depth [_] (inc (depth matcher)))
|
||||
(length [_] (count path)))))
|
||||
(wild-matcher [_ key end matcher]
|
||||
(reify Matcher
|
||||
(match [_ i max path]
|
||||
(if (and (< i max) (not= (get path i) end))
|
||||
(loop [percent? false, j i]
|
||||
(if (= max j)
|
||||
(if-let [match (match matcher max max path)]
|
||||
(assoc-param match key (decode path i max percent?)))
|
||||
(let [c ^char (get path j)]
|
||||
(condp = c
|
||||
end (if-let [match (match matcher j max path)]
|
||||
(assoc-param match key (decode path i j percent?)))
|
||||
\% (recur true (inc j))
|
||||
(recur percent? (inc j))))))))
|
||||
(view [_] [key (view matcher)])
|
||||
(depth [_] (inc (depth matcher)))
|
||||
(length [_])))
|
||||
(catch-all-matcher [_ key params data]
|
||||
(let [match (->Match params data)]
|
||||
(reify Matcher
|
||||
(match [_ i max path]
|
||||
(if (< i max) (assoc-param match key (decode path i max true))))
|
||||
(view [_] [key [data]])
|
||||
(depth [_] 1)
|
||||
(length [_]))))
|
||||
(linear-matcher [_ matchers]
|
||||
(let [matchers (vec (reverse (sort-by (juxt depth length) matchers)))
|
||||
size (count matchers)]
|
||||
(reify Matcher
|
||||
(match [_ i max path]
|
||||
(loop [j 0]
|
||||
(if (< j size)
|
||||
(or (match (get matchers j) i max path)
|
||||
(recur (inc j))))))
|
||||
(view [_] (mapv view matchers))
|
||||
(depth [_] (inc (apply max 0 (map depth matchers))))
|
||||
(length [_]))))
|
||||
(-pretty [_ matcher]
|
||||
(view matcher))
|
||||
(-path-matcher [_ matcher]
|
||||
(fn [path]
|
||||
(if-let [match (match matcher 0 (count path) path)]
|
||||
(->Match (:params match) (:data match)))))))
|
||||
|
||||
#?(:clj
|
||||
(defn java-trie-compiler []
|
||||
(reify
|
||||
TrieCompiler
|
||||
(data-matcher [_ params data]
|
||||
(Trie/dataMatcher params data))
|
||||
(static-matcher [_ path matcher]
|
||||
(Trie/staticMatcher ^String path ^Trie$Matcher matcher))
|
||||
(wild-matcher [_ key end matcher]
|
||||
(Trie/wildMatcher key (if end (Character. end)) matcher))
|
||||
(catch-all-matcher [_ key params data]
|
||||
(Trie/catchAllMatcher key params data))
|
||||
(linear-matcher [_ matchers]
|
||||
(Trie/linearMatcher matchers))
|
||||
(-pretty [_ matcher]
|
||||
(-> matcher str read-string eval))
|
||||
(-path-matcher [_ matcher]
|
||||
(fn [path]
|
||||
(if-let [match ^Trie$Match (Trie/lookup ^Trie$Matcher matcher ^String path)]
|
||||
(->Match (.params match) (.data match))))))))
|
||||
|
||||
;;
|
||||
;; Managing Tries
|
||||
;;
|
||||
|
||||
(defn insert
|
||||
"Returns a trie with routes added to it."
|
||||
([routes]
|
||||
(insert nil routes))
|
||||
([node routes]
|
||||
(reduce
|
||||
(fn [acc [p d]]
|
||||
(insert acc p d))
|
||||
node routes))
|
||||
([node path data]
|
||||
(let [parts (split-path path)
|
||||
params (zipmap (->> parts (remove string?) (map :value)) (repeat nil))]
|
||||
(-insert (or node (-node {})) (split-path path) params data))))
|
||||
|
||||
(defn compiler
|
||||
"Returns a default [[TrieCompiler]]."
|
||||
[]
|
||||
#?(:cljs (clojure-trie-compiler)
|
||||
:clj (java-trie-compiler)))
|
||||
|
||||
(defn compile
|
||||
"Returns a compiled trie, to be used with [[pretty]] or [[path-matcher]]."
|
||||
([options]
|
||||
(compile options (compiler)))
|
||||
([{:keys [data params children wilds catch-all] :or {params {}}} compiler]
|
||||
(let [ends (fn [{:keys [children]}] (or (keys children) ["/"]))
|
||||
matchers (-> []
|
||||
(cond-> data (conj (data-matcher compiler params data)))
|
||||
(into (for [[p c] children] (static-matcher compiler p (compile c compiler))))
|
||||
(into
|
||||
(for [[p c] wilds]
|
||||
(let [p (:value p)
|
||||
ends (ends c)]
|
||||
(if (next ends)
|
||||
(ex/fail! (str "Trie compliation error: wild " p " has two terminators: " ends))
|
||||
(wild-matcher compiler p (ffirst ends) (compile c compiler))))))
|
||||
(into (for [[p c] catch-all] (catch-all-matcher compiler (:value p) params (:data c)))))]
|
||||
(cond
|
||||
(> (count matchers) 1) (linear-matcher compiler matchers)
|
||||
(= (count matchers) 1) (first matchers)
|
||||
:else (data-matcher compiler {} nil)))))
|
||||
|
||||
(defn pretty
|
||||
"Returns a simplified EDN structure of a compiled trie for printing purposes."
|
||||
([compiled-trie]
|
||||
(pretty compiled-trie (compiler)))
|
||||
([compiled-trie compiler]
|
||||
(-pretty compiler compiled-trie)))
|
||||
|
||||
(defn path-matcher
|
||||
"Returns a function of `path -> Match` from a compiled trie."
|
||||
([compiled-trie]
|
||||
(path-matcher compiled-trie (compiler)))
|
||||
([compiled-trie compiler]
|
||||
(-path-matcher compiler compiled-trie)))
|
||||
|
||||
;;
|
||||
;; spike
|
||||
;;
|
||||
|
||||
(comment
|
||||
(->
|
||||
[["/v2/whoami" 1]
|
||||
["/v2/users/:user-id/datasets" 2]
|
||||
["/v2/public/projects/:project-id/datasets" 3]
|
||||
["/v1/public/topics/:topic" 4]
|
||||
["/v1/users/:user-id/orgs/:org-id" 5]
|
||||
["/v1/search/topics/:term" 6]
|
||||
["/v1/users/:user-id/invitations" 7]
|
||||
["/v1/users/:user-id/topics" 9]
|
||||
["/v1/users/:user-id/bookmarks/followers" 10]
|
||||
["/v2/datasets/:dataset-id" 11]
|
||||
["/v1/orgs/:org-id/usage-stats" 12]
|
||||
["/v1/orgs/:org-id/devices/:client-id" 13]
|
||||
["/v1/messages/user/:user-id" 14]
|
||||
["/v1/users/:user-id/devices" 15]
|
||||
["/v1/public/users/:user-id" 16]
|
||||
["/v1/orgs/:org-id/errors" 17]
|
||||
["/v1/public/orgs/:org-id" 18]
|
||||
["/v1/orgs/:org-id/invitations" 19]
|
||||
["/v1/users/:user-id/device-errors" 22]
|
||||
["/v2/login" 23]
|
||||
["/v1/users/:user-id/usage-stats" 24]
|
||||
["/v2/users/:user-id/devices" 25]
|
||||
["/v1/users/:user-id/claim-device/:client-id" 26]
|
||||
["/v2/public/projects/:project-id" 27]
|
||||
["/v2/public/datasets/:dataset-id" 28]
|
||||
["/v2/users/:user-id/topics/bulk" 29]
|
||||
["/v1/messages/device/:client-id" 30]
|
||||
["/v1/users/:user-id/owned-orgs" 31]
|
||||
["/v1/topics/:topic" 32]
|
||||
["/v1/users/:user-id/bookmark/:topic" 33]
|
||||
["/v1/orgs/:org-id/members/:user-id" 34]
|
||||
["/v1/users/:user-id/devices/:client-id" 35]
|
||||
["/v1/users/:user-id" 36]
|
||||
["/v1/orgs/:org-id/devices" 37]
|
||||
["/v1/orgs/:org-id/members" 38]
|
||||
["/v2/orgs/:org-id/topics" 40]
|
||||
["/v1/whoami" 41]
|
||||
["/v1/orgs/:org-id" 42]
|
||||
["/v1/users/:user-id/api-key" 43]
|
||||
["/v2/schemas" 44]
|
||||
["/v2/users/:user-id/topics" 45]
|
||||
["/v1/orgs/:org-id/confirm-membership/:token" 46]
|
||||
["/v2/topics/:topic" 47]
|
||||
["/v1/messages/topic/:topic" 48]
|
||||
["/v1/users/:user-id/devices/:client-id/reset-password" 49]
|
||||
["/v2/topics" 50]
|
||||
["/v1/login" 51]
|
||||
["/v1/users/:user-id/orgs" 52]
|
||||
["/v2/public/messages/dataset/:dataset-id" 53]
|
||||
["/v1/topics" 54]
|
||||
["/v1/orgs" 55]
|
||||
["/v1/users/:user-id/bookmarks" 56]
|
||||
["/v1/orgs/:org-id/topics" 57]]
|
||||
(insert)
|
||||
(compile)
|
||||
(pretty)
|
||||
(./aprint))
|
||||
|
||||
(-> [["/{a}/2"]
|
||||
["/{a}.2"]]
|
||||
(insert)
|
||||
(compile))
|
||||
|
||||
(-> [["/kikka" 2]
|
||||
["/kikka/kakka/kukka" 3]
|
||||
["/kikka/:kakka/kurkku" 4]
|
||||
["/kikka/kuri/{user/doc}/html" 5]]
|
||||
(insert)
|
||||
(compile)
|
||||
(pretty))
|
||||
|
||||
(map str (.toCharArray "\u2215\u0048\u0065\u006C\u006C\u006F"))
|
||||
(count ["∕" "H" "e" "l" "l" "o" " " "W" "o" "r" "l" "d"]))
|
||||
|
|
@ -1,7 +1,8 @@
|
|||
(ns reitit.ring.spec
|
||||
(:require [clojure.spec.alpha :as s]
|
||||
[reitit.middleware :as middleware]
|
||||
[reitit.spec :as rs]))
|
||||
[reitit.spec :as rs]
|
||||
[reitit.exception :as exception]))
|
||||
|
||||
;;
|
||||
;; Specs
|
||||
|
|
@ -19,11 +20,10 @@
|
|||
|
||||
(defn merge-specs [specs]
|
||||
(when-let [non-specs (seq (remove #(or (s/spec? %) (s/get-spec %)) specs))]
|
||||
(throw
|
||||
(ex-info
|
||||
(str "Not all specs satisfy the Spec protocol: " non-specs)
|
||||
{:specs specs
|
||||
:non-specs non-specs})))
|
||||
(exception/fail!
|
||||
(str "Not all specs satisfy the Spec protocol: " non-specs)
|
||||
{:specs specs
|
||||
:non-specs non-specs}))
|
||||
(s/merge-spec-impl (vec specs) (vec specs) nil))
|
||||
|
||||
(defn validate-route-data [routes key spec]
|
||||
|
|
|
|||
|
|
@ -46,9 +46,9 @@
|
|||
(reify coercion/Coercion
|
||||
(-get-name [_] :schema)
|
||||
(-get-options [_] opts)
|
||||
(-get-apidocs [this spesification {:keys [parameters responses]}]
|
||||
(-get-apidocs [this specification {:keys [parameters responses]}]
|
||||
;; TODO: this looks identical to spec, refactor when schema is done.
|
||||
(case spesification
|
||||
(case specification
|
||||
:swagger (swagger/swagger-spec
|
||||
(merge
|
||||
(if parameters
|
||||
|
|
@ -69,8 +69,8 @@
|
|||
$))]))})))
|
||||
(throw
|
||||
(ex-info
|
||||
(str "Can't produce Schema apidocs for " spesification)
|
||||
{:type spesification, :coercion :schema}))))
|
||||
(str "Can't produce Schema apidocs for " specification)
|
||||
{:type specification, :coercion :schema}))))
|
||||
(-compile-model [_ model _] model)
|
||||
(-open-model [_ schema] (st/open-schema schema))
|
||||
(-encode-error [_ error]
|
||||
|
|
|
|||
|
|
@ -91,8 +91,8 @@
|
|||
(reify coercion/Coercion
|
||||
(-get-name [_] :spec)
|
||||
(-get-options [_] opts)
|
||||
(-get-apidocs [this spesification {:keys [parameters responses]}]
|
||||
(case spesification
|
||||
(-get-apidocs [this specification {:keys [parameters responses]}]
|
||||
(case specification
|
||||
:swagger (swagger/swagger-spec
|
||||
(merge
|
||||
(if parameters
|
||||
|
|
@ -113,8 +113,8 @@
|
|||
$))]))})))
|
||||
(throw
|
||||
(ex-info
|
||||
(str "Can't produce Spec apidocs for " spesification)
|
||||
{:type spesification, :coercion :spec}))))
|
||||
(str "Can't produce Spec apidocs for " specification)
|
||||
{:specification specification, :coercion :spec}))))
|
||||
(-compile-model [_ model name]
|
||||
(into-spec model name))
|
||||
(-open-model [_ spec] spec)
|
||||
|
|
|
|||
|
|
@ -5,7 +5,8 @@
|
|||
[clojure.spec.alpha :as s]
|
||||
[clojure.set :as set]
|
||||
[clojure.string :as str]
|
||||
[reitit.coercion :as coercion]))
|
||||
[reitit.coercion :as coercion]
|
||||
[reitit.trie :as trie]))
|
||||
|
||||
(s/def ::id (s/or :keyword keyword? :set (s/coll-of keyword? :into #{})))
|
||||
(s/def ::no-doc boolean?)
|
||||
|
|
@ -64,12 +65,8 @@
|
|||
{:name ::swagger
|
||||
:spec ::spec})
|
||||
|
||||
(defn- path->template [path]
|
||||
(->> (impl/segments path)
|
||||
(map #(if (impl/wild-or-catch-all-param? %)
|
||||
(str "{" (subs % 1) "}") %))
|
||||
(str/join "/")
|
||||
(str "/")))
|
||||
(defn- swagger-path [path]
|
||||
(-> path trie/normalize (str/replace #"\{\*" "{")))
|
||||
|
||||
(defn create-swagger-handler []
|
||||
"Create a ring handler to emit swagger spec. Collects all routes from router which have
|
||||
|
|
@ -100,7 +97,7 @@
|
|||
(strip-top-level-keys swagger))]))
|
||||
transform-path (fn [[p _ c]]
|
||||
(if-let [endpoint (some->> c (keep transform-endpoint) (seq) (into {}))]
|
||||
[(path->template p) endpoint]))]
|
||||
[(swagger-path p) endpoint]))]
|
||||
(let [paths (->> router (r/compiled-routes) (filter accept-route) (map transform-path) (into {}))]
|
||||
{:status 200
|
||||
:body (meta-merge swagger {:paths paths})})))
|
||||
|
|
|
|||
|
|
@ -165,6 +165,7 @@
|
|||
;; 530 µs (4-24x) -25% prefix-tree-router
|
||||
;; 710 µs (3-18x) segment-router
|
||||
;; 320 µs (6-40x) java-segment-router
|
||||
;; 115 µs (18-111x) trie-router
|
||||
(title "reitit")
|
||||
(assert (reitit/match-by-path reitit-routes "/workspace/1/1"))
|
||||
(cc/quick-bench
|
||||
|
|
@ -205,3 +206,41 @@
|
|||
(routing-test1)
|
||||
(routing-test2)
|
||||
(reverse-routing-test))
|
||||
|
||||
|
||||
(comment
|
||||
(import '[reitit Trie])
|
||||
(set! *warn-on-reflection* true)
|
||||
|
||||
(let [trie (Trie/linearMatcher
|
||||
[(Trie/staticMatcher
|
||||
"/auth/" (Trie/linearMatcher
|
||||
[(Trie/staticMatcher "login" (Trie/dataMatcher 1))
|
||||
(Trie/staticMatcher "recovery/token/" (Trie/wildMatcher :token (Trie/dataMatcher 2)))]))
|
||||
(Trie/staticMatcher
|
||||
"/workspace/" (Trie/wildMatcher :project (Trie/staticMatcher "/" (Trie/wildMatcher :page (Trie/dataMatcher 3)))))])]
|
||||
|
||||
|
||||
(println
|
||||
(Trie/lookup trie "/auth/login"))
|
||||
|
||||
;; 27ns
|
||||
(cc/quick-bench
|
||||
(dotimes [_ 1000]
|
||||
(Trie/lookup trie "/auth/login")))
|
||||
|
||||
(println
|
||||
(Trie/lookup trie "/auth/recovery/token/123"))
|
||||
|
||||
;; 82ns
|
||||
(cc/quick-bench
|
||||
(dotimes [_ 1000]
|
||||
(Trie/lookup trie "/auth/recovery/token/123")))
|
||||
|
||||
(println
|
||||
(Trie/lookup trie "/workspace/1/1"))
|
||||
|
||||
;; 96ns
|
||||
(cc/quick-bench
|
||||
(dotimes [_ 1000]
|
||||
(Trie/lookup trie "/workspace/1/1")))))
|
||||
|
|
|
|||
|
|
@ -5,8 +5,7 @@
|
|||
[reitit.impl]
|
||||
[clojure.edn :as edn]
|
||||
[reitit.ring :as ring]
|
||||
[reitit.core :as r])
|
||||
(:import (reitit SegmentTrie)))
|
||||
[reitit.core :as r]))
|
||||
|
||||
;;
|
||||
;; start repl with `lein perf repl`
|
||||
|
|
@ -87,34 +86,23 @@
|
|||
{: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))
|
||||
(let [request {:request-method :get, :uri "/user/1234/profile/compact/"}]
|
||||
;; 1338ns (old)
|
||||
;; 981ns (new)
|
||||
;; 805ns (java)
|
||||
;; 704ns (no-inject)
|
||||
;; 458ns (trie)
|
||||
(cc/quick-bench
|
||||
(handler-reitit request))
|
||||
(handler-reitit request)))
|
||||
|
||||
|
||||
(comment
|
||||
;; 281ns
|
||||
;; 190ns
|
||||
(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"))
|
||||
(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))))))
|
||||
|
||||
|
|
|
|||
|
|
@ -20,8 +20,7 @@
|
|||
;;
|
||||
|
||||
(defn h [path]
|
||||
(fn [_]
|
||||
{:status 200, :body path}))
|
||||
(constantly {:status 200, :body path}))
|
||||
|
||||
(defn add [handler routes route]
|
||||
(let [method (-> route keys first str/lower-case keyword)
|
||||
|
|
@ -300,7 +299,7 @@
|
|||
(ring/create-default-handler)
|
||||
{:inject-match? false, :inject-router? false}))
|
||||
|
||||
(defrecord Req [uri request-method])
|
||||
(defrecord Req [uri request-method path-params])
|
||||
|
||||
(defn route->req [route]
|
||||
(map->Req {:request-method (-> route keys first str/lower-case keyword)
|
||||
|
|
@ -317,6 +316,8 @@
|
|||
;; 120ns (faster decode params)
|
||||
;; 140µs (java-segment-router)
|
||||
;; 60ns (java-segment-router, no injects)
|
||||
;; 55ns (trie-router, no injects)
|
||||
;; 54ns (trie-router, no injects, optimized)
|
||||
(let [req (map->Req {:request-method :get, :uri "/user/repos"})]
|
||||
(title "static")
|
||||
(assert (= {:status 200, :body "/user/repos"} (app req)))
|
||||
|
|
@ -328,6 +329,11 @@
|
|||
;; 560µs (java-segment-router)
|
||||
;; 490ns (java-segment-router, no injects)
|
||||
;; 440ns (java-segment-router, no injects, single-wild-optimization)
|
||||
;; 305ns (trie-router, no injects)
|
||||
;; 281ns (trie-router, no injects, optimized)
|
||||
;; 277ns (trie-router, no injects, switch-case) - 690ns clojure
|
||||
;; 273ns (trie-router, no injects, direct-data)
|
||||
;; 256ns (trie-router, pre-defined 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)))
|
||||
|
|
@ -339,6 +345,12 @@
|
|||
;; 120µs (java-segment-router)
|
||||
;; 100µs (java-segment-router, no injects)
|
||||
;; 90µs (java-segment-router, no injects, single-wild-optimization)
|
||||
;; 66µs (trie-router, no injects)
|
||||
;; 64µs (trie-router, no injects, optimized) - 124µs (clojure)
|
||||
;; 63µs (trie-router, no injects, switch-case) - 124µs (clojure)
|
||||
;; 63µs (trie-router, no injects, direct-data)
|
||||
;; 54µs (trie-router, non-transient params)
|
||||
;; 49µs (trie-router, pre-defined parameters)
|
||||
(let [requests (mapv route->req routes)]
|
||||
(title "all")
|
||||
(cc/quick-bench
|
||||
|
|
@ -346,4 +358,13 @@
|
|||
(app r)))))
|
||||
|
||||
(comment
|
||||
(routing-test))
|
||||
(routing-test)
|
||||
(ring/get-router app)
|
||||
(app {:uri "/authorizations/1", :request-method :get})
|
||||
(app {:request-method :get, :uri "/repos/julienschmidt/httprouter/stargazers"})
|
||||
(do
|
||||
(require '[clj-async-profiler.core :as prof])
|
||||
(prof/profile
|
||||
(dotimes [_ 1000000]
|
||||
(app {:request-method :get, :uri "/repos/julienschmidt/httprouter/stargazers"})))
|
||||
(prof/serve-files 8080)))
|
||||
|
|
|
|||
|
|
@ -185,15 +185,6 @@
|
|||
: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!)
|
||||
|
|
|
|||
|
|
@ -2,9 +2,13 @@
|
|||
(:require [criterium.core :as cc]
|
||||
[reitit.perf-utils :refer :all]
|
||||
|
||||
;; aleph
|
||||
[aleph.http :as http]
|
||||
|
||||
;; reitit
|
||||
[reitit.ring :as ring]
|
||||
[muuntaja.middleware :as mm]
|
||||
[muuntaja.core :as m]
|
||||
[reitit.ring.middleware.muuntaja :as rm]
|
||||
|
||||
;; bidi-yada
|
||||
[yada.yada :as yada]
|
||||
|
|
@ -14,7 +18,8 @@
|
|||
;; defaults
|
||||
[ring.middleware.defaults :as defaults]
|
||||
[compojure.core :as compojure]
|
||||
[clojure.string :as str]))
|
||||
[clojure.string :as str]
|
||||
[muuntaja.middleware :as mm]))
|
||||
|
||||
;;
|
||||
;; start repl with `lein perf repl`
|
||||
|
|
@ -31,16 +36,16 @@
|
|||
;; Memory: 16 GB
|
||||
;;
|
||||
|
||||
;; TODO: naive implementation
|
||||
(defn- with-security-headers [response]
|
||||
(update
|
||||
(assoc
|
||||
response
|
||||
:headers
|
||||
(fn [headers]
|
||||
(-> headers
|
||||
(assoc "x-frame-options" "SAMEORIGIN")
|
||||
(assoc "x-xss-protection" "1; mode=block")
|
||||
(assoc "x-content-type-options" "nosniff")))))
|
||||
(reduce-kv
|
||||
assoc
|
||||
{"x-frame-options" "SAMEORIGIN"
|
||||
"x-xss-protection" "1; mode=block"
|
||||
"x-content-type-options" "nosniff"}
|
||||
(:headers response))))
|
||||
|
||||
(def security-middleware
|
||||
{:name ::security
|
||||
|
|
@ -53,7 +58,8 @@
|
|||
(ring/router
|
||||
["/api/ping"
|
||||
{:get {:handler (fn [_] {:status 200, :body {:ping "pong"}})}}]
|
||||
{:data {:middleware [mm/wrap-format
|
||||
{:data {:muuntaja (m/create (assoc m/default-options :return :bytes))
|
||||
:middleware [rm/format-middleware
|
||||
security-middleware]}})))
|
||||
|
||||
(def bidi-yada-app
|
||||
|
|
@ -87,7 +93,7 @@
|
|||
|
||||
(defn perf-test []
|
||||
|
||||
;; 176µs
|
||||
;; 206µs
|
||||
(title "compojure + ring-defaults")
|
||||
(let [f (fn [] (defaults-app request))]
|
||||
(expect! (-> (f) :body slurp))
|
||||
|
|
@ -99,7 +105,7 @@
|
|||
(expect! (-> (f) deref :body bs/to-string))
|
||||
(cc/quick-bench (f)))
|
||||
|
||||
;; 5.0µs
|
||||
;; 6.0µs
|
||||
(title "reitit-ring")
|
||||
(let [f (fn [] (reitit-app request))]
|
||||
(expect! (-> (f) :body slurp))
|
||||
|
|
@ -107,3 +113,20 @@
|
|||
|
||||
(comment
|
||||
(perf-test))
|
||||
|
||||
(comment
|
||||
|
||||
;; 10198
|
||||
;; http :3000/api/ping
|
||||
;; wrk -d ${DURATION:="30s"} http://127.0.0.1:3000/api/ping
|
||||
(http/start-server defaults-app {:port 3000})
|
||||
|
||||
;; 16230
|
||||
;; http :3001/api/ping
|
||||
;; wrk -d ${DURATION:="30s"} http://127.0.0.1:3001/api/ping
|
||||
(http/start-server bidi-yada-app {:port 3001})
|
||||
|
||||
;; 48084
|
||||
;; http :3002/api/ping
|
||||
;; wrk -d ${DURATION:="30s"} http://127.0.0.1:3002/api/ping
|
||||
(http/start-server reitit-app {:port 3002}))
|
||||
|
|
|
|||
|
|
@ -29,6 +29,9 @@
|
|||
(for [name ["product" "a" "b" "c" "d" "e" "f" "g" "h" "i" "j" "k" "l" "m" "n" "o" "p" "q" "r" "twenty"]]
|
||||
[(str "/" name "/:id") {:get (partial h name)}]))))
|
||||
|
||||
(for [name ["product" "a" "b" "c" "d" "e" "f" "g" "h" "i" "j" "k" "l" "m" "n" "o" "p" "q" "r" "twenty"]]
|
||||
[(str "/" name "/:id") {:get (partial h name)}])
|
||||
|
||||
(app {:request-method :get, :uri "/product/foo"})
|
||||
|
||||
(defn routing-test []
|
||||
|
|
@ -69,7 +72,7 @@
|
|||
;; 25310 / 25126
|
||||
"regex"
|
||||
|
||||
;; 88060 / 90778
|
||||
;; 112719 / 113959
|
||||
(title "reitit")
|
||||
;; wrk -d ${DURATION:="30s"} http://127.0.0.1:2048/product/foo
|
||||
;; wrk -d ${DURATION:="30s"} http://127.0.0.1:2048/twenty/bar
|
||||
|
|
@ -79,3 +82,16 @@
|
|||
(comment
|
||||
(web/run app {:port 2048, :dispatch? false, :server {:always-set-keep-alive false}})
|
||||
(routing-test))
|
||||
|
||||
(comment
|
||||
(require '[compojure.core :as c])
|
||||
(def app (apply
|
||||
c/routes
|
||||
(for [name ["product" "a" "b" "c" "d" "e" "f" "g" "h" "i" "j" "k" "l" "m" "n" "o" "p" "q" "r" "twenty"]]
|
||||
(eval `(c/GET ~(str "/" name "/:id") [~'id] (str "Got " ~name " id " ~'id))))))
|
||||
|
||||
(require '[ring.adapter.jetty :as jetty])
|
||||
;; 57862 / 54290
|
||||
;; wrk -d ${DURATION:="30s"} http://127.0.0.1:8080/product/foo
|
||||
;; wrk -d ${DURATION:="30s"} http://127.0.0.1:8080/twenty/bar
|
||||
(jetty/run-jetty app {:port 8080}))
|
||||
|
|
|
|||
|
|
@ -14,7 +14,9 @@
|
|||
[io.pedestal.http.route.definition.table :as table]
|
||||
[io.pedestal.http.route.map-tree :as map-tree]
|
||||
[io.pedestal.http.route.router :as pedestal]
|
||||
[reitit.core :as r]))
|
||||
[reitit.core :as r]
|
||||
[criterium.core :as cc]
|
||||
[reitit.trie :as trie]))
|
||||
|
||||
;;
|
||||
;; start repl with `lein perf repl`
|
||||
|
|
@ -568,6 +570,10 @@
|
|||
;; 662ns (prefix-tree-router)
|
||||
;; 567ns (segment-router)
|
||||
;; 326ns (java-segment-router)
|
||||
;; 194ns (trie)
|
||||
;; 160ns (trie, prioritized)
|
||||
;; 130ns (trie, non-transient, direct-data)
|
||||
;; 121ns (trie, pre-defined parameters)
|
||||
(b! "reitit" reitit-f)
|
||||
|
||||
;; 2845ns
|
||||
|
|
@ -578,13 +584,23 @@
|
|||
;; 806ns (decode path-parameters)
|
||||
;; 735ns (maybe-map-values)
|
||||
;; 474ns (java-segment-router)
|
||||
;; 373ns (trie)
|
||||
;; 323ns (trie, prioritized)
|
||||
;; 289ns (trie, prioritized, zero-copy)
|
||||
;; 266ns (trie, non-transient, direct-data)
|
||||
;; 251ns (trie, pre-defined parameters)
|
||||
(b! "reitit-ring" reitit-ring-f)
|
||||
|
||||
;; 385ns (java-segment-router, no injects)
|
||||
;; 271ms (trie)
|
||||
;; 240ns (trie, prioritized)
|
||||
;; 214ns (trie, non-transient, direct-data)
|
||||
;; 187ns (trie, pre-defined parameters)
|
||||
(b! "reitit-ring-fast" reitit-ring-fast-f)
|
||||
|
||||
;; 2553ns (linear-router)
|
||||
;; 630ns (segment-router-backed)
|
||||
;; 464ns (trie, non-transient, direct-data)
|
||||
(b! "reitit-ring-linear" reitit-ring-linear-f)
|
||||
|
||||
;; 2137ns
|
||||
|
|
@ -610,3 +626,29 @@
|
|||
|
||||
(comment
|
||||
(bench-rest!))
|
||||
|
||||
(comment
|
||||
(set! *warn-on-reflection* true)
|
||||
(require '[clj-async-profiler.core :as prof])
|
||||
;; 629ms (arraylist)
|
||||
;; 409ns (transient)
|
||||
;; 409ns (staticMultiMatcher)
|
||||
;; 305ns (non-persistent-params)
|
||||
;; 293ns (pre-defined parameters)
|
||||
(let [app (ring/ring-handler (ring/router opensensors-routes) {:inject-match? false, :inject-router? false})
|
||||
request {:uri "/v1/users/1/devices/1", :request-method :get}]
|
||||
(doseq [[p r] (-> app (ring/get-router) (r/routes))]
|
||||
(when-not (app {:uri p, :request-method :get})
|
||||
(println "FAIL:" p)))
|
||||
(println (app request))
|
||||
(cc/quick-bench
|
||||
(app request))
|
||||
(prof/start {})
|
||||
; "Elapsed time: 9183.657012 msecs"
|
||||
; "Elapsed time: 8674.70132 msecs"
|
||||
; "Elapsed time: 6714.434915 msecs"
|
||||
; "Elapsed time: 6325.310043 msecs"
|
||||
(time
|
||||
(dotimes [_ 20000000]
|
||||
(app request)))
|
||||
(str (prof/stop {}))))
|
||||
|
|
|
|||
|
|
@ -34,9 +34,11 @@
|
|||
(mapv
|
||||
(fn [path]
|
||||
(let [request (map->Request (req path))
|
||||
time (int (* (first (:sample-mean (cc/quick-benchmark (dotimes [_ 1000] (f request)) {}))) 1e6))]
|
||||
(println path "=>" time "ns")
|
||||
[path time]))
|
||||
results (cc/quick-benchmark (dotimes [_ 1000] (f request)) {})
|
||||
mean (int (* (first (:sample-mean results)) 1e6))
|
||||
lower (int (* (first (:lower-q results)) 1e6))]
|
||||
(println path "=>" lower "/" mean "ns")
|
||||
[path [mean lower]]))
|
||||
urls)))
|
||||
|
||||
(defn bench [routes req no-paths?]
|
||||
|
|
@ -45,8 +47,8 @@
|
|||
[(str/replace path #"\:" "") name]
|
||||
[path name])) routes)
|
||||
router (reitit/router routes)]
|
||||
(doseq [[path time] (bench-routes routes req #(reitit/match-by-path router %))]
|
||||
(println path "\t" time))))
|
||||
(doseq [[path [mean lower]] (bench-routes routes req #(reitit/match-by-path router %))]
|
||||
(println path "\t" mean lower))))
|
||||
|
||||
;;
|
||||
;; Perf tests
|
||||
|
|
@ -58,8 +60,8 @@
|
|||
(println)
|
||||
(suite name)
|
||||
(println)
|
||||
(let [times (for [[path time] (bench-routes routes req f)]
|
||||
(let [times (for [[path [mean lower]] (bench-routes routes req f)]
|
||||
(do
|
||||
(when verbose? (println (format "%7s" time) "\t" path))
|
||||
time))]
|
||||
(title (str "average: " (int (/ (reduce + times) (count times)))))))
|
||||
(when verbose? (println (format "%7s\t%7s" mean lower) "\t" path))
|
||||
[mean lower]))]
|
||||
(title (str "average, mean: " (int (/ (reduce + (map first times)) (count times)))))))
|
||||
|
|
|
|||
|
|
@ -1,9 +1,8 @@
|
|||
(ns reitit.prefix-tree-perf-test
|
||||
(:require [clojure.test :refer :all]
|
||||
[io.pedestal.http.route.prefix-tree :as p]
|
||||
[reitit.segment :as segment]
|
||||
[criterium.core :as cc])
|
||||
(:import (reitit SegmentTrie)))
|
||||
[reitit.trie :as trie]
|
||||
[criterium.core :as cc]))
|
||||
|
||||
;;
|
||||
;; testing
|
||||
|
|
@ -70,19 +69,21 @@
|
|||
(p/insert acc p d))
|
||||
nil routes))
|
||||
|
||||
(def matcher
|
||||
(.matcher
|
||||
^SegmentTrie
|
||||
(reduce
|
||||
(fn [acc [p d]]
|
||||
(segment/insert acc p d))
|
||||
nil routes)))
|
||||
(def trie-matcher
|
||||
(trie/path-matcher
|
||||
(trie/compile
|
||||
(reduce
|
||||
(fn [acc [p d]]
|
||||
(trie/insert acc p d))
|
||||
nil routes))))
|
||||
|
||||
(defn bench! []
|
||||
|
||||
;; 2.3µs
|
||||
#_(cc/quick-bench
|
||||
(p/lookup pedestal-tree "/v1/orgs/1/topics"))
|
||||
;; 2.1µs (28.2.2019)
|
||||
(cc/with-progress-reporting
|
||||
(cc/bench
|
||||
(p/lookup pedestal-tree "/v1/orgs/1/topics")))
|
||||
|
||||
;; 3.1µs
|
||||
;; 2.5µs (string equals)
|
||||
|
|
@ -100,7 +101,7 @@
|
|||
;; 0.8µs (return route-data)
|
||||
;; 0.8µs (fix payloads)
|
||||
#_(cc/quick-bench
|
||||
(trie/lookup reitit-tree "/v1/orgs/1/topics" {}))
|
||||
(trie/path-matcher reitit-tree "/v1/orgs/1/topics" {}))
|
||||
|
||||
;; 0.9µs (initial)
|
||||
;; 0.5µs (protocols)
|
||||
|
|
@ -109,13 +110,24 @@
|
|||
;; 0.63µs (Single sweep path paraµs)
|
||||
;; 0.51µs (Cleanup)
|
||||
;; 0.30µs (Java)
|
||||
(cc/quick-bench
|
||||
(segment/lookup matcher "/v1/orgs/1/topics")))
|
||||
#_(cc/quick-bench
|
||||
(segment/lookup segment-matcher "/v1/orgs/1/topics"))
|
||||
|
||||
;; 0.320µs (initial)
|
||||
;; 0.300µs (iterate arrays)
|
||||
;; 0.280µs (list-params)
|
||||
;; 0.096µs (trie)
|
||||
(cc/with-progress-reporting
|
||||
(cc/bench
|
||||
(trie-matcher "/v1/orgs/1/topics"))))
|
||||
|
||||
(comment
|
||||
(bench!))
|
||||
|
||||
(set! *warn-on-reflection* true)
|
||||
|
||||
(comment
|
||||
(p/lookup pedestal-tree "/v1/orgs/1/topics")
|
||||
#_(trie/lookup reitit-tree "/v1/orgs/1/topics" {})
|
||||
(segment/lookup matcher "/v1/orgs/1/topics"))
|
||||
(trie-matcher "/v1/orgs/1/topics")
|
||||
#_(segment/lookup segment-matcher "/v1/orgs/1/topics"))
|
||||
|
||||
|
|
|
|||
13
project.clj
13
project.clj
|
|
@ -10,7 +10,7 @@
|
|||
:metadata {:doc/format :markdown}}
|
||||
:scm {:name "git"
|
||||
:url "https://github.com/metosin/reitit"}
|
||||
:javac-options ["-Xlint:unchecked" "-target" "1.7" "-source" "1.7"]
|
||||
:javac-options ["-Xlint:unchecked" "-target" "1.8" "-source" "1.8"]
|
||||
:managed-dependencies [[metosin/reitit "0.2.13"]
|
||||
[metosin/reitit-core "0.2.13"]
|
||||
[metosin/reitit-spec "0.2.13"]
|
||||
|
|
@ -38,6 +38,7 @@
|
|||
[io.pedestal/pedestal.service "0.5.5"]]
|
||||
|
||||
:plugins [[jonase/eastwood "0.3.4"]
|
||||
;[lein-virgil "0.1.7"]
|
||||
[lein-doo "0.1.11"]
|
||||
[lein-cljsbuild "1.1.7"]
|
||||
[lein-cloverage "1.0.13"]
|
||||
|
|
@ -67,7 +68,13 @@
|
|||
[org.clojure/clojurescript "1.10.439"]
|
||||
|
||||
;; modules dependencies
|
||||
[metosin/reitit "0.2.13"]
|
||||
[metosin/schema-tools]
|
||||
[metosin/spec-tools]
|
||||
[metosin/muuntaja]
|
||||
[metosin/sieppari]
|
||||
[metosin/jsonista]
|
||||
[lambdaisland/deep-diff]
|
||||
[meta-merge]
|
||||
|
||||
[expound "0.7.2"]
|
||||
[orchestra "2018.12.06-2"]
|
||||
|
|
@ -90,6 +97,8 @@
|
|||
[manifold "0.1.8"]
|
||||
[funcool/promesa "1.9.0"]
|
||||
|
||||
[com.clojure-goes-fast/clj-async-profiler "0.3.0"]
|
||||
|
||||
;; https://github.com/bensu/doo/issues/180
|
||||
[fipp "0.6.14" :exclusions [org.clojure/core.rrb-vector]]]}
|
||||
:1.9 {:dependencies [[org.clojure/clojure "1.9.0"]]}
|
||||
|
|
|
|||
|
|
@ -1,382 +0,0 @@
|
|||
; 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.
|
||||
|
||||
(comment
|
||||
(ns reitit.chain
|
||||
"Interceptor pattern. Executes a chain of Interceptor functions on a
|
||||
common \"context\" map, maintaining a virtual \"stack\", with error
|
||||
handling and support for asynchronous execution."
|
||||
(:refer-clojure :exclude (name))
|
||||
(:require [clojure.core.async :as async :refer [<! go]]
|
||||
[io.pedestal.log :as log]
|
||||
[io.pedestal.interceptor :as interceptor])
|
||||
(:import java.util.concurrent.atomic.AtomicLong))
|
||||
|
||||
(defrecord Context [execution-id stack queue terminators supressed async-info rebind])
|
||||
|
||||
(declare execute)
|
||||
(declare execute-only)
|
||||
|
||||
(defn- channel? [c] (instance? clojure.core.async.impl.protocols.Channel c))
|
||||
|
||||
;; This is used for printing out interceptors within debug messages
|
||||
(defn- name [interceptor]
|
||||
(:name interceptor (pr-str interceptor)))
|
||||
|
||||
(defn- throwable->ex-info [^Throwable t execution-id interceptor stage]
|
||||
(let [iname (name interceptor)
|
||||
throwable-str (pr-str (type t))]
|
||||
(ex-info (str throwable-str " in Interceptor " iname " - " (.getMessage t))
|
||||
(merge {:execution-id execution-id
|
||||
:stage stage
|
||||
:interceptor iname
|
||||
:exception-type (keyword throwable-str)
|
||||
:exception t}
|
||||
(ex-data t))
|
||||
t)))
|
||||
|
||||
(defn- try-f
|
||||
"If f is not nil, invokes it on context. If f throws an exception,
|
||||
assoc's it on to context as :error."
|
||||
[context interceptor stage]
|
||||
(let [execution-id (:execution-id context)]
|
||||
(if-let [f (stage interceptor)]
|
||||
(try (log/debug :interceptor (name interceptor)
|
||||
:stage stage
|
||||
:execution-id execution-id
|
||||
:fn f)
|
||||
(f context)
|
||||
(catch Throwable t
|
||||
(log/debug :throw t :execution-id execution-id)
|
||||
(assoc context :error (throwable->ex-info t execution-id interceptor stage))))
|
||||
(do (log/trace :interceptor (name interceptor)
|
||||
:skipped? true
|
||||
:stage stage
|
||||
:execution-id execution-id)
|
||||
context))))
|
||||
|
||||
(defn- try-error
|
||||
"If error-fn is not nil, invokes it on context and the current :error
|
||||
from context."
|
||||
[context interceptor]
|
||||
(let [execution-id (:execution-id context)]
|
||||
(if-let [error-fn (:error interceptor)]
|
||||
(let [ex (:error context)
|
||||
stage :error]
|
||||
(log/debug :interceptor (name interceptor)
|
||||
:stage :error
|
||||
:execution-id execution-id)
|
||||
(try (error-fn (assoc context :error nil) ex)
|
||||
(catch Throwable t
|
||||
(if (identical? (type t) (type (:exception ex)))
|
||||
(do (log/debug :rethrow t :execution-id execution-id)
|
||||
context)
|
||||
(do (log/debug :throw t :suppressed (:exception-type ex) :execution-id execution-id)
|
||||
(-> context
|
||||
(assoc :error (throwable->ex-info t execution-id interceptor :error))
|
||||
(update :suppressed conj ex)))))))
|
||||
(do (log/trace :interceptor (name interceptor)
|
||||
:skipped? true
|
||||
:stage :error
|
||||
:execution-id execution-id)
|
||||
context))))
|
||||
|
||||
(defn- check-terminators
|
||||
"Invokes each predicate in :terminators on context. If any predicate
|
||||
returns true, removes :queue from context."
|
||||
[context]
|
||||
(if (some #(% context) (:terminators context))
|
||||
(let [execution-id (:execution-id context)]
|
||||
(log/debug :in 'check-terminators
|
||||
:terminate? true
|
||||
:execution-id execution-id)
|
||||
(assoc context :queue nil))
|
||||
context))
|
||||
|
||||
(defn- prepare-for-async
|
||||
"Call all of the :enter-async functions in a context. The purpose of these
|
||||
functions is to ready backing servlets or any other machinery for preparing
|
||||
an asynchronous response."
|
||||
[{:keys [enter-async] :as context}]
|
||||
(doseq [enter-async-fn enter-async]
|
||||
(enter-async-fn context)))
|
||||
|
||||
(defn- go-async
|
||||
"When presented with a channel as the return value of an enter function,
|
||||
wait for the channel to return a new-context (via a go block). When a new
|
||||
context is received, restart execution of the interceptor chain with that
|
||||
context.
|
||||
This function is non-blocking, returning nil immediately (a signal to halt
|
||||
further execution on this thread)."
|
||||
([old-context context-channel]
|
||||
(prepare-for-async old-context)
|
||||
(go
|
||||
(if-let [new-context (<! context-channel)]
|
||||
(execute new-context)
|
||||
(execute (assoc (assoc old-context :queue nil :async-info nil)
|
||||
:stack (get-in old-context [:async-info :stack])
|
||||
:error (ex-info "Async Interceptor closed Context Channel before delivering a Context"
|
||||
{:execution-id (:execution-id old-context)
|
||||
:stage (get-in old-context [:async-info :stage])
|
||||
:interceptor (name (get-in old-context [:async-info :interceptor]))
|
||||
:exception-type :PedestalChainAsyncPrematureClose})))))
|
||||
nil)
|
||||
([old-context context-channel interceptor-key]
|
||||
(prepare-for-async old-context)
|
||||
(go
|
||||
(if-let [new-context (<! context-channel)]
|
||||
(execute-only new-context interceptor-key)
|
||||
(execute-only (assoc (assoc old-context :queue nil :async-info nil)
|
||||
:stack (get-in old-context [:async-info :stack])
|
||||
:error (ex-info "Async Interceptor closed Context Channel before delivering a Context"
|
||||
{:execution-id (:execution-id old-context)
|
||||
:stage (get-in old-context [:async-info :stage])
|
||||
:interceptor (name (get-in old-context [:async-info :interceptor]))
|
||||
:exception-type :PedestalChainAsyncPrematureClose}))
|
||||
interceptor-key)))
|
||||
nil))
|
||||
|
||||
(defn- process-all-with-binding
|
||||
"Invokes `interceptor-key` functions of all Interceptors on the execution
|
||||
:queue of context, saves them on the :stack of context.
|
||||
Returns updated context.
|
||||
By default, `interceptor-key` is :enter"
|
||||
([context]
|
||||
(process-all-with-binding context :enter))
|
||||
([context interceptor-key]
|
||||
(log/debug :in 'process-all :handling interceptor-key :execution-id (:execution-id context))
|
||||
(loop [context context]
|
||||
(let [queue (:queue context)
|
||||
stack (:stack context)]
|
||||
(log/trace :context context)
|
||||
(if (empty? queue)
|
||||
context
|
||||
(let [interceptor (peek queue)
|
||||
old-context context
|
||||
new-queue (pop queue)
|
||||
;; conj on nil returns a list, acts like a stack:
|
||||
new-stack (conj stack interceptor)
|
||||
context (-> context
|
||||
(assoc :queue new-queue)
|
||||
(assoc :stack new-stack)
|
||||
(try-f interceptor interceptor-key))]
|
||||
(cond
|
||||
(channel? context) (go-async (assoc old-context
|
||||
:async-info {:interceptor interceptor
|
||||
:stage interceptor-key
|
||||
:stack new-stack})
|
||||
context)
|
||||
(:error context) (assoc context :queue nil)
|
||||
(not= (:bindings context) (:bindings old-context)) (assoc context :rebind true)
|
||||
true (recur (check-terminators context)))))))))
|
||||
|
||||
(defn- process-all
|
||||
[context interceptor-key]
|
||||
;; If we're processing leave handlers, reverse the queue
|
||||
(let [context (if (= interceptor-key :leave) (update context :queue reverse) context)
|
||||
context (with-bindings (or (:bindings context)
|
||||
{})
|
||||
(process-all-with-binding context interceptor-key))]
|
||||
(if (:rebind context)
|
||||
(recur (assoc context :rebind nil) interceptor-key)
|
||||
context)))
|
||||
|
||||
(defn- process-any-errors-with-binding
|
||||
"Unwinds the context by invoking :error functions of Interceptors on
|
||||
the :stack of context, but **only** if there is an :error present in the context."
|
||||
[context]
|
||||
(log/debug :in 'process-any-errors :execution-id (:execution-id context))
|
||||
(loop [context context]
|
||||
(let [stack (:stack context)]
|
||||
(log/trace :context context)
|
||||
(if (empty? stack)
|
||||
context
|
||||
(let [interceptor (peek stack)
|
||||
pre-bindings (:bindings context)
|
||||
old-context context
|
||||
context (assoc context :stack (pop stack))
|
||||
context (if (:error context)
|
||||
(try-error context interceptor)
|
||||
context)]
|
||||
(cond
|
||||
(channel? context) (go-async old-context context)
|
||||
(not= (:bindings context) pre-bindings) (assoc context :rebind true)
|
||||
true (recur context)))))))
|
||||
|
||||
(defn- process-any-errors
|
||||
"Establish the bindings present in `context` as thread local
|
||||
bindings, and then invoke process-any-errors-with-binding.
|
||||
Conditionally re-establish bindings if a change in bindings is made by an
|
||||
interceptor."
|
||||
[context]
|
||||
(let [context (with-bindings (or (:bindings context) {})
|
||||
(process-any-errors-with-binding context))]
|
||||
(if (:rebind context)
|
||||
(recur (assoc context :rebind nil))
|
||||
context)))
|
||||
|
||||
(defn- enter-all
|
||||
"Establish the bindings present in `context` as thread local
|
||||
bindings, and then invoke enter-all-with-binding. Conditionally
|
||||
re-establish bindings if a change in bindings is made by an
|
||||
interceptor."
|
||||
[context]
|
||||
(process-all context :enter))
|
||||
|
||||
(defn- leave-all-with-binding
|
||||
"Unwinds the context by invoking :leave functions of Interceptors on
|
||||
the :stack of context. Returns updated context."
|
||||
[context]
|
||||
(log/debug :in 'leave-all :execution-id (:execution-id context))
|
||||
(loop [context context]
|
||||
(let [stack (:stack context)]
|
||||
(log/trace :context context)
|
||||
(if (empty? stack)
|
||||
context
|
||||
(let [interceptor (peek stack)
|
||||
pre-bindings (:bindings context)
|
||||
old-context context
|
||||
context (assoc context :stack (pop stack))
|
||||
context (if (:error context)
|
||||
(try-error context interceptor)
|
||||
(try-f context interceptor :leave))]
|
||||
(cond
|
||||
(channel? context) (go-async old-context context)
|
||||
(not= (:bindings context) pre-bindings) (assoc context :rebind true)
|
||||
true (recur context)))))))
|
||||
|
||||
(defn- leave-all
|
||||
"Establish the bindings present in `context` as thread local
|
||||
bindings, and then invoke leave-all-with-binding. Conditionally
|
||||
re-establish bindings if a change in bindings is made by an
|
||||
interceptor."
|
||||
[context]
|
||||
(let [context (with-bindings (or (:bindings context) {})
|
||||
(leave-all-with-binding context))]
|
||||
(if (:rebind context)
|
||||
(recur (assoc context :rebind nil))
|
||||
context)))
|
||||
|
||||
(defn enqueue
|
||||
"Adds interceptors to the end of context's execution queue. Creates
|
||||
the queue if necessary. Returns updated context."
|
||||
[context interceptors]
|
||||
{:pre (every? interceptor/interceptor? interceptors)}
|
||||
(log/trace :enqueue (map name interceptors) :context context)
|
||||
(update context :queue
|
||||
(fnil into clojure.lang.PersistentQueue/EMPTY)
|
||||
interceptors))
|
||||
|
||||
(defn enqueue*
|
||||
"Like 'enqueue' but vararg.
|
||||
If the last argument is a sequence of interceptors,
|
||||
they're unpacked and to added to the context's execution queue."
|
||||
[context & interceptors-and-seq]
|
||||
(if (seq? (last interceptors-and-seq))
|
||||
(enqueue context (apply list* interceptors-and-seq))
|
||||
(enqueue context interceptors-and-seq)))
|
||||
|
||||
(defn terminate
|
||||
"Removes all remaining interceptors from context's execution queue.
|
||||
This effectively short-circuits execution of Interceptors' :enter
|
||||
functions and begins executing the :leave functions."
|
||||
[context]
|
||||
(log/trace :in 'terminate :context context)
|
||||
(assoc context :queue nil))
|
||||
|
||||
(defn terminate-when
|
||||
"Adds pred as a terminating condition of the context. pred is a
|
||||
function that takes a context as its argument. It will be invoked
|
||||
after every Interceptor's :enter function. If pred returns logical
|
||||
true, execution will stop at that Interceptor."
|
||||
[context pred]
|
||||
(update context :terminators conj pred))
|
||||
|
||||
(def ^:private ^AtomicLong execution-id (AtomicLong.))
|
||||
|
||||
(defn- begin [context]
|
||||
(if (:execution-id context)
|
||||
context
|
||||
(let [execution-id (.incrementAndGet execution-id)]
|
||||
(log/debug :in 'begin :execution-id execution-id)
|
||||
(log/trace :context context)
|
||||
(assoc context :execution-id execution-id))))
|
||||
|
||||
(defn- end [context]
|
||||
(if (:execution-id context)
|
||||
(do
|
||||
(log/debug :in 'end :execution-id (:execution-id context) :context-keys (keys context))
|
||||
(log/trace :context context)
|
||||
(assoc context :stack nil :execution-id nil))
|
||||
context))
|
||||
|
||||
(defn execute-only
|
||||
"Like `execute`, but only processes the interceptors in a single direction,
|
||||
using `interceptor-key` (i.e. :enter, :leave) to determine which functions
|
||||
to call.
|
||||
---
|
||||
Executes a queue of Interceptors attached to the context. Context
|
||||
must be a map, Interceptors are added with 'enqueue'.
|
||||
An Interceptor Record has keys :enter, :leave, and :error.
|
||||
The value of each key is a function; missing
|
||||
keys or nil values are ignored. When executing a context, all
|
||||
the `interceptor-key` functions are invoked in order. As this happens, the
|
||||
Interceptors are pushed on to a stack."
|
||||
([context interceptor-key]
|
||||
(let [context (some-> context
|
||||
map->Context
|
||||
begin
|
||||
(process-all interceptor-key)
|
||||
terminate
|
||||
process-any-errors
|
||||
end)]
|
||||
(if-let [ex (:error context)]
|
||||
(throw ex)
|
||||
context)))
|
||||
([context interceptor-key interceptors]
|
||||
(execute-only (enqueue context interceptors) interceptor-key)))
|
||||
|
||||
(defn execute
|
||||
"Executes a queue of Interceptors attached to the context. Context
|
||||
must be a map, Interceptors are added with 'enqueue'.
|
||||
An Interceptor is a map or map-like object with the keys :enter,
|
||||
:leave, and :error. The value of each key is a function; missing
|
||||
keys or nil values are ignored. When executing a context, first all
|
||||
the :enter functions are invoked in order. As this happens, the
|
||||
Interceptors are pushed on to a stack.
|
||||
When execution reaches the end of the queue, it begins popping
|
||||
Interceptors off the stack and calling their :leave functions.
|
||||
Therefore :leave functions are called in the opposite order from
|
||||
:enter functions.
|
||||
Both the :enter and :leave functions are called on a single
|
||||
argument, the context map, and return an updated context.
|
||||
If any Interceptor function throws an exception, execution stops and
|
||||
begins popping Interceptors off the stack and calling their :error
|
||||
functions. The :error function takes two arguments: the context and
|
||||
an exception. It may either handle the exception, in which case the
|
||||
execution continues with the next :leave function on the stack; or
|
||||
re-throw the exception, passing control to the :error function on
|
||||
the stack. If the exception reaches the end of the stack without
|
||||
being handled, execute will throw it."
|
||||
([context]
|
||||
(let [context (some-> context
|
||||
begin
|
||||
enter-all
|
||||
terminate
|
||||
leave-all
|
||||
end)]
|
||||
(if-let [ex (:error context)]
|
||||
(throw ex)
|
||||
context)))
|
||||
([context interceptors]
|
||||
(execute (enqueue context interceptors))))
|
||||
)
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
(ns reitit.core-test
|
||||
(:require [clojure.test :refer [deftest testing is are]]
|
||||
[reitit.core :as r #?@(:cljs [:refer [Match Router]])])
|
||||
[reitit.core :as r #?@(:cljs [:refer [Match Router]])]
|
||||
[reitit.impl :as impl])
|
||||
#?(:clj
|
||||
(:import (reitit.core Match Router)
|
||||
(clojure.lang ExceptionInfo))))
|
||||
|
|
@ -78,15 +79,64 @@
|
|||
["/abba/:dabba/boo" ::boo]
|
||||
["/:jabba/:dabba/:doo/:daa/*foo" ::wild]]
|
||||
{:router r})
|
||||
matches #(-> router (r/match-by-path %) :data :name)]
|
||||
(is (= ::abba (matches "/abba")))
|
||||
(is (= ::abba2 (matches "/abba/1")))
|
||||
(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")))))
|
||||
by-path #(-> router (r/match-by-path %) :data :name)]
|
||||
(is (= ::abba (by-path "/abba")))
|
||||
(is (= ::abba2 (by-path "/abba/1")))
|
||||
(is (= ::jabba2 (by-path "/abba/2")))
|
||||
(is (= ::doo (by-path "/abba/1/doo")))
|
||||
(is (= ::boo (by-path "/abba/1/boo")))
|
||||
(is (= ::baa (by-path "/abba/dabba/boo/baa")))
|
||||
(is (= ::boo (by-path "/abba/dabba/boo")))
|
||||
(is (= ::wild (by-path "/olipa/kerran/avaruus/vaan/ei/toista/kertaa")))))
|
||||
|
||||
(testing "bracket-params"
|
||||
(testing "successful"
|
||||
(let [router (r/router
|
||||
[["/{abba}" ::abba]
|
||||
["/abba/1" ::abba2]
|
||||
["/{jabba}/2" ::jabba2]
|
||||
["/{abba}/{dabba}/doo" ::doo]
|
||||
["/abba/dabba/boo/baa" ::baa]
|
||||
["/abba/{dabba}/boo" ::boo]
|
||||
["/{a/jabba}/{a.b/dabba}/{a.b.c/doo}/{a.b.c.d/daa}/{*foo/bar}" ::wild]
|
||||
["/files/file-{name}.html" ::html]
|
||||
["/files/file-{name}.json" ::json]
|
||||
["/{eskon}/{saum}/pium\u2215paum" ::loru]
|
||||
["/{🌈}🤔/🎈" ::emoji]
|
||||
["/extra-end}s-are/ok" ::bracket]]
|
||||
{:router r})
|
||||
by-path #(-> router (r/match-by-path %) ((juxt (comp :name :data) :path-params)))]
|
||||
(is (= [::abba {:abba "abba"}] (by-path "/abba")))
|
||||
(is (= [::abba2 {}] (by-path "/abba/1")))
|
||||
(is (= [::jabba2 {:jabba "abba"}] (by-path "/abba/2")))
|
||||
(is (= [::doo {:abba "abba", :dabba "1"}] (by-path "/abba/1/doo")))
|
||||
(is (= [::boo {:dabba "1"}] (by-path "/abba/1/boo")))
|
||||
(is (= [::baa {}] (by-path "/abba/dabba/boo/baa")))
|
||||
(is (= [::boo {:dabba "dabba"}] (by-path "/abba/dabba/boo")))
|
||||
(is (= [::wild {:a/jabba "olipa"
|
||||
:a.b/dabba "kerran"
|
||||
:a.b.c/doo "avaruus"
|
||||
:a.b.c.d/daa "vaan"
|
||||
:foo/bar "ei/toista/kertaa"}]
|
||||
(by-path "/olipa/kerran/avaruus/vaan/ei/toista/kertaa")))
|
||||
(is (= [::html {:name "10"}] (by-path "/files/file-10.html")))
|
||||
(is (= [::loru {:eskon "viitan", :saum "aa"}] (by-path "/viitan/aa/pium\u2215paum")))
|
||||
(is (= [nil nil] (by-path "/ei/osu/pium/paum")))
|
||||
(is (= [::emoji {:🌈 "brackets"}] (by-path "/brackets🤔/🎈")))
|
||||
(is (= [::bracket {}] (by-path "/extra-end}s-are/ok")))))
|
||||
|
||||
(testing "invalid syntax fails fast"
|
||||
(testing "unclosed brackets"
|
||||
(is (thrown-with-msg?
|
||||
ExceptionInfo
|
||||
#"^Unclosed brackets"
|
||||
(r/router ["/kikka/{kukka"]))))
|
||||
(testing "multiple terminators"
|
||||
(is (thrown-with-msg?
|
||||
ExceptionInfo
|
||||
#"^Trie compliation error: wild :kukka has two terminators"
|
||||
(r/router [["/{kukka}.json"]
|
||||
["/{kukka}-json"]]))))))
|
||||
|
||||
(testing "empty path segments"
|
||||
(let [router (r/router
|
||||
|
|
@ -103,7 +153,7 @@
|
|||
(is (= nil (matches ""))))))
|
||||
|
||||
r/linear-router :linear-router
|
||||
r/segment-router :segment-router
|
||||
r/trie-router :trie-router
|
||||
r/mixed-router :mixed-router
|
||||
r/quarantine-router :quarantine-router))
|
||||
|
||||
|
|
@ -136,13 +186,14 @@
|
|||
ExceptionInfo
|
||||
#"can't create :lookup-router with wildcard routes"
|
||||
(r/lookup-router
|
||||
(r/resolve-routes
|
||||
["/api/:version/ping"] {}))))))
|
||||
(impl/resolve-routes
|
||||
["/api/:version/ping"]
|
||||
(r/default-router-options)))))))
|
||||
|
||||
r/lookup-router :lookup-router
|
||||
r/single-static-path-router :single-static-path-router
|
||||
r/linear-router :linear-router
|
||||
r/segment-router :segment-router
|
||||
r/trie-router :trie-router
|
||||
r/mixed-router :mixed-router
|
||||
r/quarantine-router :quarantine-router))
|
||||
|
||||
|
|
@ -208,7 +259,7 @@
|
|||
expected [["/auth/login" {:name :auth/login}]
|
||||
["/auth/recovery/token/:token" {:name :auth/recovery}]
|
||||
["/workspace/:project-uuid/:page-uuid" {:name :workspace/page}]]]
|
||||
(is (= expected (r/resolve-routes routes {})))))
|
||||
(is (= expected (impl/resolve-routes routes (r/default-router-options))))))
|
||||
|
||||
(testing "ring sample"
|
||||
(let [pong (constantly "ok")
|
||||
|
|
@ -226,7 +277,7 @@
|
|||
["/api/admin/user" {:mw [:api :admin], :roles #{:user}}]
|
||||
["/api/admin/db" {:mw [:api :admin :db], :roles #{:admin}}]]
|
||||
router (r/router routes)]
|
||||
(is (= expected (r/resolve-routes routes {})))
|
||||
(is (= expected (impl/resolve-routes routes (r/default-router-options))))
|
||||
(is (= (r/map->Match
|
||||
{:template "/api/user/:id/:sub-id"
|
||||
:data {:mw [:api], :parameters {:id "String", :sub-id "String"}}
|
||||
|
|
@ -237,10 +288,10 @@
|
|||
(deftest conflicting-routes-test
|
||||
(testing "path conflicts"
|
||||
(are [conflicting? data]
|
||||
(let [routes (r/resolve-routes data {})
|
||||
(let [routes (impl/resolve-routes data (r/default-router-options))
|
||||
conflicts (-> routes
|
||||
(r/resolve-routes {})
|
||||
(r/path-conflicting-routes))]
|
||||
(impl/resolve-routes (r/default-router-options))
|
||||
(impl/path-conflicting-routes))]
|
||||
(if conflicting? (seq conflicts) (nil? conflicts)))
|
||||
|
||||
true [["/a"]
|
||||
|
|
@ -275,8 +326,8 @@
|
|||
["/:b" {}] #{["/c" {}] ["/*d" {}]},
|
||||
["/c" {}] #{["/*d" {}]}}
|
||||
(-> [["/a"] ["/:b"] ["/c"] ["/*d"]]
|
||||
(r/resolve-routes {})
|
||||
(r/path-conflicting-routes)))))
|
||||
(impl/resolve-routes (r/default-router-options))
|
||||
(impl/path-conflicting-routes)))))
|
||||
|
||||
(testing "router with conflicting routes"
|
||||
(testing "throws by default"
|
||||
|
|
|
|||
|
|
@ -2,11 +2,26 @@
|
|||
(:require [clojure.test :refer [deftest testing is are]]
|
||||
[reitit.impl :as impl]))
|
||||
|
||||
(deftest segments-test
|
||||
(is (= ["api" "ipa" "beer" "craft" "bisse"]
|
||||
(into [] (impl/segments "/api/ipa/beer/craft/bisse"))))
|
||||
(is (= ["a" "" "b" "" "c" ""]
|
||||
(into [] (impl/segments "/a//b//c/")))))
|
||||
(deftest conflicting-route-test
|
||||
(are [c? p1 p2]
|
||||
(is (= c? (impl/conflicting-routes? [p1] [p2])))
|
||||
|
||||
true "/a" "/a"
|
||||
true "/a" "/:a"
|
||||
true "/a/:b" "/:a/b"
|
||||
true "/ab/:b" "/:a/ba"
|
||||
true "/*a" "/:a/ba/ca"
|
||||
|
||||
true "/a" "/{a}"
|
||||
true "/a/{b}" "/{a}/b"
|
||||
true "/ab/{b}" "/{a}/ba"
|
||||
true "/{*a}" "/{a}/ba/ca"
|
||||
|
||||
false "/a" "/:a/b"
|
||||
false "/a" "/:a/b"
|
||||
|
||||
false "/a" "/{a}/b"
|
||||
false "/a" "/{a}/b"))
|
||||
|
||||
(deftest strip-nils-test
|
||||
(is (= {:a 1, :c false} (impl/strip-nils {:a 1, :b nil, :c false}))))
|
||||
|
|
|
|||
|
|
@ -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 "")))))
|
||||
|
|
@ -32,16 +32,16 @@
|
|||
:handler (fn [{{{:keys [x y]} :query
|
||||
{:keys [z]} :path} :parameters}]
|
||||
{:status 200, :body {:total (+ x y z)}})}
|
||||
:post {:summary "plus with body"
|
||||
:post {:summary "plus with body"
|
||||
:parameters {:body [int?]
|
||||
:path {:z int?}}
|
||||
:swagger {:responses {400 {:schema {:type "string"}
|
||||
:description "kosh"}}}
|
||||
:responses {200 {:body {:total int?}}
|
||||
500 {:description "fail"}}
|
||||
:handler (fn [{{{:keys [z]} :path
|
||||
xs :body} :parameters}]
|
||||
{:status 200, :body {:total (+ (reduce + xs) z)}})}}]]
|
||||
:swagger {:responses {400 {:schema {:type "string"}
|
||||
:description "kosh"}}}
|
||||
:responses {200 {:body {:total int?}}
|
||||
500 {:description "fail"}}
|
||||
:handler (fn [{{{:keys [z]} :path
|
||||
xs :body} :parameters}]
|
||||
{:status 200, :body {:total (+ (reduce + xs) z)}})}}]]
|
||||
|
||||
["/schema" {:coercion schema/coercion}
|
||||
["/plus/*z"
|
||||
|
|
@ -72,8 +72,8 @@
|
|||
(is (= {:body {:total 7}, :status 200}
|
||||
(app
|
||||
{:request-method :post
|
||||
:uri "/api/spec/plus/3"
|
||||
:body-params [1 3]}))))
|
||||
:uri "/api/spec/plus/3"
|
||||
:body-params [1 3]}))))
|
||||
(testing "schema"
|
||||
(is (= {:body {:total 6}, :status 200}
|
||||
(app
|
||||
|
|
@ -142,28 +142,28 @@
|
|||
:description "kosh"}
|
||||
500 {:description "fail"}}
|
||||
:summary "plus"}
|
||||
:post {:parameters [{:in "body",
|
||||
:name "",
|
||||
:post {:parameters [{:in "body",
|
||||
:name "",
|
||||
:description "",
|
||||
:required true,
|
||||
:schema {:type "array",
|
||||
:items {:type "integer",
|
||||
:format "int64"}}}
|
||||
{:in "path"
|
||||
:name "z"
|
||||
:required true,
|
||||
:schema {:type "array",
|
||||
:items {:type "integer",
|
||||
:format "int64"}}}
|
||||
{:in "path"
|
||||
:name "z"
|
||||
:description ""
|
||||
:type "integer"
|
||||
:required true
|
||||
:format "int64"}]
|
||||
:responses {200 {:description ""
|
||||
:schema {:properties {"total" {:format "int64"
|
||||
:type "integer"}}
|
||||
:required ["total"]
|
||||
:type "object"}}
|
||||
400 {:schema {:type "string"}
|
||||
:description "kosh"}
|
||||
500 {:description "fail"}}
|
||||
:summary "plus with body"}}}}]
|
||||
:type "integer"
|
||||
:required true
|
||||
:format "int64"}]
|
||||
:responses {200 {:description ""
|
||||
:schema {:properties {"total" {:format "int64"
|
||||
:type "integer"}}
|
||||
:required ["total"]
|
||||
:type "object"}}
|
||||
400 {:schema {:type "string"}
|
||||
:description "kosh"}
|
||||
500 {:description "fail"}}
|
||||
:summary "plus with body"}}}}]
|
||||
(is (= expected spec))
|
||||
|
||||
(testing "ring-async swagger-spec"
|
||||
|
|
|
|||
37
test/cljc/reitit/trie_test.cljc
Normal file
37
test/cljc/reitit/trie_test.cljc
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
(ns reitit.trie-test
|
||||
(:require [clojure.test :refer [deftest testing is are]]
|
||||
[reitit.trie :as trie]))
|
||||
|
||||
(deftest normalize-test
|
||||
(are [path expected]
|
||||
(is (= expected (trie/normalize path)))
|
||||
|
||||
"/olipa/:kerran/avaruus", "/olipa/{kerran}/avaruus"
|
||||
"/olipa/{kerran}/avaruus", "/olipa/{kerran}/avaruus"
|
||||
"/olipa/{a.b/c}/avaruus", "/olipa/{a.b/c}/avaruus"
|
||||
"/olipa/kerran/*avaruus", "/olipa/kerran/{*avaruus}"
|
||||
"/olipa/kerran/{*avaruus}", "/olipa/kerran/{*avaruus}"
|
||||
"/olipa/kerran/{*valvavan.suuri/avaruus}", "/olipa/kerran/{*valvavan.suuri/avaruus}"))
|
||||
|
||||
(deftest tests
|
||||
(is (= (trie/->Match {} {:a 1})
|
||||
((-> (trie/insert nil "/foo" {:a 1})
|
||||
(trie/compile)
|
||||
(trie/path-matcher)) "/foo")))
|
||||
|
||||
(is (= (trie/->Match {} {:a 1})
|
||||
((-> (trie/insert nil "/foo" {:a 1})
|
||||
(trie/insert "/foo/*bar" {:b 1})
|
||||
(trie/compile)
|
||||
(trie/path-matcher)) "/foo")))
|
||||
|
||||
(is (= (trie/->Match {:bar "bar"} {:b 1})
|
||||
((-> (trie/insert nil "/foo" {:a 1})
|
||||
(trie/insert "/foo/*bar" {:b 1})
|
||||
(trie/compile)
|
||||
(trie/path-matcher)) "/foo/bar")))
|
||||
|
||||
(is (= (trie/->Match {} {:a 1})
|
||||
((-> (trie/insert nil "" {:a 1})
|
||||
(trie/compile)
|
||||
(trie/path-matcher)) ""))))
|
||||
Loading…
Reference in a new issue