Merge branch 'master' into ignore-anchor-click-fn

This commit is contained in:
Juho Teperi 2019-08-21 13:43:01 +03:00 committed by GitHub
commit 802c9b04c4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
127 changed files with 3740 additions and 1907 deletions

View file

@ -12,20 +12,169 @@ We use [Break Versioning][breakver]. The version numbers follow a `<major>.<mino
[breakver]: https://github.com/ptaoussanis/encore/blob/master/BREAK-VERSIONING.md [breakver]: https://github.com/ptaoussanis/encore/blob/master/BREAK-VERSIONING.md
## 0.3.2-SNAPSHOT ## 0.3.9 (2019-06-16)
### `reitit-ring`
* Added async support for `default-options-handler` on `reitit-ring`, fixes [#293](https://github.com/metosin/reitit/issues/293)
## 0.3.8 (2019-06-15)
* Updated dependencies: * Updated dependencies:
```clj ```clj
[metosin/spec-tools "0.9.1"] is available but we use "0.9.0" [metosin/schema-tools "0.12.0"] is available but we use "0.11.0"
[metosin/spec-tools "0.9.3"] is available but we use "0.9.2"
[metosin/jsonista "0.2.3"] is available but we use "0.2.2"
```
### `reitit-core`
* Schema coercion supports transformtatins from keywords->clojure, via [schema-tools](https://github.com/metosin/schema-tools).
* Add support for explixit selection of router path-parameter `:syntax`, fixes [#276](https://github.com/metosin/reitit/issues/276)
```clj
(require '[reitit.core :as r])
;; default
(-> (r/router
["http://localhost:8080/api/user/{id}" ::user-by-id])
(r/match-by-path "http://localhost:8080/api/user/123"))
;#Match{:template "http://localhost:8080/api/user/{id}",
; :data {:name :user/user-by-id},
; :result nil,
; :path-params {:id "123", :8080 ":8080"},
; :path "http://localhost:8080/api/user/123"}
;; just bracket-syntax
(-> (r/router
["http://localhost:8080/api/user/{id}" ::user-by-id]
{:syntax :bracket})
(r/match-by-path "http://localhost:8080/api/user/123"))
;#Match{:template "http://localhost:8080/api/user/{id}",
; :data {:name :user/user-by-id},
; :result nil,
; :path-params {:id "123"},
; :path "http://localhost:8080/api/user/123"}
```
## 0.3.7 (2019-05-25)
### `reitit-pedestal`
* Fixed Pedestal Interceptor coercion bug, see [#285](https://github.com/metosin/reitit/issues/285).
## 0.3.6 (2019-05-23)
* Fixed [a zillion typos](https://github.com/metosin/reitit/pull/281) in docs by [Marcus Spiegel](https://github.com/malesch).
### `reitit-ring`
* Fix on `reitit.ring/create-default-handler` to support overriding just some handlers, fixes [#283](https://github.com/metosin/reitit/issues/283), by [Daniel Sunnerek](https://github.com/kardan).
## 0.3.5 (2019-05-22)
### `reitit-core`
* **MAJOR**: Fix bug in Java Trie (since 0.3.0!), [which made invalid path parameter parsing in concurrent requests](https://github.com/metosin/reitit/issues/277). All Trie implementation classes are final from now on.
## 0.3.4 (2019-05-20)
### `reitit-core`
* Spec problems are [reported correctly in coercion](https://github.com/metosin/reitit/pull/275) by [Kevin W. van Rooijen](https://github.com/kwrooijen).
## 0.3.3 (2019-05-16)
* Better error messages on route data merge error:
```clj
(ns user
(:require [reitit.core :as r]
[schema.core :as s]
[reitit.dev.pretty :as pretty]))
(r/router
["/kikka"
{:parameters {:body {:id s/Str}}}
["/kakka"
{:parameters {:body [s/Str]}}]]
{:exception pretty/exception})
; -- Router creation failed -------------------------------------------- user:7 --
;
; Error merging route-data:
;
; -- On route -----------------------
;
; /kikka/kakka
;
; -- Exception ----------------------
;
; Don't know how to create ISeq from: java.lang.Class
;
; {:parameters {:body {:id java.lang.String}}}
;
; {:parameters {:body [java.lang.String]}}
;
; https://cljdoc.org/d/metosin/reitit/CURRENT/doc/basics/route-data
;
; --------------------------------------------------------------------------------
```
## 0.3.2 (2019-05-13)
* Updated dependencies:
```clj
[metosin/spec-tools "0.9.2"] is available but we use "0.9.0"
[metosin/muuntaja "0.6.4"] is available but we use "0.6.3" [metosin/muuntaja "0.6.4"] is available but we use "0.6.3"
[fipp "0.6.18"] is available but we use "0.6.17"
[lambdaisland/deep-diff "0.0-47"] is available but we use "0.0-25" [lambdaisland/deep-diff "0.0-47"] is available but we use "0.0-25"
``` ```
* Updated guides on [Error Messages](https://metosin.github.io/reitit/basics/error_messages.html) & [Route-data Validation](https://metosin.github.io/reitit/basics/route_data_validation.html)
### `reitit-core`
* new options `:reitit.spec/wrap` to wrap top-level route data specs when spec validation is enabled. Using `spec-tools.spell/closed` closes top-level specs.
* Updated swagger-examples to easily enable closed spec validation
```clj
(require '[reitit.core :as r])
(require '[reitit.spec :as rs])
(require '[reitit.dev.pretty :as pretty)
(require '[spec-tools.spell :as spell])
(require '[clojure.spec.alpha :as s])
(s/def ::description string?)
(r/router
["/api" {:summary "kikka"}]
{:validate rs/validate
:spec (s/merge ::rs/default-data (s/keys :req-un [::description]))
::rs/wrap spell/closed
:exception pretty/exception})
```
![closed](./doc/images/closed-spec1.png)
### `reitit-frontend`
* add support for html5 links inside Shadow DOM by [Antti Leppänen](https://github.com/fraxu).
* lot's of React-router [examples](./examples) ported in, thanks to [Valtteri Harmainen](https://github.com/vharmain)
### `reitit.pedestal` ### `reitit.pedestal`
* Automatically coerce Sieppari-style 1-arity `:error` handlers into Pedestal-style 2-arity `:error` handlers. Thanks to [Mathieu MARCHANDISE](https://github.com/vielmath). * Automatically coerce Sieppari-style 1-arity `:error` handlers into Pedestal-style 2-arity `:error` handlers. Thanks to [Mathieu MARCHANDISE](https://github.com/vielmath).
### `reitit-middleware`
* `reitit.ring.middleware.dev/print-request-diffs` prints also response diffs.
<img src="https://user-images.githubusercontent.com/567532/56895987-3e54ea80-6a93-11e9-80ee-9ba6f8896db6.png">
## 0.3.1 (2019-03-18) ## 0.3.1 (2019-03-18)
* Recompiled with Java8 as target, fixes [#241](https://github.com/metosin/reitit/issues/241). * Recompiled with Java8 as target, fixes [#241](https://github.com/metosin/reitit/issues/241).
@ -367,7 +516,7 @@ We use [Break Versioning][breakver]. The version numbers follow a `<major>.<mino
### `reitit-spec` ### `reitit-spec`
* Latest features from [spec-tools](https://github.com/metosin/spec-tools) * Latest features from [spec-tools](https://github.com/metosin/spec-tools)
* Swagger enchancements * Swagger enhancements
* Better spec coercion via `st/coerce` using spec walking & inference: many simple specs (core predicates, `spec-tools.core/spec`, `s/and`, `s/or`, `s/coll-of`, `s/keys`, `s/map-of`, `s/nillable` and `s/every`) can be transformed without needing spec to be wrapped. Fallbacks to old conformed based approach. * Better spec coercion via `st/coerce` using spec walking & inference: many simple specs (core predicates, `spec-tools.core/spec`, `s/and`, `s/or`, `s/coll-of`, `s/keys`, `s/map-of`, `s/nillable` and `s/every`) can be transformed without needing spec to be wrapped. Fallbacks to old conformed based approach.
* [example app](https://github.com/metosin/reitit/blob/master/examples/ring-spec-swagger/src/example/server.clj). * [example app](https://github.com/metosin/reitit/blob/master/examples/ring-spec-swagger/src/example/server.clj).
@ -390,7 +539,7 @@ We use [Break Versioning][breakver]. The version numbers follow a `<major>.<mino
(fn [request] (fn [request]
(handler (update request ::acc (fnil conj []) id)))) (handler (update request ::acc (fnil conj []) id))))
(defn handler [{:keys [::acc]}] (defn handler [{::keys [acc]}]
{:status 200, :body (conj acc :handler)}) {:status 200, :body (conj acc :handler)})
(def app (def app
@ -479,7 +628,7 @@ We use [Break Versioning][breakver]. The version numbers follow a `<major>.<mino
## 0.2.0 (2018-09-03) ## 0.2.0 (2018-09-03)
Sample apps demonstraing the current status of `reitit`: Sample apps demonstrating the current status of `reitit`:
* [`reitit-ring` with coercion, swagger and default middleware](https://github.com/metosin/reitit/blob/master/examples/ring-swagger/src/example/server.clj) * [`reitit-ring` with coercion, swagger and default middleware](https://github.com/metosin/reitit/blob/master/examples/ring-swagger/src/example/server.clj)
* [`reitit-frontend`, the easy way](https://github.com/metosin/reitit/blob/master/examples/frontend/src/frontend/core.cljs) * [`reitit-frontend`, the easy way](https://github.com/metosin/reitit/blob/master/examples/frontend/src/frontend/core.cljs)

View file

@ -17,13 +17,13 @@ If you have questions about contributing or about reitit in general, join the [#
* Fork the repository on Github * Fork the repository on Github
* Create a topic branch from where you want to base your work (usually the master branch) * Create a topic branch from where you want to base your work (usually the master branch)
* Check the formatting rules from existing code (no trailing whitepace, mostly default indentation) * Check the formatting rules from existing code (no trailing whitespace, mostly default indentation)
* Ensure any new code is well-tested, and if possible, any issue fixed is covered by one or more new tests * Ensure any new code is well-tested, and if possible, any issue fixed is covered by one or more new tests
* Verify that all tests pass using `./scripts/test.sh clj` and `./scripts/test.sh cljs`. * Verify that all tests pass using `./scripts/test.sh clj` and `./scripts/test.sh cljs`.
* Push your code to your fork of the repository * Push your code to your fork of the repository
* Make a Pull Request * Make a Pull Request
For more deveploment instructions, [see the manual](https://cljdoc.org/d/metosin/reitit/CURRENT/doc/misc/development-instructions). For more development instructions, [see the manual](https://cljdoc.org/d/metosin/reitit/CURRENT/doc/misc/development-instructions).
## Commit messages ## Commit messages

View file

@ -8,12 +8,13 @@ A fast data-driven router for Clojure(Script).
* Bi-directional routing * Bi-directional routing
* [Pluggable coercion](https://metosin.github.io/reitit/coercion/coercion.html) ([schema](https://github.com/plumatic/schema) & [clojure.spec](https://clojure.org/about/spec)) * [Pluggable coercion](https://metosin.github.io/reitit/coercion/coercion.html) ([schema](https://github.com/plumatic/schema) & [clojure.spec](https://clojure.org/about/spec))
* Helpers for [ring](https://metosin.github.io/reitit/ring/ring.html), [http](https://metosin.github.io/reitit/http/interceptors.html), [pedestal](https://metosin.github.io/reitit/http/pedestal.html) & [frontend](https://metosin.github.io/reitit/frontend/basics.html) * Helpers for [ring](https://metosin.github.io/reitit/ring/ring.html), [http](https://metosin.github.io/reitit/http/interceptors.html), [pedestal](https://metosin.github.io/reitit/http/pedestal.html) & [frontend](https://metosin.github.io/reitit/frontend/basics.html)
* Friendly [Error Messages](https://metosin.github.io/reitit/basics/error_messages.html)
* Extendable * Extendable
* Modular * Modular
* [Fast](https://metosin.github.io/reitit/performance.html) * [Fast](https://metosin.github.io/reitit/performance.html)
Posts: Presentations:
* [Reitit, The Ancient Art of Data-Driven](https://www.slideshare.net/mobile/metosin/reitit-clojurenorth-2019-141438093) * [Reitit, The Ancient Art of Data-Driven](https://www.slideshare.net/mobile/metosin/reitit-clojurenorth-2019-141438093), Clojure/North 2019, [video](https://youtu.be/cSntRGAjPiM)
* [Faster and Friendlier Routing with Reitit 0.3.0](https://www.metosin.fi/blog/faster-and-friendlier-routing-with-reitit030/) * [Faster and Friendlier Routing with Reitit 0.3.0](https://www.metosin.fi/blog/faster-and-friendlier-routing-with-reitit030/)
* [Welcome Reitit 0.2.0!](https://www.metosin.fi/blog/reitit020/) * [Welcome Reitit 0.2.0!](https://www.metosin.fi/blog/reitit020/)
* [Data-Driven Ring with Reitit](https://www.metosin.fi/blog/reitit-ring/) * [Data-Driven Ring with Reitit](https://www.metosin.fi/blog/reitit-ring/)
@ -48,7 +49,7 @@ There is [#reitit](https://clojurians.slack.com/messages/reitit/) in [Clojurians
All main modules bundled: All main modules bundled:
```clj ```clj
[metosin/reitit "0.3.1"] [metosin/reitit "0.3.9"]
``` ```
Optionally, the parts can be required separately. Optionally, the parts can be required separately.
@ -152,8 +153,8 @@ Roadmap is mostly written in [issues](https://github.com/metosin/reitit/issues).
## Special thanks ## Special thanks
* Existing Clojure(Script) routing libs, expecially to * Existing Clojure(Script) routing libs, especially to
[Ataraxy](https://github.com/weavejester/ataraxy), [Bide](https://github.com/funcool/bide), [Bidi](https://github.com/juxt/bidi), [calfpath](https://github.com/ikitommi/calfpath), [Compojure](https://github.com/weavejester/compojure) and [Ataraxy](https://github.com/weavejester/ataraxy), [Bide](https://github.com/funcool/bide), [Bidi](https://github.com/juxt/bidi), [calfpath](https://github.com/ikitommi/calfpath), [Compojure](https://github.com/weavejester/compojure), [Keechma](https://keechma.com/) and
[Pedestal](https://github.com/pedestal/pedestal/tree/master/route). [Pedestal](https://github.com/pedestal/pedestal/tree/master/route).
* [Compojure-api](https://github.com/metosin/compojure-api), [Kekkonen](https://github.com/metosin/kekkonen), [Ring-swagger](https://github.com/metosin/ring-swagger) and [Yada](https://github.com/juxt/yada) and for ideas, coercion & stuff. * [Compojure-api](https://github.com/metosin/compojure-api), [Kekkonen](https://github.com/metosin/kekkonen), [Ring-swagger](https://github.com/metosin/ring-swagger) and [Yada](https://github.com/juxt/yada) and for ideas, coercion & stuff.
* [Schema](https://github.com/plumatic/schema) and [clojure.spec](https://clojure.org/about/spec) for the validation part. * [Schema](https://github.com/plumatic/schema) and [clojure.spec](https://clojure.org/about/spec) for the validation part.

View file

@ -8,6 +8,7 @@
* Bi-directional routing * Bi-directional routing
* [Pluggable coercion](./coercion/coercion.md) ([schema](https://github.com/plumatic/schema) & [clojure.spec](https://clojure.org/about/spec)) * [Pluggable coercion](./coercion/coercion.md) ([schema](https://github.com/plumatic/schema) & [clojure.spec](https://clojure.org/about/spec))
* Helpers for [ring](./ring/ring.md), [http](./http/interceptors.md), [pedestal](./http/pedestal.md) & [frontend](./frontend/basics.md) * Helpers for [ring](./ring/ring.md), [http](./http/interceptors.md), [pedestal](./http/pedestal.md) & [frontend](./frontend/basics.md)
* Friendly [Error Messages](./basics/error_messages.md)
* Extendable * Extendable
* Modular * Modular
* [Fast](performance.md) * [Fast](performance.md)
@ -39,7 +40,7 @@ There is [#reitit](https://clojurians.slack.com/messages/reitit/) in [Clojurians
All bundled: All bundled:
```clj ```clj
[metosin/reitit "0.3.1"] [metosin/reitit "0.3.9"]
``` ```
Optionally, the parts can be required separately. Optionally, the parts can be required separately.

View file

@ -13,6 +13,7 @@
* [Route Data](basics/route_data.md) * [Route Data](basics/route_data.md)
* [Route Data Validation](basics/route_data_validation.md) * [Route Data Validation](basics/route_data_validation.md)
* [Route Conflicts](basics/route_conflicts.md) * [Route Conflicts](basics/route_conflicts.md)
* [Error Messages](basics/error_messages.md)
## Coercion ## Coercion

View file

@ -2,7 +2,7 @@
Data-driven approach in `reitit` allows us to compose routes, route data, route specs, middleware and interceptors chains. We can compose routers too. This is needed to achieve dynamic routing like in [Compojure](https://github.com/weavejester/compojure). Data-driven approach in `reitit` allows us to compose routes, route data, route specs, middleware and interceptors chains. We can compose routers too. This is needed to achieve dynamic routing like in [Compojure](https://github.com/weavejester/compojure).
## Immutatability ## Immutability
Once a router is created, the routing tree is immutable and cannot be changed. To change the routing, we need to create a new router with changed routes and/or options. For this, the `Router` protocol exposes it's resolved routes via `r/routes` and options via `r/options`. Once a router is created, the routing tree is immutable and cannot be changed. To change the routing, we need to create a new router with changed routes and/or options. For this, the `Router` protocol exposes it's resolved routes via `r/routes` and options via `r/options`.
@ -336,7 +336,7 @@ Can we make the nester routing faster? Sure. We could use the Router `:compile`
### When to use nested routers? ### When to use nested routers?
Nesting routers is not trivial and because of that, should be avoided. For dynamic (request-time) route generation, it's the only choise. For other cases, nested routes are most likely a better option. Nesting routers is not trivial and because of that, should be avoided. For dynamic (request-time) route generation, it's the only choice. For other cases, nested routes are most likely a better option.
Let's re-create the previous example with normal route nesting/composition. Let's re-create the previous example with normal route nesting/composition.

View file

@ -2,12 +2,13 @@
Routers can be configured via options. The following options are available for the `reitit.core/router`: Routers can be configured via options. The following options are available for the `reitit.core/router`:
| key | description | | key | description
|--------------|-------------| |--------------|-------------
| `:path` | Base-path for routes | `:path` | Base-path for routes
| `:routes` | Initial resolved routes (default `[]`) | `:routes` | Initial resolved routes (default `[]`)
| `:data` | Initial route data (default `{}`) | `:data` | Initial route data (default `{}`)
| `:spec` | clojure.spec definition for a route data, see `reitit.spec` on how to use this | `:spec` | clojure.spec definition for a route data, see `reitit.spec` on how to use this
| `:syntax` | Path-parameter syntax as keyword or set of keywords (default #{:bracket :colon})
| `:expand` | Function of `arg opts => data` to expand route arg to route data (default `reitit.core/expand`) | `:expand` | Function of `arg opts => data` to expand route arg to route data (default `reitit.core/expand`)
| `:coerce` | Function of `route opts => route` to coerce resolved route, can throw or return `nil` | `:coerce` | Function of `route opts => route` to coerce resolved route, can throw or return `nil`
| `:compile` | Function of `route opts => result` to compile a route handler | `:compile` | Function of `route opts => result` to compile a route handler

View file

@ -1,6 +1,6 @@
# Dev Worklfow # Dev Workflow
Many applications will require the routes to span multiple namespaces. It is quite easy to do so with reitit, but we might hit a problem during developement. Many applications will require the routes to span multiple namespaces. It is quite easy to do so with reitit, but we might hit a problem during development.
## An example ## An example
@ -122,7 +122,7 @@ Notice that the name is now correct, without reloading every namespace under the
## Why is this a crude solution ? ## Why is this a crude solution ?
The astute reader will have noticed that we're recompiling the full routing tree on every invocation. While this solution is practical during developement, it goes contrary to the performance goals of reitit. The astute reader will have noticed that we're recompiling the full routing tree on every invocation. While this solution is practical during development, it goes contrary to the performance goals of reitit.
We need a way to only do this once at production time. We need a way to only do this once at production time.

View file

@ -0,0 +1,54 @@
# Error Messages
All exceptions thrown in router creation are caught, formatted and rethrown by the `reitit.core/router` function. Exception formatting is done by the exception formatter defined by the `:exception` router option.
## Default Errors
The default exception formatting uses `reitit.exception/exception`. It produces single-color, partly human-readable, error messages.
```clj
(require '[reitit.core :as r])
(r/router
[["/ping"]
["/:user-id/orders"]
["/bulk/:bulk-id"]
["/public/*path"]
["/:version/status"]])
```
![Pretty error](../images/conflicts1.png)
## Pretty Errors
```clj
[metosin/reitit-dev "0.3.9"]
```
For human-readable and developer-friendly exception messages, there is `reitit.dev.pretty/exception` (in the `reitit-dev` module). It is inspired by the lovely errors messages of [ELM](https://elm-lang.org/blog/compiler-errors-for-humans) and [ETA](https://twitter.com/jyothsnasrin/status/1037703436043603968) and uses [fipp](https://github.com/brandonbloom/fipp), [expound](https://github.com/bhb/expound) and [spell-spec](https://github.com/bhauman/spell-spec) for most of heavy lifting.
```clj
(require '[reitit.dev.pretty :as pretty])
(r/router
[["/ping"]
["/:user-id/orders"]
["/bulk/:bulk-id"]
["/public/*path"]
["/:version/status"]]
{:exception pretty/exception})
```
![Pretty error](../images/conflicts2.png)
## Extending
Behind the scenes, both error formatters are backed by a multimethod, so they are easy to extend.
## More examples
See the [validating route data](route_data_validation.md) page.
## Runtime Exception
See [Exception Handling with Ring](../ring/exceptions.md).

View file

@ -4,7 +4,7 @@ Route data can be anything, so it's easy to go wrong. Accidentally adding a `:ro
To fail fast, we could use the custom `:coerce` and `:compile` hooks to apply data validation and throw exceptions on first sighted problem. To fail fast, we could use the custom `:coerce` and `:compile` hooks to apply data validation and throw exceptions on first sighted problem.
But there is a better way. Router has a `:validation` hook to validate the whole route tree after it's successfuly compiled. It expects a 2-arity function `routes opts => ()` that can side-effect in case of validation errors. But there is a better way. Router has a `:validation` hook to validate the whole route tree after it's successfully compiled. It expects a 2-arity function `routes opts => ()` that can side-effect in case of validation errors.
## clojure.spec ## clojure.spec
@ -20,7 +20,7 @@ A Router with invalid route data:
; #object[reitit.core$...] ; #object[reitit.core$...]
``` ```
Fails fast with `clojure.spec` validation turned on: Failing fast with `clojure.spec` validation turned on:
```clj ```clj
(require '[reitit.spec :as rs]) (require '[reitit.spec :as rs])
@ -40,22 +40,36 @@ Fails fast with `clojure.spec` validation turned on:
``` ```
### Pretty errors
Turning on [Pretty Errors](error_messages.md#pretty-errors) will give much nicer error messages:
```clj
(require '[reitit.dev.pretty :as pretty])
(r/router
["/api" {:handler "identity"}]
{:validate rs/validate
:exception pretty/exception})
```
![Pretty error](../images/pretty-error.png)
### Customizing spec validation ### Customizing spec validation
`rs/validate` reads the following router options: `rs/validate` reads the following router options:
| key | description | | key | description |
| ---------------|-------------| | --------------------|-------------|
| `:spec` | the spec to verify the route data (default `::rs/default-data`) | `:spec` | the spec to verify the route data (default `::rs/default-data`)
| `::rs/explain` | custom explain function (default `clojure.spec.alpha/explain-str`) | `:reitit.spec/wrap` | function of `spec => spec` to wrap all route specs
**NOTE**: `clojure.spec` implicitly validates all values with fully-qualified keys if specs exist with the same name. **NOTE**: `clojure.spec` implicitly validates all values with fully-qualified keys if specs exist with the same name.
Below is an example of using [expound](https://github.com/bhb/expound) to pretty-print route data problems. Invalid spec value:
```clj ```clj
(require '[clojure.spec.alpha :as s]) (require '[clojure.spec.alpha :as s])
(require '[expound.alpha :as e])
(s/def ::role #{:admin :manager}) (s/def ::role #{:admin :manager})
(s/def ::roles (s/coll-of ::role :into #{})) (s/def ::roles (s/coll-of ::role :into #{}))
@ -63,67 +77,45 @@ Below is an example of using [expound](https://github.com/bhb/expound) to pretty
(r/router (r/router
["/api" {:handler identity ["/api" {:handler identity
::roles #{:adminz}}] ::roles #{:adminz}}]
{::rs/explain e/expound-str {:validate rs/validate
:validate rs/validate}) :exception pretty/exception})
; CompilerException clojure.lang.ExceptionInfo: Invalid route data:
;
; -- On route -----------------------
;
; "/api"
;
; -- Spec failed --------------------
;
; {:handler ..., :user/roles #{:adminz}}
; ^^^^^^^
;
; should be one of: `:admin`,`:manager`
;
; -- Relevant specs -------
;
; :user/role:
; #{:admin :manager}
; :user/roles:
; (clojure.spec.alpha/coll-of :user/role :into #{})
; :reitit.spec/default-data:
; (clojure.spec.alpha/keys
; :opt-un
; [:reitit.spec/name :reitit.spec/handler])
;
; -------------------------
; Detected 1 error
;
; {:problems (#reitit.spec.Problem{:path "/api", :scope nil, :data {:handler #object[clojure.core$identity 0x15b59b0e "clojure.core$identity@15b59b0e"], :user/roles #{:adminz}}, :spec :reitit.spec/default-data, :problems #:clojure.spec.alpha{:problems ({:path [:user/roles], :pred #{:admin :manager}, :val :adminz, :via [:reitit.spec/default-data :user/roles :user/role], :in [:user/roles 0]}), :spec :reitit.spec/default-data, :value {:handler #object[clojure.core$identity 0x15b59b0e "clojure.core$identity@15b59b0e"], :user/roles #{:adminz}}}})}, compiling: ...
``` ```
Explicitly requiring a `::roles` key in a route data: ![Invalid Role Error](../images/invalid_roles.png)
## Closed Specs
To fail-fast on non-defined and misspelled keys on route data, we can close the specs using `:reitit.spec/wrap` options with value of `spec-tools.spell/closed` that closed the top-level specs.
Requiring a`:description` and validating using closed specs:
```clj
(require '[spec-tools.spell :as spell])
(s/def ::description string?)
(r/router
["/api" {:summary "kikka"}]
{:validate rs/validate
:spec (s/merge ::rs/default-data
(s/keys :req-un [::description]))
::rs/wrap spell/closed
:exception pretty/exception})
```
![Closed Spec error](../images/closed-spec1.png)
It catches also typing errors:
```clj ```clj
(r/router (r/router
["/api" {:handler identity}] ["/api" {:descriptionz "kikka"}]
{:spec (s/merge (s/keys :req [::roles]) ::rs/default-data) {:validate rs/validate
::rs/explain e/expound-str :spec (s/merge ::rs/default-data
:validate rs/validate}) (s/keys :req-un [::description]))
; CompilerException clojure.lang.ExceptionInfo: Invalid route data: ::rs/wrap spell/closed
; :exception pretty/exception})
; -- On route -----------------------
;
; "/api"
;
; -- Spec failed --------------------
;
; {:handler
; #object[clojure.core$identity 0x15b59b0e "clojure.core$identity@15b59b0e"]}
;
; should contain key: `:user/roles`
;
; | key | spec |
; |-------------+----------------------------------------|
; | :user/roles | (coll-of #{:admin :manager} :into #{}) |
;
;
;
; -------------------------
; Detected 1 error
;
; {:problems (#reitit.spec.Problem{:path "/api", :scope nil, :data {:handler #object[clojure.core$identity 0x15b59b0e "clojure.core$identity@15b59b0e"]}, :spec #object[clojure.spec.alpha$merge_spec_impl$reify__2124 0x7461744b "clojure.spec.alpha$merge_spec_impl$reify__2124@7461744b"], :problems #:clojure.spec.alpha{:problems ({:path [], :pred (clojure.core/fn [%] (clojure.core/contains? % :user/roles)), :val {:handler #object[clojure.core$identity 0x15b59b0e "clojure.core$identity@15b59b0e"]}, :via [], :in []}), :spec #object[clojure.spec.alpha$merge_spec_impl$reify__2124 0x7461744b "clojure.spec.alpha$merge_spec_impl$reify__2124@7461744b"], :value {:handler #object[clojure.core$identity 0x15b59b0e "clojure.core$identity@15b59b0e"]}}})}, compiling:(/Users/tommi/projects/metosin/reitit/test/cljc/reitit/spec_test.cljc:151:1)
``` ```
![Closed Spec error](../images/closed-spec2.png)

View file

@ -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. Routes can be wrapped in vectors and lists and `nil` routes are ignored.
Paths can have path-parameters (`:id`) or catch-all-parameters (`*path`). Since version `0.3.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. Paths can have path-parameters (`:id`) or catch-all-parameters (`*path`). Parameters can also be wrapped in brackets, enabling use of qualified keywords `{user/id}`, `{*user/path}`. By default, both syntaxes are supported, see [configuring routers](../advanced/configuring_routers.md) on how to change this.
### Examples ### Examples
@ -129,3 +129,36 @@ Routes are just data, so it's easy to create them programmatically:
; ["/add-user" {:post {:interceptors [add-user]}}] ; ["/add-user" {:post {:interceptors [add-user]}}]
; ["/add-order" {:post {:interceptors [add-order]}}])] ; ["/add-order" {:post {:interceptors [add-order]}}])]
``` ```
### Explicit path-parameter syntax
Router options `:syntax` allows the path-parameter syntax to be explicitely defined. It takes a keyword or set of keywords as a value. Valid values are `:colon` and `:bracket`. Default value is `#{:colon :bracket}`.
With defaults:
```clj
(-> (r/router
["http://localhost:8080/api/user/{id}" ::user-by-id])
(r/match-by-path "http://localhost:8080/api/user/123"))
;#Match{:template "http://localhost:8080/api/user/{id}",
; :data {:name :user/user-by-id},
; :result nil,
; :path-params {:id "123", :8080 ":8080"},
; :path "http://localhost:8080/api/user/123"}
```
Supporting only `:bracket` syntax:
```clj
(require '[reitit.core :as r])
(-> (r/router
["http://localhost:8080/api/user/{id}" ::user-by-id]
{:syntax :bracket})
(r/match-by-path "http://localhost:8080/api/user/123"))
;#Match{:template "http://localhost:8080/api/user/{id}",
; :data {:name :user/user-by-id},
; :result nil,
; :path-params {:id "123"},
; :path "http://localhost:8080/api/user/123"}
```

View file

@ -1,11 +1,18 @@
{:cljdoc/include-namespaces-from-dependencies {:cljdoc/include-namespaces-from-dependencies
[metosin/reitit [metosin/reitit
metosin/reitit-core metosin/reitit-core
metosin/reitit-dev
metosin/reitit-ring metosin/reitit-ring
metosin/reitit-http
metosin/reitit-middleware
metosin/reitit-interceptors
metosin/reitit-spec metosin/reitit-spec
metosin/reitit-schema metosin/reitit-schema
metosin/reitit-swagger metosin/reitit-swagger
metosin/reitit-swagger-ui], metosin/reitit-swagger-ui
metosin/reitit-frontend
metosin/reitit-sieppari
metosin/reitit-pedestal]
:cljdoc.doc/tree :cljdoc.doc/tree
[["Introduction" {:file "doc/README.md"}] [["Introduction" {:file "doc/README.md"}]
["Basics" {} ["Basics" {}
@ -15,7 +22,8 @@
["Name-based Routing" {:file "doc/basics/name_based_routing.md"}] ["Name-based Routing" {:file "doc/basics/name_based_routing.md"}]
["Route Data" {:file "doc/basics/route_data.md"}] ["Route Data" {:file "doc/basics/route_data.md"}]
["Route Data Validation" {:file "doc/basics/route_data_validation.md"}] ["Route Data Validation" {:file "doc/basics/route_data_validation.md"}]
["Route Conflicts" {:file "doc/basics/route_conflicts.md"}]] ["Route Conflicts" {:file "doc/basics/route_conflicts.md"}]
["Error Messages" {:file "doc/basics/error_messages.md"}]]
["Coercion" {} ["Coercion" {}
["Coercion Explained" {:file "doc/coercion/coercion.md"}] ["Coercion Explained" {:file "doc/coercion/coercion.md"}]
["Plumatic Schema" {:file "doc/coercion/schema_coercion.md"}] ["Plumatic Schema" {:file "doc/coercion/schema_coercion.md"}]
@ -31,8 +39,9 @@
["Data-driven Middleware" {:file "doc/ring/data_driven_middleware.md"}] ["Data-driven Middleware" {:file "doc/ring/data_driven_middleware.md"}]
["Transforming Middleware Chain" {:file "doc/ring/transforming_middleware_chain.md"}] ["Transforming Middleware Chain" {:file "doc/ring/transforming_middleware_chain.md"}]
["Middleware Registry" {:file "doc/ring/middleware_registry.md"}] ["Middleware Registry" {:file "doc/ring/middleware_registry.md"}]
["Exception Handling with Ring" {:file "doc/ring/exceptions.md"}]
["Default Middleware" {:file "doc/ring/default_middleware.md"}] ["Default Middleware" {:file "doc/ring/default_middleware.md"}]
["Ring Coercion" {:file "doc/ring/coercion.md"}] ["Pluggable Coercion" {:file "doc/ring/coercion.md"}]
["Route Data Validation" {:file "doc/ring/route_data_validation.md"}] ["Route Data Validation" {:file "doc/ring/route_data_validation.md"}]
["Compiling Middleware" {:file "doc/ring/compiling_middleware.md"}] ["Compiling Middleware" {:file "doc/ring/compiling_middleware.md"}]
["Swagger Support" {:file "doc/ring/swagger.md"}] ["Swagger Support" {:file "doc/ring/swagger.md"}]

View file

@ -4,7 +4,7 @@ The [clojure.spec](https://clojure.org/guides/spec) library specifies the struct
## Warning ## Warning
`clojure.spec` by itself doesn't support coercion. `reitit` uses [spec-tools](https://github.com/metosin/spec-tools) that adds coercion to spec. Like `clojure.spec`, it's alpha as it leans both on spec walking and `clojure.spec.alpha/conform`, which is concidered a spec internal, that might be changed or removed later. `clojure.spec` by itself doesn't support coercion. `reitit` uses [spec-tools](https://github.com/metosin/spec-tools) that adds coercion to spec. Like `clojure.spec`, it's alpha as it leans both on spec walking and `clojure.spec.alpha/conform`, which is considered a spec internal, that might be changed or removed later.
## Usage ## Usage

View file

@ -125,7 +125,7 @@ We can use a helper function `reitit.coercion/coerce!` to do the actual coercion
; {:path {:company "metosin", :user-id 123}} ; {:path {:company "metosin", :user-id 123}}
``` ```
We get the coerced paremeters back. If a coercion fails, a typed (`:reitit.coercion/request-coercion`) ExceptionInfo is thrown, with data about the actual error: We get the coerced parameters back. If a coercion fails, a typed (`:reitit.coercion/request-coercion`) ExceptionInfo is thrown, with data about the actual error:
```clj ```clj
(coercion/coerce! (coercion/coerce!

View file

@ -1,7 +1,7 @@
# Default Interceptors # Default Interceptors
```clj ```clj
[metosin/reitit-interceptors "0.3.1"] [metosin/reitit-interceptors "0.3.9"]
``` ```
Just like the [ring default middleware](../ring/default_middleware.md), but for interceptors. Just like the [ring default middleware](../ring/default_middleware.md), but for interceptors.

View file

@ -5,7 +5,7 @@ Reitit also support for [interceptors](http://pedestal.io/reference/interceptors
## Reitit-http ## Reitit-http
```clj ```clj
[metosin/reitit-http "0.3.1"] [metosin/reitit-http "0.3.9"]
``` ```
An module for http-routing using interceptors instead of middleware. Builds on top of the [`reitit-ring`](../ring/ring.md) module having all the same features. An module for http-routing using interceptors instead of middleware. Builds on top of the [`reitit-ring`](../ring/ring.md) module having all the same features.

View file

@ -3,7 +3,7 @@
[Pedestal](http://pedestal.io/) is a backend web framework for Clojure. `reitit-pedestal` provides an alternative routing engine for Pedestal. [Pedestal](http://pedestal.io/) is a backend web framework for Clojure. `reitit-pedestal` provides an alternative routing engine for Pedestal.
```clj ```clj
[metosin/reitit-pedestal "0.3.1"] [metosin/reitit-pedestal "0.3.9"]
``` ```
Why should one use reitit instead of the Pedestal [default routing](http://pedestal.io/reference/routing-quick-reference)? Why should one use reitit instead of the Pedestal [default routing](http://pedestal.io/reference/routing-quick-reference)?
@ -26,8 +26,8 @@ A minimalistic example on how to to swap the default-router with a reitit router
```clj ```clj
; [io.pedestal/pedestal.service "0.5.5"] ; [io.pedestal/pedestal.service "0.5.5"]
; [io.pedestal/pedestal.jetty "0.5.5"] ; [io.pedestal/pedestal.jetty "0.5.5"]
; [metosin/reitit-pedestal "0.3.1"] ; [metosin/reitit-pedestal "0.3.9"]
; [metosin/reitit "0.3.1"] ; [metosin/reitit "0.3.9"]
(require '[io.pedestal.http :as server]) (require '[io.pedestal.http :as server])
(require '[reitit.pedestal :as pedestal]) (require '[reitit.pedestal :as pedestal])

View file

@ -1,14 +1,14 @@
# Sieppari # Sieppari
```clj ```clj
[metosin/reitit-sieppari "0.3.1"] [metosin/reitit-sieppari "0.3.9"]
``` ```
[Sieppari](https://github.com/metosin/sieppari) is a new and fast interceptor implementation for Clojure, with pluggable async supporting [core.async](https://github.com/clojure/core.async), [Manifold](https://github.com/ztellman/manifold) and [Promesa](http://funcool.github.io/promesa/latest). [Sieppari](https://github.com/metosin/sieppari) is a new and fast interceptor implementation for Clojure, with pluggable async supporting [core.async](https://github.com/clojure/core.async), [Manifold](https://github.com/ztellman/manifold) and [Promesa](http://funcool.github.io/promesa/latest).
To use Sieppari with `reitit-http`, we need to attach a `reitit.interceptor.sieppari/executor` to a `http-router` to compile and execute the interceptor chains. Reitit and Sieppari share the same interceptor model, so all reitit default interceptors work seamlesly together. To use Sieppari with `reitit-http`, we need to attach a `reitit.interceptor.sieppari/executor` to a `http-router` to compile and execute the interceptor chains. Reitit and Sieppari share the same interceptor model, so all reitit default interceptors work seamlessly together.
We can use both syncronous ring and [async-ring](https://www.booleanknot.com/blog/2016/07/15/asynchronous-ring.html) with Sieppari. We can use both synchronous ring and [async-ring](https://www.booleanknot.com/blog/2016/07/15/asynchronous-ring.html) with Sieppari.
## Synchronous Ring ## Synchronous Ring

View file

@ -65,7 +65,7 @@ There is an extra option in http-router (actually, in the underlying interceptor
### Printing Context Diffs ### Printing Context Diffs
```clj ```clj
[metosin/reitit-interceptors "0.3.1"] [metosin/reitit-interceptors "0.3.9"]
``` ```
Using `reitit.http.interceptors.dev/print-context-diffs` transformation, the context diffs between each interceptor are printed out to the console. To use it, add the following router option: Using `reitit.http.interceptors.dev/print-context-diffs` transformation, the context diffs between each interceptor are printed out to the console. To use it, add the following router option:

BIN
doc/images/closed-spec1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

BIN
doc/images/closed-spec2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

BIN
doc/images/conflicts1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

BIN
doc/images/conflicts2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

BIN
doc/images/pretty-error.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

View file

@ -33,4 +33,4 @@ And apply the middleware like this:
wrap-hidden-method]}) ;; our hidden method wrapper wrap-hidden-method]}) ;; our hidden method wrapper
``` ```
(NOTE: This middleware must be placed here and not inside the route data given to `reitit.ring/handler`. (NOTE: This middleware must be placed here and not inside the route data given to `reitit.ring/handler`.
This is so that our middleware is applied before reitit matches the request with a spesific handler using the wrong method.) This is so that our middleware is applied before reitit matches the request with a specific handler using the wrong method.)

View file

@ -215,7 +215,7 @@ Spec problems are exposed as-is into request & response coercion errors, enablin
### Optimizations ### Optimizations
The coercion middleware are [compiled againts a route](compiling_middleware.md). In the middleware compilation step the actual coercer implementations are constructed for the defined models. Also, the middleware doesn't mount itself if a route doesn't have `:coercion` and `:parameters` or `:responses` defined. The coercion middleware are [compiled against a route](compiling_middleware.md). In the middleware compilation step the actual coercer implementations are constructed for the defined models. Also, the middleware doesn't mount itself if a route doesn't have `:coercion` and `:parameters` or `:responses` defined.
We can query the compiled middleware chain for the routes: We can query the compiled middleware chain for the routes:

View file

@ -6,7 +6,7 @@ Reitit defines middleware as data:
1. Middleware can be defined as first-class data entries 1. Middleware can be defined as first-class data entries
2. Middleware can be mounted as a [duct-style](https://github.com/duct-framework/duct/wiki/Configuration) vector (of middleware) 2. Middleware can be mounted as a [duct-style](https://github.com/duct-framework/duct/wiki/Configuration) vector (of middleware)
4. Middleware can be optimized & [compiled](compiling_middleware.md) againt an endpoint 4. Middleware can be optimized & [compiled](compiling_middleware.md) against an endpoint
3. Middleware chain can be transformed by the router 3. Middleware chain can be transformed by the router
## Middleware as data ## Middleware as data
@ -53,7 +53,7 @@ The following produce identical middleware runtime function.
(require '[reitit.middleware :as middleware]) (require '[reitit.middleware :as middleware])
(def wrap2 (def wrap2
(middleware/create (middleware/map->Middleware
{:name ::wrap2 {:name ::wrap2
:description "Middleware that does things." :description "Middleware that does things."
:wrap wrap})) :wrap wrap}))
@ -66,7 +66,7 @@ The following produce identical middleware runtime function.
```clj ```clj
(require '[reitit.ring :as ring]) (require '[reitit.ring :as ring])
(defn handler [{:keys [::acc]}] (defn handler [{::keys [acc]}]
{:status 200, :body (conj acc :handler)}) {:status 200, :body (conj acc :handler)})
(def app (def app

View file

@ -30,7 +30,7 @@ Setting the default-handler as a second argument to `ring-handler`:
; {:status 404, :body ""} ; {:status 404, :body ""}
``` ```
To get more correct http error responses, `ring/create-default-handler` can be used. It differentiates `:not-found` (no route matched), `:method-not-accepted` (no method matched) and `:not-acceptable` (handler returned `nil`). To get more correct http error responses, `ring/create-default-handler` can be used. It differentiates `:not-found` (no route matched), `:method-not-allowed` (no method matched) and `:not-acceptable` (handler returned `nil`).
With defaults: With defaults:

View file

@ -1,7 +1,7 @@
# Default Middleware # Default Middleware
```clj ```clj
[metosin/reitit-middleware "0.3.1"] [metosin/reitit-middleware "0.3.9"]
``` ```
Any Ring middleware can be used with `reitit-ring`, but using data-driven middleware is preferred as they are easier to manage and in many cases, yield better performance. `reitit-middleware` contains a set of common ring middleware, lifted into data-driven middleware. Any Ring middleware can be used with `reitit-ring`, but using data-driven middleware is preferred as they are easier to manage and in many cases, yield better performance. `reitit-middleware` contains a set of common ring middleware, lifted into data-driven middleware.
@ -10,7 +10,7 @@ Any Ring middleware can be used with `reitit-ring`, but using data-driven middle
* [Exception Handling](#exception-handling) * [Exception Handling](#exception-handling)
* [Content Negotiation](#content-negotiation) * [Content Negotiation](#content-negotiation)
* [Multipart Request Handling](#multipart-request-handling) * [Multipart Request Handling](#multipart-request-handling)
* [Inspecting Requests](#inspecting-requests) * [Inspecting Middleware Chain](#inspecting-middleware-chain)
## Parameters Handling ## Parameters Handling
@ -21,107 +21,7 @@ Any Ring middleware can be used with `reitit-ring`, but using data-driven middle
## Exception Handling ## Exception Handling
A polished version of [compojure-api](https://github.com/metosin/compojure-api) exception handling. Catches all exceptions and invokes configured exception handler. See [Exception Handling with Ring](exceptions.md).
```clj
(require '[reitit.ring.middleware.exception :as exception])
```
### `exception/exception-middleware`
A preconfigured middleware using `exception/default-handlers`. Catches:
* Request & response [Coercion](coercion.md) exceptions
* [Muuntaja](https://github.com/metosin/muuntaja) decode exceptions
* Exceptions with `:type` of `:reitit.ring/response`, returning `:response` key from `ex-data`.
* Safely all other exceptions
```clj
(require '[reitit.ring :as ring])
(def app
(ring/ring-handler
(ring/router
["/fail" (fn [_] (throw (Exception. "fail")))]
{:data {:middleware [exception/exception-middleware]}})))
(app {:request-method :get, :uri "/fail"})
;{:status 500
; :body {:type "exception"
; :class "java.lang.Exception"}}
```
### `exception/create-exception-middleware`
Creates the exception-middleware with custom options. Takes a map of `identifier => exception request => response` that is used to select the exception handler for the thrown/raised exception identifier. Exception idenfier is either a `Keyword` or a Exception Class.
The following handlers are available by default:
| key | description
|--------------------------------------|-------------
| `:reitit.ring/response` | value in ex-data key `:response` will be returned
| `:muuntaja/decode` | handle Muuntaja decoding exceptions
| `:reitit.coercion/request-coercion` | request coercion errors (http 400 response)
| `:reitit.coercion/response-coercion` | response coercion errors (http 500 response)
| `::exception/default` | a default exception handler if nothing else matched (default `exception/default-handler`).
| `::exception/wrap` | a 3-arity handler to wrap the actual handler `handler exception request => response` (no default).
The handler is selected from the options map by exception idenfitifier in the following lookup order:
1) `:type` of exception ex-data
2) Class of exception
3) `:type` ancestors of exception ex-data
4) Super Classes of exception
5) The ::default handler
```clj
;; type hierarchy
(derive ::error ::exception)
(derive ::failure ::exception)
(derive ::horror ::exception)
(defn handler [message exception request]
{:status 500
:body {:message message
:exception (.getClass exception)
:data (ex-data exception)
:uri (:uri request)}})
(def exception-middleware
(exception/create-exception-middleware
(merge
exception/default-handlers
{;; ex-data with :type ::error
::error (partial handler "error")
;; ex-data with ::exception or ::failure
::exception (partial handler "exception")
;; SQLException and all it's child classes
java.sql.SQLException (partial handler "sql-exception")
;; override the default handler
::exception/default (partial handler "default")
;; print stack-traces for all exceptions
::exception/wrap (fn [handler e request]
(println "ERROR" (pr-str (:uri request)))
(handler e request))})))
(def app
(ring/ring-handler
(ring/router
["/fail" (fn [_] (throw (ex-info "fail" {:type ::failue})))]
{:data {:middleware [exception-middleware]}})))
(app {:request-method :get, :uri "/fail"})
; ERROR "/fail"
; => {:status 500,
; :body {:message "default"
; :exception clojure.lang.ExceptionInfo
; :data {:type :user/failue}
; :uri "/fail"}}
```
## Content Negotiation ## Content Negotiation
@ -226,9 +126,9 @@ Expected route data:
* `multipart/multipart-middleware` a preconfigured middleware for multipart handling * `multipart/multipart-middleware` a preconfigured middleware for multipart handling
* `multipart/create-multipart-middleware` to generate with custom configuration * `multipart/create-multipart-middleware` to generate with custom configuration
## Inspecting Requests ## Inspecting Middleware Chain
`reitit.ring.middleware.dev/print-request-diffs` is a [middleware chain transforming function](transforming_middleware_chain.md). It prints a request diff between each middleware. To use it, add the following router option: `reitit.ring.middleware.dev/print-request-diffs` is a [middleware chain transforming function](transforming_middleware_chain.md). It prints a request and response diff between each middleware. To use it, add the following router option:
```clj ```clj
:reitit.middleware/transform reitit.ring.middleware.dev/print-request-diffs :reitit.middleware/transform reitit.ring.middleware.dev/print-request-diffs

View file

@ -9,7 +9,7 @@ Example middleware to guard routes based on user roles:
(require '[clojure.set :as set]) (require '[clojure.set :as set])
(defn wrap-enforce-roles [handler] (defn wrap-enforce-roles [handler]
(fn [{:keys [::roles] :as request}] (fn [{::keys [roles] :as request}]
(let [required (some-> request (ring/get-match) :data ::roles)] (let [required (some-> request (ring/get-match) :data ::roles)]
(if (and (seq required) (not (set/subset? required roles))) (if (and (seq required) (not (set/subset? required roles)))
{:status 403, :body "forbidden"} {:status 403, :body "forbidden"}

107
doc/ring/exceptions.md Normal file
View file

@ -0,0 +1,107 @@
# Exception Handling with Ring
```clj
[metosin/reitit-middleware "0.3.9"]
```
Exceptions thrown in router creation can be [handled with custom exception handler](../basics/error_messages.md). By default, exceptions thrown at runtime from a handler or a middleware are not caught by the `reitit.ring/ring-handler`. A good practise is a have an top-level exception handler to log and format the errors for clients.
```clj
(require '[reitit.ring.middleware.exception :as exception])
```
### `exception/exception-middleware`
A preconfigured middleware using `exception/default-handlers`. Catches:
* Request & response [Coercion](coercion.md) exceptions
* [Muuntaja](https://github.com/metosin/muuntaja) decode exceptions
* Exceptions with `:type` of `:reitit.ring/response`, returning `:response` key from `ex-data`.
* Safely all other exceptions
```clj
(require '[reitit.ring :as ring])
(def app
(ring/ring-handler
(ring/router
["/fail" (fn [_] (throw (Exception. "fail")))]
{:data {:middleware [exception/exception-middleware]}})))
(app {:request-method :get, :uri "/fail"})
;{:status 500
; :body {:type "exception"
; :class "java.lang.Exception"}}
```
### `exception/create-exception-middleware`
Creates the exception-middleware with custom options. Takes a map of `identifier => exception request => response` that is used to select the exception handler for the thrown/raised exception identifier. Exception identifier is either a `Keyword` or a Exception Class.
The following handlers are available by default:
| key | description
|--------------------------------------|-------------
| `:reitit.ring/response` | value in ex-data key `:response` will be returned
| `:muuntaja/decode` | handle Muuntaja decoding exceptions
| `:reitit.coercion/request-coercion` | request coercion errors (http 400 response)
| `:reitit.coercion/response-coercion` | response coercion errors (http 500 response)
| `::exception/default` | a default exception handler if nothing else matched (default `exception/default-handler`).
| `::exception/wrap` | a 3-arity handler to wrap the actual handler `handler exception request => response` (no default).
The handler is selected from the options map by exception identifier in the following lookup order:
1) `:type` of exception ex-data
2) Class of exception
3) `:type` ancestors of exception ex-data
4) Super Classes of exception
5) The ::default handler
```clj
;; type hierarchy
(derive ::error ::exception)
(derive ::failure ::exception)
(derive ::horror ::exception)
(defn handler [message exception request]
{:status 500
:body {:message message
:exception (.getClass exception)
:data (ex-data exception)
:uri (:uri request)}})
(def exception-middleware
(exception/create-exception-middleware
(merge
exception/default-handlers
{;; ex-data with :type ::error
::error (partial handler "error")
;; ex-data with ::exception or ::failure
::exception (partial handler "exception")
;; SQLException and all it's child classes
java.sql.SQLException (partial handler "sql-exception")
;; override the default handler
::exception/default (partial handler "default")
;; print stack-traces for all exceptions
::exception/wrap (fn [handler e request]
(println "ERROR" (pr-str (:uri request)))
(handler e request))})))
(def app
(ring/ring-handler
(ring/router
["/fail" (fn [_] (throw (ex-info "fail" {:type ::failue})))]
{:data {:middleware [exception-middleware]}})))
(app {:request-method :get, :uri "/fail"})
; ERROR "/fail"
; => {:status 500,
; :body {:message "default"
; :exception clojure.lang.ExceptionInfo
; :data {:type :user/failue}
; :uri "/fail"}}
```

View file

@ -12,7 +12,7 @@ Below is an example how to do reverse routing from a ring handler:
(ring/ring-handler (ring/ring-handler
(ring/router (ring/router
[["/users" [["/users"
{:get (fn [{:keys [::r/router]}] {:get (fn [{::r/keys [router]}]
{:status 200 {:status 200
:body (for [i (range 10)] :body (for [i (range 10)]
{:uri (-> router {:uri (-> router

View file

@ -5,7 +5,7 @@
Read more about the [Ring Concepts](https://github.com/ring-clojure/ring/wiki/Concepts). Read more about the [Ring Concepts](https://github.com/ring-clojure/ring/wiki/Concepts).
```clj ```clj
[metosin/reitit-ring "0.3.1"] [metosin/reitit-ring "0.3.9"]
``` ```
## `reitit.ring/ring-router` ## `reitit.ring/ring-router`
@ -155,7 +155,7 @@ A middleware and a handler:
(fn [request] (fn [request]
(handler (update request ::acc (fnil conj []) id)))) (handler (update request ::acc (fnil conj []) id))))
(defn handler [{:keys [::acc]}] (defn handler [{::keys [acc]}]
{:status 200, :body (conj acc :handler)}) {:status 200, :body (conj acc :handler)})
``` ```

View file

@ -155,7 +155,7 @@ Let's reuse the `wrap-enforce-roles` from [Dynamic extensions](dynamic_extension
(s/def ::roles (s/coll-of ::role :into #{})) (s/def ::roles (s/coll-of ::role :into #{}))
(defn wrap-enforce-roles [handler] (defn wrap-enforce-roles [handler]
(fn [{:keys [::roles] :as request}] (fn [{::keys [roles] :as request}]
(let [required (some-> request (ring/get-match) :data ::roles)] (let [required (some-> request (ring/get-match) :data ::roles)]
(if (and (seq required) (not (set/subset? required roles))) (if (and (seq required) (not (set/subset? required roles)))
{:status 403, :body "forbidden"} {:status 403, :body "forbidden"}
@ -265,7 +265,7 @@ Or even flatten the routes:
::rs/explain e/expound-str}))) ::rs/explain e/expound-str})))
``` ```
The common Middleware can also be pushed to the router, here cleanly separing behavior and data: The common Middleware can also be pushed to the router, here cleanly separating behavior and data:
```clj ```clj
(def app (def app

View file

@ -1,7 +1,7 @@
# Swagger Support # Swagger Support
``` ```
[metosin/reitit-swagger "0.3.1"] [metosin/reitit-swagger "0.3.9"]
``` ```
Reitit supports [Swagger2](https://swagger.io/) documentation, thanks to [schema-tools](https://github.com/metosin/schema-tools) and [spec-tools](https://github.com/metosin/spec-tools). Documentation is extracted from route definitions, coercion `:parameters` and `:responses` and from a set of new documentation keys. Reitit supports [Swagger2](https://swagger.io/) documentation, thanks to [schema-tools](https://github.com/metosin/schema-tools) and [spec-tools](https://github.com/metosin/spec-tools). Documentation is extracted from route definitions, coercion `:parameters` and `:responses` and from a set of new documentation keys.
@ -29,7 +29,7 @@ Coercion keys also contribute to the docs:
| key | description | | key | description |
| --------------|-------------| | --------------|-------------|
| :parameters | optional input parameters for a route, in a format defined by the coercion | :parameters | optional input parameters for a route, in a format defined by the coercion
| :responses | optional descriptions of responess, in a format defined by coercion | :responses | optional descriptions of responses, in a format defined by coercion
There is a `reitit.swagger.swagger-feature`, which acts as both a `Middleware` and an `Interceptor` that is not participating in any request processing - it just defines the route data specs for the routes it's mounted to. It is only needed if the [route data validation](route_data_validation.md) is turned on. There is a `reitit.swagger.swagger-feature`, which acts as both a `Middleware` and an `Interceptor` that is not participating in any request processing - it just defines the route data specs for the routes it's mounted to. It is only needed if the [route data validation](route_data_validation.md) is turned on.
@ -44,7 +44,7 @@ If you need to post-process the generated spec, just wrap the handler with a cus
[Swagger-ui](https://github.com/swagger-api/swagger-ui) is a user interface to visualize and interact with the Swagger specification. To make things easy, there is a pre-integrated version of the swagger-ui as a separate module. [Swagger-ui](https://github.com/swagger-api/swagger-ui) is a user interface to visualize and interact with the Swagger specification. To make things easy, there is a pre-integrated version of the swagger-ui as a separate module.
``` ```
[metosin/reitit-swagger-ui "0.3.1"] [metosin/reitit-swagger-ui "0.3.9"]
``` ```
`reitit.swagger-ui/create-swagger-ui-hander` can be used to create a ring-handler to serve the swagger-ui. It accepts the following options: `reitit.swagger-ui/create-swagger-ui-hander` can be used to create a ring-handler to serve the swagger-ui. It accepts the following options:
@ -55,7 +55,7 @@ If you need to post-process the generated spec, just wrap the handler with a cus
| :root | optional resource root, defaults to `"swagger-ui"` | :root | optional resource root, defaults to `"swagger-ui"`
| :url | path to swagger endpoint, defaults to `/swagger.json` | :url | path to swagger endpoint, defaults to `/swagger.json`
| :path | optional path to mount the handler to. Works only if mounted outside of a router. | :path | optional path to mount the handler to. Works only if mounted outside of a router.
| :config | parameters passed to swaggger-ui as-is. See [the docs](https://github.com/swagger-api/swagger-ui/tree/2.x#parameters) | :config | parameters passed to swagger-ui as-is. See [the docs](https://github.com/swagger-api/swagger-ui/tree/2.x#parameters)
We use swagger-ui from [ring-swagger-ui](https://github.com/metosin/ring-swagger-ui), which can be easily configured from routing application. It stores files `swagger-ui` in the resource classpath. We use swagger-ui from [ring-swagger-ui](https://github.com/metosin/ring-swagger-ui), which can be easily configured from routing application. It stores files `swagger-ui` in the resource classpath.

View file

@ -12,7 +12,7 @@ There is an extra option in ring-router (actually, in the underlying middleware-
(fn [request] (fn [request]
(handler (update request ::acc (fnil conj []) id)))) (handler (update request ::acc (fnil conj []) id))))
(defn handler [{:keys [::acc]}] (defn handler [{::keys [acc]}]
{:status 200, :body (conj acc :handler)}) {:status 200, :body (conj acc :handler)})
(def app (def app
@ -59,7 +59,7 @@ There is an extra option in ring-router (actually, in the underlying middleware-
### Printing Request Diffs ### Printing Request Diffs
```clj ```clj
[metosin/reitit-middleware "0.3.1"] [metosin/reitit-middleware "0.3.9"]
``` ```
Using `reitit.ring.middleware.dev/print-request-diffs` transformation, the request diffs between each middleware are printed out to the console. To use it, add the following router option: Using `reitit.ring.middleware.dev/print-request-diffs` transformation, the request diffs between each middleware are printed out to the console. To use it, add the following router option:

View file

@ -10,9 +10,9 @@
[ring "1.7.1"] [ring "1.7.1"]
[hiccup "1.0.5"] [hiccup "1.0.5"]
[org.clojure/clojurescript "1.10.439"] [org.clojure/clojurescript "1.10.439"]
[metosin/reitit "0.3.1"] [metosin/reitit "0.3.9"]
[metosin/reitit-schema "0.3.1"] [metosin/reitit-schema "0.3.9"]
[metosin/reitit-frontend "0.3.1"] [metosin/reitit-frontend "0.3.9"]
;; Just for pretty printting the match ;; Just for pretty printting the match
[fipp "0.6.14"]] [fipp "0.6.14"]]

View file

@ -140,7 +140,7 @@
(fn [new-match] (fn [new-match]
(swap! state (fn [state] (swap! state (fn [state]
(if new-match (if new-match
;; Only run the controllers, which are likely to call authentcated APIs, ;; Only run the controllers, which are likely to call authenticated APIs,
;; if user has been authenticated. ;; if user has been authenticated.
;; Alternative solution could be to always run controllers, ;; Alternative solution could be to always run controllers,
;; check authentication status in each controller, or check authentication status in API calls. ;; check authentication status in each controller, or check authentication status in API calls.

View file

@ -10,9 +10,9 @@
[ring "1.7.1"] [ring "1.7.1"]
[hiccup "1.0.5"] [hiccup "1.0.5"]
[org.clojure/clojurescript "1.10.439"] [org.clojure/clojurescript "1.10.439"]
[metosin/reitit "0.3.1"] [metosin/reitit "0.3.9"]
[metosin/reitit-schema "0.3.1"] [metosin/reitit-schema "0.3.9"]
[metosin/reitit-frontend "0.3.1"] [metosin/reitit-frontend "0.3.9"]
;; Just for pretty printting the match ;; Just for pretty printting the match
[fipp "0.6.14"]] [fipp "0.6.14"]]

View file

@ -0,0 +1,13 @@
# reitit-frontend example
## Usage
```clj
> lein figwheel
```
Go with browser to http://localhost:3449
## License
Copyright © 2018 Metosin Oy

View file

@ -0,0 +1,62 @@
(defproject frontend "0.1.0-SNAPSHOT"
:description "FIXME: write description"
:url "http://example.com/FIXME"
:license {:name "Eclipse Public License"
:url "http://www.eclipse.org/legal/epl-v10.html"}
:dependencies [[org.clojure/clojure "1.10.0"]
[ring-server "0.5.0"]
[reagent "0.8.1"]
[ring "1.7.1"]
[hiccup "1.0.5"]
[org.clojure/clojurescript "1.10.520"]
[metosin/reitit "0.3.9"]
[metosin/reitit-spec "0.3.9"]
[metosin/reitit-frontend "0.3.9"]
;; Just for pretty printting the match
[fipp "0.6.14"]]
:plugins [[lein-cljsbuild "1.1.7"]
[lein-figwheel "0.5.18"]
[cider/cider-nrepl "0.21.1"]]
:repl-options {:nrepl-middleware [cider.piggieback/wrap-cljs-repl]}
:source-paths ["src"]
:resource-paths ["resources" "target/cljsbuild"]
:profiles
{:dev
{:dependencies
[[binaryage/devtools "0.9.10"]
[cider/piggieback "0.4.0"]
[figwheel-sidecar "0.5.18"]]}}
:cljsbuild
{:builds
[{:id "app"
:figwheel true
:source-paths ["src"]
:compiler {:main "frontend.core"
:asset-path "/js/out"
:output-to "target/cljsbuild/public/js/app.js"
:output-dir "target/cljsbuild/public/js/out"
:source-map true
:optimizations :none
:pretty-print true
:preloads [devtools.preload]
:aot-cache true}}
{:id "min"
:source-paths ["src"]
:compiler {:output-to "target/cljsbuild/public/js/app.js"
:output-dir "target/cljsbuild/public/js"
:source-map "target/cljsbuild/public/js/app.js.map"
:optimizations :advanced
:pretty-print false
:aot-cache true}}]}
:figwheel {:http-server-root "public"
:server-port 3449
:nrepl-port 7002
;; Server index.html for all routes for HTML5 routing
:ring-handler backend.server/handler})

View file

@ -0,0 +1,10 @@
<!DOCTYPE html>
<html>
<head>
<title>Reitit frontend example</title>
</head>
<body>
<div id="app"></div>
<script src="/js/app.js"></script>
</body>
</html>

View file

@ -0,0 +1,11 @@
(ns backend.server
(:require [clojure.java.io :as io]
[ring.util.response :as resp]
[ring.middleware.content-type :as content-type]))
(def handler
(-> (fn [request]
(or (resp/resource-response (:uri request) {:root "public"})
(-> (resp/resource-response "index.html" {:root "public"})
(resp/content-type "text/html"))))
content-type/wrap-content-type))

View file

@ -0,0 +1,147 @@
(ns frontend.core
(:require [clojure.string :as string]
[fipp.edn :as fedn]
[reagent.core :as r]
[reitit.coercion :as rc]
[reitit.coercion.spec :as rss]
[reitit.frontend :as rf]
[reitit.frontend.easy :as rfe]
[spec-tools.data-spec :as ds]))
;; Components similar to react-router `Link`, `NavLink` and `Redirect`
;; with Reitit frontend.
(defn home-page []
[:div
[:h2 "Welcome to frontend"]
[:p "This is home page"]])
(defn about-page []
[:div
[:h2 "About frontend"]
[:p "This is about page"]])
(defn redirect!
"If `push` is truthy, previous page will be left in history."
[{:keys [to path-params query-params push]}]
(if push
(rfe/push-state to path-params query-params)
(rfe/replace-state to path-params query-params)))
(defn Redirect
"Component that only causes a redirect side-effect."
[props]
(r/create-class
{:component-did-mount (fn [this] (redirect! (r/props this)))
:component-did-update (fn [this [_ prev-props]]
(if (not= (r/props this) prev-props)
(redirect! (r/props this))))
:render (fn [this] nil)}))
(defn item-page [match]
(let [{:keys [path query]} (:parameters match)
{:keys [id]} path]
(if (< id 1)
[Redirect {:to ::frontpage}]
[:div
[:h2 "Selected item " id]
(when (:foo query)
[:p "Optional foo query param: " (:foo query)])])))
(def routes
[["/"
{:name ::frontpage
:view home-page}]
["/about"
{:name ::about
:view about-page}]
["/item/:id"
{:name ::item
:view item-page
:parameters
{:path {:id int?}
:query {(ds/opt :foo) keyword?}}}]])
(def router
(rf/router routes {:data {:coercion rss/coercion}}))
(defonce current-match (r/atom nil))
(defn- resolve-href
[to path-params query-params]
(if (keyword? to)
(rfe/href to path-params query-params)
(let [match (rf/match-by-path router to)
route (-> match :data :name)
params (or path-params (:path-params match))
query (or query-params (:query-params match))]
(if match
(rfe/href route params query)
to))))
(defn Link
[{:keys [to path-params query-params active]} & children]
(let [href (resolve-href to path-params query-params)]
(into
[:a {:href href} (when active "> ")] ;; Apply styles or whatever
children)))
(defn- name-matches?
[name path-params match]
(and (= name (-> match :data :name))
(= (not-empty path-params)
(-> match :parameters :path not-empty))))
(defn- url-matches?
[url match]
(= (-> url (string/split #"\?") first)
(:path match)))
(defn NavLink
[{:keys [to path-params] :as props} & children]
(let [active (or (name-matches? to path-params @current-match)
(url-matches? to @current-match))]
[Link (assoc props :active active) children]))
(defn current-page []
[:div
[:h4 "Link"]
[:ul
[:li [Link {:to ::frontpage} "Frontpage"]]
[:li [Link {:to "/about"} "About"]]
[:li [Link {:to ::item :path-params {:id 1}} "Item 1"]]
[:li [Link {:to "/item/2?foo=bar"} "Item 2"]]
[:li [Link {:to "/item/-1"} "Item -1 (redirects to frontpage)"]]
[:li [Link {:to "http://www.google.fi"} "Google"]]]
[:h4 "NavLink"]
[:ul
[:li [NavLink {:to ::frontpage} "Frontpage"]]
[:li [NavLink {:to "/about"} "About"]]
[:li [NavLink {:to ::item :path-params {:id 1}} "Item 1"]]
[:li [NavLink {:to "/item/2?foo=bar"} "Item 2"]]
[:li [NavLink {:to "/item/-1"} "Item -1 (redirects to frontpage)"]]
[:li [NavLink {:to "http://www.google.fi"} "Google"]]]
(if @current-match
(let [view (:view (:data @current-match))]
[view @current-match]))
[:pre (with-out-str (fedn/pprint @current-match))]])
(defn init! []
(rfe/start!
router
(fn [m] (reset! current-match m))
;; set to false to enable HistoryAPI
{:use-fragment true})
(r/render [current-page] (.getElementById js/document "app")))
(init!)
(comment
(rf/match-by-path router "/about?kissa=1&koira=true")
(rf/match-by-path router "/item/2?kissa=1&koira=true"))

View file

@ -0,0 +1,13 @@
# reitit-frontend example
## Usage
```clj
> lein figwheel
```
Go with browser to http://localhost:3449
## License
Copyright © 2018 Metosin Oy

View file

@ -0,0 +1,62 @@
(defproject frontend "0.1.0-SNAPSHOT"
:description "FIXME: write description"
:url "http://example.com/FIXME"
:license {:name "Eclipse Public License"
:url "http://www.eclipse.org/legal/epl-v10.html"}
:dependencies [[org.clojure/clojure "1.10.0"]
[ring-server "0.5.0"]
[reagent "0.8.1"]
[ring "1.7.1"]
[hiccup "1.0.5"]
[org.clojure/clojurescript "1.10.520"]
[metosin/reitit "0.3.9"]
[metosin/reitit-spec "0.3.9"]
[metosin/reitit-frontend "0.3.9"]
;; Just for pretty printting the match
[fipp "0.6.14"]]
:plugins [[lein-cljsbuild "1.1.7"]
[lein-figwheel "0.5.18"]
[cider/cider-nrepl "0.21.1"]]
:repl-options {:nrepl-middleware [cider.piggieback/wrap-cljs-repl]}
:source-paths ["src"]
:resource-paths ["resources" "target/cljsbuild"]
:profiles
{:dev
{:dependencies
[[binaryage/devtools "0.9.10"]
[cider/piggieback "0.4.0"]
[figwheel-sidecar "0.5.18"]]}}
:cljsbuild
{:builds
[{:id "app"
:figwheel true
:source-paths ["src"]
:compiler {:main "frontend.core"
:asset-path "/js/out"
:output-to "target/cljsbuild/public/js/app.js"
:output-dir "target/cljsbuild/public/js/out"
:source-map true
:optimizations :none
:pretty-print true
:preloads [devtools.preload]
:aot-cache true}}
{:id "min"
:source-paths ["src"]
:compiler {:output-to "target/cljsbuild/public/js/app.js"
:output-dir "target/cljsbuild/public/js"
:source-map "target/cljsbuild/public/js/app.js.map"
:optimizations :advanced
:pretty-print false
:aot-cache true}}]}
:figwheel {:http-server-root "public"
:server-port 3449
:nrepl-port 7002
;; Server index.html for all routes for HTML5 routing
:ring-handler backend.server/handler})

View file

@ -0,0 +1,10 @@
<!DOCTYPE html>
<html>
<head>
<title>Reitit frontend example</title>
</head>
<body>
<div id="app"></div>
<script src="/js/app.js"></script>
</body>
</html>

View file

@ -0,0 +1,11 @@
(ns backend.server
(:require [clojure.java.io :as io]
[ring.util.response :as resp]
[ring.middleware.content-type :as content-type]))
(def handler
(-> (fn [request]
(or (resp/resource-response (:uri request) {:root "public"})
(-> (resp/resource-response "index.html" {:root "public"})
(resp/content-type "text/html"))))
content-type/wrap-content-type))

View file

@ -0,0 +1,70 @@
(ns frontend.core
(:require [fipp.edn :as fedn]
[reagent.core :as r]
[reitit.coercion :as rc]
[reitit.coercion.spec :as rss]
[reitit.frontend :as rf]
[reitit.frontend.easy :as rfe]
[spec-tools.data-spec :as ds]))
;; Implementing conditional prompt on navigation with Reitit frontend.
(defn home-page []
[:div
[:h2 "Home"]
[:p "You will not be prompted to leave this page"]])
(defn prompt-page []
[:div
[:h2 "Prompt"]
[:p "You will be prompted to leave this page"]])
(def routes
[["/"
{:name ::home
:view home-page}]
["/prompt"
{:name ::prompt
:view prompt-page
;; Routes can contain arbitrary keys so we add custom :prompt
;; key here. See how it's handled in `on-navigate` function.
:prompt "Are you sure you want to leave?"
;; It would be possible to define a function here that resolves
;; whether prompting is needed or not but we'll keep it simple.
}]])
(def router
(rf/router routes {:data {:coercion rss/coercion}}))
(defonce current-match (r/atom nil))
(defn current-page []
[:div
[:ul
[:li [:a {:href (rfe/href ::home)} "Home"]]
[:li [:a {:href (rfe/href ::prompt)} "Prompt page"]]]
(if @current-match
(let [view (-> @current-match :data :view)]
[view @current-match]))
[:pre (with-out-str (fedn/pprint @current-match))]])
(defn on-navigate [m]
(if-let [prompt (and (not= @current-match m)
(-> @current-match :data :prompt))]
(if (js/window.confirm prompt) ;; Returns true if OK is pressed.
(reset! current-match m)
(.back js/window.history)) ;; Restore browser location
(reset! current-match m)))
(defn init! []
(rfe/start!
router
on-navigate
;; set to false to enable HistoryAPI
{:use-fragment true})
(r/render [current-page] (.getElementById js/document "app")))
(init!)

View file

@ -0,0 +1,26 @@
# frontend-re-frame
A [re-frame](https://github.com/Day8/re-frame) application designed to ... well, that part is up to you.
## Development Mode
### Run application:
```
lein clean
lein figwheel dev
```
Figwheel will automatically push cljs changes to the browser.
Wait a bit, then browse to [http://localhost:3449](http://localhost:3449).
## Production Build
To compile clojurescript to javascript:
```
lein clean
lein cljsbuild once min
```

View file

@ -0,0 +1,56 @@
(defproject frontend-re-frame "0.1.0-SNAPSHOT"
:dependencies [[org.clojure/clojure "1.10.0"]
[org.clojure/clojurescript "1.10.520"]
[metosin/reitit "0.3.9"]
[reagent "0.8.1"]
[re-frame "0.10.6"]]
:plugins [[lein-cljsbuild "1.1.7"]
[lein-figwheel "0.5.18"]
[cider/cider-nrepl "0.21.1"]]
:repl-options {:nrepl-middleware [cider.piggieback/wrap-cljs-repl]}
:min-lein-version "2.5.3"
:source-paths ["src/clj" "src/cljs"]
:clean-targets ^{:protect false} ["resources/public/js/compiled" "target"]
:figwheel
{:css-dirs ["resources/public/css"]
:server-port 3449
:nrepl-port 7002
:ring-handler backend.server/handler}
:profiles
{:dev
{:dependencies
[[binaryage/devtools "0.9.10"]
[cider/piggieback "0.4.0"]
[figwheel-sidecar "0.5.18"]]
:plugins [[lein-figwheel "0.5.18"]]}
:prod {}}
:cljsbuild
{:builds
[{:id "dev"
:source-paths ["src/cljs"]
:figwheel {:on-jsload "frontend-re-frame.core/mount-root"}
:compiler {:main frontend-re-frame.core
:output-to "resources/public/js/compiled/app.js"
:output-dir "resources/public/js/compiled/out"
:asset-path "js/compiled/out"
:source-map-timestamp true
:preloads [devtools.preload]
:external-config {:devtools/config {:features-to-install :all}}
}}
{:id "min"
:source-paths ["src/cljs"]
:compiler {:main frontend-re-frame.core
:output-to "resources/public/js/compiled/app.js"
:optimizations :advanced
:closure-defines {goog.DEBUG false}
:pretty-print false}}
]}
)

View file

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset='utf-8'>
</head>
<body>
<div id="app"></div>
<script src="js/compiled/app.js"></script>
<script>frontend_re_frame.core.init();</script>
</body>
</html>

View file

@ -0,0 +1,11 @@
(ns backend.server
(:require [clojure.java.io :as io]
[ring.util.response :as resp]
[ring.middleware.content-type :as content-type]))
(def handler
(-> (fn [request]
(or (resp/resource-response (:uri request) {:root "public"})
(-> (resp/resource-response "index.html" {:root "public"})
(resp/content-type "text/html"))))
content-type/wrap-content-type))

View file

@ -0,0 +1 @@
(ns frontend-re-frame.core)

View file

@ -0,0 +1,155 @@
(ns frontend-re-frame.core
(:require
[re-frame.core :as re-frame]
[reagent.core :as reagent]
[reitit.core :as r]
[reitit.coercion :as rc]
[reitit.coercion.spec :as rss]
[reitit.frontend :as rf]
[reitit.frontend.controllers :as rfc]
[reitit.frontend.easy :as rfe]))
;;; Events ;;;
(re-frame/reg-event-db
::initialize-db
(fn [_ _]
{:current-route nil}))
(re-frame/reg-event-fx
::navigate
(fn [db [_ route]]
;; See `navigate` effect in routes.cljs
{::navigate! route}))
(re-frame/reg-event-db
::navigated
(fn [db [_ new-match]]
(let [old-match (:current-route db)
controllers (rfc/apply-controllers (:controllers old-match) new-match)]
(assoc db :current-route (assoc new-match :controllers controllers)))))
;;; Subscriptions ;;;
(re-frame/reg-sub
::current-route
(fn [db]
(:current-route db)))
;;; Views ;;;
(defn home-page []
[:div
[:h1 "This is home page"]
[:button
;; Dispatch navigate event that triggers a (side)effect.
{:on-click #(re-frame/dispatch [::navigate ::sub-page2])}
"Go to sub-page 2"]])
(defn sub-page1 []
[:div
[:h1 "This is sub-page 1"]])
(defn sub-page2 []
[:div
[:h1 "This is sub-page 2"]])
;;; Effects ;;;
;; Triggering navigation from events.
(re-frame/reg-fx
::navigate!
(fn [k params query]
(rfe/push-state k params query)))
;;; Routes ;;;
(defn href
"Return relative url for given route. Url can be used in HTML links."
([k]
(href k nil nil))
([k params]
(href k params nil))
([k params query]
(rfe/href k params query)))
(def routes
["/"
[""
{:name ::home
:view home-page
:link-text "Home"
:controllers
[{;; Do whatever initialization needed for home page
;; I.e (re-frame/dispatch [::events/load-something-with-ajax])
:start (fn [& params](js/console.log "Entering home page"))
;; Teardown can be done here.
:stop (fn [& params] (js/console.log "Leaving home page"))}]}]
["sub-page1"
{:name ::sub-page1
:view sub-page1
:link-text "Sub page 1"
:controllers
[{:start (fn [& params] (js/console.log "Entering sub-page 1"))
:stop (fn [& params] (js/console.log "Leaving sub-page 1"))}]}]
["sub-page2"
{:name ::sub-page2
:view sub-page2
:link-text "Sub-page 2"
:controllers
[{:start (fn [& params] (js/console.log "Entering sub-page 2"))
:stop (fn [& params] (js/console.log "Leaving sub-page 2"))}]}]])
(defn on-navigate [new-match]
(when new-match
(re-frame/dispatch [::navigated new-match])))
(def router
(rf/router
routes
{:data {:coercion rss/coercion}}))
(defn init-routes! []
(js/console.log "initializing routes")
(rfe/start!
router
on-navigate
{:use-fragment true}))
(defn nav [{:keys [router current-route]}]
[:ul
(for [route-name (r/route-names router)
:let [route (r/match-by-name router route-name)
text (-> route :data :link-text)]]
[:li {:key route-name}
(when (= route-name (-> current-route :data :name))
"> ")
;; Create a normal links that user can click
[:a {:href (href route-name)} text]])])
(defn router-component [{:keys [router]}]
(let [current-route @(re-frame/subscribe [::current-route])]
[:div
[nav {:router router :current-route current-route}]
(when current-route
[(-> current-route :data :view)])]))
;;; Setup ;;;
(def debug? ^boolean goog.DEBUG)
(defn dev-setup []
(when debug?
(enable-console-print!)
(println "dev mode")))
(defn mount-root []
(re-frame/clear-subscription-cache!)
(init-routes!) ;; Reset routes on figwheel reload
(reagent/render [router-component {:router router}]
(.getElementById js/document "app")))
(defn ^:export init []
(re-frame/dispatch-sync [::initialize-db])
(dev-setup)
(mount-root))

View file

@ -10,9 +10,9 @@
[ring "1.7.1"] [ring "1.7.1"]
[hiccup "1.0.5"] [hiccup "1.0.5"]
[org.clojure/clojurescript "1.10.439"] [org.clojure/clojurescript "1.10.439"]
[metosin/reitit "0.3.1"] [metosin/reitit "0.3.9"]
[metosin/reitit-spec "0.3.1"] [metosin/reitit-spec "0.3.9"]
[metosin/reitit-frontend "0.3.1"] [metosin/reitit-frontend "0.3.9"]
;; Just for pretty printting the match ;; Just for pretty printting the match
[fipp "0.6.14"]] [fipp "0.6.14"]]

View file

@ -3,5 +3,5 @@
:dependencies [[org.clojure/clojure "1.10.0"] :dependencies [[org.clojure/clojure "1.10.0"]
[ring/ring-jetty-adapter "1.7.1"] [ring/ring-jetty-adapter "1.7.1"]
[aleph "0.4.6"] [aleph "0.4.6"]
[metosin/reitit "0.3.1"]] [metosin/reitit "0.3.9"]]
:repl-options {:init-ns example.server}) :repl-options {:init-ns example.server})

View file

@ -1,16 +1,19 @@
(ns example.server (ns example.server
(:require [reitit.ring :as ring] (:require [reitit.ring :as ring]
[reitit.http :as http] [reitit.http :as http]
[reitit.coercion.spec]
[reitit.swagger :as swagger] [reitit.swagger :as swagger]
[reitit.swagger-ui :as swagger-ui] [reitit.swagger-ui :as swagger-ui]
[reitit.http.coercion :as coercion] [reitit.http.coercion :as coercion]
[reitit.coercion.spec :as spec-coercion] [reitit.dev.pretty :as pretty]
[reitit.interceptor.sieppari :as sieppari]
[reitit.http.interceptors.parameters :as parameters] [reitit.http.interceptors.parameters :as parameters]
[reitit.http.interceptors.muuntaja :as muuntaja] [reitit.http.interceptors.muuntaja :as muuntaja]
[reitit.http.interceptors.exception :as exception] [reitit.http.interceptors.exception :as exception]
[reitit.http.interceptors.multipart :as multipart] [reitit.http.interceptors.multipart :as multipart]
[reitit.http.interceptors.dev :as dev] [reitit.http.interceptors.dev :as dev]
[reitit.interceptor.sieppari :as sieppari] [reitit.http.spec :as spec]
[spec-tools.spell :as spell]
[ring.adapter.jetty :as jetty] [ring.adapter.jetty :as jetty]
[aleph.http :as client] [aleph.http :as client]
[muuntaja.core :as m] [muuntaja.core :as m]
@ -72,7 +75,7 @@
"https://randomuser.me/api/" "https://randomuser.me/api/"
{:query-params {:seed seed, :results results}}) {:query-params {:seed seed, :results results}})
:body :body
(partial m/decode m/instance "application/json") (partial m/decode "application/json")
:results :results
(fn [results] (fn [results]
{:status 200 {:status 200
@ -109,10 +112,15 @@
{:status 200 {:status 200
:body {:total (- x y)}})}}]]] :body {:total (- x y)}})}}]]]
{;;:reitit.interceptor/transform dev/print-context-diffs {;:reitit.interceptor/transform dev/print-context-diffs ;; pretty context diffs
:data {:coercion spec-coercion/coercion ;;:validate spec/validate ;; enable spec validation for route data
;;:reitit.spec/wrap spell/closed ;; strict top-level validation
:exception pretty/exception
:data {:coercion reitit.coercion.spec/coercion
:muuntaja m/instance :muuntaja m/instance
:interceptors [;; query-params & form-params :interceptors [;; swagger feature
swagger/swagger-feature
;; query-params & form-params
(parameters/parameters-interceptor) (parameters/parameters-interceptor)
;; content-negotiation ;; content-negotiation
(muuntaja/format-negotiate-interceptor) (muuntaja/format-negotiate-interceptor)
@ -131,7 +139,8 @@
(ring/routes (ring/routes
(swagger-ui/create-swagger-ui-handler (swagger-ui/create-swagger-ui-handler
{:path "/" {:path "/"
:config {:validatorUrl nil}}) :config {:validatorUrl nil
:operationsSorter "alpha"}})
(ring/create-default-handler)) (ring/create-default-handler))
{:executor sieppari/executor})) {:executor sieppari/executor}))

View file

@ -5,5 +5,5 @@
[funcool/promesa "1.9.0"] [funcool/promesa "1.9.0"]
[manifold "0.1.8"] [manifold "0.1.8"]
[ring/ring-jetty-adapter "1.7.1"] [ring/ring-jetty-adapter "1.7.1"]
[metosin/reitit "0.3.1"]] [metosin/reitit "0.3.9"]]
:repl-options {:init-ns example.server}) :repl-options {:init-ns example.server})

View file

@ -2,4 +2,4 @@
:description "Reitit coercion with vanilla ring" :description "Reitit coercion with vanilla ring"
:dependencies [[org.clojure/clojure "1.10.0"] :dependencies [[org.clojure/clojure "1.10.0"]
[ring/ring-jetty-adapter "1.7.1"] [ring/ring-jetty-adapter "1.7.1"]
[metosin/reitit "0.3.1"]]) [metosin/reitit "0.3.9"]])

View file

@ -3,6 +3,6 @@
:dependencies [[org.clojure/clojure "1.10.0"] :dependencies [[org.clojure/clojure "1.10.0"]
[io.pedestal/pedestal.service "0.5.5"] [io.pedestal/pedestal.service "0.5.5"]
[io.pedestal/pedestal.jetty "0.5.5"] [io.pedestal/pedestal.jetty "0.5.5"]
[metosin/reitit-pedestal "0.3.1"] [metosin/reitit-pedestal "0.3.9"]
[metosin/reitit "0.3.1"]] [metosin/reitit "0.3.9"]]
:repl-options {:init-ns example.server}) :repl-options {:init-ns example.server})

View file

@ -1,16 +1,21 @@
(ns example.server (ns example.server
(:require [io.pedestal.http :as server] (:require [io.pedestal.http :as server]
[reitit.pedestal :as pedestal]
[reitit.ring :as ring] [reitit.ring :as ring]
[reitit.http :as http] [reitit.http :as http]
[reitit.coercion.spec]
[reitit.swagger :as swagger] [reitit.swagger :as swagger]
[reitit.swagger-ui :as swagger-ui] [reitit.swagger-ui :as swagger-ui]
[reitit.http.coercion :as coercion] [reitit.http.coercion :as coercion]
[reitit.coercion.spec :as spec-coercion] [reitit.dev.pretty :as pretty]
[reitit.http.interceptors.parameters :as parameters] [reitit.http.interceptors.parameters :as parameters]
[reitit.http.interceptors.muuntaja :as muuntaja] [reitit.http.interceptors.muuntaja :as muuntaja]
[reitit.http.interceptors.exception :as exception]
[reitit.http.interceptors.multipart :as multipart] [reitit.http.interceptors.multipart :as multipart]
[reitit.http.interceptors.dev :as dev] [reitit.http.interceptors.dev :as dev]
[reitit.http.spec :as spec]
[spec-tools.spell :as spell]
[io.pedestal.http :as server]
[reitit.pedestal :as pedestal]
[clojure.core.async :as a] [clojure.core.async :as a]
[clojure.java.io :as io] [clojure.java.io :as io]
[muuntaja.core :as m])) [muuntaja.core :as m]))
@ -76,15 +81,22 @@
{:status 200 {:status 200
:body {:total (+ x y)}})}}]]] :body {:total (+ x y)}})}}]]]
{;;:reitit.interceptor/transform dev/print-context-diffs {;:reitit.interceptor/transform dev/print-context-diffs ;; pretty context diffs
:data {:coercion spec-coercion/coercion ;;:validate spec/validate ;; enable spec validation for route data
;;:reitit.spec/wrap spell/closed ;; strict top-level validation
:exception pretty/exception
:data {:coercion reitit.coercion.spec/coercion
:muuntaja m/instance :muuntaja m/instance
:interceptors [;; query-params & form-params :interceptors [;; swagger feature
swagger/swagger-feature
;; query-params & form-params
(parameters/parameters-interceptor) (parameters/parameters-interceptor)
;; content-negotiation ;; content-negotiation
(muuntaja/format-negotiate-interceptor) (muuntaja/format-negotiate-interceptor)
;; encoding response body ;; encoding response body
(muuntaja/format-response-interceptor) (muuntaja/format-response-interceptor)
;; exception handling
(exception/exception-interceptor)
;; decoding request body ;; decoding request body
(muuntaja/format-request-interceptor) (muuntaja/format-request-interceptor)
;; coercing response bodys ;; coercing response bodys
@ -98,7 +110,8 @@
(ring/routes (ring/routes
(swagger-ui/create-swagger-ui-handler (swagger-ui/create-swagger-ui-handler
{:path "/" {:path "/"
:config {:validatorUrl nil}}) :config {:validatorUrl nil
:operationsSorter "alpha"}})
(ring/create-resource-handler) (ring/create-resource-handler)
(ring/create-default-handler)))) (ring/create-default-handler))))
@ -114,12 +127,12 @@
{:default-src "'self'" {:default-src "'self'"
:style-src "'self' 'unsafe-inline'" :style-src "'self' 'unsafe-inline'"
:script-src "'self' 'unsafe-inline'"}}} :script-src "'self' 'unsafe-inline'"}}}
(io.pedestal.http/default-interceptors) (server/default-interceptors)
;; use the reitit router ;; use the reitit router
(pedestal/replace-last-interceptor router) (pedestal/replace-last-interceptor router)
(io.pedestal.http/dev-interceptors) (server/dev-interceptors)
(io.pedestal.http/create-server) (server/create-server)
(io.pedestal.http/start)) (server/start))
(println "server running in port 3000")) (println "server running in port 3000"))
(comment (comment

View file

@ -3,6 +3,6 @@
:dependencies [[org.clojure/clojure "1.10.0"] :dependencies [[org.clojure/clojure "1.10.0"]
[io.pedestal/pedestal.service "0.5.5"] [io.pedestal/pedestal.service "0.5.5"]
[io.pedestal/pedestal.jetty "0.5.5"] [io.pedestal/pedestal.jetty "0.5.5"]
[metosin/reitit-pedestal "0.3.1"] [metosin/reitit-pedestal "0.3.9"]
[metosin/reitit "0.3.1"]] [metosin/reitit "0.3.9"]]
:repl-options {:init-ns example.server}) :repl-options {:init-ns example.server})

View file

@ -2,5 +2,5 @@
:description "Reitit Ring App" :description "Reitit Ring App"
:dependencies [[org.clojure/clojure "1.10.0"] :dependencies [[org.clojure/clojure "1.10.0"]
[ring/ring-jetty-adapter "1.7.1"] [ring/ring-jetty-adapter "1.7.1"]
[metosin/reitit "0.3.1"]] [metosin/reitit "0.3.9"]]
:repl-options {:init-ns example.server}) :repl-options {:init-ns example.server})

View file

@ -2,6 +2,6 @@
:description "Reitit Ring App with Swagger" :description "Reitit Ring App with Swagger"
:dependencies [[org.clojure/clojure "1.10.0"] :dependencies [[org.clojure/clojure "1.10.0"]
[ring/ring-jetty-adapter "1.7.1"] [ring/ring-jetty-adapter "1.7.1"]
[metosin/reitit "0.3.1"]] [metosin/reitit "0.3.9"]]
:repl-options {:init-ns example.server} :repl-options {:init-ns example.server}
:profiles{:dev {:dependencies [[ring/ring-mock "0.3.2"]]}}) :profiles {:dev {:dependencies [[ring/ring-mock "0.3.2"]]}})

View file

@ -1,13 +1,17 @@
(ns example.server (ns example.server
(:require [reitit.ring :as ring] (:require [reitit.ring :as ring]
[reitit.coercion.spec]
[reitit.swagger :as swagger] [reitit.swagger :as swagger]
[reitit.swagger-ui :as swagger-ui] [reitit.swagger-ui :as swagger-ui]
[reitit.ring.coercion :as coercion] [reitit.ring.coercion :as coercion]
[reitit.coercion.spec] [reitit.dev.pretty :as pretty]
[reitit.ring.middleware.muuntaja :as muuntaja] [reitit.ring.middleware.muuntaja :as muuntaja]
[reitit.ring.middleware.exception :as exception] [reitit.ring.middleware.exception :as exception]
[reitit.ring.middleware.multipart :as multipart] [reitit.ring.middleware.multipart :as multipart]
[reitit.ring.middleware.parameters :as parameters] [reitit.ring.middleware.parameters :as parameters]
[reitit.ring.middleware.dev :as dev]
[reitit.ring.spec :as spec]
[spec-tools.spell :as spell]
[ring.adapter.jetty :as jetty] [ring.adapter.jetty :as jetty]
[muuntaja.core :as m] [muuntaja.core :as m]
[clojure.spec.alpha :as s] [clojure.spec.alpha :as s]
@ -72,9 +76,15 @@
{:status 200 {:status 200
:body {:total (+ x y)}})}}]]] :body {:total (+ x y)}})}}]]]
{:data {:coercion reitit.coercion.spec/coercion {;;:reitit.middleware/transform dev/print-request-diffs ;; pretty diffs
;;:validate spec/validate ;; enable spec validation for route data
;;:reitit.spec/wrap spell/closed ;; strict top-level validation
:exception pretty/exception
:data {:coercion reitit.coercion.spec/coercion
:muuntaja m/instance :muuntaja m/instance
:middleware [;; query-params & form-params :middleware [;; swagger feature
swagger/swagger-feature
;; query-params & form-params
parameters/parameters-middleware parameters/parameters-middleware
;; content-negotiation ;; content-negotiation
muuntaja/format-negotiate-middleware muuntaja/format-negotiate-middleware
@ -93,7 +103,8 @@
(ring/routes (ring/routes
(swagger-ui/create-swagger-ui-handler (swagger-ui/create-swagger-ui-handler
{:path "/" {:path "/"
:config {:validatorUrl nil}}) :config {:validatorUrl nil
:operationsSorter "alpha"}})
(ring/create-default-handler)))) (ring/create-default-handler))))
(defn start [] (defn start []

View file

@ -2,5 +2,5 @@
:description "Reitit Ring App with Swagger" :description "Reitit Ring App with Swagger"
:dependencies [[org.clojure/clojure "1.10.0"] :dependencies [[org.clojure/clojure "1.10.0"]
[ring/ring-jetty-adapter "1.7.1"] [ring/ring-jetty-adapter "1.7.1"]
[metosin/reitit "0.3.1"]] [metosin/reitit "0.3.9"]]
:repl-options {:init-ns example.server}) :repl-options {:init-ns example.server})

View file

@ -1,14 +1,17 @@
(ns example.server (ns example.server
(:require [reitit.ring :as ring] (:require [reitit.ring :as ring]
[reitit.coercion.spec]
[reitit.swagger :as swagger] [reitit.swagger :as swagger]
[reitit.swagger-ui :as swagger-ui] [reitit.swagger-ui :as swagger-ui]
[reitit.ring.coercion :as coercion] [reitit.ring.coercion :as coercion]
[reitit.coercion.spec] [reitit.dev.pretty :as pretty]
[reitit.ring.middleware.muuntaja :as muuntaja] [reitit.ring.middleware.muuntaja :as muuntaja]
[reitit.ring.middleware.exception :as exception] [reitit.ring.middleware.exception :as exception]
[reitit.ring.middleware.multipart :as multipart] [reitit.ring.middleware.multipart :as multipart]
[reitit.ring.middleware.parameters :as parameters] [reitit.ring.middleware.parameters :as parameters]
[reitit.ring.middleware.dev :as dev] [reitit.ring.middleware.dev :as dev]
[reitit.ring.spec :as spec]
[spec-tools.spell :as spell]
[ring.adapter.jetty :as jetty] [ring.adapter.jetty :as jetty]
[muuntaja.core :as m] [muuntaja.core :as m]
[clojure.java.io :as io])) [clojure.java.io :as io]))
@ -61,10 +64,15 @@
{:status 200 {:status 200
:body {:total (+ x y)}})}}]]] :body {:total (+ x y)}})}}]]]
{;;:reitit.middleware/transform dev/print-request-diffs {;;:reitit.middleware/transform dev/print-request-diffs ;; pretty diffs
;;:validate spec/validate ;; enable spec validation for route data
;;:reitit.spec/wrap spell/closed ;; strict top-level validation
:exception pretty/exception
:data {:coercion reitit.coercion.spec/coercion :data {:coercion reitit.coercion.spec/coercion
:muuntaja m/instance :muuntaja m/instance
:middleware [;; query-params & form-params :middleware [;; swagger feature
swagger/swagger-feature
;; query-params & form-params
parameters/parameters-middleware parameters/parameters-middleware
;; content-negotiation ;; content-negotiation
muuntaja/format-negotiate-middleware muuntaja/format-negotiate-middleware

View file

@ -38,8 +38,8 @@ public class Trie {
return decode(new String(chars, begin, end - begin), hasPercent, hasPlus); return decode(new String(chars, begin, end - begin), hasPercent, hasPlus);
} }
public static class Match { public final static class Match {
public IPersistentMap params; public final IPersistentMap params;
public final Object data; public final Object data;
public Match(IPersistentMap params, Object data) { public Match(IPersistentMap params, Object data) {
@ -47,6 +47,10 @@ public class Trie {
this.data = data; this.data = data;
} }
Match assoc(Object key, Object value) {
return new Match(params.assoc(key, value), data);
}
@Override @Override
public String toString() { public String toString() {
Map<Object, Object> m = new HashMap<>(); Map<Object, Object> m = new HashMap<>();
@ -174,10 +178,7 @@ public class Trie {
} }
} }
final Match m = child.match(stop, max, path); final Match m = child.match(stop, max, path);
if (m != null) { return m != null ? m.assoc(key, decode(new String(path, i, stop - i), hasPercent, hasPlus)) : null;
m.params = m.params.assoc(key, decode(new String(path, i, stop - i), hasPercent, hasPlus));
}
return m;
} }
return null; return null;
} }
@ -216,7 +217,7 @@ public class Trie {
@Override @Override
public Match match(int i, int max, char[] path) { public Match match(int i, int max, char[] path) {
if (i <= max) { if (i <= max) {
return new Match(params.assoc(parameter, decode(path, i, max)), data); return new Match(params, data).assoc(parameter, decode(path, i, max));
} }
return null; return null;
} }
@ -291,8 +292,8 @@ public class Trie {
staticMatcher("/auth/", staticMatcher("/auth/",
linearMatcher( linearMatcher(
Arrays.asList( Arrays.asList(
staticMatcher("login", dataMatcher(null, 1)), staticMatcher("login", dataMatcher(PersistentArrayMap.EMPTY, 1)),
staticMatcher("recovery", dataMatcher(null, 2))), true))), true); staticMatcher("recovery", dataMatcher(PersistentArrayMap.EMPTY, 2))), true))), true);
System.err.println(matcher); System.err.println(matcher);
System.out.println(lookup(matcher, "/auth/login")); System.out.println(lookup(matcher, "/auth/login"));
System.out.println(lookup(matcher, "/auth/recovery")); System.out.println(lookup(matcher, "/auth/recovery"));

View file

@ -1,4 +1,4 @@
(defproject metosin/reitit-core "0.3.1" (defproject metosin/reitit-core "0.3.9"
:description "Snappy data-driven router for Clojure(Script)" :description "Snappy data-driven router for Clojure(Script)"
:url "https://github.com/metosin/reitit" :url "https://github.com/metosin/reitit"
:license {:name "Eclipse Public License" :license {:name "Eclipse Public License"

View file

@ -70,7 +70,7 @@
(-> request :muuntaja/request :format)) (-> request :muuntaja/request :format))
;; TODO: support faster key walking, walk/keywordize-keys is quite slow... ;; TODO: support faster key walking, walk/keywordize-keys is quite slow...
(defn request-coercer [coercion type model {:keys [::extract-request-format ::parameter-coercion] (defn request-coercer [coercion type model {::keys [extract-request-format parameter-coercion]
:or {extract-request-format extract-request-format-default :or {extract-request-format extract-request-format-default
parameter-coercion default-parameter-coercion}}] parameter-coercion default-parameter-coercion}}]
(if coercion (if coercion

View file

@ -67,7 +67,7 @@
([match] ([match]
(match->path match nil)) (match->path match nil))
([match query-params] ([match query-params]
(some-> match :path (cond-> query-params (str "?" (impl/query-string query-params)))))) (some-> match :path (cond-> (seq query-params) (str "?" (impl/query-string query-params))))))
;; ;;
;; Different routers ;; Different routers
@ -88,7 +88,7 @@
names (impl/find-names compiled-routes opts) names (impl/find-names compiled-routes opts)
[pl nl] (reduce [pl nl] (reduce
(fn [[pl nl] [p {:keys [name] :as data} result]] (fn [[pl nl] [p {:keys [name] :as data} result]]
(let [{:keys [path-params] :as route} (impl/parse p) (let [{:keys [path-params] :as route} (impl/parse p opts)
f #(if-let [path (impl/path-for route %)] f #(if-let [path (impl/path-for route %)]
(->Match p data result (impl/url-decode-coll %) path) (->Match p data result (impl/url-decode-coll %) path)
(->PartialMatch p data result (impl/url-decode-coll %) path-params))] (->PartialMatch p data result (impl/url-decode-coll %) path-params))]
@ -131,7 +131,7 @@
([compiled-routes] ([compiled-routes]
(lookup-router compiled-routes {})) (lookup-router compiled-routes {}))
([compiled-routes opts] ([compiled-routes opts]
(when-let [wilds (seq (filter impl/wild-route? compiled-routes))] (when-let [wilds (seq (filter (impl/->wild-route? opts) compiled-routes))]
(exception/fail! (exception/fail!
(str "can't create :lookup-router with wildcard routes: " wilds) (str "can't create :lookup-router with wildcard routes: " wilds)
{:wilds wilds {:wilds wilds
@ -184,7 +184,7 @@
names (impl/find-names compiled-routes opts) names (impl/find-names compiled-routes opts)
[pl nl] (reduce [pl nl] (reduce
(fn [[pl nl] [p {:keys [name] :as data} result]] (fn [[pl nl] [p {:keys [name] :as data} result]]
(let [{:keys [path-params] :as route} (impl/parse p) (let [{:keys [path-params] :as route} (impl/parse p opts)
f #(if-let [path (impl/path-for route %)] f #(if-let [path (impl/path-for route %)]
(->Match p data result (impl/url-decode-coll %) path) (->Match p data result (impl/url-decode-coll %) path)
(->PartialMatch p data result (impl/url-decode-coll %) path-params))] (->PartialMatch p data result (impl/url-decode-coll %) path-params))]
@ -227,7 +227,7 @@
([compiled-routes] ([compiled-routes]
(single-static-path-router compiled-routes {})) (single-static-path-router compiled-routes {}))
([compiled-routes opts] ([compiled-routes opts]
(when (or (not= (count compiled-routes) 1) (some impl/wild-route? compiled-routes)) (when (or (not= (count compiled-routes) 1) (some (impl/->wild-route? opts) compiled-routes))
(exception/fail! (exception/fail!
(str ":single-static-path-router requires exactly 1 static route: " compiled-routes) (str ":single-static-path-router requires exactly 1 static route: " compiled-routes)
{:routes compiled-routes})) {:routes compiled-routes}))
@ -266,7 +266,7 @@
([compiled-routes] ([compiled-routes]
(mixed-router compiled-routes {})) (mixed-router compiled-routes {}))
([compiled-routes opts] ([compiled-routes opts]
(let [{wild true, lookup false} (group-by impl/wild-route? compiled-routes) (let [{wild true, lookup false} (group-by (impl/->wild-route? opts) compiled-routes)
->static-router (if (= 1 (count lookup)) single-static-path-router lookup-router) ->static-router (if (= 1 (count lookup)) single-static-path-router lookup-router)
wildcard-router (trie-router wild opts) wildcard-router (trie-router wild opts)
static-router (->static-router lookup opts) static-router (->static-router lookup opts)
@ -301,7 +301,7 @@
([compiled-routes] ([compiled-routes]
(quarantine-router compiled-routes {})) (quarantine-router compiled-routes {}))
([compiled-routes opts] ([compiled-routes opts]
(let [conflicting-paths (-> compiled-routes impl/path-conflicting-routes impl/conflicting-paths) (let [conflicting-paths (-> compiled-routes (impl/path-conflicting-routes opts) impl/conflicting-paths)
conflicting? #(contains? conflicting-paths (first %)) conflicting? #(contains? conflicting-paths (first %))
{conflicting true, non-conflicting false} (group-by conflicting? compiled-routes) {conflicting true, non-conflicting false} (group-by conflicting? compiled-routes)
linear-router (linear-router conflicting opts) linear-router (linear-router conflicting opts)
@ -347,12 +347,13 @@
Selects implementation based on route details. The following options Selects implementation based on route details. The following options
are available: are available:
| key | description | | key | description
| -------------|-------------| | -------------|-------------
| `:path` | Base-path for routes | `:path` | Base-path for routes
| `:routes` | Initial resolved routes (default `[]`) | `:routes` | Initial resolved routes (default `[]`)
| `:data` | Initial route data (default `{}`) | `:data` | Initial route data (default `{}`)
| `:spec` | clojure.spec definition for a route data, see `reitit.spec` on how to use this | `:spec` | clojure.spec definition for a route data, see `reitit.spec` on how to use this
| `:syntax` | Path-parameter syntax as keyword or set of keywords (default #{:bracket :colon})
| `:expand` | Function of `arg opts => data` to expand route arg to route data (default `reitit.core/expand`) | `:expand` | Function of `arg opts => data` to expand route arg to route data (default `reitit.core/expand`)
| `:coerce` | Function of `route opts => route` to coerce resolved route, can throw or return `nil` | `:coerce` | Function of `route opts => route` to coerce resolved route, can throw or return `nil`
| `:compile` | Function of `route opts => result` to compile a route handler | `:compile` | Function of `route opts => result` to compile a route handler
@ -366,11 +367,11 @@
(let [{:keys [router] :as opts} (merge (default-router-options) opts)] (let [{:keys [router] :as opts} (merge (default-router-options) opts)]
(try (try
(let [routes (impl/resolve-routes raw-routes opts) (let [routes (impl/resolve-routes raw-routes opts)
path-conflicting (impl/path-conflicting-routes routes) path-conflicting (impl/path-conflicting-routes routes opts)
name-conflicting (impl/name-conflicting-routes routes) name-conflicting (impl/name-conflicting-routes routes)
compiled-routes (impl/compile-routes routes opts) compiled-routes (impl/compile-routes routes opts)
wilds? (boolean (some impl/wild-route? compiled-routes)) wilds? (boolean (some (impl/->wild-route? opts) compiled-routes))
all-wilds? (every? impl/wild-route? compiled-routes) all-wilds? (every? (impl/->wild-route? opts) compiled-routes)
router (cond router (cond
router router router router
(and (= 1 (count compiled-routes)) (not wilds?)) single-static-path-router (and (= 1 (count compiled-routes)) (not wilds?)) single-static-path-router

View file

@ -7,11 +7,14 @@
([type data] ([type data]
(throw (ex-info (str type) {:type type, :data data})))) (throw (ex-info (str type) {:type type, :data data}))))
(defn get-message [e]
#?(:clj (.getMessage ^Exception e) :cljs (ex-message e)))
(defmulti format-exception (fn [type _ _] type)) (defmulti format-exception (fn [type _ _] type))
(defn exception [e] (defn exception [e]
(let [data (ex-data e) (let [data (ex-data e)
message (format-exception (:type data) #?(:clj (.getMessage ^Exception e) :cljs (ex-message e)) (:data data))] message (format-exception (:type data) (get-message e) (:data data))]
;; there is a 3-arity version (+cause) of ex-info, but the default repl error message is taken from the cause ;; there is a 3-arity version (+cause) of ex-info, but the default repl error message is taken from the cause
(ex-info message (assoc (or data {}) ::cause e)))) (ex-info message (assoc (or data {}) ::cause e))))
@ -35,3 +38,6 @@
(fn [[name vals]] (fn [[name vals]]
(str name "\n-> " (str/join "\n-> " (mapv first vals)) "\n")) (str name "\n-> " (str/join "\n-> " (mapv first vals)) "\n"))
conflicts))) conflicts)))
(defmethod format-exception :reitit.impl/merge-data [_ _ data]
(str "Error merging route-data\n\n" (pr-str data)))

View file

@ -4,24 +4,26 @@
[clojure.set :as set] [clojure.set :as set]
[meta-merge.core :as mm] [meta-merge.core :as mm]
[reitit.trie :as trie] [reitit.trie :as trie]
[reitit.exception :as exception]) [reitit.exception :as exception]
[reitit.exception :as ex])
#?(:clj #?(:clj
(:import (java.util.regex Pattern) (:import (java.util.regex Pattern)
(java.util HashMap Map) (java.util HashMap Map)
(java.net URLEncoder URLDecoder)))) (java.net URLEncoder URLDecoder))))
(defrecord Route [path path-parts path-params]) (defn parse [path opts]
(let [path #?(:clj (.intern ^String (trie/normalize path opts)) :cljs (trie/normalize path opts))
(defn parse [path] path-parts (trie/split-path path opts)
(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)] path-params (->> path-parts (remove string?) (map :value) set)]
(map->Route {:path-params path-params {:path-params path-params
:path-parts path-parts :path-parts path-parts
:path path}))) :path path}))
(defn wild-route? [[path]] (defn wild-path? [path opts]
(-> path parse :path-params seq boolean)) (-> path (parse opts) :path-params seq boolean))
(defn ->wild-route? [opts]
(fn [[path]] (-> path (parse opts) :path-params seq boolean)))
(defn maybe-map-values (defn maybe-map-values
"Applies a function to every value of a map, updates the value if not nil. "Applies a function to every value of a map, updates the value if not nil.
@ -58,26 +60,26 @@
(walk-one path (mapv identity data) raw-routes))) (walk-one path (mapv identity data) raw-routes)))
(defn map-data [f routes] (defn map-data [f routes]
(mapv #(update % 1 f) routes)) (mapv (fn [[p ds]] [p (f p ds)]) routes))
(defn merge-data [x] (defn merge-data [p x]
(reduce (reduce
(fn [acc [k v]] (fn [acc [k v]]
(mm/meta-merge acc {k v})) (try
(mm/meta-merge acc {k v})
(catch #?(:clj Exception, :cljs js/Error) e
(ex/fail! ::merge-data {:path p, :left acc, :right {k v}, :exception e}))))
{} x)) {} x))
(defn resolve-routes [raw-routes {:keys [coerce] :as opts}] (defn resolve-routes [raw-routes {:keys [coerce] :as opts}]
(cond->> (->> (walk raw-routes opts) (map-data merge-data)) (cond->> (->> (walk raw-routes opts) (map-data merge-data))
coerce (into [] (keep #(coerce % opts))))) coerce (into [] (keep #(coerce % opts)))))
(defn conflicting-routes? [route1 route2] (defn path-conflicting-routes [routes opts]
(trie/conflicting-paths? (first route1) (first route2)))
(defn path-conflicting-routes [routes]
(-> (into {} (-> (into {}
(comp (map-indexed (fn [index route] (comp (map-indexed (fn [index route]
[route (into #{} [route (into #{}
(filter (partial conflicting-routes? route)) (filter #(trie/conflicting-paths? (first route) (first %) opts))
(subvec routes (inc index)))])) (subvec routes (inc index)))]))
(filter (comp seq second))) (filter (comp seq second)))
routes) routes)
@ -110,7 +112,7 @@
(defn uncompile-routes [routes] (defn uncompile-routes [routes]
(mapv (comp vec (partial take 2)) routes)) (mapv (comp vec (partial take 2)) routes))
(defn path-for [^Route route path-params] (defn path-for [route path-params]
(if (:path-params route) (if (:path-params route)
(if-let [parts (reduce (if-let [parts (reduce
(fn [acc part] (fn [acc part]

View file

@ -33,7 +33,7 @@
#?(:clj clojure.lang.Keyword #?(:clj clojure.lang.Keyword
:cljs cljs.core.Keyword) :cljs cljs.core.Keyword)
(into-interceptor [this data {:keys [::registry] :as opts}] (into-interceptor [this data {::keys [registry] :as opts}]
(if-let [interceptor (if registry (registry this))] (if-let [interceptor (if registry (registry this))]
(into-interceptor interceptor data opts) (into-interceptor interceptor data opts)
(throw (throw
@ -108,7 +108,7 @@
(chain interceptors nil nil)) (chain interceptors nil nil))
([interceptors data] ([interceptors data]
(chain interceptors data nil)) (chain interceptors data nil))
([interceptors data {:keys [::transform] :or {transform identity} :as opts}] ([interceptors data {::keys [transform] :or {transform identity} :as opts}]
(let [transform (if (vector? transform) (apply comp (reverse transform)) transform)] (let [transform (if (vector? transform) (apply comp (reverse transform)) transform)]
(->> interceptors (->> interceptors
(keep #(into-interceptor % data opts)) (keep #(into-interceptor % data opts))
@ -119,7 +119,7 @@
(defn compile-result (defn compile-result
([route opts] ([route opts]
(compile-result route opts nil)) (compile-result route opts nil))
([[_ {:keys [interceptors handler] :as data}] {:keys [::queue] :as opts} _] ([[_ {:keys [interceptors handler] :as data}] {::keys [queue] :as opts} _]
(let [chain (chain (into (vec interceptors) [handler]) data opts)] (let [chain (chain (into (vec interceptors) [handler]) data opts)]
(map->Endpoint (map->Endpoint
{:interceptors chain {:interceptors chain

View file

@ -17,7 +17,7 @@
#?(:clj clojure.lang.Keyword #?(:clj clojure.lang.Keyword
:cljs cljs.core.Keyword) :cljs cljs.core.Keyword)
(into-middleware [this data {:keys [::registry] :as opts}] (into-middleware [this data {::keys [registry] :as opts}]
(if-let [middleware (if registry (registry this))] (if-let [middleware (if registry (registry this))]
(into-middleware middleware data opts) (into-middleware middleware data opts)
(throw (throw
@ -83,7 +83,7 @@
(if scope {:scope scope}))))) (if scope {:scope scope})))))
(defn- expand-and-transform (defn- expand-and-transform
[middleware data {:keys [::transform] :or {transform identity} :as opts}] [middleware data {::keys [transform] :or {transform identity} :as opts}]
(let [transform (if (vector? transform) (apply comp (reverse transform)) transform)] (let [transform (if (vector? transform) (apply comp (reverse transform)) transform)]
(->> middleware (->> middleware
(keep #(into-middleware % data opts)) (keep #(into-middleware % data opts))

View file

@ -39,7 +39,8 @@
(s/def ::name keyword?) (s/def ::name keyword?)
(s/def ::handler fn?) (s/def ::handler fn?)
(s/def ::default-data (s/keys :opt-un [::name ::handler])) (s/def ::no-doc boolean?)
(s/def ::default-data (s/keys :opt-un [::name ::handler ::no-doc]))
;; ;;
;; router ;; router
@ -75,6 +76,8 @@
;; coercion ;; coercion
;; ;;
(s/def :reitit.core.coercion/coercion any?)
(s/def :reitit.core.coercion/model any?) (s/def :reitit.core.coercion/model any?)
(s/def :reitit.core.coercion/query :reitit.core.coercion/model) (s/def :reitit.core.coercion/query :reitit.core.coercion/model)
@ -90,7 +93,8 @@
:reitit.core.coercion/path])) :reitit.core.coercion/path]))
(s/def ::parameters (s/def ::parameters
(s/keys :opt-un [:reitit.core.coercion/parameters])) (s/keys :opt-un [:reitit.core.coercion/coercion
:reitit.core.coercion/parameters]))
(s/def :reitit.core.coercion/status (s/def :reitit.core.coercion/status
(s/or :number number? :default #{:default})) (s/or :number number? :default #{:default}))
@ -103,7 +107,8 @@
(s/map-of :reitit.core.coercion/status :reitit.core.coercion/response)) (s/map-of :reitit.core.coercion/status :reitit.core.coercion/response))
(s/def ::responses (s/def ::responses
(s/keys :opt-un [:reitit.core.coercion/responses])) (s/keys :opt-un [:reitit.core.coercion/coercion
:reitit.core.coercion/responses]))
;; ;;
;; Route data validator ;; Route data validator
@ -111,14 +116,15 @@
(defrecord Problem [path scope data spec problems]) (defrecord Problem [path scope data spec problems])
(defn validate-route-data [routes spec] (defn validate-route-data [routes wrap spec]
(some->> (for [[p d _] routes] (let [spec (wrap spec)]
(when-let [problems (and spec (s/explain-data spec d))] (some->> (for [[p d _] routes]
(->Problem p nil d spec problems))) (when-let [problems (and spec (s/explain-data spec d))]
(keep identity) (seq) (vec))) (->Problem p nil d spec problems)))
(keep identity) (seq) (vec))))
(defn validate [routes {:keys [spec] :or {spec ::default-data}}] (defn validate [routes {:keys [spec] ::keys [wrap] :or {spec ::default-data, wrap identity}}]
(when-let [problems (validate-route-data routes spec)] (when-let [problems (validate-route-data routes wrap spec)]
(exception/fail! (exception/fail!
::invalid-route-data ::invalid-route-data
{:problems problems}))) {:problems problems})))

View file

@ -5,6 +5,12 @@
#?(:clj (:import [reitit Trie Trie$Match Trie$Matcher] #?(:clj (:import [reitit Trie Trie$Match Trie$Matcher]
(java.net URLDecoder)))) (java.net URLDecoder))))
(defn ^:no-doc into-set [x]
(cond
(or (set? x) (sequential? x)) (set x)
(nil? x) #{}
:else (conj #{} x)))
(defrecord Wild [value]) (defrecord Wild [value])
(defrecord CatchAll [value]) (defrecord CatchAll [value])
(defrecord Match [params data]) (defrecord Match [params data])
@ -51,25 +57,36 @@
(keyword (subs s 0 i) (subs s (inc i))) (keyword (subs s 0 i) (subs s (inc i)))
(keyword s))) (keyword s)))
(defn split-path [s] (defn split-path [s {:keys [syntax] :or {syntax #{:bracket :colon}}}]
(let [-static (fn [from to] (if-not (= from to) [(subs s from to)])) (let [bracket? (-> syntax (into-set) :bracket)
colon? (-> syntax (into-set) :colon)
-static (fn [from to] (if-not (= from to) [(subs s from to)]))
-wild (fn [from to] [(->Wild (-keyword (subs s (inc 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)))])] -catch-all (fn [from to] [(->CatchAll (keyword (subs s (inc from) to)))])]
(loop [ss nil, from 0, to 0] (loop [ss nil, from 0, to 0]
(if (= to (count s)) (if (= to (count s))
(concat ss (-static from to)) (concat ss (-static from to))
(case (get s to) (let [c (get s to)]
\{ (let [to' (or (str/index-of s "}" to) (ex/fail! ::unclosed-brackets {:path s}))] (cond
(if (= \* (get s (inc to)))
(recur (concat ss (-static from to) (-catch-all (inc to) to')) (long (inc to')) (long (inc to'))) (and bracket? (= \{ c))
(recur (concat ss (-static from to) (-wild to to')) (long (inc to')) (long (inc to'))))) (let [to' (or (str/index-of s "}" to) (ex/fail! ::unclosed-brackets {:path s}))]
\: (let [to' (or (str/index-of s "/" to) (count s))] (if (= \* (get s (inc to)))
(if (= 1 (- to' to)) (recur (concat ss (-static from to) (-catch-all (inc to) to')) (long (inc to')) (long (inc to')))
(recur ss from (inc to)) (recur (concat ss (-static from to) (-wild to to')) (long (inc to')) (long (inc to')))))
(recur (concat ss (-static from to) (-wild to to')) (long to') (long to'))))
\* (let [to' (count s)] (and colon? (= \: c))
(recur (concat ss (-static from to) (-catch-all to to')) (long to') (long to'))) (let [to' (or (str/index-of s "/" to) (count s))]
(recur ss from (inc to))))))) (if (= 1 (- to' to))
(recur ss from (inc to))
(recur (concat ss (-static from to) (-wild to to')) (long to') (long to'))))
(and colon? (= \* c))
(let [to' (count s)]
(recur (concat ss (-static from to) (-catch-all to to')) (long to') (long to')))
:else
(recur ss from (inc to))))))))
(defn join-path [xs] (defn join-path [xs]
(reduce (reduce
@ -80,8 +97,8 @@
(instance? CatchAll x) (str "{*" (-> x :value str (subs 1)) "}")))) (instance? CatchAll x) (str "{*" (-> x :value str (subs 1)) "}"))))
"" xs)) "" xs))
(defn normalize [s] (defn normalize [s opts]
(-> s (split-path) (join-path))) (-> s (split-path opts) (join-path)))
;; ;;
;; Conflict Resolution ;; Conflict Resolution
@ -115,9 +132,9 @@
(concat [(subs x i)] xs) (concat [(subs x i)] xs)
xs))) xs)))
(defn conflicting-paths? [path1 path2] (defn conflicting-paths? [path1 path2 opts]
(loop [parts1 (split-path path1) (loop [parts1 (split-path path1 opts)
parts2 (split-path path2)] parts2 (split-path path2 opts)]
(let [[[s1 & ss1] [s2 & ss2]] (-slice-start parts1 parts2)] (let [[[s1 & ss1] [s2 & ss2]] (-slice-start parts1 parts2)]
(cond (cond
(= s1 s2 nil) true (= s1 s2 nil) true
@ -314,10 +331,10 @@
node routes)) node routes))
([node path data] ([node path data]
(insert node path data nil)) (insert node path data nil))
([node path data {::keys [parameters] :or {parameters map-parameters}}] ([node path data {::keys [parameters] :or {parameters map-parameters} :as opts}]
(let [parts (split-path path) (let [parts (split-path path opts)
params (parameters (->> parts (remove string?) (map :value)))] params (parameters (->> parts (remove string?) (map :value)))]
(-insert (or node (-node {})) (split-path path) path params data)))) (-insert (or node (-node {})) (split-path path opts) path params data))))
(defn compiler (defn compiler
"Returns a default [[TrieCompiler]]." "Returns a default [[TrieCompiler]]."

View file

@ -1,4 +1,4 @@
(defproject metosin/reitit-dev "0.3.1" (defproject metosin/reitit-dev "0.3.9"
:description "Snappy data-driven router for Clojure(Script)" :description "Snappy data-driven router for Clojure(Script)"
:url "https://github.com/metosin/reitit" :url "https://github.com/metosin/reitit"
:license {:name "Eclipse Public License" :license {:name "Eclipse Public License"
@ -9,5 +9,6 @@
:parent-project {:path "../../project.clj" :parent-project {:path "../../project.clj"
:inherit [:deploy-repositories :managed-dependencies]} :inherit [:deploy-repositories :managed-dependencies]}
:dependencies [[metosin/reitit-core] :dependencies [[metosin/reitit-core]
[com.bhauman/spell-spec]
[expound] [expound]
[fipp]]) [fipp]])

View file

@ -3,6 +3,9 @@
[clojure.spec.alpha :as s] [clojure.spec.alpha :as s]
[reitit.exception :as exception] [reitit.exception :as exception]
[arrangement.core] [arrangement.core]
;; spell-spec
[spec-tools.spell :as spell]
[spell-spec.expound]
;; expound ;; expound
[expound.ansi] [expound.ansi]
[expound.alpha] [expound.alpha]
@ -178,7 +181,7 @@
(if (and (not= 1 line)) (if (and (not= 1 line))
(let [file-name (str/replace file #"(.*?)\.\S[^\.]+" "$1") (let [file-name (str/replace file #"(.*?)\.\S[^\.]+" "$1")
target-name (name target) target-name (name target)
ns (str (subs target-name 0 (str/index-of target-name (str "user" "$"))) file-name)] ns (str (subs target-name 0 (or (str/index-of target-name (str file-name "$")) 0)) file-name)]
(str ns ":" line)) (str ns ":" line))
"repl") "repl")
(catch #?(:clj Exception, :cljs js/Error) _ (catch #?(:clj Exception, :cljs js/Error) _
@ -220,10 +223,10 @@
(defn exception [e] (defn exception [e]
(let [data (-> e ex-data :data) (let [data (-> e ex-data :data)
message (format-exception (-> e ex-data :type) #?(:clj (.getMessage ^Exception e) :cljs (ex-message e)) data) message (format-exception (-> e ex-data :type) #?(:clj (.getMessage ^Exception e) :cljs (ex-message e)) data)
source #?(:clj (->> e Throwable->map :trace source #?(:clj (->> e Throwable->map :trace
(drop-while #(not= (name (first %)) "reitit.core$router")) (drop-while #(not= (name (first %)) "reitit.core$router"))
(drop-while #(= (name (first %)) "reitit.core$router")) (drop-while #(= (name (first %)) "reitit.core$router"))
next first source-str) next first source-str)
:cljs "unknown")] :cljs "unknown")]
(ex-info (exception-str message source (printer)) (assoc (or data {}) ::exception/cause e)))) (ex-info (exception-str message source (printer)) (assoc (or data {}) ::exception/cause e))))
@ -316,12 +319,12 @@
(into (into
[:group] [:group]
(map (map
(fn [{:keys [data path spec]}] (fn [{:keys [data path spec scope]}]
[:group [:group
[:span (color :grey "-- On route -----------------------")] [:span (color :grey "-- On route -----------------------")]
[:break] [:break]
[:break] [:break]
(text path) (text path) (if scope [:span " " (text scope)])
[:break] [:break]
[:break] [:break]
(-> (s/explain-data spec data) (-> (s/explain-data spec data)
@ -332,3 +335,27 @@
problems)) problems))
(color :white "https://cljdoc.org/d/metosin/reitit/CURRENT/doc/basics/route-data-validation") (color :white "https://cljdoc.org/d/metosin/reitit/CURRENT/doc/basics/route-data-validation")
[:break]]) [:break]])
(defmethod format-exception :reitit.impl/merge-data [_ _ {:keys [path left right exception]}]
[:group
(text "Error merging route-data:")
[:break] [:break]
[:group
[:span (color :grey "-- On route -----------------------")]
[:break]
[:break]
(text path)
[:break]
[:break]
[:span (color :grey "-- Exception ----------------------")]
[:break]
[:break]
(color :red (exception/get-message exception))
[:break]
[:break]
(edn left {:margin 3})
[:break]
(edn right {:margin 3})]
[:break]
(color :white "https://cljdoc.org/d/metosin/reitit/CURRENT/doc/basics/route-data")
[:break]])

View file

@ -1,4 +1,4 @@
(defproject metosin/reitit-frontend "0.3.1" (defproject metosin/reitit-frontend "0.3.9"
:description "Reitit: Clojurescript frontend routing core" :description "Reitit: Clojurescript frontend routing core"
:url "https://github.com/metosin/reitit" :url "https://github.com/metosin/reitit"
:license {:name "Eclipse Public License" :license {:name "Eclipse Public License"

View file

@ -103,7 +103,7 @@
;; Prevent document load when clicking a elements, if the href points to URL that is part ;; Prevent document load when clicking a elements, if the href points to URL that is part
;; of the routing tree." ;; of the routing tree."
ignore-anchor-click (fn [e] ignore-anchor-click (fn [e]
;; Returns the next matching anchestor of event target ;; Returns the next matching ancestor of event target
(when-let [el (closest-by-tag (event-target e) "a")] (when-let [el (closest-by-tag (event-target e) "a")]
(let [uri (.parse Uri (.-href el))] (let [uri (.parse Uri (.-href el))]
(when (ignore-anchor-click-predicate router e el uri) (when (ignore-anchor-click-predicate router e el uri)
@ -133,7 +133,7 @@
Returns History object. Returns History object.
When using with development workflow like Figwheel, rememeber to When using with development workflow like Figwheel, remember to
remove listeners using stop! call before calling start! again. remove listeners using stop! call before calling start! again.
Parameters: Parameters:

View file

@ -1,4 +1,4 @@
(defproject metosin/reitit-http "0.3.1" (defproject metosin/reitit-http "0.3.9"
:description "Reitit: HTTP routing with interceptors" :description "Reitit: HTTP routing with interceptors"
:url "https://github.com/metosin/reitit" :url "https://github.com/metosin/reitit"
:license {:name "Eclipse Public License" :license {:name "Eclipse Public License"

View file

@ -2,8 +2,7 @@
(:require [meta-merge.core :refer [meta-merge]] (:require [meta-merge.core :refer [meta-merge]]
[reitit.interceptor :as interceptor] [reitit.interceptor :as interceptor]
[reitit.ring :as ring] [reitit.ring :as ring]
[reitit.core :as r] [reitit.core :as r]))
[reitit.impl :as impl]))
(defrecord Endpoint [data interceptors queue handler path method]) (defrecord Endpoint [data interceptors queue handler path method])
@ -14,8 +13,11 @@
(update acc method expand opts) (update acc method expand opts)
acc)) data ring/http-methods)]) acc)) data ring/http-methods)])
(defn compile-result [[path data] {:keys [::default-options-handler] :as opts}] (defn compile-result [[path data] {::keys [default-options-handler] :as opts}]
(let [[top childs] (ring/group-keys data) (let [[top childs] (ring/group-keys data)
childs (cond-> childs
(and (not (:options childs)) (not (:handler top)) default-options-handler)
(assoc :options {:no-doc true, :handler default-options-handler}))
compile (fn [[path data] opts scope] compile (fn [[path data] opts scope]
(interceptor/compile-result [path data] opts scope)) (interceptor/compile-result [path data] opts scope))
->endpoint (fn [p d m s] ->endpoint (fn [p d m s]
@ -29,12 +31,7 @@
(fn [acc method] (fn [acc method]
(cond-> acc (cond-> acc
any? (assoc method (->endpoint path data method nil)))) any? (assoc method (->endpoint path data method nil))))
(ring/map->Methods (ring/map->Methods {})
{:options
(if default-options-handler
(->endpoint path (assoc data
:handler default-options-handler
:no-doc true) :options nil))})
ring/http-methods))] ring/http-methods))]
(if-not (seq childs) (if-not (seq childs)
(->methods true top) (->methods true top)
@ -73,7 +70,7 @@
(r/router data opts)))) (r/router data opts))))
(defn routing-interceptor (defn routing-interceptor
"Creates a Pedestal-style routing interceptor that enqueus the interceptors into context. "Creates a Pedestal-style routing interceptor that enqueues the interceptors into context.
Takes http-router, default ring-handler and and options map, with the following keys: Takes http-router, default ring-handler and and options map, with the following keys:
| key | description | | key | description |

View file

@ -11,7 +11,13 @@
{:name ::coerce-request {:name ::coerce-request
:spec ::rs/parameters :spec ::rs/parameters
:compile (fn [{:keys [coercion parameters]} opts] :compile (fn [{:keys [coercion parameters]} opts]
(if (and coercion parameters) (cond
;; no coercion, skip
(not coercion) nil
;; just coercion, don't mount
(not parameters) {}
;; mount
:else
(let [coercers (coercion/request-coercers coercion parameters opts)] (let [coercers (coercion/request-coercers coercion parameters opts)]
{:enter (fn [ctx] {:enter (fn [ctx]
(let [request (:request ctx) (let [request (:request ctx)
@ -27,7 +33,13 @@
{:name ::coerce-response {:name ::coerce-response
:spec ::rs/responses :spec ::rs/responses
:compile (fn [{:keys [coercion responses]} opts] :compile (fn [{:keys [coercion responses]} opts]
(if (and coercion responses) (cond
;; no coercion, skip
(not coercion) nil
;; just coercion, don't mount
(not responses) {}
;; mount
:else
(let [coercers (coercion/response-coercers coercion responses opts)] (let [coercers (coercion/response-coercers coercion responses opts)]
{:leave (fn [ctx] {:leave (fn [ctx]
(let [request (:request ctx) (let [request (:request ctx)

View file

@ -12,15 +12,15 @@
(s/def ::interceptors (s/coll-of (partial satisfies? interceptor/IntoInterceptor))) (s/def ::interceptors (s/coll-of (partial satisfies? interceptor/IntoInterceptor)))
(s/def ::data (s/def ::data
(s/keys :opt-un [::rs/handler ::rs/name ::interceptors])) (s/keys :opt-un [::rs/handler ::rs/name ::rs/no-doc ::interceptors]))
;; ;;
;; Validator ;; Validator
;; ;;
(defn validate (defn validate
[routes {:keys [spec] :or {spec ::data}}] [routes {:keys [spec ::rs/wrap] :or {spec ::data, wrap identity}}]
(when-let [problems (rrs/validate-route-data routes :interceptors spec)] (when-let [problems (rrs/validate-route-data routes :interceptors wrap spec)]
(exception/fail! (exception/fail!
::invalid-route-data ::rs/invalid-route-data
{:problems problems}))) {:problems problems})))

View file

@ -1,4 +1,4 @@
(defproject metosin/reitit-interceptors "0.3.1" (defproject metosin/reitit-interceptors "0.3.9"
:description "Reitit, common interceptors bundled" :description "Reitit, common interceptors bundled"
:url "https://github.com/metosin/reitit" :url "https://github.com/metosin/reitit"
:license {:name "Eclipse Public License" :license {:name "Eclipse Public License"

View file

@ -24,7 +24,7 @@
(update :request dissoc ::r/match ::r/router))) (update :request dissoc ::r/match ::r/router)))
(defn- handle [name stage] (defn- handle [name stage]
(fn [{:keys [::original ::previous] :as ctx}] (fn [{::keys [previous] :as ctx}]
(let [current (polish ctx) (let [current (polish ctx)
previous (polish previous)] previous (polish previous)]
(printer/print-doc (diff-doc stage name previous current) printer) (printer/print-doc (diff-doc stage name previous current) printer)

View file

@ -85,17 +85,17 @@
(defn exception-interceptor (defn exception-interceptor
"Creates an Interceptor that catches all exceptions. Takes a map "Creates an Interceptor that catches all exceptions. Takes a map
of `identifier => exception request => response` that is used to select of `identifier => exception request => response` that is used to select
the exception handler for the thown/raised exception identifier. Exception the exception handler for the thrown/raised exception identifier. Exception
idenfier is either a `Keyword` or a Exception Class. identifier is either a `Keyword` or a Exception Class.
The following handlers special handlers are available: The following handlers special handlers are available:
| key | description | key | description
|------------------------|------------- |------------------------|-------------
| `::exception/default` | a default exception handler if nothing else mathced (default [[default-handler]]). | `::exception/default` | a default exception handler if nothing else matched (default [[default-handler]]).
| `::exception/wrap` | a 3-arity handler to wrap the actual handler `handler exception request => response` | `::exception/wrap` | a 3-arity handler to wrap the actual handler `handler exception request => response`
The handler is selected from the options map by exception idenfiter The handler is selected from the options map by exception identifier
in the following lookup order: in the following lookup order:
1) `:type` of exception ex-data 1) `:type` of exception ex-data

View file

@ -11,6 +11,9 @@
(s/def ::bytes bytes?) (s/def ::bytes bytes?)
(s/def ::size int?) (s/def ::size int?)
(s/def ::multipart :reitit.core.coercion/model)
(s/def ::parameters (s/keys :opt-un [::multipart]))
(def temp-file-part (def temp-file-part
"Spec for file param created by ring.middleware.multipart-params.temp-file store." "Spec for file param created by ring.middleware.multipart-params.temp-file store."
(st/spec (st/spec
@ -41,6 +44,7 @@
(multipart-interceptor nil)) (multipart-interceptor nil))
([options] ([options]
{:name ::multipart {:name ::multipart
:spec ::parameters
:compile (fn [{:keys [parameters coercion]} opts] :compile (fn [{:keys [parameters coercion]} opts]
(if-let [multipart (:multipart parameters)] (if-let [multipart (:multipart parameters)]
(let [parameter-coercion {:multipart (coercion/->ParameterCoercion (let [parameter-coercion {:multipart (coercion/->ParameterCoercion

View file

@ -1,4 +1,4 @@
(defproject metosin/reitit-middleware "0.3.1" (defproject metosin/reitit-middleware "0.3.9"
:description "Reitit, common middleware bundled" :description "Reitit, common middleware bundled"
:url "https://github.com/metosin/reitit" :url "https://github.com/metosin/reitit"
:license {:name "Eclipse Public License" :license {:name "Eclipse Public License"

View file

@ -9,21 +9,28 @@
(assoc :width 70) (assoc :width 70)
(update :color-scheme merge {:middleware [:blue]}))) (update :color-scheme merge {:middleware [:blue]})))
(defn diff-doc [name previous current] (defn diff-doc [stage name previous current]
[:group [:group
[:span "--- Middleware " (if name (color/document printer :middleware (str name " "))) "---" :break :break] [:span "--- " (str stage) (if name (color/document printer :middleware (str " " name " "))) "---" :break :break]
[:nest (printer/format-doc (if previous (ddiff/diff previous current) current) printer)] [:nest (printer/format-doc (if previous (ddiff/diff previous current) current) printer)]
:break]) :break])
(defn polish [request] (defn polish [request]
(dissoc request ::r/match ::r/router ::original ::previous)) (dissoc request ::r/match ::r/router ::original ::previous))
(defn printed-request [name {:keys [::original ::previous] :as request}] (defn printed-request [name {::keys [previous] :as request}]
(printer/print-doc (diff-doc name (polish previous) (polish request)) printer) (printer/print-doc (diff-doc :request name (polish previous) (polish request)) printer)
(-> request (-> request
(update ::original (fnil identity request)) (update ::original (fnil identity request))
(assoc ::previous request))) (assoc ::previous request)))
(defn printed-response [name {::keys [previous] :as response}]
(printer/print-doc (diff-doc :response name (polish previous) (polish response)) printer)
(-> response
(update ::original (fnil identity response))
(assoc ::previous response)
(cond-> (nil? name) (dissoc ::original ::previous))))
(defn print-diff-middleware (defn print-diff-middleware
([] ([]
(print-diff-middleware nil)) (print-diff-middleware nil))
@ -32,12 +39,13 @@
:wrap (fn [handler] :wrap (fn [handler]
(fn (fn
([request] ([request]
(handler (printed-request name request))) (printed-response name (handler (printed-request name request))))
([request respond raise] ([request respond raise]
(handler (printed-request name request) respond raise))))})) (handler (printed-request name request) (comp respond (partial printed-response name)) raise))))}))
(defn print-request-diffs (defn print-request-diffs
"A middleware chain transformer that adds a request-diff printer between all middleware" "A middleware chain transformer that adds a request & response diff
printer between all middleware."
[chain] [chain]
(reduce (reduce
(fn [chain mw] (fn [chain mw]

View file

@ -118,17 +118,17 @@
(defn create-exception-middleware (defn create-exception-middleware
"Creates a Middleware that catches all exceptions. Takes a map "Creates a Middleware that catches all exceptions. Takes a map
of `identifier => exception request => response` that is used to select of `identifier => exception request => response` that is used to select
the exception handler for the thown/raised exception identifier. Exception the exception handler for the thrown/raised exception identifier. Exception
idenfier is either a `Keyword` or a Exception Class. identifier is either a `Keyword` or a Exception Class.
The following handlers special handlers are available: The following handlers special handlers are available:
| key | description | key | description
|------------------------|------------- |------------------------|-------------
| `::exception/default` | a default exception handler if nothing else mathced (default [[default-handler]]). | `::exception/default` | a default exception handler if nothing else matched (default [[default-handler]]).
| `::exception/wrap` | a 3-arity handler to wrap the actual handler `handler exception request => response` | `::exception/wrap` | a 3-arity handler to wrap the actual handler `handler exception request => response`
The handler is selected from the options map by exception idenfiter The handler is selected from the options map by exception identifier
in the following lookup order: in the following lookup order:
1) `:type` of exception ex-data 1) `:type` of exception ex-data

View file

@ -1,4 +1,4 @@
(defproject metosin/reitit-pedestal "0.3.1" (defproject metosin/reitit-pedestal "0.3.9"
:description "Reitit + Pedestal Integration" :description "Reitit + Pedestal Integration"
:url "https://github.com/metosin/reitit" :url "https://github.com/metosin/reitit"
:license {:name "Eclipse Public License" :license {:name "Eclipse Public License"

View file

@ -4,18 +4,19 @@
[io.pedestal.http :as http] [io.pedestal.http :as http]
[reitit.interceptor] [reitit.interceptor]
[reitit.http]) [reitit.http])
(:import (reitit.interceptor Executor))) (:import (reitit.interceptor Executor)
(java.lang.reflect Method)))
(defn- arity [f] ;; TODO: variadic
(defn- arities [f]
(->> (class f) (->> (class f)
.getDeclaredMethods .getDeclaredMethods
(filter #(= "invoke" (.getName %))) (filter #(= "invoke" (.getName %)))
first (map #(alength (.getParameterTypes ^Method %)))
.getParameterTypes (set)))
alength))
(defn- error-with-arity-1? [{error-fn :error}] (defn- error-without-arity-2? [{error-fn :error}]
(and error-fn (= 1 (arity error-fn)))) (and error-fn (not (contains? (arities error-fn) 2))))
(defn- error-arity-2->1 [error] (defn- error-arity-2->1 [error]
(fn [context ex] (fn [context ex]
@ -26,23 +27,31 @@
(dissoc :error)) (dissoc :error))
context)))) context))))
(defn wrap-error-arity-2->1 [interceptor] (defn- wrap-error-arity-2->1 [interceptor]
(update interceptor :error error-arity-2->1)) (update interceptor :error error-arity-2->1))
(defn ->interceptor [interceptor]
(cond
(interceptor/interceptor? interceptor)
interceptor
(->> (select-keys interceptor [:enter :leave :error]) (vals) (keep identity) (seq))
(interceptor/interceptor
(if (error-without-arity-2? interceptor)
(wrap-error-arity-2->1 interceptor)
interceptor))))
;;
;; Public API
;;
(def pedestal-executor (def pedestal-executor
(reify (reify
Executor Executor
(queue [_ interceptors] (queue [_ interceptors]
(->> interceptors (->> interceptors
(map (fn [{:keys [::interceptor/handler] :as interceptor}] (map (fn [{::interceptor/keys [handler] :as interceptor}]
(or handler interceptor))) (or handler interceptor)))
(map (fn [interceptor] (keep ->interceptor)))
(if (interceptor/interceptor? interceptor)
interceptor
(interceptor/interceptor
(if (error-with-arity-1? interceptor)
(wrap-error-arity-2->1 interceptor)
interceptor)))))))
(enqueue [_ context interceptors] (enqueue [_ context interceptors]
(chain/enqueue context interceptors)))) (chain/enqueue context interceptors))))

View file

@ -1,4 +1,4 @@
(defproject metosin/reitit-ring "0.3.1" (defproject metosin/reitit-ring "0.3.9"
:description "Reitit: Ring routing" :description "Reitit: Ring routing"
:url "https://github.com/metosin/reitit" :url "https://github.com/metosin/reitit"
:license {:name "Eclipse Public License" :license {:name "Eclipse Public License"

View file

@ -28,8 +28,11 @@
(update acc method expand opts) (update acc method expand opts)
acc)) data http-methods)]) acc)) data http-methods)])
(defn compile-result [[path data] {:keys [::default-options-handler] :as opts}] (defn compile-result [[path data] {::keys [default-options-handler] :as opts}]
(let [[top childs] (group-keys data) (let [[top childs] (group-keys data)
childs (cond-> childs
(and (not (:options childs)) (not (:handler top)) default-options-handler)
(assoc :options {:no-doc true, :handler default-options-handler}))
->endpoint (fn [p d m s] ->endpoint (fn [p d m s]
(-> (middleware/compile-result [p d] opts s) (-> (middleware/compile-result [p d] opts s)
(map->Endpoint) (map->Endpoint)
@ -40,12 +43,7 @@
(fn [acc method] (fn [acc method]
(cond-> acc (cond-> acc
any? (assoc method (->endpoint path data method nil)))) any? (assoc method (->endpoint path data method nil))))
(map->Methods (map->Methods {})
{:options
(if default-options-handler
(->endpoint path (assoc data
:handler default-options-handler
:no-doc true) :options nil))})
http-methods))] http-methods))]
(if-not (seq childs) (if-not (seq childs)
(->methods true top) (->methods true top)
@ -56,10 +54,16 @@
(->methods (:handler top) data) (->methods (:handler top) data)
childs)))) childs))))
(defn default-options-handler [request] (def default-options-handler
(let [methods (->> request get-match :result (keep (fn [[k v]] (if v k)))) (let [handle (fn [request]
allow (->> methods (map (comp str/upper-case name)) (str/join ","))] (let [methods (->> request get-match :result (keep (fn [[k v]] (if v k))))
{:status 200, :body "", :headers {"Allow" allow}})) allow (->> methods (map (comp str/upper-case name)) (str/join ","))]
{:status 200, :body "", :headers {"Allow" allow}}))]
(fn
([request]
(handle request))
([request respond _]
(respond (handle request))))))
;; ;;
;; public api ;; public api
@ -145,14 +149,14 @@
| key | description | | key | description |
| -----------------------|-------------| | -----------------------|-------------|
| `:not-found` | 404, no routes matches | `:not-found` | 404, no routes matches
| `:method-not-accepted` | 405, no method matches | `:method-not-allowed` | 405, no method matches
| `:not-acceptable` | 406, handler returned `nil`" | `:not-acceptable` | 406, handler returned `nil`"
([] ([]
(create-default-handler (create-default-handler {}))
{:not-found (constantly {:status 404, :body "", :headers {}}) ([{:keys [not-found method-not-allowed not-acceptable]
:method-not-allowed (constantly {:status 405, :body "", :headers {}}) :or {not-found (constantly {:status 404, :body "", :headers {}})
:not-acceptable (constantly {:status 406, :body "", :headers {}})})) method-not-allowed (constantly {:status 405, :body "", :headers {}})
([{:keys [not-found method-not-allowed not-acceptable]}] not-acceptable (constantly {:status 406, :body "", :headers {}})}}]
(fn (fn
([request] ([request]
(if-let [match (::r/match request)] (if-let [match (::r/match request)]

View file

@ -25,7 +25,13 @@
{:name ::coerce-request {:name ::coerce-request
:spec ::rs/parameters :spec ::rs/parameters
:compile (fn [{:keys [coercion parameters]} opts] :compile (fn [{:keys [coercion parameters]} opts]
(if (and coercion parameters) (cond
;; no coercion, skip
(not coercion) nil
;; just coercion, don't mount
(not parameters) {}
;; mount
:else
(let [coercers (coercion/request-coercers coercion parameters opts)] (let [coercers (coercion/request-coercers coercion parameters opts)]
(fn [handler] (fn [handler]
(fn (fn
@ -43,7 +49,13 @@
{:name ::coerce-response {:name ::coerce-response
:spec ::rs/responses :spec ::rs/responses
:compile (fn [{:keys [coercion responses]} opts] :compile (fn [{:keys [coercion responses]} opts]
(if (and coercion responses) (cond
;; no coercion, skip
(not coercion) nil
;; just coercion, don't mount
(not responses) {}
;; mount
:else
(let [coercers (coercion/response-coercers coercion responses opts)] (let [coercers (coercion/response-coercers coercion responses opts)]
(fn [handler] (fn [handler]
(fn (fn

View file

@ -9,10 +9,19 @@
;; ;;
(s/def ::middleware (s/coll-of #(satisfies? middleware/IntoMiddleware %))) (s/def ::middleware (s/coll-of #(satisfies? middleware/IntoMiddleware %)))
(s/def ::get map?)
(s/def ::head map?)
(s/def ::post map?)
(s/def ::put map?)
(s/def ::delete map?)
(s/def ::connect map?)
(s/def ::options map?)
(s/def ::trace map?)
(s/def ::patch map?)
(s/def ::data (s/def ::data
(s/keys :req-un [::rs/handler] (s/keys :opt-un [::rs/handler ::rs/name ::rs/no-doc ::middleware]))
:opt-un [::rs/name ::middleware]))
;; ;;
;; Validator ;; Validator
@ -26,21 +35,21 @@
:invalid non-specs})) :invalid non-specs}))
(s/merge-spec-impl (vec specs) (vec specs) nil)) (s/merge-spec-impl (vec specs) (vec specs) nil))
(defn validate-route-data [routes key spec] (defn validate-route-data [routes key wrap spec]
(->> (for [[p _ c] routes (->> (for [[p _ c] routes
[method {:keys [data] :as endpoint}] c [method {:keys [data] :as endpoint}] c
:when endpoint :when endpoint
:let [target (key endpoint) :let [target (key endpoint)
component-specs (seq (keep :spec target)) component-specs (seq (keep :spec target))
specs (keep identity (into [spec] component-specs)) specs (keep identity (into [spec] component-specs))
spec (merge-specs specs)]] spec (wrap (merge-specs specs))]]
(when-let [problems (and spec (s/explain-data spec data))] (when-let [problems (and spec (s/explain-data spec data))]
(rs/->Problem p method data spec problems))) (rs/->Problem p method data spec problems)))
(keep identity) (seq))) (keep identity) (seq)))
(defn validate (defn validate
[routes {:keys [spec] :or {spec ::data}}] [routes {:keys [spec ::rs/wrap] :or {spec ::data, wrap identity}}]
(when-let [problems (validate-route-data routes :middleware spec)] (when-let [problems (validate-route-data routes :middleware wrap spec)]
(exception/fail! (exception/fail!
::invalid-route-data ::rs/invalid-route-data
{:problems problems}))) {:problems problems})))

Some files were not shown because too many files have changed in this diff Show more