New reitit-dev module for pretty errors

This commit is contained in:
Tommi Reiman 2019-03-03 20:54:21 +02:00
parent 59560860d8
commit a2843dd097
22 changed files with 659 additions and 241 deletions

View file

@ -14,9 +14,51 @@
* backed by a new `:trie-router`, replacing `:segment-router` * backed by a new `:trie-router`, replacing `:segment-router`
* [over 40% faster](https://metosin.github.io/reitit/performance.html) on the JVM * [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 `:params` function has been deprecated
* Controller `:identity` function works the same as `:params` * Controller `:identity` function works the same as `:params`
* New `:parameters` option can be used to declare which parameters * New `:parameters` option can be used to declare which parameters
@ -24,13 +66,24 @@
use cases: `{:start start-fn, :parameters {:path [:foo-id]}}` use cases: `{:start start-fn, :parameters {:path [:foo-id]}}`
* Ensure HTML5 History routing works with IE11 * 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). * 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). * 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) ## 0.2.13 (2019-01-26)

View file

@ -1,6 +1,5 @@
# reitit [![Build Status](https://img.shields.io/circleci/project/github/metosin/reitit.svg)](https://circleci.com/gh/metosin/reitit) [![cljdoc badge](https://cljdoc.xyz/badge/metosin/reitit)](https://cljdoc.xyz/jump/release/metosin/reitit) # reitit [![Build Status](https://img.shields.io/circleci/project/github/metosin/reitit.svg)](https://circleci.com/gh/metosin/reitit) [![cljdoc badge](https://cljdoc.xyz/badge/metosin/reitit)](https://cljdoc.xyz/jump/release/metosin/reitit)
A fast data-driven router for Clojure(Script). A fast data-driven router for Clojure(Script).
* Simple data-driven [route syntax](https://metosin.github.io/reitit/basics/route_syntax.html) * 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-http` http-routing with Interceptors
* `reitit-interceptors` - [common interceptors](https://metosin.github.io/reitit/http/default_interceptors.html) * `reitit-interceptors` - [common interceptors](https://metosin.github.io/reitit/http/default_interceptors.html)
* `reitit-sieppari` support for [Sieppari](https://github.com/metosin/sieppari) * `reitit-sieppari` support for [Sieppari](https://github.com/metosin/sieppari)
* `reitit-dev` - development utilities
## Extra modules ## Extra modules
@ -49,36 +49,7 @@ All main modules bundled:
[metosin/reitit "0.2.13"] [metosin/reitit "0.2.13"]
``` ```
Optionally, the parts can be required separately: 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"]
```
## Quick start ## Quick start

View file

@ -12,6 +12,8 @@
* Modular * Modular
* [Fast](performance.md) * [Fast](performance.md)
There is [#reitit](https://clojurians.slack.com/messages/reitit/) in [Clojurians Slack](http://clojurians.net/) for discussion & help.
## Main Modules ## Main Modules
* `reitit` - all bundled * `reitit` - all bundled
@ -26,6 +28,7 @@
* `reitit-http` http-routing with Pedestal-style Interceptors * `reitit-http` http-routing with Pedestal-style Interceptors
* `reitit-interceptors` - [common interceptors](./http/default_interceptors.md) for `reitit-http` * `reitit-interceptors` - [common interceptors](./http/default_interceptors.md) for `reitit-http`
* `reitit-sieppari` support for [Sieppari](https://github.com/metosin/sieppari) Interceptors * `reitit-sieppari` support for [Sieppari](https://github.com/metosin/sieppari) Interceptors
* `reitit-dev` - development utilities
## Extra modules ## Extra modules
@ -39,38 +42,7 @@ All bundled:
[metosin/reitit "0.2.13"] [metosin/reitit "0.2.13"]
``` ```
Optionally, the parts can be required separately: 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.
# Examples # Examples

View file

@ -4,13 +4,14 @@ Routers can be configured via options. The following options are available for t
| 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
| `: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
| `:validate` | Function of `routes opts => ()` to validate route (data) via side-effects | | `: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
| `:router` | Function of `routes opts => router` to override the actual router implementation | | `: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

View file

@ -8,7 +8,7 @@ But there is a better way. Router has a `:validation` hook to validate the whole
## clojure.spec ## 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: A Router with invalid route data:
@ -27,7 +27,7 @@ Fails fast with `clojure.spec` validation turned on:
(r/router (r/router
["/api" {:handler "identity"}] ["/api" {:handler "identity"}]
{:validate rs/validate-spec!}) {:validate rs/validate})
; CompilerException clojure.lang.ExceptionInfo: Invalid route data: ; CompilerException clojure.lang.ExceptionInfo: Invalid route data:
; ;
; -- On route ----------------------- ; -- On route -----------------------
@ -42,7 +42,7 @@ Fails fast with `clojure.spec` validation turned on:
### Customizing spec validation ### Customizing spec validation
`rs/validate-spec!` reads the following router options: `rs/validate` reads the following router options:
| key | description | | key | description |
| ---------------|-------------| | ---------------|-------------|
@ -64,7 +64,7 @@ Below is an example of using [expound](https://github.com/bhb/expound) to pretty
["/api" {:handler identity ["/api" {:handler identity
::roles #{:adminz}}] ::roles #{:adminz}}]
{::rs/explain e/expound-str {::rs/explain e/expound-str
:validate rs/validate-spec!}) :validate rs/validate})
; CompilerException clojure.lang.ExceptionInfo: Invalid route data: ; CompilerException clojure.lang.ExceptionInfo: Invalid route data:
; ;
; -- On route ----------------------- ; -- On route -----------------------
@ -102,7 +102,7 @@ Explicitly requiring a `::roles` key in a route data:
["/api" {:handler identity}] ["/api" {:handler identity}]
{:spec (s/merge (s/keys :req [::roles]) ::rs/default-data) {:spec (s/merge (s/keys :req [::roles]) ::rs/default-data)
::rs/explain e/expound-str ::rs/explain e/expound-str
:validate rs/validate-spec!}) :validate rs/validate})
; CompilerException clojure.lang.ExceptionInfo: Invalid route data: ; CompilerException clojure.lang.ExceptionInfo: Invalid route data:
; ;
; -- On route ----------------------- ; -- On route -----------------------

View file

@ -2,7 +2,7 @@
Ring route validation works [just like with core router](../basics/route_data_validation.md), with few differences: 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. * 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 ## Example
@ -28,7 +28,7 @@ A simple app with spec-validation turned on:
["/internal" ["/internal"
["/users" {:get {:handler handler} ["/users" {:get {:handler handler}
:delete {:handler handler}}]]] :delete {:handler handler}}]]]
{:validate rrs/validate-spec! {:validate rrs/validate
::rs/explain e/expound-str}))) ::rs/explain e/expound-str})))
``` ```
@ -69,7 +69,7 @@ Missing route data fails fast at router creation:
["/internal" ["/internal"
["/users" {:get {:handler handler} ["/users" {:get {:handler handler}
:delete {:handler handler}}]]] :delete {:handler handler}}]]]
{:validate rrs/validate-spec! {:validate rrs/validate
::rs/explain e/expound-str}))) ::rs/explain e/expound-str})))
; CompilerException clojure.lang.ExceptionInfo: Invalid route data: ; CompilerException clojure.lang.ExceptionInfo: Invalid route data:
; ;
@ -133,7 +133,7 @@ Adding the `:zone` to route data fixes the problem:
["/internal" {:zone :internal} ;; <--- added ["/internal" {:zone :internal} ;; <--- added
["/users" {:get {:handler handler} ["/users" {:get {:handler handler}
:delete {:handler handler}}]]] :delete {:handler handler}}]]]
{:validate rrs/validate-spec! {:validate rrs/validate
::rs/explain e/expound-str}))) ::rs/explain e/expound-str})))
(app {:request-method :get (app {:request-method :get
@ -175,7 +175,7 @@ Let's reuse the `wrap-enforce-roles` from [Dynamic extensions](dynamic_extension
["/internal" {:zone :internal} ["/internal" {:zone :internal}
["/users" {:get {:handler handler} ["/users" {:get {:handler handler}
:delete {:handler handler}}]]] :delete {:handler handler}}]]]
{:validate rrs/validate-spec! {:validate rrs/validate
::rs/explain e/expound-str}))) ::rs/explain e/expound-str})))
(app {:request-method :get (app {:request-method :get
@ -199,7 +199,7 @@ But fails if they are present and invalid:
::roles #{:manager} ;; <--- added ::roles #{:manager} ;; <--- added
:delete {:handler handler :delete {:handler handler
::roles #{:adminz}}}]]] ;; <--- added ::roles #{:adminz}}}]]] ;; <--- added
{:validate rrs/validate-spec! {:validate rrs/validate
::rs/explain e/expound-str}))) ::rs/explain e/expound-str})))
; CompilerException clojure.lang.ExceptionInfo: Invalid route data: ; 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}} ::roles #{:manager}}
:delete {:handler handler :delete {:handler handler
::roles #{:admin}}}]]] ::roles #{:admin}}}]]]
{:validate rrs/validate-spec! {:validate rrs/validate
::rs/explain e/expound-str}))) ::rs/explain e/expound-str})))
``` ```
@ -261,7 +261,7 @@ Or even flatten the routes:
::roles #{:manager}} ::roles #{:manager}}
:delete {:handler handler :delete {:handler handler
::roles #{:admin}}}]] ::roles #{:admin}}}]]
{:validate rrs/validate-spec! {:validate rrs/validate
::rs/explain e/expound-str}))) ::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 :delete {:handler handler
::roles #{:admin}}}]] ::roles #{:admin}}}]]
{:data {:middleware [zone-middleware wrap-enforce-roles]} {:data {:middleware [zone-middleware wrap-enforce-roles]}
:validate rrs/validate-spec! :validate rrs/validate
::rs/explain e/expound-str}))) ::rs/explain e/expound-str})))
``` ```

