Compare commits

..

No commits in common. "master" and "0.10.0" have entirely different histories.

13 changed files with 33 additions and 155 deletions

View file

@ -12,12 +12,6 @@ We use [Break Versioning][breakver]. The version numbers follow a `<major>.<mino
[breakver]: https://github.com/ptaoussanis/encore/blob/master/BREAK-VERSIONING.md
## UNRELEASED
* **FIX** redirect-trailing-slash-handler won't make external redirects. [#776](https://github.com/metosin/reitit/pull/776)
* Allow colons in bracket parameter syntax. [#770](https://github.com/metosin/reitit/pull/770)
* Add `url-encode?` option to `match-by-name`. [#778](https://github.com/metosin/reitit/pull/778)
## 0.10.0 (2026-01-09)
* 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).

View file

@ -75,17 +75,6 @@ Path-parameters are automatically coerced into strings, with the help of (curren
; :path-params {:id "1"}}
```
In case you want to do something like generate a template path for documentation, you can disable url-encoding:
```clj
(r/match-by-name router ::user {:id "<id goes here>"} {:url-encode? false})
; #reitit.core.Match{:template "/api/user/:id"
; :data {:name :user/user}
; :path "/api/user/<id goes here>"
; :result nil
; :path-params {:id "<id goes here>"}}
```
There is also an exception throwing version:
```clj
@ -108,5 +97,5 @@ It can take an optional map of query-parameters too:
(-> router
(r/match-by-name ::user {:id 1})
(r/match->path {:iso "möly"}))
; "/api/user/1?iso=m%C3%B6ly"
; "/api/user/1?iso=m%C3%B6ly"
```

View file

@ -63,6 +63,8 @@ Handlers can access the coerced parameters via the `:parameters` key in the requ
{:status 200
:body {:total total}}))})
```
### Nested parameter definitions
Parameters are accumulated recursively along the route tree, just like
@ -92,26 +94,6 @@ handling for merging eg. malli `:map` schemas.
; [:task-id :int]]}
```
### Differences in behaviour for different parameters
All parameter coercions *except* `:body`:
1. Allow keys outside the schema (by opening up the schema using eg. `malli.util/open-schema`)
2. Keywordize the keys (ie. header & query parameter names) of the input before coercing
In contrast, the `:body` coercion:
1. Uses the specified schema
* depending on the coercion, it can be configured as open or closed, see specific coercion docs for details
2. Does not keywordize the keys of the input before coercion
* however, coercions like malli might do the keywordization when coercing json bodies, depending on configuration
This admittedly confusing behaviour is retained currently due to
backwards compatibility reasons. It can be configured by passing
option `:reitit.coercion/parameter-coercion` to `reitit.ring/router`
or `reitit.coercion/compile-request-coercers`. See also:
`reitit.coercion/default-parameter-coercion`.
## Coercion Middleware
Defining a coercion for a route data doesn't do anything, as it's just data. We have to attach some code to apply the actual coercion. We can use the middleware from `reitit.ring.coercion`:

View file

@ -4,7 +4,6 @@
[metosin/jsonista "0.3.8"]
[ring/ring-jetty-adapter "1.12.1"]
[metosin/reitit "0.10.0"]
[metosin/ring-swagger-ui "5.9.0"]
[org.slf4j/slf4j-simple "2.0.9"]]
[metosin/ring-swagger-ui "5.9.0"]]
:repl-options {:init-ns example.server}
:profiles {:dev {:dependencies [[ring/ring-mock "0.4.0"]]}})

View file

