mirror of
https://github.com/tokio-rs/axum.git
synced 2026-08-28 00:00:20 +02:00
689 lines
21 KiB
Rust
689 lines
21 KiB
Rust
//! Routing between [`Service`]s and handlers.
|
|
|
|
use self::{future::RouteFuture, not_found::NotFound};
|
|
use crate::{
|
|
body::{boxed, Body, Bytes, HttpBody},
|
|
extract::connect_info::IntoMakeServiceWithConnectInfo,
|
|
response::Response,
|
|
routing::strip_prefix::StripPrefix,
|
|
util::try_downcast,
|
|
BoxError,
|
|
};
|
|
use http::Request;
|
|
use matchit::MatchError;
|
|
use std::{
|
|
borrow::Cow,
|
|
collections::HashMap,
|
|
convert::Infallible,
|
|
fmt,
|
|
marker::PhantomData,
|
|
sync::Arc,
|
|
task::{Context, Poll},
|
|
};
|
|
use tower::{layer::layer_fn, ServiceBuilder};
|
|
use tower_http::map_response_body::MapResponseBodyLayer;
|
|
use tower_layer::Layer;
|
|
use tower_service::Service;
|
|
|
|
pub mod future;
|
|
|
|
mod into_make_service;
|
|
mod method_filter;
|
|
mod method_routing;
|
|
mod not_found;
|
|
mod route;
|
|
mod strip_prefix;
|
|
pub(crate) mod url_params;
|
|
|
|
#[cfg(test)]
|
|
mod tests;
|
|
|
|
pub use self::{into_make_service::IntoMakeService, method_filter::MethodFilter, route::Route};
|
|
|
|
pub use self::method_routing::{
|
|
any, any_service, delete, delete_service, get, get_service, head, head_service, on, on_service,
|
|
options, options_service, patch, patch_service, post, post_service, put, put_service, trace,
|
|
trace_service, MethodRouter,
|
|
};
|
|
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
|
struct RouteId(u32);
|
|
|
|
impl RouteId {
|
|
fn next() -> Self {
|
|
use std::sync::atomic::{AtomicU32, Ordering};
|
|
// `AtomicU64` isn't supported on all platforms
|
|
static ID: AtomicU32 = AtomicU32::new(0);
|
|
let id = ID.fetch_add(1, Ordering::Relaxed);
|
|
if id == u32::MAX {
|
|
panic!("Over `u32::MAX` routes created. If you need this, please file an issue.");
|
|
}
|
|
Self(id)
|
|
}
|
|
}
|
|
|
|
/// The router type for composing handlers and services.
|
|
pub struct Router<S, B = Body, R = MissingState> {
|
|
// Invariant: If `R == MissingState` then `state` is `None`
|
|
// If `R == WithState` then state is `Some`
|
|
// `R` cannot have other values
|
|
state: Option<S>,
|
|
routes: HashMap<RouteId, Endpoint<B>>,
|
|
node: Arc<Node>,
|
|
fallback: Fallback<B>,
|
|
_marker: PhantomData<R>,
|
|
}
|
|
|
|
impl<S, B, R> Clone for Router<S, B, R>
|
|
where
|
|
S: Clone,
|
|
{
|
|
fn clone(&self) -> Self {
|
|
Self {
|
|
state: self.state.clone(),
|
|
routes: self.routes.clone(),
|
|
node: Arc::clone(&self.node),
|
|
fallback: self.fallback.clone(),
|
|
_marker: PhantomData,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<S, B> Default for Router<S, B, MissingState>
|
|
where
|
|
B: HttpBody + Send + 'static,
|
|
{
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|
|
|
|
impl<S, B, R> fmt::Debug for Router<S, B, R>
|
|
where
|
|
S: fmt::Debug,
|
|
{
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
let Self {
|
|
state,
|
|
routes,
|
|
node,
|
|
fallback,
|
|
_marker,
|
|
} = self;
|
|
f.debug_struct("Router")
|
|
.field("state", &state)
|
|
.field("routes", &routes)
|
|
.field("node", &node)
|
|
.field("fallback", &fallback)
|
|
.finish()
|
|
}
|
|
}
|
|
|
|
pub(crate) const NEST_TAIL_PARAM: &str = "__private__axum_nest_tail_param";
|
|
const NEST_TAIL_PARAM_CAPTURE: &str = "/*__private__axum_nest_tail_param";
|
|
|
|
impl<S, B> Router<S, B, MissingState>
|
|
where
|
|
B: HttpBody + Send + 'static,
|
|
{
|
|
/// Create a new `Router`.
|
|
///
|
|
/// Unless you add additional routes this will respond with `404 Not Found` to
|
|
/// all requests.
|
|
pub fn new() -> Self {
|
|
Self {
|
|
state: None,
|
|
routes: Default::default(),
|
|
node: Default::default(),
|
|
fallback: Fallback::Default(Route::new(NotFound)),
|
|
_marker: PhantomData,
|
|
}
|
|
}
|
|
|
|
/// TODO(david): docs
|
|
pub fn state(self, state: S) -> Router<S, B, WithState> {
|
|
Router {
|
|
state: Some(state),
|
|
routes: self.routes,
|
|
node: self.node,
|
|
fallback: self.fallback,
|
|
_marker: PhantomData,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<S, B> Router<S, B, WithState>
|
|
where
|
|
B: HttpBody + Send + 'static,
|
|
{
|
|
/// TODO(david): docs
|
|
pub fn with_state(state: S) -> Self {
|
|
Router::new().state(state)
|
|
}
|
|
}
|
|
|
|
impl<S, B, R> Router<S, B, R>
|
|
where
|
|
B: HttpBody + Send + 'static,
|
|
S: 'static,
|
|
R: 'static,
|
|
{
|
|
#[doc = include_str!("../docs/routing/route.md")]
|
|
pub fn route<T>(mut self, path: &str, service: T) -> Self
|
|
where
|
|
T: Service<Request<B>, Response = Response, Error = Infallible> + Clone + Send + 'static,
|
|
T::Future: Send + 'static,
|
|
{
|
|
if path.is_empty() {
|
|
panic!("Paths must start with a `/`. Use \"/\" for root routes");
|
|
} else if !path.starts_with('/') {
|
|
panic!("Paths must start with a `/`");
|
|
}
|
|
|
|
// Downcase to `WithState` rather than `R` because `Router<S, B, R>` only implements
|
|
// `Service` if `R == WithState` so any other type of `R` cannot be passed to `.router` in
|
|
// the first place
|
|
let service = match try_downcast::<Router<S, B, WithState>, _>(service) {
|
|
Ok(_) => {
|
|
panic!("Invalid route: `Router::route` cannot be used with `Router`s. Use `Router::nest` instead")
|
|
}
|
|
Err(svc) => svc,
|
|
};
|
|
|
|
let id = RouteId::next();
|
|
|
|
let service = match try_downcast::<MethodRouter<B, Infallible>, _>(service) {
|
|
Ok(method_router) => {
|
|
if let Some((route_id, Endpoint::MethodRouter(prev_method_router))) = self
|
|
.node
|
|
.path_to_route_id
|
|
.get(path)
|
|
.and_then(|route_id| self.routes.get(route_id).map(|svc| (*route_id, svc)))
|
|
{
|
|
// if we're adding a new `MethodRouter` to a route that already has one just
|
|
// merge them. This makes `.route("/", get(_)).route("/", post(_))` work
|
|
let service =
|
|
Endpoint::MethodRouter(prev_method_router.clone().merge(method_router));
|
|
self.routes.insert(route_id, service);
|
|
return self;
|
|
} else {
|
|
Endpoint::MethodRouter(method_router)
|
|
}
|
|
}
|
|
Err(service) => Endpoint::Route(Route::new(service)),
|
|
};
|
|
|
|
let mut node =
|
|
Arc::try_unwrap(Arc::clone(&self.node)).unwrap_or_else(|node| (*node).clone());
|
|
if let Err(err) = node.insert(path, id) {
|
|
panic!("Invalid route: {}", err);
|
|
}
|
|
self.node = Arc::new(node);
|
|
|
|
self.routes.insert(id, service);
|
|
|
|
self
|
|
}
|
|
|
|
#[doc = include_str!("../docs/routing/nest.md")]
|
|
pub fn nest(mut self, mut path: &str, router: Router<S, B, MissingState>) -> Self {
|
|
if path.is_empty() {
|
|
// nesting at `""` and `"/"` should mean the same thing
|
|
path = "/";
|
|
}
|
|
|
|
if path.contains('*') {
|
|
panic!("Invalid route: nested routes cannot contain wildcards (*)");
|
|
}
|
|
|
|
let prefix = path;
|
|
|
|
let Router {
|
|
state,
|
|
mut routes,
|
|
node,
|
|
fallback,
|
|
_marker: _,
|
|
} = router;
|
|
|
|
debug_assert!(state.is_none());
|
|
|
|
if let Fallback::Custom(_) = fallback {
|
|
panic!("Cannot nest `Router`s that has a fallback");
|
|
}
|
|
|
|
for (id, nested_path) in &node.route_id_to_path {
|
|
let route = routes.remove(id).unwrap();
|
|
let full_path: Cow<str> = if &**nested_path == "/" {
|
|
path.into()
|
|
} else if path == "/" {
|
|
(&**nested_path).into()
|
|
} else if let Some(path) = path.strip_suffix('/') {
|
|
format!("{}{}", path, nested_path).into()
|
|
} else {
|
|
format!("{}{}", path, nested_path).into()
|
|
};
|
|
self = match route {
|
|
Endpoint::MethodRouter(method_router) => self.route(
|
|
&full_path,
|
|
method_router.layer(layer_fn(|s| StripPrefix::new(s, prefix))),
|
|
),
|
|
Endpoint::Route(route) => self.route(&full_path, StripPrefix::new(route, prefix)),
|
|
};
|
|
}
|
|
|
|
debug_assert!(routes.is_empty());
|
|
|
|
self
|
|
}
|
|
|
|
#[doc = include_str!("../docs/routing/nest_service.md")]
|
|
pub fn nest_service<T>(mut self, mut path: &str, svc: T) -> Self
|
|
where
|
|
T: Service<Request<B>, Response = Response, Error = Infallible> + Clone + Send + 'static,
|
|
T::Future: Send + 'static,
|
|
{
|
|
if path.is_empty() {
|
|
// nesting at `""` and `"/"` should mean the same thing
|
|
path = "/";
|
|
}
|
|
|
|
if path.contains('*') {
|
|
panic!("Invalid route: nested routes cannot contain wildcards (*)");
|
|
}
|
|
|
|
let prefix = path;
|
|
|
|
let path = if path.ends_with('/') {
|
|
format!("{}*{}", path, NEST_TAIL_PARAM)
|
|
} else {
|
|
format!("{}/*{}", path, NEST_TAIL_PARAM)
|
|
};
|
|
|
|
let svc = strip_prefix::StripPrefix::new(svc, prefix);
|
|
self = self.route(&path, svc.clone());
|
|
|
|
// `/*rest` is not matched by `/` so we need to also register a router at the
|
|
// prefix itself. Otherwise if you were to nest at `/foo` then `/foo` itself
|
|
// wouldn't match, which it should
|
|
self = self.route(prefix, svc.clone());
|
|
// same goes for `/foo/`, that should also match
|
|
self = self.route(&format!("{}/", prefix), svc);
|
|
|
|
self
|
|
}
|
|
|
|
#[doc = include_str!("../docs/routing/merge.md")]
|
|
pub fn merge<R2>(mut self, other: R2) -> Self
|
|
where
|
|
R2: Into<Router<S, B, MissingState>>,
|
|
{
|
|
let Router {
|
|
state,
|
|
routes,
|
|
node,
|
|
fallback,
|
|
_marker: _,
|
|
} = other.into();
|
|
|
|
debug_assert!(state.is_none());
|
|
|
|
for (id, route) in routes {
|
|
let path = node
|
|
.route_id_to_path
|
|
.get(&id)
|
|
.expect("no path for route id. This is a bug in axum. Please file an issue");
|
|
self = match route {
|
|
Endpoint::MethodRouter(route) => self.route(path, route),
|
|
Endpoint::Route(route) => self.route(path, route),
|
|
};
|
|
}
|
|
|
|
self.fallback = match (self.fallback, fallback) {
|
|
(Fallback::Default(_), pick @ Fallback::Default(_)) => pick,
|
|
(Fallback::Default(_), pick @ Fallback::Custom(_)) => pick,
|
|
(pick @ Fallback::Custom(_), Fallback::Default(_)) => pick,
|
|
(Fallback::Custom(_), Fallback::Custom(_)) => {
|
|
panic!("Cannot merge two `Router`s that both have a fallback")
|
|
}
|
|
};
|
|
|
|
self
|
|
}
|
|
|
|
#[doc = include_str!("../docs/routing/layer.md")]
|
|
pub fn layer<L, NewReqBody, NewResBody>(self, layer: L) -> Router<S, NewReqBody, R>
|
|
where
|
|
L: Layer<Route<B>>,
|
|
L::Service:
|
|
Service<Request<NewReqBody>, Response = Response<NewResBody>> + Clone + Send + 'static,
|
|
<L::Service as Service<Request<NewReqBody>>>::Error: Into<Infallible> + 'static,
|
|
<L::Service as Service<Request<NewReqBody>>>::Future: Send + 'static,
|
|
NewResBody: HttpBody<Data = Bytes> + Send + 'static,
|
|
NewResBody::Error: Into<BoxError>,
|
|
{
|
|
let layer = ServiceBuilder::new()
|
|
.map_err(Into::into)
|
|
.layer(MapResponseBodyLayer::new(boxed))
|
|
.layer(layer)
|
|
.into_inner();
|
|
|
|
let routes = self
|
|
.routes
|
|
.into_iter()
|
|
.map(|(id, route)| {
|
|
let route = match route {
|
|
Endpoint::MethodRouter(method_router) => {
|
|
Endpoint::MethodRouter(method_router.layer(&layer))
|
|
}
|
|
Endpoint::Route(route) => Endpoint::Route(Route::new(layer.layer(route))),
|
|
};
|
|
(id, route)
|
|
})
|
|
.collect();
|
|
|
|
let fallback = self.fallback.map(|svc| Route::new(layer.layer(svc)));
|
|
|
|
Router {
|
|
state: self.state,
|
|
routes,
|
|
node: self.node,
|
|
fallback,
|
|
_marker: self._marker,
|
|
}
|
|
}
|
|
|
|
#[doc = include_str!("../docs/routing/route_layer.md")]
|
|
pub fn route_layer<L, NewResBody>(self, layer: L) -> Self
|
|
where
|
|
L: Layer<Route<B>>,
|
|
L::Service: Service<Request<B>, Response = Response<NewResBody>> + Clone + Send + 'static,
|
|
<L::Service as Service<Request<B>>>::Error: Into<Infallible> + 'static,
|
|
<L::Service as Service<Request<B>>>::Future: Send + 'static,
|
|
NewResBody: HttpBody<Data = Bytes> + Send + 'static,
|
|
NewResBody::Error: Into<BoxError>,
|
|
{
|
|
let layer = ServiceBuilder::new()
|
|
.map_err(Into::into)
|
|
.layer(MapResponseBodyLayer::new(boxed))
|
|
.layer(layer)
|
|
.into_inner();
|
|
|
|
let routes = self
|
|
.routes
|
|
.into_iter()
|
|
.map(|(id, route)| {
|
|
let route = match route {
|
|
Endpoint::MethodRouter(method_router) => {
|
|
Endpoint::MethodRouter(method_router.layer(&layer))
|
|
}
|
|
Endpoint::Route(route) => Endpoint::Route(Route::new(layer.layer(route))),
|
|
};
|
|
(id, route)
|
|
})
|
|
.collect();
|
|
|
|
Router {
|
|
state: self.state,
|
|
routes,
|
|
node: self.node,
|
|
fallback: self.fallback,
|
|
_marker: self._marker,
|
|
}
|
|
}
|
|
|
|
#[doc = include_str!("../docs/routing/fallback.md")]
|
|
pub fn fallback<T>(mut self, svc: T) -> Self
|
|
where
|
|
T: Service<Request<B>, Response = Response, Error = Infallible> + Clone + Send + 'static,
|
|
T::Future: Send + 'static,
|
|
{
|
|
self.fallback = Fallback::Custom(Route::new(svc));
|
|
self
|
|
}
|
|
|
|
/// Convert this router into a [`MakeService`], that is a [`Service`] whose
|
|
/// response is another service.
|
|
///
|
|
/// This is useful when running your application with hyper's
|
|
/// [`Server`](hyper::server::Server):
|
|
///
|
|
/// ```
|
|
/// use axum::{
|
|
/// routing::get,
|
|
/// Router,
|
|
/// };
|
|
///
|
|
/// let app = Router::new().route("/", get(|| async { "Hi!" }));
|
|
///
|
|
/// # async {
|
|
/// axum::Server::bind(&"0.0.0.0:3000".parse().unwrap())
|
|
/// .serve(app.into_make_service())
|
|
/// .await
|
|
/// .expect("server failed");
|
|
/// # };
|
|
/// ```
|
|
///
|
|
/// [`MakeService`]: tower::make::MakeService
|
|
pub fn into_make_service(self) -> IntoMakeService<Self> {
|
|
IntoMakeService::new(self)
|
|
}
|
|
|
|
#[doc = include_str!("../docs/routing/into_make_service_with_connect_info.md")]
|
|
pub fn into_make_service_with_connect_info<C>(self) -> IntoMakeServiceWithConnectInfo<Self, C> {
|
|
IntoMakeServiceWithConnectInfo::new(self)
|
|
}
|
|
}
|
|
|
|
impl<S, B> Router<S, B, WithState>
|
|
where
|
|
B: HttpBody + Send + 'static,
|
|
S: Clone + Send + Sync + 'static,
|
|
{
|
|
#[inline]
|
|
fn call_route(
|
|
&self,
|
|
match_: matchit::Match<&RouteId>,
|
|
mut req: Request<B>,
|
|
) -> RouteFuture<B, Infallible> {
|
|
let id = *match_.value;
|
|
|
|
#[cfg(feature = "matched-path")]
|
|
if let Some(matched_path) = self.node.route_id_to_path.get(&id) {
|
|
use crate::extract::MatchedPath;
|
|
|
|
let matched_path = if let Some(previous) = req.extensions_mut().get::<MatchedPath>() {
|
|
// a previous `MatchedPath` might exist if we're inside a nested Router
|
|
let previous = if let Some(previous) =
|
|
previous.as_str().strip_suffix(NEST_TAIL_PARAM_CAPTURE)
|
|
{
|
|
previous
|
|
} else {
|
|
previous.as_str()
|
|
};
|
|
|
|
let matched_path = format!("{}{}", previous, matched_path);
|
|
matched_path.into()
|
|
} else {
|
|
Arc::clone(matched_path)
|
|
};
|
|
req.extensions_mut().insert(MatchedPath(matched_path));
|
|
} else {
|
|
#[cfg(debug_assertions)]
|
|
panic!("should always have a matched path for a route id");
|
|
}
|
|
|
|
url_params::insert_url_params(req.extensions_mut(), match_.params);
|
|
|
|
// the `unwrap` is safe because `self.state` is always some if `R = WithState`, which it is
|
|
req.extensions_mut()
|
|
.insert(crate::extract::State(self.state.as_ref().unwrap().clone()));
|
|
|
|
let mut route = self
|
|
.routes
|
|
.get(&id)
|
|
.expect("no route for id. This is a bug in axum. Please file an issue")
|
|
.clone();
|
|
|
|
match &mut route {
|
|
Endpoint::MethodRouter(inner) => inner.call(req),
|
|
Endpoint::Route(inner) => inner.call(req),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<S, B> Service<Request<B>> for Router<S, B, WithState>
|
|
where
|
|
B: HttpBody + Send + 'static,
|
|
S: Clone + Send + Sync + 'static,
|
|
{
|
|
type Response = Response;
|
|
type Error = Infallible;
|
|
type Future = RouteFuture<B, Infallible>;
|
|
|
|
#[inline]
|
|
fn poll_ready(&mut self, _: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
|
Poll::Ready(Ok(()))
|
|
}
|
|
|
|
#[inline]
|
|
fn call(&mut self, mut req: Request<B>) -> Self::Future {
|
|
#[cfg(feature = "original-uri")]
|
|
{
|
|
use crate::extract::OriginalUri;
|
|
|
|
if req.extensions().get::<OriginalUri>().is_none() {
|
|
let original_uri = OriginalUri(req.uri().clone());
|
|
req.extensions_mut().insert(original_uri);
|
|
}
|
|
}
|
|
|
|
let path = req.uri().path().to_owned();
|
|
|
|
match self.node.at(&path) {
|
|
Ok(match_) => self.call_route(match_, req),
|
|
Err(
|
|
MatchError::NotFound
|
|
| MatchError::ExtraTrailingSlash
|
|
| MatchError::MissingTrailingSlash,
|
|
) => match &self.fallback {
|
|
Fallback::Default(inner) => inner.clone().call(req),
|
|
Fallback::Custom(inner) => inner.clone().call(req),
|
|
},
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Copy, Clone, Debug)]
|
|
pub enum MissingState {}
|
|
|
|
#[derive(Copy, Clone, Debug)]
|
|
pub enum WithState {}
|
|
|
|
/// Wrapper around `matchit::Router` that supports merging two `Router`s.
|
|
#[derive(Clone, Default)]
|
|
struct Node {
|
|
inner: matchit::Router<RouteId>,
|
|
route_id_to_path: HashMap<RouteId, Arc<str>>,
|
|
path_to_route_id: HashMap<Arc<str>, RouteId>,
|
|
}
|
|
|
|
impl Node {
|
|
fn insert(
|
|
&mut self,
|
|
path: impl Into<String>,
|
|
val: RouteId,
|
|
) -> Result<(), matchit::InsertError> {
|
|
let path = path.into();
|
|
|
|
self.inner.insert(&path, val)?;
|
|
|
|
let shared_path: Arc<str> = path.into();
|
|
self.route_id_to_path.insert(val, shared_path.clone());
|
|
self.path_to_route_id.insert(shared_path, val);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn at<'n, 'p>(
|
|
&'n self,
|
|
path: &'p str,
|
|
) -> Result<matchit::Match<'n, 'p, &'n RouteId>, MatchError> {
|
|
self.inner.at(path)
|
|
}
|
|
}
|
|
|
|
impl fmt::Debug for Node {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
f.debug_struct("Node")
|
|
.field("paths", &self.route_id_to_path)
|
|
.finish()
|
|
}
|
|
}
|
|
|
|
enum Fallback<B, E = Infallible> {
|
|
Default(Route<B, E>),
|
|
Custom(Route<B, E>),
|
|
}
|
|
|
|
impl<B, E> Clone for Fallback<B, E> {
|
|
fn clone(&self) -> Self {
|
|
match self {
|
|
Fallback::Default(inner) => Fallback::Default(inner.clone()),
|
|
Fallback::Custom(inner) => Fallback::Custom(inner.clone()),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<B, E> fmt::Debug for Fallback<B, E> {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
match self {
|
|
Self::Default(inner) => f.debug_tuple("Default").field(inner).finish(),
|
|
Self::Custom(inner) => f.debug_tuple("Custom").field(inner).finish(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<B, E> Fallback<B, E> {
|
|
fn map<F, B2, E2>(self, f: F) -> Fallback<B2, E2>
|
|
where
|
|
F: FnOnce(Route<B, E>) -> Route<B2, E2>,
|
|
{
|
|
match self {
|
|
Fallback::Default(inner) => Fallback::Default(f(inner)),
|
|
Fallback::Custom(inner) => Fallback::Custom(f(inner)),
|
|
}
|
|
}
|
|
}
|
|
|
|
enum Endpoint<B> {
|
|
MethodRouter(MethodRouter<B>),
|
|
Route(Route<B>),
|
|
}
|
|
|
|
impl<B> Clone for Endpoint<B> {
|
|
fn clone(&self) -> Self {
|
|
match self {
|
|
Endpoint::MethodRouter(inner) => Endpoint::MethodRouter(inner.clone()),
|
|
Endpoint::Route(inner) => Endpoint::Route(inner.clone()),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<B> fmt::Debug for Endpoint<B> {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
match self {
|
|
Self::MethodRouter(inner) => inner.fmt(f),
|
|
Self::Route(inner) => inner.fmt(f),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
#[allow(warnings)]
|
|
fn traits() {
|
|
use crate::test_helpers::*;
|
|
assert_send::<Router<(), (), WithState>>();
|
|
assert_send::<Router<(), (), MissingState>>();
|
|
}
|