PartialMatch & route-names

This commit is contained in:
Tommi Reiman 2017-08-19 16:04:13 +03:00
parent 24bb758e99
commit bc2706147c
4 changed files with 91 additions and 23 deletions

View file

@ -96,6 +96,13 @@ The expanded routes:
; ["/api/user/:id" {:name :user/user}]]
```
Route names:
```clj
(reitit/route-names router)
; [:user/ping :user/user]
```
Path-based routing:
```clj
@ -114,8 +121,19 @@ Name-based (reverse) routing:
```clj
(reitit/match-by-name router ::user)
; ExceptionInfo missing path-params for route '/api/user/:id': #{:id}
; #PartialMatch{:template "/api/user/:id",
; :meta {:name :user/user},
; :handler nil,
; :params nil,
; :required #{:id}}
(reitit/partial-match? (reitit/match-by-name router ::user))
; true
```
Only a partial match. Let's provide path-parameters:
```clj
(reitit/match-by-name router ::user {:id "1"})
; #Match{:template "/api/user/:id"
; :meta {:name :user/user}
@ -124,6 +142,13 @@ Name-based (reverse) routing:
; :params {:id "1"}}
```
There is also a exception throwing version:
```
(reitit/match-by-name! router ::user)
; ExceptionInfo missing path-params for route /api/user/:id: #{:id}
```
## Route meta-data
Routes can have arbitrary meta-data. For nested routes, the meta-data is accumulated from root towards leafs using [meta-merge](https://github.com/weavejester/meta-merge).

View file

@ -60,30 +60,54 @@
(cond->> (->> (walk data opts) (map-meta merge-meta))
coerce (into [] (keep #(coerce % opts)))))
(defn name-lookup [[_ {:keys [name]}] opts]
(if name #{name}))
(defn find-names [routes opts]
(into [] (keep #(-> % second :name) routes)))
(defn compile-route [[p m :as route] {:keys [compile] :as opts}]
[p m (if compile (compile route opts))])
(defprotocol Routing
(routes [this])
(route-names [this])
(match-by-path [this path])
(match-by-name [this name] [this name parameters]))
(match-by-name [this name] [this name params]))
(defrecord Match [template meta path handler params])
(defrecord Match [template meta handler params path])
(defrecord PartialMatch [template meta handler params required])
(defn partial-match? [x]
(instance? PartialMatch x))
(defn match-by-name!
([this name]
(match-by-name! this name nil))
([this name params]
(if-let [match (match-by-name this name params)]
(if-not (partial-match? match)
match
(impl/throw-on-missing-path-params
(:template match) (:required match) params)))))
(def default-router-options
{:expand expand
{:lookup name-lookup
:expand expand
:coerce (fn [route _] route)
:compile (fn [[_ {:keys [handler]}] _] handler)})
(defrecord LinearRouter [routes data lookup]
(defrecord LinearRouter [routes names data lookup]
Routing
(routes [_]
routes)
(route-names [_]
names)
(match-by-path [_ path]
(reduce
(fn [acc ^Route route]
(if-let [params ((:matcher route) path)]
(reduced (->Match (:path route) (:meta route) path (:handler route) params))))
(reduced (->Match (:path route) (:meta route) (:handler route) params path))))
nil data))
(match-by-name [_ name]
(if-let [match (lookup name)]
@ -99,19 +123,24 @@
(linear-router routes {}))
([routes opts]
(let [compiled (map #(compile-route % opts) routes)
names (find-names routes opts)
[data lookup] (reduce
(fn [[data lookup] [p {:keys [name] :as meta} handler]]
(let [route (impl/create [p meta handler])]
(let [{:keys [params] :as route} (impl/create [p meta handler])
f #(if-let [path (impl/path-for route %)]
(->Match p meta handler % path)
(->PartialMatch p meta handler % params))]
[(conj data route)
(if name
(assoc lookup name #(->Match p meta (impl/path-for route %) handler %))
lookup)])) [[] {}] compiled)]
(->LinearRouter routes data lookup))))
(if name (assoc lookup name f) lookup)]))
[[] {}] compiled)]
(->LinearRouter routes names data lookup))))
(defrecord LookupRouter [routes data lookup]
(defrecord LookupRouter [routes names data lookup]
Routing
(routes [_]
routes)
(route-names [_]
names)
(match-by-path [_ path]
(data path))
(match-by-name [_ name]
@ -134,13 +163,14 @@
{:route route
:routes routes})))
(let [compiled (map #(compile-route % opts) routes)
names (find-names routes opts)
[data lookup] (reduce
(fn [[data lookup] [p {:keys [name] :as meta} handler]]
[(assoc data p (->Match p meta p handler {}))
[(assoc data p (->Match p meta handler {} p))
(if name
(assoc lookup name #(->Match p meta p handler %))
(assoc lookup name #(->Match p meta handler % p))
lookup)]) [{} {}] compiled)]
(->LookupRouter routes data lookup))))
(->LookupRouter routes names data lookup))))
(defn router
"Create a [[Router]] from raw route data and optionally an options map.

View file

@ -122,12 +122,16 @@
:handler handler})))
(defn path-for [^Route route params]
(when-not (every? #(contains? params %) (:params route))
(if-let [required (:params route)]
(if (every? #(contains? params %) required)
(str "/" (str/join \/ (map #(get (or params {}) % %) (:parts route)))))
(:path route)))
(defn throw-on-missing-path-params [template required params]
(when-not (every? #(contains? params %) required)
(let [defined (-> params keys set)
required (:params route)
missing (clojure.set/difference required defined)]
(throw
(ex-info
(str "missing path-params for route '" (:path route) "': " missing)
{:params params, :required required}))))
(str "/" (str/join \/ (map #(get params % %) (:parts route)))))
(str "missing path-params for route " template ": " missing)
{:params params, :required required})))))

View file

@ -25,11 +25,19 @@
:params {:size "large"}})
(reitit/match-by-name router ::beer {:size "large"})))
(is (= nil (reitit/match-by-name router "ILLEGAL")))
(testing "name-based routing at runtime for missing parameters"
(is (= [::beer] (reitit/route-names router)))
(testing "name-based routing with missing parameters"
(is (= (reitit/map->PartialMatch
{:template "/api/ipa/:size"
:meta {:name ::beer}
:required #{:size}
:params nil})
(reitit/match-by-name router ::beer)))
(is (= true (reitit/partial-match? (reitit/match-by-name router ::beer))))
(is (thrown-with-msg?
ExceptionInfo
#"^missing path-params for route '/api/ipa/:size': \#\{:size\}$"
(reitit/match-by-name router ::beer))))))
#"^missing path-params for route /api/ipa/:size: \#\{:size\}$"
(reitit/match-by-name! router ::beer))))))
(testing "lookup router"
(let [router (reitit/router ["/api" ["/ipa" ["/large" ::beer]]])]
@ -49,6 +57,7 @@
:params {:size "large"}})
(reitit/match-by-name router ::beer {:size "large"})))
(is (= nil (reitit/match-by-name router "ILLEGAL")))
(is (= [::beer] (reitit/route-names router)))
(testing "can't be created with wildcard routes"
(is (thrown-with-msg?
ExceptionInfo