mirror of
https://github.com/metosin/reitit.git
synced 2025-12-16 16:01:11 +00:00
Malli + coercion
This commit is contained in:
parent
9ae4083270
commit
10be520a0d
7 changed files with 216 additions and 5 deletions
13
modules/reitit-malli/project.clj
Normal file
13
modules/reitit-malli/project.clj
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
(defproject metosin/reitit-malli "0.3.10"
|
||||
:description "Reitit: Malli coercion"
|
||||
:url "https://github.com/metosin/reitit"
|
||||
:license {:name "Eclipse Public License"
|
||||
:url "http://www.eclipse.org/legal/epl-v10.html"}
|
||||
:scm {:name "git"
|
||||
:url "https://github.com/metosin/reitit"
|
||||
:dir "../.."}
|
||||
:plugins [[lein-parent "0.3.2"]]
|
||||
:parent-project {:path "../../project.clj"
|
||||
:inherit [:deploy-repositories :managed-dependencies]}
|
||||
:dependencies [[metosin/reitit-core]
|
||||
[metosin/malli]])
|
||||
89
modules/reitit-malli/src/reitit/coercion/malli.cljc
Normal file
89
modules/reitit-malli/src/reitit/coercion/malli.cljc
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
(ns reitit.coercion.malli
|
||||
(:require [reitit.coercion :as coercion]
|
||||
[malli.transform :as mt]
|
||||
[malli.core :as m]))
|
||||
|
||||
(defrecord Coercer [decoder encoder validator explainer])
|
||||
|
||||
(def string-transformer
|
||||
mt/string-transformer)
|
||||
|
||||
(def json-transformer
|
||||
mt/json-transformer)
|
||||
|
||||
(def default-transformer
|
||||
(mt/transformer {:name :default}))
|
||||
|
||||
(defmulti coerce-response? identity :default ::default)
|
||||
(defmethod coerce-response? ::default [_] true)
|
||||
|
||||
(def default-options
|
||||
{:coerce-response? coerce-response?
|
||||
:transformers {:body {:default default-transformer
|
||||
:formats {"application/json" json-transformer}}
|
||||
:string {:default string-transformer}
|
||||
:response {:default default-transformer}}})
|
||||
|
||||
(defn create [{:keys [transformers coerce-response?] :as opts}]
|
||||
^{:type ::coercion/coercion}
|
||||
(reify coercion/Coercion
|
||||
(-get-name [_] :malli)
|
||||
(-get-options [_] opts)
|
||||
(-get-apidocs [this specification {:keys [parameters responses]}]
|
||||
;; TODO: this looks identical to spec, refactor when schema is done.
|
||||
#_(case specification
|
||||
:swagger (swagger/swagger-spec
|
||||
(merge
|
||||
(if parameters
|
||||
{:swagger/parameters
|
||||
(into
|
||||
(empty parameters)
|
||||
(for [[k v] parameters]
|
||||
[k (coercion/-compile-model this v nil)]))})
|
||||
(if responses
|
||||
{:swagger/responses
|
||||
(into
|
||||
(empty responses)
|
||||
(for [[k response] responses]
|
||||
[k (as-> response $
|
||||
(set/rename-keys $ {:body :schema})
|
||||
(if (:schema $)
|
||||
(update $ :schema #(coercion/-compile-model this % nil))
|
||||
$))]))})))
|
||||
(throw
|
||||
(ex-info
|
||||
(str "Can't produce Schema apidocs for " specification)
|
||||
{:type specification, :coercion :schema}))))
|
||||
(-compile-model [_ model _] (m/schema model))
|
||||
(-open-model [_ schema] schema)
|
||||
(-encode-error [_ error] error)
|
||||
(-request-coercer [_ type schema]
|
||||
(if schema
|
||||
(let [->coercer (fn [t] (->Coercer (m/decoder schema t)
|
||||
(m/encoder schema t)
|
||||
(m/validator schema)
|
||||
(m/explainer schema)))
|
||||
{:keys [formats default]} (transformers type)
|
||||
default-coercer (->coercer default)
|
||||
format-coercers (->> (for [[f t] formats] [f (->coercer t)]) (into {}))
|
||||
get-coercer (if (seq format-coercers)
|
||||
(fn [format] (or (get format-coercers format) default-coercer))
|
||||
(constantly default-coercer))]
|
||||
(fn [value format]
|
||||
(if-let [coercer (get-coercer format)]
|
||||
(let [decoder (:decoder coercer)
|
||||
validator (:validator coercer)
|
||||
decoded (decoder value)]
|
||||
(if (validator decoded)
|
||||
decoded
|
||||
(let [explainer (:explainer coercer)
|
||||
errors (explainer decoded)]
|
||||
(coercion/map->CoercionError
|
||||
{:schema schema
|
||||
:errors errors}))))
|
||||
value)))))
|
||||
(-response-coercer [this schema]
|
||||
(if (coerce-response? schema)
|
||||
(coercion/-request-coercer this :response schema)))))
|
||||
|
||||
(def coercion (create default-options))
|
||||
29
modules/reitit-malli/src/reitit/ring/schema.cljc
Normal file
29
modules/reitit-malli/src/reitit/ring/schema.cljc
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
(ns reitit.ring.malli)
|
||||
|
||||
(comment
|
||||
|
||||
(defrecord Upload [m]
|
||||
s/Schema
|
||||
(spec [_]
|
||||
(s/spec m))
|
||||
(explain [_]
|
||||
(list 'file m))
|
||||
|
||||
swagger/SwaggerSchema
|
||||
(-transform [_ _]
|
||||
{:type "file"}))
|
||||
|
||||
#?(:clj
|
||||
(def TempFilePart
|
||||
"Schema for file param created by ring.middleware.multipart-params.temp-file store."
|
||||
(->Upload {:filename s/Str
|
||||
:content-type s/Str
|
||||
:size s/Int
|
||||
:tempfile File})))
|
||||
|
||||
#?(:clj
|
||||
(def BytesPart
|
||||
"Schema for file param created by ring.middleware.multipart-params.byte-array store."
|
||||
(->Upload {:filename s/Str
|
||||
:content-type s/Str
|
||||
:bytes s/Any}))))
|
||||
|
|
@ -16,6 +16,7 @@
|
|||
[metosin/reitit-core "0.3.10"]
|
||||
[metosin/reitit-dev "0.3.10"]
|
||||
[metosin/reitit-spec "0.3.10"]
|
||||
[metosin/reitit-malli "0.3.10"]
|
||||
[metosin/reitit-schema "0.3.10"]
|
||||
[metosin/reitit-ring "0.3.10"]
|
||||
[metosin/reitit-middleware "0.3.10"]
|
||||
|
|
@ -32,6 +33,7 @@
|
|||
[metosin/muuntaja "0.6.5"]
|
||||
[metosin/jsonista "0.2.5"]
|
||||
[metosin/sieppari "0.0.0-alpha7"]
|
||||
[metosin/malli "0.0.1-SNAPSHOT"]
|
||||
|
||||
[meta-merge "1.0.0"]
|
||||
[fipp "0.6.21" :exclusions [org.clojure/core.rrb-vector]]
|
||||
|
|
@ -60,6 +62,7 @@
|
|||
"modules/reitit-http/src"
|
||||
"modules/reitit-middleware/src"
|
||||
"modules/reitit-interceptors/src"
|
||||
"modules/reitit-malli/src"
|
||||
"modules/reitit-spec/src"
|
||||
"modules/reitit-schema/src"
|
||||
"modules/reitit-swagger/src"
|
||||
|
|
@ -79,6 +82,7 @@
|
|||
[metosin/muuntaja]
|
||||
[metosin/sieppari]
|
||||
[metosin/jsonista]
|
||||
[metosin/malli]
|
||||
[lambdaisland/deep-diff]
|
||||
[meta-merge]
|
||||
[com.bhauman/spell-spec]
|
||||
|
|
@ -91,9 +95,6 @@
|
|||
[ikitommi/immutant-web "3.0.0-alpha1"]
|
||||
[metosin/ring-http-response "0.9.1"]
|
||||
[metosin/ring-swagger-ui "2.2.10"]
|
||||
[metosin/muuntaja]
|
||||
[metosin/sieppari]
|
||||
[metosin/jsonista]
|
||||
|
||||
[criterium "0.4.5"]
|
||||
[org.clojure/test.check "0.10.0"]
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ for ext in \
|
|||
reitit-core \
|
||||
reitit-dev \
|
||||
reitit-spec \
|
||||
reitit-malli \
|
||||
reitit-schema \
|
||||
reitit-ring \
|
||||
reitit-middleware \
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
[reitit.core :as r]
|
||||
[reitit.coercion :as coercion]
|
||||
[reitit.coercion.spec]
|
||||
[reitit.coercion.malli]
|
||||
[reitit.coercion.schema])
|
||||
#?(:clj
|
||||
(:import (clojure.lang ExceptionInfo))))
|
||||
|
|
@ -15,6 +16,11 @@
|
|||
["/:number/:keyword" {:parameters {:path {:number s/Int
|
||||
:keyword s/Keyword}
|
||||
:query (s/maybe {:int s/Int, :ints [s/Int], :map {s/Int s/Int}})}}]]
|
||||
["/malli" {:coercion reitit.coercion.malli/coercion}
|
||||
["/:number/:keyword" {:parameters {:path [:map [:number int?] [:keyword keyword?]]
|
||||
:query [:maybe [:map [:int int?]
|
||||
[:ints [:vector int?]]
|
||||
[:map [:map-of int? int?]]]]}}]]
|
||||
["/spec" {:coercion reitit.coercion.spec/coercion}
|
||||
["/:number/:keyword" {:parameters {:path {:number int?
|
||||
:keyword keyword?}
|
||||
|
|
@ -31,11 +37,23 @@
|
|||
(coercion/coerce! m))))
|
||||
(let [m (r/match-by-path r "/schema/1/abba")]
|
||||
(is (= {:path {:keyword :abba, :number 1}, :query {:int 10, :ints [1,2,3], :map {1 1}}}
|
||||
(coercion/coerce! (assoc m :query-params {"int" "10", "ints" ["1" "2" "3"], "map" {:1 :1}}))))))
|
||||
(coercion/coerce! (assoc m :query-params {"int" "10", "ints" ["1" "2" "3"], "map" {:1 "1"}}))))))
|
||||
(testing "throws with invalid input"
|
||||
(let [m (r/match-by-path r "/schema/kikka/abba")]
|
||||
(is (thrown? ExceptionInfo (coercion/coerce! m))))))
|
||||
|
||||
(testing "malli-coercion"
|
||||
(testing "succeeds"
|
||||
(let [m (r/match-by-path r "/malli/1/abba")]
|
||||
(is (= {:path {:keyword :abba, :number 1}, :query nil}
|
||||
(coercion/coerce! m))))
|
||||
(let [m (r/match-by-path r "/malli/1/abba")]
|
||||
(is (= {:path {:keyword :abba, :number 1}, :query {:int 10, :ints [1,2,3], :map {1 1}}}
|
||||
(coercion/coerce! (assoc m :query-params {"int" "10", "ints" ["1" "2" "3"], "map" {:1 "1"}}))))))
|
||||
(testing "throws with invalid input"
|
||||
(let [m (r/match-by-path r "/malli/kikka/abba")]
|
||||
(is (thrown? ExceptionInfo (coercion/coerce! m))))))
|
||||
|
||||
(testing "spec-coercion"
|
||||
(testing "succeeds"
|
||||
(let [m (r/match-by-path r "/spec/1/abba")]
|
||||
|
|
@ -43,7 +61,7 @@
|
|||
(coercion/coerce! m))))
|
||||
(let [m (r/match-by-path r "/schema/1/abba")]
|
||||
(is (= {:path {:keyword :abba, :number 1}, :query {:int 10, :ints [1,2,3], :map {1 1}}}
|
||||
(coercion/coerce! (assoc m :query-params {"int" "10", "ints" ["1" "2" "3"], "map" {:1 :1}}))))))
|
||||
(coercion/coerce! (assoc m :query-params {"int" "10", "ints" ["1" "2" "3"], "map" {:1 "1"}}))))))
|
||||
(testing "throws with invalid input"
|
||||
(let [m (r/match-by-path r "/spec/kikka/abba")]
|
||||
(is (thrown? ExceptionInfo (coercion/coerce! m))))))
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
[reitit.ring :as ring]
|
||||
[reitit.ring.coercion :as rrc]
|
||||
[reitit.coercion.spec :as spec]
|
||||
[reitit.coercion.malli :as malli]
|
||||
[reitit.coercion.schema :as schema]
|
||||
#?@(:clj [[muuntaja.middleware]
|
||||
[jsonista.core :as j]]))
|
||||
|
|
@ -163,6 +164,65 @@
|
|||
(let [{:keys [status]} (app invalid-request2)]
|
||||
(is (= 500 status))))))))))
|
||||
|
||||
(deftest malli-coercion-test
|
||||
(let [create (fn [middleware]
|
||||
(ring/ring-handler
|
||||
(ring/router
|
||||
["/api"
|
||||
["/plus/:e"
|
||||
{:get {:parameters {:query [:map [:a int?]]
|
||||
:body [:map [:b int?]]
|
||||
:form [:map [:c int?]]
|
||||
:header [:map [:d int?]]
|
||||
:path [:map [:e int?]]}
|
||||
:responses {200 {:body [:map [:total pos-int?]]}
|
||||
500 {:description "fail"}}
|
||||
:handler handler}}]]
|
||||
{:data {:middleware middleware
|
||||
:coercion malli/coercion}})))]
|
||||
|
||||
(testing "withut exception handling"
|
||||
(let [app (create [rrc/coerce-request-middleware
|
||||
rrc/coerce-response-middleware])]
|
||||
|
||||
(testing "all good"
|
||||
(is (= {:status 200
|
||||
:body {:total 15}}
|
||||
(app valid-request)))
|
||||
(is (= {:status 500
|
||||
:body {:evil true}}
|
||||
(app (assoc-in valid-request [:query-params "a"] "666")))))
|
||||
|
||||
(testing "invalid request"
|
||||
(is (thrown-with-msg?
|
||||
ExceptionInfo
|
||||
#"Request coercion failed"
|
||||
(app invalid-request))))
|
||||
|
||||
(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])]
|
||||
|
||||
(testing "all good"
|
||||
(is (= {:status 200
|
||||
:body {:total 15}}
|
||||
(app valid-request))))
|
||||
|
||||
(testing "invalid request"
|
||||
(let [{:keys [status]} (app invalid-request)]
|
||||
(is (= 400 status))))
|
||||
|
||||
(testing "invalid response"
|
||||
(let [{:keys [status]} (app invalid-request2)]
|
||||
(is (= 500 status))))))))))
|
||||
|
||||
#?(:clj
|
||||
(deftest muuntaja-test
|
||||
(let [app (ring/ring-handler
|
||||
|
|
|
|||
Loading…
Reference in a new issue