mirror of
https://github.com/metosin/reitit.git
synced 2025-12-18 08:51:12 +00:00
:inject-router? and :inject-match? for ring & http
This commit is contained in:
parent
80dea6cfef
commit
75c4f78f5d
3 changed files with 193 additions and 55 deletions
|
|
@ -73,12 +73,22 @@
|
||||||
(r/router data opts))))
|
(r/router data opts))))
|
||||||
|
|
||||||
(defn routing-interceptor
|
(defn routing-interceptor
|
||||||
"A Pedestal-style routing interceptor that enqueus the interceptors into context."
|
"Creates a Pedestal-style routing interceptor that enqueus the interceptors into context.
|
||||||
[router default-handler {:keys [interceptors executor]}]
|
Takes http-router, default ring-handler and and options map, with the following keys:
|
||||||
|
|
||||||
|
| key | description |
|
||||||
|
| ------------------|-------------|
|
||||||
|
| `:executor` | `reitit.interceptor.Executor` for the interceptor chain
|
||||||
|
| `:interceptors` | Optional sequence of interceptors that are always run before any other interceptors, even for the default handler
|
||||||
|
| `:inject-match?` | Boolean to inject `match` into request under `:reitit.core/match` key (default true)
|
||||||
|
| `:inject-router?` | Boolean to inject `router` into request under `:reitit.core/router` key (default true)"
|
||||||
|
[router default-handler {:keys [interceptors executor inject-match? inject-router?]
|
||||||
|
:or {inject-match? true, inject-router? true}}]
|
||||||
(let [default-handler (or default-handler (fn ([_])))
|
(let [default-handler (or default-handler (fn ([_])))
|
||||||
default-interceptors (->> interceptors
|
default-interceptors (->> interceptors
|
||||||
(map #(interceptor/into-interceptor % nil (r/options router))))
|
(map #(interceptor/into-interceptor % nil (r/options router))))
|
||||||
default-queue (interceptor/queue executor default-interceptors)]
|
default-queue (interceptor/queue executor default-interceptors)
|
||||||
|
enrich-request (ring/create-enrich-request inject-match? inject-router?)]
|
||||||
{:name ::router
|
{:name ::router
|
||||||
:enter (fn [{:keys [request] :as context}]
|
:enter (fn [{:keys [request] :as context}]
|
||||||
(if-let [match (r/match-by-path router (:uri request))]
|
(if-let [match (r/match-by-path router (:uri request))]
|
||||||
|
|
@ -86,10 +96,7 @@
|
||||||
path-params (:path-params match)
|
path-params (:path-params match)
|
||||||
endpoint (-> match :result method)
|
endpoint (-> match :result method)
|
||||||
interceptors (or (:queue endpoint) (:interceptors endpoint))
|
interceptors (or (:queue endpoint) (:interceptors endpoint))
|
||||||
request (-> request
|
request (enrich-request request path-params match router)
|
||||||
(impl/fast-assoc :path-params path-params)
|
|
||||||
(impl/fast-assoc ::r/match match)
|
|
||||||
(impl/fast-assoc ::r/router router))
|
|
||||||
context (assoc context :request request)
|
context (assoc context :request request)
|
||||||
queue (interceptor/queue executor (concat default-interceptors interceptors))]
|
queue (interceptor/queue executor (concat default-interceptors interceptors))]
|
||||||
(interceptor/enqueue executor context queue))
|
(interceptor/enqueue executor context queue))
|
||||||
|
|
@ -103,13 +110,16 @@
|
||||||
"Creates a ring-handler out of a http-router, optional default ring-handler
|
"Creates a ring-handler out of a http-router, optional default ring-handler
|
||||||
and options map, with the following keys:
|
and options map, with the following keys:
|
||||||
|
|
||||||
| key | description |
|
| key | description |
|
||||||
| ----------------|-------------|
|
| ------------------|-------------|
|
||||||
| `:executor` | `reitit.interceptor.Executor` for the interceptor chain
|
| `:executor` | `reitit.interceptor.Executor` for the interceptor chain
|
||||||
| `:interceptors` | Optional sequence of interceptors that are always run before any other interceptors, even for the default handler"
|
| `:interceptors` | Optional sequence of interceptors that are always run before any other interceptors, even for the default handler
|
||||||
|
| `:inject-match?` | Boolean to inject `match` into request under `:reitit.core/match` key (default true)
|
||||||
|
| `:inject-router?` | Boolean to inject `router` into request under `:reitit.core/router` key (default true)"
|
||||||
([router opts]
|
([router opts]
|
||||||
(ring-handler router nil opts))
|
(ring-handler router nil opts))
|
||||||
([router default-handler {:keys [executor interceptors]}]
|
([router default-handler {:keys [executor interceptors inject-match? inject-router?]
|
||||||
|
:or {inject-match? true, inject-router? true}}]
|
||||||
(let [default-handler (or default-handler (fn ([_]) ([_ respond _] (respond nil))))
|
(let [default-handler (or default-handler (fn ([_]) ([_ respond _] (respond nil))))
|
||||||
default-queue (->> [default-handler]
|
default-queue (->> [default-handler]
|
||||||
(concat interceptors)
|
(concat interceptors)
|
||||||
|
|
@ -120,7 +130,9 @@
|
||||||
(dissoc :data) ; data is already merged into routes
|
(dissoc :data) ; data is already merged into routes
|
||||||
(cond-> (seq interceptors)
|
(cond-> (seq interceptors)
|
||||||
(update-in [:data :interceptors] (partial into (vec interceptors)))))
|
(update-in [:data :interceptors] (partial into (vec interceptors)))))
|
||||||
router (reitit.http/router (r/routes router) router-opts)]
|
router (reitit.http/router (r/routes router) router-opts)
|
||||||
|
enrich-request (ring/create-enrich-request inject-match? inject-router?)
|
||||||
|
enrich-default-request (ring/create-enrich-default-request inject-router?)]
|
||||||
(with-meta
|
(with-meta
|
||||||
(fn
|
(fn
|
||||||
([request]
|
([request]
|
||||||
|
|
@ -129,13 +141,10 @@
|
||||||
path-params (:path-params match)
|
path-params (:path-params match)
|
||||||
endpoint (-> match :result method)
|
endpoint (-> match :result method)
|
||||||
interceptors (or (:queue endpoint) (:interceptors endpoint))
|
interceptors (or (:queue endpoint) (:interceptors endpoint))
|
||||||
request (-> request
|
request (enrich-request request path-params match router)]
|
||||||
(impl/fast-assoc :path-params path-params)
|
|
||||||
(impl/fast-assoc ::r/match match)
|
|
||||||
(impl/fast-assoc ::r/router router))]
|
|
||||||
(or (interceptor/execute executor interceptors request)
|
(or (interceptor/execute executor interceptors request)
|
||||||
(interceptor/execute executor default-queue request)))
|
(interceptor/execute executor default-queue request)))
|
||||||
(interceptor/execute executor default-queue (impl/fast-assoc request ::r/router router))))
|
(interceptor/execute executor default-queue (enrich-default-request request))))
|
||||||
([request respond raise]
|
([request respond raise]
|
||||||
(let [default #(interceptor/execute executor default-queue % respond raise)]
|
(let [default #(interceptor/execute executor default-queue % respond raise)]
|
||||||
(if-let [match (r/match-by-path router (:uri request))]
|
(if-let [match (r/match-by-path router (:uri request))]
|
||||||
|
|
@ -143,10 +152,7 @@
|
||||||
path-params (:path-params match)
|
path-params (:path-params match)
|
||||||
endpoint (-> match :result method)
|
endpoint (-> match :result method)
|
||||||
interceptors (or (:queue endpoint) (:interceptors endpoint))
|
interceptors (or (:queue endpoint) (:interceptors endpoint))
|
||||||
request (-> request
|
request (enrich-request request path-params match router)
|
||||||
(impl/fast-assoc :path-params path-params)
|
|
||||||
(impl/fast-assoc ::r/match match)
|
|
||||||
(impl/fast-assoc ::r/router router))
|
|
||||||
respond' (fn [response]
|
respond' (fn [response]
|
||||||
(if response
|
(if response
|
||||||
(respond response)
|
(respond response)
|
||||||
|
|
@ -154,7 +160,7 @@
|
||||||
(if interceptors
|
(if interceptors
|
||||||
(interceptor/execute executor interceptors request respond' raise)
|
(interceptor/execute executor interceptors request respond' raise)
|
||||||
(default request)))
|
(default request)))
|
||||||
(default (impl/fast-assoc request ::r/router router))))
|
(default (enrich-default-request request))))
|
||||||
nil))
|
nil))
|
||||||
{::r/router router}))))
|
{::r/router router}))))
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -226,20 +226,54 @@
|
||||||
(not-found-handler request)))))]
|
(not-found-handler request)))))]
|
||||||
(create handler)))))
|
(create handler)))))
|
||||||
|
|
||||||
|
(defn create-enrich-request [inject-match? inject-router?]
|
||||||
|
(cond
|
||||||
|
(and inject-match? inject-router?)
|
||||||
|
(fn enrich-request [request path-params match router]
|
||||||
|
(-> request
|
||||||
|
(impl/fast-assoc :path-params path-params)
|
||||||
|
(impl/fast-assoc ::r/match match)
|
||||||
|
(impl/fast-assoc ::r/router router)))
|
||||||
|
inject-router?
|
||||||
|
(fn enrich-request [request path-params _ router]
|
||||||
|
(-> request
|
||||||
|
(impl/fast-assoc :path-params path-params)
|
||||||
|
(impl/fast-assoc ::r/router router)))
|
||||||
|
inject-match?
|
||||||
|
(fn enrich-request [request path-params match _]
|
||||||
|
(-> request
|
||||||
|
(impl/fast-assoc :path-params path-params)
|
||||||
|
(impl/fast-assoc ::r/match match)))
|
||||||
|
:else
|
||||||
|
(fn enrich-request [request path-params _ _]
|
||||||
|
(-> request
|
||||||
|
(impl/fast-assoc :path-params path-params)))))
|
||||||
|
|
||||||
|
(defn create-enrich-default-request [inject-router?]
|
||||||
|
(if inject-router?
|
||||||
|
(fn enrich-request [request router]
|
||||||
|
(impl/fast-assoc request ::r/router router))
|
||||||
|
identity))
|
||||||
|
|
||||||
(defn ring-handler
|
(defn ring-handler
|
||||||
"Creates a ring-handler out of a router, optional default ring-handler
|
"Creates a ring-handler out of a router, optional default ring-handler
|
||||||
and options map, with the following keys:
|
and options map, with the following keys:
|
||||||
|
|
||||||
| key | description |
|
| key | description |
|
||||||
| --------------|-------------|
|
| ------------------|-------------|
|
||||||
| `:middleware` | Optional sequence of middleware that wrap the ring-handler"
|
| `:middleware` | Optional sequence of middleware that wrap the ring-handler
|
||||||
|
| `:inject-match?` | Boolean to inject `match` into request under `:reitit.core/match` key (default true)
|
||||||
|
| `:inject-router?` | Boolean to inject `router` into request under `:reitit.core/router` key (default true)"
|
||||||
([router]
|
([router]
|
||||||
(ring-handler router nil))
|
(ring-handler router nil))
|
||||||
([router default-handler]
|
([router default-handler]
|
||||||
(ring-handler router default-handler nil))
|
(ring-handler router default-handler nil))
|
||||||
([router default-handler {:keys [middleware]}]
|
([router default-handler {:keys [middleware inject-match? inject-router?]
|
||||||
|
:or {inject-match? true, inject-router? true}}]
|
||||||
(let [default-handler (or default-handler (fn ([_]) ([_ respond _] (respond nil))))
|
(let [default-handler (or default-handler (fn ([_]) ([_ respond _] (respond nil))))
|
||||||
wrap (if middleware (partial middleware/chain middleware) identity)]
|
wrap (if middleware (partial middleware/chain middleware) identity)
|
||||||
|
enrich-request (create-enrich-request inject-match? inject-router?)
|
||||||
|
enrich-default-request (create-enrich-default-request inject-router?)]
|
||||||
(with-meta
|
(with-meta
|
||||||
(wrap
|
(wrap
|
||||||
(fn
|
(fn
|
||||||
|
|
@ -249,24 +283,18 @@
|
||||||
path-params (:path-params match)
|
path-params (:path-params match)
|
||||||
result (:result match)
|
result (:result match)
|
||||||
handler (-> result method :handler (or default-handler))
|
handler (-> result method :handler (or default-handler))
|
||||||
request (-> request
|
request (enrich-request request path-params match router)]
|
||||||
(impl/fast-assoc :path-params path-params)
|
|
||||||
(impl/fast-assoc ::r/match match)
|
|
||||||
(impl/fast-assoc ::r/router router))]
|
|
||||||
(or (handler request) (default-handler request)))
|
(or (handler request) (default-handler request)))
|
||||||
(default-handler (impl/fast-assoc request ::r/router router))))
|
(default-handler (enrich-default-request request))))
|
||||||
([request respond raise]
|
([request respond raise]
|
||||||
(if-let [match (r/match-by-path router (:uri request))]
|
(if-let [match (r/match-by-path router (:uri request))]
|
||||||
(let [method (:request-method request)
|
(let [method (:request-method request)
|
||||||
path-params (:path-params match)
|
path-params (:path-params match)
|
||||||
result (:result match)
|
result (:result match)
|
||||||
handler (-> result method :handler (or default-handler))
|
handler (-> result method :handler (or default-handler))
|
||||||
request (-> request
|
request (enrich-request request path-params match router)]
|
||||||
(impl/fast-assoc :path-params path-params)
|
|
||||||
(impl/fast-assoc ::r/match match)
|
|
||||||
(impl/fast-assoc ::r/router router))]
|
|
||||||
((routes handler default-handler) request respond raise))
|
((routes handler default-handler) request respond raise))
|
||||||
(default-handler (impl/fast-assoc request ::r/router router) respond raise))
|
(default-handler (enrich-default-request request) respond raise))
|
||||||
nil)))
|
nil)))
|
||||||
{::r/router router}))))
|
{::r/router router}))))
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
[reitit.impl :as impl]
|
[reitit.impl :as impl]
|
||||||
[reitit.ring :as ring]
|
[reitit.ring :as ring]
|
||||||
[reitit.core :as r])
|
[reitit.core :as r])
|
||||||
(:import (reitit Trie)))
|
(:import (reitit Trie Trie$Matcher)))
|
||||||
|
|
||||||
;;
|
;;
|
||||||
;; start repl with `lein perf repl`
|
;; start repl with `lein perf repl`
|
||||||
|
|
@ -77,35 +77,133 @@
|
||||||
[["/user/:id/profile/:type/" {:get (fn [{{:keys [id type]} :path-params}] (h11 id type))
|
[["/user/:id/profile/:type/" {:get (fn [{{:keys [id type]} :path-params}] (h11 id type))
|
||||||
:put (fn [{{:keys [id type]} :path-params}] (h12 id type))
|
:put (fn [{{:keys [id type]} :path-params}] (h12 id type))
|
||||||
:handler (fn [_] (h1x))}]
|
:handler (fn [_] (h1x))}]
|
||||||
#_["/user/:id/permissions/" {:get (fn [{{:keys [id]} :path-params}] (h21 id))
|
["/user/:id/permissions/" {:get (fn [{{:keys [id]} :path-params}] (h21 id))
|
||||||
:put (fn [{{:keys [id]} :path-params}] (h22 id))
|
:put (fn [{{:keys [id]} :path-params}] (h22 id))
|
||||||
:handler (fn [_] (h2x))}]
|
:handler (fn [_] (h2x))}]
|
||||||
#_["/company/:cid/dept/:did/" {:put (fn [{{:keys [cid did]} :path-params}] (h30 cid did))
|
["/company/:cid/dept/:did/" {:put (fn [{{:keys [cid did]} :path-params}] (h30 cid did))
|
||||||
:handler (fn [_] (h3x))}]
|
:handler (fn [_] (h3x))}]
|
||||||
#_["/this/is/a/static/route" {:put (fn [_] (h40))
|
["/this/is/a/static/route" {:put (fn [_] (h40))
|
||||||
:handler (fn [_] (h4x))}]])
|
:handler (fn [_] (h4x))}]])
|
||||||
(fn [_] (hxx))))
|
(fn [_] (hxx))))
|
||||||
|
|
||||||
#_(let [request {:request-method :put
|
#_(let [request {:request-method :put
|
||||||
:uri "/this/is/a/static/route"}]
|
:uri "/this/is/a/static/route"}]
|
||||||
(handler-reitit request)
|
(handler-reitit request)
|
||||||
(cc/quick-bench
|
(cc/quick-bench
|
||||||
(handler-reitit request)))
|
(handler-reitit request)))
|
||||||
|
|
||||||
(let [request {:request-method :get
|
(let [request {:request-method :get
|
||||||
:uri "/user/1234/profile/compact/"}]
|
:uri "/user/1234/profile/compact/"}]
|
||||||
(handler-reitit request)
|
;; OLD: 1338ns
|
||||||
;; OLD: 1338ns
|
;; NEW: 981ns
|
||||||
;; NEW: 981ns
|
;; JAVA: 805ns
|
||||||
|
;; NO-INJECT: 704ns
|
||||||
#_(cc/quick-bench
|
#_(cc/quick-bench
|
||||||
(handler-reitit request)))
|
(handler-reitit request))
|
||||||
|
(handler-reitit request))
|
||||||
|
|
||||||
|
(comment
|
||||||
|
(impl/segments "/user/1234/profile/compact")
|
||||||
|
;; 145ns
|
||||||
|
(cc/quick-bench
|
||||||
|
(impl/segments "/user/1234/profile/compact")))
|
||||||
|
|
||||||
|
(comment
|
||||||
|
(Trie/split "/user/1234/profile/compact")
|
||||||
|
;; 91ns
|
||||||
|
(cc/quick-bench
|
||||||
|
(Trie/split "/user/1234/profile/compact")))
|
||||||
|
|
||||||
|
(comment
|
||||||
|
(let [router (r/router ["/user/:id/profile/:type"])]
|
||||||
|
(cc/quick-bench
|
||||||
|
(r/match-by-path router "/user/1234/profile/compact"))))
|
||||||
|
|
||||||
|
(let [lookup ^Trie$Matcher (Trie/sample)]
|
||||||
|
(Trie/lookup lookup "/user/1234/profile/compact")
|
||||||
|
#_(cc/quick-bench
|
||||||
|
(Trie/lookup lookup "/user/1234/profile/compact")))
|
||||||
|
|
||||||
|
(let [router (r/router [["/user/:id" ::1]
|
||||||
|
["/user/:id/permissions" ::2]
|
||||||
|
["/company/:cid/dept/:did" ::3]
|
||||||
|
["/this/is/a/static/route" ::4]])]
|
||||||
|
#_(cc/quick-bench
|
||||||
|
(r/match-by-path router "/user/1234/profile/compact"))
|
||||||
|
(r/match-by-path router "/user/1234"))
|
||||||
|
|
||||||
|
;; 281ns
|
||||||
|
(let [router (r/router [["/user/:id/profile/:type" ::1]
|
||||||
|
["/user/:id/permissions" ::2]
|
||||||
|
["/company/:cid/dept/:did" ::3]
|
||||||
|
["/this/is/a/static/route" ::4]])]
|
||||||
|
#_(cc/quick-bench
|
||||||
|
(r/match-by-path router "/user/1234/profile/compact"))
|
||||||
|
(r/match-by-path router "/user/1234/profile/compact"))
|
||||||
|
|
||||||
|
(read-string
|
||||||
|
(str
|
||||||
|
(.matcher
|
||||||
|
(doto (Trie.)
|
||||||
|
(.add "/user" 1)
|
||||||
|
#_(.add "/user/id/permissions" 2)
|
||||||
|
(.add "/user/id/permissions2" 3)))))
|
||||||
|
|
||||||
|
(Trie/lookup
|
||||||
|
(.matcher
|
||||||
|
(doto (Trie.)
|
||||||
|
(.add "/user/1" 1)
|
||||||
|
(.add "/user/1/permissions" 2)))
|
||||||
|
"/user/1")
|
||||||
|
|
||||||
|
(.matcher
|
||||||
|
(doto (Trie.)
|
||||||
|
(.add "/user/1" 1)
|
||||||
|
(.add "/user/1/permissions" 2)))
|
||||||
|
|
||||||
|
;; 137ns
|
||||||
|
(let [m (.matcher
|
||||||
|
(doto (Trie.)
|
||||||
|
(.add "/user/:id/profile/:type" 1)))]
|
||||||
|
#_(cc/quick-bench
|
||||||
|
(Trie/lookup m "/user/1234/profile/compact"))
|
||||||
|
(Trie/lookup m "/user/1234/profile/compact"))
|
||||||
|
|
||||||
(comment
|
(comment
|
||||||
|
|
||||||
|
(let [matcher ^Trie$Matcher (Trie/sample)]
|
||||||
|
(Trie/lookup matcher "/user/1234/profile/compact")
|
||||||
|
(cc/quick-bench
|
||||||
|
(Trie/lookup matcher "/user/1234/profile/compact")))
|
||||||
|
|
||||||
|
;; 173ns
|
||||||
|
(let [lookup ^Trie$Matcher (Trie/tree2)]
|
||||||
|
(Trie/lookup lookup "/user/1234/profile/compact")
|
||||||
|
(cc/quick-bench
|
||||||
|
(Trie/lookup lookup "/user/1234/profile/compact")))
|
||||||
|
|
||||||
|
|
||||||
|
;; 140ns
|
||||||
|
(let [lookup ^Trie$Matcher (Trie/tree1)]
|
||||||
|
(Trie/lookup lookup "/user/1234/profile/compact")
|
||||||
|
(cc/quick-bench
|
||||||
|
(Trie/lookup lookup "/user/1234/profile/compact")))
|
||||||
|
|
||||||
;; 849ns (clojure, original)
|
;; 849ns (clojure, original)
|
||||||
;; 599ns (java, initial)
|
;; 599ns (java, initial)
|
||||||
;; 810ns (linear)
|
;; 173ns (fast split)
|
||||||
(let [router (r/router ["/user/:id/profile/:type"])]
|
(let [router (r/router ["/user/:id/profile/:type"])]
|
||||||
|
(r/match-by-path router "/user/1234/profile/compact")
|
||||||
|
(cc/quick-bench
|
||||||
|
(r/match-by-path router "/user/1234/profile/compact")))
|
||||||
|
|
||||||
|
;; 849ns (clojure, original)
|
||||||
|
;; 599ns (java, initial)
|
||||||
|
;; 173ns (java, optimized)
|
||||||
|
(let [router (r/router [["/user/:id/profile/:type/" ::1]
|
||||||
|
["/user/:id/permissions/" ::2]
|
||||||
|
["/company/:cid/dept/:did/" ::3]
|
||||||
|
["/this/is/a/static/route" ::4]])]
|
||||||
(cc/quick-bench
|
(cc/quick-bench
|
||||||
(r/match-by-path router "/user/1234/profile/compact")))
|
(r/match-by-path router "/user/1234/profile/compact")))
|
||||||
|
|
||||||
|
|
@ -129,10 +227,16 @@
|
||||||
|
|
||||||
(import '[reitit Util])
|
(import '[reitit Util])
|
||||||
|
|
||||||
#_(cc/quick-bench
|
(comment
|
||||||
(Trie/split "/this/is/a/static/route"))
|
(Util/matchURI "/user/1234/profile/compact/" ["/user/" :id "/profile/" :type "/"])
|
||||||
|
(cc/quick-bench
|
||||||
|
(Util/matchURI "/user/1234/profile/compact/" ["/user/" :id "/profile/" :type "/"]))
|
||||||
|
|
||||||
(Util/matchURI "/user/1234/profile/compact/" ["/user/" :id "/profile/" :type "/"])
|
(cc/quick-bench
|
||||||
|
(Trie/split "/user/1234/profile/compact/"))
|
||||||
|
|
||||||
|
(cc/quick-bench
|
||||||
|
(.split "/user/1234/profile/compact/" "/" 666)))
|
||||||
|
|
||||||
(import '[reitit Segment2])
|
(import '[reitit Segment2])
|
||||||
|
|
||||||
|
|
@ -161,7 +265,7 @@
|
||||||
(segment/lookup segment "/user/1/permissions/"))))
|
(segment/lookup segment "/user/1/permissions/"))))
|
||||||
|
|
||||||
#_(cc/quick-bench
|
#_(cc/quick-bench
|
||||||
(Trie/split "/user/1/profile/compat"))
|
(Trie/split "/user/1/profile/compat"))
|
||||||
|
|
||||||
#_(Trie/split "/user/1/profile/compat")
|
#_(Trie/split "/user/1/profile/compat")
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue