Merge pull request #290 from metosin/parameter-syntax

Parameter syntax
This commit is contained in:
Tommi Reiman 2019-06-09 20:59:20 +03:00 committed by GitHub
commit 0bcfda755f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 196 additions and 89 deletions

View file

@ -21,6 +21,24 @@ We use [Break Versioning][breakver]. The version numbers follow a `<major>.<mino
[metosin/jsonista "0.2.3"] is available but we use "0.2.2"
```
### `reitit-core`
* Add support for explixit selection of router path-parameter `:syntax`, fixes [#276](https://github.com/metosin/reitit/issues/276)
```clj
(require '[reitit.core :as r])
(-> (r/router
["http://localhost:8080/api/user/{id}" ::user-by-id]
{:syntax :bracket})
(r/match-by-path "http://localhost:8080/api/user/123"))
;#Match{:template "http://localhost:8080/api/user/{id}",
; :data {:name :user/user-by-id},
; :result nil,
; :path-params {:id "123"},
; :path "http://localhost:8080/api/user/123"}
```
## 0.3.7 (2019-05-25)
### `reitit-pedestal`

View file

@ -2,12 +2,13 @@
Routers can be configured via options. The following options are available for the `reitit.core/router`:
| key | description |
|--------------|-------------|
| key | description
|--------------|-------------
| `:path` | Base-path for routes
| `:routes` | Initial resolved routes (default `[]`)
| `:data` | Initial route data (default `{}`)
| `:spec` | clojure.spec definition for a route data, see `reitit.spec` on how to use this
| `:syntax` | Path-parameter syntax as keyword or set of keywords (default #{:bracket :colon})
| `:expand` | Function of `arg opts => data` to expand route arg to route data (default `reitit.core/expand`)
| `:coerce` | Function of `route opts => route` to coerce resolved route, can throw or return `nil`
| `:compile` | Function of `route opts => result` to compile a route handler

View file

@ -4,7 +4,7 @@ Routes are defined as vectors of String path and optional (non-sequential) route
Routes can be wrapped in vectors and lists and `nil` routes are ignored.
Paths can have path-parameters (`:id`) or catch-all-parameters (`*path`). Since version `0.3.0`, parameters can also be wrapped in brackets, enabling use of qualified keywords `{user/id}`, `{*user/path}`. The non-bracket syntax might be deprecated later.
Paths can have path-parameters (`:id`) or catch-all-parameters (`*path`). Parameters can also be wrapped in brackets, enabling use of qualified keywords `{user/id}`, `{*user/path}`. By default, both syntaxes are supported, see [configuring routers](../advanced/configuring_routers.md) on how to change this.
### Examples
@ -129,3 +129,23 @@ Routes are just data, so it's easy to create them programmatically:
; ["/add-user" {:post {:interceptors [add-user]}}]
; ["/add-order" {:post {:interceptors [add-order]}}])]
```
### Explicit path-parameter syntax
Router options `:syntax` allows the path-parameter syntax to be explicitely defined. It takes a keyword or set of keywords as a value. Valid values are `:colon` and `:bracket`. Default value is `#{:colon :bracket}`.
Supporting only `:bracket` syntax:
```clj
(require '[reitit.core :as r])
(-> (r/router
["http://localhost:8080/api/user/{id}" ::user-by-id]
{:syntax :bracket})
(r/match-by-path "http://localhost:8080/api/user/123"))
;#Match{:template "http://localhost:8080/api/user/{id}",
; :data {:name :user/user-by-id},
; :result nil,
; :path-params {:id "123"},
; :path "http://localhost:8080/api/user/123"}
```

View file

