Merge pull request #581 from metosin/add-support-for-fragment-parameters-2

Add reitit-frontend support for fragment string
This commit is contained in:
Ilmo Raunio 2023-02-21 15:05:25 +02:00 committed by GitHub
commit 310dcd0e99
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 120 additions and 14 deletions

View file

@ -39,7 +39,8 @@
:body (->ParameterCoercion :body-params :body false false) :body (->ParameterCoercion :body-params :body false false)
: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 :string true true)})
(defn ^:no-doc request-coercion-failed! [result coercion value in request serialize-failed-result] (defn ^:no-doc request-coercion-failed! [result coercion value in request serialize-failed-result]
(throw (throw

View file

@ -37,12 +37,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)
match (assoc match :query-params q) fragment (when (.hasFragment uri)
(.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 fragment})]
(assoc match :parameters parameters)))))) (assoc match :parameters parameters))))))
(defn match-by-name (defn match-by-name

View file

@ -4,12 +4,23 @@
[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
(->> (.entries (js/URLSearchParams. s))
(map (fn [[k v]] [(keyword k) v]))
(into {}))))
(deftest match-by-path-test (deftest match-by-path-test
(testing "simple" (testing "simple"
(let [router (r/router ["/" (let [router (r/router ["/"
@ -22,8 +33,10 @@
:path-params {} :path-params {}
:query-params {} :query-params {}
:path "/" :path "/"
:fragment nil
:parameters {:query {} :parameters {:query {}
:path {}}}) :path {}
:fragment nil}})
(rf/match-by-path router "/"))) (rf/match-by-path router "/")))
(is (= "/" (is (= "/"
@ -35,8 +48,10 @@
:path-params {} :path-params {}
:query-params {} :query-params {}
:path "/foo" :path "/foo"
:fragment nil
:parameters {:query {} :parameters {:query {}
:path {}}}) :path {}
:fragment nil}})
(rf/match-by-path router "/foo"))) (rf/match-by-path router "/foo")))
(is (= (r/map->Match (is (= (r/map->Match
@ -45,8 +60,10 @@
:path-params {} :path-params {}
:query-params {:mode ["foo", "bar"]} :query-params {:mode ["foo", "bar"]}
:path "/foo" :path "/foo"
:fragment nil
:parameters {:query {:mode ["foo", "bar"]} :parameters {:query {:mode ["foo", "bar"]}
:path {}}}) :path {}
: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"
@ -64,17 +81,20 @@
(let [router (r/router ["/" (let [router (r/router ["/"
[":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/maybe 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 {}
:path "/5" :path "/5"
:fragment nil
:parameters {:query {} :parameters {:query {}
:path {:id 5}}}) :path {:id 5}
:fragment nil}})
(m (rf/match-by-path router "/5")))) (m (rf/match-by-path router "/5"))))
(is (= "/5" (is (= "/5"
@ -99,21 +119,25 @@
:path-params {:id "5"} :path-params {:id "5"}
:query-params {:mode "foo"} :query-params {:mode "foo"}
:path "/5" :path "/5"
:fragment nil
:parameters {:path {:id 5} :parameters {:path {:id 5}
:query {:mode :foo}}}) :query {:mode :foo}
: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 ignored" (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"}
:path "/5" :path "/5"
:fragment "fragment"
:parameters {:path {:id 5} :parameters {:path {:id 5}
:query {:mode :foo}}}) :query {:mode :foo}
:fragment "fragment"}})
(m (rf/match-by-path router "/5?mode=foo#fragment"))))) (m (rf/match-by-path router "/5?mode=foo#fragment")))))
(testing "console warning about missing params" (testing "console warning about missing params"
@ -126,4 +150,80 @@
(: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}}})
(m (rf/match-by-path router "/5?mode=foo#access_token=foo&refresh_token=bar&provider_token=baz&token_type=bearer&expires_in=3600"))))))))