Move query string coercion to coercion ns from core

This commit is contained in:
Juho Teperi 2025-01-28 14:34:21 +02:00
parent ce6d9e26cd
commit 7ae118fbb5
5 changed files with 80 additions and 59 deletions

View file

@ -1,5 +1,6 @@
(ns reitit.coercion (ns reitit.coercion
(:require [#?(:clj reitit.walk :cljs clojure.walk) :as walk] (:require [#?(:clj reitit.walk :cljs clojure.walk) :as walk]
[reitit.core :as r]
[reitit.impl :as impl]) [reitit.impl :as impl])
#?(:clj #?(:clj
(:import (java.io Writer)))) (:import (java.io Writer))))
@ -220,3 +221,33 @@
[match] [match]
(if-let [coercers (-> match :result :coerce)] (if-let [coercers (-> match :result :coerce)]
(coerce-request coercers match))) (coerce-request coercers match)))
(defn coerce-query-params
"Uses an input schema and coercion implementation from the given match to
encode query-parameters map.
If no match, no input schema or coercion implementation, just returns the
original parameters map."
[match query-params]
(when query-params
(let [coercion (-> match :data :coercion)
schema (when coercion
(-compile-model coercion (-> match :data :parameters :query) nil))
coercer (when (and schema coercion)
(-query-string-coercer coercion schema))]
(if coercer
(let [result (coercer query-params :default)]
(if (error? result)
(throw (ex-info (str "Query parameters coercion failed")
result))
result))
query-params))))
(defn match->path
"Create routing path from given match and optional query-parameters map.
Query-parameters are encoded using the input schema and coercion implementation."
([match]
(r/match->path match))
([match query-params]
(r/match->path match (coerce-query-params match query-params))))

View file

