diff --git a/modules/reitit-core/src/reitit/coercion.cljc b/modules/reitit-core/src/reitit/coercion.cljc index 4436214c..8c382e9a 100644 --- a/modules/reitit-core/src/reitit/coercion.cljc +++ b/modules/reitit-core/src/reitit/coercion.cljc @@ -39,7 +39,8 @@ :body (->ParameterCoercion :body-params :body false false) :form (->ParameterCoercion :form-params :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] (throw diff --git a/modules/reitit-frontend/src/reitit/frontend.cljs b/modules/reitit-frontend/src/reitit/frontend.cljs index d1e9c50a..faa258e0 100644 --- a/modules/reitit-frontend/src/reitit/frontend.cljs +++ b/modules/reitit-frontend/src/reitit/frontend.cljs @@ -37,12 +37,17 @@ coercion/coerce!)] (if-let [match (r/match-by-path router (.getPath 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 ;; that tha parameters are always accessible from same property. parameters (or (coerce! match) {:path (:path-params match) - :query q})] + :query q + :fragment fragment})] (assoc match :parameters parameters)))))) (defn match-by-name diff --git a/test/cljs/reitit/frontend/core_test.cljs b/test/cljs/reitit/frontend/core_test.cljs index 72bf9428..d0684398 100644 --- a/test/cljs/reitit/frontend/core_test.cljs +++ b/test/cljs/reitit/frontend/core_test.cljs @@ -4,12 +4,23 @@ [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 + (->> (.entries (js/URLSearchParams. s)) + (map (fn [[k v]] [(keyword k) v])) + (into {})))) + (deftest match-by-path-test (testing "simple" (let [router (r/router ["/" @@ -22,8 +33,10 @@ :path-params {} :query-params {} :path "/" + :fragment nil :parameters {:query {} - :path {}}}) + :path {} + :fragment nil}}) (rf/match-by-path router "/"))) (is (= "/" @@ -35,8 +48,10 @@ :path-params {} :query-params {} :path "/foo" + :fragment nil :parameters {:query {} - :path {}}}) + :path {} + :fragment nil}}) (rf/match-by-path router "/foo"))) (is (= (r/map->Match @@ -45,8 +60,10 @@ :path-params {} :query-params {:mode ["foo", "bar"]} :path "/foo" + :fragment nil :parameters {:query {:mode ["foo", "bar"]} - :path {}}}) + :path {} + :fragment nil}}) (rf/match-by-path router "/foo?mode=foo&mode=bar"))) (is (= "/foo" @@ -64,17 +81,20 @@ (let [router (r/router ["/" [":id" {:name ::foo :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 - :data {:coercion rsc/coercion}})] + :data {:coercion rcs/coercion}})] (is (= (r/map->Match {:template "/:id" :path-params {:id "5"} :query-params {} :path "/5" + :fragment nil :parameters {:query {} - :path {:id 5}}}) + :path {:id 5} + :fragment nil}}) (m (rf/match-by-path router "/5")))) (is (= "/5" @@ -99,21 +119,25 @@ :path-params {:id "5"} :query-params {:mode "foo"} :path "/5" + :fragment nil :parameters {:path {:id 5} - :query {:mode :foo}}}) + :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 is ignored" + (testing "fragment string is read" (is (= (r/map->Match {:template "/:id" :path-params {:id "5"} :query-params {:mode "foo"} :path "/5" + :fragment "fragment" :parameters {:path {:id 5} - :query {:mode :foo}}}) + :query {:mode :foo} + :fragment "fragment"}}) (m (rf/match-by-path router "/5?mode=foo#fragment"))))) (testing "console warning about missing params" @@ -126,4 +150,80 @@ (: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}}}) + (m (rf/match-by-path router "/5?mode=foo#access_token=foo&refresh_token=bar&provider_token=baz&token_type=bearer&expires_in=3600"))))))))