//! Routing between [`Service`]s. use self::future::{BoxRouteFuture, EmptyRouterFuture, NestedFuture, RouteFuture}; use crate::{ body::{box_body, BoxBody}, buffer::MpscBuffer, extract::{ connect_info::{Connected, IntoMakeServiceWithConnectInfo}, OriginalUri, }, service::{HandleError, HandleErrorFromRouter}, util::ByteStr, }; use async_trait::async_trait; use bytes::Bytes; use http::{Request, Response, StatusCode, Uri}; use regex::Regex; use std::{ borrow::Cow, convert::Infallible, fmt, marker::PhantomData, sync::Arc, task::{Context, Poll}, }; use tower::{ util::{BoxService, ServiceExt}, BoxError, Layer, Service, ServiceBuilder, }; use tower_http::map_response_body::MapResponseBodyLayer; pub mod future; pub mod or; pub use self::method_filter::MethodFilter; mod method_filter; /// A route that sends requests to one of two [`Service`]s depending on the /// path. /// /// Created with [`route`](crate::route). See that function for more details. #[derive(Debug, Clone)] pub struct Route { pub(crate) pattern: PathPattern, pub(crate) svc: S, pub(crate) fallback: F, } /// Trait for building routers. #[async_trait] pub trait RoutingDsl: crate::sealed::Sealed + Sized { /// Add another route to the router. /// /// # Example /// /// ```rust /// use axum::{ /// handler::get, /// route, /// routing::RoutingDsl /// }; /// /// async fn first_handler() { /* ... */ } /// /// async fn second_handler() { /* ... */ } /// /// async fn third_handler() { /* ... */ } /// /// // `GET /` goes to `first_handler`, `POST /` goes to `second_handler`, /// // and `GET /foo` goes to third_handler. /// let app = route("/", get(first_handler).post(second_handler)) /// .route("/foo", get(third_handler)); /// # async { /// # axum::Server::bind(&"".parse().unwrap()).serve(app.into_make_service()).await.unwrap(); /// # }; /// ``` fn route(self, description: &str, svc: T) -> Route where T: Service> + Clone, { Route { pattern: PathPattern::new(description), svc, fallback: self, } } /// Nest another service inside this router at the given path. /// /// See [`nest`] for more details. fn nest(self, description: &str, svc: T) -> Nested where T: Service> + Clone, { Nested { pattern: PathPattern::new(description), svc, fallback: self, } } /// Create a boxed route trait object. /// /// This makes it easier to name the types of routers to, for example, /// return them from functions: /// /// ```rust /// use axum::{ /// body::Body, /// handler::get, /// route, /// routing::{BoxRoute, RoutingDsl} /// }; /// /// async fn first_handler() { /* ... */ } /// /// async fn second_handler() { /* ... */ } /// /// async fn third_handler() { /* ... */ } /// /// fn app() -> BoxRoute { /// route("/", get(first_handler).post(second_handler)) /// .route("/foo", get(third_handler)) /// .boxed() /// } /// ``` /// /// It also helps with compile times when you have a very large number of /// routes. fn boxed(self) -> BoxRoute where Self: Service, Response = Response> + Send + 'static, >>::Error: Into + Send + Sync, >>::Future: Send, ReqBody: http_body::Body + Send + Sync + 'static, ReqBody::Error: Into + Send + Sync + 'static, ResBody: http_body::Body + Send + Sync + 'static, ResBody::Error: Into + Send + Sync + 'static, { ServiceBuilder::new() .layer_fn(BoxRoute) .layer_fn(MpscBuffer::new) .layer(BoxService::layer()) .layer(MapResponseBodyLayer::new(box_body)) .service(self) } /// Apply a [`tower::Layer`] to the router. /// /// All requests to the router will be processed by the layer's /// corresponding middleware. /// /// This can be used to add additional processing to a request for a group /// of routes. /// /// Note this differs from [`handler::Layered`](crate::handler::Layered) /// which adds a middleware to a single handler. /// /// # Example /// /// Adding the [`tower::limit::ConcurrencyLimit`] middleware to a group of /// routes can be done like so: /// /// ```rust /// use axum::{ /// handler::get, /// route, /// routing::RoutingDsl /// }; /// use tower::limit::{ConcurrencyLimitLayer, ConcurrencyLimit}; /// /// async fn first_handler() { /* ... */ } /// /// async fn second_handler() { /* ... */ } /// /// async fn third_handler() { /* ... */ } /// /// // All requests to `handler` and `other_handler` will be sent through /// // `ConcurrencyLimit` /// let app = route("/", get(first_handler)) /// .route("/foo", get(second_handler)) /// .layer(ConcurrencyLimitLayer::new(64)) /// // Request to `GET /bar` will go directly to `third_handler` and /// // wont be sent through `ConcurrencyLimit` /// .route("/bar", get(third_handler)); /// # async { /// # axum::Server::bind(&"".parse().unwrap()).serve(app.into_make_service()).await.unwrap(); /// # }; /// ``` /// /// This is commonly used to add middleware such as tracing/logging to your /// entire app: /// /// ```rust /// use axum::{ /// handler::get, /// route, /// routing::RoutingDsl /// }; /// use tower_http::trace::TraceLayer; /// /// async fn first_handler() { /* ... */ } /// /// async fn second_handler() { /* ... */ } /// /// async fn third_handler() { /* ... */ } /// /// let app = route("/", get(first_handler)) /// .route("/foo", get(second_handler)) /// .route("/bar", get(third_handler)) /// .layer(TraceLayer::new_for_http()); /// # async { /// # axum::Server::bind(&"".parse().unwrap()).serve(app.into_make_service()).await.unwrap(); /// # }; /// ``` fn layer(self, layer: L) -> Layered where L: Layer, { Layered::new(layer.layer(self)) } /// Convert this router into a [`MakeService`], that is a [`Service`] who's /// response is another service. /// /// This is useful when running your application with hyper's /// [`Server`](hyper::server::Server): /// /// ``` /// use axum::{ /// handler::get, /// route, /// routing::RoutingDsl /// }; /// /// let app = 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 fn into_make_service(self) -> tower::make::Shared where Self: Clone, { tower::make::Shared::new(self) } /// Convert this router into a [`MakeService`], that will store `C`'s /// associated `ConnectInfo` in a request extension such that [`ConnectInfo`] /// can extract it. /// /// This enables extracting things like the client's remote address. /// /// Extracting [`std::net::SocketAddr`] is supported out of the box: /// /// ``` /// use axum::{ /// extract::ConnectInfo, /// handler::get, /// route, /// routing::RoutingDsl /// }; /// use std::net::SocketAddr; /// /// let app = route("/", get(handler)); /// /// async fn handler(ConnectInfo(addr): ConnectInfo) -> String { /// format!("Hello {}", addr) /// } /// /// # async { /// axum::Server::bind(&"0.0.0.0:3000".parse().unwrap()) /// .serve( /// app.into_make_service_with_connect_info::() /// ) /// .await /// .expect("server failed"); /// # }; /// ``` /// /// You can implement custom a [`Connected`] like so: /// /// ``` /// use axum::{ /// extract::connect_info::{ConnectInfo, Connected}, /// handler::get, /// route, /// routing::RoutingDsl /// }; /// use hyper::server::conn::AddrStream; /// /// let app = route("/", get(handler)); /// /// async fn handler( /// ConnectInfo(my_connect_info): ConnectInfo, /// ) -> String { /// format!("Hello {:?}", my_connect_info) /// } /// /// #[derive(Clone, Debug)] /// struct MyConnectInfo { /// // ... /// } /// /// impl Connected<&AddrStream> for MyConnectInfo { /// type ConnectInfo = MyConnectInfo; /// /// fn connect_info(target: &AddrStream) -> Self::ConnectInfo { /// MyConnectInfo { /// // ... /// } /// } /// } /// /// # async { /// axum::Server::bind(&"0.0.0.0:3000".parse().unwrap()) /// .serve( /// app.into_make_service_with_connect_info::() /// ) /// .await /// .expect("server failed"); /// # }; /// ``` /// /// See the [unix domain socket example][uds] for an example of how to use /// this to collect UDS connection info. /// /// [`MakeService`]: tower::make::MakeService /// [`Connected`]: crate::extract::connect_info::Connected /// [`ConnectInfo`]: crate::extract::connect_info::ConnectInfo /// [uds]: https://github.com/tokio-rs/axum/blob/main/examples/unix_domain_socket.rs fn into_make_service_with_connect_info( self, ) -> IntoMakeServiceWithConnectInfo where Self: Clone, C: Connected, { IntoMakeServiceWithConnectInfo::new(self) } /// Merge two routers into one. /// /// This is useful for breaking apps into smaller pieces and combining them /// into one. /// /// ``` /// use axum::{ /// handler::get, /// route, /// routing::RoutingDsl /// }; /// # /// # async fn users_list() {} /// # async fn users_show() {} /// # async fn teams_list() {} /// /// // define some routes separately /// let user_routes = route("/users", get(users_list)) /// .route("/users/:id", get(users_show)); /// /// let team_routes = route("/teams", get(teams_list)); /// /// // combine them into one /// let app = user_routes.or(team_routes); /// # async { /// # hyper::Server::bind(&"".parse().unwrap()).serve(app.into_make_service()).await.unwrap(); /// # }; /// ``` fn or(self, other: S) -> or::Or where S: RoutingDsl, { or::Or { first: self, second: other, } } /// Handle errors services in this router might produce, by mapping them to /// responses. /// /// Unhandled errors will close the connection without sending a response. /// /// # Example /// /// ``` /// use axum::{ /// handler::get, /// http::StatusCode, /// route, /// routing::RoutingDsl /// }; /// use tower::{BoxError, timeout::TimeoutLayer}; /// use std::{time::Duration, convert::Infallible}; /// /// // This router can never fail, since handlers can never fail. /// let app = route("/", get(|| async {})); /// /// // Now the router can fail since the `tower::timeout::Timeout` /// // middleware will return an error if the timeout elapses. /// let app = app.layer(TimeoutLayer::new(Duration::from_secs(10))); /// /// // With `handle_error` we can handle errors `Timeout` might produce. /// // Our router now cannot fail, that is its error type is `Infallible`. /// let app = app.handle_error(|error: BoxError| { /// if error.is::() { /// Ok::<_, Infallible>(( /// StatusCode::REQUEST_TIMEOUT, /// "request took too long to handle".to_string(), /// )) /// } else { /// Ok::<_, Infallible>(( /// StatusCode::INTERNAL_SERVER_ERROR, /// format!("Unhandled error: {}", error), /// )) /// } /// }); /// # async { /// # hyper::Server::bind(&"".parse().unwrap()).serve(app.into_make_service()).await.unwrap(); /// # }; /// ``` /// /// You can return `Err(_)` from the closure if you don't wish to handle /// some errors: /// /// ``` /// use axum::{ /// handler::get, /// http::StatusCode, /// route, /// routing::RoutingDsl /// }; /// use tower::{BoxError, timeout::TimeoutLayer}; /// use std::time::Duration; /// /// let app = route("/", get(|| async {})) /// .layer(TimeoutLayer::new(Duration::from_secs(10))) /// .handle_error(|error: BoxError| { /// if error.is::() { /// Ok(( /// StatusCode::REQUEST_TIMEOUT, /// "request took too long to handle".to_string(), /// )) /// } else { /// // return the error as is /// Err(error) /// } /// }); /// # async { /// # hyper::Server::bind(&"".parse().unwrap()).serve(app.into_make_service()).await.unwrap(); /// # }; /// ``` fn handle_error( self, f: F, ) -> HandleError { HandleError::new(self, f) } /// Check that your service cannot fail. /// /// That is, its error type is [`Infallible`]. fn check_infallible(self) -> CheckInfallible { CheckInfallible(self) } } impl RoutingDsl for Route {} impl crate::sealed::Sealed for Route {} impl Service> for Route where S: Service, Response = Response> + Clone, F: Service, Response = Response, Error = S::Error> + Clone, { type Response = Response; type Error = S::Error; type Future = RouteFuture; fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll> { Poll::Ready(Ok(())) } fn call(&mut self, mut req: Request) -> Self::Future { if let Some(captures) = self.pattern.full_match(&req) { insert_url_params(&mut req, captures); let fut = self.svc.clone().oneshot(req); RouteFuture::a(fut) } else { let fut = self.fallback.clone().oneshot(req); RouteFuture::b(fut) } } } #[derive(Debug)] pub(crate) struct UrlParams(pub(crate) Vec<(ByteStr, ByteStr)>); fn insert_url_params(req: &mut Request, params: Vec<(String, String)>) { let params = params .into_iter() .map(|(k, v)| (ByteStr::new(k), ByteStr::new(v))); if let Some(current) = req.extensions_mut().get_mut::>() { let mut current = current.take().unwrap(); current.0.extend(params); req.extensions_mut().insert(Some(current)); } else { req.extensions_mut() .insert(Some(UrlParams(params.collect()))); } } /// A [`Service`] that responds with `404 Not Found` or `405 Method not allowed` /// to all requests. /// /// This is used as the bottom service in a router stack. You shouldn't have to /// use it manually. pub struct EmptyRouter { status: StatusCode, _marker: PhantomData E>, } impl EmptyRouter { pub(crate) fn not_found() -> Self { Self { status: StatusCode::NOT_FOUND, _marker: PhantomData, } } pub(crate) fn method_not_allowed() -> Self { Self { status: StatusCode::METHOD_NOT_ALLOWED, _marker: PhantomData, } } } impl Clone for EmptyRouter { fn clone(&self) -> Self { Self { status: self.status, _marker: PhantomData, } } } impl fmt::Debug for EmptyRouter { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_tuple("EmptyRouter").finish() } } impl RoutingDsl for EmptyRouter {} impl crate::sealed::Sealed for EmptyRouter {} impl Service> for EmptyRouter where B: Send + Sync + 'static, { type Response = Response; type Error = E; type Future = EmptyRouterFuture; fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll> { Poll::Ready(Ok(())) } fn call(&mut self, request: Request) -> Self::Future { let mut res = Response::new(crate::body::empty()); if request.extensions().get::().is_some() { res.extensions_mut().insert(FromEmptyRouter { request }); } *res.status_mut() = self.status; EmptyRouterFuture { future: futures_util::future::ok(res), } } } /// Response extension used by [`EmptyRouter`] to send the request back to [`Or`] so /// the other service can be called. /// /// Without this we would loose ownership of the request when calling the first /// service in [`Or`]. We also wouldn't be able to identify if the response came /// from [`EmptyRouter`] and therefore can be discarded in [`Or`]. struct FromEmptyRouter { request: Request, } /// We need to track whether we're inside an `Or` or not, and only if we then /// should we save the request into the response extensions. /// /// This is to work around https://github.com/hyperium/hyper/issues/2621. /// /// Since ours can be nested we have to track the depth to know when we're /// leaving the top most `Or`. /// /// Hopefully when https://github.com/hyperium/hyper/issues/2621 is resolved we /// can remove this nasty hack. #[derive(Debug)] struct OrDepth(usize); impl OrDepth { fn new() -> Self { Self(1) } fn increment(&mut self) { self.0 += 1; } fn decrement(&mut self) { self.0 -= 1; } } impl PartialEq for &mut OrDepth { fn eq(&self, other: &usize) -> bool { self.0 == *other } } #[derive(Debug, Clone)] pub(crate) struct PathPattern(Arc); #[derive(Debug)] struct Inner { full_path_regex: Regex, capture_group_names: Box<[Bytes]>, } impl PathPattern { pub(crate) fn new(pattern: &str) -> Self { assert!( pattern.starts_with('/'), "Route description must start with a `/`" ); let mut capture_group_names = Vec::new(); let pattern = pattern .split('/') .map(|part| { if let Some(key) = part.strip_prefix(':') { capture_group_names.push(Bytes::copy_from_slice(key.as_bytes())); Cow::Owned(format!("(?P<{}>[^/]*)", key)) } else { Cow::Borrowed(part) } }) .collect::>() .join("/"); let full_path_regex = Regex::new(&format!("^{}", pattern)).expect("invalid regex generated from route"); Self(Arc::new(Inner { full_path_regex, capture_group_names: capture_group_names.into(), })) } pub(crate) fn full_match(&self, req: &Request) -> Option { self.do_match(req).and_then(|match_| { if match_.full_match { Some(match_.captures) } else { None } }) } pub(crate) fn prefix_match<'a, B>(&self, req: &'a Request) -> Option<(&'a str, Captures)> { self.do_match(req) .map(|match_| (match_.matched, match_.captures)) } fn do_match<'a, B>(&self, req: &'a Request) -> Option> { let path = req.uri().path(); self.0.full_path_regex.captures(path).map(|captures| { let matched = captures.get(0).unwrap(); let full_match = matched.as_str() == path; let captures = self .0 .capture_group_names .iter() .map(|bytes| { std::str::from_utf8(bytes) .expect("bytes were created from str so is valid utf-8") }) .filter_map(|name| captures.name(name).map(|value| (name, value.as_str()))) .map(|(key, value)| (key.to_string(), value.to_string())) .collect::>(); Match { captures, full_match, matched: matched.as_str(), } }) } } struct Match<'a> { captures: Captures, // true if regex matched whole path, false if it only matched a prefix full_match: bool, matched: &'a str, } type Captures = Vec<(String, String)>; /// A boxed route trait object. /// /// See [`RoutingDsl::boxed`] for more details. pub struct BoxRoute( MpscBuffer, Response, E>, Request>, ); impl Clone for BoxRoute { fn clone(&self) -> Self { Self(self.0.clone()) } } impl fmt::Debug for BoxRoute { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("BoxRoute").finish() } } impl RoutingDsl for BoxRoute {} impl crate::sealed::Sealed for BoxRoute {} impl Service> for BoxRoute where E: Into, { type Response = Response; type Error = E; type Future = BoxRouteFuture; #[inline] fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll> { Poll::Ready(Ok(())) } #[inline] fn call(&mut self, req: Request) -> Self::Future { BoxRouteFuture { inner: self.0.clone().oneshot(req), } } } /// A [`Service`] created from a router by applying a Tower middleware. /// /// Created with [`RoutingDsl::layer`]. See that method for more details. pub struct Layered { inner: S, } impl Layered { fn new(inner: S) -> Self { Self { inner } } } impl Clone for Layered where S: Clone, { fn clone(&self) -> Self { Self::new(self.inner.clone()) } } impl fmt::Debug for Layered where S: fmt::Debug, { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("Layered") .field("inner", &self.inner) .finish() } } impl RoutingDsl for Layered {} impl crate::sealed::Sealed for Layered {} impl Service for Layered where S: Service, { type Response = S::Response; type Error = S::Error; type Future = S::Future; #[inline] fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { self.inner.poll_ready(cx) } #[inline] fn call(&mut self, req: R) -> Self::Future { self.inner.call(req) } } /// Nest a group of routes (or a [`Service`]) at some path. /// /// This allows you to break your application into smaller pieces and compose /// them together. /// /// ``` /// use axum::{ /// handler::get, /// route, /// routing::{nest, RoutingDsl}, /// }; /// use http::Uri; /// /// async fn users_get(uri: Uri) { /// // `users_get` will still see the whole URI. /// assert_eq!(uri.path(), "/api/users"); /// } /// /// async fn users_post() {} /// /// async fn careers() {} /// /// let users_api = route("/users", get(users_get).post(users_post)); /// /// let app = nest("/api", users_api).route("/careers", get(careers)); /// # async { /// # axum::Server::bind(&"".parse().unwrap()).serve(app.into_make_service()).await.unwrap(); /// # }; /// ``` /// /// Take care when using `nest` together with dynamic routes as nesting also /// captures from the outer routes: /// /// ``` /// use axum::{ /// extract::Path, /// handler::get, /// route, /// routing::{nest, RoutingDsl}, /// }; /// use std::collections::HashMap; /// /// async fn users_get(Path(params): Path>) { /// // Both `version` and `id` were captured even though `users_api` only /// // explicitly captures `id`. /// let version = params.get("version"); /// let id = params.get("id"); /// } /// /// let users_api = route("/users/:id", get(users_get)); /// /// let app = nest("/:version/api", users_api); /// # async { /// # axum::Server::bind(&"".parse().unwrap()).serve(app.into_make_service()).await.unwrap(); /// # }; /// ``` /// /// `nest` also accepts any [`Service`]. This can for example be used with /// [`tower_http::services::ServeDir`] to serve static files from a directory: /// /// ``` /// use axum::{ /// routing::{nest, RoutingDsl}, /// service::get, /// }; /// use tower_http::services::ServeDir; /// /// // Serves files inside the `public` directory at `GET /public/*` /// let serve_dir_service = ServeDir::new("public"); /// /// let app = nest("/public", get(serve_dir_service)); /// # async { /// # axum::Server::bind(&"".parse().unwrap()).serve(app.into_make_service()).await.unwrap(); /// # }; /// ``` /// /// If necessary you can use [`RoutingDsl::boxed`] to box a group of routes /// making the type easier to name. This is sometimes useful when working with /// `nest`. pub fn nest(description: &str, svc: S) -> Nested> where S: Service> + Clone, { Nested { pattern: PathPattern::new(description), svc, fallback: EmptyRouter::not_found(), } } /// A [`Service`] that has been nested inside a router at some path. /// /// Created with [`nest`] or [`RoutingDsl::nest`]. #[derive(Debug, Clone)] pub struct Nested { pattern: PathPattern, svc: S, fallback: F, } impl RoutingDsl for Nested {} impl crate::sealed::Sealed for Nested {} impl Service> for Nested where S: Service, Response = Response> + Clone, F: Service, Response = Response, Error = S::Error> + Clone, { type Response = Response; type Error = S::Error; type Future = NestedFuture; fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll> { Poll::Ready(Ok(())) } fn call(&mut self, mut req: Request) -> Self::Future { if req.extensions().get::().is_none() { let original_uri = OriginalUri(req.uri().clone()); req.extensions_mut().insert(original_uri); } let f = if let Some((prefix, captures)) = self.pattern.prefix_match(&req) { let without_prefix = strip_prefix(req.uri(), prefix); *req.uri_mut() = without_prefix; insert_url_params(&mut req, captures); let fut = self.svc.clone().oneshot(req); RouteFuture::a(fut) } else { let fut = self.fallback.clone().oneshot(req); RouteFuture::b(fut) }; NestedFuture { inner: f } } } fn strip_prefix(uri: &Uri, prefix: &str) -> Uri { let path_and_query = if let Some(path_and_query) = uri.path_and_query() { let new_path = if let Some(path) = path_and_query.path().strip_prefix(prefix) { path } else { path_and_query.path() }; let new_path = if new_path.starts_with('/') { Cow::Borrowed(new_path) } else { Cow::Owned(format!("/{}", new_path)) }; if let Some(query) = path_and_query.query() { Some( format!("{}?{}", new_path, query) .parse::() .unwrap(), ) } else { Some(new_path.parse().unwrap()) } } else { None }; let mut parts = http::uri::Parts::default(); parts.scheme = uri.scheme().cloned(); parts.authority = uri.authority().cloned(); parts.path_and_query = path_and_query; Uri::from_parts(parts).unwrap() } /// Middleware that statically verifies that a service cannot fail. /// /// Created with [`check_infallible`](RoutingDsl::check_infallible). #[derive(Debug, Clone, Copy)] pub struct CheckInfallible(S); impl Service for CheckInfallible where S: Service, { type Response = S::Response; type Error = S::Error; type Future = S::Future; #[inline] fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { self.0.poll_ready(cx) } #[inline] fn call(&mut self, req: R) -> Self::Future { self.0.call(req) } } impl RoutingDsl for CheckInfallible {} impl crate::sealed::Sealed for CheckInfallible {} #[cfg(test)] mod tests { use super::*; #[test] fn test_routing() { assert_match("/", "/"); assert_match("/foo", "/foo"); assert_match("/foo/", "/foo/"); refute_match("/foo", "/foo/"); refute_match("/foo/", "/foo"); assert_match("/foo/bar", "/foo/bar"); refute_match("/foo/bar/", "/foo/bar"); refute_match("/foo/bar", "/foo/bar/"); assert_match("/:value", "/foo"); assert_match("/users/:id", "/users/1"); assert_match("/users/:id/action", "/users/42/action"); refute_match("/users/:id/action", "/users/42"); refute_match("/users/:id", "/users/42/action"); } fn assert_match(route_spec: &'static str, path: &'static str) { let route = PathPattern::new(route_spec); let req = Request::builder().uri(path).body(()).unwrap(); assert!( route.full_match(&req).is_some(), "`{}` doesn't match `{}`", path, route_spec ); } fn refute_match(route_spec: &'static str, path: &'static str) { let route = PathPattern::new(route_spec); let req = Request::builder().uri(path).body(()).unwrap(); assert!( route.full_match(&req).is_none(), "`{}` did match `{}` (but shouldn't)", path, route_spec ); } }