From d0f712649176fdef3e1369aca6fa593183c68440 Mon Sep 17 00:00:00 2001 From: Tommi Reiman Date: Sun, 13 Feb 2022 19:29:19 +0200 Subject: [PATCH] add support for malli-lite --- CHANGELOG.md | 12 + doc/coercion/malli_coercion.md | 20 ++ .../src/reitit/coercion/malli.cljc | 9 +- project.clj | 2 +- test/cljc/reitit/coercion_test.cljc | 21 +- test/cljc/reitit/ring_coercion_test.cljc | 285 ++++++++++-------- 6 files changed, 226 insertions(+), 123 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d4aeca3..66bf9794 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,18 @@ We use [Break Versioning][breakver]. The version numbers follow a `. ExceptionInfo Request coercion failed... ``` +## Lite Syntax + +Same using [Lite Syntax](https://github.com/metosin/malli#lite): + +```clj +(def router + (r/router + ["/:company/users/:user-id" {:name ::user-view + :coercion reitit.coercion.malli/coercion + :parameters {:path {:company string? + :user-id int?}}}] + {:compile coercion/compile-request-coercers})) +``` + ## Configuring coercion Using `create` with options to create the coercion instead of `coercion`: @@ -58,6 +76,8 @@ Using `create` with options to create the coercion instead of `coercion`: :response {:default reitit.coercion.malli/default-transformer-provider}} ;; set of keys to include in error messages :error-keys #{:type :coercion :in :schema :value :errors :humanized #_:transformed} + ;; support lite syntax? + :lite true ;; schema identity function (default: close all map schemas) :compile mu/closed-schema ;; validate request & response diff --git a/modules/reitit-malli/src/reitit/coercion/malli.cljc b/modules/reitit-malli/src/reitit/coercion/malli.cljc index 9d56665e..20d97fa2 100644 --- a/modules/reitit-malli/src/reitit/coercion/malli.cljc +++ b/modules/reitit-malli/src/reitit/coercion/malli.cljc @@ -3,6 +3,7 @@ [clojure.set :as set] [clojure.walk :as walk] [malli.core :as m] + [malli.experimental.lite :as l] [malli.edn :as edn] [malli.error :as me] [malli.swagger :as swagger] @@ -115,6 +116,8 @@ :formats {"application/json" json-transformer-provider}}} ;; set of keys to include in error messages :error-keys #{:type :coercion :in :schema :value :errors :humanized #_:transformed} + ;; support lite syntax? + :lite true ;; schema identity function (default: close all map schemas) :compile mu/closed-schema ;; validate request & response @@ -134,9 +137,11 @@ ([] (create nil)) ([opts] - (let [{:keys [transformers compile options error-keys encode-error] :as opts} (merge default-options opts) + (let [{:keys [transformers lite compile options error-keys encode-error] :as opts} (merge default-options opts) show? (fn [key] (contains? error-keys key)) - transformers (walk/prewalk #(if (satisfies? TransformationProvider %) (-transformer % opts) %) transformers)] + transformers (walk/prewalk #(if (satisfies? TransformationProvider %) (-transformer % opts) %) transformers) + compile (if lite (fn [schema options] (compile (binding [l/*options* options] (l/schema schema)) options)) + compile)] ^{:type ::coercion/coercion} (reify coercion/Coercion (-get-name [_] :malli) diff --git a/project.clj b/project.clj index c2e4b3d0..c942499c 100644 --- a/project.clj +++ b/project.clj @@ -86,7 +86,7 @@ [metosin/muuntaja "0.6.8"] [metosin/sieppari "0.0.0-alpha13"] [metosin/jsonista "0.3.5"] - [metosin/malli "0.8.1"] + [metosin/malli "0.8.2-SNAPSHOT"] [lambdaisland/deep-diff "0.0-47"] [meta-merge "1.0.0"] [com.bhauman/spell-spec "0.1.2"] diff --git a/test/cljc/reitit/coercion_test.cljc b/test/cljc/reitit/coercion_test.cljc index ffd86be5..c836a853 100644 --- a/test/cljc/reitit/coercion_test.cljc +++ b/test/cljc/reitit/coercion_test.cljc @@ -7,7 +7,8 @@ [reitit.coercion.spec] [reitit.core :as r] [schema.core :as s] - [spec-tools.data-spec :as ds]) + [spec-tools.data-spec :as ds] + [malli.experimental.lite :as l]) #?(:clj (:import (clojure.lang ExceptionInfo)))) @@ -23,6 +24,12 @@ :query [:maybe [:map [:int int?] [:ints [:vector int?]] [:map [:map-of int? int?]]]]}}]] + ["/malli-lite" {:coercion reitit.coercion.malli/coercion} + ["/:number/:keyword" {:parameters {:path {:number int? + :keyword keyword?} + :query (l/maybe {:int int? + :ints (l/vector int?) + :map (l/map-of int? int?)})}}]] ["/spec" {:coercion reitit.coercion.spec/coercion} ["/:number/:keyword" {:parameters {:path {:number int? :keyword keyword?} @@ -56,6 +63,18 @@ (let [m (r/match-by-path r "/malli/kikka/abba")] (is (thrown? ExceptionInfo (coercion/coerce! m)))))) + (testing "malli-lite coercion" + (testing "succeeds" + (let [m (r/match-by-path r "/malli-lite/1/abba")] + (is (= {:path {:keyword :abba, :number 1}, :query nil} + (coercion/coerce! m)))) + (let [m (r/match-by-path r "/malli-lite/1/abba")] + (is (= {:path {:keyword :abba, :number 1}, :query {:int 10, :ints [1, 2, 3], :map {1 1, 2 2}}} + (coercion/coerce! (assoc m :query-params {"int" "10", "ints" ["1" "2" "3"], "map" {:1 "1", "2" "2"}})))))) + (testing "throws with invalid input" + (let [m (r/match-by-path r "/malli-lite/kikka/abba")] + (is (thrown? ExceptionInfo (coercion/coerce! m)))))) + ;; TODO: :map-of fails with string-keys (testing "spec-coercion" (testing "succeeds" diff --git a/test/cljc/reitit/ring_coercion_test.cljc b/test/cljc/reitit/ring_coercion_test.cljc index 0b485825..e527f0fe 100644 --- a/test/cljc/reitit/ring_coercion_test.cljc +++ b/test/cljc/reitit/ring_coercion_test.cljc @@ -2,7 +2,7 @@ (:require [clojure.test :refer [deftest is testing]] #?@(:clj [[muuntaja.middleware] - [jsonista.core :as j]]) + [jsonista.core :as j]]) [reitit.coercion.malli :as malli] [reitit.coercion.schema :as schema] [reitit.coercion.spec :as spec] @@ -10,7 +10,8 @@ [reitit.ring :as ring] [reitit.ring.coercion :as rrc] [schema.core :as s] - [spec-tools.data-spec :as ds]) + [spec-tools.data-spec :as ds] + [malli.experimental.lite :as l]) #?(:clj (:import (clojure.lang ExceptionInfo) @@ -210,139 +211,185 @@ (is (= 500 status)))))))) (deftest malli-coercion-test - (let [create (fn [middleware] + (let [create (fn [middleware routes] (ring/ring-handler (ring/router - ["/api" - - ["/validate" {:summary "just validation" - :coercion (reitit.coercion.malli/create {:transformers {}}) - :post {:parameters {:body [:map [:x int?]]} - :responses {200 {:body [:map [:x int?]]}} - :handler (fn [req] - {:status 200 - :body (-> req :parameters :body)})}}] - - ["/no-op" {:summary "no-operation" - :coercion (reitit.coercion.malli/create {:transformers {}, :validate false}) - :post {:parameters {:body [:map [:x int?]]} - :responses {200 {:body [:map [:x int?]]}} - :handler (fn [req] - {:status 200 - :body (-> req :parameters :body)})}}] - - ["/skip" {:summary "skip" - :coercion (reitit.coercion.malli/create {:enabled false}) - :post {:parameters {:body [:map [:x int?]]} - :responses {200 {:body [:map [:x int?]]}} - :handler (fn [req] - {:status 200 - :body (-> req :parameters :body)})}}] - - ["/or" {:post {:summary "accepts either of two map schemas" - :parameters {:body [:or [:map [:x int?]] [:map [:y int?]]]} - :responses {200 {:body [:map [:msg string?]]}} - :handler (fn [{{{:keys [x]} :body} :parameters}] - {:status 200 - :body {:msg (if x "you sent x" "you sent y")}})}}] - - ["/plus/:e" {:get {:parameters {:query [:map [:a {:optional true} int?]] - :body [:map [:b int?]] - :form [:map [:c [int? {:default 3}]]] - :header [:map [:d int?]] - :path [:map [:e int?]]} - :responses {200 {:body [:map [:total pos-int?]]} - 500 {:description "fail"}} - :handler handler}}]] + routes {:data {:middleware middleware :coercion malli/coercion}})))] - (testing "without exception handling" - (let [app (create [rrc/coerce-request-middleware - rrc/coerce-response-middleware])] + (doseq [{:keys [style routes]} [{:style "malli" + :routes ["/api" + ["/validate" {:summary "just validation" + :coercion (reitit.coercion.malli/create {:transformers {}}) + :post {:parameters {:body [:map [:x int?]]} + :responses {200 {:body [:map [:x int?]]}} + :handler (fn [req] + {:status 200 + :body (-> req :parameters :body)})}}] - (testing "all good" - (is (= {:status 200 - :body {:total 15}} - (app valid-request1))) - (is (= {:status 200 - :body {:total 115}} - (app valid-request2))) - (is (= {:status 200 - :body {:total 15}} - (app valid-request3))) - (testing "default values work" - (is (= {:status 200 - :body {:total 15}} - (app (update valid-request3 :form-params dissoc :c))))) - (is (= {:status 500 - :body {:evil true}} - (app (assoc-in valid-request1 [:query-params "a"] "666"))))) + ["/no-op" {:summary "no-operation" + :coercion (reitit.coercion.malli/create {:transformers {}, :validate false}) + :post {:parameters {:body [:map [:x int?]]} + :responses {200 {:body [:map [:x int?]]}} + :handler (fn [req] + {:status 200 + :body (-> req :parameters :body)})}}] - (testing "invalid request" - (is (thrown-with-msg? - ExceptionInfo - #"Request coercion failed" - (app invalid-request1)))) + ["/skip" {:summary "skip" + :coercion (reitit.coercion.malli/create {:enabled false}) + :post {:parameters {:body [:map [:x int?]]} + :responses {200 {:body [:map [:x int?]]}} + :handler (fn [req] + {:status 200 + :body (-> req :parameters :body)})}}] - (testing "invalid response" - (is (thrown-with-msg? - ExceptionInfo - #"Response coercion failed" - (app invalid-request2)))))) + ["/or" {:post {:summary "accepts either of two map schemas" + :parameters {:body [:or [:map [:x int?]] [:map [:y int?]]]} + :responses {200 {:body [:map [:msg string?]]}} + :handler (fn [{{{:keys [x]} :body} :parameters}] + {:status 200 + :body {:msg (if x "you sent x" "you sent y")}})}}] - (testing "with exception handling" - (let [app (create [rrc/coerce-exceptions-middleware - rrc/coerce-request-middleware - rrc/coerce-response-middleware])] + ["/plus/:e" {:get {:parameters {:query [:map [:a {:optional true} int?]] + :body [:map [:b int?]] + :form [:map [:c [int? {:default 3}]]] + :header [:map [:d int?]] + :path [:map [:e int?]]} + :responses {200 {:body [:map [:total pos-int?]]} + 500 {:description "fail"}} + :handler handler}}]]} + {:style "lite" + :routes ["/api" - (testing "just validation" - (is (= 400 (:status (app {:uri "/api/validate" - :request-method :post - :muuntaja/request {:format "application/edn"} - :body-params 123})))) - (is (= [:reitit.ring.coercion/coerce-exceptions - :reitit.ring.coercion/coerce-request - :reitit.ring.coercion/coerce-response] - (mounted-middleware app "/api/validate" :post)))) + ["/validate" {:summary "just validation" + :coercion (reitit.coercion.malli/create {:transformers {}}) + :post {:parameters {:body {:x int?}} + :responses {200 {:body {:x int?}}} + :handler (fn [req] + {:status 200 + :body (-> req :parameters :body)})}}] - (testing "no tranformation & validation" - (is (= 123 (:body (app {:uri "/api/no-op" - :request-method :post - :muuntaja/request {:format "application/edn"} - :body-params 123})))) - (is (= [:reitit.ring.coercion/coerce-exceptions - :reitit.ring.coercion/coerce-request - :reitit.ring.coercion/coerce-response] - (mounted-middleware app "/api/no-op" :post)))) + ["/no-op" {:summary "no-operation" + :coercion (reitit.coercion.malli/create {:transformers {}, :validate false}) + :post {:parameters {:body {:x int?}} + :responses {200 {:body {:x int?}}} + :handler (fn [req] + {:status 200 + :body (-> req :parameters :body)})}}] - (testing "skipping coercion" - (is (= nil (:body (app {:uri "/api/skip" - :request-method :post - :muuntaja/request {:format "application/edn"} - :body-params 123})))) - (is (= [:reitit.ring.coercion/coerce-exceptions] - (mounted-middleware app "/api/skip" :post)))) + ["/skip" {:summary "skip" + :coercion (reitit.coercion.malli/create {:enabled false}) + :post {:parameters {:body {:x int?}} + :responses {200 {:body {:x int?}}} + :handler (fn [req] + {:status 200 + :body (-> req :parameters :body)})}}] - (testing "or #407" - (is (= {:status 200 - :body {:msg "you sent x"}} - (app {:uri "/api/or" - :request-method :post - :body-params {:x 1}})))) + ["/or" {:post {:summary "accepts either of two map schemas" + :parameters {:body (l/or {:x int?} {:y int?})} + :responses {200 {:body {:msg string?}}} + :handler (fn [{{{:keys [x]} :body} :parameters}] + {:status 200 + :body {:msg (if x "you sent x" "you sent y")}})}}] - (testing "all good" - (is (= {:status 200 - :body {:total 15}} - (app valid-request1)))) + ["/plus/:e" {:get {:parameters {:query {:a (l/optional int?)} + :body {:b int?} + :form {:c [int? {:default 3}]} + :header {:d int?} + :path {:e int?}} + :responses {200 {:body {:total pos-int?}} + 500 {:description "fail"}} + :handler handler}}]]}]] - (testing "invalid request" - (let [{:keys [status]} (app invalid-request1)] - (is (= 400 status)))) + (testing (str "malli with style " style) - (testing "invalid response" - (let [{:keys [status]} (app invalid-request2)] - (is (= 500 status)))))) + (testing "without exception handling" + (let [app (create [rrc/coerce-request-middleware + rrc/coerce-response-middleware] routes)] + + (testing "all good" + (is (= {:status 200 + :body {:total 15}} + (app valid-request1))) + (is (= {:status 200 + :body {:total 115}} + (app valid-request2))) + (is (= {:status 200 + :body {:total 15}} + (app valid-request3))) + (testing "default values work" + (is (= {:status 200 + :body {:total 15}} + (app (update valid-request3 :form-params dissoc :c))))) + (is (= {:status 500 + :body {:evil true}} + (app (assoc-in valid-request1 [:query-params "a"] "666"))))) + + (testing "invalid request" + (is (thrown-with-msg? + ExceptionInfo + #"Request coercion failed" + (app invalid-request1)))) + + (testing "invalid response" + (is (thrown-with-msg? + ExceptionInfo + #"Response coercion failed" + (app invalid-request2)))))) + + (testing "with exception handling" + (let [app (create [rrc/coerce-exceptions-middleware + rrc/coerce-request-middleware + rrc/coerce-response-middleware] routes)] + + (testing "just validation" + (is (= 400 (:status (app {:uri "/api/validate" + :request-method :post + :muuntaja/request {:format "application/edn"} + :body-params 123})))) + (is (= [:reitit.ring.coercion/coerce-exceptions + :reitit.ring.coercion/coerce-request + :reitit.ring.coercion/coerce-response] + (mounted-middleware app "/api/validate" :post)))) + + (testing "no tranformation & validation" + (is (= 123 (:body (app {:uri "/api/no-op" + :request-method :post + :muuntaja/request {:format "application/edn"} + :body-params 123})))) + (is (= [:reitit.ring.coercion/coerce-exceptions + :reitit.ring.coercion/coerce-request + :reitit.ring.coercion/coerce-response] + (mounted-middleware app "/api/no-op" :post)))) + + (testing "skipping coercion" + (is (= nil (:body (app {:uri "/api/skip" + :request-method :post + :muuntaja/request {:format "application/edn"} + :body-params 123})))) + (is (= [:reitit.ring.coercion/coerce-exceptions] + (mounted-middleware app "/api/skip" :post)))) + + (testing "or #407" + (is (= {:status 200 + :body {:msg "you sent x"}} + (app {:uri "/api/or" + :request-method :post + :body-params {:x 1}})))) + + (testing "all good" + (is (= {:status 200 + :body {:total 15}} + (app valid-request1)))) + + (testing "invalid request" + (let [{:keys [status]} (app invalid-request1)] + (is (= 400 status)))) + + (testing "invalid response" + (let [{:keys [status]} (app invalid-request2)] + (is (= 500 status)))))))) (testing "open & closed schemas" (let [endpoint (fn [schema]