From 6ac873524520a202d0e91683d650bc76cf12143e Mon Sep 17 00:00:00 2001 From: Tommi Reiman Date: Fri, 15 Dec 2017 19:37:04 +0200 Subject: [PATCH 01/19] docs --- doc/coercion/coercion.md | 5 +---- doc/ring/coercion.md | 13 +++++++++---- perf-test/clj/reitit/coercion_perf_test.clj | 5 +---- perf-test/clj/reitit/go_perf_test.clj | 2 +- perf-test/clj/reitit/lupapiste_perf_test.clj | 9 +-------- perf-test/clj/reitit/nodejs_perf_test.clj | 3 +-- perf-test/clj/reitit/opensensors_perf_test.clj | 9 ++------- perf-test/clj/reitit/perf_utils.clj | 6 +----- perf-test/clj/reitit/prefix_tree_perf_test.clj | 12 ++++++------ 9 files changed, 23 insertions(+), 41 deletions(-) diff --git a/doc/coercion/coercion.md b/doc/coercion/coercion.md index 7d7e4d21..52d66320 100644 --- a/doc/coercion/coercion.md +++ b/doc/coercion/coercion.md @@ -138,7 +138,7 @@ We get the coerced paremeters back. If a coercion fails, a typed (`:reitit.coerc ## Full example -Here's an full example for doing both routing and coercion with Reitit: +Here's an full example for doing routing and coercion with Reitit and Schema: ```clj (require '[reitit.coercion.schema]) @@ -179,9 +179,6 @@ For a full-blown http-coercion, see the [ring coercion](../ring/coercion.md). ## Thanks to -Most of the thing are just polished version of the original implementations. Thanks to: - * [compojure-api](https://clojars.org/metosin/compojure-api) for the initial `Coercion` protocol -* [ring-swagger](https://github.com/metosin/ring-swagger#more-complete-example) for the `:parameters` and `:responses` syntax. * [schema](https://github.com/plumatic/schema) and [schema-tools](https://github.com/metosin/schema-tools) for Schema Coercion * [spec-tools](https://github.com/metosin/spec-tools) for Spec Coercion diff --git a/doc/ring/coercion.md b/doc/ring/coercion.md index 62f3eff6..e9291c9d 100644 --- a/doc/ring/coercion.md +++ b/doc/ring/coercion.md @@ -52,10 +52,17 @@ Defining a coercion for a route data doesn't do anything, as it's just data. We * `coerce-response-middleware` for the response coercion * `coerce-exceptions-middleware` to turn coercion exceptions into pretty responses -### Example with Schema +### Full example + +Here's an full example for applying coercion with Reitit, Ring and Schema: ```clj (require '[reitit.ring.coercion-middleware :as mw]) +(require '[reitit.coercion.schema]) +(require '[reitit.ring :as ring]) +(require '[schema.core :as s]) + +(def PositiveInt (s/constrained s/Int pos? 'PositiveInt)) (def app (ring/ring-handler @@ -126,7 +133,7 @@ Invalid response: ### Optimizations -The coercion middleware are [compiled againts a route](compiling_middleware,md). This enables them to compile and cache the actual coercers for the defined models ahead of time. They also unmount if a route doesn't have `:coercion` and `:parameters` or `:responses` defined. +The coercion middleware are [compiled againts a route](compiling_middleware,md). In the compile step the actual coercer implementations are compiled for the defined models. Also, the mw doesn't mount itself if a route doesn't have `:coercion` and `:parameters` or `:responses` defined. We can query the compiled middleware chain for the routes: @@ -160,8 +167,6 @@ Has no mounted middleware: ``` ## Thanks to -Most of the thing are just polished version of the original implementations. Thanks to: - * [compojure-api](https://clojars.org/metosin/compojure-api) for the initial `Coercion` protocol * [ring-swagger](https://github.com/metosin/ring-swagger#more-complete-example) for the `:parameters` and `:responses` syntax. * [schema](https://github.com/plumatic/schema) and [schema-tools](https://github.com/metosin/schema-tools) for Schema Coercion diff --git a/perf-test/clj/reitit/coercion_perf_test.clj b/perf-test/clj/reitit/coercion_perf_test.clj index d3641446..2b496374 100644 --- a/perf-test/clj/reitit/coercion_perf_test.clj +++ b/perf-test/clj/reitit/coercion_perf_test.clj @@ -4,7 +4,6 @@ [reitit.perf-utils :refer :all] [clojure.spec.alpha :as s] [spec-tools.core :as st] - [spec-tools.data-spec :as ds] [muuntaja.middleware :as mm] [muuntaja.core :as m] [muuntaja.format.jsonista :as jsonista-format] @@ -13,9 +12,7 @@ [reitit.coercion.spec :as spec] [reitit.coercion.schema :as schema] [reitit.coercion :as coercion] - [reitit.ring :as ring] - [reitit.core :as r]) - (:import (java.io ByteArrayInputStream))) + [reitit.ring :as ring])) ;; ;; start repl with `lein perf repl` diff --git a/perf-test/clj/reitit/go_perf_test.clj b/perf-test/clj/reitit/go_perf_test.clj index 676c99a2..5c5dd175 100644 --- a/perf-test/clj/reitit/go_perf_test.clj +++ b/perf-test/clj/reitit/go_perf_test.clj @@ -20,7 +20,7 @@ ;; (defn h [path] - (fn [req] + (fn [_] {:status 200, :body path})) (defn add [handler routes route] diff --git a/perf-test/clj/reitit/lupapiste_perf_test.clj b/perf-test/clj/reitit/lupapiste_perf_test.clj index dff097ce..9d44b02b 100644 --- a/perf-test/clj/reitit/lupapiste_perf_test.clj +++ b/perf-test/clj/reitit/lupapiste_perf_test.clj @@ -1,22 +1,15 @@ (ns reitit.lupapiste-perf-test (:require [clojure.test :refer [deftest testing is]] - [criterium.core :as cc] [reitit.perf-utils :refer :all] - [cheshire.core :as json] - [clojure.string :as str] [reitit.core :as reitit] [reitit.ring :as ring] [bidi.bidi :as bidi] - - [ataraxy.core :as ataraxy] - [compojure.core :as compojure] [io.pedestal.http.route.definition.table :as table] [io.pedestal.http.route.map-tree :as map-tree] - [io.pedestal.http.route.router :as pedestal] - [io.pedestal.http.route :as route])) + [io.pedestal.http.route.router :as pedestal])) ;; ;; start repl with `lein perf repl` diff --git a/perf-test/clj/reitit/nodejs_perf_test.clj b/perf-test/clj/reitit/nodejs_perf_test.clj index fd31d31b..512a421f 100644 --- a/perf-test/clj/reitit/nodejs_perf_test.clj +++ b/perf-test/clj/reitit/nodejs_perf_test.clj @@ -1,6 +1,5 @@ (ns reitit.nodejs-perf-test - (:require [criterium.core :as cc] - [reitit.perf-utils :refer :all] + (:require [reitit.perf-utils :refer :all] [immutant.web :as web] [reitit.ring :as ring])) diff --git a/perf-test/clj/reitit/opensensors_perf_test.clj b/perf-test/clj/reitit/opensensors_perf_test.clj index 05f0c268..2beba938 100644 --- a/perf-test/clj/reitit/opensensors_perf_test.clj +++ b/perf-test/clj/reitit/opensensors_perf_test.clj @@ -1,22 +1,17 @@ (ns reitit.opensensors-perf-test - (:require [clojure.test :refer [deftest testing is]] - [criterium.core :as cc] - [reitit.perf-utils :refer :all] + (:require [reitit.perf-utils :refer :all] [cheshire.core :as json] [clojure.string :as str] [reitit.core :as reitit] [reitit.ring :as ring] [bidi.bidi :as bidi] - [ataraxy.core :as ataraxy] - [compojure.core :refer [routes context ANY]] [io.pedestal.http.route.definition.table :as table] [io.pedestal.http.route.map-tree :as map-tree] - [io.pedestal.http.route.router :as pedestal] - [io.pedestal.http.route :as route])) + [io.pedestal.http.route.router :as pedestal])) ;; ;; start repl with `lein perf repl` diff --git a/perf-test/clj/reitit/perf_utils.clj b/perf-test/clj/reitit/perf_utils.clj index 4ce4b33b..deb7b499 100644 --- a/perf-test/clj/reitit/perf_utils.clj +++ b/perf-test/clj/reitit/perf_utils.clj @@ -30,11 +30,7 @@ (defn bench-routes [routes req f] (let [router (reitit/router routes) - urls (valid-urls router) - random-url #(rand-nth urls) - log-time #(let [now (System/nanoTime)] (%) (- (System/nanoTime) now)) - total 10000 - dropped (int (* total 0.45))] + urls (valid-urls router)] (mapv (fn [path] (let [request (map->Request (req path)) diff --git a/perf-test/clj/reitit/prefix_tree_perf_test.clj b/perf-test/clj/reitit/prefix_tree_perf_test.clj index 1f09421e..f920aadf 100644 --- a/perf-test/clj/reitit/prefix_tree_perf_test.clj +++ b/perf-test/clj/reitit/prefix_tree_perf_test.clj @@ -70,10 +70,10 @@ nil routes)) #_(def reitit-tree - (reduce - (fn [acc [p d]] - (trie/insert acc p d)) - nil routes)) + (reduce + (fn [acc [p d]] + (trie/insert acc p d)) + nil routes)) (def reitit-segment (segment/create routes)) @@ -101,8 +101,8 @@ ;; 0.8ms (return route-data) ;; 0.8ms (fix payloads) #_(cc/quick-bench - (dotimes [_ 1000] - (trie/lookup reitit-tree "/v1/orgs/1/topics" {}))) + (dotimes [_ 1000] + (trie/lookup reitit-tree "/v1/orgs/1/topics" {}))) ;; 0.9ms (initial) ;; 0.5ms (protocols) From 8a48d6790b9a9248d545a164e140a41c21cc5854 Mon Sep 17 00:00:00 2001 From: Tommi Reiman Date: Sat, 16 Dec 2017 10:51:32 +0200 Subject: [PATCH 02/19] . --- doc/ring/coercion.md | 2 +- .../src/reitit/ring/coercion_middleware.cljc | 2 +- perf-test/clj/reitit/coercion_perf_test.clj | 18 +++++++++++++----- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/doc/ring/coercion.md b/doc/ring/coercion.md index e9291c9d..8b4e7d0d 100644 --- a/doc/ring/coercion.md +++ b/doc/ring/coercion.md @@ -145,7 +145,7 @@ We can query the compiled middleware chain for the routes: :result :post :middleware (->> (mapv :name))) ; [::mw/coerce-exceptions -; ::mw/coerce-parameters +; ::mw/coerce-request ; ::mw/coerce-response] ``` diff --git a/modules/reitit-ring/src/reitit/ring/coercion_middleware.cljc b/modules/reitit-ring/src/reitit/ring/coercion_middleware.cljc index 19ea395d..22eaefdc 100644 --- a/modules/reitit-ring/src/reitit/ring/coercion_middleware.cljc +++ b/modules/reitit-ring/src/reitit/ring/coercion_middleware.cljc @@ -21,7 +21,7 @@ "Middleware for pluggable request coercion. Expects a :coercion of type `reitit.coercion/Coercion` and :parameters from route data, otherwise does not mount." - {:name ::coerce-parameters + {:name ::coerce-request :compile (fn [{:keys [coercion parameters]} opts] (if (and coercion parameters) (let [coercers (coercion/request-coercers coercion parameters opts)] diff --git a/perf-test/clj/reitit/coercion_perf_test.clj b/perf-test/clj/reitit/coercion_perf_test.clj index 2b496374..32776195 100644 --- a/perf-test/clj/reitit/coercion_perf_test.clj +++ b/perf-test/clj/reitit/coercion_perf_test.clj @@ -8,7 +8,7 @@ [muuntaja.core :as m] [muuntaja.format.jsonista :as jsonista-format] [jsonista.core :as j] - [reitit.coercion-middleware :as coercion-middleware] + [reitit.ring.coercion-middleware :as coercion-middleware] [reitit.coercion.spec :as spec] [reitit.coercion.schema :as schema] [reitit.coercion :as coercion] @@ -43,7 +43,7 @@ ;; 4600ns (bench! "coerce-parameters" - (#'coercion-middleware/coerce-parameters coercers request)) + (#'coercion-middleware/coerce-request-middleware coercers request)) ;; 2700ns (bench! @@ -87,7 +87,7 @@ (-open-model [_ spec] spec) (-encode-error [_ error] error) (-request-coercer [_ type spec] (fn [value format] value)) - (-response-coercer [this spec] (protocol/request-coercer this :response spec))) + (-response-coercer [this spec] (coercion/request-coercer this :response spec {}))) (comment (doseq [coercion [nil (->NoOpCoercion) spec/coercion]] @@ -172,11 +172,15 @@ (cc/quick-bench (app req))))) (defn json-perf-test [] + (title "json") (let [m (m/create (jsonista-format/with-json-format m/default-options)) app (ring/ring-handler (ring/router - ["/plus" {:post {:handler (fn [{{:keys [x y]} :body-params}] - {:status 200, :body {:result (+ x y)}})}}] + ["/plus" {:post {:handler (fn [request] + (let [body (:body-params request) + x (:x body) + y (:y body)] + {:status 200, :body {:result (+ x y)}}))}}] {:data {:middleware [[mm/wrap-format m]]}})) request {:request-method :post :uri "/plus" @@ -191,6 +195,7 @@ (-> request app :body slurp)))) (defn schema-json-perf-test [] + (title "schema-json") (let [m (m/create (jsonista-format/with-json-format m/default-options)) app (ring/ring-handler (ring/router @@ -216,6 +221,7 @@ (-> request app :body slurp)))) (defn schema-perf-test [] + (title "schema") (let [app (ring/ring-handler (ring/router ["/plus" {:post {:responses {200 {:schema {:result Long}}} @@ -239,6 +245,7 @@ (call)))) (defn data-spec-perf-test [] + (title "data-spec") (let [app (ring/ring-handler (ring/router ["/plus" {:post {:responses {200 {:schema {:result int?}}} @@ -267,6 +274,7 @@ (s/def ::response (s/keys :req-un [::result])) (defn spec-perf-test [] + (title "spec") (let [app (ring/ring-handler (ring/router ["/plus" {:post {:responses {200 {:schema ::response}} From b5d1ecc453af025f9ac1965788c6c07edc46351b Mon Sep 17 00:00:00 2001 From: Tommi Reiman Date: Sun, 17 Dec 2017 21:24:21 +0200 Subject: [PATCH 03/19] Middleware & Interceptor perf tests --- dev-resources/logback.xml | 18 + .../reitit/middleware_interceptor_perf.clj | 250 ++++++++++++ test/cljc/reitit/chain.clj | 380 ++++++++++++++++++ 3 files changed, 648 insertions(+) create mode 100644 dev-resources/logback.xml create mode 100644 perf-test/clj/reitit/middleware_interceptor_perf.clj create mode 100644 test/cljc/reitit/chain.clj diff --git a/dev-resources/logback.xml b/dev-resources/logback.xml new file mode 100644 index 00000000..dd87ec5e --- /dev/null +++ b/dev-resources/logback.xml @@ -0,0 +1,18 @@ + + + + %-5level %logger{36} - %msg%n + + + INFO + + + + + + + + + + + \ No newline at end of file diff --git a/perf-test/clj/reitit/middleware_interceptor_perf.clj b/perf-test/clj/reitit/middleware_interceptor_perf.clj new file mode 100644 index 00000000..d769ec61 --- /dev/null +++ b/perf-test/clj/reitit/middleware_interceptor_perf.clj @@ -0,0 +1,250 @@ +(ns reitit.middleware-interceptor-perf + (:require [criterium.core :as cc] + [reitit.perf-utils :refer :all] + [reitit.middleware :as middleware] + [reitit.interceptor :as interceptor] + + reitit.chain + io.pedestal.interceptor + io.pedestal.interceptor.chain)) + +;; +;; start repl with `lein perf repl` +;; perf measured with the following setup: +;; +;; Model Name: MacBook Pro +;; Model Identifier: MacBookPro113 +;; Processor Name: Intel Core i7 +;; Processor Speed: 2,5 GHz +;; Number of Processors: 1 +;; Total Number of Cores: 4 +;; L2 Cache (per Core): 256 KB +;; L3 Cache: 6 MB +;; Memory: 16 GB +;; + +;; +;; middleware +;; + +(set! *warn-on-reflection* true) + +(defrecord RequestOrContext [values queue stack]) + +(def +items+ 100) + +(defn expected! [x] + (assert (= (range +items+) (:values x)))) + +(defn middleware [handler value] + (fn [request] + (let [values (or (:values request) [])] + (handler (assoc request :values (conj values value)))))) + +(defn middleware-test [] + (let [mw (map (fn [value] [middleware value]) (range +items+)) + app (middleware/chain mw identity) + map-request {} + record-request (map->RequestOrContext map-request)] + + ;; 10.8 µs + (title "middleware - map") + (expected! (app map-request)) + (cc/quick-bench + (app map-request)) + + ;; 4.7 µs + (title "middleware - record") + (expected! (app record-request)) + (cc/quick-bench + (app record-request)) + + (title "middleware - dynamic") + (expected! ((middleware/chain mw identity) record-request)) + (cc/quick-bench + ((middleware/chain mw identity) record-request)))) + +;; +;; Reduce +;; + +(defn test-reduce [] + (let [ints (vec (range +items+)) + size (count ints)] + + ;; 64µs + (cc/quick-bench + (reduce #(+ ^int %1 ^int %2) ints)) + + ;; 123µs + (cc/quick-bench + (loop [sum 0, i 0] + (if (= i size) + sum + (recur (+ sum ^int (nth ints i)) (inc i))))) + + ;; 34µs + (cc/quick-bench + (let [iter (clojure.lang.RT/iter ints)] + (loop [sum 0] + (if (.hasNext iter) + (recur (+ sum ^int (.next iter))) + sum)))))) + +;; +;; Interceptor +;; + +(defn interceptor [value] + (fn [context] + (let [values (or (:values context) [])] + (assoc context :values (conj values value))))) + +;; +;; Pedestal +;; + +(defn pedestal-chain-text [] + (let [is (map io.pedestal.interceptor/interceptor + (map (fn [value] + {:enter (interceptor value)}) (range +items+))) + ctx (io.pedestal.interceptor.chain/enqueue nil is)] + + ;; 78 µs + (title "pedestal") + (cc/quick-bench + (io.pedestal.interceptor.chain/execute ctx)))) + +(defn pedestal-tuned-chain-text [] + (let [is (map io.pedestal.interceptor/interceptor + (map (fn [value] + {:enter (interceptor value)}) (range +items+))) + ctx (reitit.chain/map->Context (reitit.chain/enqueue nil is))] + + ;; 67 µs + (title "pedestal - tuned") + (cc/quick-bench + (reitit.chain/execute ctx)))) + +;; +;; Naive chain +;; + +(defn execute [ctx f] (f ctx)) + +(defn executor-reduce [interceptors] + (fn [ctx] + (as-> ctx $ + (reduce execute $ (keep :enter interceptors)) + (reduce execute $ (reverse (keep :leave interceptors)))))) + +(defn interceptor-test [] + (let [interceptors (map (fn [value] [interceptor value]) (range +items+)) + app (executor-reduce (interceptor/chain interceptors identity)) + map-request {} + record-request (map->RequestOrContext map-request)] + + ;; 13.5 µs (Map) + (title "interceptors - map") + (expected! (app map-request)) + (cc/quick-bench + (app map-request)) + + ;; 7.2 µs (Record) + (title "interceptors - record") + (expected! (app record-request)) + (cc/quick-bench + (app record-request)))) + +;; +;; different reducers +;; + +(defn enqueue [ctx interceptors] + (let [queue (or (:queue ctx) clojure.lang.PersistentQueue/EMPTY)] + (assoc ctx :queue (into queue interceptors)))) + +(defn queue [ctx interceptors] + (let [queue (or (:queue ctx) clojure.lang.PersistentQueue/EMPTY)] + (into queue interceptors))) + +(defn leavel-all-queue [ctx stack] + (let [it (clojure.lang.RT/iter stack)] + (loop [ctx ctx] + (if (.hasNext it) + (if-let [leave (-> it .next :leave)] + (recur (leave ctx)) + (recur ctx)) + ctx)))) + +(defn executor-queue [interceptors] + (fn [ctx] + (loop [queue (queue ctx interceptors) + stack nil + ctx ctx] + (if-let [interceptor (peek queue)] + (let [queue (pop queue) + stack (conj stack interceptor) + f (or (:enter interceptor) identity)] + (recur queue stack (f ctx))) + (leavel-all-queue ctx stack))))) + +(defn leave-all-ctx-queue [ctx stack] + (let [it (clojure.lang.RT/iter stack)] + (loop [ctx ctx] + (if (.hasNext it) + (if-let [leave (-> it .next :leave)] + (recur (leave ctx)) + (recur ctx)) + ctx)))) + +(defn executor-ctx-queue [interceptors] + (fn [ctx] + (loop [ctx (assoc ctx :queue (queue ctx interceptors))] + (let [queue ^clojure.lang.PersistentQueue (:queue ctx) + stack (:stack ctx)] + (if-let [interceptor (peek queue)] + (let [queue (pop queue) + stack (conj stack interceptor) + f (or (:enter interceptor) identity)] + (recur (-> ctx (assoc :queue queue) (assoc :stac stack) f))) + (leave-all-ctx-queue ctx stack)))))) + +(defn interceptor-chain-test [] + (let [interceptors (map (fn [value] [interceptor value]) (range +items+)) + app-reduce (executor-reduce (interceptor/chain interceptors identity)) + app-queue (executor-queue (interceptor/chain interceptors identity)) + app-ctx-queue (executor-ctx-queue (interceptor/chain interceptors identity)) + request {}] + + ;; 14.2 µs + (title "interceptors - reduce") + (expected! (app-reduce request)) + (cc/quick-bench + (app-reduce request)) + + ;; 19.4 µs + (title "interceptors - queue") + (expected! (app-queue request)) + (cc/quick-bench + (app-queue request)) + + ;; 30.9 µs + (title "interceptors - ctx-queue") + (expected! (app-ctx-queue request)) + (cc/quick-bench + (app-ctx-queue request)))) + +(comment + (interceptor-test) + (middleware-test) + (pedestal-chain-text) + (pedestal-tuned-chain-text) + (interceptor-chain-test)) + +; Middleware (static chain) => 5µs +; Middleware (dynamic chain) => 60µs + +; Interceptor (static queue) => 20µs +; Interceptor (context queues) => 30µs +; Pedestal (context queues) => 79µs diff --git a/test/cljc/reitit/chain.clj b/test/cljc/reitit/chain.clj new file mode 100644 index 00000000..cff3242c --- /dev/null +++ b/test/cljc/reitit/chain.clj @@ -0,0 +1,380 @@ +; Copyright 2013 Relevance, Inc. +; Copyright 2014-2016 Cognitect, Inc. + +; The use and distribution terms for this software are covered by the +; Eclipse Public License 1.0 (http://opensource.org/licenses/eclipse-1.0) +; which can be found in the file epl-v10.html at the root of this distribution. +; +; By using this software in any fashion, you are agreeing to be bound by +; the terms of this license. +; +; You must not remove this notice, or any other, from this software. + +(ns reitit.chain + "Interceptor pattern. Executes a chain of Interceptor functions on a + common \"context\" map, maintaining a virtual \"stack\", with error + handling and support for asynchronous execution." + (:refer-clojure :exclude (name)) + (:require [clojure.core.async :as async :refer [ex-info [^Throwable t execution-id interceptor stage] + (let [iname (name interceptor) + throwable-str (pr-str (type t))] + (ex-info (str throwable-str " in Interceptor " iname " - " (.getMessage t)) + (merge {:execution-id execution-id + :stage stage + :interceptor iname + :exception-type (keyword throwable-str) + :exception t} + (ex-data t)) + t))) + +(defn- try-f + "If f is not nil, invokes it on context. If f throws an exception, + assoc's it on to context as :error." + [context interceptor stage] + (let [execution-id (:execution-id context)] + (if-let [f (stage interceptor)] + (try (log/debug :interceptor (name interceptor) + :stage stage + :execution-id execution-id + :fn f) + (f context) + (catch Throwable t + (log/debug :throw t :execution-id execution-id) + (assoc context :error (throwable->ex-info t execution-id interceptor stage)))) + (do (log/trace :interceptor (name interceptor) + :skipped? true + :stage stage + :execution-id execution-id) + context)))) + +(defn- try-error + "If error-fn is not nil, invokes it on context and the current :error + from context." + [context interceptor] + (let [execution-id (:execution-id context)] + (if-let [error-fn (:error interceptor)] + (let [ex (:error context) + stage :error] + (log/debug :interceptor (name interceptor) + :stage :error + :execution-id execution-id) + (try (error-fn (assoc context :error nil) ex) + (catch Throwable t + (if (identical? (type t) (type (:exception ex))) + (do (log/debug :rethrow t :execution-id execution-id) + context) + (do (log/debug :throw t :suppressed (:exception-type ex) :execution-id execution-id) + (-> context + (assoc :error (throwable->ex-info t execution-id interceptor :error)) + (update :suppressed conj ex))))))) + (do (log/trace :interceptor (name interceptor) + :skipped? true + :stage :error + :execution-id execution-id) + context)))) + +(defn- check-terminators + "Invokes each predicate in :terminators on context. If any predicate + returns true, removes :queue from context." + [context] + (if (some #(% context) (:terminators context)) + (let [execution-id (:execution-id context)] + (log/debug :in 'check-terminators + :terminate? true + :execution-id execution-id) + (assoc context :queue nil)) + context)) + +(defn- prepare-for-async + "Call all of the :enter-async functions in a context. The purpose of these + functions is to ready backing servlets or any other machinery for preparing + an asynchronous response." + [{:keys [enter-async] :as context}] + (doseq [enter-async-fn enter-async] + (enter-async-fn context))) + +(defn- go-async + "When presented with a channel as the return value of an enter function, + wait for the channel to return a new-context (via a go block). When a new + context is received, restart execution of the interceptor chain with that + context. + This function is non-blocking, returning nil immediately (a signal to halt + further execution on this thread)." + ([old-context context-channel] + (prepare-for-async old-context) + (go + (if-let [new-context ( context + (assoc :queue new-queue) + (assoc :stack new-stack) + (try-f interceptor interceptor-key))] + (cond + (channel? context) (go-async (assoc old-context + :async-info {:interceptor interceptor + :stage interceptor-key + :stack new-stack}) + context) + (:error context) (assoc context :queue nil) + (not= (:bindings context) (:bindings old-context)) (assoc context :rebind true) + true (recur (check-terminators context))))))))) + +(defn- process-all + [context interceptor-key] + ;; If we're processing leave handlers, reverse the queue + (let [context (if (= interceptor-key :leave) (update context :queue reverse) context) + context (with-bindings (or (:bindings context) + {}) + (process-all-with-binding context interceptor-key))] + (if (:rebind context) + (recur (assoc context :rebind nil) interceptor-key) + context))) + +(defn- process-any-errors-with-binding + "Unwinds the context by invoking :error functions of Interceptors on + the :stack of context, but **only** if there is an :error present in the context." + [context] + (log/debug :in 'process-any-errors :execution-id (:execution-id context)) + (loop [context context] + (let [stack (:stack context)] + (log/trace :context context) + (if (empty? stack) + context + (let [interceptor (peek stack) + pre-bindings (:bindings context) + old-context context + context (assoc context :stack (pop stack)) + context (if (:error context) + (try-error context interceptor) + context)] + (cond + (channel? context) (go-async old-context context) + (not= (:bindings context) pre-bindings) (assoc context :rebind true) + true (recur context))))))) + +(defn- process-any-errors + "Establish the bindings present in `context` as thread local + bindings, and then invoke process-any-errors-with-binding. + Conditionally re-establish bindings if a change in bindings is made by an + interceptor." + [context] + (let [context (with-bindings (or (:bindings context) {}) + (process-any-errors-with-binding context))] + (if (:rebind context) + (recur (assoc context :rebind nil)) + context))) + +(defn- enter-all + "Establish the bindings present in `context` as thread local + bindings, and then invoke enter-all-with-binding. Conditionally + re-establish bindings if a change in bindings is made by an + interceptor." + [context] + (process-all context :enter)) + +(defn- leave-all-with-binding + "Unwinds the context by invoking :leave functions of Interceptors on + the :stack of context. Returns updated context." + [context] + (log/debug :in 'leave-all :execution-id (:execution-id context)) + (loop [context context] + (let [stack (:stack context)] + (log/trace :context context) + (if (empty? stack) + context + (let [interceptor (peek stack) + pre-bindings (:bindings context) + old-context context + context (assoc context :stack (pop stack)) + context (if (:error context) + (try-error context interceptor) + (try-f context interceptor :leave))] + (cond + (channel? context) (go-async old-context context) + (not= (:bindings context) pre-bindings) (assoc context :rebind true) + true (recur context))))))) + +(defn- leave-all + "Establish the bindings present in `context` as thread local + bindings, and then invoke leave-all-with-binding. Conditionally + re-establish bindings if a change in bindings is made by an + interceptor." + [context] + (let [context (with-bindings (or (:bindings context) {}) + (leave-all-with-binding context))] + (if (:rebind context) + (recur (assoc context :rebind nil)) + context))) + +(defn enqueue + "Adds interceptors to the end of context's execution queue. Creates + the queue if necessary. Returns updated context." + [context interceptors] + {:pre (every? interceptor/interceptor? interceptors)} + (log/trace :enqueue (map name interceptors) :context context) + (update context :queue + (fnil into clojure.lang.PersistentQueue/EMPTY) + interceptors)) + +(defn enqueue* + "Like 'enqueue' but vararg. + If the last argument is a sequence of interceptors, + they're unpacked and to added to the context's execution queue." + [context & interceptors-and-seq] + (if (seq? (last interceptors-and-seq)) + (enqueue context (apply list* interceptors-and-seq)) + (enqueue context interceptors-and-seq))) + +(defn terminate + "Removes all remaining interceptors from context's execution queue. + This effectively short-circuits execution of Interceptors' :enter + functions and begins executing the :leave functions." + [context] + (log/trace :in 'terminate :context context) + (assoc context :queue nil)) + +(defn terminate-when + "Adds pred as a terminating condition of the context. pred is a + function that takes a context as its argument. It will be invoked + after every Interceptor's :enter function. If pred returns logical + true, execution will stop at that Interceptor." + [context pred] + (update context :terminators conj pred)) + +(def ^:private ^AtomicLong execution-id (AtomicLong.)) + +(defn- begin [context] + (if (:execution-id context) + context + (let [execution-id (.incrementAndGet execution-id)] + (log/debug :in 'begin :execution-id execution-id) + (log/trace :context context) + (assoc context :execution-id execution-id)))) + +(defn- end [context] + (if (:execution-id context) + (do + (log/debug :in 'end :execution-id (:execution-id context) :context-keys (keys context)) + (log/trace :context context) + (assoc context :stack nil :execution-id nil)) + context)) + +(defn execute-only + "Like `execute`, but only processes the interceptors in a single direction, + using `interceptor-key` (i.e. :enter, :leave) to determine which functions + to call. + --- + Executes a queue of Interceptors attached to the context. Context + must be a map, Interceptors are added with 'enqueue'. + An Interceptor Record has keys :enter, :leave, and :error. + The value of each key is a function; missing + keys or nil values are ignored. When executing a context, all + the `interceptor-key` functions are invoked in order. As this happens, the + Interceptors are pushed on to a stack." + ([context interceptor-key] + (let [context (some-> context + map->Context + begin + (process-all interceptor-key) + terminate + process-any-errors + end)] + (if-let [ex (:error context)] + (throw ex) + context))) + ([context interceptor-key interceptors] + (execute-only (enqueue context interceptors) interceptor-key))) + +(defn execute + "Executes a queue of Interceptors attached to the context. Context + must be a map, Interceptors are added with 'enqueue'. + An Interceptor is a map or map-like object with the keys :enter, + :leave, and :error. The value of each key is a function; missing + keys or nil values are ignored. When executing a context, first all + the :enter functions are invoked in order. As this happens, the + Interceptors are pushed on to a stack. + When execution reaches the end of the queue, it begins popping + Interceptors off the stack and calling their :leave functions. + Therefore :leave functions are called in the opposite order from + :enter functions. + Both the :enter and :leave functions are called on a single + argument, the context map, and return an updated context. + If any Interceptor function throws an exception, execution stops and + begins popping Interceptors off the stack and calling their :error + functions. The :error function takes two arguments: the context and + an exception. It may either handle the exception, in which case the + execution continues with the next :leave function on the stack; or + re-throw the exception, passing control to the :error function on + the stack. If the exception reaches the end of the stack without + being handled, execute will throw it." + ([context] + (let [context (some-> context + begin + enter-all + terminate + leave-all + end)] + (if-let [ex (:error context)] + (throw ex) + context))) + ([context interceptors] + (execute (enqueue context interceptors)))) From 7e37ab9ad051b68e3355b14e75ea95092a53ed41 Mon Sep 17 00:00:00 2001 From: Tommi Reiman Date: Tue, 26 Dec 2017 17:43:51 +0200 Subject: [PATCH 04/19] Update expound --- project.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project.clj b/project.clj index 3e23ab29..989867ab 100644 --- a/project.clj +++ b/project.clj @@ -42,7 +42,7 @@ [metosin/reitit] [metosin/schema-tools "0.10.0-SNAPSHOT"] - [expound "0.3.4"] + [expound "0.4.0"] [orchestra "2017.11.12-1"] [ring "1.6.3"] From ee0c733726b372b487b8feddc450885a5a9bbcf0 Mon Sep 17 00:00:00 2001 From: Tommi Reiman Date: Tue, 26 Dec 2017 17:56:46 +0200 Subject: [PATCH 05/19] Compile routes already in `reitit.core/router` --- modules/reitit-core/src/reitit/core.cljc | 29 +++++++++++------------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/modules/reitit-core/src/reitit/core.cljc b/modules/reitit-core/src/reitit/core.cljc index 656f7156..552b8377 100644 --- a/modules/reitit-core/src/reitit/core.cljc +++ b/modules/reitit-core/src/reitit/core.cljc @@ -142,8 +142,7 @@ ([routes] (linear-router routes {})) ([routes opts] - (let [compiled (compile-routes routes opts) - names (find-names routes opts) + (let [names (find-names routes opts) [pl nl] (reduce (fn [[pl nl] [p {:keys [name] :as data} result]] (let [{:keys [params] :as route} (impl/create [p data result]) @@ -152,7 +151,7 @@ (->PartialMatch p data result % params))] [(conj pl route) (if name (assoc nl name f) nl)])) - [[] {}] compiled) + [[] {}] routes) lookup (impl/fast-map nl)] ^{:type ::router} (reify @@ -160,7 +159,7 @@ (router-name [_] :linear-router) (routes [_] - compiled) + routes) (options [_] opts) (route-names [_] @@ -190,14 +189,13 @@ (str "can't create :lookup-router with wildcard routes: " wilds) {:wilds wilds :routes routes}))) - (let [compiled (compile-routes routes opts) - names (find-names routes opts) + (let [names (find-names routes opts) [pl nl] (reduce (fn [[pl nl] [p {:keys [name] :as data} result]] [(assoc pl p (->Match p data result {} p)) (if name (assoc nl name #(->Match p data result % p)) - nl)]) [{} {}] compiled) + nl)]) [{} {}] routes) data (impl/fast-map pl) lookup (impl/fast-map nl)] ^{:type ::router} @@ -205,7 +203,7 @@ (router-name [_] :lookup-router) (routes [_] - compiled) + routes) (options [_] opts) (route-names [_] @@ -225,8 +223,7 @@ ([routes] (segment-router routes {})) ([routes opts] - (let [compiled (compile-routes routes opts) - names (find-names routes opts) + (let [names (find-names routes opts) [pl nl] (reduce (fn [[pl nl] [p {:keys [name] :as data} result]] (let [{:keys [params] :as route} (impl/create [p data result]) @@ -235,7 +232,7 @@ (->PartialMatch p data result % params))] [(segment/insert pl p (->Match p data result nil nil)) (if name (assoc nl name f) nl)])) - [nil {}] compiled) + [nil {}] routes) lookup (impl/fast-map nl)] ^{:type ::router} (reify @@ -243,7 +240,7 @@ (router-name [_] :segment-router) (routes [_] - compiled) + routes) (options [_] opts) (route-names [_] @@ -272,7 +269,7 @@ (str ":single-static-path-router requires exactly 1 static route: " routes) {:routes routes}))) (let [[n :as names] (find-names routes opts) - [[p data result] :as compiled] (compile-routes routes opts) + [[p data result] :as compiled] routes p #?(:clj (.intern ^String p) :cljs p) match (->Match p data result {} p)] ^{:type ::router} @@ -280,7 +277,7 @@ (router-name [_] :single-static-path-router) (routes [_] - compiled) + routes) (options [_] opts) (route-names [_] @@ -304,7 +301,6 @@ (mixed-router routes {})) ([routes opts] (let [{wild true, lookup false} (group-by impl/wild-route? routes) - compiled (compile-routes routes opts) ->static-router (if (= 1 (count lookup)) single-static-path-router lookup-router) wildcard-router (segment-router wild opts) static-router (->static-router lookup opts) @@ -314,7 +310,7 @@ (router-name [_] :mixed-router) (routes [_] - compiled) + routes) (options [_] opts) (route-names [_] @@ -350,6 +346,7 @@ (let [{:keys [router] :as opts} (meta-merge default-router-options opts) routes (resolve-routes raw-routes opts) conflicting (conflicting-routes routes) + routes (compile-routes routes opts) wilds? (boolean (some impl/wild-route? routes)) all-wilds? (every? impl/wild-route? routes) router (cond From ce15ae95ec493cc1d8c738a8e883e0286af30df4 Mon Sep 17 00:00:00 2001 From: Tommi Reiman Date: Tue, 26 Dec 2017 22:39:14 +0200 Subject: [PATCH 06/19] Exclude -lookup (cljs) --- modules/reitit-core/src/reitit/core.cljc | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/reitit-core/src/reitit/core.cljc b/modules/reitit-core/src/reitit/core.cljc index 552b8377..f7fe9c83 100644 --- a/modules/reitit-core/src/reitit/core.cljc +++ b/modules/reitit-core/src/reitit/core.cljc @@ -1,4 +1,5 @@ (ns reitit.core + (:refer-clojure :exclude [-lookup]) (:require [meta-merge.core :refer [meta-merge]] [clojure.string :as str] [reitit.segment :as segment] From 06cb1301cd561d17276865a2729bc25bfc66bbe7 Mon Sep 17 00:00:00 2001 From: Tommi Reiman Date: Tue, 26 Dec 2017 22:40:34 +0200 Subject: [PATCH 07/19] Support route data validation in router --- modules/reitit-core/src/reitit/core.cljc | 5 +++ modules/reitit-core/src/reitit/spec.cljc | 42 ++++++++++++++++++++++++ test/cljc/reitit/spec_test.cljc | 30 +++++++++++++++-- 3 files changed, 75 insertions(+), 2 deletions(-) diff --git a/modules/reitit-core/src/reitit/core.cljc b/modules/reitit-core/src/reitit/core.cljc index f7fe9c83..c471d6d6 100644 --- a/modules/reitit-core/src/reitit/core.cljc +++ b/modules/reitit-core/src/reitit/core.cljc @@ -336,9 +336,11 @@ | `: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 => side-effect` to validate route (data) | `:conflicts` | Function of `{route #{route}} => side-effect` to handle conflicting routes (default `reitit.core/throw-on-conflicts!`) | `:router` | Function of `routes opts => router` to override the actual router implementation" ([raw-routes] @@ -358,6 +360,9 @@ all-wilds? segment-router :else mixed-router)] + (when-let [validate (:validate opts)] + (validate routes opts)) + (when-let [conflicts (:conflicts opts)] (when conflicting (conflicts conflicting))) diff --git a/modules/reitit-core/src/reitit/spec.cljc b/modules/reitit-core/src/reitit/spec.cljc index 0538ecad..d2a93e79 100644 --- a/modules/reitit-core/src/reitit/spec.cljc +++ b/modules/reitit-core/src/reitit/spec.cljc @@ -33,6 +33,14 @@ (s/or :route ::route :routes (s/coll-of ::route :into []))) +;; +;; Default data +;; + +(s/def ::name keyword?) +(s/def ::handler fn?) +(s/def ::default-data (s/keys :opt-un [::name ::handler])) + ;; ;; router ;; @@ -62,3 +70,37 @@ :args (s/or :1arity (s/cat :data (s/spec ::raw-routes)) :2arity (s/cat :data (s/spec ::raw-routes), :opts ::opts)) :ret ::router) + +;; +;; Route data validator +;; + + +(defrecord Problem [path scope data spec problems]) + +(defn problems-str [problems explain] + (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")) + problems))) + +(defn throw-on-problems! [problems explain] + (throw + (ex-info + (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))) diff --git a/test/cljc/reitit/spec_test.cljc b/test/cljc/reitit/spec_test.cljc index ece98bd1..6260088c 100644 --- a/test/cljc/reitit/spec_test.cljc +++ b/test/cljc/reitit/spec_test.cljc @@ -3,7 +3,8 @@ [#?(:clj clojure.spec.test.alpha :cljs cljs.spec.test.alpha) :as stest] [clojure.spec.alpha :as s] [reitit.core :as r] - [reitit.spec :as spec]) + [reitit.spec :as rs] + [expound.alpha :as e]) #?(:clj (:import (clojure.lang ExceptionInfo)))) @@ -45,7 +46,7 @@ ["/ipa"]]))) (testing "routes conform to spec (can't spec protocol functions)" - (is (= true (s/valid? ::spec/routes (r/routes (r/router ["/ping"])))))) + (is (= true (s/valid? ::rs/routes (r/routes (r/router ["/ping"])))))) (testing "options" @@ -75,3 +76,28 @@ {:compile nil} {:conflicts nil} {:router nil})))) + +(deftest route-data-validation-test + (testing "validation is turned off by default" + (is (true? (r/router? (r/router + ["/api" {:handler "identity"}]))))) + + (testing "with default spec validates :name and :handler" + (is (thrown-with-msg? + ExceptionInfo + #"Invalid route data" + (r/router + ["/api" {:handler "identity"}] + {:validate rs/validate-spec!}))) + (is (thrown-with-msg? + ExceptionInfo + #"Invalid route data" + (r/router + ["/api" {:name "kikka"}] + {:validate rs/validate-spec!})))) + + (testing "spec can be overridden" + (is (true? (r/router? (r/router + ["/api" {:handler "identity"}] + {:spec any? + :validate rs/validate-spec!})))))) From 0dbb75ad4414ceb28e0b530d2f03285bd51fb022 Mon Sep 17 00:00:00 2001 From: Tommi Reiman Date: Tue, 26 Dec 2017 22:40:53 +0200 Subject: [PATCH 08/19] Add :path & :method to Endpoints --- modules/reitit-ring/src/reitit/ring.cljc | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/modules/reitit-ring/src/reitit/ring.cljc b/modules/reitit-ring/src/reitit/ring.cljc index e9270895..7b4c332e 100644 --- a/modules/reitit-ring/src/reitit/ring.cljc +++ b/modules/reitit-ring/src/reitit/ring.cljc @@ -6,6 +6,7 @@ (def http-methods #{:get :head :patch :delete :options :post :put}) (defrecord Methods [get head post put delete trace options connect patch any]) +(defrecord Endpoint [data handler path method middleware]) (defn- group-keys [data] (reduce-kv @@ -58,14 +59,19 @@ acc)) data http-methods)]) (defn compile-result [[path data] opts] - (let [[top childs] (group-keys data)] + (let [[top childs] (group-keys data) + ->endpoint (fn [p d m s] + (-> (middleware/compile-result [p d] opts s) + (map->Endpoint) + (assoc :path p) + (assoc :method m)))] (if-not (seq childs) - (map->Methods {:any (middleware/compile-result [path top] opts)}) + (map->Methods {:any (->endpoint path top :any nil)}) (reduce-kv (fn [acc method data] (let [data (meta-merge top data)] - (assoc acc method (middleware/compile-result [path data] opts method)))) - (map->Methods {:any (if (:handler top) (middleware/compile-result [path data] opts))}) + (assoc acc method (->endpoint path data method method)))) + (map->Methods {:any (if (:handler top) (->endpoint path data :any nil))}) childs)))) (defn router From 1a9583b31bf5651a48d95e4b9cb0f6c955b635e5 Mon Sep 17 00:00:00 2001 From: Tommi Reiman Date: Tue, 26 Dec 2017 22:41:17 +0200 Subject: [PATCH 09/19] Support ring-route-data validation --- modules/reitit-ring/src/reitit/ring/spec.cljc | 33 ++++++++++++ test/cljc/reitit/ring_spec_test.cljc | 52 +++++++++++++++++++ 2 files changed, 85 insertions(+) create mode 100644 modules/reitit-ring/src/reitit/ring/spec.cljc create mode 100644 test/cljc/reitit/ring_spec_test.cljc diff --git a/modules/reitit-ring/src/reitit/ring/spec.cljc b/modules/reitit-ring/src/reitit/ring/spec.cljc new file mode 100644 index 00000000..96c407fa --- /dev/null +++ b/modules/reitit-ring/src/reitit/ring/spec.cljc @@ -0,0 +1,33 @@ +(ns reitit.ring.spec + (:require [clojure.spec.alpha :as s] + [reitit.middleware #?@(:cljs [:refer [Middleware]])] + [reitit.spec :as rs]) + #?(:clj + (:import (reitit.middleware Middleware)))) + +;; +;; Specs +;; + +(s/def ::middleware (s/coll-of (partial instance? Middleware))) + +(s/def ::data + (s/keys :req-un [::rs/handler] + :opt-un [::rs/name ::middleware])) + +;; +;; Validator +;; + +(defn- validate-ring-route-data [routes spec] + (->> (for [[p _ c] routes + [method {:keys [data] :as endpoint}] c + :when endpoint] + (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}}] + (when-let [problems (validate-ring-route-data routes spec)] + (rs/throw-on-problems! problems explain))) diff --git a/test/cljc/reitit/ring_spec_test.cljc b/test/cljc/reitit/ring_spec_test.cljc new file mode 100644 index 00000000..8d063d79 --- /dev/null +++ b/test/cljc/reitit/ring_spec_test.cljc @@ -0,0 +1,52 @@ +(ns reitit.ring-spec-test + (:require [clojure.test :refer [deftest testing is]] + [reitit.ring :as ring] + [reitit.ring.spec :as rrs] + [reitit.core :as r] + [reitit.spec :as rs]) + #?(:clj + (:import (clojure.lang ExceptionInfo)))) + + +(deftest route-data-validation-test + (testing "validation is turned off by default" + (is (true? (r/router? + (r/router + ["/api" {:handler "identity"}]))))) + + (testing "with default spec validates :name, :handler and :middleware" + (is (thrown-with-msg? + ExceptionInfo + #"Invalid route data" + (ring/router + ["/api" {:handler "identity"}] + {:validate rrs/validate-spec!}))) + (is (thrown-with-msg? + ExceptionInfo + #"Invalid route data" + (ring/router + ["/api" {:handler identity + :name "kikka"}] + {:validate rrs/validate-spec!}))) + (is (thrown-with-msg? + ExceptionInfo + #"Invalid route data" + (ring/router + ["/api" {:handler identity + :middleware [{}]}] + {:validate rrs/validate-spec!})))) + + (testing "all endpoints are validated" + (is (thrown-with-msg? + ExceptionInfo + #"Invalid route data" + (ring/router + ["/api" {:patch {:handler "identity"}}] + {:validate rrs/validate-spec!})))) + + (testing "spec can be overridden" + (is (true? (r/router? + (ring/router + ["/api" {:handler "identity"}] + {:spec any? + :validate rrs/validate-spec!})))))) From 388de03ead8e72af04c5ac8713e4c2708e0d797c Mon Sep 17 00:00:00 2001 From: Tommi Reiman Date: Tue, 26 Dec 2017 22:42:12 +0200 Subject: [PATCH 10/19] Exclude chain --- test/cljc/reitit/chain.clj | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/cljc/reitit/chain.clj b/test/cljc/reitit/chain.clj index cff3242c..ece79b96 100644 --- a/test/cljc/reitit/chain.clj +++ b/test/cljc/reitit/chain.clj @@ -10,6 +10,7 @@ ; ; You must not remove this notice, or any other, from this software. +(comment (ns reitit.chain "Interceptor pattern. Executes a chain of Interceptor functions on a common \"context\" map, maintaining a virtual \"stack\", with error @@ -378,3 +379,4 @@ context))) ([context interceptors] (execute (enqueue context interceptors)))) +) From 6321d1e8be413e711e60e5fdc66e7ac1293af3f9 Mon Sep 17 00:00:00 2001 From: Tommi Reiman Date: Wed, 27 Dec 2017 20:27:51 +0200 Subject: [PATCH 11/19] Docs for route-data validation --- doc/SUMMARY.md | 1 + doc/advanced/route_validation.md | 10 +- doc/basics/README.md | 1 + doc/basics/route_data_validation.md | 129 +++++++++++++++++++++++ modules/reitit-core/src/reitit/core.cljc | 4 +- 5 files changed, 135 insertions(+), 10 deletions(-) create mode 100644 doc/basics/route_data_validation.md diff --git a/doc/SUMMARY.md b/doc/SUMMARY.md index 3be1ac81..92495a97 100644 --- a/doc/SUMMARY.md +++ b/doc/SUMMARY.md @@ -7,6 +7,7 @@ * [Path-based Routing](basics/path_based_routing.md) * [Name-based Routing](basics/name_based_routing.md) * [Route Data](basics/route_data.md) + * [Route Data Validation](basics/route_data_validation.md) * [Route Conflicts](basics/route_conflicts.md) * [Coercion](coercion/README.md) * [Coercion Explained](coercion/coercion.md) diff --git a/doc/advanced/route_validation.md b/doc/advanced/route_validation.md index 167bf526..7d261684 100644 --- a/doc/advanced/route_validation.md +++ b/doc/advanced/route_validation.md @@ -2,8 +2,6 @@ Namespace `reitit.spec` contains [clojure.spec](https://clojure.org/about/spec) definitions for raw-routes, routes, router and router options. -**NOTE:** Use of specs requires to use Clojure 1.9.0 or higher. - ## Example ```clj @@ -26,12 +24,12 @@ Namespace `reitit.spec` contains [clojure.spec](https://clojure.org/about/spec) ## At development time -`reitit.core/router` can be instrumented and use something like [expound](https://github.com/bhb/expound) to pretty-print the spec problems. +`reitit.core/router` can be instrumented and use a tool like [expound](https://github.com/bhb/expound) to pretty-print the spec problems. First add a `:dev` dependency to: ```clj -[expound "0.3.0"] ; or higher +[expound "0.4.0"] ; or higher ``` Some bootstrapping: @@ -162,7 +160,3 @@ And we are ready to go: ; ------------------------- ; Detected 2 errors ``` - -# Validating route data - -*TODO* diff --git a/doc/basics/README.md b/doc/basics/README.md index 65fc6a69..77fcb3d7 100644 --- a/doc/basics/README.md +++ b/doc/basics/README.md @@ -5,4 +5,5 @@ * [Path-based Routing](path_based_routing.md) * [Name-based Routing](name_based_routing.md) * [Route Data](route_data.md) +* [Route Data Validation](route_data_validation.md) * [Route Conflicts](route_conflicts.md) diff --git a/doc/basics/route_data_validation.md b/doc/basics/route_data_validation.md new file mode 100644 index 00000000..119a6ab0 --- /dev/null +++ b/doc/basics/route_data_validation.md @@ -0,0 +1,129 @@ +# Route Data Validation + +Route data can be anything, so it's easy to do mistakes. Accidentally using a `:role` key instead of `:roles` might render the whole routing app without any authorization in place. + +To fail fast, we could use the custom `:coerce` and `:compile` hooks to apply data validation and throw exceptions on first sighted problem. + +But there is a better way. Router also has a `:validation` hook to validate the whole route tree after it's successfuly compiled. It expects a 2-arity function `routes opts => ()` that can side-effect in case of validation errors. + +## 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. + +A Router with invalid route data: + +```clj +(require '[reitit.core :as r]) + +(r/router + ["/api" {:handler "identity"}]) +; #object[reitit.core$...] +``` + +Fails fast with `clojure.spec` validation turned on: + +```clj +(require '[reitit.spec :as rs]) + +(r/router + ["/api" {:handler "identity"}] + {:validate rs/validate-spec!}) +; CompilerException clojure.lang.ExceptionInfo: Invalid route data: +; +; -- On route ----------------------- +; +; "/api" +; +; In: [:handler] val: "identity" fails spec: :reitit.spec/handler at: [:handler] predicate: fn? +; +; {:problems (#reitit.spec.Problem{:path "/api", :scope nil, :data {:handler "identity"}, :spec :reitit.spec/default-data, :problems #:clojure.spec.alpha{:problems ({:path [:handler], :pred clojure.core/fn?, :val "identity", :via [:reitit.spec/default-data :reitit.spec/handler], :in [:handler]}), :spec :reitit.spec/default-data, :value {:handler "identity"}}})}, compiling: ... + +``` + +### Customizing spec validation + +`rs/validate-spec!` reads the following router options: + + | key | description | + | ---------------|-------------| + | `:spec` | the spec to verify the route data (default `::rs/default-data`) + | `::rs/explain` | custom explain function (default `clojure.spec.alpha/explain-str`) + +**NOTE**: `clojure.spec` implicitly validates all values with fully-qualified keys if specs exist with the same name. + +Below is an example of using [expound](https://github.com/bhb/expound) to pretty-print route data problems. + +```clj +(require '[clojure.spec.alpha :as s]) +(require '[expound.alpha :as e]) + +(s/def ::role #{:admin :manager}) +(s/def ::roles (s/coll-of ::role :into #{})) + +(r/router + ["/api" {:handler identity + ::roles #{:adminz}}] + {::rs/explain e/expound-str + :validate rs/validate-spec!}) +; CompilerException clojure.lang.ExceptionInfo: Invalid route data: +; +; -- On route ----------------------- +; +; "/api" +; +; -- Spec failed -------------------- +; +; {:handler ..., :user/roles #{:adminz}} +; ^^^^^^^ +; +; should be one of: `:admin`,`:manager` +; +; -- Relevant specs ------- +; +; :user/role: +; #{:admin :manager} +; :user/roles: +; (clojure.spec.alpha/coll-of :user/role :into #{}) +; :reitit.spec/default-data: +; (clojure.spec.alpha/keys +; :opt-un +; [:reitit.spec/name :reitit.spec/handler]) +; +; ------------------------- +; Detected 1 error +; +; {:problems (#reitit.spec.Problem{:path "/api", :scope nil, :data {:handler #object[clojure.core$identity 0x15b59b0e "clojure.core$identity@15b59b0e"], :user/roles #{:adminz}}, :spec :reitit.spec/default-data, :problems #:clojure.spec.alpha{:problems ({:path [:user/roles], :pred #{:admin :manager}, :val :adminz, :via [:reitit.spec/default-data :user/roles :user/role], :in [:user/roles 0]}), :spec :reitit.spec/default-data, :value {:handler #object[clojure.core$identity 0x15b59b0e "clojure.core$identity@15b59b0e"], :user/roles #{:adminz}}}})}, compiling: ... +``` + +Explicitly requiring a `::roles` key in a route data: + +```clj +(r/router + ["/api" {:handler identity}] + {:spec (s/merge (s/keys :req [::roles]) ::rs/default-data) + ::rs/explain e/expound-str + :validate rs/validate-spec!}) +; CompilerException clojure.lang.ExceptionInfo: Invalid route data: +; +; -- On route ----------------------- +; +; "/api" +; +; -- Spec failed -------------------- +; +; {:handler +; #object[clojure.core$identity 0x15b59b0e "clojure.core$identity@15b59b0e"]} +; +; should contain key: `:user/roles` +; +; | key | spec | +; |-------------+----------------------------------------| +; | :user/roles | (coll-of #{:admin :manager} :into #{}) | +; +; +; +; ------------------------- +; Detected 1 error +; +; {:problems (#reitit.spec.Problem{:path "/api", :scope nil, :data {:handler #object[clojure.core$identity 0x15b59b0e "clojure.core$identity@15b59b0e"]}, :spec #object[clojure.spec.alpha$merge_spec_impl$reify__2124 0x7461744b "clojure.spec.alpha$merge_spec_impl$reify__2124@7461744b"], :problems #:clojure.spec.alpha{:problems ({:path [], :pred (clojure.core/fn [%] (clojure.core/contains? % :user/roles)), :val {:handler #object[clojure.core$identity 0x15b59b0e "clojure.core$identity@15b59b0e"]}, :via [], :in []}), :spec #object[clojure.spec.alpha$merge_spec_impl$reify__2124 0x7461744b "clojure.spec.alpha$merge_spec_impl$reify__2124@7461744b"], :value {:handler #object[clojure.core$identity 0x15b59b0e "clojure.core$identity@15b59b0e"]}}})}, compiling:(/Users/tommi/projects/metosin/reitit/test/cljc/reitit/spec_test.cljc:151:1) +``` diff --git a/modules/reitit-core/src/reitit/core.cljc b/modules/reitit-core/src/reitit/core.cljc index c471d6d6..53163406 100644 --- a/modules/reitit-core/src/reitit/core.cljc +++ b/modules/reitit-core/src/reitit/core.cljc @@ -340,8 +340,8 @@ | `: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 => side-effect` to validate route (data) - | `:conflicts` | Function of `{route #{route}} => side-effect` to handle conflicting routes (default `reitit.core/throw-on-conflicts!`) + | `: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" ([raw-routes] (router raw-routes {})) From 98637f9db517c7aa07a6911ac2bed43df0e97822 Mon Sep 17 00:00:00 2001 From: Tommi Reiman Date: Wed, 27 Dec 2017 20:28:03 +0200 Subject: [PATCH 12/19] Polish docs --- doc/basics/route_conflicts.md | 4 ++-- doc/basics/route_data.md | 4 +--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/doc/basics/route_conflicts.md b/doc/basics/route_conflicts.md index 401a2aa9..8a16ce29 100644 --- a/doc/basics/route_conflicts.md +++ b/doc/basics/route_conflicts.md @@ -1,8 +1,8 @@ # Route Conflicts -Many routing libraries allow multiple matches for a single path lookup. Usually, the first match is used and the rest are effecively unreachanle. This is not good, especially if route tree is merged from multiple sources. +Most routing libraries allow conflicting paths within a router. On lookup, the first match is used making rest of the matching routes effecively unreachable. This is not good, especially if route tree is merged from multiple sources. -Reitit resolves this by running explicit conflicit resolution when a `router` is called. Conflicting routes are passed into a `:conflicts` callback. Default implementation throws `ex-info` with a descriptive message. +Reitit resolves this by running explicit conflicit resolution when a Router is created. Conflicting routes are passed into a `:conflicts` callback. Default implementation throws `ex-info` with a descriptive message. Examples router with conflicting routes: diff --git a/doc/basics/route_data.md b/doc/basics/route_data.md index 81d918a9..9c08c654 100644 --- a/doc/basics/route_data.md +++ b/doc/basics/route_data.md @@ -1,6 +1,6 @@ # Route Data -Route data is the heart of this library. Routes can have any data attachted to them. Data is interpeted either by the client application or the `Router` via it's `:coerce` and `:compile` hooks. This enables co-existence of both [adaptive and principled](https://youtu.be/x9pxbnFC4aQ?t=1907) components. +Route data is the heart of this library. Routes can have any data attachted to them. Data is interpeted either by the client application or the `Router` via it's `:coerce` and `:compile` hooks. Together with `clojure.spec` -validation this enables co-existence of both [adaptive and principled](https://youtu.be/x9pxbnFC4aQ?t=1907) components. Routes can have a non-sequential route argument that is expanded into route data map when a router is created. @@ -75,8 +75,6 @@ Resolved route tree: By default, `reitit/Expand` protocol is used to expand the route arguments. It expands keywords into `:name` and functions into `:handler` key in the route data map. It's easy to add custom expanders and one can chenge the whole expand implementation via [router options](../advanced/configuring_routers.md). ```clj -(require '[reitit.core :as r]) - (def router (r/router [["/ping" ::ping] From 5c53b6e9896aaa610e6701c50c4301585fd7a356 Mon Sep 17 00:00:00 2001 From: Tommi Reiman Date: Wed, 27 Dec 2017 20:28:25 +0200 Subject: [PATCH 13/19] Fix path spec - doesn't have to start with "/" --- modules/reitit-core/src/reitit/spec.cljc | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/modules/reitit-core/src/reitit/spec.cljc b/modules/reitit-core/src/reitit/spec.cljc index d2a93e79..5e225ee2 100644 --- a/modules/reitit-core/src/reitit/spec.cljc +++ b/modules/reitit-core/src/reitit/spec.cljc @@ -8,8 +8,7 @@ ;; routes ;; -(s/def ::path (s/with-gen (s/and string? #(or (str/blank? %) (str/starts-with? % "/"))) - #(gen/fmap (fn [s] (str "/" s)) (s/gen string?)))) +(s/def ::path (s/with-gen string? #(gen/fmap (fn [s] (str "/" s)) (s/gen string?)))) (s/def ::arg (s/and any? (complement vector?))) (s/def ::data (s/map-of keyword? any?)) @@ -82,7 +81,7 @@ (apply str "Invalid route data:\n\n" (mapv (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")) problems))) From 974f4534fcd9632d697fdca834b4f074a1bc03ea Mon Sep 17 00:00:00 2001 From: Tommi Reiman Date: Wed, 27 Dec 2017 21:35:37 +0200 Subject: [PATCH 14/19] Update README --- README.md | 71 ++++++++++++++++++++++++++++++++++++++++++++++++++- doc/README.md | 7 ++--- 2 files changed, 74 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 37ae93a9..c65bbaab 100644 --- a/README.md +++ b/README.md @@ -7,11 +7,12 @@ A friendly data-driven router for Clojure(Script). * First-class [route data](https://metosin.github.io/reitit/basics/route_data.html) * Bi-directional routing * [Pluggable coercion](https://metosin.github.io/reitit/coercion/coercion.html) ([schema](https://github.com/plumatic/schema) & [clojure.spec](https://clojure.org/about/spec)) -* [Ring-router](https://metosin.github.io/reitit/ring/ring.html) with [data-driven middleware](https://metosin.github.io/reitit/ring/data_driven_middleware.html) * Extendable * Modular * [Fast](https://metosin.github.io/reitit/performance.html) +There are also [Ring-router](https://metosin.github.io/reitit/ring/ring.html) with [data-driven middleware](https://metosin.github.io/reitit/ring/data_driven_middleware.html) as a separate module. + See the [full documentation](https://metosin.github.io/reitit/) for details. ## Latest version @@ -56,6 +57,74 @@ Optionally, the parts can be required separately: ; :path "/api/orders/2"} ``` +## Ring example + +A Ring routing app with input & output coercion using [data-specs](https://github.com/metosin/spec-tools/blob/master/README.md#data-specs). + +```clj +(require '[reitit.ring :as ring]) +(require '[reitit.coercion.spec]) +(require '[reitit.ring.coercion-middleware :as mw]) + +(def app + (ring/ring-handler + (ring/router + ["/api" + ["/math" {:name ::math + :get {:coercion reitit.coercion.spec/coercion + :parameters {:query {:x int?, :y int?}} + :responses {200 {:schema {:total pos-int?}}} + :handler (fn [{{{:keys [x y]} :query} :parameters}] + {:status 200 + :body {:total (+ x y)}})}}]] + {:data {:middleware [mw/coerce-exceptions-middleware + mw/coerce-request-middleware + mw/coerce-response-middleware]}}))) +``` + +Valid request: + +```clj +(app {:request-method :get + :uri "/api/math" + :query-params {:x "1", :y "2"}}) +; {:status 200 +; :body {:total 3}} +``` + +Invalid request: + +```clj +(app {:request-method :get + :uri "/api/math" + :query-params {:x "1", :y "a"}}) +;{:status 400, +; :body {:type :reitit.coercion/request-coercion, +; :coercion :spec, +; :spec "(spec-tools.core/spec {:spec (clojure.spec.alpha/keys :req-un [:$spec20745/x :$spec20745/y]), :type :map, :keys #{:y :x}, :keys/req #{:y :x}})", +; :problems [{:path [:y], +; :pred "clojure.core/int?", +; :val "a", +; :via [:$spec20745/y], +; :in [:y]}], +; :value {:x "1", :y "a"}, +; :in [:request :query-params]}} + + +``` + +Reverse routing: + +```clj +(require '[reitit.core :as r]) + +(-> app + (ring/get-router) + (r/match-by-name ::math) + :path) +;; "/api/math" +``` + ## More info [Check out the full documentation!](https://metosin.github.io/reitit/) diff --git a/doc/README.md b/doc/README.md index ad3a4549..e0248a21 100644 --- a/doc/README.md +++ b/doc/README.md @@ -6,12 +6,13 @@ * [Route conflict resolution](./basics/route_conflicts.md) * First-class [route data](./basics/route_data.md) * Bi-directional routing -* [Ring-router](./ring/ring.html) with [data-driven middleware](./ring/data_driven_middleware.html) -* [Pluggable coercion](./coercion/coercion.html) ([schema](https://github.com/plumatic/schema) & [clojure.spec](https://clojure.org/about/spec)) +* [Pluggable coercion](./coercion/coercion.md) ([schema](https://github.com/plumatic/schema) & [clojure.spec](https://clojure.org/about/spec)) * Extendable * Modular * [Fast](performance.md) +There are also [Ring-router](./ring/ring.md) with [data-driven middleware](./ring/data_driven_middleware.md) as a separate module. + To use Reitit, add the following dependecy to your project: ```clj @@ -23,7 +24,7 @@ Optionally, the parts can be required separately: ```clj [metosin/reitit-core "0.1.0-SNAPSHOT"] ; just the router [metosin/reitit-ring "0.1.0-SNAPSHOT"] ; ring-router -[metosin/reitit-spec "0.1.0-SNAPSHOT"] ; spec-coercion +[metosin/reitit-spec "0.1.0-SNAPSHOT"] ; spec coercion [metosin/reitit-schema "0.1.0-SNAPSHOT"] ; schema coercion ``` From db77b5383104043e4925871ad2669a3d90906048 Mon Sep 17 00:00:00 2001 From: Tommi Reiman Date: Wed, 27 Dec 2017 21:37:04 +0200 Subject: [PATCH 15/19] Fix tests --- test/cljc/reitit/spec_test.cljc | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/test/cljc/reitit/spec_test.cljc b/test/cljc/reitit/spec_test.cljc index 6260088c..cd07b822 100644 --- a/test/cljc/reitit/spec_test.cljc +++ b/test/cljc/reitit/spec_test.cljc @@ -20,6 +20,8 @@ ["/api" {}] + ["api" {}] + [["/api" {}]] ["/api" @@ -35,9 +37,6 @@ (r/router data))) - ;; missing slash - ["invalid" {}] - ;; path [:invalid {}] @@ -68,7 +67,7 @@ (r/router ["/api"] opts))) - {:path "api"} + {:path :api} {:path nil} {:data nil} {:expand nil} From abb09e2736a685b5e44f39cd7da6ff8ce22ec565 Mon Sep 17 00:00:00 2001 From: Tommi Reiman Date: Wed, 27 Dec 2017 21:40:36 +0200 Subject: [PATCH 16/19] Fix -exclude --- modules/reitit-core/src/reitit/core.cljc | 1 - modules/reitit-core/src/reitit/segment.cljc | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/reitit-core/src/reitit/core.cljc b/modules/reitit-core/src/reitit/core.cljc index 53163406..2340e647 100644 --- a/modules/reitit-core/src/reitit/core.cljc +++ b/modules/reitit-core/src/reitit/core.cljc @@ -1,5 +1,4 @@ (ns reitit.core - (:refer-clojure :exclude [-lookup]) (:require [meta-merge.core :refer [meta-merge]] [clojure.string :as str] [reitit.segment :as segment] diff --git a/modules/reitit-core/src/reitit/segment.cljc b/modules/reitit-core/src/reitit/segment.cljc index 5fbad362..214a6fac 100644 --- a/modules/reitit-core/src/reitit/segment.cljc +++ b/modules/reitit-core/src/reitit/segment.cljc @@ -1,4 +1,5 @@ (ns reitit.segment + (:refer-clojure :exclude [-lookup]) (:require [reitit.impl :as impl] [clojure.string :as str])) From 76095a42cf0d78862c7e2d961ac7fb8d5638cdda Mon Sep 17 00:00:00 2001 From: Tommi Reiman Date: Thu, 28 Dec 2017 10:07:06 +0200 Subject: [PATCH 17/19] Coveralls-script doesn't work? --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 9272e162..172042e6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,7 +7,7 @@ install: - npm install script: - ./scripts/test.sh $TEST - - ./scripts/submit-to-coveralls.sh $TEST +# - ./scripts/submit-to-coveralls.sh $TEST env: matrix: - TEST=clj From 526fb49f5a05a18fe8a2060bc75678ae8614ae9b Mon Sep 17 00:00:00 2001 From: Tommi Reiman Date: Thu, 28 Dec 2017 10:44:35 +0200 Subject: [PATCH 18/19] . --- doc/advanced/configuring_routers.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/doc/advanced/configuring_routers.md b/doc/advanced/configuring_routers.md index eee5ce9a..1ac757f7 100644 --- a/doc/advanced/configuring_routers.md +++ b/doc/advanced/configuring_routers.md @@ -1,15 +1,16 @@ # Configuring Routers -Routers can be configured via options. Options allow things like [`clojure.spec`](https://clojure.org/about/spec) validation for route data and fast, compiled handlers. The following options are available for the `reitit.core/router`: +Routers can be configured via options. The following options are available for the `reitit.core/router`: | key | description | | -------------|-------------| | `: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 - | `:conflicts` | Function of `{route #{route}} => side-effect` to handle conflicting routes (default `reitit.core/throw-on-conflicts!`) + | `: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 - From 8ffa9cd1a43fd21f1b42bc9527f9d2fba85f9bd9 Mon Sep 17 00:00:00 2001 From: Tommi Reiman Date: Thu, 28 Dec 2017 10:48:18 +0200 Subject: [PATCH 19/19] coveralls doesn't still work --- .circleci/config.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index adc1033d..053fd450 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -18,9 +18,9 @@ jobs: command: ./scripts/test.sh clj - store_test_results: path: ~/test/target/junit.xml - - run: - name: Run coverage - command: ./scripts/submit-to-coveralls.sh clj +# - run: +# name: Run coverage +# command: ./scripts/submit-to-coveralls.sh clj - save_cache: key: 'v1-test-{{ checksum "project.clj" }}' paths: