mirror of
https://github.com/metosin/reitit.git
synced 2025-12-17 00:11:11 +00:00
Compare commits
23 commits
a4e1cc5757
...
4ec29540cb
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4ec29540cb | ||
|
|
c3a152a44e | ||
|
|
c0bc789863 | ||
|
|
78aba57d2d | ||
|
|
451b286f1d | ||
|
|
eb06404f1e | ||
|
|
af7313bd9b | ||
|
|
ea58100fec | ||
|
|
a4576cc622 | ||
|
|
9d88d92241 | ||
|
|
d16aac673e | ||
|
|
dede2db213 | ||
|
|
1dc961f661 | ||
|
|
2ce9850de6 | ||
|
|
0bc30e9361 | ||
|
|
9b26d5c0fd | ||
|
|
e671f78741 | ||
|
|
55f8d98bde | ||
|
|
342bae3ffe | ||
|
|
ae52000b29 | ||
|
|
39c5ae86a4 | ||
|
|
7fb9c27e46 | ||
|
|
33d155dc84 |
13 changed files with 368 additions and 191 deletions
|
|
@ -12,6 +12,12 @@ We use [Break Versioning][breakver]. The version numbers follow a `<major>.<mino
|
|||
|
||||
[breakver]: https://github.com/ptaoussanis/encore/blob/master/BREAK-VERSIONING.md
|
||||
|
||||
## UNRELEASED
|
||||
|
||||
* Improve & document how response schemas get picked in per-content-type coercion. See [docs](./doc/ring/coercion.md#per-content-type-coercion). [#745](https://github.com/metosin/reitit/issues/745).
|
||||
* **BREAKING** Remove unused `reitit.dependency` ns. [#763](https://github.com/metosin/reitit/pull/763)
|
||||
* Support passing options to malli humanize. See [docs](./doc/coercion/malli_coercion.md). [#467](https://github.com/metosin/reitit/issues/467)
|
||||
|
||||
## 0.9.2 (2025-10-28)
|
||||
|
||||
* Allow multimethods as handlers when validating [#755](https://github.com/metosin/reitit/pull/755)
|
||||
|
|
|
|||
|
|
@ -96,3 +96,29 @@ Using `create` with options to create the coercion instead of `coercion`:
|
|||
;; malli options
|
||||
:options nil})
|
||||
```
|
||||
|
||||
## Configuring humanize error messages
|
||||
|
||||
Malli humanized error messages can be configured using `:options :errors`:
|
||||
|
||||
```clj
|
||||
(reitit.coercion.malli/create
|
||||
{:options
|
||||
{:errors (assoc malli.error/default-errors
|
||||
:malli.core/missing-key {:error/message {:en "MISSING"}})}})
|
||||
```
|
||||
|
||||
See the malli docs for more info.
|
||||
|
||||
## Custom registry
|
||||
|
||||
Malli registry can be configured conveniently via `:options :registry`:
|
||||
|
||||
```clj
|
||||
(require '[malli.core :as m])
|
||||
|
||||
(reitit.coercion.malli/create
|
||||
{:options
|
||||
{:registry {:registry (merge (m/default-schemas)
|
||||
{:my-type :string})}}})
|
||||
```
|
||||
|
|
|
|||
|
|
@ -202,9 +202,11 @@ is:
|
|||
"application/edn" {:schema {:x s/Int}}
|
||||
:default {:schema {:ww s/Int}}}}}
|
||||
:handler ...}}]]
|
||||
{:data {:middleware [rrc/coerce-exceptions-middleware
|
||||
rrc/coerce-request-middleware
|
||||
rrc/coerce-response-middleware]}})))
|
||||
{:data {:muuntaja muuntaja.core/instance
|
||||
:middleware [reitit.ring.middleware.muuntaja/format-middleware
|
||||
reitit.ring.coercion/coerce-exceptions-middleware
|
||||
reitit.ring.coercion/coerce-request-middleware
|
||||
reitit.ring.coercion/coerce-response-middleware]}})))
|
||||
```
|
||||
|
||||
The resolution logic for response coercers is:
|
||||
|
|
@ -215,6 +217,17 @@ The resolution logic for response coercers is:
|
|||
3. `:body`
|
||||
3. If nothing was found, do not coerce
|
||||
|
||||
To select the response content-type, you can either:
|
||||
1. Let muuntaja pick the content-type based on things like the request Accept header
|
||||
- This is what most users want
|
||||
2. Set `:muuntaja/content-type` in the response to pick an explicit content type
|
||||
3. Set the `"Content-Type"` header in the response
|
||||
- This disables muuntaja, so you need to encode your response body in some other way!
|
||||
- This is not compatible with response schema checking, since coercion won't know what to do with the already-encoded response body.
|
||||
4. Use the `:extract-response-format` option to inject your own logic. See `reitit.coercion/extract-response-format-default` for the default.
|
||||
|
||||
See also the [muuntaja content negotiation](./content_negotiation.md) docs.
|
||||
|
||||
## Pretty printing spec errors
|
||||
|
||||
Spec problems are exposed as is in request & response coercion errors. Pretty-printers like [expound](https://github.com/bhb/expound) can be enabled like this:
|
||||
|
|
|
|||
|
|
@ -71,7 +71,10 @@
|
|||
:parameters {:path [:map
|
||||
[:id :int]]
|
||||
:query [:map
|
||||
[:foo {:optional true} :keyword]]}}]])
|
||||
[:foo {:optional true} :keyword]
|
||||
;; ?repeated=a ==> ["a"]
|
||||
;; ?repeated=a&repeated=b ==> ["a" "b"]
|
||||
[:repeated [:vector {:decode/string #(if (string? %) [%] %)} :string]]]}}]])
|
||||
|
||||
(defn init! []
|
||||
(rfe/start!
|
||||
|
|
|
|||
|
|
@ -52,23 +52,34 @@
|
|||
{:get {:summary "Fetch a pizza | Multiple content-types, multiple examples"
|
||||
:responses {200 {:description "Fetch a pizza as json or EDN"
|
||||
:content {"application/json" {:schema [:map
|
||||
[:format [:enum :json]]
|
||||
[:color :keyword]
|
||||
[:pineapple :boolean]]
|
||||
:examples {:white {:description "White pizza with pineapple"
|
||||
:value {:color :white
|
||||
:value {:format :json
|
||||
:color :white
|
||||
:pineapple true}}
|
||||
:red {:description "Red pizza"
|
||||
:value {:color :red
|
||||
:value {:format :json
|
||||
:color :red
|
||||
:pineapple false}}}}
|
||||
"application/edn" {:schema [:map
|
||||
[:format [:enum :edn]]
|
||||
[:color :keyword]
|
||||
[:pineapple :boolean]]
|
||||
:examples {:red {:description "Red pizza with pineapple"
|
||||
:value (pr-str {:color :red :pineapple true})}}}}}}
|
||||
:value (pr-str {:format :edn :color :red :pineapple true})}}}}}}
|
||||
:handler (fn [_request]
|
||||
{:status 200
|
||||
:body {:color :red
|
||||
:pineapple true}})}
|
||||
(rand-nth [{:status 200
|
||||
:muuntaja/content-type "application/json"
|
||||
:body {:format :json
|
||||
:color :red
|
||||
:pineapple true}}
|
||||
{:status 200
|
||||
:muuntaja/content-type "application/edn"
|
||||
:body {:format :edn
|
||||
:color :red
|
||||
:pineapple true}}]))}
|
||||
:post {:summary "Create a pizza | Multiple content-types, multiple examples | Default response schema"
|
||||
:request {:description "Create a pizza using json or EDN"
|
||||
:content {"application/json" {:schema [:map
|
||||
|
|
|
|||
|
|
@ -152,8 +152,10 @@
|
|||
rcs (request-coercers coercion parameters (cond-> opts route-request (assoc ::skip #{:body})))]
|
||||
(if (and crc rcs) (into crc (vec rcs)) (or crc rcs)))))
|
||||
|
||||
(defn extract-response-format-default [request _]
|
||||
(-> request :muuntaja/response :format))
|
||||
(defn extract-response-format-default [request response]
|
||||
(or (get-in response [:headers "Content-Type"])
|
||||
(:muuntaja/content-type response)
|
||||
(-> request :muuntaja/response :format)))
|
||||
|
||||
(defn -format->coercer [coercion {:keys [content body]} _opts]
|
||||
(->> (concat (when body
|
||||
|
|
|
|||
|
|
@ -1,52 +0,0 @@
|
|||
(ns reitit.dependency
|
||||
"Dependency resolution for middleware/interceptors."
|
||||
(:require [reitit.exception :as exception]))
|
||||
|
||||
(defn- providers
|
||||
"Map from provision key to provider. `get-provides` should return the provision keys of a dependent."
|
||||
[get-provides nodes]
|
||||
(reduce (fn [acc dependent]
|
||||
(into acc
|
||||
(map (fn [provide]
|
||||
(when (contains? acc provide)
|
||||
(exception/fail!
|
||||
(str "multiple providers for: " provide)
|
||||
{::multiple-providers provide}))
|
||||
[provide dependent]))
|
||||
(get-provides dependent)))
|
||||
{} nodes))
|
||||
|
||||
(defn- get-provider
|
||||
"Get the provider for `k`, throw if no provider can be found for it."
|
||||
[providers k]
|
||||
(if (contains? providers k)
|
||||
(get providers k)
|
||||
(exception/fail!
|
||||
(str "provider missing for dependency: " k)
|
||||
{::missing-provider k})))
|
||||
|
||||
(defn post-order
|
||||
"Put `nodes` in post-order. Can also be described as a reverse topological sort.
|
||||
`get-provides` and `get-requires` are callbacks that you can provide to compute the provide and depend
|
||||
key sets of nodes, the defaults are `:provides` and `:requires`."
|
||||
([nodes] (post-order :provides :requires nodes))
|
||||
([get-provides get-requires nodes]
|
||||
(let [providers-by-key (providers get-provides nodes)]
|
||||
(letfn [(toposort [node path colors]
|
||||
(case (get colors node)
|
||||
:white (let [requires (get-requires node)
|
||||
[nodes* colors] (toposort-seq (map (partial get-provider providers-by-key) requires)
|
||||
(conj path node)
|
||||
(assoc colors node :grey))]
|
||||
[(conj nodes* node)
|
||||
(assoc colors node :black)])
|
||||
:grey (exception/fail! "circular dependency" {:cycle (drop-while #(not= % node) (conj path node))})
|
||||
:black [() colors]))
|
||||
|
||||
(toposort-seq [nodes path colors]
|
||||
(reduce (fn [[nodes* colors] node]
|
||||
(let [[nodes** colors] (toposort node path colors)]
|
||||
[(into nodes* nodes**) colors]))
|
||||
[[] colors] nodes))]
|
||||
|
||||
(first (toposort-seq nodes [] (zipmap nodes (repeat :white))))))))
|
||||
|
|
@ -188,7 +188,8 @@
|
|||
(-open-model [_ schema] schema)
|
||||
(-encode-error [_ error]
|
||||
(cond-> error
|
||||
(show? :humanized) (assoc :humanized (me/humanize error {:wrap :message}))
|
||||
(show? :humanized) (assoc :humanized (me/humanize error (cond-> {:wrap :message}
|
||||
options (merge options))))
|
||||
(show? :schema) (update :schema edn/write-string opts)
|
||||
(show? :errors) (-> (me/with-error-messages opts)
|
||||
(update :errors (partial map #(update % :schema edn/write-string opts))))
|
||||
|
|
|
|||
|
|
@ -298,7 +298,8 @@
|
|||
| :index-redirect? | optional boolean: if true (default false), redirect to index file, if false serve it directly
|
||||
| :canonicalize-uris? | optional boolean: if true (default), try to serve index files for non directory paths (paths that end with slash)
|
||||
| :not-found-handler | optional handler function to use if the requested resource is missing (404 Not Found)
|
||||
| :mime-types | optional map of filename extensions to mime-types that will be used to guess the content type in addition to the ones defined in ring.util.mime-type/default-mime-types"
|
||||
| :mime-types | optional map of filename extensions to mime-types that will be used to guess the content type in addition to the ones defined in ring.util.mime-type/default-mime-types
|
||||
| :allow-symlinks? | allow symlinks that lead to paths outside the root classpath directories, defaults to false"
|
||||
([]
|
||||
(create-resource-handler nil))
|
||||
([opts]
|
||||
|
|
@ -318,7 +319,8 @@
|
|||
| :index-redirect? | optional boolean: if true (default false), redirect to index file, if false serve it directly
|
||||
| :canonicalize-uris? | optional boolean: if true (default), try to serve index files for non directory paths (paths that end with slash)
|
||||
| :not-found-handler | optional handler function to use if the requested resource is missing (404 Not Found)
|
||||
| :mime-types | optional map of filename extensions to mime-types that will be used to guess the content type in addition to the ones defined in ring.util.mime-type/default-mime-types"
|
||||
| :mime-types | optional map of filename extensions to mime-types that will be used to guess the content type in addition to the ones defined in ring.util.mime-type/default-mime-types
|
||||
| :allow-symlinks? | allow symlinks that lead to paths outside the root classpath directories, defaults to false"
|
||||
([]
|
||||
(create-file-handler nil))
|
||||
([opts]
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@
|
|||
[metosin/reitit-sieppari "0.9.2"]
|
||||
[metosin/reitit-pedestal "0.9.2"]
|
||||
[metosin/ring-swagger-ui "5.20.0"]
|
||||
[metosin/spec-tools "0.10.7"]
|
||||
[metosin/spec-tools "0.10.8"]
|
||||
[metosin/schema-tools "0.13.1"]
|
||||
[metosin/muuntaja "0.6.11"]
|
||||
[metosin/jsonista "0.3.13"]
|
||||
|
|
@ -95,7 +95,7 @@
|
|||
|
||||
;; modules dependencies
|
||||
[metosin/schema-tools "0.13.1"]
|
||||
[metosin/spec-tools "0.10.7"]
|
||||
[metosin/spec-tools "0.10.8"]
|
||||
[metosin/muuntaja "0.6.11"]
|
||||
[metosin/sieppari "0.0.0-alpha13"]
|
||||
[metosin/jsonista "0.3.13"]
|
||||
|
|
|
|||
|
|
@ -1,33 +0,0 @@
|
|||
(ns reitit.dependency-test
|
||||
(:require [clojure.test :refer [are deftest is testing]]
|
||||
[reitit.dependency :as rc])
|
||||
#?(:clj (:import [clojure.lang ExceptionInfo])))
|
||||
|
||||
(deftest post-order-test
|
||||
(let [base-middlewares [{:name ::bar, :provides #{:bar}, :requires #{:foo}, :wrap identity}
|
||||
{:name ::baz, :provides #{:baz}, :requires #{:bar :foo}, :wrap identity}
|
||||
{:name ::foo, :provides #{:foo}, :requires #{}, :wrap identity}]]
|
||||
(testing "happy cases"
|
||||
(testing "default ordering works"
|
||||
(is (= (rc/post-order base-middlewares)
|
||||
(into (vec (drop 2 base-middlewares)) (take 2 base-middlewares)))))
|
||||
|
||||
(testing "custom provides and requires work"
|
||||
(is (= (rc/post-order (comp hash-set :name)
|
||||
(fn [node] (into #{} (map (fn [k] (keyword "reitit.dependency-test" (name k))))
|
||||
(:requires node)))
|
||||
base-middlewares)
|
||||
(into (vec (drop 2 base-middlewares)) (take 2 base-middlewares))))))
|
||||
|
||||
(testing "errors"
|
||||
(testing "missing dependency detection"
|
||||
(is (thrown-with-msg? ExceptionInfo #"missing"
|
||||
(rc/post-order (drop 1 base-middlewares)))))
|
||||
|
||||
(testing "ambiguous dependency detection"
|
||||
(is (thrown-with-msg? ExceptionInfo #"multiple providers"
|
||||
(rc/post-order (update-in base-middlewares [0 :provides] conj :foo)))))
|
||||
|
||||
(testing "circular dependency detection"
|
||||
(is (thrown-with-msg? ExceptionInfo #"circular"
|
||||
(rc/post-order (assoc-in base-middlewares [2 :requires] #{:baz}))))))))
|
||||
|
|
@ -18,6 +18,7 @@
|
|||
[reitit.swagger-ui :as swagger-ui]
|
||||
[schema.core :as s]
|
||||
[schema-tools.core]
|
||||
[clojure.spec.alpha :as sp]
|
||||
[spec-tools.core :as st]
|
||||
[spec-tools.data-spec :as ds]))
|
||||
|
||||
|
|
@ -1027,3 +1028,36 @@
|
|||
"reitit.openapi-test.Y" {:type "integer"}}}}
|
||||
spec))
|
||||
(is (nil? (validate spec))))))
|
||||
|
||||
(sp/def ::address string?)
|
||||
(sp/def ::zip int?)
|
||||
(sp/def ::city string?)
|
||||
(sp/def ::street string?)
|
||||
(sp/def ::or-and-schema (sp/keys :req-un [(or (and ::address ::zip) (and ::city ::street))]))
|
||||
|
||||
(deftest openapi-spec-tests
|
||||
(testing "s/keys + or maps to :anyOf"
|
||||
(let [app (ring/ring-handler
|
||||
(ring/router
|
||||
[["/openapi.json"
|
||||
{:get {:no-doc true
|
||||
:openapi {:info {:title "" :version "0.0.1"}}
|
||||
:handler (openapi/create-openapi-handler)}}]
|
||||
|
||||
["/spec" {:coercion spec/coercion
|
||||
:post {:summary "or-and-schema"
|
||||
:request {:content {"application/json" {:schema ::or-and-schema}}}
|
||||
:handler identity}}]]
|
||||
{:validate reitit.ring.spec/validate
|
||||
:data {:middleware [openapi/openapi-feature]}}))
|
||||
spec (:body (app {:request-method :get :uri "/openapi.json"}))]
|
||||
(is (nil? (validate spec)))
|
||||
(is (= {:title "reitit.openapi-test/or-and-schema"
|
||||
:type "object"
|
||||
:properties {"address" {:type "string"}
|
||||
"zip" {:type "integer" :format "int64"}
|
||||
"city" {:type "string"}
|
||||
"street" {:type "string"}}
|
||||
:anyOf [{:required ["address" "zip"]}
|
||||
{:required ["city" "street"]}]}
|
||||
(get-in spec [:paths "/spec" :post :requestBody :content "application/json" :schema]))))))
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
(ns reitit.ring-coercion-test
|
||||
(:require [clojure.test :refer [deftest is testing]]
|
||||
[malli.experimental.lite :as l]
|
||||
#?@(:clj [[muuntaja.middleware]
|
||||
[jsonista.core :as j]])
|
||||
#?@(:clj [[muuntaja.core]
|
||||
[muuntaja.middleware]
|
||||
[jsonista.core :as j]
|
||||
[reitit.ring.middleware.muuntaja]])
|
||||
[malli.core :as m]
|
||||
[malli.util :as mu]
|
||||
[meta-merge.core :refer [meta-merge]]
|
||||
|
|
@ -581,103 +583,168 @@
|
|||
:request-method :get}))]
|
||||
|
||||
(is (= {:status 200, :body {:total "FOO: this, BAR: that"}} (call m/schema custom-meta-merge-checking-schema)))
|
||||
(is (= {:status 200, :body {:total "FOO: this, BAR: that"}} (call identity custom-meta-merge-checking-parameters)))))))
|
||||
(is (= {:status 200, :body {:total "FOO: this, BAR: that"}} (call identity custom-meta-merge-checking-parameters)))))
|
||||
|
||||
(testing "malli options"
|
||||
(let [->app (fn [options]
|
||||
(ring/ring-handler
|
||||
(ring/router
|
||||
["/api" {:get {:parameters {:body [:map
|
||||
[:i :int]
|
||||
[:x :string]]}
|
||||
:handler (fn [{{:keys [body]} :parameters}]
|
||||
{:status 200 :body body})}}]
|
||||
{:data {:middleware [rrc/coerce-exceptions-middleware
|
||||
rrc/coerce-request-middleware
|
||||
rrc/coerce-response-middleware]
|
||||
:coercion (malli/create options)}})))
|
||||
request {:uri "/api"
|
||||
:request-method :get
|
||||
:muuntaja/request {:format "application/json"}}]
|
||||
(testing "humanize options"
|
||||
(is (= {:i ["should be an integer"] :x ["missing required key"]}
|
||||
(-> ((->app nil) (assoc request :body-params {:i "x"}))
|
||||
:body
|
||||
:humanized)))
|
||||
(is (= {:i ["SHOULD INT"] :x ["MISSING"]}
|
||||
(-> ((->app {:options {:errors {:int {:error/message {:en "SHOULD INT"}}
|
||||
:malli.core/missing-key {:error/message {:en "MISSING"}}}}})
|
||||
(assoc request :body-params {:i "x"}))
|
||||
:body
|
||||
:humanized))))
|
||||
(testing "overriding registry"
|
||||
(is (= {:body {:i "x" :x "x"} :status 200}
|
||||
(-> ((->app {:options {:registry (merge (m/default-schemas)
|
||||
{:int :string})}})
|
||||
(assoc request :body-params {:i "x" :x "x"}))))))))))
|
||||
|
||||
#?(:clj
|
||||
(deftest per-content-type-test
|
||||
(doseq [[coercion json-request edn-request default-request json-response edn-response default-response]
|
||||
[[malli/coercion
|
||||
[:map [:request [:enum :json]] [:response any?]]
|
||||
[:map [:request [:enum :edn]] [:response any?]]
|
||||
[:map [:request [:enum :default]] [:response any?]]
|
||||
[:map [:request any?] [:response [:enum :json]]]
|
||||
[:map [:request any?] [:response [:enum :edn]]]
|
||||
[:map [:request any?] [:response [:enum :default]]]]
|
||||
[schema/coercion
|
||||
{:request (s/eq :json) :response s/Any}
|
||||
{:request (s/eq :edn) :response s/Any}
|
||||
{:request (s/eq :default) :response s/Any}
|
||||
{:request s/Any :response (s/eq :json)}
|
||||
{:request s/Any :response (s/eq :edn)}
|
||||
{:request s/Any :response (s/eq :default)}]
|
||||
[spec/coercion
|
||||
{:request (clojure.spec.alpha/spec #{:json}) :response any?}
|
||||
{:request (clojure.spec.alpha/spec #{:edn}) :response any?}
|
||||
{:request (clojure.spec.alpha/spec #{:default}) :response any?}
|
||||
{:request any? :response (clojure.spec.alpha/spec #{:json})}
|
||||
{:request any? :response (clojure.spec.alpha/spec #{:end})}
|
||||
{:request any? :response (clojure.spec.alpha/spec #{:default})}]]]
|
||||
(testing (str coercion)
|
||||
(doseq [{:keys [name app]}
|
||||
[{:name "using top-level :body"
|
||||
:app (ring/ring-handler
|
||||
(ring/router
|
||||
["/foo" {:post {:request {:content {"application/json" {:schema json-request}
|
||||
"application/edn" {:schema edn-request}}
|
||||
:body default-request}
|
||||
:responses {200 {:content {"application/json" {:schema json-response}
|
||||
"application/edn" {:schema edn-response}}
|
||||
:body default-response}}
|
||||
:handler (fn [req]
|
||||
{:status 200
|
||||
:body (-> req :parameters :request)})}}]
|
||||
{:validate reitit.ring.spec/validate
|
||||
:data {:middleware [rrc/coerce-request-middleware
|
||||
rrc/coerce-response-middleware]
|
||||
:coercion coercion}}))}
|
||||
{:name "using :default content"
|
||||
:app (ring/ring-handler
|
||||
(ring/router
|
||||
["/foo" {:post {:request {:content {"application/json" {:schema json-request}
|
||||
"application/edn" {:schema edn-request}
|
||||
:default {:schema default-request}}
|
||||
:body json-request} ;; not applied as :default exists
|
||||
:responses {200 {:content {"application/json" {:schema json-response}
|
||||
"application/edn" {:schema edn-response}
|
||||
:default {:schema default-response}}
|
||||
:body json-response}} ;; not applied as :default exists
|
||||
:handler (fn [req]
|
||||
{:status 200
|
||||
:body (-> req :parameters :request)})}}]
|
||||
{:validate reitit.ring.spec/validate
|
||||
:data {:middleware [rrc/coerce-request-middleware
|
||||
rrc/coerce-response-middleware]
|
||||
:coercion coercion}}))}]]
|
||||
(testing name
|
||||
(let [call (fn [request]
|
||||
(let [normalize-json (fn [resp]
|
||||
(update resp :body #(-> % j/write-value-as-string (j/read-value j/keyword-keys-object-mapper))))]
|
||||
(doseq [[coercion json-request edn-request default-request json-response edn-response default-response]
|
||||
[[malli/coercion
|
||||
[:map [:request [:enum :json]] [:response any?]]
|
||||
[:map [:request [:enum :edn]] [:response any?]]
|
||||
[:map [:request [:enum :default]] [:response any?]]
|
||||
[:map [:request any?] [:response [:enum :json]]]
|
||||
[:map [:request any?] [:response [:enum :edn]]]
|
||||
[:map [:request any?] [:response [:enum :default]]]]
|
||||
[schema/coercion
|
||||
{:request (s/eq :json) :response s/Any}
|
||||
{:request (s/eq :edn) :response s/Any}
|
||||
{:request (s/eq :default) :response s/Any}
|
||||
{:request s/Any :response (s/eq :json)}
|
||||
{:request s/Any :response (s/eq :edn)}
|
||||
{:request s/Any :response (s/eq :default)}]
|
||||
[spec/coercion
|
||||
{:request (clojure.spec.alpha/spec #{:json}) :response any?}
|
||||
{:request (clojure.spec.alpha/spec #{:edn}) :response any?}
|
||||
{:request (clojure.spec.alpha/spec #{:default}) :response any?}
|
||||
{:request any? :response (clojure.spec.alpha/spec #{:json})}
|
||||
{:request any? :response (clojure.spec.alpha/spec #{:end})}
|
||||
{:request any? :response (clojure.spec.alpha/spec #{:default})}]]]
|
||||
(testing (str coercion)
|
||||
(doseq [{:keys [name app]}
|
||||
[{:name "using top-level :body"
|
||||
:app (ring/ring-handler
|
||||
(ring/router
|
||||
["/foo" {:post {:request {:content {"application/json" {:schema json-request}
|
||||
"application/edn" {:schema edn-request}}
|
||||
:body default-request}
|
||||
:responses {200 {:content {"application/json" {:schema json-response}
|
||||
"application/edn" {:schema edn-response}}
|
||||
:body default-response}}
|
||||
:handler (fn [req]
|
||||
{:status 200
|
||||
:body (-> req :parameters :request)})}}]
|
||||
{:validate reitit.ring.spec/validate
|
||||
:data {:middleware [rrc/coerce-request-middleware
|
||||
rrc/coerce-response-middleware]
|
||||
:coercion coercion}}))}
|
||||
{:name "using :default content"
|
||||
:app (ring/ring-handler
|
||||
(ring/router
|
||||
["/foo" {:post {:request {:content {"application/json" {:schema json-request}
|
||||
"application/edn" {:schema edn-request}
|
||||
:default {:schema default-request}}
|
||||
:body json-request} ;; not applied as :default exists
|
||||
:responses {200 {:content {"application/json" {:schema json-response}
|
||||
"application/edn" {:schema edn-response}
|
||||
:default {:schema default-response}}
|
||||
:body json-response}} ;; not applied as :default exists
|
||||
:handler (fn [req]
|
||||
{:status 200
|
||||
:body (-> req :parameters :request)})}}]
|
||||
{:validate reitit.ring.spec/validate
|
||||
:data {:middleware [rrc/coerce-request-middleware
|
||||
rrc/coerce-response-middleware]
|
||||
:coercion coercion}}))}]]
|
||||
(testing name
|
||||
(let [call (fn [request]
|
||||
(try
|
||||
(app request)
|
||||
(catch ExceptionInfo e
|
||||
(select-keys (ex-data e) [:type :in]))))
|
||||
request (fn [request-format response-format body]
|
||||
{:request-method :post
|
||||
:uri "/foo"
|
||||
:muuntaja/request {:format request-format}
|
||||
:muuntaja/response {:format response-format}
|
||||
:body-params body})]
|
||||
(testing "succesful call"
|
||||
(is (= {:status 200 :body {:request "json", :response "json"}}
|
||||
(normalize-json (call (request "application/json" "application/json" {:request :json :response :json})))))
|
||||
(is (= {:status 200 :body {:request "edn", :response "json"}}
|
||||
(normalize-json (call (request "application/edn" "application/json" {:request :edn :response :json})))))
|
||||
(is (= {:status 200 :body {:request :default, :response :default}}
|
||||
(call (request "application/transit" "application/transit" {:request :default :response :default})))))
|
||||
(testing "request validation fails"
|
||||
(is (= {:type :reitit.coercion/request-coercion :in [:request :body-params]}
|
||||
(call (request "application/edn" "application/json" {:request :json :response :json}))))
|
||||
(is (= {:type :reitit.coercion/request-coercion :in [:request :body-params]}
|
||||
(call (request "application/json" "application/json" {:request :edn :response :json}))))
|
||||
(is (= {:type :reitit.coercion/request-coercion :in [:request :body-params]}
|
||||
(call (request "application/transit" "application/json" {:request :edn :response :json})))))
|
||||
(testing "response validation fails"
|
||||
(is (= {:type :reitit.coercion/response-coercion :in [:response :body]}
|
||||
(call (request "application/json" "application/json" {:request :json :response :edn}))))
|
||||
(is (= {:type :reitit.coercion/response-coercion :in [:response :body]}
|
||||
(call (request "application/json" "application/edn" {:request :json :response :json}))))
|
||||
(is (= {:type :reitit.coercion/response-coercion :in [:response :body]}
|
||||
(call (request "application/json" "application/transit" {:request :json :response :json}))))))))
|
||||
(testing "explicit response content type"
|
||||
(let [response (atom nil)
|
||||
app (ring/ring-handler
|
||||
(ring/router
|
||||
["/foo" {:post {:responses {200 {:content {"application/json" {:schema json-response}
|
||||
"application/edn" {:schema edn-response}
|
||||
:default {:schema default-response}}}}
|
||||
:handler (fn [req]
|
||||
@response)}}]
|
||||
{:validate reitit.ring.spec/validate
|
||||
:data {:middleware [rrc/coerce-request-middleware
|
||||
rrc/coerce-response-middleware]
|
||||
:coercion coercion}}))
|
||||
call (fn [request]
|
||||
(try
|
||||
(app request)
|
||||
(catch ExceptionInfo e
|
||||
#_(ex-data e)
|
||||
(select-keys (ex-data e) [:type :in]))))
|
||||
request (fn [request-format response-format body]
|
||||
request (fn [request-format body resp]
|
||||
(reset! response resp)
|
||||
{:request-method :post
|
||||
:uri "/foo"
|
||||
:muuntaja/request {:format request-format}
|
||||
:muuntaja/response {:format response-format}
|
||||
:body-params body})
|
||||
normalize-json (fn[body]
|
||||
(-> body j/write-value-as-string (j/read-value j/keyword-keys-object-mapper)))]
|
||||
(testing "succesful call"
|
||||
(is (= {:status 200 :body {:request "json", :response "json"}}
|
||||
(normalize-json (call (request "application/json" "application/json" {:request :json :response :json})))))
|
||||
(is (= {:status 200 :body {:request "edn", :response "json"}}
|
||||
(normalize-json (call (request "application/edn" "application/json" {:request :edn :response :json})))))
|
||||
(is (= {:status 200 :body {:request :default, :response :default}}
|
||||
(call (request "application/transit" "application/transit" {:request :default :response :default})))))
|
||||
(testing "request validation fails"
|
||||
(is (= {:type :reitit.coercion/request-coercion :in [:request :body-params]}
|
||||
(call (request "application/edn" "application/json" {:request :json :response :json}))))
|
||||
(is (= {:type :reitit.coercion/request-coercion :in [:request :body-params]}
|
||||
(call (request "application/json" "application/json" {:request :edn :response :json}))))
|
||||
(is (= {:type :reitit.coercion/request-coercion :in [:request :body-params]}
|
||||
(call (request "application/transit" "application/json" {:request :edn :response :json})))))
|
||||
(testing "response validation fails"
|
||||
(is (= {:type :reitit.coercion/response-coercion :in [:response :body]}
|
||||
(call (request "application/json" "application/json" {:request :json :response :edn}))))
|
||||
(is (= {:type :reitit.coercion/response-coercion :in [:response :body]}
|
||||
(call (request "application/json" "application/edn" {:request :json :response :json}))))
|
||||
(is (= {:type :reitit.coercion/response-coercion :in [:response :body]}
|
||||
(call (request "application/json" "application/transit" {:request :json :response :json}))))))))))))
|
||||
:body-params body})]
|
||||
(testing "via :muuntaja/content-type"
|
||||
(is (= {:status 200 :body {:request "json" :response "json"} :muuntaja/content-type "application/json"}
|
||||
(normalize-json (call (request "application/json" {:request :json :response :json} {:status 200 :body {:request :json :response :json} :muuntaja/content-type "application/json"}))))
|
||||
"valid reponse")
|
||||
(is (= {:in [:response :body] :type :reitit.coercion/response-coercion}
|
||||
(call (request "application/json" {:request :json :response :json} {:status 200 :body {:request :json :response :invalid} :muuntaja/content-type "application/json"})))
|
||||
"invalid reponse")))))))))
|
||||
|
||||
|
||||
#?(:clj
|
||||
|
|
@ -801,3 +868,100 @@
|
|||
(app) :body slurp (read-string))]
|
||||
(is (= data-edn (e2e (assoc data-edn :EXTRA "VALUE"))))
|
||||
(is (thrown? ExceptionInfo (e2e data-json))))))))
|
||||
|
||||
#?(:clj
|
||||
(deftest muuntaja-per-content-type-coercion-test
|
||||
;; Test integration between per-content-type coercion and muuntaja.
|
||||
;; Malli-only for now.
|
||||
(let [response (atom nil)
|
||||
app (ring/ring-handler
|
||||
(ring/router
|
||||
["/foo" {:post {:request {:content {"application/json" {:schema [:map [:request [:enum :json]]]}
|
||||
"application/edn" {:schema [:map [:request [:enum :edn]]]}
|
||||
:default {:schema [:map [:request [:enum :default]]]}}}
|
||||
:responses {200 {:content {"application/json" {:schema [:map [:response [:enum :json]]]}
|
||||
"application/edn" {:schema [:map [:response [:enum :edn]]]}
|
||||
:default {}}}}
|
||||
:handler (fn [req] @response)}}]
|
||||
{:data {:middleware [reitit.ring.middleware.muuntaja/format-middleware
|
||||
rrc/coerce-request-middleware
|
||||
rrc/coerce-response-middleware]
|
||||
:muuntaja muuntaja.core/instance
|
||||
:coercion malli/coercion}}))
|
||||
maybe-slurp #(if (instance? java.io.InputStream %)
|
||||
(slurp %)
|
||||
%)
|
||||
call (fn [request resp]
|
||||
(reset! response resp)
|
||||
(try
|
||||
(-> (merge {:request-method :post :uri "/foo"} request)
|
||||
(update :body #(ByteArrayInputStream. (.getBytes % "UTF-8")))
|
||||
(app))
|
||||
(catch ExceptionInfo e
|
||||
#_(ex-data e)
|
||||
(select-keys (ex-data e) [:in :type]))))
|
||||
read-json #(j/read-value % (j/object-mapper {:decode-key-fn true}))
|
||||
json-response? (fn [resp]
|
||||
(and (.startsWith (get-in resp [:headers "Content-Type"]) "application/json") ;; ignore the ;charset=utf-8 part
|
||||
(= {:response "json"} (read-json (maybe-slurp (:body resp))))))
|
||||
edn-response? (fn [resp]
|
||||
(and (.startsWith (get-in resp [:headers "Content-Type"]) "application/edn") ;; ignore the ;charset=utf-8 part
|
||||
(= {:response :edn} (read-string (maybe-slurp (:body resp))))))
|
||||
custom-response? (fn [resp]
|
||||
(and (= (get-in resp [:headers "Content-Type"]) "application/custom")
|
||||
(= "custom data" (maybe-slurp (:body resp)))))]
|
||||
(testing "response content-type defaults to json"
|
||||
(is (json-response?
|
||||
(call {:headers {"content-type" "application/json"}
|
||||
:body (j/write-value-as-string {:request :json})}
|
||||
{:status 200
|
||||
:body {:response :json}})))
|
||||
(is (json-response?
|
||||
(call {:headers {"content-type" "application/edn"}
|
||||
:body (pr-str {:request :edn})}
|
||||
{:status 200
|
||||
:body {:response :json}})))
|
||||
(is (= {:in [:response :body] :type :reitit.coercion/response-coercion}
|
||||
(call {:headers {"content-type" "application/json"}
|
||||
:body (j/write-value-as-string {:request :json})}
|
||||
{:status 200
|
||||
:body {:response :invalid}}))
|
||||
"invalid response"))
|
||||
(testing "response content-type negotiated via accept header"
|
||||
(is (json-response?
|
||||
(call {:headers {"content-type" "application/json" "accept" "application/json"}
|
||||
:body (j/write-value-as-string {:request :json})}
|
||||
{:status 200
|
||||
:body {:response :json}})))
|
||||
(is (edn-response?
|
||||
(call {:headers {"content-type" "application/json" "accept" "application/edn"}
|
||||
:body (j/write-value-as-string {:request :json})}
|
||||
{:status 200
|
||||
:body {:response :edn}})))
|
||||
(is (= {:in [:response :body] :type :reitit.coercion/response-coercion}
|
||||
(call {:headers {"content-type" "application/json" "accept" "application/edn"}
|
||||
:body (j/write-value-as-string {:request :json})}
|
||||
{:status 200
|
||||
:body {:response :invalid}}))
|
||||
"invalid response"))
|
||||
(testing "response content-type set via :muuntaja/content-type"
|
||||
(is (edn-response?
|
||||
(call {:headers {"content-type" "application/json" "accept" "application/json"}
|
||||
:body (j/write-value-as-string {:request :json})}
|
||||
{:status 200
|
||||
:muuntaja/content-type "application/edn"
|
||||
:body {:response :edn}})))
|
||||
(is (= {:in [:response :body] :type :reitit.coercion/response-coercion}
|
||||
(call {:headers {"content-type" "application/json" "accept" "application/json"}
|
||||
:body (j/write-value-as-string {:request :json})}
|
||||
{:status 200
|
||||
:muuntaja/content-type "application/edn"
|
||||
:body {:response :invalid}}))
|
||||
"invalid response"))
|
||||
(testing "response content-type set via Content-Type header. muuntaja disabled for response."
|
||||
(is (custom-response?
|
||||
(call {:headers {"content-type" "application/json" "accept" "application/json"}
|
||||
:body (j/write-value-as-string {:request :json})}
|
||||
{:status 200
|
||||
:headers {"Content-Type" "application/custom"}
|
||||
:body "custom data"})))))))
|
||||
|
|
|
|||
Loading…
Reference in a new issue