@ -1,9 +1,7 @@
(ns reitit.core (ns reitit.core
(:require [reitit.exception :as exception] (:require [reitit.exception :as exception]
[reitit.impl :as impl] [reitit.impl :as impl]
[reitit.trie :as trie] [reitit.trie :as trie]))
;; FIXME: Should avoid coercion require here?
[reitit.coercion :as coercion]))
;; ;;
;; Expand ;; Expand
@ -70,25 +68,12 @@
(:template match) (:required match) path-params))))) (:template match) (:required match) path-params)))))
(defn match->path (defn match->path
"Create routing path from given match and optional query-parameters map."
([match] ([match]
(match->path match nil)) (match->path match nil))
([match query-params] ([match query-params]
(some-> match :path (cond-> (seq query-params) (some-> match :path (cond-> (seq query-params)
;; TODO: Should the coercion be applied elsewhere (FE ns?) so the core ns doesn't depend (str "?" (impl/query-string query-params))))))
;; on the coercion?
;; NOTE: Re-creates coercer on every call, could this be pre-compiled somewhere
;; or memoized? Does it matter much?
;; TODO: query-coercer could be compiled in reitit.frontend/router, same as request coercers.
(str "?" (let [coercion (-> match :data :coercion)
schema (when coercion
(coercion/-compile-model coercion (-> match :data :parameters :query) nil))
coercer (when (and schema coercion)
(coercion/-query-string-coercer coercion schema))
query-params (if coercer
(coercer query-params :default)
query-params)]
;; Default encoding for values will handle values that aren't encoded using coercer
(impl/query-string query-params)))))))
;; ;;
;; Different routers ;; Different routers

View file

@ -40,14 +40,16 @@
(defn (defn
^{:see-also ["reitit.core/match->path"]} ^{:see-also ["reitit.core/match->path"]}
match->path match->path
"Create routing path from given match and optional query-string map and "Create routing path from given match and optional query-parameters map and
optional fragment string." optional fragment string.
Query-parameters are encoded using the input schema and coercion implementation."
([match] ([match]
(match->path match nil nil)) (match->path match nil nil))
([match query-params] ([match query-params]
(match->path match query-params nil)) (match->path match query-params nil))
([match query-params fragment] ([match query-params fragment]
(when-let [path (r/match->path match query-params)] (when-let [path (coercion/match->path match query-params)]
(cond-> path (cond-> path
(and fragment (seq fragment)) (str "#" (impl/form-encode fragment)))))) (and fragment (seq fragment)) (str "#" (impl/form-encode fragment))))))

View file

@ -160,13 +160,13 @@
(is (= "/olipa/kerran?x=a&x=b" (is (= "/olipa/kerran?x=a&x=b"
(-> router (-> router
(r/match-by-name! ::route {:a "olipa", :b "kerran"}) (r/match-by-name! ::route {:a "olipa", :b "kerran"})
(r/match->path {:x [:a :b]})))) (coercion/match->path {:x [:a :b]}))))
(is (= "/olipa/kerran?x=a&x=b&extra=extra-param" (is (= "/olipa/kerran?x=a&x=b&extra=extra-param"
(-> router (-> router
(r/match-by-name! ::route {:a "olipa", :b "kerran"}) (r/match-by-name! ::route {:a "olipa", :b "kerran"})
(r/match->path {:x [:a :b] (coercion/match->path {:x [:a :b]
:extra "extra-param"})))))) :extra "extra-param"}))))))
(testing "custom encode/string for a collection" (testing "custom encode/string for a collection"
(let [router (r/router ["/:a/:b" (let [router (r/router ["/:a/:b"
@ -182,18 +182,20 @@
(mapv keyword (str/split s #",")) (mapv keyword (str/split s #","))
s))} s))}
:keyword]]]}}] :keyword]]]}}]
{:compile coercion/compile-request-coercers})] {:compile coercion/compile-request-coercers})
;; NOTE: "," is urlencoded by the impl/query-string step, is that ok? match (r/match-by-name! router ::route {:a "olipa", :b "kerran"})]
(is (= {:x "a,b"}
(coercion/coerce-query-params match {:x [:a :b]})))
;; NOTE: "," is urlencoded by the impl/query-string step
(is (= "/olipa/kerran?x=a%2Cb" (is (= "/olipa/kerran?x=a%2Cb"
(-> router (coercion/match->path match {:x [:a :b]})))
(r/match-by-name! ::route {:a "olipa", :b "kerran"})
(r/match->path {:x [:a :b]}))))
(testing "extra query-string parameters aren't removed by coercion" (testing "extra query-string parameters aren't removed by coercion"
(is (= "/olipa/kerran?x=a%2Cb&extra=extra-param" (is (= "/olipa/kerran?x=a%2Cb&extra=extra-param"
(-> router (-> router
(r/match-by-name! ::route {:a "olipa", :b "kerran"}) (r/match-by-name! ::route {:a "olipa", :b "kerran"})
(r/match->path {:x [:a :b] (coercion/match->path {:x [:a :b]
:extra "extra-param"}))))) :extra "extra-param"})))))
(is (= {:query {:x [:a :b]}} (is (= {:query {:x [:a :b]}}
@ -220,7 +222,7 @@
(is (= "/olipa/kerran?x=__a&x=__b" (is (= "/olipa/kerran?x=__a&x=__b"
(-> router (-> router
(r/match-by-name! ::route {:a "olipa", :b "kerran"}) (r/match-by-name! ::route {:a "olipa", :b "kerran"})
(r/match->path {:x [:a :b]})))) (coercion/match->path {:x [:a :b]}))))
(is (= {:query {:x [:a :b]}} (is (= {:query {:x [:a :b]}}
(-> (r/match-by-path router "/olipa/kerran") (-> (r/match-by-path router "/olipa/kerran")

View file

@ -40,74 +40,75 @@
(fn on-navigate [match history] (fn on-navigate [match history]
(let [url (rfh/-get-path history)] (let [url (rfh/-get-path history)]
(case (swap! n inc) (case (swap! n inc)
1 (do (is (some? (:popstate-listener history))) 1 (rfh/push-state history ::frontpage)
2 (do (is (some? (:popstate-listener history)))
(is (= "/" url) (is (= "/" url)
"start at root") "start at root")
(rfe/push-state ::foo nil {:a 1} "foo bar")) (rfe/push-state ::foo nil {:a 1} "foo bar"))
;; 0. / ;; 0. /
;; 1. /foo?a=1#foo+bar ;; 1. /foo?a=1#foo+bar
2 (do (is (= "/foo?a=1#foo+bar" url) 3 (do (is (= "/foo?a=1#foo+bar" url)
"push-state") "push-state")
(.back js/window.history)) (.back js/window.history))
;; 0. / ;; 0. /
3 (do (is (= "/" url) 4 (do (is (= "/" url)
"go back") "go back")
(rfe/navigate ::bar {:path-params {:id 1} (rfe/navigate ::bar {:path-params {:id 1}
:query-params {:q "x"}})) :query-params {:q "x"}}))
;; 0. / ;; 0. /
;; 1. /bar/1 ;; 1. /bar/1
4 (do (is (= "/bar/1?q=__x" url) 5 (do (is (= "/bar/1?q=__x" url)
"push-state 2") "push-state 2")
(rfe/replace-state ::bar {:id 2})) (rfe/replace-state ::bar {:id 2}))
;; 0. / ;; 0. /
;; 1. /bar/2 ;; 1. /bar/2
5 (do (is (= "/bar/2" url) 6 (do (is (= "/bar/2" url)
"replace-state") "replace-state")
(rfe/set-query {:a 1})) (rfe/set-query {:a 1}))
;; 0. / ;; 0. /
;; 1. /bar/2 ;; 1. /bar/2
;; 2. /bar/2?a=1 ;; 2. /bar/2?a=1
6 (do (is (= "/bar/2?a=1" url) 7 (do (is (= "/bar/2?a=1" url)
"update-query with map") "update-query with map")
(rfe/set-query #(assoc % :q "x") {:replace true})) (rfe/set-query #(assoc % :q "x") {:replace true}))
;; 0. / ;; 0. /
;; 1. /bar/2 ;; 1. /bar/2
;; 2. /bar/2?a=1&b=foo ;; 2. /bar/2?a=1&b=foo
7 (do (is (= "/bar/2?a=1&q=__x" url) 8 (do (is (= "/bar/2?a=1&q=__x" url)
"update-query with fn") "update-query with fn")
(.go js/window.history -2)) (.go js/window.history -2))
;; Go to non-matching path and check set-query works ;; Go to non-matching path and check set-query works
;; (without coercion) without a match ;; (without coercion) without a match
8 (do (is (= "/" url) "go back two events") 9 (do (is (= "/" url) "go back two events")
(.pushState js/window.history nil "" "#/non-matching-path")) (.pushState js/window.history nil "" "#/non-matching-path"))
9 (do (is (= "/non-matching-path" url)) 10 (do (is (= "/non-matching-path" url))
(rfe/set-query #(assoc % :q "x"))) (rfe/set-query #(assoc % :q "x")))
10 (do (is (= "/non-matching-path?q=x" url)) 11 (do (is (= "/non-matching-path?q=x" url))
(.go js/window.history -2)) (.go js/window.history -2))
;; 0. / ;; 0. /
11 (do (is (= "/" url) 12 (do (is (= "/" url)
"go back two events") "go back two events")
;; Reset to ensure old event listeners aren't called ;; Reset to ensure old event listeners aren't called
(rfe/start! router (rfe/start! router
(fn on-navigate [match history] (fn on-navigate [match history]
(let [url (rfh/-get-path history)] (let [url (rfh/-get-path history)]
(case (swap! n inc) (case (swap! n inc)
12 (do (is (= "/" url) 13 (do (is (= "/" url)
"start at root") "start at root")
(rfe/push-state ::foo)) (rfe/push-state ::foo))
13 (do (is (= "/foo" url) 14 (do (is (= "/foo" url)
"push-state") "push-state")
(rfh/stop! @rfe/history) (rfh/stop! @rfe/history)
(done)) (done))
(do (do
(is false (str "extra event 2" {:n @n, :url url})) (is false (str "extra event 2" {:n @n, :url url}))
(done))))) (done)))))
{:use-fragment true})) {:use-fragment true}))
(do (do
(is false (str "extra event 1" {:n @n, :url url})) (is false (str "extra event 1" {:n @n, :url url}))
(done))))) (done)))))