add support for malli-lite

This commit is contained in:
Tommi Reiman 2022-02-13 19:29:19 +02:00
parent 88170bc495
commit d0f7126491
6 changed files with 226 additions and 123 deletions

View file

@ -16,6 +16,18 @@ We use [Break Versioning][breakver]. The version numbers follow a `<major>.<mino
**[compare](https://github.com/metosin/reitit/compare/0.5.15...master)**
* Support for [Malli Lite Syntax][Lite Syntax](https://github.com/metosin/malli#lite) in coercion (enabled by default):
```clj
["/add/:id" {:post {:parameters {:path {:id int?}
:query {:a (l/optional int?)}
:body {:id int?
:data {:id (l/maybe int?)
:orders (l/map-of uuid? {:name string?})}}}
:responses {200 {:body {:total pos-int?}}
500 {:description "fail"}}}}]
```
* Improved Reitit-frontend function docstrings
* Updated deps:

View file

@ -2,6 +2,10 @@
[Malli](https://github.com/metosin/malli) is data-driven Schema library for Clojure/Script.
## Default Syntax
By default, [Vector Syntax](https://github.com/metosin/malli#vector-syntax) is used:
```clj
(require '[reitit.coercion.malli])
(require '[reitit.coercion :as coercion])
@ -44,6 +48,20 @@ Failing coercion:
; => 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

View file

@ -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)

View file

@ -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"]

View file

@ -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"

View file

@ -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]