metosin / reitit Goto Github PK
View Code? Open in Web Editor NEWA fast data-driven routing library for Clojure/Script
Home Page: https://cljdoc.org/d/metosin/reitit/
License: Eclipse Public License 1.0
A fast data-driven routing library for Clojure/Script
Home Page: https://cljdoc.org/d/metosin/reitit/
License: Eclipse Public License 1.0
Either an example or a real implementation with accumulated :middleware
+ :handler
compiled into Ring-handler.
Currently, in compojure-api, all response bodies with clojure collections are automatically encoded into a format negotiated with the use (usin Muuntaja). If a return/response data is defined, ALL DATA is encoded.
response is not encoded:
(GET “/ping” []
(ok))
response is encoded:
(GET “/ping” []
:return (s/maybe s/Str)
(ok))
currently, in reitit-ring
with muuntaja, only collections are encoded. Returning nil
from an endpoint will cause the data not to be encoded: no content-type is set and the body is an empty string. This blows up the client parsers.
;; yes
["/ping" {:handler (constantly {:status 500 :body {:a 1}})}]
;; yes
["/ping" {:handler (constantly {:status 200})}]
;; yes
["/ping" {:handler (constantly {:status 500 :body {:a 1}})}]
;; no
["/ping" {:handler (constantly {:status 200})}]
nil
;; yes
["/ping" {:handler (constantly {:status 500 :body {:a 1}})}]
;; yes
["/ping" {:handler (constantly {:status 200})}]
;; no
["/ping" {:handler (constantly {:status 500 :body 123})}]
:responses
is defined in route data;; yes
["/ping" {:handler (constantly {:status 500 :body {:a 1}})}]
;; yes
["/ping" {:responses {200 {:body any?}}
:handler (constantly {:status 500})}]
;; no
["/ping" {:handler (constantly {:status 200})}]
:responses
is defined the returned status;; yes
["/ping" {:handler (constantly {:status 500 :body {:a 1}})}]
;; no
["/ping" {:responses {200 {:body any?}}
:handler (constantly {:status 500})}]
;; no
["/ping" {:handler (constantly {:status 200})}]
Like ring-router but with enchanced pedestal-style interceptors. Related to #10. We don't need a interceptor interpreter here, can do that later.
Can't make the Interceptors truely portable without changes to Pedestal (context keys (queue, error etc) keys are namespaced in P.
Current draft:
(require '[reitit.http.coercion :as rhc])
(require '[reitit.http :as http])
(require '[reitit.coercion.spec])
(require '[clojure.set :as set])
(def auth-interceptor
"Interceptor that mounts itself if route has `:roles` data. Expects `:roles`
to be a set of keyword and the context to have `[:user :roles]` with user roles.
responds with HTTP 403 if user doesn't have the roles defined, otherwise no-op."
{:name ::auth
:compile (fn [{:keys [roles]} _]
(if (seq roles)
{:description (str "requires roles " roles)
:spec {:roles #{keyword?}}
:context-spec {:user {:roles #{keyword}}}
:enter (fn [{{user-roles :roles} :user :as ctx}]
(if (not (set/subset? roles user-roles))
(assoc ctx :response {:status 403, :body "forbidden"})
ctx))}))})(require '[clojure.set :as set])
(def app
(http/http-handler
(http/router
["/api" {:interceptors [auth-interceptor]}
["/ping" {:name ::ping
:get (constantly
{:status 200
:body "pong"})}]
["/plus/:z" {:name ::plus
:post {:parameters {:query {:x int?}
:body {:y int?}
:path {:z int?}}
:responses {200 {:body {:total pos-int?}}}
:roles #{:admin}
:handler (fn [{:keys [parameters]}]
(let [total (+ (-> parameters :query :x)
(-> parameters :body :y)
(-> parameters :path :z))]
{:status 200
:body {:total total}}))}}]]
{:data {:coercion reitit.coercion.spec/coercion
:interceptors [rhc/coerce-exceptions-interceptor
rhc/coerce-request-interceptor
rhc/coerce-response-interceptor]}})))
Either an example or a real implementation with accumulated :interceptor
&/ :handler
meta-data to create a Pedestal-style routing interceptor.
interactive like the swagger-ui, but built in cljs and for reitit internal data model: routes, middleware/interceptors and route data.
Either an example or a real implementation for browser-app routing.
Like the Pedestal prefix-tree router does. This would open up nice possibilities to optimise mixed trees with both wildcard & wildcard-free routes: the free-ones could be lookup-matched first. Also, if prefix-tree router is imported, we should test it's (ported) perf too. Might be best if tree has lot's of wildcard-routes...
I'd be useful to have coercion to work with reitit/match-by-name
. One example is conforming keyword to a string path parameter.
Related spec-tools
issue: metosin/spec-tools#112
(r/match-by-name
(r/router
["/olipa/:kerran" ::avaruus])
::avaruus
{:kerran :joskus})
;#Match{:template "/olipa/:kerran",
; :data {:name :user/avaruus},
; :result nil,
; :path-params {:kerran :joskus}, <-- wrong
; :path "/olipa/:joskus"} <-- wrong
separate ns so there is no dependency to spec.
This spans over multiple repos, but can be done in parts.
spec-tools.swagger
overriding some JSON Schema parts (if any!)(require '[reitit.ring :as ring])
(require '[reitit.ring.coercion])
(require '[reitit.coercion.spec])
(require '[muuntaja.middleware])
(ring/ring-handler
(ring/router
["/plus"
{:get {:parameters {:body {:x int?, :y int?}}
:responses {200 {:body {:total pos-int?}}}
:handler (fn [{{{:keys [x y]} :body} :parameters}]
{:status 200, :body (+ x y)})}}]
{:data {:middleware [muuntaja.middleware/wrap-format
reitit.ring.coercion/coerce-request-middleware
reitit.ring.coercion/coerce-response-middleware]
:coercion reitit.coercion.spec/coercion}}))
:content
(from OpenAPI3) which allows content-type->model mappings.(ring/ring-handler
(ring/router
["/plus"
{:get {:parameters {:content {"application/json" {:x neg-int?, :y neg-int?}
"application/edn" {:x pos-int?, :y pos-int?}}}
:responses {200 {:content {"application/json" {:json int?}
"application/edn" {:edn int?}}}}
:handler (fn [{{{:keys [x y]} :body} :parameters :as request}]
(let [content-type (-> request :muuntaja/response :format)
body (case content-type
"application/json" {:json (+ x y)}
"application/edn" {:edn (+ x y)})]
{:status 200, :body body}))}}]
{:data {:middleware [muuntaja.middleware/wrap-format
reitit.ring.coercion/coerce-request-middleware
reitit.ring.coercion/coerce-response-middleware]
:coercion reitit.coercion.spec/coercion}}))
... having both :body
and :content
could be allowed, the :body
could be used as a default if the content-type doesn't match.
should be configurable. Maybe via route data / router options (so that setting it once would effect all places).
Setting nil
is not good here, as it's in the branch where the route has matched -> returning nil
would cause 406 (not-acceptable).
https://github.com/metosin/reitit/blob/master/modules/reitit-ring/src/reitit/ring.cljc#L116
It could be useful to be able to reference Middleware by their distinct names instead of instances. Names would be looked from explicit registry. Registry could be a map (name -> middleware) or via a registry construcing function , consuming a vector of Middleware
instances. :name
of Middleware
could be used as a key.
Something like:
(extend-protocol IntoMiddleware
#?(:clj clojure.lang.Keyword
:cljs cljs.core.Keyword)
(into-middleware [this _ {:keys [middleware-registry]}]
(into-middleware (middleware-registry this))))
allowing:
(require '[reitit.ring :as ring])
(ring/ring-router
["/api" {:middleware [::api-format]}
["/ping" (constantly {:status 200, :body {:ping "pong"}})]]
{:middleware-registry {::api-format ....})
Related: macchiato-framework/macchiato-core#9
We could extract information from middleware and publish them to handler they are linked to. Kekkonen does this for interceptors already, allowing input & output parameters to be read from interceptors.
As a bigger solution, it should be possible to accumulate any data from middleware into handlers, for example, all keys with namespace middleware
. Middleware wouldn't even need to have the :wrap
defined, just accumulate data. Sweet.
(def wrap-load-user-from-token
"add's requirement for a `:x-token` header parameter,
publishes `:user` key under request"
(middleware/create
{:name ::wrap-load-user
:middleware/parameters {:header {:x-token string?}}
:provides {:user}
:wrap ...}))
Still, not sure if it's a good idea. Accumulating data from 3rd party middleware into route-tree...
Proposal:
metosin/reitit-swagger
(and metosin/reitit-openapi
) module:responses
and :parameters
data is read from coercion:no-docs
(disable docs), :swagger
(any swagger-data)swagger-middleware
that doesn't participate in request processing but provides specs for the new keys for route data validationswagger-spec-handler
that produces the swagger-spec[:swagger :id]
defines the api(docs). All routes with the same id are part of the apidcs
Draft with data-specs:
(require '[reitit.ring :as ring])
(require '[reitit.ring.swagger :as swagger])
(require '[reitit.ring.coercion :as rrc])
(require '[reitit.coercion.spec :as spec])
(def app
(ring/ring-handler
(ring/router
["/api"
;; identify a swagger api
;; there can be several in a routing tree
{:swagger {:id :math}}
;; the (undocumented) swagger spec endpoint
["/swagger.json"
{:get {:no-doc true
:swagger {:info {:title "my-api"}}
:handler swagger/swagger-spec-handler}}]
["/minus"
{:get {:summary "minus"
:parameters {:query {:x int?, :y int?}}
:responses {200 {:body {:total int?}}}
:handler (fn [{{{:keys [x y]} :query} :parameters}]
{:status 200, :body {:total (- x y)}})}}]
["/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 [;; does not particiate in request processing
;; just defines specs for the extra keys
swagger/swagger-middleware
rrc/coerce-exceptions-middleware
rrc/coerce-request-middleware
rrc/coerce-response-middleware]
:coercion spec/coercion}})))
Router with coercion enabled needs configuration keys :extract-request-format
and :extract-response-format
options on where to find the :body
content-type to select the coercion. e.g. for "application/json" a json-coercion is used, for "application/edn" just validation is applied.
There should be a some defaults for this. Ideas:
muuntaja.core
to extract these?:extract-request-format (comp :format :muuntaja/request)
:extract-response-format (comp :format :muuntaja/response)
:configure
key to Middleware, allowing mounted middleware to modify the configuration for the routes it's attached to.:configure
key and would add those keys automatically.After setting the handler option :config {:jsonEditor true}
body parameters should be expanded into individual fields instead of one JSON input.
Even with the option set, Swagger UI displays the parameters in one JSON textfield.
With most Clojure ring-web libs, one can return anything from a handler and it get's coerced into a valid Ring Response:
Reitit-ring should support something like this too, as otherwise, one has to always return a valid ring response map from handlers.
IntoResponse
and a middleware in reitit.ring
to do thisOptions how to integrate:
ring-router
ring-router
options ::ring/wrap-handler
?::middleware/transform
option)e.g. validate keys, compile into handler etc. Like expand
but coerce
?
Where should be host ring-middleware that have been wrapped as data-driven Middleware
? For example, data-driven version of Muuntaja Middleware
should done one of the following:
:produces
& :consumes
):produces
& :consumes
or just :formats
)Alternatives:
reitit-middleware
module with most of the common things within (params, security, headers, body, cors etc)From https://github.com/julienschmidt/httprouter :
| A compressing dynamic trie (radix tree) structure is used for efficient matching.
Similar implementation in pedestal:
It seems that the thing that is returned by reitit.ring/ring-handler
doesn't conform to the :ring/handler spec:
(s/explain :ring/handler (rr/ring-handler (rr/router ["" #(prn "")])))
In: [:args 0] val: :sync fails spec: :ring/request at: [:sync+async :fn :sync :args :request] predicate: map?
In: [:args 0] val: :sync fails spec: :ring/request at: [:sync+async :fn :async :args :request] predicate: map?
In: [:ret] val: [:async nil] fails spec: :ring/response at: [:sync+async :fn :sync :ret] predicate: map?
val: nil fails spec: :ring/response at: [:sync :ret] predicate: map?
val: ({:server-port 1, :server-name "", :remote-addr "", :uri "/", :scheme :http, :protocol "HTTP/1.1", :headers {}, :request-method :a} #object[clojure.spec.alpha$fspec_impl$reify__2451$fn__2454 0x658e90f6 "clojure.spec.alpha$fspec_impl$reify__2451$fn__2454@658e90f6"] #object[clojure.spec.alpha$fspec_impl$reify__2451$fn__2454 0x343045e0 "clojure.spec.alpha$fspec_impl$reify__2451$fn__2454@343045e0"]) fails spec: :ring.async/handler at: [:async] predicate: (apply fn), Assert failed: In: [0] val: nil fails spec: :ring/response at: [:response] predicate: map?
(pvalid? argspec args)
This is a simple example to show the issue, I also see the problem with a more complex (and working) ring handler.
Just a sample code or a separate ns for it?
(require '[reitit.middleware :as middleware])
(extend-protocol middleware/IntoMiddleware
#?(:clj clojure.lang.Fn
:cljs function)
(into-middleware [this _ _]
(let [macchiato-meta (-> this meta :macchiato/middleware)]
(middleware/map->Middleware
(merge
macchiato-meta
{:wrap this})))))
related: macchiato-framework/macchiato-core#6
adding things like :info
to top-level route data causes the data to be merged to all endpoints, effecting broken swagger-data. Options:
I want to use swagger-ui in reitit.
First, I try
git clone https://github.com/metosin/reitit/
cd reitit/examples/ring-swagger
lein repl
=> (start)
and access http://localhost:3000/api-docs/ .
But browser return "not found"
Accessing http://localhost:3000 returns correct swagger-ui's page, but it isn't referring api/swagger.json but referring localhost:3000/api/docs
Both extra & missing backslashes should be handled somehow - fast & easy to use. Maybe new router option(s)?
Currently, for a router:
(r/router
[["/dashboard" ::dashboard]
["/files/*" ::files]])
Both /dashboard/
and /files
fail
httprouter has some logic to handle these.
Currently, clients can either strip or add a training /
if a match is not found, but we should do better.
I want to match "/users" -> :user/list and "/users/123" -> :user/detail 123, but Reitit does not seem to string an empty string as a terminating match. For example:
(r/router
["/"
["users/"
["" :user/list]
[":id" :user/detail]]])
=>
clojure.lang.ExceptionInfo: Router contains conflicting routes:
/users/
-> /users/:id
However, it works if I differentiate with a trailing slash:
(r/router
["/"
["users" :user/list]
["users/:id" :user/detail]]) ;; note slash differentiates
But now "/users/" won't match to :user/list.
If that's allowed, I would expect this to work:
(r/router
["/"
["users/" :user/list] ;; note slash here
["users/:id" :user/detail]])
=>
clojure.lang.ExceptionInfo: Router contains conflicting routes:
/users/
-> /users/:id
But :id matches on an empty string. I tried using a regex without luck.
Then I tried:
(r/router
["/"
["users/list" :user/list]
["users/:id" :user/detail]])
=>
clojure.lang.ExceptionInfo: Router contains conflicting routes:
/users/list
-> /users/:id
How do I constrain a parameter?
If all routes are wildcard-free, router
should be just a hash lookup.
Currently, reitit.coercion/coerce!
coerces just :path-parameters
. We could add support to coerce also :query-parameters
as it's a common case in routing. :query-parameters
could be injeted into Match
or passed as a optional second argument into coerce!
.
Currently:
(defn coerce!
"Returns a map of coerced input parameters using pre-compiled
coercers under `:result` (provided by [[compile-request-coercers]].
If coercion or parameters are not defined, return `nil`"
[match]
(if-let [result (:result match)]
(coerce-request result {:path-params (:path-params match)})))
WIth both frontend & backend routing.
The README is quite bloated. Should contain just a sample what the lib provides. The actual docs could be done with Gitbook? Sample: https://diode.suzaku.io/
Examples like https://github.com/metosin/reitit#meta-data-based-extensions inspect the route :match
at runtime. As the middleware/interceptor - chain is known at router creation time, we can compile the middleware/interceptors in advance for bette perf: captured values via closures are a order of magnitude faster to access than map lookups.
need to figure out the syntax for this:
;; function
{:middleware [#(wrap % :kikka)]}
;; generator
{:middleware [[wrap :kikka]}
;; generator with a special marker to inject the match
{:middleware [[ring/coerce-parameters :spec ::ring/match]]}
;; generator-generator: match => params => middleware
{:middleware [(ring/middleware ring/coerce-parameters [:spec]]}
Many routing components (router, middleware, interceptors) can contribute to route data structure. All should be able to contribute partial clojure.spec
s which should be merged to get the effective spec for route data.
e.g.
ring-handler
defines keys like :get
, :post
, :middleware
etc.:parameters
and :responses
::role
=> effective spec from route would be a s/merge
d s/keys
specs, with the keys: :get
, :post
, :middleware
, :parameters
, :responses
.
=> these should be enforced at router creation, optionally failing on invalid keys, optionally with figwheel-style awesome keyword proposals.
For the Ring-router (later for Interceptor-router too). Can be done both via:
The implementation can be mostly copy-pasted from compojure-api.
Or should this belong in a another lib?
$ clj
Clojure 1.9.0
user=> (require '[reitit.core :as r])
nil
(def routes
[["/ping"]
["/:user-id/orders"]
["/bulk/:bulk-id"]
["/public/*path"]
["/:version/status"]])
#'user/routes
user=> (r/router routes)
ExceptionInfo Router contains conflicting routes:
/:user-id/orders
-> /public/*path
-> /bulk/:bulk-id
/bulk/:bulk-id
-> /:version/status
/public/*path
-> /:version/status
clojure.core/ex-info (core.clj:4739)
(r/router
routes
{:conflicts nil})
ExceptionInfo Router contains conflicting routes:
/:user-id/orders
-> /public/*path
-> /bulk/:bulk-id
/bulk/:bulk-id
-> /:version/status
/public/*path
-> /:version/status
clojure.core/ex-info (core.clj:4739)
It looks like if I instead pass an fn, e.g. {:conflicts (constantly nil)}
as options, it works.
reitit version 0.1.3
e.g. reverse routing.
Middleware could have keys like :provides
and :requires
with common pseudo-values like :parsed-body
, :catch-exceptions
or project-spesific ::authenticated-user
etc. Middleware-chain could be automatically constructed based on these or if some dependency is missing, the router can't be constructed (with descriptive message).
Like Anger-interceptor but for Middleware.
Not sure if this is a good feature thou.
If implemented, should work together with Macchiato's middleware resolution.
should be enforced & documented, see 5e5345b
Trying to swap out Compojure and Bidi for Reitit.
How should I go about serving resource paths or static file paths with Reitit? Compojure has resources
which calls its resource-response and add-mime-type helpers.
Is there an equivalent in Reitit?
metosin/reitit-schema
when api-docs are generated, there should be enough helpers to provide example output based on the spec (generators).
Apologies if I'm just doing something stupid, but in your ring-swagger
example, if I just change these lines:
- {:get {:summary "plus with spec"
- :parameters {:query {:x int?, :y int?}}
+ {:post {:summary "plus with spec"
+ :parameters {:body {:x keyword?, :y int?}}
:responses {200 {:body {:total int?}}}
:handler (fn [{{{:keys [x y]} :query} :parameters}]
{:status 200
- :body {:total (+ x y)}})}}]]
+ :body {:total (+ 1 y)}})}}]]
I get a spec error:
{
"spec": "(spec-tools.core/spec {:spec (clojure.spec.alpha/keys :req-un [:$spec10430/x :$spec10430/y]), :type :map, :keys #{:y :x}, :keys/req #{:y :x}})",
"problems": [
{
"path": [
"x"
],
"pred": "clojure.core/keyword?",
"val": "string",
"via": [
"$spec10430/x"
],
"in": [
"x"
]
}
],
"type": "reitit.coercion/request-coercion",
"coercion": "spec",
"value": {
"x": "string",
"y": 0
},
"in": [
"request",
"body-params"
]
}
Changing the :body
to :query
fixes the problem (and I get a keyword as expected). So coercion is working, just not for :body
?
Attaching the Swagger UI showing the data posted and the response.
It's also needed in the ClojureScript routing. r.g.
(router
[["" ::frontpage]
["/users/:user-id" {:name ::user
:parameters {:user-id int?}}]]
{:coercion reitit.coercion.spec/SpecCoercion})
ring-handler doesn't always call respond or raise.
http://localhost:3000/index.html/index.html/index.html/index.html/index.html/index.html/index.html/index.html/index.html/index.html/index.html/index.html/index.html/index.html/index.html/index.html/index.html/index.html/index.html/index.html
end up not serving at all
Query and fragment strings cause wierd values for path parameters at the end of URLs, and cause routes to fail to match. In my opinion, path and fragment strings should either be coerced as :query-params
and :fragment-id
, extracted as :query-string
and :fragment-string
, or at least trimmed and ignored prior to matching. :path-params
shouldn't include the query string appended to the final parameter. I am unsure of whether the :path
key should trim or retain the query string.
(reitit/match-by-path
(reitit/router [["/foo" :foo] ["/bar/:id" :bar]])
"/bar/100?a=b&c=d#foo")
;; Returns:
#_
#reitit.core.Match{:template "/bar/:id",
:data {:name :bar},
:result nil,
:path-params {:id "100?a=b&c=d#foo"},
:path "/bar/100?a=b&c=d#foo"}
(reitit/match-by-path
(reitit/router [["/foo" :foo] ["/bar/:id/baz" :bar]])
"/bar/100/baz?a=b&c=d#foo")
;;Returns:
#_
nil
Using the code from the example and printing the payload, the header for accept: application/json
is correctly changed, but the new Authorization
header is not passed through to headers.
Handler Code
(defn handler [r]
(clojure.pprint/pprint r)
{:status 200 :body "ok"})
(def app
(ring/ring-handler
(ring/router
["/ping" {:get {:handler handler}}]
{:data {:middleware [ring.middleware.params/wrap-params
muuntaja.middleware/wrap-format
rrc/coerce-exceptions-middleware
rrc/coerce-request-middleware
rrc/coerce-response-middleware]}})))
curl command
curl -v -H "Accept: application/json" -H "Authorization Token=...." http://0.0.0.0:8080/ping
headers value from print
:headers
{"accept" "application/json",
"user-agent" "curl/7.56.1",
"host" "0.0.0.0:8080"},
Right now, reitit
doesn't seem to have direct support for the 404 scenario. What I'm doing is creating a catch-all route, and then ignoring route conflicts by setting the :conflicts
opt.
For example:
(def router
(ring/ring-handler
(ring/router
[["/api"
["/query/:name" hello-n]
["/goodbye" goodbye]
["/hello" hello]]
["/*path" not-found]]
{:conflicts (comp println reitit.core/conflicts-str)})))
The downside of this is that I no longer get nice handy exceptions on route conflicts. :/
It would be nice to maybe have a 404 option in the opts map:
(def router
(ring/ring-handler
(ring/router
["/api"
["/query/:name" hello-n]
["/goodbye" goodbye]
["/hello" hello]]
{:not-found not-found-handler}))) ; Something like this
I can put a pull-request together for this, if you want.
I'm trying to have my router dynamically update when I recompile the sub-routes in another namespace.
I thought that using a var might give me that but it looks like it doesn't.
(r/match-by-path (r/router
["/foo"
#'m/router])
path)
No implementation of method: :expand of protocol:
#'reitit.core/Expand found for class: clojure.lang.Var
I'm not sure this makes sense at all. But that's a concern that might maybe be adressed.
When a Match
is found, it should contain directly a optionally compiled handler to be invoked by the client.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.