Read fragment string without decoding

Users can use Malli decoding to control decoding per schema.
This commit is contained in:
Juho Teperi 2023-01-18 20:24:33 +02:00
parent 25a051b003
commit b059c0c682
4 changed files with 3753 additions and 57 deletions

View file

@ -40,7 +40,7 @@
:form (->ParameterCoercion :form-params :string true true) :form (->ParameterCoercion :form-params :string true true)
:header (->ParameterCoercion :headers :string true true) :header (->ParameterCoercion :headers :string true true)
:path (->ParameterCoercion :path-params :string true true) :path (->ParameterCoercion :path-params :string true true)
:fragment (->ParameterCoercion :fragment-params :string true true)}) :fragment (->ParameterCoercion :fragment :string true true)})
(defn ^:no-doc request-coercion-failed! [result coercion value in request] (defn ^:no-doc request-coercion-failed! [result coercion value in request]
(throw (throw

View file

@ -21,19 +21,6 @@
(map (juxt keyword #(query-param q %))) (map (juxt keyword #(query-param q %)))
(into {})))) (into {}))))
(defn fragment-params
"Given goog.Uri, read fragment parameters into Clojure map."
[^Uri uri]
(let [fp (.getFragment uri)]
(if-not (seq fp)
{}
(into {}
(comp
(map #(str/split % #"="))
(map (fn [[k v]]
[(keyword k) v])))
(str/split fp #"&")))))
(defn match-by-path (defn match-by-path
"Given routing tree and current path, return match with possibly "Given routing tree and current path, return match with possibly
coerced parameters. Return nil if no match found. coerced parameters. Return nil if no match found.
@ -51,14 +38,17 @@
coercion/coerce!)] coercion/coerce!)]
(if-let [match (r/match-by-path router (.getPath uri))] (if-let [match (r/match-by-path router (.getPath uri))]
(let [q (query-params uri) (let [q (query-params uri)
fp (fragment-params uri) fragment (when (.hasFragment uri)
match (assoc match :query-params q :fragment-params fp) (.getFragment uri))
match (assoc match
:query-params q
:fragment fragment)
;; Return uncoerced values if coercion is not enabled - so ;; Return uncoerced values if coercion is not enabled - so
;; that tha parameters are always accessible from same property. ;; that tha parameters are always accessible from same property.
parameters (or (coerce! match) parameters (or (coerce! match)
{:path (:path-params match) {:path (:path-params match)
:query q :query q
:fragment fp})] :fragment fragment})]
(assoc match :parameters parameters)))))) (assoc match :parameters parameters))))))
(defn match-by-name (defn match-by-name

3647
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -4,12 +4,22 @@
[reitit.frontend :as rf] [reitit.frontend :as rf]
[reitit.coercion :as rc] [reitit.coercion :as rc]
[schema.core :as s] [schema.core :as s]
[reitit.coercion.schema :as rsc] [reitit.coercion.schema :as rcs]
[reitit.coercion.malli :as rcm]
[reitit.frontend.test-utils :refer [capture-console]])) [reitit.frontend.test-utils :refer [capture-console]]))
(defn m [x] (defn m [x]
(assoc x :data nil :result nil)) (assoc x :data nil :result nil))
(defn decode-form [s]
;; RFC 6749 4.2.2 specifies OAuth token response uses
;; form-urlencoded format to encode values in the fragment string.
;; Use built-in JS function to decode.
;; ring.util.codec/decode-form works on Clj.
(when s
;; fragment coercion will keywordize string keys
(into {} (.entries (js/URLSearchParams. s)))))
(deftest match-by-path-test (deftest match-by-path-test
(testing "simple" (testing "simple"
(let [router (r/router ["/" (let [router (r/router ["/"
@ -21,11 +31,11 @@
:data {:name ::frontpage} :data {:name ::frontpage}
:path-params {} :path-params {}
:query-params {} :query-params {}
:fragment-params {}
:path "/" :path "/"
:fragment nil
:parameters {:query {} :parameters {:query {}
:path {} :path {}
:fragment {}}}) :fragment nil}})
(rf/match-by-path router "/"))) (rf/match-by-path router "/")))
(is (= "/" (is (= "/"
@ -36,11 +46,11 @@
:data {:name ::foo} :data {:name ::foo}
:path-params {} :path-params {}
:query-params {} :query-params {}
:fragment-params {}
:path "/foo" :path "/foo"
:fragment nil
:parameters {:query {} :parameters {:query {}
:path {} :path {}
:fragment {}}}) :fragment nil}})
(rf/match-by-path router "/foo"))) (rf/match-by-path router "/foo")))
(is (= (r/map->Match (is (= (r/map->Match
@ -48,11 +58,11 @@
:data {:name ::foo} :data {:name ::foo}
:path-params {} :path-params {}
:query-params {:mode ["foo", "bar"]} :query-params {:mode ["foo", "bar"]}
:fragment-params {}
:path "/foo" :path "/foo"
:fragment nil
:parameters {:query {:mode ["foo", "bar"]} :parameters {:query {:mode ["foo", "bar"]}
:path {} :path {}
:fragment {}}}) :fragment nil}})
(rf/match-by-path router "/foo?mode=foo&mode=bar"))) (rf/match-by-path router "/foo?mode=foo&mode=bar")))
(is (= "/foo" (is (= "/foo"
@ -71,23 +81,19 @@
[":id" {:name ::foo [":id" {:name ::foo
:parameters {:path {:id s/Int} :parameters {:path {:id s/Int}
:query {(s/optional-key :mode) s/Keyword} :query {(s/optional-key :mode) s/Keyword}
:fragment {(s/optional-key :access_token) s/Str :fragment (s/maybe s/Str)}}]]
(s/optional-key :refresh_token) s/Str
(s/optional-key :expires_in) s/Int
(s/optional-key :provider_token) s/Str
(s/optional-key :token_type) s/Str}}}]]
{:compile rc/compile-request-coercers {:compile rc/compile-request-coercers
:data {:coercion rsc/coercion}})] :data {:coercion rcs/coercion}})]
(is (= (r/map->Match (is (= (r/map->Match
{:template "/:id" {:template "/:id"
:path-params {:id "5"} :path-params {:id "5"}
:query-params {} :query-params {}
:fragment-params {}
:path "/5" :path "/5"
:fragment nil
:parameters {:query {} :parameters {:query {}
:path {:id 5} :path {:id 5}
:fragment {}}}) :fragment nil}})
(m (rf/match-by-path router "/5")))) (m (rf/match-by-path router "/5"))))
(is (= "/5" (is (= "/5"
@ -111,35 +117,27 @@
{:template "/:id" {:template "/:id"
:path-params {:id "5"} :path-params {:id "5"}
:query-params {:mode "foo"} :query-params {:mode "foo"}
:fragment-params {}
:path "/5" :path "/5"
:fragment nil
:parameters {:path {:id 5} :parameters {:path {:id 5}
:query {:mode :foo} :query {:mode :foo}
:fragment {}}}) :fragment nil}})
(m (rf/match-by-path router "/5?mode=foo")))) (m (rf/match-by-path router "/5?mode=foo"))))
(is (= "/5?mode=foo" (is (= "/5?mode=foo"
(r/match->path (rf/match-by-name router ::foo {:id 5}) {:mode :foo})))) (r/match->path (rf/match-by-name router ::foo {:id 5}) {:mode :foo}))))
(testing "fragment is read" (testing "fragment string is read"
(is (= (r/map->Match (is (= (r/map->Match
{:template "/:id" {:template "/:id"
:path-params {:id "5"} :path-params {:id "5"}
:query-params {:mode "foo"} :query-params {:mode "foo"}
:fragment-params {:access_token "foo"
:refresh_token "bar"
:provider_token "baz"
:token_type "bearer"
:expires_in "3600"}
:path "/5" :path "/5"
:fragment "fragment"
:parameters {:path {:id 5} :parameters {:path {:id 5}
:query {:mode :foo} :query {:mode :foo}
:fragment {:access_token "foo" :fragment "fragment"}})
:refresh_token "bar" (m (rf/match-by-path router "/5?mode=foo#fragment")))))
:provider_token "baz"
:token_type "bearer"
:expires_in 3600}}})
(m (rf/match-by-path router "/5?mode=foo#access_token=foo&refresh_token=bar&provider_token=baz&token_type=bearer&expires_in=3600")))))
(testing "console warning about missing params" (testing "console warning about missing params"
(is (= [{:type :warn (is (= [{:type :warn
@ -151,4 +149,85 @@
(:messages (:messages
(capture-console (capture-console
(fn [] (fn []
(rf/match-by-name! router ::foo {})))))))))) (rf/match-by-name! router ::foo {})))))))))
(testing "malli coercion"
(let [router (r/router ["/"
[":id" {:name ::foo
:parameters {:path [:map
[:id :int]]
:query [:map
[:mode {:optional true} :keyword]]
:fragment [:maybe
[:map
{:decode/string decode-form}
[:access_token :string]
[:refresh_token :string]
[:expires_in :int]
[:provider_token :string]
[:token_type :string]]]}}]]
{:compile rc/compile-request-coercers
:data {:coercion rcm/coercion}})]
(is (= (r/map->Match
{:template "/:id"
:path-params {:id "5"}
:query-params {}
:path "/5"
:fragment nil
:parameters {:query {}
:path {:id 5}
:fragment nil}})
(m (rf/match-by-path router "/5"))))
(is (= "/5"
(r/match->path (rf/match-by-name router ::foo {:id 5}))))
(testing "coercion error"
(testing "throws without options"
(is (thrown? js/Error (m (rf/match-by-path router "/a")))))
(testing "thows and calles on-coercion-error"
(let [exception (atom nil)
match (atom nil)]
(is (thrown? js/Error (m (rf/match-by-path router "/a" {:on-coercion-error (fn [m e]
(reset! match m)
(reset! exception e))}))))
(is (= {:id "a"} (-> @match :path-params)))
(is (= {:id "a"} (-> @exception (ex-data) :value))))))
(testing "query param is read"
(is (= (r/map->Match
{:template "/:id"
:path-params {:id "5"}
:query-params {:mode "foo"}
:path "/5"
:fragment nil
:parameters {:path {:id 5}
:query {:mode :foo}
:fragment nil}})
(m (rf/match-by-path router "/5?mode=foo"))))
(is (= "/5?mode=foo"
(r/match->path (rf/match-by-name router ::foo {:id 5}) {:mode :foo}))))
(testing "fragment string is read"
(is (= (r/map->Match
{:template "/:id"
:path-params {:id "5"}
:query-params {:mode "foo"}
:path "/5"
:fragment "access_token=foo&refresh_token=bar&provider_token=baz&token_type=bearer&expires_in=3600"
:parameters {:path {:id 5}
:query {:mode :foo}
:fragment {:access_token "foo"
:refresh_token "bar"
:provider_token "baz"
:token_type "bearer"
:expires_in 3600}}})
(try
(m (rf/match-by-path router "/5?mode=foo#access_token=foo&refresh_token=bar&provider_token=baz&token_type=bearer&expires_in=3600"))
(catch :default e
(js/console.log e)
{})))))
)))