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)
:header (->ParameterCoercion :headers :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]
(throw

View file

@ -21,19 +21,6 @@
(map (juxt keyword #(query-param q %)))
(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
"Given routing tree and current path, return match with possibly
coerced parameters. Return nil if no match found.
@ -51,14 +38,17 @@
coercion/coerce!)]
(if-let [match (r/match-by-path router (.getPath uri))]
(let [q (query-params uri)
fp (fragment-params uri)
match (assoc match :query-params q :fragment-params fp)
fragment (when (.hasFragment uri)
(.getFragment uri))
match (assoc match
:query-params q
:fragment fragment)
;; Return uncoerced values if coercion is not enabled - so
;; that tha parameters are always accessible from same property.
parameters (or (coerce! match)
{:path (:path-params match)
:query q
:fragment fp})]
:fragment fragment})]
(assoc match :parameters parameters))))))
(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.coercion :as rc]
[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]]))
(defn m [x]
(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
(testing "simple"
(let [router (r/router ["/"
@ -21,11 +31,11 @@
:data {:name ::frontpage}
:path-params {}
:query-params {}
:fragment-params {}
:path "/"
:fragment nil
:parameters {:query {}
:path {}
:fragment {}}})
:fragment nil}})
(rf/match-by-path router "/")))
(is (= "/"
@ -36,11 +46,11 @@
:data {:name ::foo}
:path-params {}
:query-params {}
:fragment-params {}
:path "/foo"
:fragment nil
:parameters {:query {}
:path {}
:fragment {}}})
:fragment nil}})
(rf/match-by-path router "/foo")))
(is (= (r/map->Match
@ -48,11 +58,11 @@
:data {:name ::foo}
:path-params {}
:query-params {:mode ["foo", "bar"]}
:fragment-params {}
:path "/foo"
:fragment nil
:parameters {:query {:mode ["foo", "bar"]}
:path {}
:fragment {}}})
:fragment nil}})
(rf/match-by-path router "/foo?mode=foo&mode=bar")))
(is (= "/foo"
@ -71,23 +81,19 @@
[":id" {:name ::foo
:parameters {:path {:id s/Int}
:query {(s/optional-key :mode) s/Keyword}
:fragment {(s/optional-key :access_token) 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}}}]]
:fragment (s/maybe s/Str)}}]]
{:compile rc/compile-request-coercers
:data {:coercion rsc/coercion}})]
:data {:coercion rcs/coercion}})]
(is (= (r/map->Match
{:template "/:id"
:path-params {:id "5"}
:query-params {}
:fragment-params {}
:path "/5"
:fragment nil
:parameters {:query {}
:path {:id 5}
:fragment {}}})
:fragment nil}})
(m (rf/match-by-path router "/5"))))
(is (= "/5"
@ -111,35 +117,27 @@
{:template "/:id"
:path-params {:id "5"}
:query-params {:mode "foo"}
:fragment-params {}
:path "/5"
:fragment nil
:parameters {:path {:id 5}
:query {:mode :foo}
:fragment {}}})
: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 is read"
(testing "fragment string is read"
(is (= (r/map->Match
{:template "/:id"
:path-params {:id "5"}
:query-params {:mode "foo"}
:fragment-params {:access_token "foo"
:refresh_token "bar"
:provider_token "baz"
:token_type "bearer"
:expires_in "3600"}
:path "/5"
:fragment "fragment"
:parameters {:path {:id 5}
:query {:mode :foo}
:fragment {:access_token "foo"
:refresh_token "bar"
: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")))))
:fragment "fragment"}})
(m (rf/match-by-path router "/5?mode=foo#fragment")))))
(testing "console warning about missing params"
(is (= [{:type :warn
@ -151,4 +149,85 @@
(:messages
(capture-console
(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)
{})))))
)))