mirror of
https://github.com/metosin/reitit.git
synced 2025-12-16 16:01:11 +00:00
New reitit-dev module for pretty errors
This commit is contained in:
parent
59560860d8
commit
a2843dd097
22 changed files with 659 additions and 241 deletions
61
CHANGELOG.md
61
CHANGELOG.md
|
|
@ -14,9 +14,51 @@
|
|||
* backed by a new `:trie-router`, replacing `:segment-router`
|
||||
* [over 40% faster](https://metosin.github.io/reitit/performance.html) on the JVM
|
||||
|
||||
## `reitit-frontend`
|
||||
* **BREAKING**: `reitit.spec/validate-spec!` has been renamed to `validate`
|
||||
|
||||
* **BREAKING** New frontend controllers:
|
||||
### `reitit-dev`
|
||||
|
||||
* new module for friendly router creation time exception handling
|
||||
* new option `:exception` in `r/router`, of type `Exception => Exception` (default `reitit.exception/exception`)
|
||||
* new exception pretty-printer `reitit.dev.pretty/exception`, based on [fipp](https://github.com/brandonbloom/fipp) and [expund](https://github.com/bhb/expound) for human readable, newbie-friendly errors.
|
||||
|
||||
#### Conflicting paths
|
||||
|
||||
```clj
|
||||
(require '[reitit.core :as r])
|
||||
(require '[reitit.dev.pretty :as pretty])
|
||||
|
||||
(r/router
|
||||
[["/ping"]
|
||||
["/:user-id/orders"]
|
||||
["/bulk/:bulk-id"]
|
||||
["/public/*path"]
|
||||
["/:version/status"]]
|
||||
{:exception pretty/exception})
|
||||
```
|
||||
|
||||
<img src="https://gist.githubusercontent.com/ikitommi/ff9b091ffe87880d9847c9832bbdd3d2/raw/0e185e07e4ac49109bb653b4ad4656896cb41b2f/path-conflicts.png" width=640>
|
||||
|
||||
#### Route data error
|
||||
|
||||
```clj
|
||||
(require '[reitit.spec :as spec])
|
||||
(require '[clojure.spec.alpha :as s])
|
||||
|
||||
(s/def ::role #{:admin :user})
|
||||
(s/def ::roles (s/coll-of ::role :into #{}))
|
||||
|
||||
(r/router
|
||||
["/api/admin" {::roles #{:adminz}}]
|
||||
{:validate spec/validate
|
||||
:exception pretty/exception})
|
||||
```
|
||||
|
||||
<img src="https://gist.githubusercontent.com/ikitommi/ff9b091ffe87880d9847c9832bbdd3d2/raw/0e185e07e4ac49109bb653b4ad4656896cb41b2f/route-data-error.png" width=640>
|
||||
|
||||
### `reitit-frontend`
|
||||
|
||||
* **BREAKING**: Frontend controllers redesigned
|
||||
* Controller `:params` function has been deprecated
|
||||
* Controller `:identity` function works the same as `:params`
|
||||
* New `:parameters` option can be used to declare which parameters
|
||||
|
|
@ -24,13 +66,24 @@
|
|||
use cases: `{:start start-fn, :parameters {:path [:foo-id]}}`
|
||||
* Ensure HTML5 History routing works with IE11
|
||||
|
||||
## `reitit-ring`
|
||||
### `reitit-ring`
|
||||
|
||||
* Allow Middleware to compile to `nil` with Middleware Registries, fixes to [#216](https://github.com/metosin/reitit/issues/216).
|
||||
* **BREAKING**: `reitit.ring.spec/validate-spec!` has been renamed to `validate`
|
||||
|
||||
## `reitit-http`
|
||||
### `reitit-http`
|
||||
|
||||
* Allow Interceptors to compile to `nil` with Interceptor Registries, related to [#216](https://github.com/metosin/reitit/issues/216).
|
||||
* **BREAKING**: `reitit.http.spec/validate-spec!` has been renamed to `validate`
|
||||
|
||||
## Dependencies
|
||||
|
||||
* updated:
|
||||
|
||||
```clj
|
||||
[metosin/spec-tools "0.9.0"] is available but we use "0.8.3"
|
||||
[metosin/schema-tools "0.11.0"] is available but we use "0.10.5"
|
||||
```
|
||||
|
||||
## 0.2.13 (2019-01-26)
|
||||
|
||||
|
|
|
|||
33
README.md
33
README.md
|
|
@ -1,6 +1,5 @@
|
|||
# reitit [](https://circleci.com/gh/metosin/reitit) [](https://cljdoc.xyz/jump/release/metosin/reitit)
|
||||
|
||||
|
||||
A fast data-driven router for Clojure(Script).
|
||||
|
||||
* Simple data-driven [route syntax](https://metosin.github.io/reitit/basics/route_syntax.html)
|
||||
|
|
@ -36,6 +35,7 @@ There is [#reitit](https://clojurians.slack.com/messages/reitit/) in [Clojurians
|
|||
* `reitit-http` http-routing with Interceptors
|
||||
* `reitit-interceptors` - [common interceptors](https://metosin.github.io/reitit/http/default_interceptors.html)
|
||||
* `reitit-sieppari` support for [Sieppari](https://github.com/metosin/sieppari)
|
||||
* `reitit-dev` - development utilities
|
||||
|
||||
## Extra modules
|
||||
|
||||
|
|
@ -49,36 +49,7 @@ All main modules bundled:
|
|||
[metosin/reitit "0.2.13"]
|
||||
```
|
||||
|
||||
Optionally, the parts can be required separately:
|
||||
|
||||
```clj
|
||||
[metosin/reitit-core "0.2.13"]
|
||||
|
||||
;; coercion
|
||||
[metosin/reitit-spec "0.2.13"]
|
||||
[metosin/reitit-schema "0.2.13"]
|
||||
|
||||
;; ring helpers
|
||||
[metosin/reitit-ring "0.2.13"]
|
||||
[metosin/reitit-middleware "0.2.13"]
|
||||
|
||||
;; swagger-support for ring & http
|
||||
[metosin/reitit-swagger "0.2.13"]
|
||||
[metosin/reitit-swagger-ui "0.2.13"]
|
||||
|
||||
;; frontend helpers
|
||||
[metosin/reitit-frontend "0.2.13"]
|
||||
|
||||
;; http with interceptors
|
||||
[metosin/reitit-http "0.2.13"]
|
||||
[metosin/reitit-interceptors "0.2.13"]
|
||||
[metosin/reitit-sieppari "0.2.13"]
|
||||
```
|
||||
|
||||
```clj
|
||||
;; pedestal
|
||||
[metosin/reitit-pedestal "0.2.13"]
|
||||
```
|
||||
Optionally, the parts can be required separately.
|
||||
|
||||
## Quick start
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@
|
|||
* Modular
|
||||
* [Fast](performance.md)
|
||||
|
||||
There is [#reitit](https://clojurians.slack.com/messages/reitit/) in [Clojurians Slack](http://clojurians.net/) for discussion & help.
|
||||
|
||||
## Main Modules
|
||||
|
||||
* `reitit` - all bundled
|
||||
|
|
@ -26,6 +28,7 @@
|
|||
* `reitit-http` http-routing with Pedestal-style Interceptors
|
||||
* `reitit-interceptors` - [common interceptors](./http/default_interceptors.md) for `reitit-http`
|
||||
* `reitit-sieppari` support for [Sieppari](https://github.com/metosin/sieppari) Interceptors
|
||||
* `reitit-dev` - development utilities
|
||||
|
||||
## Extra modules
|
||||
|
||||
|
|
@ -39,38 +42,7 @@ All bundled:
|
|||
[metosin/reitit "0.2.13"]
|
||||
```
|
||||
|
||||
Optionally, the parts can be required separately:
|
||||
|
||||
```clj
|
||||
[metosin/reitit-core "0.2.13"]
|
||||
|
||||
;; coercion
|
||||
[metosin/reitit-spec "0.2.13"]
|
||||
[metosin/reitit-schema "0.2.13"]
|
||||
|
||||
;; ring helpers
|
||||
[metosin/reitit-ring "0.2.13"]
|
||||
[metosin/reitit-middleware "0.2.13"]
|
||||
|
||||
;; swagger-support for ring & http
|
||||
[metosin/reitit-swagger "0.2.13"]
|
||||
[metosin/reitit-swagger-ui "0.2.13"]
|
||||
|
||||
;; frontend helpers
|
||||
[metosin/reitit-frontend "0.2.13"]
|
||||
|
||||
;; http with interceptors
|
||||
[metosin/reitit-http "0.2.13"]
|
||||
[metosin/reitit-interceptors "0.2.13"]
|
||||
[metosin/reitit-sieppari "0.2.13"]
|
||||
```
|
||||
|
||||
```clj
|
||||
;; pedestal
|
||||
[metosin/reitit-pedestal "0.2.13"]
|
||||
```
|
||||
|
||||
There is [#reitit](https://clojurians.slack.com/messages/reitit/) in [Clojurians Slack](http://clojurians.net/) for discussion & help.
|
||||
Optionally, the parts can be required separately.
|
||||
|
||||
# Examples
|
||||
|
||||
|
|
|
|||
|
|
@ -4,13 +4,14 @@ Routers can be configured via options. The following options are available for t
|
|||
|
||||
| key | description |
|
||||
|--------------|-------------|
|
||||
| `:path` | Base-path for routes |
|
||||
| `:routes` | Initial resolved routes (default `[]`) |
|
||||
| `:data` | Initial route data (default `{}`) |
|
||||
| `:spec` | clojure.spec definition for a route data, see `reitit.spec` on how to use this |
|
||||
| `: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` |
|
||||
| `:compile` | Function of `route opts => result` to compile a route handler |
|
||||
| `:validate` | Function of `routes opts => ()` to validate route (data) via side-effects |
|
||||
| `:conflicts` | Function of `{route #{route}} => ()` to handle conflicting routes (default `reitit.core/throw-on-conflicts!`) |
|
||||
| `:router` | Function of `routes opts => router` to override the actual router implementation |
|
||||
| `:path` | Base-path for routes
|
||||
| `:routes` | Initial resolved routes (default `[]`)
|
||||
| `:data` | Initial route data (default `{}`)
|
||||
| `:spec` | clojure.spec definition for a route data, see `reitit.spec` on how to use this
|
||||
| `: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`
|
||||
| `:compile` | Function of `route opts => result` to compile a route handler
|
||||
| `:validate` | Function of `routes opts => ()` to validate route (data) via side-effects
|
||||
| `:conflicts` | Function of `{route #{route}} => ()` to handle conflicting routes
|
||||
| `:exception` | Function of `Exception => Exception ` to handle creation time exceptions (default `reitit.exception/exception`)
|
||||
| `:router` | Function of `routes opts => router` to override the actual router implementation
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ But there is a better way. Router has a `:validation` hook to validate the whole
|
|||
|
||||
## clojure.spec
|
||||
|
||||
Namespace `reitit.spec` contains specs for main parts of `reitit.core` and a helper function `validate-spec!` that runs spec validation for all route data and throws an exception if any errors are found.
|
||||
Namespace `reitit.spec` contains specs for main parts of `reitit.core` and a helper function `validate` that runs spec validation for all route data and throws an exception if any errors are found.
|
||||
|
||||
A Router with invalid route data:
|
||||
|
||||
|
|
@ -27,7 +27,7 @@ Fails fast with `clojure.spec` validation turned on:
|
|||
|
||||
(r/router
|
||||
["/api" {:handler "identity"}]
|
||||
{:validate rs/validate-spec!})
|
||||
{:validate rs/validate})
|
||||
; CompilerException clojure.lang.ExceptionInfo: Invalid route data:
|
||||
;
|
||||
; -- On route -----------------------
|
||||
|
|
@ -42,7 +42,7 @@ Fails fast with `clojure.spec` validation turned on:
|
|||
|
||||
### Customizing spec validation
|
||||
|
||||
`rs/validate-spec!` reads the following router options:
|
||||
`rs/validate` reads the following router options:
|
||||
|
||||
| key | description |
|
||||
| ---------------|-------------|
|
||||
|
|
@ -64,7 +64,7 @@ Below is an example of using [expound](https://github.com/bhb/expound) to pretty
|
|||
["/api" {:handler identity
|
||||
::roles #{:adminz}}]
|
||||
{::rs/explain e/expound-str
|
||||
:validate rs/validate-spec!})
|
||||
:validate rs/validate})
|
||||
; CompilerException clojure.lang.ExceptionInfo: Invalid route data:
|
||||
;
|
||||
; -- On route -----------------------
|
||||
|
|
@ -102,7 +102,7 @@ Explicitly requiring a `::roles` key in a route data:
|
|||
["/api" {:handler identity}]
|
||||
{:spec (s/merge (s/keys :req [::roles]) ::rs/default-data)
|
||||
::rs/explain e/expound-str
|
||||
:validate rs/validate-spec!})
|
||||
:validate rs/validate})
|
||||
; CompilerException clojure.lang.ExceptionInfo: Invalid route data:
|
||||
;
|
||||
; -- On route -----------------------
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
Ring route validation works [just like with core router](../basics/route_data_validation.md), with few differences:
|
||||
|
||||
* `reitit.ring.spec/validate-spec!` should be used instead of `reitit.spec/validate-spec!` - to support validating all endpoints (`:get`, `:post` etc.)
|
||||
* `reitit.ring.spec/validate` should be used instead of `reitit.spec/validate` - to support validating all endpoints (`:get`, `:post` etc.)
|
||||
* With `clojure.spec` validation, Middleware can contribute to route spec via `:specs` key. The effective route data spec is router spec merged with middleware specs.
|
||||
|
||||
## Example
|
||||
|
|
@ -28,7 +28,7 @@ A simple app with spec-validation turned on:
|
|||
["/internal"
|
||||
["/users" {:get {:handler handler}
|
||||
:delete {:handler handler}}]]]
|
||||
{:validate rrs/validate-spec!
|
||||
{:validate rrs/validate
|
||||
::rs/explain e/expound-str})))
|
||||
```
|
||||
|
||||
|
|
@ -69,7 +69,7 @@ Missing route data fails fast at router creation:
|
|||
["/internal"
|
||||
["/users" {:get {:handler handler}
|
||||
:delete {:handler handler}}]]]
|
||||
{:validate rrs/validate-spec!
|
||||
{:validate rrs/validate
|
||||
::rs/explain e/expound-str})))
|
||||
; CompilerException clojure.lang.ExceptionInfo: Invalid route data:
|
||||
;
|
||||
|
|
@ -133,7 +133,7 @@ Adding the `:zone` to route data fixes the problem:
|
|||
["/internal" {:zone :internal} ;; <--- added
|
||||
["/users" {:get {:handler handler}
|
||||
:delete {:handler handler}}]]]
|
||||
{:validate rrs/validate-spec!
|
||||
{:validate rrs/validate
|
||||
::rs/explain e/expound-str})))
|
||||
|
||||
(app {:request-method :get
|
||||
|
|
@ -175,7 +175,7 @@ Let's reuse the `wrap-enforce-roles` from [Dynamic extensions](dynamic_extension
|
|||
["/internal" {:zone :internal}
|
||||
["/users" {:get {:handler handler}
|
||||
:delete {:handler handler}}]]]
|
||||
{:validate rrs/validate-spec!
|
||||
{:validate rrs/validate
|
||||
::rs/explain e/expound-str})))
|
||||
|
||||
(app {:request-method :get
|
||||
|
|
@ -199,7 +199,7 @@ But fails if they are present and invalid:
|
|||
::roles #{:manager} ;; <--- added
|
||||
:delete {:handler handler
|
||||
::roles #{:adminz}}}]]] ;; <--- added
|
||||
{:validate rrs/validate-spec!
|
||||
{:validate rrs/validate
|
||||
::rs/explain e/expound-str})))
|
||||
; CompilerException clojure.lang.ExceptionInfo: Invalid route data:
|
||||
;
|
||||
|
|
@ -240,7 +240,7 @@ Ability to define (and reuse) route-data in mid-paths is a powerful feature, but
|
|||
::roles #{:manager}}
|
||||
:delete {:handler handler
|
||||
::roles #{:admin}}}]]]
|
||||
{:validate rrs/validate-spec!
|
||||
{:validate rrs/validate
|
||||
::rs/explain e/expound-str})))
|
||||
```
|
||||
|
||||
|
|
@ -261,7 +261,7 @@ Or even flatten the routes:
|
|||
::roles #{:manager}}
|
||||
:delete {:handler handler
|
||||
::roles #{:admin}}}]]
|
||||
{:validate rrs/validate-spec!
|
||||
{:validate rrs/validate
|
||||
::rs/explain e/expound-str})))
|
||||
```
|
||||
|
||||
|
|
@ -279,6 +279,6 @@ The common Middleware can also be pushed to the router, here cleanly separing be
|
|||
:delete {:handler handler
|
||||
::roles #{:admin}}}]]
|
||||
{:data {:middleware [zone-middleware wrap-enforce-roles]}
|
||||
:validate rrs/validate-spec!
|
||||
:validate rrs/validate
|
||||
::rs/explain e/expound-str})))
|
||||
```
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
(ns reitit.core
|
||||
(:require [clojure.string :as str]
|
||||
[reitit.impl :as impl]
|
||||
(:require [reitit.impl :as impl]
|
||||
[reitit.exception :as exception]
|
||||
[reitit.trie :as trie]))
|
||||
|
||||
|
|
@ -32,27 +31,6 @@
|
|||
nil
|
||||
(expand [_ _]))
|
||||
|
||||
;;
|
||||
;; Conflicts
|
||||
;;
|
||||
|
||||
(defn path-conflicts-str [conflicts]
|
||||
(apply str "Router contains conflicting route paths:\n\n"
|
||||
(mapv
|
||||
(fn [[[path] vals]]
|
||||
(str " " path "\n-> " (str/join "\n-> " (mapv first vals)) "\n\n"))
|
||||
conflicts)))
|
||||
|
||||
(defn name-conflicts-str [conflicts]
|
||||
(apply str "Router contains conflicting route names:\n\n"
|
||||
(mapv
|
||||
(fn [[name vals]]
|
||||
(str name "\n-> " (str/join "\n-> " (mapv first vals)) "\n\n"))
|
||||
conflicts)))
|
||||
|
||||
(defn throw-on-conflicts! [f conflicts]
|
||||
(exception/fail! (f conflicts) {:conflicts conflicts}))
|
||||
|
||||
;;
|
||||
;; Router
|
||||
;;
|
||||
|
|
@ -359,7 +337,8 @@
|
|||
:expand expand
|
||||
:coerce (fn coerce [route _] route)
|
||||
:compile (fn compile [[_ {:keys [handler]}] _] handler)
|
||||
:conflicts (fn throw! [conflicts] (throw-on-conflicts! path-conflicts-str conflicts))})
|
||||
:exception exception/exception
|
||||
:conflicts (fn throw! [conflicts] (exception/fail! :path-conflicts conflicts))})
|
||||
|
||||
(defn router
|
||||
"Create a [[Router]] from raw route data and optionally an options map.
|
||||
|
|
@ -376,33 +355,39 @@
|
|||
| `: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
|
||||
| `:validate` | Function of `routes opts => ()` to validate route (data) via side-effects
|
||||
| `:conflicts` | Function of `{route #{route}} => ()` to handle conflicting routes (default `reitit.core/throw-on-conflicts!`)
|
||||
| `:conflicts` | Function of `{route #{route}} => ()` to handle conflicting routes
|
||||
| `:exception` | Function of `Exception => Exception ` to handle creation time exceptions (default `reitit.exception/exception`)
|
||||
| `:router` | Function of `routes opts => router` to override the actual router implementation"
|
||||
([raw-routes]
|
||||
(router raw-routes {}))
|
||||
([raw-routes opts]
|
||||
(let [{:keys [router] :as opts} (merge (default-router-options) opts)
|
||||
routes (impl/resolve-routes raw-routes opts)
|
||||
path-conflicting (impl/path-conflicting-routes routes)
|
||||
name-conflicting (impl/name-conflicting-routes routes)
|
||||
compiled-routes (impl/compile-routes routes opts)
|
||||
wilds? (boolean (some impl/wild-route? compiled-routes))
|
||||
all-wilds? (every? impl/wild-route? compiled-routes)
|
||||
router (cond
|
||||
router router
|
||||
(and (= 1 (count compiled-routes)) (not wilds?)) single-static-path-router
|
||||
path-conflicting quarantine-router
|
||||
(not wilds?) lookup-router
|
||||
all-wilds? trie-router
|
||||
:else mixed-router)]
|
||||
(let [{:keys [router] :as opts} (merge (default-router-options) opts)]
|
||||
(try
|
||||
(let [routes (impl/resolve-routes raw-routes opts)
|
||||
path-conflicting (impl/path-conflicting-routes routes)
|
||||
name-conflicting (impl/name-conflicting-routes routes)
|
||||
compiled-routes (impl/compile-routes routes opts)
|
||||
wilds? (boolean (some impl/wild-route? compiled-routes))
|
||||
all-wilds? (every? impl/wild-route? compiled-routes)
|
||||
router (cond
|
||||
router router
|
||||
(and (= 1 (count compiled-routes)) (not wilds?)) single-static-path-router
|
||||
path-conflicting quarantine-router
|
||||
(not wilds?) lookup-router
|
||||
all-wilds? trie-router
|
||||
:else mixed-router)]
|
||||
|
||||
(when-let [conflicts (:conflicts opts)]
|
||||
(when path-conflicting (conflicts path-conflicting)))
|
||||
(when-let [conflicts (:conflicts opts)]
|
||||
(when path-conflicting (conflicts path-conflicting)))
|
||||
|
||||
(when name-conflicting
|
||||
(throw-on-conflicts! name-conflicts-str name-conflicting))
|
||||
(when name-conflicting
|
||||
(exception/fail! :name-conflicts name-conflicting))
|
||||
|
||||
(when-let [validate (:validate opts)]
|
||||
(validate compiled-routes opts))
|
||||
(when-let [validate (:validate opts)]
|
||||
(validate compiled-routes opts))
|
||||
|
||||
(router compiled-routes opts))))
|
||||
(router compiled-routes opts))
|
||||
|
||||
(catch #?(:clj Exception, :cljs js/Error) e
|
||||
(let [exception (:exception opts)]
|
||||
(throw (if exception (exception e) e))))))))
|
||||
|
|
|
|||
|
|
@ -1,7 +1,36 @@
|
|||
(ns reitit.exception)
|
||||
(ns reitit.exception
|
||||
(:require [clojure.string :as str]))
|
||||
|
||||
(defn fail!
|
||||
([message]
|
||||
(throw (ex-info message {::type :exeption})))
|
||||
([message data]
|
||||
(throw (ex-info message (merge {::type ::exeption} data)))))
|
||||
([type]
|
||||
(fail! type nil))
|
||||
([type data]
|
||||
(throw (ex-info (str type) {:type type, :data data}))))
|
||||
|
||||
(defmulti format-type (fn [type _ _] type))
|
||||
|
||||
(defn exception [e]
|
||||
(let [data (ex-data e)
|
||||
message (format-type (:type data) #?(:clj (.getMessage ^Exception e) :cljs (ex-message e)) (:data data))]
|
||||
(ex-info message (or data {}))))
|
||||
|
||||
;;
|
||||
;; Formatters
|
||||
;;
|
||||
|
||||
(defmethod format-type :default [_ message data]
|
||||
(str message (if data (str "\n\n" (pr-str data)))))
|
||||
|
||||
(defmethod format-type :path-conflicts [_ _ conflicts]
|
||||
(apply str "Router contains conflicting route paths:\n\n"
|
||||
(mapv
|
||||
(fn [[[path] vals]]
|
||||
(str " " path "\n-> " (str/join "\n-> " (mapv first vals)) "\n\n"))
|
||||
conflicts)))
|
||||
|
||||
(defmethod format-type :name-conflicts [_ _ conflicts]
|
||||
(apply str "Router contains conflicting route names:\n\n"
|
||||
(mapv
|
||||
(fn [[name vals]]
|
||||
(str name "\n-> " (str/join "\n-> " (mapv first vals)) "\n"))
|
||||
conflicts)))
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
(ns reitit.spec
|
||||
(:require [clojure.spec.alpha :as s]
|
||||
[clojure.spec.gen.alpha :as gen]
|
||||
[reitit.core :as reitit]
|
||||
[reitit.exception :as exception]))
|
||||
[reitit.exception :as exception]
|
||||
[reitit.core :as r]))
|
||||
|
||||
;;
|
||||
;; routes
|
||||
|
|
@ -45,7 +45,7 @@
|
|||
;; router
|
||||
;;
|
||||
|
||||
(s/def ::router reitit/router?)
|
||||
(s/def ::router r/router?)
|
||||
(s/def :reitit.router/path ::path)
|
||||
(s/def :reitit.router/routes ::routes)
|
||||
(s/def :reitit.router/data ::data)
|
||||
|
|
@ -66,7 +66,7 @@
|
|||
:reitit.router/conflicts
|
||||
:reitit.router/router])))
|
||||
|
||||
(s/fdef reitit/router
|
||||
(s/fdef r/router
|
||||
:args (s/or :1arity (s/cat :data (s/spec ::raw-routes))
|
||||
:2arity (s/cat :data (s/spec ::raw-routes), :opts ::opts))
|
||||
:ret ::router)
|
||||
|
|
@ -111,28 +111,24 @@
|
|||
|
||||
(defrecord Problem [path scope data spec problems])
|
||||
|
||||
(defn problems-str [problems explain]
|
||||
(defn validate-route-data [routes spec]
|
||||
(some->> (for [[p d _] routes]
|
||||
(when-let [problems (and spec (s/explain-data spec d))]
|
||||
(->Problem p nil d spec problems)))
|
||||
(keep identity) (seq) (vec)))
|
||||
|
||||
(defn validate [routes {:keys [spec] :or {spec ::default-data}}]
|
||||
(when-let [problems (validate-route-data routes spec)]
|
||||
(exception/fail!
|
||||
::invalid-route-data
|
||||
{:problems problems})))
|
||||
|
||||
(defmethod exception/format-type :reitit.spec/invalid-route-data [_ _ {:keys [problems]}]
|
||||
(apply str "Invalid route data:\n\n"
|
||||
(mapv
|
||||
(fn [{:keys [path scope data spec]}]
|
||||
(str "-- On route -----------------------\n\n"
|
||||
(pr-str path) (if scope (str " " (pr-str scope))) "\n\n" (explain spec data) "\n"))
|
||||
(pr-str path) (if scope (str " " (pr-str scope))) "\n\n"
|
||||
(pr-str data) "\n\n"
|
||||
(s/explain-str spec data) "\n"))
|
||||
problems)))
|
||||
|
||||
(defn throw-on-problems! [problems explain]
|
||||
(exception/fail!
|
||||
(problems-str problems explain)
|
||||
{:problems problems}))
|
||||
|
||||
(defn validate-route-data [routes spec]
|
||||
(->> (for [[p d _] routes]
|
||||
(when-let [problems (and spec (s/explain-data spec d))]
|
||||
(->Problem p nil d spec problems)))
|
||||
(keep identity) (seq)))
|
||||
|
||||
(defn validate-spec!
|
||||
[routes {:keys [spec ::explain]
|
||||
:or {explain s/explain-str
|
||||
spec ::default-data}}]
|
||||
(when-let [problems (validate-route-data routes spec)]
|
||||
(throw-on-problems! problems explain)))
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@
|
|||
(if (= to (count s))
|
||||
(concat ss (-static from to))
|
||||
(case (get s to)
|
||||
\{ (let [to' (or (str/index-of s "}" to) (ex/fail! (str "Unclosed brackets: " (pr-str s))))]
|
||||
\{ (let [to' (or (str/index-of s "}" to) (ex/fail! ::unclosed-brackets {:path s}))]
|
||||
(if (= \* (get s (inc to)))
|
||||
(recur (concat ss (-static from to) (-catch-all (inc to) to')) (long (inc to')) (long (inc to')))
|
||||
(recur (concat ss (-static from to) (-wild to to')) (long (inc to')) (long (inc to')))))
|
||||
|
|
@ -134,7 +134,7 @@
|
|||
(defn- -node [m]
|
||||
(map->Node (merge {:children {}, :wilds {}, :catch-all {}, :params {}} m)))
|
||||
|
||||
(defn- -insert [node [path & ps] params data]
|
||||
(defn- -insert [node [path & ps] fp params data]
|
||||
(let [node' (cond
|
||||
|
||||
(nil? path)
|
||||
|
|
@ -143,14 +143,14 @@
|
|||
(instance? Wild path)
|
||||
(let [next (first ps)]
|
||||
(if (or (instance? Wild next) (instance? CatchAll next))
|
||||
(ex/fail! (str "Two following wilds: " path ", " next))
|
||||
(update-in node [:wilds path] (fn [n] (-insert (or n (-node {})) ps params data)))))
|
||||
(ex/fail! ::following-parameters {:path fp, :parameters (map :value [path next])})
|
||||
(update-in node [:wilds path] (fn [n] (-insert (or n (-node {})) ps fp params data)))))
|
||||
|
||||
(instance? CatchAll path)
|
||||
(assoc-in node [:catch-all path] (-node {:params params, :data data}))
|
||||
|
||||
(str/blank? path)
|
||||
(-insert node ps params data)
|
||||
(-insert node ps fp params data)
|
||||
|
||||
:else
|
||||
(or
|
||||
|
|
@ -159,20 +159,20 @@
|
|||
(if-let [cp (common-prefix p path)]
|
||||
(if (= cp p)
|
||||
;; insert into child node
|
||||
(let [n' (-insert n (conj ps (subs path (count p))) params data)]
|
||||
(let [n' (-insert n (conj ps (subs path (count p))) fp params data)]
|
||||
(reduced (assoc-in node [:children p] n')))
|
||||
;; split child node
|
||||
(let [rp (subs p (count cp))
|
||||
rp' (subs path (count cp))
|
||||
n' (-insert (-node {}) ps params data)
|
||||
n'' (-insert (-node {:children {rp n, rp' n'}}) nil nil nil)]
|
||||
n' (-insert (-node {}) ps fp params data)
|
||||
n'' (-insert (-node {:children {rp n, rp' n'}}) nil nil nil nil)]
|
||||
(reduced (update node :children (fn [children]
|
||||
(-> children
|
||||
(dissoc p)
|
||||
(assoc cp n'')))))))))
|
||||
nil (:children node))
|
||||
;; new child node
|
||||
(assoc-in node [:children path] (-insert (-node {}) ps params data))))]
|
||||
(assoc-in node [:children path] (-insert (-node {}) ps fp params data))))]
|
||||
(if-let [child (get-in node' [:children ""])]
|
||||
;; optimize by removing empty paths
|
||||
(-> (merge-with merge (dissoc node' :data) child)
|
||||
|
|
@ -300,7 +300,7 @@
|
|||
([node path data]
|
||||
(let [parts (split-path path)
|
||||
params (zipmap (->> parts (remove string?) (map :value)) (repeat nil))]
|
||||
(-insert (or node (-node {})) (split-path path) params data))))
|
||||
(-insert (or node (-node {})) (split-path path) path params data))))
|
||||
|
||||
(defn compiler
|
||||
"Returns a default [[TrieCompiler]]."
|
||||
|
|
@ -312,18 +312,20 @@
|
|||
"Returns a compiled trie, to be used with [[pretty]] or [[path-matcher]]."
|
||||
([options]
|
||||
(compile options (compiler)))
|
||||
([{:keys [data params children wilds catch-all] :or {params {}}} compiler]
|
||||
([options compiler]
|
||||
(compile options compiler []))
|
||||
([{:keys [data params children wilds catch-all] :or {params {}}} compiler cp]
|
||||
(let [ends (fn [{:keys [children]}] (or (keys children) ["/"]))
|
||||
matchers (-> []
|
||||
(cond-> data (conj (data-matcher compiler params data)))
|
||||
(into (for [[p c] children] (static-matcher compiler p (compile c compiler))))
|
||||
(into (for [[p c] children] (static-matcher compiler p (compile c compiler (conj cp p)))))
|
||||
(into
|
||||
(for [[p c] wilds]
|
||||
(let [p (:value p)
|
||||
(let [pv (:value p)
|
||||
ends (ends c)]
|
||||
(if (next ends)
|
||||
(ex/fail! (str "Trie compliation error: wild " p " has two terminators: " ends))
|
||||
(wild-matcher compiler p (ffirst ends) (compile c compiler))))))
|
||||
(ex/fail! ::multiple-terminators {:terminators ends, :path (join-path (conj cp p))})
|
||||
(wild-matcher compiler pv (ffirst ends) (compile c compiler (conj cp pv)))))))
|
||||
(into (for [[p c] catch-all] (catch-all-matcher compiler (:value p) params (:data c)))))]
|
||||
(cond
|
||||
(> (count matchers) 1) (linear-matcher compiler matchers)
|
||||
|
|
|
|||
13
modules/reitit-dev/project.clj
Normal file
13
modules/reitit-dev/project.clj
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
(defproject metosin/reitit-dev "0.2.13"
|
||||
:description "Snappy data-driven router for Clojure(Script)"
|
||||
:url "https://github.com/metosin/reitit"
|
||||
:license {:name "Eclipse Public License"
|
||||
:url "http://www.eclipse.org/legal/epl-v10.html"}
|
||||
:scm {:name "git"
|
||||
:url "https://github.com/metosin/reitit"}
|
||||
:plugins [[lein-parent "0.3.2"]]
|
||||
:parent-project {:path "../../project.clj"
|
||||
:inherit [:deploy-repositories :managed-dependencies]}
|
||||
:dependencies [[metosin/reitit-core]
|
||||
[expound]
|
||||
[fipp]])
|
||||
333
modules/reitit-dev/src/reitit/dev/pretty.cljc
Normal file
333
modules/reitit-dev/src/reitit/dev/pretty.cljc
Normal file
|
|
@ -0,0 +1,333 @@
|
|||
(ns reitit.dev.pretty
|
||||
(:require [clojure.string :as str]
|
||||
[clojure.spec.alpha :as s]
|
||||
[arrangement.core]
|
||||
;; expound
|
||||
[expound.ansi]
|
||||
[expound.alpha]
|
||||
;; fipp
|
||||
[fipp.visit]
|
||||
[fipp.edn]
|
||||
[fipp.ednize]
|
||||
[fipp.engine]))
|
||||
|
||||
;;
|
||||
;; colors
|
||||
;;
|
||||
|
||||
(def colors
|
||||
{:white 255
|
||||
:text 253
|
||||
:grey 245
|
||||
:title-dark 32
|
||||
:title 45
|
||||
:red 217
|
||||
|
||||
:string 180
|
||||
:comment 243
|
||||
:doc 223
|
||||
:core-form 39
|
||||
:function-name 178
|
||||
:variable-name 85
|
||||
:constant 149
|
||||
:type 123
|
||||
:foreign 220
|
||||
:builtin 167
|
||||
:half-contrast 243
|
||||
:half-contrast-inverse 243
|
||||
:eldoc-varname 178
|
||||
:eldoc-separator 243
|
||||
:arglists 243
|
||||
:anchor 39
|
||||
:light-anchor 39
|
||||
:apropos-highlight 45
|
||||
:apropos-namespace 243
|
||||
:error 196})
|
||||
|
||||
(defn -color [color & text]
|
||||
(str "\033[38;5;" (colors color color) "m" (apply str text) "\u001B[0m"))
|
||||
|
||||
(comment
|
||||
(doseq [c (range 0 255)]
|
||||
(println (-color c "kikka") "->" c))
|
||||
|
||||
(doseq [[n c] colors]
|
||||
(println (-color c "kikka") "->" c n))
|
||||
|
||||
(doseq [[k v] expound.ansi/sgr-code]
|
||||
(println (expound.ansi/sgr "kikka" k) "->" k))
|
||||
)
|
||||
|
||||
(defn -start [x] (str "\033[38;5;" x "m"))
|
||||
(defn -end [] "\u001B[0m")
|
||||
|
||||
(defn color [color & text]
|
||||
[:span
|
||||
[:pass (-start (colors color))]
|
||||
(apply str text)
|
||||
[:pass (-end)]])
|
||||
|
||||
;;
|
||||
;; EDN
|
||||
;;
|
||||
|
||||
(defrecord EdnPrinter [symbols print-meta print-length print-level]
|
||||
|
||||
fipp.visit/IVisitor
|
||||
|
||||
(visit-unknown [this x]
|
||||
(fipp.visit/visit this (fipp.ednize/edn x)))
|
||||
|
||||
(visit-nil [this]
|
||||
(color :text "nil"))
|
||||
|
||||
(visit-boolean [this x]
|
||||
(color :text (str x)))
|
||||
|
||||
(visit-string [this x]
|
||||
(color :string (pr-str x)))
|
||||
|
||||
(visit-character [this x]
|
||||
(color :text (pr-str x)))
|
||||
|
||||
(visit-symbol [this x]
|
||||
(color :text (str x)))
|
||||
|
||||
(visit-keyword [this x]
|
||||
(color :constant (pr-str x)))
|
||||
|
||||
(visit-number [this x]
|
||||
(color :text (pr-str x)))
|
||||
|
||||
(visit-seq [this x]
|
||||
(if-let [pretty (symbols (first x))]
|
||||
(pretty this x)
|
||||
(fipp.edn/pretty-coll this (color :text "(") x :line (color :text ")") fipp.visit/visit)))
|
||||
|
||||
(visit-vector [this x]
|
||||
(fipp.edn/pretty-coll this (color :text "[") x :line (color :text "]") fipp.visit/visit))
|
||||
|
||||
(visit-map [this x]
|
||||
(let [xs (sort-by identity (fn [a b] (arrangement.core/rank (first a) (first b))) x)]
|
||||
(fipp.edn/pretty-coll this (color :text "{") xs [:span (color :text ",") :line] (color :text "}")
|
||||
(fn [printer [k v]]
|
||||
[:span (fipp.visit/visit printer k) " " (fipp.visit/visit printer v)]))))
|
||||
|
||||
(visit-set [this x]
|
||||
(let [xs (sort-by identity (fn [a b] (arrangement.core/rank a b)) x)]
|
||||
(fipp.edn/pretty-coll this "#{" xs :line "}" fipp.visit/visit)))
|
||||
|
||||
(visit-tagged [this {:keys [tag form]}]
|
||||
(let [object? (= 'object tag)
|
||||
tag-f (if (map? form) (partial color :type) identity)]
|
||||
[:group "#" (tag-f (pr-str tag))
|
||||
(when (or (and print-meta (meta form)) (not (coll? form)))
|
||||
" ")
|
||||
(if object?
|
||||
[:group "["
|
||||
[:align
|
||||
(color :type (first form)) :line
|
||||
(color :text (second form)) :line
|
||||
(fipp.visit/visit this (last form))] "]"]
|
||||
(fipp.visit/visit this form))]))
|
||||
|
||||
(visit-meta [this m x]
|
||||
(if print-meta
|
||||
[:align [:span "^" (fipp.visit/visit this m)] :line (fipp.visit/visit* this x)]
|
||||
(fipp.visit/visit* this x)))
|
||||
|
||||
(visit-var [this x]
|
||||
[:text (str x)])
|
||||
|
||||
(visit-pattern [this x]
|
||||
[:text (pr-str x)])
|
||||
|
||||
(visit-record [this x]
|
||||
(fipp.visit/visit this (fipp.ednize/record->tagged x))))
|
||||
|
||||
(defn ->printer
|
||||
([]
|
||||
(->printer nil))
|
||||
([options]
|
||||
(map->EdnPrinter
|
||||
(merge
|
||||
{:width 80
|
||||
:symbols {}
|
||||
:print-length *print-length*
|
||||
:print-level *print-level*
|
||||
:print-meta *print-meta*}
|
||||
options))))
|
||||
|
||||
(defn pprint
|
||||
([x] (pprint x {}))
|
||||
([x options]
|
||||
(let [printer (->printer (dissoc options :margin))
|
||||
margin (apply str (take (:margin options 0) (repeat " ")))]
|
||||
(binding [*print-meta* false]
|
||||
(fipp.engine/pprint-document [:group margin [:group (fipp.visit/visit printer x)]] options)))))
|
||||
|
||||
(defn print-doc [doc printer]
|
||||
(fipp.engine/pprint-document doc {:width (:width printer)}))
|
||||
|
||||
(defn repeat-str [s n]
|
||||
(apply str (take n (repeat s))))
|
||||
|
||||
;; TODO: this is hack, but seems to work and is safe.
|
||||
(defn source-str [[target _ file line]]
|
||||
(try
|
||||
(if (and (not= 1 line))
|
||||
(let [file-name (str/replace file #"(.*?)\.\S[^\.]+" "$1")
|
||||
target-name (name target)
|
||||
ns (str (subs target-name 0 (str/index-of target-name (str "user" "$"))) file-name)]
|
||||
(str ns ":" line))
|
||||
"repl")
|
||||
(catch #?(:clj Exception, :cljs js/Error) _
|
||||
"unknown")))
|
||||
|
||||
(defn title [message source {:keys [width]}]
|
||||
(let [between (- width (count message) 8 (count source))]
|
||||
[:group
|
||||
(color :title-dark "-- ")
|
||||
(color :title message " ")
|
||||
(color :title-dark (repeat-str "-" between) " ")
|
||||
(color :title source) " "
|
||||
(color :title-dark (str "--"))]))
|
||||
|
||||
(defn footer [{:keys [width]}]
|
||||
(color :title-dark (repeat-str "-" width)))
|
||||
|
||||
(defn text [& text]
|
||||
(apply color :text text))
|
||||
|
||||
(defn edn
|
||||
([x] (edn x {}))
|
||||
([x options]
|
||||
(with-out-str (pprint x options))))
|
||||
|
||||
(defn exception-str [message source printer]
|
||||
(with-out-str
|
||||
(print-doc
|
||||
[:group
|
||||
(title "Router creation failed" source printer)
|
||||
[:break] [:break]
|
||||
message
|
||||
[:break]
|
||||
(footer printer)]
|
||||
printer)))
|
||||
|
||||
(defmulti format-type (fn [type _ _] type))
|
||||
|
||||
(defn exception [e]
|
||||
(let [data (-> e ex-data :data)
|
||||
message (format-type (-> e ex-data :type) #?(:clj (.getMessage ^Exception e) :cljs (ex-message e)) data)
|
||||
source (->> e Throwable->map :trace
|
||||
(drop-while #(not= (name (first %)) "reitit.core$router"))
|
||||
(drop-while #(= (name (first %)) "reitit.core$router"))
|
||||
next first source-str)]
|
||||
(ex-info (exception-str message source (->printer)) (or data {}))))
|
||||
|
||||
(defn de-expound-colors [^String s mappings]
|
||||
(let [s' (reduce
|
||||
(fn [s [from to]]
|
||||
(.replace ^String s
|
||||
^String (expound.ansi/esc [from])
|
||||
^String (-start (colors to))))
|
||||
s mappings)]
|
||||
(.replace ^String s'
|
||||
^String (expound.ansi/esc [:none])
|
||||
(str (expound.ansi/esc [:none]) (-start (colors :text))))))
|
||||
|
||||
(defn fippify [s]
|
||||
[:align
|
||||
(-> s
|
||||
(de-expound-colors {:cyan :grey
|
||||
:red :red
|
||||
:magenta :grey
|
||||
:green :constant})
|
||||
(str/split #"\n") (interleave (repeat [:break])))])
|
||||
|
||||
(defn indent [x n]
|
||||
[:group (repeat-str " " n) [:align x]])
|
||||
|
||||
(def expound-printer
|
||||
(expound.alpha/custom-printer
|
||||
{:theme :figwheel-theme
|
||||
:show-valid-values? false
|
||||
:print-specs? false}))
|
||||
|
||||
;;
|
||||
;; Formatters
|
||||
;;
|
||||
|
||||
(defmethod format-type :default [_ message data]
|
||||
(into [:group (text message)] (if data [[:break] [:break] (edn data)])))
|
||||
|
||||
(defmethod format-type :path-conflicts [_ _ conflicts]
|
||||
[:group
|
||||
(text "Router contains conflicting route paths:")
|
||||
[:break] [:break]
|
||||
(into
|
||||
[:group]
|
||||
(mapv
|
||||
(fn [[[path] vals]]
|
||||
[:group
|
||||
[:span " " (text path)]
|
||||
[:break]
|
||||
(into
|
||||
[:group]
|
||||
(map
|
||||
(fn [p] [:span (color :grey "-> " p) [:break]])
|
||||
(mapv first vals)))
|
||||
[:break]])
|
||||
conflicts))
|
||||
[:span (text "Either fix the conflicting paths or disable the conflict resolution")
|
||||
[:break] (text "by setting a router option: ") [:break] [:break]
|
||||
(edn {:conflicts nil} {:margin 3})]
|
||||
[:break]
|
||||
(color :white "https://cljdoc.org/d/metosin/reitit/CURRENT/doc/basics/route-conflicts")
|
||||
[:break]])
|
||||
|
||||
(defmethod format-type :name-conflicts [_ _ conflicts]
|
||||
[:group
|
||||
(text "Router contains conflicting route names:")
|
||||
[:break] [:break]
|
||||
(into
|
||||
[:group]
|
||||
(mapv
|
||||
(fn [[name vals]]
|
||||
[:group
|
||||
[:span (text name)]
|
||||
[:break]
|
||||
(into
|
||||
[:group]
|
||||
(map
|
||||
(fn [p] [:span (color :grey "-> " p) [:break]])
|
||||
(mapv first vals)))
|
||||
[:break]])
|
||||
conflicts))
|
||||
(color :white "https://cljdoc.org/d/metosin/reitit/CURRENT/doc/basics/route-conflicts")
|
||||
[:break]])
|
||||
|
||||
(defmethod format-type :reitit.spec/invalid-route-data [_ _ {:keys [problems]}]
|
||||
[:group
|
||||
(text "Route data validation failed:")
|
||||
[:break] [:break]
|
||||
(into
|
||||
[:group]
|
||||
(map
|
||||
(fn [{:keys [data path spec]}]
|
||||
[:group
|
||||
[:span (color :grey "-- On route -----------------------")]
|
||||
[:break]
|
||||
[:break]
|
||||
(text path)
|
||||
[:break]
|
||||
[:break]
|
||||
(-> (s/explain-data spec data)
|
||||
(expound-printer)
|
||||
(with-out-str)
|
||||
(fippify))
|
||||
[:break]])
|
||||
problems))
|
||||
(color :white "https://cljdoc.org/d/metosin/reitit/CURRENT/doc/basics/route-data-validation")
|
||||
[:break]])
|
||||
|
|
@ -35,22 +35,22 @@
|
|||
(let [response (coercion/coerce-response coercers request response)]
|
||||
(assoc ctx :response response))))})))})
|
||||
|
||||
(defn coerce-exceptions-interceptor
|
||||
"Interceptor for handling coercion exceptions.
|
||||
Expects a :coercion of type `reitit.coercion/Coercion`
|
||||
and :parameters or :responses from route data, otherwise does not mount."
|
||||
[]
|
||||
{:name ::coerce-exceptions
|
||||
:compile (fn [{:keys [coercion parameters responses]} _]
|
||||
(if (and coercion (or parameters responses))
|
||||
{:error (fn [ctx]
|
||||
(let [data (ex-data (:error ctx))]
|
||||
(if-let [status (case (:type data)
|
||||
::coercion/request-coercion 400
|
||||
::coercion/response-coercion 500
|
||||
nil)]
|
||||
(let [response {:status status, :body (coercion/encode-error data)}]
|
||||
(-> ctx
|
||||
(assoc :response response)
|
||||
(assoc :error nil)))
|
||||
ctx)))}))})
|
||||
(defn coerce-exceptions-interceptor
|
||||
"Interceptor for handling coercion exceptions.
|
||||
Expects a :coercion of type `reitit.coercion/Coercion`
|
||||
and :parameters or :responses from route data, otherwise does not mount."
|
||||
[]
|
||||
{:name ::coerce-exceptions
|
||||
:compile (fn [{:keys [coercion parameters responses]} _]
|
||||
(if (and coercion (or parameters responses))
|
||||
{:error (fn [ctx]
|
||||
(let [data (ex-data (:error ctx))]
|
||||
(if-let [status (case (:type data)
|
||||
::coercion/request-coercion 400
|
||||
::coercion/response-coercion 500
|
||||
nil)]
|
||||
(let [response {:status status, :body (coercion/encode-error data)}]
|
||||
(-> ctx
|
||||
(assoc :response response)
|
||||
(assoc :error nil)))
|
||||
ctx)))}))})
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
(:require [clojure.spec.alpha :as s]
|
||||
[reitit.ring.spec :as rrs]
|
||||
[reitit.interceptor :as interceptor]
|
||||
[reitit.exception :as exception]
|
||||
[reitit.spec :as rs]))
|
||||
|
||||
;;
|
||||
|
|
@ -17,7 +18,9 @@
|
|||
;; Validator
|
||||
;;
|
||||
|
||||
(defn validate-spec!
|
||||
[routes {:keys [spec ::rs/explain] :or {explain s/explain-str, spec ::data}}]
|
||||
(defn validate
|
||||
[routes {:keys [spec] :or {spec ::data}}]
|
||||
(when-let [problems (rrs/validate-route-data routes :interceptors spec)]
|
||||
(rs/throw-on-problems! problems explain)))
|
||||
(exception/fail!
|
||||
::invalid-route-data
|
||||
{:problems problems})))
|
||||
|
|
|
|||
|
|
@ -21,9 +21,9 @@
|
|||
(defn merge-specs [specs]
|
||||
(when-let [non-specs (seq (remove #(or (s/spec? %) (s/get-spec %)) specs))]
|
||||
(exception/fail!
|
||||
(str "Not all specs satisfy the Spec protocol: " non-specs)
|
||||
::invalid-specs
|
||||
{:specs specs
|
||||
:non-specs non-specs}))
|
||||
:invalid non-specs}))
|
||||
(s/merge-spec-impl (vec specs) (vec specs) nil))
|
||||
|
||||
(defn validate-route-data [routes key spec]
|
||||
|
|
@ -31,14 +31,16 @@
|
|||
[method {:keys [data] :as endpoint}] c
|
||||
:when endpoint
|
||||
:let [target (key endpoint)
|
||||
mw-specs (seq (keep :spec target))
|
||||
specs (keep identity (into [spec] mw-specs))
|
||||
component-specs (seq (keep :spec target))
|
||||
specs (keep identity (into [spec] component-specs))
|
||||
spec (merge-specs specs)]]
|
||||
(when-let [problems (and spec (s/explain-data spec data))]
|
||||
(rs/->Problem p method data spec problems)))
|
||||
(keep identity) (seq)))
|
||||
|
||||
(defn validate-spec!
|
||||
[routes {:keys [spec ::rs/explain] :or {explain s/explain-str, spec ::data}}]
|
||||
(defn validate
|
||||
[routes {:keys [spec] :or {spec ::data}}]
|
||||
(when-let [problems (validate-route-data routes :middleware spec)]
|
||||
(rs/throw-on-problems! problems explain)))
|
||||
(exception/fail!
|
||||
::invalid-route-data
|
||||
{:problems problems})))
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
:parent-project {:path "../../project.clj"
|
||||
:inherit [:deploy-repositories :managed-dependencies]}
|
||||
:dependencies [[metosin/reitit-core]
|
||||
[metosin/reitit-dev]
|
||||
[metosin/reitit-spec]
|
||||
[metosin/reitit-schema]
|
||||
[metosin/reitit-ring]
|
||||
|
|
|
|||
24
project.clj
24
project.clj
|
|
@ -13,6 +13,7 @@
|
|||
:javac-options ["-Xlint:unchecked" "-target" "1.8" "-source" "1.8"]
|
||||
:managed-dependencies [[metosin/reitit "0.2.13"]
|
||||
[metosin/reitit-core "0.2.13"]
|
||||
[metosin/reitit-dev "0.2.13"]
|
||||
[metosin/reitit-spec "0.2.13"]
|
||||
[metosin/reitit-schema "0.2.13"]
|
||||
[metosin/reitit-ring "0.2.13"]
|
||||
|
|
@ -25,24 +26,26 @@
|
|||
[metosin/reitit-sieppari "0.2.13"]
|
||||
[metosin/reitit-pedestal "0.2.13"]
|
||||
[metosin/ring-swagger-ui "2.2.10"]
|
||||
[metosin/spec-tools "0.8.3"]
|
||||
[metosin/schema-tools "0.10.5"]
|
||||
[metosin/spec-tools "0.9.0"]
|
||||
[metosin/schema-tools "0.11.0"]
|
||||
[metosin/muuntaja "0.6.3"]
|
||||
[metosin/jsonista "0.2.2"]
|
||||
[metosin/sieppari "0.0.0-alpha7"]
|
||||
|
||||
[meta-merge "1.0.0"]
|
||||
[fipp "0.6.17" :exclusions [org.clojure/core.rrb-vector]]
|
||||
[expound "0.7.2"]
|
||||
[lambdaisland/deep-diff "0.0-25"]
|
||||
[ring/ring-core "1.7.1"]
|
||||
|
||||
[io.pedestal/pedestal.service "0.5.5"]]
|
||||
|
||||
:plugins [[jonase/eastwood "0.3.4"]
|
||||
:plugins [[jonase/eastwood "0.3.5"]
|
||||
;[lein-virgil "0.1.7"]
|
||||
[lein-doo "0.1.11"]
|
||||
[lein-cljsbuild "1.1.7"]
|
||||
[lein-cloverage "1.0.13"]
|
||||
[lein-codox "0.10.5"]
|
||||
[lein-codox "0.10.6"]
|
||||
[metosin/bat-test "0.4.2"]]
|
||||
|
||||
:profiles {:dev {:jvm-opts ^:replace ["-server"]
|
||||
|
|
@ -50,6 +53,7 @@
|
|||
;; all module sources for development
|
||||
:source-paths ["modules/reitit/src"
|
||||
"modules/reitit-core/src"
|
||||
"modules/reitit-dev/src"
|
||||
"modules/reitit-ring/src"
|
||||
"modules/reitit-http/src"
|
||||
"modules/reitit-middleware/src"
|
||||
|
|
@ -75,9 +79,10 @@
|
|||
[metosin/jsonista]
|
||||
[lambdaisland/deep-diff]
|
||||
[meta-merge]
|
||||
[expound]
|
||||
[fipp]
|
||||
|
||||
[expound "0.7.2"]
|
||||
[orchestra "2018.12.06-2"]
|
||||
[orchestra "2019.02.06-1"]
|
||||
|
||||
[ring "1.7.1"]
|
||||
[ikitommi/immutant-web "3.0.0-alpha1"]
|
||||
|
|
@ -95,12 +100,11 @@
|
|||
|
||||
[org.clojure/core.async "0.4.490"]
|
||||
[manifold "0.1.8"]
|
||||
[funcool/promesa "1.9.0"]
|
||||
[funcool/promesa "2.0.0"]
|
||||
|
||||
[com.clojure-goes-fast/clj-async-profiler "0.3.0"]
|
||||
|
||||
;; https://github.com/bensu/doo/issues/180
|
||||
[fipp "0.6.14" :exclusions [org.clojure/core.rrb-vector]]]}
|
||||
[com.bhauman/rebel-readline "0.1.4"]]}
|
||||
:1.9 {:dependencies [[org.clojure/clojure "1.9.0"]]}
|
||||
:perf {:jvm-opts ^:replace ["-server"
|
||||
"-Xmx4096m"
|
||||
|
|
@ -114,7 +118,7 @@
|
|||
[calfpath "0.7.2"]
|
||||
[org.clojure/core.async "0.4.490"]
|
||||
[manifold "0.1.8"]
|
||||
[funcool/promesa "1.9.0"]
|
||||
[funcool/promesa "2.0.0"]
|
||||
[metosin/sieppari]
|
||||
[yada "1.2.16"]
|
||||
[aleph "0.4.6"]
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ set -e
|
|||
# Modules
|
||||
for ext in \
|
||||
reitit-core \
|
||||
reitit-dev \
|
||||
reitit-spec \
|
||||
reitit-schema \
|
||||
reitit-ring \
|
||||
|
|
|
|||
|
|
@ -129,12 +129,12 @@
|
|||
(testing "unclosed brackets"
|
||||
(is (thrown-with-msg?
|
||||
ExceptionInfo
|
||||
#"^Unclosed brackets"
|
||||
#":reitit.trie/unclosed-brackets"
|
||||
(r/router ["/kikka/{kukka"]))))
|
||||
(testing "multiple terminators"
|
||||
(is (thrown-with-msg?
|
||||
ExceptionInfo
|
||||
#"^Trie compliation error: wild :kukka has two terminators"
|
||||
#":reitit.trie/multiple-terminators"
|
||||
(r/router [["/{kukka}.json"]
|
||||
["/{kukka}-json"]]))))))
|
||||
|
||||
|
|
|
|||
52
test/cljc/reitit/errors_test.cljc
Normal file
52
test/cljc/reitit/errors_test.cljc
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
(ns reitit.errors-test
|
||||
(:require [reitit.spec :as rs]
|
||||
[reitit.core :as r]
|
||||
[reitit.dev.pretty :as pretty]
|
||||
[clojure.spec.alpha :as s]))
|
||||
|
||||
(s/def ::role #{:admin :manager})
|
||||
(s/def ::roles (s/coll-of ::role :into #{}))
|
||||
(s/def ::data (s/keys :req [::role ::roles]))
|
||||
|
||||
(comment
|
||||
|
||||
;; route conflicts
|
||||
(r/router
|
||||
[["/:a/1"]
|
||||
["/1/:a"]]
|
||||
{:exception pretty/exception})
|
||||
|
||||
;; path conflicts
|
||||
(r/router
|
||||
[["/kikka" ::kikka]
|
||||
["/kukka" ::kikka]]
|
||||
{:exception pretty/exception})
|
||||
|
||||
;;
|
||||
;; trie
|
||||
;;
|
||||
|
||||
;; two terminators
|
||||
(r/router
|
||||
[["/{a}.pdf"]
|
||||
["/{a}-pdf"]]
|
||||
{:exception pretty/exception})
|
||||
|
||||
;; two following wilds
|
||||
(r/router
|
||||
["/{a}{b}"]
|
||||
{:exception pretty/exception})
|
||||
|
||||
;; unclosed brackers
|
||||
(r/router
|
||||
["/api/{ipa"]
|
||||
{:exception pretty/exception})
|
||||
|
||||
;;
|
||||
;; spec
|
||||
;;
|
||||
|
||||
(r/router
|
||||
["/api/ipa" {::roles #{:adminz}}]
|
||||
{:validate rs/validate
|
||||
:exception pretty/exception}))
|
||||
|
|
@ -21,59 +21,59 @@
|
|||
(testing "with default spec validates :name, :handler and :middleware"
|
||||
(is (thrown-with-msg?
|
||||
ExceptionInfo
|
||||
#"Invalid route data"
|
||||
#":reitit.ring.spec/invalid-route-data"
|
||||
(ring/router
|
||||
["/api" {:handler "identity"}]
|
||||
{:validate rrs/validate-spec!})))
|
||||
{:validate rrs/validate})))
|
||||
(is (thrown-with-msg?
|
||||
ExceptionInfo
|
||||
#"Invalid route data"
|
||||
#":reitit.ring.spec/invalid-route-data"
|
||||
(ring/router
|
||||
["/api" {:handler identity
|
||||
:name "kikka"}]
|
||||
{:validate rrs/validate-spec!}))))
|
||||
{:validate rrs/validate}))))
|
||||
|
||||
(testing "all endpoints are validated"
|
||||
(is (thrown-with-msg?
|
||||
ExceptionInfo
|
||||
#"Invalid route data"
|
||||
#":reitit.ring.spec/invalid-route-data"
|
||||
(ring/router
|
||||
["/api" {:patch {:handler "identity"}}]
|
||||
{:validate rrs/validate-spec!}))))
|
||||
{:validate rrs/validate}))))
|
||||
|
||||
(testing "spec can be overridden"
|
||||
(is (r/router?
|
||||
(ring/router
|
||||
["/api" {:handler "identity"}]
|
||||
{:spec (s/spec any?)
|
||||
:validate rrs/validate-spec!})))
|
||||
:validate rrs/validate})))
|
||||
|
||||
(testing "predicates are not allowed"
|
||||
(is (thrown-with-msg?
|
||||
ExceptionInfo
|
||||
#"Not all specs satisfy the Spec protocol"
|
||||
#":reitit.ring.spec/invalid-specs"
|
||||
(ring/router
|
||||
["/api" {:handler "identity"}]
|
||||
{:spec any?
|
||||
:validate rrs/validate-spec!})))))
|
||||
:validate rrs/validate})))))
|
||||
|
||||
(testing "middleware can contribute to specs"
|
||||
(is (r/router?
|
||||
(ring/router
|
||||
["/api" {:get {:handler identity
|
||||
:roles #{:admin}}}]
|
||||
{:validate rrs/validate-spec!
|
||||
{:validate rrs/validate
|
||||
:data {:middleware [{:spec (s/keys :opt-un [::roles])
|
||||
:wrap (fn [handler]
|
||||
(fn [request]
|
||||
(handler request)))}]}})))
|
||||
(is (thrown-with-msg?
|
||||
ExceptionInfo
|
||||
#"Invalid route data"
|
||||
#":reitit.ring.spec/invalid-route-data"
|
||||
(ring/router
|
||||
["/api" {:get {:handler identity
|
||||
:roles #{:adminz}}}]
|
||||
{:validate rrs/validate-spec!
|
||||
{:validate rrs/validate
|
||||
:data {:middleware [{:spec (s/keys :opt-un [::roles])
|
||||
:wrap (fn [handler]
|
||||
(fn [request]
|
||||
|
|
@ -97,7 +97,7 @@
|
|||
rrc/coerce-request-middleware
|
||||
rrc/coerce-response-middleware]
|
||||
:coercion reitit.coercion.spec/coercion}
|
||||
:validate rrs/validate-spec!})))
|
||||
:validate rrs/validate})))
|
||||
|
||||
(is (r/router?
|
||||
(ring/router
|
||||
|
|
@ -109,11 +109,11 @@
|
|||
rrc/coerce-request-middleware
|
||||
rrc/coerce-response-middleware]
|
||||
:coercion reitit.coercion.spec/coercion}
|
||||
:validate rrs/validate-spec!})))
|
||||
:validate rrs/validate})))
|
||||
|
||||
(is (thrown-with-msg?
|
||||
ExceptionInfo
|
||||
#"Invalid route data"
|
||||
#":reitit.ring.spec/invalid-route-data"
|
||||
(ring/router
|
||||
["/api"
|
||||
["/plus/:e"
|
||||
|
|
@ -123,4 +123,4 @@
|
|||
rrc/coerce-request-middleware
|
||||
rrc/coerce-response-middleware]
|
||||
:coercion reitit.coercion.spec/coercion}
|
||||
:validate rrs/validate-spec!}))))
|
||||
:validate rrs/validate}))))
|
||||
|
|
|
|||
|
|
@ -93,19 +93,19 @@
|
|||
#"Invalid route data"
|
||||
(r/router
|
||||
["/api" {:handler "identity"}]
|
||||
{:validate rs/validate-spec!})))
|
||||
{:validate rs/validate})))
|
||||
(is (thrown-with-msg?
|
||||
ExceptionInfo
|
||||
#"Invalid route data"
|
||||
(r/router
|
||||
["/api" {:name "kikka"}]
|
||||
{:validate rs/validate-spec!}))))
|
||||
{:validate rs/validate}))))
|
||||
|
||||
(testing "spec can be overridden"
|
||||
(is (r/router? (r/router
|
||||
["/api" {:handler "identity"}]
|
||||
{:spec any?
|
||||
:validate rs/validate-spec!})))))
|
||||
:validate rs/validate})))))
|
||||
|
||||
(deftest parameters-test
|
||||
(is (s/valid?
|
||||
|
|
|
|||
Loading…
Reference in a new issue