View file

@ -1,6 +1,5 @@
(ns reitit.core (ns reitit.core
(:require [clojure.string :as str] (:require [reitit.impl :as impl]
[reitit.impl :as impl]
[reitit.exception :as exception] [reitit.exception :as exception]
[reitit.trie :as trie])) [reitit.trie :as trie]))
@ -32,27 +31,6 @@
nil nil
(expand [_ _])) (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 ;; Router
;; ;;
@ -359,7 +337,8 @@
:expand expand :expand expand
:coerce (fn coerce [route _] route) :coerce (fn coerce [route _] route)
:compile (fn compile [[_ {:keys [handler]}] _] handler) :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 (defn router
"Create a [[Router]] from raw route data and optionally an options map. "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` | `: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
| `:validate` | Function of `routes opts => ()` to validate route (data) via side-effects | `: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" | `:router` | Function of `routes opts => router` to override the actual router implementation"
([raw-routes] ([raw-routes]
(router raw-routes {})) (router raw-routes {}))
([raw-routes opts] ([raw-routes opts]
(let [{:keys [router] :as opts} (merge (default-router-options) opts) (let [{:keys [router] :as opts} (merge (default-router-options) opts)]
routes (impl/resolve-routes raw-routes opts) (try
path-conflicting (impl/path-conflicting-routes routes) (let [routes (impl/resolve-routes raw-routes opts)
name-conflicting (impl/name-conflicting-routes routes) path-conflicting (impl/path-conflicting-routes routes)
compiled-routes (impl/compile-routes routes opts) name-conflicting (impl/name-conflicting-routes routes)
wilds? (boolean (some impl/wild-route? compiled-routes)) compiled-routes (impl/compile-routes routes opts)
all-wilds? (every? impl/wild-route? compiled-routes) wilds? (boolean (some impl/wild-route? compiled-routes))
router (cond all-wilds? (every? impl/wild-route? compiled-routes)
router router router (cond
(and (= 1 (count compiled-routes)) (not wilds?)) single-static-path-router router router
path-conflicting quarantine-router (and (= 1 (count compiled-routes)) (not wilds?)) single-static-path-router
(not wilds?) lookup-router path-conflicting quarantine-router
all-wilds? trie-router (not wilds?) lookup-router
:else mixed-router)] all-wilds? trie-router
:else mixed-router)]
(when-let [conflicts (:conflicts opts)] (when-let [conflicts (:conflicts opts)]
(when path-conflicting (conflicts path-conflicting))) (when path-conflicting (conflicts path-conflicting)))
(when name-conflicting (when name-conflicting
(throw-on-conflicts! name-conflicts-str name-conflicting)) (exception/fail! :name-conflicts name-conflicting))
(when-let [validate (:validate opts)] (when-let [validate (:validate opts)]
(validate compiled-routes 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))))))))

View file

@ -1,7 +1,36 @@
(ns reitit.exception) (ns reitit.exception
(:require [clojure.string :as str]))
(defn fail! (defn fail!
([message] ([type]
(throw (ex-info message {::type :exeption}))) (fail! type nil))
([message data] ([type data]
(throw (ex-info message (merge {::type ::exeption} 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)))

View file

@ -1,8 +1,8 @@
(ns reitit.spec (ns reitit.spec
(:require [clojure.spec.alpha :as s] (:require [clojure.spec.alpha :as s]
[clojure.spec.gen.alpha :as gen] [clojure.spec.gen.alpha :as gen]
[reitit.core :as reitit] [reitit.exception :as exception]
[reitit.exception :as exception])) [reitit.core :as r]))
;; ;;
;; routes ;; routes
@ -45,7 +45,7 @@
;; router ;; router
;; ;;
(s/def ::router reitit/router?) (s/def ::router r/router?)
(s/def :reitit.router/path ::path) (s/def :reitit.router/path ::path)
(s/def :reitit.router/routes ::routes) (s/def :reitit.router/routes ::routes)
(s/def :reitit.router/data ::data) (s/def :reitit.router/data ::data)
@ -66,7 +66,7 @@
:reitit.router/conflicts :reitit.router/conflicts
:reitit.router/router]))) :reitit.router/router])))
(s/fdef reitit/router (s/fdef r/router
:args (s/or :1arity (s/cat :data (s/spec ::raw-routes)) :args (s/or :1arity (s/cat :data (s/spec ::raw-routes))
:2arity (s/cat :data (s/spec ::raw-routes), :opts ::opts)) :2arity (s/cat :data (s/spec ::raw-routes), :opts ::opts))
:ret ::router) :ret ::router)
@ -111,28 +111,24 @@
(defrecord Problem [path scope data spec problems]) (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" (apply str "Invalid route data:\n\n"
(mapv (mapv
(fn [{:keys [path scope data spec]}] (fn [{:keys [path scope data spec]}]
(str "-- On route -----------------------\n\n" (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))) 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)))

View file

@ -59,7 +59,7 @@
(if (= to (count s)) (if (= to (count s))
(concat ss (-static from to)) (concat ss (-static from to))
(case (get s 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))) (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) (-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'))))) (recur (concat ss (-static from to) (-wild to to')) (long (inc to')) (long (inc to')))))
@ -134,7 +134,7 @@
(defn- -node [m] (defn- -node [m]
(map->Node (merge {:children {}, :wilds {}, :catch-all {}, :params {}} 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 (let [node' (cond
(nil? path) (nil? path)
@ -143,14 +143,14 @@
(instance? Wild path) (instance? Wild path)
(let [next (first ps)] (let [next (first ps)]
(if (or (instance? Wild next) (instance? CatchAll next)) (if (or (instance? Wild next) (instance? CatchAll next))
(ex/fail! (str "Two following wilds: " path ", " next)) (ex/fail! ::following-parameters {:path fp, :parameters (map :value [path next])})
(update-in node [:wilds path] (fn [n] (-insert (or n (-node {})) ps params data))))) (update-in node [:wilds path] (fn [n] (-insert (or n (-node {})) ps fp params data)))))
(instance? CatchAll path) (instance? CatchAll path)
(assoc-in node [:catch-all path] (-node {:params params, :data data})) (assoc-in node [:catch-all path] (-node {:params params, :data data}))
(str/blank? path) (str/blank? path)
(-insert node ps params data) (-insert node ps fp params data)
:else :else
(or (or
@ -159,20 +159,20 @@
(if-let [cp (common-prefix p path)] (if-let [cp (common-prefix p path)]
(if (= cp p) (if (= cp p)
;; insert into child node ;; 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'))) (reduced (assoc-in node [:children p] n')))
;; split child node ;; split child node
(let [rp (subs p (count cp)) (let [rp (subs p (count cp))
rp' (subs path (count cp)) rp' (subs path (count cp))
n' (-insert (-node {}) ps params data) n' (-insert (-node {}) ps fp params data)
n'' (-insert (-node {:children {rp n, rp' n'}}) nil nil nil)] n'' (-insert (-node {:children {rp n, rp' n'}}) nil nil nil nil)]
(reduced (update node :children (fn [children] (reduced (update node :children (fn [children]
(-> children (-> children
(dissoc p) (dissoc p)
(assoc cp n''))))))))) (assoc cp n'')))))))))
nil (:children node)) nil (:children node))
;; new child 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 ""])] (if-let [child (get-in node' [:children ""])]
;; optimize by removing empty paths ;; optimize by removing empty paths
(-> (merge-with merge (dissoc node' :data) child) (-> (merge-with merge (dissoc node' :data) child)
@ -300,7 +300,7 @@
([node path data] ([node path data]
(let [parts (split-path path) (let [parts (split-path path)
params (zipmap (->> parts (remove string?) (map :value)) (repeat nil))] 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 (defn compiler
"Returns a default [[TrieCompiler]]." "Returns a default [[TrieCompiler]]."
@ -312,18 +312,20 @@
"Returns a compiled trie, to be used with [[pretty]] or [[path-matcher]]." "Returns a compiled trie, to be used with [[pretty]] or [[path-matcher]]."
([options] ([options]
(compile options (compiler))) (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) ["/"])) (let [ends (fn [{:keys [children]}] (or (keys children) ["/"]))
matchers (-> [] matchers (-> []
(cond-> data (conj (data-matcher compiler params data))) (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 (into
(for [[p c] wilds] (for [[p c] wilds]
(let [p (:value p) (let [pv (:value p)
ends (ends c)] ends (ends c)]
(if (next ends) (if (next ends)
(ex/fail! (str "Trie compliation error: wild " p " has two terminators: " ends)) (ex/fail! ::multiple-terminators {:terminators ends, :path (join-path (conj cp p))})
(wild-matcher compiler p (ffirst ends) (compile c compiler)))))) (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)))))] (into (for [[p c] catch-all] (catch-all-matcher compiler (:value p) params (:data c)))))]
(cond (cond
(> (count matchers) 1) (linear-matcher compiler matchers) (> (count matchers) 1) (linear-matcher compiler matchers)

View 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]])

View 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]])

View file

@ -35,22 +35,22 @@
(let [response (coercion/coerce-response coercers request response)] (let [response (coercion/coerce-response coercers request response)]
(assoc ctx :response response))))})))}) (assoc ctx :response response))))})))})
(defn coerce-exceptions-interceptor (defn coerce-exceptions-interceptor
"Interceptor for handling coercion exceptions. "Interceptor for handling coercion exceptions.
Expects a :coercion of type `reitit.coercion/Coercion` Expects a :coercion of type `reitit.coercion/Coercion`
and :parameters or :responses from route data, otherwise does not mount." and :parameters or :responses from route data, otherwise does not mount."
[] []
{:name ::coerce-exceptions {:name ::coerce-exceptions
:compile (fn [{:keys [coercion parameters responses]} _] :compile (fn [{:keys [coercion parameters responses]} _]
(if (and coercion (or parameters responses)) (if (and coercion (or parameters responses))
{:error (fn [ctx] {:error (fn [ctx]
(let [data (ex-data (:error ctx))] (let [data (ex-data (:error ctx))]
(if-let [status (case (:type data) (if-let [status (case (:type data)
::coercion/request-coercion 400 ::coercion/request-coercion 400
::coercion/response-coercion 500 ::coercion/response-coercion 500
nil)] nil)]
(let [response {:status status, :body (coercion/encode-error data)}] (let [response {:status status, :body (coercion/encode-error data)}]
(-> ctx (-> ctx
(assoc :response response) (assoc :response response)
(assoc :error nil))) (assoc :error nil)))
ctx)))}))}) ctx)))}))})

View file

@ -2,6 +2,7 @@
(:require [clojure.spec.alpha :as s] (:require [clojure.spec.alpha :as s]
[reitit.ring.spec :as rrs] [reitit.ring.spec :as rrs]
[reitit.interceptor :as interceptor] [reitit.interceptor :as interceptor]
[reitit.exception :as exception]
[reitit.spec :as rs])) [reitit.spec :as rs]))
;; ;;
@ -17,7 +18,9 @@
;; Validator ;; Validator
;; ;;
(defn validate-spec! (defn validate
[routes {:keys [spec ::rs/explain] :or {explain s/explain-str, spec ::data}}] [routes {:keys [spec] :or {spec ::data}}]
(when-let [problems (rrs/validate-route-data routes :interceptors spec)] (when-let [problems (rrs/validate-route-data routes :interceptors spec)]
(rs/throw-on-problems! problems explain))) (exception/fail!
::invalid-route-data
{:problems problems})))

View file

@ -21,9 +21,9 @@
(defn merge-specs [specs] (defn merge-specs [specs]
(when-let [non-specs (seq (remove #(or (s/spec? %) (s/get-spec %)) specs))] (when-let [non-specs (seq (remove #(or (s/spec? %) (s/get-spec %)) specs))]
(exception/fail! (exception/fail!
(str "Not all specs satisfy the Spec protocol: " non-specs) ::invalid-specs
{:specs specs {:specs specs
:non-specs 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 spec]
@ -31,14 +31,16 @@
[method {:keys [data] :as endpoint}] c [method {:keys [data] :as endpoint}] c
:when endpoint :when endpoint
:let [target (key endpoint) :let [target (key endpoint)
mw-specs (seq (keep :spec target)) component-specs (seq (keep :spec target))
specs (keep identity (into [spec] mw-specs)) specs (keep identity (into [spec] component-specs))
spec (merge-specs specs)]] spec (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-spec! (defn validate
[routes {:keys [spec ::rs/explain] :or {explain s/explain-str, spec ::data}}] [routes {:keys [spec] :or {spec ::data}}]
(when-let [problems (validate-route-data routes :middleware spec)] (when-let [problems (validate-route-data routes :middleware spec)]
(rs/throw-on-problems! problems explain))) (exception/fail!
::invalid-route-data
{:problems problems})))

View file

@ -10,6 +10,7 @@
: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]
[metosin/reitit-dev]
[metosin/reitit-spec] [metosin/reitit-spec]
[metosin/reitit-schema] [metosin/reitit-schema]
[metosin/reitit-ring] [metosin/reitit-ring]

View file

@ -13,6 +13,7 @@
:javac-options ["-Xlint:unchecked" "-target" "1.8" "-source" "1.8"] :javac-options ["-Xlint:unchecked" "-target" "1.8" "-source" "1.8"]
:managed-dependencies [[metosin/reitit "0.2.13"] :managed-dependencies [[metosin/reitit "0.2.13"]
[metosin/reitit-core "0.2.13"] [metosin/reitit-core "0.2.13"]
[metosin/reitit-dev "0.2.13"]
[metosin/reitit-spec "0.2.13"] [metosin/reitit-spec "0.2.13"]
[metosin/reitit-schema "0.2.13"] [metosin/reitit-schema "0.2.13"]
[metosin/reitit-ring "0.2.13"] [metosin/reitit-ring "0.2.13"]
@ -25,24 +26,26 @@
[metosin/reitit-sieppari "0.2.13"] [metosin/reitit-sieppari "0.2.13"]
[metosin/reitit-pedestal "0.2.13"] [metosin/reitit-pedestal "0.2.13"]
[metosin/ring-swagger-ui "2.2.10"] [metosin/ring-swagger-ui "2.2.10"]
[metosin/spec-tools "0.8.3"] [metosin/spec-tools "0.9.0"]
[metosin/schema-tools "0.10.5"] [metosin/schema-tools "0.11.0"]
[metosin/muuntaja "0.6.3"] [metosin/muuntaja "0.6.3"]
[metosin/jsonista "0.2.2"] [metosin/jsonista "0.2.2"]
[metosin/sieppari "0.0.0-alpha7"] [metosin/sieppari "0.0.0-alpha7"]
[meta-merge "1.0.0"] [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"] [lambdaisland/deep-diff "0.0-25"]
[ring/ring-core "1.7.1"] [ring/ring-core "1.7.1"]
[io.pedestal/pedestal.service "0.5.5"]] [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-virgil "0.1.7"]
[lein-doo "0.1.11"] [lein-doo "0.1.11"]
[lein-cljsbuild "1.1.7"] [lein-cljsbuild "1.1.7"]
[lein-cloverage "1.0.13"] [lein-cloverage "1.0.13"]
[lein-codox "0.10.5"] [lein-codox "0.10.6"]
[metosin/bat-test "0.4.2"]] [metosin/bat-test "0.4.2"]]
:profiles {:dev {:jvm-opts ^:replace ["-server"] :profiles {:dev {:jvm-opts ^:replace ["-server"]
@ -50,6 +53,7 @@
;; all module sources for development ;; all module sources for development
:source-paths ["modules/reitit/src" :source-paths ["modules/reitit/src"
"modules/reitit-core/src" "modules/reitit-core/src"
"modules/reitit-dev/src"
"modules/reitit-ring/src" "modules/reitit-ring/src"
"modules/reitit-http/src" "modules/reitit-http/src"
"modules/reitit-middleware/src" "modules/reitit-middleware/src"
@ -75,9 +79,10 @@
[metosin/jsonista] [metosin/jsonista]
[lambdaisland/deep-diff] [lambdaisland/deep-diff]
[meta-merge] [meta-merge]
[expound]
[fipp]
[expound "0.7.2"] [orchestra "2019.02.06-1"]
[orchestra "2018.12.06-2"]
[ring "1.7.1"] [ring "1.7.1"]
[ikitommi/immutant-web "3.0.0-alpha1"] [ikitommi/immutant-web "3.0.0-alpha1"]
@ -95,12 +100,11 @@
[org.clojure/core.async "0.4.490"] [org.clojure/core.async "0.4.490"]
[manifold "0.1.8"] [manifold "0.1.8"]
[funcool/promesa "1.9.0"] [funcool/promesa "2.0.0"]
[com.clojure-goes-fast/clj-async-profiler "0.3.0"] [com.clojure-goes-fast/clj-async-profiler "0.3.0"]
;; https://github.com/bensu/doo/issues/180 [com.bhauman/rebel-readline "0.1.4"]]}
[fipp "0.6.14" :exclusions [org.clojure/core.rrb-vector]]]}
:1.9 {:dependencies [[org.clojure/clojure "1.9.0"]]} :1.9 {:dependencies [[org.clojure/clojure "1.9.0"]]}
:perf {:jvm-opts ^:replace ["-server" :perf {:jvm-opts ^:replace ["-server"
"-Xmx4096m" "-Xmx4096m"
@ -114,7 +118,7 @@
[calfpath "0.7.2"] [calfpath "0.7.2"]
[org.clojure/core.async "0.4.490"] [org.clojure/core.async "0.4.490"]
[manifold "0.1.8"] [manifold "0.1.8"]
[funcool/promesa "1.9.0"] [funcool/promesa "2.0.0"]
[metosin/sieppari] [metosin/sieppari]
[yada "1.2.16"] [yada "1.2.16"]
[aleph "0.4.6"] [aleph "0.4.6"]

View file

@ -5,6 +5,7 @@ set -e
# Modules # Modules
for ext in \ for ext in \
reitit-core \ reitit-core \
reitit-dev \
reitit-spec \ reitit-spec \
reitit-schema \ reitit-schema \
reitit-ring \ reitit-ring \

View file

@ -129,12 +129,12 @@
(testing "unclosed brackets" (testing "unclosed brackets"
(is (thrown-with-msg? (is (thrown-with-msg?
ExceptionInfo ExceptionInfo
#"^Unclosed brackets" #":reitit.trie/unclosed-brackets"
(r/router ["/kikka/{kukka"])))) (r/router ["/kikka/{kukka"]))))
(testing "multiple terminators" (testing "multiple terminators"
(is (thrown-with-msg? (is (thrown-with-msg?
ExceptionInfo ExceptionInfo
#"^Trie compliation error: wild :kukka has two terminators" #":reitit.trie/multiple-terminators"
(r/router [["/{kukka}.json"] (r/router [["/{kukka}.json"]
["/{kukka}-json"]])))))) ["/{kukka}-json"]]))))))

View 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}))

View file

@ -21,59 +21,59 @@
(testing "with default spec validates :name, :handler and :middleware" (testing "with default spec validates :name, :handler and :middleware"
(is (thrown-with-msg? (is (thrown-with-msg?
ExceptionInfo ExceptionInfo
#"Invalid route data" #":reitit.ring.spec/invalid-route-data"
(ring/router (ring/router
["/api" {:handler "identity"}] ["/api" {:handler "identity"}]
{:validate rrs/validate-spec!}))) {:validate rrs/validate})))
(is (thrown-with-msg? (is (thrown-with-msg?
ExceptionInfo ExceptionInfo
#"Invalid route data" #":reitit.ring.spec/invalid-route-data"
(ring/router (ring/router
["/api" {:handler identity ["/api" {:handler identity
:name "kikka"}] :name "kikka"}]
{:validate rrs/validate-spec!})))) {:validate rrs/validate}))))
(testing "all endpoints are validated" (testing "all endpoints are validated"
(is (thrown-with-msg? (is (thrown-with-msg?
ExceptionInfo ExceptionInfo
#"Invalid route data" #":reitit.ring.spec/invalid-route-data"
(ring/router (ring/router
["/api" {:patch {:handler "identity"}}] ["/api" {:patch {:handler "identity"}}]
{:validate rrs/validate-spec!})))) {:validate rrs/validate}))))
(testing "spec can be overridden" (testing "spec can be overridden"
(is (r/router? (is (r/router?
(ring/router (ring/router
["/api" {:handler "identity"}] ["/api" {:handler "identity"}]
{:spec (s/spec any?) {:spec (s/spec any?)
:validate rrs/validate-spec!}))) :validate rrs/validate})))
(testing "predicates are not allowed" (testing "predicates are not allowed"
(is (thrown-with-msg? (is (thrown-with-msg?
ExceptionInfo ExceptionInfo
#"Not all specs satisfy the Spec protocol" #":reitit.ring.spec/invalid-specs"
(ring/router (ring/router
["/api" {:handler "identity"}] ["/api" {:handler "identity"}]
{:spec any? {:spec any?
:validate rrs/validate-spec!}))))) :validate rrs/validate})))))
(testing "middleware can contribute to specs" (testing "middleware can contribute to specs"
(is (r/router? (is (r/router?
(ring/router (ring/router
["/api" {:get {:handler identity ["/api" {:get {:handler identity
:roles #{:admin}}}] :roles #{:admin}}}]
{:validate rrs/validate-spec! {:validate rrs/validate
:data {:middleware [{:spec (s/keys :opt-un [::roles]) :data {:middleware [{:spec (s/keys :opt-un [::roles])
:wrap (fn [handler] :wrap (fn [handler]
(fn [request] (fn [request]
(handler request)))}]}}))) (handler request)))}]}})))
(is (thrown-with-msg? (is (thrown-with-msg?
ExceptionInfo ExceptionInfo
#"Invalid route data" #":reitit.ring.spec/invalid-route-data"
(ring/router (ring/router
["/api" {:get {:handler identity ["/api" {:get {:handler identity
:roles #{:adminz}}}] :roles #{:adminz}}}]
{:validate rrs/validate-spec! {:validate rrs/validate
:data {:middleware [{:spec (s/keys :opt-un [::roles]) :data {:middleware [{:spec (s/keys :opt-un [::roles])
:wrap (fn [handler] :wrap (fn [handler]
(fn [request] (fn [request]
@ -97,7 +97,7 @@
rrc/coerce-request-middleware rrc/coerce-request-middleware
rrc/coerce-response-middleware] rrc/coerce-response-middleware]
:coercion reitit.coercion.spec/coercion} :coercion reitit.coercion.spec/coercion}
:validate rrs/validate-spec!}))) :validate rrs/validate})))
(is (r/router? (is (r/router?
(ring/router (ring/router
@ -109,11 +109,11 @@
rrc/coerce-request-middleware rrc/coerce-request-middleware
rrc/coerce-response-middleware] rrc/coerce-response-middleware]
:coercion reitit.coercion.spec/coercion} :coercion reitit.coercion.spec/coercion}
:validate rrs/validate-spec!}))) :validate rrs/validate})))
(is (thrown-with-msg? (is (thrown-with-msg?
ExceptionInfo ExceptionInfo
#"Invalid route data" #":reitit.ring.spec/invalid-route-data"
(ring/router (ring/router
["/api" ["/api"
["/plus/:e" ["/plus/:e"
@ -123,4 +123,4 @@
rrc/coerce-request-middleware rrc/coerce-request-middleware
rrc/coerce-response-middleware] rrc/coerce-response-middleware]
:coercion reitit.coercion.spec/coercion} :coercion reitit.coercion.spec/coercion}
:validate rrs/validate-spec!})))) :validate rrs/validate}))))

View file

@ -93,19 +93,19 @@
#"Invalid route data" #"Invalid route data"
(r/router (r/router
["/api" {:handler "identity"}] ["/api" {:handler "identity"}]
{:validate rs/validate-spec!}))) {:validate rs/validate})))
(is (thrown-with-msg? (is (thrown-with-msg?
ExceptionInfo ExceptionInfo
#"Invalid route data" #"Invalid route data"
(r/router (r/router
["/api" {:name "kikka"}] ["/api" {:name "kikka"}]
{:validate rs/validate-spec!})))) {:validate rs/validate}))))
(testing "spec can be overridden" (testing "spec can be overridden"
(is (r/router? (r/router (is (r/router? (r/router
["/api" {:handler "identity"}] ["/api" {:handler "identity"}]
{:spec any? {:spec any?
:validate rs/validate-spec!}))))) :validate rs/validate})))))
(deftest parameters-test (deftest parameters-test
(is (s/valid? (is (s/valid?