@ -88,7 +88,7 @@
names (impl/find-names compiled-routes opts)
[pl nl] (reduce
(fn [[pl nl] [p {:keys [name] :as data} result]]
(let [{:keys [path-params] :as route} (impl/parse p)
(let [{:keys [path-params] :as route} (impl/parse p opts)
f #(if-let [path (impl/path-for route %)]
(->Match p data result (impl/url-decode-coll %) path)
(->PartialMatch p data result (impl/url-decode-coll %) path-params))]
@ -131,7 +131,7 @@
([compiled-routes]
(lookup-router compiled-routes {}))
([compiled-routes opts]
(when-let [wilds (seq (filter impl/wild-route? compiled-routes))]
(when-let [wilds (seq (filter (impl/->wild-route? opts) compiled-routes))]
(exception/fail!
(str "can't create :lookup-router with wildcard routes: " wilds)
{:wilds wilds
@ -184,7 +184,7 @@
names (impl/find-names compiled-routes opts)
[pl nl] (reduce
(fn [[pl nl] [p {:keys [name] :as data} result]]
(let [{:keys [path-params] :as route} (impl/parse p)
(let [{:keys [path-params] :as route} (impl/parse p opts)
f #(if-let [path (impl/path-for route %)]
(->Match p data result (impl/url-decode-coll %) path)
(->PartialMatch p data result (impl/url-decode-coll %) path-params))]
@ -227,7 +227,7 @@
([compiled-routes]
(single-static-path-router compiled-routes {}))
([compiled-routes opts]
(when (or (not= (count compiled-routes) 1) (some impl/wild-route? compiled-routes))
(when (or (not= (count compiled-routes) 1) (some (impl/->wild-route? opts) compiled-routes))
(exception/fail!
(str ":single-static-path-router requires exactly 1 static route: " compiled-routes)
{:routes compiled-routes}))
@ -266,7 +266,7 @@
([compiled-routes]
(mixed-router compiled-routes {}))
([compiled-routes opts]
(let [{wild true, lookup false} (group-by impl/wild-route? compiled-routes)
(let [{wild true, lookup false} (group-by (impl/->wild-route? opts) compiled-routes)
->static-router (if (= 1 (count lookup)) single-static-path-router lookup-router)
wildcard-router (trie-router wild opts)
static-router (->static-router lookup opts)
@ -301,7 +301,7 @@
([compiled-routes]
(quarantine-router compiled-routes {}))
([compiled-routes opts]
(let [conflicting-paths (-> compiled-routes impl/path-conflicting-routes impl/conflicting-paths)
(let [conflicting-paths (-> compiled-routes (impl/path-conflicting-routes opts) impl/conflicting-paths)
conflicting? #(contains? conflicting-paths (first %))
{conflicting true, non-conflicting false} (group-by conflicting? compiled-routes)
linear-router (linear-router conflicting opts)
@ -347,12 +347,13 @@
Selects implementation based on route details. The following options
are available:
| key | description |
| -------------|-------------|
| key | description
| -------------|-------------
| `:path` | Base-path for routes
| `:routes` | Initial resolved routes (default `[]`)
| `:data` | Initial route data (default `{}`)
| `:spec` | clojure.spec definition for a route data, see `reitit.spec` on how to use this
| `:syntax` | Path-parameter syntax as keyword or set of keywords (default #{:bracket :colon})
| `:expand` | Function of `arg opts => data` to expand route arg to route data (default `reitit.core/expand`)
| `:coerce` | Function of `route opts => route` to coerce resolved route, can throw or return `nil`
| `:compile` | Function of `route opts => result` to compile a route handler
@ -366,11 +367,11 @@
(let [{:keys [router] :as opts} (merge (default-router-options) opts)]
(try
(let [routes (impl/resolve-routes raw-routes opts)
path-conflicting (impl/path-conflicting-routes routes)
path-conflicting (impl/path-conflicting-routes routes opts)
name-conflicting (impl/name-conflicting-routes routes)
compiled-routes (impl/compile-routes routes opts)
wilds? (boolean (some impl/wild-route? compiled-routes))
all-wilds? (every? impl/wild-route? compiled-routes)
wilds? (boolean (some (impl/->wild-route? opts) compiled-routes))
all-wilds? (every? (impl/->wild-route? opts) compiled-routes)
router (cond
router router
(and (= 1 (count compiled-routes)) (not wilds?)) single-static-path-router

View file

@ -11,18 +11,19 @@
(java.util HashMap Map)
(java.net URLEncoder URLDecoder))))
(defrecord Route [path path-parts path-params])
(defn parse [path]
(let [path #?(:clj (.intern ^String (trie/normalize path)) :cljs (trie/normalize path))
path-parts (trie/split-path path)
(defn parse [path opts]
(let [path #?(:clj (.intern ^String (trie/normalize path opts)) :cljs (trie/normalize path opts))
path-parts (trie/split-path path opts)
path-params (->> path-parts (remove string?) (map :value) set)]
(map->Route {:path-params path-params
{:path-params path-params
:path-parts path-parts
:path path})))
:path path}))
(defn wild-route? [[path]]
(-> path parse :path-params seq boolean))
(defn wild-path? [path opts]
(-> path (parse opts) :path-params seq boolean))
(defn ->wild-route? [opts]
(fn [[path]] (-> path (parse opts) :path-params seq boolean)))
(defn maybe-map-values
"Applies a function to every value of a map, updates the value if not nil.
@ -74,14 +75,11 @@
(cond->> (->> (walk raw-routes opts) (map-data merge-data))
coerce (into [] (keep #(coerce % opts)))))
(defn conflicting-routes? [route1 route2]
(trie/conflicting-paths? (first route1) (first route2)))
(defn path-conflicting-routes [routes]
(defn path-conflicting-routes [routes opts]
(-> (into {}
(comp (map-indexed (fn [index route]
[route (into #{}
(filter (partial conflicting-routes? route))
(filter #(trie/conflicting-paths? (first route) (first %) opts))
(subvec routes (inc index)))]))
(filter (comp seq second)))
routes)
@ -114,7 +112,7 @@
(defn uncompile-routes [routes]
(mapv (comp vec (partial take 2)) routes))
(defn path-for [^Route route path-params]
(defn path-for [route path-params]
(if (:path-params route)
(if-let [parts (reduce
(fn [acc part]

View file

@ -5,6 +5,12 @@
#?(:clj (:import [reitit Trie Trie$Match Trie$Matcher]
(java.net URLDecoder))))
(defn ^:no-doc into-set [x]
(cond
(or (set? x) (sequential? x)) (set x)
(nil? x) #{}
:else (conj #{} x)))
(defrecord Wild [value])
(defrecord CatchAll [value])
(defrecord Match [params data])
@ -51,25 +57,36 @@
(keyword (subs s 0 i) (subs s (inc i)))
(keyword s)))
(defn split-path [s]
(let [-static (fn [from to] (if-not (= from to) [(subs s from to)]))
(defn split-path [s {:keys [syntax] :or {syntax #{:bracket :colon}}}]
(let [bracket? (-> syntax (into-set) :bracket)
colon? (-> syntax (into-set) :colon)
-static (fn [from to] (if-not (= from to) [(subs s from to)]))
-wild (fn [from to] [(->Wild (-keyword (subs s (inc from) to)))])
-catch-all (fn [from to] [(->CatchAll (keyword (subs s (inc from) to)))])]
(loop [ss nil, from 0, to 0]
(if (= to (count s))
(concat ss (-static from to))
(case (get s to)
\{ (let [to' (or (str/index-of s "}" to) (ex/fail! ::unclosed-brackets {:path s}))]
(let [c (get s to)]
(cond
(and bracket? (= \{ c))
(let [to' (or (str/index-of s "}" to) (ex/fail! ::unclosed-brackets {:path s}))]
(if (= \* (get s (inc to)))
(recur (concat ss (-static from to) (-catch-all (inc to) to')) (long (inc to')) (long (inc to')))
(recur (concat ss (-static from to) (-wild to to')) (long (inc to')) (long (inc to')))))
\: (let [to' (or (str/index-of s "/" to) (count s))]
(and colon? (= \: c))
(let [to' (or (str/index-of s "/" to) (count s))]
(if (= 1 (- to' to))
(recur ss from (inc to))
(recur (concat ss (-static from to) (-wild to to')) (long to') (long to'))))
\* (let [to' (count s)]
(and colon? (= \* c))
(let [to' (count s)]
(recur (concat ss (-static from to) (-catch-all to to')) (long to') (long to')))
(recur ss from (inc to)))))))
:else
(recur ss from (inc to))))))))
(defn join-path [xs]
(reduce
@ -80,8 +97,8 @@
(instance? CatchAll x) (str "{*" (-> x :value str (subs 1)) "}"))))
"" xs))
(defn normalize [s]
(-> s (split-path) (join-path)))
(defn normalize [s opts]
(-> s (split-path opts) (join-path)))
;;
;; Conflict Resolution
@ -115,9 +132,9 @@
(concat [(subs x i)] xs)
xs)))
(defn conflicting-paths? [path1 path2]
(loop [parts1 (split-path path1)
parts2 (split-path path2)]
(defn conflicting-paths? [path1 path2 opts]
(loop [parts1 (split-path path1 opts)
parts2 (split-path path2 opts)]
(let [[[s1 & ss1] [s2 & ss2]] (-slice-start parts1 parts2)]
(cond
(= s1 s2 nil) true
@ -314,10 +331,10 @@
node routes))
([node path data]
(insert node path data nil))
([node path data {::keys [parameters] :or {parameters map-parameters}}]
(let [parts (split-path path)
([node path data {::keys [parameters] :or {parameters map-parameters} :as opts}]
(let [parts (split-path path opts)
params (parameters (->> parts (remove string?) (map :value)))]
(-insert (or node (-node {})) (split-path path) path params data))))
(-insert (or node (-node {})) (split-path path opts) path params data))))
(defn compiler
"Returns a default [[TrieCompiler]]."

View file

@ -1,6 +1,5 @@
(ns reitit.swagger
(:require [reitit.core :as r]
[reitit.impl :as impl]
[meta-merge.core :refer [meta-merge]]
[clojure.spec.alpha :as s]
[clojure.set :as set]
@ -65,8 +64,8 @@
{:name ::swagger
:spec ::spec})
(defn- swagger-path [path]
(-> path trie/normalize (str/replace #"\{\*" "{")))
(defn- swagger-path [path opts]
(-> path (trie/normalize opts) (str/replace #"\{\*" "{")))
(defn create-swagger-handler []
"Create a ring handler to emit swagger spec. Collects all routes from router which have
@ -74,15 +73,14 @@
(fn create-swagger
([{:keys [::r/router ::r/match :request-method]}]
(let [{:keys [id] :or {id ::default} :as swagger} (-> match :result request-method :data :swagger)
->set (fn [x] (if (or (set? x) (sequential? x)) (set x) (conj #{} x)))
ids (->set id)
ids (trie/into-set id)
strip-top-level-keys #(dissoc % :id :info :host :basePath :definitions :securityDefinitions)
strip-endpoint-keys #(dissoc % :id :parameters :responses :summary :description)
swagger (->> (strip-endpoint-keys swagger)
(merge {:swagger "2.0"
:x-id ids}))
accept-route (fn [route]
(-> route second :swagger :id (or ::default) ->set (set/intersection ids) seq))
(-> route second :swagger :id (or ::default) (trie/into-set) (set/intersection ids) seq))
transform-endpoint (fn [[method {{:keys [coercion no-doc swagger] :as data} :data
middleware :middleware
interceptors :interceptors}]]
@ -97,7 +95,7 @@
(strip-top-level-keys swagger))]))
transform-path (fn [[p _ c]]
(if-let [endpoint (some->> c (keep transform-endpoint) (seq) (into {}))]
[(swagger-path p) endpoint]))]
[(swagger-path p (r/options router)) endpoint]))]
(let [map-in-order #(->> % (apply concat) (apply array-map))
paths (->> router (r/compiled-routes) (filter accept-route) (map transform-path) map-in-order)]
{:status 200

View file

@ -292,7 +292,7 @@
(let [routes (impl/resolve-routes data (r/default-router-options))
conflicts (-> routes
(impl/resolve-routes (r/default-router-options))
(impl/path-conflicting-routes))]
(impl/path-conflicting-routes nil))]
(if conflicting? (seq conflicts) (nil? conflicts)))
true [["/a"]
@ -328,7 +328,7 @@
["/c" {}] #{["/*d" {}]}}
(-> [["/a"] ["/:b"] ["/c"] ["/*d"]]
(impl/resolve-routes (r/default-router-options))
(impl/path-conflicting-routes)))))
(impl/path-conflicting-routes nil)))))
(testing "router with conflicting routes"
(testing "throws by default"

View file

@ -2,27 +2,6 @@
(:require [clojure.test :refer [deftest testing is are]]
[reitit.impl :as impl]))
(deftest conflicting-route-test
(are [c? p1 p2]
(is (= c? (impl/conflicting-routes? [p1] [p2])))
true "/a" "/a"
true "/a" "/:a"
true "/a/:b" "/:a/b"
true "/ab/:b" "/:a/ba"
true "/*a" "/:a/ba/ca"
true "/a" "/{a}"
true "/a/{b}" "/{a}/b"
true "/ab/{b}" "/{a}/ba"
true "/{*a}" "/{a}/ba/ca"
false "/a" "/:a/b"
false "/a" "/:a/b"
false "/a" "/{a}/b"
false "/a" "/{a}/b"))
(deftest strip-nils-test
(is (= {:a 1, :c false} (impl/strip-nils {:a 1, :b nil, :c false}))))
@ -188,8 +167,7 @@
"%2B632+905+123+4567" "+632 905 123 4567"))
(deftest parse-test
(is (= (impl/map->Route
{:path "https://google.com"
(is (= {:path "https://google.com"
:path-parts ["https://google.com"]
:path-params #{}})
(impl/parse "https://google.com"))))
:path-params #{}}
(impl/parse "https://google.com" nil))))

View file

@ -2,16 +2,92 @@
(:require [clojure.test :refer [deftest testing is are]]
[reitit.trie :as trie]))
(deftest into-set-test
(is (= #{} (trie/into-set nil)))
(is (= #{} (trie/into-set [])))
(is (= #{1} (trie/into-set 1)))
(is (= #{1 2} (trie/into-set [1 2 1]))))
(deftest conflicting-paths?-test
(are [c? p1 p2]
(is (= c? (trie/conflicting-paths? p1 p2 nil)))
true "/a" "/a"
true "/a" "/:a"
true "/a/:b" "/:a/b"
true "/ab/:b" "/:a/ba"
true "/*a" "/:a/ba/ca"
true "/a" "/{a}"
true "/a/{b}" "/{a}/b"
true "/ab/{b}" "/{a}/ba"
true "/{*a}" "/{a}/ba/ca"
false "/a" "/:a/b"
false "/a" "/:a/b"
false "/a" "/{a}/b"
false "/a" "/{a}/b"))
(deftest split-path-test
(testing "colon"
(doseq [syntax [:colon #{:colon}]]
(are [path expected]
(is (= expected (trie/split-path path {:syntax syntax})))
"/olipa/:kerran/avaruus", ["/olipa/" (trie/->Wild :kerran) "/avaruus"]
"/olipa/{kerran}/avaruus", ["/olipa/{kerran}/avaruus"]
"/olipa/{a.b/c}/avaruus", ["/olipa/{a.b/c}/avaruus"]
"/olipa/kerran/*avaruus", ["/olipa/kerran/" (trie/->CatchAll :avaruus)]
"/olipa/kerran/{*avaruus}", ["/olipa/kerran/{" (trie/->CatchAll (keyword "avaruus}"))]
"/olipa/kerran/{*valtavan.suuri/avaruus}", ["/olipa/kerran/{" (trie/->CatchAll (keyword "valtavan.suuri/avaruus}"))])))
(testing "bracket"
(doseq [syntax [:bracket #{:bracket}]]
(are [path expected]
(is (= expected (trie/split-path path {:syntax syntax})))
"/olipa/:kerran/avaruus", ["/olipa/:kerran/avaruus"]
"/olipa/{kerran}/avaruus", ["/olipa/" (trie/->Wild :kerran) "/avaruus"]
"/olipa/{a.b/c}/avaruus", ["/olipa/" (trie/->Wild :a.b/c) "/avaruus"]
"/olipa/kerran/*avaruus", ["/olipa/kerran/*avaruus"]
"/olipa/kerran/{*avaruus}", ["/olipa/kerran/" (trie/->CatchAll :avaruus)]
"/olipa/kerran/{*valtavan.suuri/avaruus}", ["/olipa/kerran/" (trie/->CatchAll :valtavan.suuri/avaruus)])))
(testing "both"
(doseq [syntax [#{:bracket :colon}]]
(are [path expected]
(is (= expected (trie/split-path path {:syntax syntax})))
"/olipa/:kerran/avaruus", ["/olipa/" (trie/->Wild :kerran) "/avaruus"]
"/olipa/{kerran}/avaruus", ["/olipa/" (trie/->Wild :kerran) "/avaruus"]
"/olipa/{a.b/c}/avaruus", ["/olipa/" (trie/->Wild :a.b/c) "/avaruus"]
"/olipa/kerran/*avaruus", ["/olipa/kerran/" (trie/->CatchAll :avaruus)]
"/olipa/kerran/{*avaruus}", ["/olipa/kerran/" (trie/->CatchAll :avaruus)]
"/olipa/kerran/{*valtavan.suuri/avaruus}", ["/olipa/kerran/" (trie/->CatchAll :valtavan.suuri/avaruus)])))
(testing "nil"
(doseq [syntax [nil]]
(are [path expected]
(is (= expected (trie/split-path path {:syntax syntax})))
"/olipa/:kerran/avaruus", ["/olipa/:kerran/avaruus"]
"/olipa/{kerran}/avaruus", ["/olipa/{kerran}/avaruus"]
"/olipa/{a.b/c}/avaruus", ["/olipa/{a.b/c}/avaruus"]
"/olipa/kerran/*avaruus", ["/olipa/kerran/*avaruus"]
"/olipa/kerran/{*avaruus}", ["/olipa/kerran/{*avaruus}"]
"/olipa/kerran/{*valtavan.suuri/avaruus}", ["/olipa/kerran/{*valtavan.suuri/avaruus}"]))))
(deftest normalize-test
(are [path expected]
(is (= expected (trie/normalize path)))
(is (= expected (trie/normalize path nil)))
"/olipa/:kerran/avaruus", "/olipa/{kerran}/avaruus"
"/olipa/{kerran}/avaruus", "/olipa/{kerran}/avaruus"
"/olipa/{a.b/c}/avaruus", "/olipa/{a.b/c}/avaruus"
"/olipa/kerran/*avaruus", "/olipa/kerran/{*avaruus}"
"/olipa/kerran/{*avaruus}", "/olipa/kerran/{*avaruus}"
"/olipa/kerran/{*valvavan.suuri/avaruus}", "/olipa/kerran/{*valvavan.suuri/avaruus}"))
"/olipa/kerran/{*valtavan.suuri/avaruus}", "/olipa/kerran/{*valtavan.suuri/avaruus}"))
(deftest tests
(is (= (trie/->Match {} {:a 1})