From 37cb99a13e9a8b7cd24513047925aea83b9915ea Mon Sep 17 00:00:00 2001 From: Tommi Reiman Date: Sat, 17 Nov 2018 20:50:04 +0200 Subject: [PATCH] Expose spec problems to error handlers --- CHANGELOG.md | 48 +++++++++++++++++++ .../reitit-spec/src/reitit/coercion/spec.cljc | 9 ++-- .../reitit/ring/middleware/exception_test.clj | 42 ++++++++++++++++ 3 files changed, 95 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c8f2f77..1314ec99 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,51 @@ +## UNRELEASED + +## `reitit-spec` + +* Spec problems are exposed as-is into request & response coercion errors, enabling pretty-printers like [expound](https://github.com/bhb/expound) to be used: + +```clj +(require '[reitit.ring :as ring]) +(require '[reitit.ring.middleware.exception :as exception]) +(require '[reitit.ring.coercion :as coercion]) +(require '[expound.alpha :as expound]) + +(defn coercion-error-handler [status] + (let [printer (expound/custom-printer {:theme :figwheel-theme, :print-specs? false}) + handler (exception/create-coercion-handler status)] + (fn [exception request] + (printer (-> exception ex-data :problems)) + (handler exception request)))) + +(def app + (ring/ring-handler + (ring/router + ["/plus" + {:get + {:parameters {:query {:x int?, :y int?}} + :responses {200 {:body {:total pos-int?}}} + :handler (fn [{{{:keys [x y]} :query} :parameters}] + {:status 200, :body {:total (+ x y)}})}}] + {:data {:coercion reitit.coercion.spec/coercion + :middleware [(exception/create-exception-middleware + (merge + exception/default-handlers + {:reitit.coercion/request-coercion (coercion-error-handler 400) + :reitit.coercion/response-coercion (coercion-error-handler 500)})) + coercion/coerce-request-middleware + coercion/coerce-response-middleware]}}))) + +(app + {:uri "/plus" + :request-method :get + :query-params {"x" "1", "y" "fail"}}) + +(app + {:uri "/plus" + :request-method :get + :query-params {"x" "1", "y" "-2"}}) +``` + ## 0.2.7 (2018-11-11) ## `reitit-spec` diff --git a/modules/reitit-spec/src/reitit/coercion/spec.cljc b/modules/reitit-spec/src/reitit/coercion/spec.cljc index a9b3ea33..9eff8bc6 100644 --- a/modules/reitit-spec/src/reitit/coercion/spec.cljc +++ b/modules/reitit-spec/src/reitit/coercion/spec.cljc @@ -114,9 +114,10 @@ (into-spec model name)) (-open-model [_ spec] spec) (-encode-error [_ error] - (-> error - (update :spec (comp str s/form)) - (update :problems (partial mapv #(update % :pred stringify-pred))))) + (let [problems (::s/problems error)] + (-> error + (update :spec (comp str s/form)) + (assoc :problems (mapv #(update % :pred stringify-pred) problems))))) (-request-coercer [this type spec] (let [spec (coercion/-compile-model this spec nil) {:keys [formats default]} (transformers type)] @@ -130,7 +131,7 @@ (let [problems (st/explain-data spec value transformer)] (coercion/map->CoercionError {:spec spec - :problems (::s/problems problems)})) + :problems problems})) (s/unform spec transformed))))) value)))) (-response-coercer [this spec] diff --git a/test/clj/reitit/ring/middleware/exception_test.clj b/test/clj/reitit/ring/middleware/exception_test.clj index 7e13f7c4..8414a020 100644 --- a/test/clj/reitit/ring/middleware/exception_test.clj +++ b/test/clj/reitit/ring/middleware/exception_test.clj @@ -2,6 +2,8 @@ (:require [clojure.test :refer [deftest testing is]] [reitit.ring :as ring] [reitit.ring.middleware.exception :as exception] + [reitit.coercion :as coercion] + [clojure.spec.alpha :as s] [reitit.coercion.spec] [reitit.ring.coercion] [muuntaja.core :as m]) @@ -114,3 +116,43 @@ (app {:request-method :get, :uri "/defaults"}))) (is (= {:status 500, :body "too many tries"} (app {:request-method :get, :uri "/defaults"}))))))) + +(deftest spec-coercion-exception-test + (let [app (ring/ring-handler + (ring/router + ["/plus" + {:get + {:parameters {:query {:x int?, :y int?}} + :responses {200 {:body {:total pos-int?}}} + :handler (fn [{{{:keys [x y]} :query} :parameters}] + {:status 200, :body {:total (+ x y)}})}}] + {:data {:coercion reitit.coercion.spec/coercion + :middleware [(exception/create-exception-middleware + (merge + exception/default-handlers + {::coercion/request-coercion (fn [e _] {:status 400, :body (ex-data e)}) + ::coercion/response-coercion (fn [e _] {:status 500, :body (ex-data e)})})) + reitit.ring.coercion/coerce-request-middleware + reitit.ring.coercion/coerce-response-middleware]}}))] + (testing "success" + (let [{:keys [status body]} (app {:uri "/plus", :request-method :get, :query-params {"x" "1", "y" "2"}})] + (is (= 200 status)) + (is (= body {:total 3})))) + + (testing "request error" + (let [{:keys [status body]} (app {:uri "/plus", :request-method :get, :query-params {"x" "1", "y" "fail"}})] + (is (= 400 status)) + (testing "spec error is exposed as is" + (let [problems (:problems body)] + (is (contains? problems ::s/spec)) + (is (contains? problems ::s/value)) + (is (contains? problems ::s/problems)))))) + + (testing "response error" + (let [{:keys [status body]} (app {:uri "/plus", :request-method :get, :query-params {"x" "1", "y" "-2"}})] + (is (= 500 status)) + (testing "spec error is exposed as is" + (let [problems (:problems body)] + (is (contains? problems ::s/spec)) + (is (contains? problems ::s/value)) + (is (contains? problems ::s/problems))))))))