mirror of
https://github.com/metosin/reitit.git
synced 2025-12-16 16:01:11 +00:00
add support for malli-lite
This commit is contained in:
parent
88170bc495
commit
d0f7126491
6 changed files with 226 additions and 123 deletions
12
CHANGELOG.md
12
CHANGELOG.md
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
Loading…
Reference in a new issue