@ -156,22 +156,22 @@
[:regex [:re "[0-9]+"]]
[:enum [:enum 1 3 5 42]]
[:multi [:multi {:dispatch :type}
["literal" [:map
[:type [:= "literal"]]
[:literal [:map
[:type [:= :literal]]
[:value [:or :int :string]]]]
["reference" [:map
[:type [:= "reference"]]
[:reference [:map
[:type [:= :reference]]
[:description :string]
[:ref :uuid]]]]]]
:example {:vector-of-tuples [["a" 1] ["b" 2]]
:regex "01234"
:enum 5
:multi {:type "literal"
:multi {:type :literal
:value "x"}}}}}
:responses {200 {:content {:default {:schema [:map-of :keyword :any]}}}}
:responses {200 {:content {:default {:schema [:map]}}}}
:handler (fn [request]
{:status 200
:body (get-in request [:parameters :request])})}}]
:body (:body request)})}}]
["/secure"
{:tags #{"secure"}

View file

@ -46,7 +46,7 @@
(options [this])
(route-names [this])
(match-by-path [this path])
(match-by-name [this name] [this name path-params] [this name path-params opts]))
(match-by-name [this name] [this name path-params]))
(defn router? [x]
(satisfies? Router x))
@ -122,11 +122,9 @@
(match-by-name [_ name]
(if-let [match (impl/fast-get lookup name)]
(match nil)))
(match-by-name [r name path-params]
(match-by-name r name path-params nil))
(match-by-name [_ name path-params opts]
(match-by-name [_ name path-params]
(if-let [match (impl/fast-get lookup name)]
(match (impl/path-params path-params opts))))))))
(match (impl/path-params path-params))))))))
(defn lookup-router
"Creates a lookup-router from resolved routes and optional
@ -163,11 +161,9 @@
(match-by-name [_ name]
(if-let [match (impl/fast-get lookup name)]
(match nil)))
(match-by-name [r name path-params]
(match-by-name r name path-params nil))
(match-by-name [_ name path-params opts]
(match-by-name [_ name path-params]
(if-let [match (impl/fast-get lookup name)]
(match (impl/path-params path-params opts))))))))
(match (impl/path-params path-params))))))))
(defn trie-router
"Creates a special prefix-tree router from resolved routes and optional
@ -212,11 +208,9 @@
(match-by-name [_ name]
(if-let [match (impl/fast-get lookup name)]
(match nil)))
(match-by-name [r name path-params]
(match-by-name r name path-params nil))
(match-by-name [_ name path-params opts]
(match-by-name [_ name path-params]
(if-let [match (impl/fast-get lookup name)]
(match (impl/path-params path-params opts))))))))
(match (impl/path-params path-params))))))))
(defn single-static-path-router
"Creates a fast router of 1 static route(s) and optional
@ -244,10 +238,8 @@
(if (#?(:clj .equals :cljs =) p path) match))
(match-by-name [_ name]
(if (= n name) match))
(match-by-name [r name path-params]
(match-by-name r name path-params nil))
(match-by-name [_ name path-params opts]
(if (= n name) (impl/fast-assoc match :path-params (impl/path-params path-params opts))))))))
(match-by-name [_ name path-params]
(if (= n name) (impl/fast-assoc match :path-params (impl/path-params path-params))))))))
(defn mixed-router
"Creates two routers: [[lookup-router]] or [[single-static-path-router]] for
@ -276,11 +268,9 @@
(match-by-name [_ name]
(or (match-by-name static-router name)
(match-by-name wildcard-router name)))
(match-by-name [r name path-params]
(match-by-name r name path-params nil))
(match-by-name [_ name path-params opts]
(or (match-by-name static-router name path-params opts)
(match-by-name wildcard-router name path-params opts)))))))
(match-by-name [_ name path-params]
(or (match-by-name static-router name path-params)
(match-by-name wildcard-router name path-params)))))))
(defn quarantine-router
"Creates two routers: [[mixed-router]] for non-conflicting routes
@ -309,11 +299,9 @@
(match-by-name [_ name]
(or (match-by-name mixed-router name)
(match-by-name linear-router name)))
(match-by-name [r name path-params]
(match-by-name r name path-params nil))
(match-by-name [_ name path-params opts]
(or (match-by-name mixed-router name path-params opts)
(match-by-name linear-router name path-params opts)))))))
(match-by-name [_ name path-params]
(or (match-by-name mixed-router name path-params)
(match-by-name linear-router name path-params)))))))
;;
;; Creating Routers

View file

