mirror of
https://github.com/metosin/reitit.git
synced 2026-02-14 07:15:16 +00:00
Compare commits
12 commits
1d10109e95
...
2d7dd52084
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2d7dd52084 | ||
|
|
1dc961f661 | ||
|
|
2ce9850de6 | ||
|
|
0bc30e9361 | ||
|
|
9b26d5c0fd | ||
|
|
e671f78741 | ||
|
|
55f8d98bde | ||
|
|
342bae3ffe | ||
|
|
ae52000b29 | ||
|
|
39c5ae86a4 | ||
|
|
7fb9c27e46 | ||
|
|
7a707f042b |
10 changed files with 664 additions and 186 deletions
|
|
@ -12,6 +12,11 @@ We use [Break Versioning][breakver]. The version numbers follow a `<major>.<mino
|
||||||
|
|
||||||
[breakver]: https://github.com/ptaoussanis/encore/blob/master/BREAK-VERSIONING.md
|
[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)
|
||||||
|
|
||||||
## 0.9.2 (2025-10-28)
|
## 0.9.2 (2025-10-28)
|
||||||
|
|
||||||
* Allow multimethods as handlers when validating [#755](https://github.com/metosin/reitit/pull/755)
|
* Allow multimethods as handlers when validating [#755](https://github.com/metosin/reitit/pull/755)
|
||||||
|
|
|
||||||
|
|
@ -202,9 +202,11 @@ is:
|
||||||
"application/edn" {:schema {:x s/Int}}
|
"application/edn" {:schema {:x s/Int}}
|
||||||
:default {:schema {:ww s/Int}}}}}
|
:default {:schema {:ww s/Int}}}}}
|
||||||
:handler ...}}]]
|
:handler ...}}]]
|
||||||
{:data {:middleware [rrc/coerce-exceptions-middleware
|
{:data {:muuntaja muuntaja.core/instance
|
||||||
rrc/coerce-request-middleware
|
:middleware [reitit.ring.middleware.muuntaja/format-middleware
|
||||||
rrc/coerce-response-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:
|
The resolution logic for response coercers is:
|
||||||
|
|
@ -215,6 +217,17 @@ The resolution logic for response coercers is:
|
||||||
3. `:body`
|
3. `:body`
|
||||||
3. If nothing was found, do not coerce
|
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
|
## 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:
|
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:
|
||||||
|
|
|
||||||
|
|
@ -52,23 +52,34 @@
|
||||||
{:get {:summary "Fetch a pizza | Multiple content-types, multiple examples"
|
{:get {:summary "Fetch a pizza | Multiple content-types, multiple examples"
|
||||||
:responses {200 {:description "Fetch a pizza as json or EDN"
|
:responses {200 {:description "Fetch a pizza as json or EDN"
|
||||||
:content {"application/json" {:schema [:map
|
:content {"application/json" {:schema [:map
|
||||||
|
[:format [:enum :json]]
|
||||||
[:color :keyword]
|
[:color :keyword]
|
||||||
[:pineapple :boolean]]
|
[:pineapple :boolean]]
|
||||||
:examples {:white {:description "White pizza with pineapple"
|
:examples {:white {:description "White pizza with pineapple"
|
||||||
:value {:color :white
|
:value {:format :json
|
||||||
|
:color :white
|
||||||
:pineapple true}}
|
:pineapple true}}
|
||||||
:red {:description "Red pizza"
|
:red {:description "Red pizza"
|
||||||
:value {:color :red
|
:value {:format :json
|
||||||
|
:color :red
|
||||||
:pineapple false}}}}
|
:pineapple false}}}}
|
||||||
"application/edn" {:schema [:map
|
"application/edn" {:schema [:map
|
||||||
|
[:format [:enum :edn]]
|
||||||
[:color :keyword]
|
[:color :keyword]
|
||||||
[:pineapple :boolean]]
|
[:pineapple :boolean]]
|
||||||
:examples {:red {:description "Red pizza with pineapple"
|
: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]
|
:handler (fn [_request]
|
||||||
{:status 200
|
(rand-nth [{:status 200
|
||||||
:body {:color :red
|
:muuntaja/content-type "application/json"
|
||||||
:pineapple true}})}
|
: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"
|
:post {:summary "Create a pizza | Multiple content-types, multiple examples | Default response schema"
|
||||||
:request {:description "Create a pizza using json or EDN"
|
:request {:description "Create a pizza using json or EDN"
|
||||||
:content {"application/json" {:schema [:map
|
:content {"application/json" {:schema [:map
|
||||||
|
|
|
||||||
|
|
@ -152,8 +152,10 @@
|
||||||
rcs (request-coercers coercion parameters (cond-> opts route-request (assoc ::skip #{:body})))]
|
rcs (request-coercers coercion parameters (cond-> opts route-request (assoc ::skip #{:body})))]
|
||||||
(if (and crc rcs) (into crc (vec rcs)) (or crc rcs)))))
|
(if (and crc rcs) (into crc (vec rcs)) (or crc rcs)))))
|
||||||
|
|
||||||
(defn extract-response-format-default [request _]
|
(defn extract-response-format-default [request response]
|
||||||
(-> request :muuntaja/response :format))
|
(or (get-in response [:headers "Content-Type"])
|
||||||
|
(:muuntaja/content-type response)
|
||||||
|
(-> request :muuntaja/response :format)))
|
||||||
|
|
||||||
(defn -format->coercer [coercion {:keys [content body]} _opts]
|
(defn -format->coercer [coercion {:keys [content body]} _opts]
|
||||||
(->> (concat (when body
|
(->> (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))))))))
|
|
||||||
121
modules/reitit-core/src/reitit/regex.cljc
Normal file
121
modules/reitit-core/src/reitit/regex.cljc
Normal file
|
|
@ -0,0 +1,121 @@
|
||||||
|
(ns reitit.regex
|
||||||
|
(:require [clojure.set :as set]
|
||||||
|
[clojure.string :as str]
|
||||||
|
[reitit.core :as r]))
|
||||||
|
|
||||||
|
(defn compile-regex-route
|
||||||
|
"Given a route vector [path route-data], returns a map with:
|
||||||
|
- :pattern: a compiled regex pattern built from the path segments,
|
||||||
|
- :group-keys: vector of parameter keys in order,
|
||||||
|
- :route-data: the provided route data,
|
||||||
|
- :original-segments: original path segments for path generation,
|
||||||
|
- :template: the original path template for Match objects."
|
||||||
|
[[path route-data]]
|
||||||
|
(let [;; Normalize route-data to ensure it's a map with :name
|
||||||
|
route-data (if (keyword? route-data)
|
||||||
|
{:name route-data}
|
||||||
|
route-data)
|
||||||
|
|
||||||
|
;; Store the original path template for Match objects
|
||||||
|
template (if (str/starts-with? path "/")
|
||||||
|
path
|
||||||
|
(str "/" path))
|
||||||
|
|
||||||
|
;; Handle paths with or without leading slashes
|
||||||
|
normalized-path (cond-> path
|
||||||
|
(str/starts-with? path "/") (subs 1))
|
||||||
|
|
||||||
|
;; Split into segments, handling empty paths
|
||||||
|
segments (if (empty? normalized-path)
|
||||||
|
[]
|
||||||
|
(str/split normalized-path #"/"))
|
||||||
|
|
||||||
|
;; Store original segments for path generation
|
||||||
|
original-segments segments
|
||||||
|
|
||||||
|
compiled-segments
|
||||||
|
(map (fn [seg]
|
||||||
|
(if (str/starts-with? seg ":")
|
||||||
|
(let [param-key (keyword (subs seg 1))
|
||||||
|
param-regex (get-in route-data [:parameters :path param-key])]
|
||||||
|
(if (and param-regex (instance? java.util.regex.Pattern param-regex))
|
||||||
|
(str "(" (.pattern ^java.util.regex.Pattern param-regex) ")")
|
||||||
|
;; Fallback: match any non-slash characters.
|
||||||
|
"([^/]+)"))
|
||||||
|
(java.util.regex.Pattern/quote seg)))
|
||||||
|
segments)
|
||||||
|
|
||||||
|
;; Create the pattern string, handling special case for root path
|
||||||
|
pattern-str (if (empty? segments)
|
||||||
|
"^/?$" ;; Match root path with optional trailing slash
|
||||||
|
(str "^/" (str/join "/" compiled-segments) "$"))
|
||||||
|
|
||||||
|
group-keys (->> segments
|
||||||
|
(filter #(str/starts-with? % ":"))
|
||||||
|
(map #(keyword (subs % 1)))
|
||||||
|
(vec))]
|
||||||
|
|
||||||
|
{:pattern (re-pattern pattern-str)
|
||||||
|
:group-keys group-keys
|
||||||
|
:route-data route-data
|
||||||
|
:original-segments original-segments
|
||||||
|
:template template}))
|
||||||
|
|
||||||
|
(defn- generate-path
|
||||||
|
"Generate a path from a route and path parameters."
|
||||||
|
[route path-params]
|
||||||
|
(if (empty? (:original-segments route))
|
||||||
|
"/"
|
||||||
|
(str "/" (str/join "/"
|
||||||
|
(map (fn [segment]
|
||||||
|
(if (str/starts-with? segment ":")
|
||||||
|
(let [param-key (keyword (subs segment 1))]
|
||||||
|
(get path-params param-key ""))
|
||||||
|
segment))
|
||||||
|
(:original-segments route))))))
|
||||||
|
|
||||||
|
(defrecord RegexRouter [compiled-routes]
|
||||||
|
r/Router
|
||||||
|
(router-name [_] :regex-router)
|
||||||
|
|
||||||
|
(routes [_]
|
||||||
|
(mapv (fn [{:keys [route-data original-segments]}]
|
||||||
|
[(str "/" (str/join "/" original-segments)) route-data])
|
||||||
|
compiled-routes))
|
||||||
|
|
||||||
|
(compiled-routes [_] compiled-routes)
|
||||||
|
|
||||||
|
(options [_] {})
|
||||||
|
|
||||||
|
(route-names [_]
|
||||||
|
(keep (comp :name :route-data) compiled-routes))
|
||||||
|
|
||||||
|
(match-by-path [_ path]
|
||||||
|
(some (fn [{:keys [pattern group-keys route-data template]}]
|
||||||
|
(when-let [matches (re-matches pattern path)]
|
||||||
|
(let [params (zipmap group-keys (rest matches))]
|
||||||
|
(r/->Match template route-data nil params path))))
|
||||||
|
compiled-routes))
|
||||||
|
|
||||||
|
(match-by-name [this name]
|
||||||
|
(r/match-by-name this name {}))
|
||||||
|
|
||||||
|
(match-by-name [router name path-params]
|
||||||
|
(when-let [{:keys [group-keys route-data template] :as route}
|
||||||
|
(first (filter #(= name (get-in % [:route-data :name])) (r/compiled-routes router)))]
|
||||||
|
;; Check if all required params are provided
|
||||||
|
(let [required-params (set group-keys)
|
||||||
|
provided-params (set (keys path-params))]
|
||||||
|
(if (every? #(contains? provided-params %) required-params)
|
||||||
|
;; All required params provided, return a Match
|
||||||
|
(let [path (generate-path route path-params)]
|
||||||
|
(r/->Match template route-data nil path-params path))
|
||||||
|
;; Some required params missing, return a PartialMatch
|
||||||
|
(let [missing (set/difference required-params provided-params)]
|
||||||
|
(r/->PartialMatch template route-data nil path-params missing)))))))
|
||||||
|
|
||||||
|
(defn create-regex-router
|
||||||
|
"Create a RegexRouter from a vector of routes.
|
||||||
|
Each route should be a vector [path route-data]."
|
||||||
|
[routes]
|
||||||
|
(->RegexRouter (mapv compile-regex-route routes)))
|
||||||
|
|
@ -298,7 +298,8 @@
|
||||||
| :index-redirect? | optional boolean: if true (default false), redirect to index file, if false serve it directly
|
| :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)
|
| :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)
|
| :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))
|
(create-resource-handler nil))
|
||||||
([opts]
|
([opts]
|
||||||
|
|
@ -318,7 +319,8 @@
|
||||||
| :index-redirect? | optional boolean: if true (default false), redirect to index file, if false serve it directly
|
| :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)
|
| :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)
|
| :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))
|
(create-file-handler nil))
|
||||||
([opts]
|
([opts]
|
||||||
|
|
|
||||||
|
|
@ -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}))))))))
|
|
||||||
278
test/cljc/reitit/regex_test.cljc
Normal file
278
test/cljc/reitit/regex_test.cljc
Normal file
|
|
@ -0,0 +1,278 @@
|
||||||
|
(ns reitit.regex-test
|
||||||
|
(:require [clojure.test :refer [deftest is testing]]
|
||||||
|
[reitit.core :as r]
|
||||||
|
[reitit.regex :as rt.regex]))
|
||||||
|
|
||||||
|
(defn re-=
|
||||||
|
"A custom equality function that handles regex patterns specially.
|
||||||
|
Returns true if a and b are equal, with special handling for regex patterns.
|
||||||
|
Also handles comparing records by their map representation."
|
||||||
|
[a b]
|
||||||
|
(cond
|
||||||
|
;; Handle record comparison by using their map representation
|
||||||
|
(and (instance? clojure.lang.IRecord a)
|
||||||
|
(instance? clojure.lang.IRecord b))
|
||||||
|
(re-= (into {} a) (into {} b))
|
||||||
|
|
||||||
|
;; If both are regex patterns, compare their string representations
|
||||||
|
(and (instance? java.util.regex.Pattern a)
|
||||||
|
(instance? java.util.regex.Pattern b))
|
||||||
|
(= (str a) (str b))
|
||||||
|
|
||||||
|
;; If one is a regex and the other isn't, they're not equal
|
||||||
|
(or (instance? java.util.regex.Pattern a)
|
||||||
|
(instance? java.util.regex.Pattern b))
|
||||||
|
false
|
||||||
|
|
||||||
|
;; For maps, compare each key-value pair using regex-aware-equals
|
||||||
|
(and (map? a) (map? b))
|
||||||
|
(and (= (set (keys a)) (set (keys b)))
|
||||||
|
(every? #(re-= (get a %) (get b %)) (keys a)))
|
||||||
|
|
||||||
|
;; For sequences, compare each element using regex-aware-equals
|
||||||
|
(and (sequential? a) (sequential? b))
|
||||||
|
(and (= (count a) (count b))
|
||||||
|
(every? identity (map re-= a b)))
|
||||||
|
|
||||||
|
;; For sets, convert to sequences and compare
|
||||||
|
(and (set? a) (set? b))
|
||||||
|
(re-= (seq a) (seq b))
|
||||||
|
|
||||||
|
;; For everything else, use regular equality
|
||||||
|
:else
|
||||||
|
(= a b)))
|
||||||
|
|
||||||
|
(def routes
|
||||||
|
(rt.regex/create-regex-router
|
||||||
|
[["" ::home]
|
||||||
|
[":item-id" {:name ::item
|
||||||
|
:parameters {:path {:item-id #"[a-z]{16,20}"}}}]
|
||||||
|
["inbox" ::inbox]
|
||||||
|
["teams" ::teams]
|
||||||
|
["teams/:team-id-b58/members" {:name ::->members
|
||||||
|
:parameters {:path {:team-id-b58 #"[a-z]"}}}]]))
|
||||||
|
|
||||||
|
(deftest regex-match-by-path-test
|
||||||
|
(testing "Basic path matching"
|
||||||
|
(is (= (r/map->Match {:path "/"
|
||||||
|
:path-params {}
|
||||||
|
:data {:name ::home}
|
||||||
|
:template "/"
|
||||||
|
:result nil})
|
||||||
|
(r/match-by-path routes "/")))
|
||||||
|
|
||||||
|
(is (= (r/map->Match {:path "/inbox"
|
||||||
|
:path-params {}
|
||||||
|
:data {:name ::inbox}
|
||||||
|
:template "/inbox"
|
||||||
|
:result nil})
|
||||||
|
(r/match-by-path routes "/inbox")))
|
||||||
|
|
||||||
|
(is (= (r/map->Match {:path "/teams"
|
||||||
|
:path-params {}
|
||||||
|
:data {:name ::teams}
|
||||||
|
:template "/teams"
|
||||||
|
:result nil})
|
||||||
|
(r/match-by-path routes "/teams"))))
|
||||||
|
|
||||||
|
(testing "Path with regex parameter"
|
||||||
|
(let [valid-id "abcdefghijklmnopq"] ; 17 lowercase letters
|
||||||
|
(is (re-= (r/map->Match {:path (str "/" valid-id)
|
||||||
|
:path-params {:item-id valid-id}
|
||||||
|
:data {:name ::item
|
||||||
|
:parameters {:path {:item-id #"[a-z]{16,20}"}}},
|
||||||
|
:template "/:item-id"
|
||||||
|
:result nil})
|
||||||
|
(r/match-by-path routes (str "/" valid-id)))))
|
||||||
|
|
||||||
|
;; Invalid parameter cases
|
||||||
|
(is (nil? (r/match-by-path routes "/abcdefg")) "Too short")
|
||||||
|
(is (nil? (r/match-by-path routes "/abcdefghijklmnopqRST")) "Contains uppercase")
|
||||||
|
(is (nil? (r/match-by-path routes "/abcdefghijklmn1234")) "Contains digits"))
|
||||||
|
|
||||||
|
(testing "Nested path with parameter"
|
||||||
|
(is (re-= (r/map->Match {:path "/teams/a/members"
|
||||||
|
:path-params {:team-id-b58 "a"}
|
||||||
|
:data {:name ::->members
|
||||||
|
:parameters {:path {:team-id-b58 #"[a-z]"}}}
|
||||||
|
:template "/teams/:team-id-b58/members"
|
||||||
|
:result nil})
|
||||||
|
(r/match-by-path routes "/teams/a/members")))
|
||||||
|
|
||||||
|
(is (nil? (r/match-by-path routes "/teams/abc/members")) "Multiple characters")
|
||||||
|
(is (nil? (r/match-by-path routes "/teams/1/members")) "Digit instead of letter"))
|
||||||
|
|
||||||
|
(testing "Non-matching paths"
|
||||||
|
(is (nil? (r/match-by-path routes "/unknown")))
|
||||||
|
(is (nil? (r/match-by-path routes "/team"))) ; 'team' not 'teams'
|
||||||
|
(is (nil? (r/match-by-path routes "/teams/extra/segments/here")))))
|
||||||
|
|
||||||
|
(deftest regex-match-by-name-test
|
||||||
|
(testing "Basic match-by-name functionality"
|
||||||
|
;; Root path
|
||||||
|
(is (re-= (r/map->Match {:path "/"
|
||||||
|
:path-params {}
|
||||||
|
:data {:name ::home}
|
||||||
|
:template "/"
|
||||||
|
:result nil})
|
||||||
|
(r/match-by-name routes ::home)))
|
||||||
|
|
||||||
|
;; Static paths
|
||||||
|
(is (re-= (r/map->Match {:path "/inbox"
|
||||||
|
:path-params {}
|
||||||
|
:data {:name ::inbox}
|
||||||
|
:template "/inbox"
|
||||||
|
:result nil})
|
||||||
|
(r/match-by-name routes ::inbox)))
|
||||||
|
|
||||||
|
(is (re-= (r/map->Match {:path "/teams"
|
||||||
|
:path-params {}
|
||||||
|
:data {:name ::teams}
|
||||||
|
:template "/teams"
|
||||||
|
:result nil})
|
||||||
|
(r/match-by-name routes ::teams)))
|
||||||
|
|
||||||
|
;; Path with parameter
|
||||||
|
(let [valid-id "abcdefghijklmnopq"]
|
||||||
|
(is (re-= (r/map->Match {:path (str "/" valid-id)
|
||||||
|
:path-params {:item-id valid-id}
|
||||||
|
:data {:name ::item
|
||||||
|
:parameters {:path {:item-id #"[a-z]{16,20}"}}}
|
||||||
|
:template "/:item-id"
|
||||||
|
:result nil})
|
||||||
|
(r/match-by-name routes ::item {:item-id valid-id}))))
|
||||||
|
|
||||||
|
;; Nested path with parameter
|
||||||
|
(is (re-= (r/map->Match {:path "/teams/a/members"
|
||||||
|
:path-params {:team-id-b58 "a"}
|
||||||
|
:data {:name ::->members
|
||||||
|
:parameters {:path {:team-id-b58 #"[a-z]"}}}
|
||||||
|
:template "/teams/:team-id-b58/members"
|
||||||
|
:result nil})
|
||||||
|
(r/match-by-name routes ::->members {:team-id-b58 "a"}))))
|
||||||
|
|
||||||
|
(testing "Path round-trip matching"
|
||||||
|
;; Test that paths generated by match-by-name can be successfully matched by match-by-path
|
||||||
|
(let [valid-id "abcdefghijklmnopq"
|
||||||
|
match (r/match-by-name routes ::item {:item-id valid-id})
|
||||||
|
path (:path match)]
|
||||||
|
|
||||||
|
(is (some? path) "Should generate a valid path")
|
||||||
|
(is (re-= match (r/match-by-path routes path))
|
||||||
|
"match-by-path should find the same route that generated the path"))
|
||||||
|
|
||||||
|
(let [match (r/match-by-name routes ::->members {:team-id-b58 "a"})
|
||||||
|
path (:path match)]
|
||||||
|
|
||||||
|
(is (some? path) "Should generate a valid path")
|
||||||
|
(is (re-= match (r/match-by-path routes path))
|
||||||
|
"match-by-path should find the same route that generated the path")))
|
||||||
|
|
||||||
|
(testing "Partial match with missing parameters"
|
||||||
|
;; Test that routes with missing parameters return PartialMatch
|
||||||
|
(let [partial-match (r/match-by-name routes ::item {})]
|
||||||
|
(is (instance? reitit.core.PartialMatch partial-match)
|
||||||
|
"Should return a PartialMatch when params are missing")
|
||||||
|
(is (= #{:item-id} (:required partial-match))
|
||||||
|
"PartialMatch should indicate the required parameters")
|
||||||
|
(is (re-= (r/map->PartialMatch {:template "/:item-id"
|
||||||
|
:data {:name ::item
|
||||||
|
:parameters {:path {:item-id #"[a-z]{16,20}"}}}
|
||||||
|
:path-params {}
|
||||||
|
:required #{:item-id}
|
||||||
|
:result nil})
|
||||||
|
partial-match)))
|
||||||
|
|
||||||
|
;; Test for a nested path with missing parameters
|
||||||
|
(let [partial-match (r/match-by-name routes ::->members {})]
|
||||||
|
(is (instance? reitit.core.PartialMatch partial-match)
|
||||||
|
"Should return a PartialMatch for nested paths too")
|
||||||
|
(is (= #{:team-id-b58} (:required partial-match))
|
||||||
|
"PartialMatch should indicate the required parameters")))
|
||||||
|
|
||||||
|
(testing "Match with invalid parameters"
|
||||||
|
;; Invalid parameters (that don't match the regex) still produce a Match
|
||||||
|
(let [match (r/match-by-name routes ::item {:item-id "too-short"})
|
||||||
|
path (:path match)]
|
||||||
|
|
||||||
|
(is (instance? reitit.core.Match match)
|
||||||
|
"Should produce a Match even with invalid parameters")
|
||||||
|
(is (= "/too-short" path)
|
||||||
|
"Path should contain the provided parameter value")
|
||||||
|
(is (nil? (r/match-by-path routes path))
|
||||||
|
"Path with invalid parameter shouldn't be matchable by match-by-path")))
|
||||||
|
|
||||||
|
(testing "Non-existent routes"
|
||||||
|
(is (nil? (r/match-by-name routes ::non-existent))
|
||||||
|
"Should return nil for non-existent routes")))
|
||||||
|
|
||||||
|
(deftest regex-router-edge-cases-test
|
||||||
|
(testing "Empty router"
|
||||||
|
(let [empty-router (rt.regex/create-regex-router [])]
|
||||||
|
(is (nil? (r/match-by-path empty-router "/any/path")))))
|
||||||
|
|
||||||
|
(testing "Handling trailing slashes"
|
||||||
|
(is (nil? (r/match-by-path routes "/inbox/")))
|
||||||
|
|
||||||
|
(let [router-with-trailing-slash (rt.regex/create-regex-router [["inbox/" ::inbox-with-slash]])]
|
||||||
|
(is (nil? (r/match-by-path router-with-trailing-slash "/inbox/")))
|
||||||
|
(is (some? (r/match-by-path router-with-trailing-slash "/inbox")))))
|
||||||
|
|
||||||
|
(testing "Complex path patterns"
|
||||||
|
(let [complex-router (rt.regex/create-regex-router
|
||||||
|
[["articles/:year/:month/:slug"
|
||||||
|
{:name ::article
|
||||||
|
:parameters {:path {:year #"\d{4}"
|
||||||
|
:month #"\d{2}"
|
||||||
|
:slug #"[a-z0-9\-]+"}}}]
|
||||||
|
["files/:path*"
|
||||||
|
{:name ::file-path}]])]
|
||||||
|
|
||||||
|
;; Test article route with valid params
|
||||||
|
(let [match (r/match-by-name complex-router ::article
|
||||||
|
{:year "2023" :month "02" :slug "test-article"})]
|
||||||
|
(is (instance? reitit.core.Match match)
|
||||||
|
"Should return a Match for complex routes with valid params")
|
||||||
|
(is (= "/articles/2023/02/test-article" (:path match))
|
||||||
|
"Path should be constructed correctly"))
|
||||||
|
|
||||||
|
;; Test match-by-path with the generated path
|
||||||
|
(let [match (r/match-by-path complex-router "/articles/2023/02/test-article")]
|
||||||
|
(is (some? match)
|
||||||
|
"Should match a valid article path")
|
||||||
|
(is (= {:year "2023", :month "02", :slug "test-article"}
|
||||||
|
(:path-params match))
|
||||||
|
"Should extract all parameters correctly"))
|
||||||
|
|
||||||
|
;; Test invalid path
|
||||||
|
(is (nil? (r/match-by-path complex-router "/articles/202/02/test-article"))
|
||||||
|
"Should not match an invalid year (3 digits)")
|
||||||
|
|
||||||
|
;; Test partial params
|
||||||
|
(let [partial-match (r/match-by-name complex-router ::article {:year "2023"})]
|
||||||
|
(is (instance? reitit.core.PartialMatch partial-match)
|
||||||
|
"Should return PartialMatch when some params are missing")
|
||||||
|
(is (= #{:month :slug} (:required partial-match))
|
||||||
|
"Should indicate which params are missing")))))
|
||||||
|
|
||||||
|
(deftest custom-router-features-test
|
||||||
|
(testing "Router information access"
|
||||||
|
;; Test that router information methods work properly
|
||||||
|
(is (= :regex-router (r/router-name routes))
|
||||||
|
"Should return the correct router name")
|
||||||
|
|
||||||
|
(is (seq (r/routes routes))
|
||||||
|
"Should return the list of routes")
|
||||||
|
|
||||||
|
(is (= (set [::home ::item ::inbox ::teams ::->members])
|
||||||
|
(set (r/route-names routes)))
|
||||||
|
"Should return all route names"))
|
||||||
|
|
||||||
|
(testing "Compiled routes access"
|
||||||
|
(let [compiled (r/compiled-routes routes)]
|
||||||
|
(is (seq compiled)
|
||||||
|
"Should return compiled routes")
|
||||||
|
(is (every? :pattern compiled)
|
||||||
|
"Every compiled route should have a pattern")
|
||||||
|
(is (every? :route-data compiled)
|
||||||
|
"Every compiled route should have route data"))))
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
(ns reitit.ring-coercion-test
|
(ns reitit.ring-coercion-test
|
||||||
(:require [clojure.test :refer [deftest is testing]]
|
(:require [clojure.test :refer [deftest is testing]]
|
||||||
[malli.experimental.lite :as l]
|
[malli.experimental.lite :as l]
|
||||||
#?@(:clj [[muuntaja.middleware]
|
#?@(:clj [[muuntaja.core]
|
||||||
[jsonista.core :as j]])
|
[muuntaja.middleware]
|
||||||
|
[jsonista.core :as j]
|
||||||
|
[reitit.ring.middleware.muuntaja]])
|
||||||
[malli.core :as m]
|
[malli.core :as m]
|
||||||
[malli.util :as mu]
|
[malli.util :as mu]
|
||||||
[meta-merge.core :refer [meta-merge]]
|
[meta-merge.core :refer [meta-merge]]
|
||||||
|
|
@ -585,99 +587,131 @@
|
||||||
|
|
||||||
#?(:clj
|
#?(:clj
|
||||||
(deftest per-content-type-test
|
(deftest per-content-type-test
|
||||||
(doseq [[coercion json-request edn-request default-request json-response edn-response default-response]
|
(let [normalize-json (fn [resp]
|
||||||
[[malli/coercion
|
(update resp :body #(-> % j/write-value-as-string (j/read-value j/keyword-keys-object-mapper))))]
|
||||||
[:map [:request [:enum :json]] [:response any?]]
|
(doseq [[coercion json-request edn-request default-request json-response edn-response default-response]
|
||||||
[:map [:request [:enum :edn]] [:response any?]]
|
[[malli/coercion
|
||||||
[:map [:request [:enum :default]] [:response any?]]
|
[:map [:request [:enum :json]] [:response any?]]
|
||||||
[:map [:request any?] [:response [:enum :json]]]
|
[:map [:request [:enum :edn]] [:response any?]]
|
||||||
[:map [:request any?] [:response [:enum :edn]]]
|
[:map [:request [:enum :default]] [:response any?]]
|
||||||
[:map [:request any?] [:response [:enum :default]]]]
|
[:map [:request any?] [:response [:enum :json]]]
|
||||||
[schema/coercion
|
[:map [:request any?] [:response [:enum :edn]]]
|
||||||
{:request (s/eq :json) :response s/Any}
|
[:map [:request any?] [:response [:enum :default]]]]
|
||||||
{:request (s/eq :edn) :response s/Any}
|
[schema/coercion
|
||||||
{:request (s/eq :default) :response s/Any}
|
{:request (s/eq :json) :response s/Any}
|
||||||
{:request s/Any :response (s/eq :json)}
|
{:request (s/eq :edn) :response s/Any}
|
||||||
{:request s/Any :response (s/eq :edn)}
|
{:request (s/eq :default) :response s/Any}
|
||||||
{:request s/Any :response (s/eq :default)}]
|
{:request s/Any :response (s/eq :json)}
|
||||||
[spec/coercion
|
{:request s/Any :response (s/eq :edn)}
|
||||||
{:request (clojure.spec.alpha/spec #{:json}) :response any?}
|
{:request s/Any :response (s/eq :default)}]
|
||||||
{:request (clojure.spec.alpha/spec #{:edn}) :response any?}
|
[spec/coercion
|
||||||
{:request (clojure.spec.alpha/spec #{:default}) :response any?}
|
{:request (clojure.spec.alpha/spec #{:json}) :response any?}
|
||||||
{:request any? :response (clojure.spec.alpha/spec #{:json})}
|
{:request (clojure.spec.alpha/spec #{:edn}) :response any?}
|
||||||
{:request any? :response (clojure.spec.alpha/spec #{:end})}
|
{:request (clojure.spec.alpha/spec #{:default}) :response any?}
|
||||||
{:request any? :response (clojure.spec.alpha/spec #{:default})}]]]
|
{:request any? :response (clojure.spec.alpha/spec #{:json})}
|
||||||
(testing (str coercion)
|
{:request any? :response (clojure.spec.alpha/spec #{:end})}
|
||||||
(doseq [{:keys [name app]}
|
{:request any? :response (clojure.spec.alpha/spec #{:default})}]]]
|
||||||
[{:name "using top-level :body"
|
(testing (str coercion)
|
||||||
:app (ring/ring-handler
|
(doseq [{:keys [name app]}
|
||||||
(ring/router
|
[{:name "using top-level :body"
|
||||||
["/foo" {:post {:request {:content {"application/json" {:schema json-request}
|
:app (ring/ring-handler
|
||||||
"application/edn" {:schema edn-request}}
|
(ring/router
|
||||||
:body default-request}
|
["/foo" {:post {:request {:content {"application/json" {:schema json-request}
|
||||||
:responses {200 {:content {"application/json" {:schema json-response}
|
"application/edn" {:schema edn-request}}
|
||||||
"application/edn" {:schema edn-response}}
|
:body default-request}
|
||||||
:body default-response}}
|
:responses {200 {:content {"application/json" {:schema json-response}
|
||||||
:handler (fn [req]
|
"application/edn" {:schema edn-response}}
|
||||||
{:status 200
|
:body default-response}}
|
||||||
:body (-> req :parameters :request)})}}]
|
:handler (fn [req]
|
||||||
{:validate reitit.ring.spec/validate
|
{:status 200
|
||||||
:data {:middleware [rrc/coerce-request-middleware
|
:body (-> req :parameters :request)})}}]
|
||||||
rrc/coerce-response-middleware]
|
{:validate reitit.ring.spec/validate
|
||||||
:coercion coercion}}))}
|
:data {:middleware [rrc/coerce-request-middleware
|
||||||
{:name "using :default content"
|
rrc/coerce-response-middleware]
|
||||||
:app (ring/ring-handler
|
:coercion coercion}}))}
|
||||||
(ring/router
|
{:name "using :default content"
|
||||||
["/foo" {:post {:request {:content {"application/json" {:schema json-request}
|
:app (ring/ring-handler
|
||||||
"application/edn" {:schema edn-request}
|
(ring/router
|
||||||
:default {:schema default-request}}
|
["/foo" {:post {:request {:content {"application/json" {:schema json-request}
|
||||||
:body json-request} ;; not applied as :default exists
|
"application/edn" {:schema edn-request}
|
||||||
:responses {200 {:content {"application/json" {:schema json-response}
|
:default {:schema default-request}}
|
||||||
"application/edn" {:schema edn-response}
|
:body json-request} ;; not applied as :default exists
|
||||||
:default {:schema default-response}}
|
:responses {200 {:content {"application/json" {:schema json-response}
|
||||||
:body json-response}} ;; not applied as :default exists
|
"application/edn" {:schema edn-response}
|
||||||
:handler (fn [req]
|
:default {:schema default-response}}
|
||||||
{:status 200
|
:body json-response}} ;; not applied as :default exists
|
||||||
:body (-> req :parameters :request)})}}]
|
:handler (fn [req]
|
||||||
{:validate reitit.ring.spec/validate
|
{:status 200
|
||||||
:data {:middleware [rrc/coerce-request-middleware
|
:body (-> req :parameters :request)})}}]
|
||||||
rrc/coerce-response-middleware]
|
{:validate reitit.ring.spec/validate
|
||||||
:coercion coercion}}))}]]
|
:data {:middleware [rrc/coerce-request-middleware
|
||||||
(testing name
|
rrc/coerce-response-middleware]
|
||||||
(let [call (fn [request]
|
: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
|
(try
|
||||||
(app request)
|
(app request)
|
||||||
(catch ExceptionInfo e
|
(catch ExceptionInfo e
|
||||||
|
#_(ex-data e)
|
||||||
(select-keys (ex-data e) [:type :in]))))
|
(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
|
{:request-method :post
|
||||||
:uri "/foo"
|
:uri "/foo"
|
||||||
:muuntaja/request {:format request-format}
|
:muuntaja/request {:format request-format}
|
||||||
:muuntaja/response {:format response-format}
|
:body-params body})]
|
||||||
:body-params body})
|
(testing "via :muuntaja/content-type"
|
||||||
normalize-json (fn[body]
|
(is (= {:status 200 :body {:request "json" :response "json"} :muuntaja/content-type "application/json"}
|
||||||
(-> body j/write-value-as-string (j/read-value j/keyword-keys-object-mapper)))]
|
(normalize-json (call (request "application/json" {:request :json :response :json} {:status 200 :body {:request :json :response :json} :muuntaja/content-type "application/json"}))))
|
||||||
(testing "succesful call"
|
"valid reponse")
|
||||||
(is (= {:status 200 :body {:request "json", :response "json"}}
|
(is (= {:in [:response :body] :type :reitit.coercion/response-coercion}
|
||||||
(normalize-json (call (request "application/json" "application/json" {:request :json :response :json})))))
|
(call (request "application/json" {:request :json :response :json} {:status 200 :body {:request :json :response :invalid} :muuntaja/content-type "application/json"})))
|
||||||
(is (= {:status 200 :body {:request "edn", :response "json"}}
|
"invalid reponse")))))))))
|
||||||
(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}))))))))))))
|
|
||||||
|
|
||||||
|
|
||||||
#?(:clj
|
#?(:clj
|
||||||
|
|
@ -801,3 +835,100 @@
|
||||||
(app) :body slurp (read-string))]
|
(app) :body slurp (read-string))]
|
||||||
(is (= data-edn (e2e (assoc data-edn :EXTRA "VALUE"))))
|
(is (= data-edn (e2e (assoc data-edn :EXTRA "VALUE"))))
|
||||||
(is (thrown? ExceptionInfo (e2e data-json))))))))
|
(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