diff --git a/CHANGELOG.md b/CHANGELOG.md index 6cc51e35..abebb0af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,69 @@ +## 0.1.1-SNAPSHOT + +### `reitit-core` + +* `match-by-path` encodes parameters into strings using (internal) `reitit.impl/IntoString` protocol. Handles all of: strings, numbers, keywords, booleans, objects. Fixes [#75](https://github.com/metosin/reitit/issues/75). + +```clj +(require '[reitit.core :as r]) + +(r/match-by-name + (r/router + ["/coffee/:type" ::coffee]) + ::coffee + {:type :luwak}) +;#Match{:template "/coffee/:type", +; :data {:name :user/coffee}, +; :result nil, +; :path-params {:type "luwak"}, +; :path "/coffee/luwak"} +``` + +### `reitit-swagger` + +* New module to produce swagger-docs from routing tree, including `Coercion` definitions. Works with both middleware & interceptors. + +```clj +(require '[reitit.ring :as ring]) +(require '[reitit.swagger :as swagger]) +(require '[reitit.ring.coercion :as rrc]) +(require '[reitit.coercion.spec :as spec]) +(require '[reitit.coercion.schema :as schema]) + +(require '[schema.core :refer [Int]]) + +(ring/ring-handler + (ring/router + ["/api" + {:swagger {:id ::math}} + + ["/swagger.json" + {:get {:no-doc true + :swagger {:info {:title "my-api"}} + :handler swagger/swagger-spec-handler}}] + + ["/spec" {:coercion spec/coercion} + ["/plus" + {:get {:summary "plus" + :parameters {:query {:x int?, :y int?}} + :responses {200 {:body {:total int?}}} + :handler (fn [{{{:keys [x y]} :query} :parameters}] + {:status 200, :body {:total (+ x y)}})}}]] + + ["/schema" {:coercion schema/coercion} + ["/plus" + {:get {:summary "plus" + :parameters {:query {:x Int, :y Int}} + :responses {200 {:body {:total Int}}} + :handler (fn [{{{:keys [x y]} :query} :parameters}] + {:status 200, :body {:total (+ x y)}})}}]]] + + {:data {:middleware [rrc/coerce-exceptions-middleware + rrc/coerce-request-middleware + rrc/coerce-response-middleware + swagger/swagger-feature]}})) +``` + ## 0.1.0 (2018-2-19) * First release diff --git a/doc/basics/name_based_routing.md b/doc/basics/name_based_routing.md index 69bbf6af..3e5c7d8c 100644 --- a/doc/basics/name_based_routing.md +++ b/doc/basics/name_based_routing.md @@ -64,6 +64,17 @@ With provided path-parameters: ; :path-params {:id "1"}} ``` +Path-parameters are automatically coerced into strings, with the help of (currently internal) Protocol `reitit.impl/IntoString`. It supports strings, numbers, booleans, keywords and objects: + +```clj +(r/match-by-name router ::user {:id 1}) +; #Match{:template "/api/user/:id" +; :data {:name :user/user} +; :path "/api/user/1" +; :result nil +; :path-params {:id "1"}} +``` + There is also a exception throwing version: ```clj diff --git a/modules/reitit-core/src/reitit/core.cljc b/modules/reitit-core/src/reitit/core.cljc index 535353f4..35d2c87b 100644 --- a/modules/reitit-core/src/reitit/core.cljc +++ b/modules/reitit-core/src/reitit/core.cljc @@ -175,7 +175,7 @@ (match nil))) (match-by-name [_ name path-params] (if-let [match (impl/fast-get lookup name)] - (match path-params))))))) + (match (impl/path-params path-params)))))))) (defn lookup-router "Creates a lookup-router from resolved routes and optional @@ -215,7 +215,7 @@ (match nil))) (match-by-name [_ name path-params] (if-let [match (impl/fast-get lookup name)] - (match path-params))))))) + (match (impl/path-params path-params)))))))) (defn segment-router "Creates a special prefix-tree style segment router from resolved routes and optional @@ -255,7 +255,7 @@ (match nil))) (match-by-name [_ name path-params] (if-let [match (impl/fast-get lookup name)] - (match path-params))))))) + (match (impl/path-params path-params)))))))) (defn single-static-path-router "Creates a fast router of 1 static route(s) and optional @@ -290,7 +290,7 @@ match)) (match-by-name [_ name path-params] (if (= n name) - (impl/fast-assoc match :path-params path-params))))))) + (impl/fast-assoc match :path-params (impl/path-params path-params)))))))) (defn mixed-router "Creates two routers: [[lookup-router]] or [[single-static-path-router]] for diff --git a/modules/reitit-core/src/reitit/impl.cljc b/modules/reitit-core/src/reitit/impl.cljc index 46d5a3b7..9795cf11 100644 --- a/modules/reitit-core/src/reitit/impl.cljc +++ b/modules/reitit-core/src/reitit/impl.cljc @@ -13,8 +13,10 @@ (ns ^:no-doc reitit.impl (:require [clojure.string :as str] [clojure.set :as set]) - #?(:clj (:import (java.util.regex Pattern) - (java.util HashMap Map)))) + #?(:clj + (:import (java.util.regex Pattern) + (java.util HashMap Map) + (java.net URLEncoder URLDecoder)))) (defn wild? [s] (contains? #{\: \*} (first (str s)))) @@ -135,7 +137,7 @@ (defn throw-on-missing-path-params [template required path-params] (when-not (every? #(contains? path-params %) required) (let [defined (-> path-params keys set) - missing (clojure.set/difference required defined)] + missing (set/difference required defined)] (throw (ex-info (str "missing path-params for route " template " -> " missing) @@ -155,3 +157,52 @@ (defn strip-nils [m] (->> m (remove (comp nil? second)) (into {}))) + +;; +;; Path-parameters, see https://github.com/metosin/reitit/issues/75 +;; + +(defn url-encode [s] + (some-> s + #?(:clj (URLEncoder/encode "UTF-8") + :cljs (js/encodeURIComponent)) + #?(:clj (.replace "+" "%20")))) + +(defn url-decode [s] + (some-> s #?(:clj (URLDecoder/decode "UTF-8") + :cljs (js/decodeURIComponent)))) + +(defprotocol IntoString + (into-string [_])) + +(extend-protocol IntoString + #?(:clj String + :cljs string) + (into-string [this] this) + + #?(:clj clojure.lang.Keyword + :cljs cljs.core.Keyword) + (into-string [this] + (let [ns (namespace this)] + (str ns (if ns "/") (name this)))) + + #?(:clj Boolean + :cljs boolean) + (into-string [this] (str this)) + + #?(:clj Number + :cljs number) + (into-string [this] (str this)) + + #?(:clj Object + :cljs object) + (into-string [this] (str this))) + +(defn path-params + "shallow transform of the path-param values into strings" + [params] + (reduce-kv + (fn [m k v] + (assoc m k (url-encode (into-string v)))) + {} + params)) diff --git a/test/cljc/reitit/core_test.cljc b/test/cljc/reitit/core_test.cljc index 83ef54c1..e8144e75 100644 --- a/test/cljc/reitit/core_test.cljc +++ b/test/cljc/reitit/core_test.cljc @@ -29,6 +29,12 @@ :path "/api/ipa/large" :path-params {:size "large"}}) (r/match-by-name router ::beer {:size "large"}))) + (is (= (r/map->Match + {:template "/api/ipa/:size" + :data {:name ::beer} + :path "/api/ipa/large" + :path-params {:size "large"}}) + (r/match-by-name router ::beer {:size :large}))) (is (= nil (r/match-by-name router "ILLEGAL"))) (is (= [::beer] (r/route-names router))) diff --git a/test/cljc/reitit/impl_test.cljc b/test/cljc/reitit/impl_test.cljc index 45032008..3e2e5ff5 100644 --- a/test/cljc/reitit/impl_test.cljc +++ b/test/cljc/reitit/impl_test.cljc @@ -10,3 +10,39 @@ (deftest strip-nils-test (is (= {:a 1, :c false} (impl/strip-nils {:a 1, :b nil, :c false})))) + +(deftest url-encode-and-decode-test + (is (= "reitit.impl-test%2Fkikka" (-> ::kikka + impl/into-string + impl/url-encode))) + (is (= ::kikka (-> ::kikka + impl/into-string + impl/url-encode + impl/url-decode + keyword)))) + +(deftest path-params-test + (is (= {:n "1" + :n1 "-1" + :n2 "1" + :n3 "1" + :n4 "1" + :n5 "1" + :d "2.2" + :b "true" + :s "kikka" + :u "c2541900-17a7-4353-9024-db8ac258ba4e" + :k "kikka" + :qk "reitit.impl-test%2Fkikka"} + (impl/path-params {:n 1 + :n1 -1 + :n2 (long 1) + :n3 (int 1) + :n4 (short 1) + :n5 (byte 1) + :d 2.2 + :b true + :s "kikka" + :u #uuid "c2541900-17a7-4353-9024-db8ac258ba4e" + :k :kikka + :qk ::kikka}))))