@ -297,11 +297,8 @@
(defn path-params
"Convert parameters' values into URL-encoded strings, suitable for URL paths"
([params] (path-params params nil))
([params {:keys [url-encode?] :or {url-encode? true}}]
(if url-encode?
(maybe-map-values #(url-encode (into-string %)) params)
(maybe-map-values #(into-string %) params))))
[params]
(maybe-map-values #(url-encode (into-string %)) params))
(defn- query-parameter [k v]
(str (form-encode (into-string k))

View file

@ -71,17 +71,11 @@
(and bracket? (= \{ c))
(let [^long to' (or (str/index-of s "}" to) (ex/fail! ::unclosed-brackets {:path s}))]
(cond
(= \* (get s (inc to)))
(if (= \* (get s (inc to)))
(recur (concat ss (-static from to) (-catch-all (inc to) to')) (long (inc to')) (long (inc to')))
(= \: (get s (inc to)))
(recur (concat ss (-static from to) (-wild (inc to) to')) (long (inc to')) (long (inc to')))
:else
(recur (concat ss (-static from to) (-wild to to')) (long (inc to')) (long (inc to')))))
(and colon? (= \: c) (not= \{ (get s (dec to))))
(and colon? (= \: c))
(let [^long to' (or (str/index-of s "/" to) (count s))]
(if (= 1 (- to' to))
(recur ss from (inc to))

View file

@ -173,8 +173,7 @@
(letfn [(maybe-redirect [{:keys [query-string] :as request} path]
(if (and (seq path) (r/match-by-path (::r/router request) path))
{:status (if (= (:request-method request) :get) 301 308)
:headers {"Location" (let [path (str/replace-first path #"^/+" "/")] ; Locations starting with // redirect to another hostname. Avoid these due to security implications.
(if query-string (str path "?" query-string) path))}
:headers {"Location" (if query-string (str path "?" query-string) path)}
:body ""}))
(redirect-handler [request]
(let [uri (:uri request)]

View file

@ -33,12 +33,6 @@
:path "/api/ipa/large"
:path-params {:size "large"}})
(r/match-by-name router ::beer {:size "large"})))
(is (= (r/map->Match
{:template "/api/ipa/:size"
:data {:name ::beer}
:path "/api/ipa/:large"
:path-params {:size ":large"}})
(r/match-by-name router ::beer {:size ":large"} {:url-encode? false})))
(is (= (r/map->Match
{:template "/api/ipa/:size"
:data {:name ::beer}

View file

@ -41,33 +41,7 @@
:u #uuid "c2541900-17a7-4353-9024-db8ac258ba4e"
:k :kikka
:qk ::kikka
:nil nil})))
(is (= {:n "1"
:n1 "-1"
:n2 "1"
:n3 "1"
:n4 "1"
:n5 "1"
:d "2.2"
:b "true"
:s "kikka"
:u "c2541900-17a7-4353-9024-db8ac258ba4e"
:k "kikka"
:qk "reitit.impl-test/kikka"
:nil nil}
(impl/path-params {:n 1
:n1 -1
:n2 (long 1)
:n3 (int 1)
:n4 (short 1)
:n5 (byte 1)
:d 2.2
:b true
:s "kikka"
:u #uuid "c2541900-17a7-4353-9024-db8ac258ba4e"
:k :kikka
:qk ::kikka
:nil nil} {:url-encode? false}))))
:nil nil}))))
(deftest query-params-test
(are [x y]

View file

@ -416,31 +416,7 @@
:get "/slash-less//" "/slash-less?kikka=kukka"
:post "/with-slash" "/with-slash/?kikka=kukka"
:post "/slash-less/" "/slash-less?kikka=kukka"
:post "/slash-less//" "/slash-less?kikka=kukka"))))
;; See issue #337
(testing "Avoid external redirects"
(let [app (ring/ring-handler
(ring/router [["*" {:get (constantly nil)}]])
(ring/redirect-trailing-slash-handler))
resp (fn [uri & [query-string]]
(let [r (app {:request-method :get :uri uri :query-string query-string})]
{:status (:status r)
:Location (get-in r [:headers "Location"])}))]
(testing "without query params"
(is (= {:status 301 :Location "/malicious.com/foo/"} (resp "//malicious.com/foo")))
(is (= {:status 301 :Location "/malicious.com/foo"} (resp "//malicious.com/foo/")))
(is (= {:status 301 :Location "/malicious.com/foo"} (resp "//malicious.com/foo//")))
(is (= {:status 301 :Location "/malicious.com/foo/"} (resp "///malicious.com/foo")))
(is (= {:status 301 :Location "/malicious.com/foo"} (resp "///malicious.com/foo/")))
(is (= {:status 301 :Location "/malicious.com/foo"} (resp "///malicious.com/foo//"))))
(testing "with query params"
(is (= {:status 301 :Location "/malicious.com/foo/?bar=quux"} (resp "//malicious.com/foo" "bar=quux")))
(is (= {:status 301 :Location "/malicious.com/foo?bar=quux"} (resp "//malicious.com/foo/" "bar=quux")))
(is (= {:status 301 :Location "/malicious.com/foo?bar=quux"} (resp "//malicious.com/foo//" "bar=quux")))
(is (= {:status 301 :Location "/malicious.com/foo/?bar=quux"} (resp "///malicious.com/foo" "bar=quux")))
(is (= {:status 301 :Location "/malicious.com/foo?bar=quux"} (resp "///malicious.com/foo/" "bar=quux")))
(is (= {:status 301 :Location "/malicious.com/foo?bar=quux"} (resp "///malicious.com/foo//" "bar=quux"))))))))
:post "/slash-less//" "/slash-less?kikka=kukka"))))))
(deftest async-ring-test
(let [promise #(let [value (atom ::nil)]

View file

@ -41,9 +41,7 @@
"/olipa/:kerran/avaruus", ["/olipa/" (trie/->Wild :kerran) "/avaruus"]
"/olipa/{kerran}/avaruus", ["/olipa/{kerran}/avaruus"]
"/olipa/{:kerran}/avaruus", ["/olipa/{:kerran}/avaruus"]
"/olipa/{a.b/c}/avaruus", ["/olipa/{a.b/c}/avaruus"]
"/olipa/{:a.b/c}/avaruus", ["/olipa/{:a.b/c}/avaruus"]
"/olipa/kerran/*avaruus", ["/olipa/kerran/" (trie/->CatchAll :avaruus)]
"/olipa/kerran/{*avaruus}", ["/olipa/kerran/{" (trie/->CatchAll (keyword "avaruus}"))]
"/olipa/kerran/{*valtavan.suuri/avaruus}", ["/olipa/kerran/{" (trie/->CatchAll (keyword "valtavan.suuri/avaruus}"))])))
@ -55,9 +53,7 @@
"/olipa/:kerran/avaruus", ["/olipa/:kerran/avaruus"]
"/olipa/{kerran}/avaruus", ["/olipa/" (trie/->Wild :kerran) "/avaruus"]
"/olipa/{:kerran}/avaruus", ["/olipa/" (trie/->Wild :kerran) "/avaruus"]
"/olipa/{a.b/c}/avaruus", ["/olipa/" (trie/->Wild :a.b/c) "/avaruus"]
"/olipa/{:a.b/c}/avaruus", ["/olipa/" (trie/->Wild :a.b/c) "/avaruus"]
"/olipa/kerran/*avaruus", ["/olipa/kerran/*avaruus"]
"/olipa/kerran/{*avaruus}", ["/olipa/kerran/" (trie/->CatchAll :avaruus)]
"/olipa/kerran/{*valtavan.suuri/avaruus}", ["/olipa/kerran/" (trie/->CatchAll :valtavan.suuri/avaruus)])))
@ -69,9 +65,7 @@
"/olipa/:kerran/avaruus", ["/olipa/" (trie/->Wild :kerran) "/avaruus"]
"/olipa/{kerran}/avaruus", ["/olipa/" (trie/->Wild :kerran) "/avaruus"]
"/olipa/{:kerran}/avaruus", ["/olipa/" (trie/->Wild :kerran) "/avaruus"]
"/olipa/{a.b/c}/avaruus", ["/olipa/" (trie/->Wild :a.b/c) "/avaruus"]
"/olipa/{:a.b/c}/avaruus", ["/olipa/" (trie/->Wild :a.b/c) "/avaruus"]
"/olipa/kerran/*avaruus", ["/olipa/kerran/" (trie/->CatchAll :avaruus)]
"/olipa/kerran/{*avaruus}", ["/olipa/kerran/" (trie/->CatchAll :avaruus)]
"/olipa/kerran/{*valtavan.suuri/avaruus}", ["/olipa/kerran/" (trie/->CatchAll :valtavan.suuri/avaruus)])))
@ -83,9 +77,7 @@
"/olipa/:kerran/avaruus", ["/olipa/:kerran/avaruus"]
"/olipa/{kerran}/avaruus", ["/olipa/{kerran}/avaruus"]
"/olipa/{:kerran}/avaruus", ["/olipa/{:kerran}/avaruus"]
"/olipa/{a.b/c}/avaruus", ["/olipa/{a.b/c}/avaruus"]
"/olipa/{:a.b/c}/avaruus", ["/olipa/{:a.b/c}/avaruus"]
"/olipa/kerran/*avaruus", ["/olipa/kerran/*avaruus"]
"/olipa/kerran/{*avaruus}", ["/olipa/kerran/{*avaruus}"]
"/olipa/kerran/{*valtavan.suuri/avaruus}", ["/olipa/kerran/{*valtavan.suuri/avaruus}"]))))