diff --git a/actix-router/src/resource.rs b/actix-router/src/resource.rs index fa77b1e7b..f1eb9caf5 100644 --- a/actix-router/src/resource.rs +++ b/actix-router/src/resource.rs @@ -29,26 +29,25 @@ const REGEX_FLAGS: &str = "(?s-m)"; /// /// /// # Pattern Format and Matching Behavior -/// /// Resource pattern is defined as a string of zero or more _segments_ where each segment is /// preceded by a slash `/`. /// -/// This means that pattern string __must__ either be empty or begin with a slash (`/`). -/// This also implies that a trailing slash in pattern defines an empty segment. -/// For example, the pattern `"/user/"` has two segments: `["user", ""]` +/// This means that pattern string __must__ either be empty or begin with a slash (`/`). This also +/// implies that a trailing slash in pattern defines an empty segment. For example, the pattern +/// `"/user/"` has two segments: `["user", ""]` /// -/// A key point to underhand is that `ResourceDef` matches segments, not strings. -/// It matches segments individually. -/// For example, the pattern `/user/` is not considered a prefix for the path `/user/123/456`, -/// because the second segment doesn't match: `["user", ""]` vs `["user", "123", "456"]`. +/// A key point to understand is that `ResourceDef` matches segments, not strings. Segments are +/// matched individually. For example, the pattern `/user/` is not considered a prefix for the path +/// `/user/123/456`, because the second segment doesn't match: `["user", ""]` +/// vs `["user", "123", "456"]`. /// /// This definition is consistent with the definition of absolute URL path in -/// [RFC 3986 (section 3.3)](https://datatracker.ietf.org/doc/html/rfc3986#section-3.3) +/// [RFC 3986 ยง3.3](https://datatracker.ietf.org/doc/html/rfc3986#section-3.3) /// /// /// # Static Resources -/// A static resource is the most basic type of definition. Pass a pattern to -/// [new][Self::new]. Conforming paths must match the pattern exactly. +/// A static resource is the most basic type of definition. Pass a pattern to [new][Self::new]. +/// Conforming paths must match the pattern exactly. /// /// ## Examples /// ``` @@ -63,7 +62,6 @@ const REGEX_FLAGS: &str = "(?s-m)"; /// assert!(!resource.is_match("/search")); /// ``` /// -/// /// # Dynamic Segments /// Also known as "path parameters". Resources can define sections of a pattern that be extracted /// from a conforming path, if it conforms to (one of) the resource pattern(s). @@ -102,15 +100,15 @@ const REGEX_FLAGS: &str = "(?s-m)"; /// assert_eq!(path.get("id").unwrap(), "123"); /// ``` /// -/// /// # Prefix Resources /// A prefix resource is defined as pattern that can match just the start of a path, up to a /// segment boundary. /// /// Prefix patterns with a trailing slash may have an unexpected, though correct, behavior. -/// They define and therefore require an empty segment in order to match. Examples are given below. +/// They define and therefore require an empty segment in order to match. It is easier to understand +/// this behavior after reading the [matching behavior section]. Examples are given below. /// -/// Empty pattern matches any path as a prefix. +/// The empty pattern (`""`), as a prefix, matches any path. /// /// Prefix resources can contain dynamic segments. /// @@ -130,7 +128,6 @@ const REGEX_FLAGS: &str = "(?s-m)"; /// assert!(!resource.is_match("/user/123")); /// ``` /// -/// /// # Custom Regex Segments /// Dynamic segments can be customised to only match a specific regular expression. It can be /// helpful to do this if resource definitions would otherwise conflict and cause one to @@ -158,7 +155,6 @@ const REGEX_FLAGS: &str = "(?s-m)"; /// assert!(!resource.is_match("/user/abc")); /// ``` /// -/// /// # Tail Segments /// As a shortcut to defining a custom regex for matching _all_ remaining characters (not just those /// up until a `/` character), there is a special pattern to match (and capture) the remaining @@ -179,7 +175,6 @@ const REGEX_FLAGS: &str = "(?s-m)"; /// assert_eq!(path.get("tail").unwrap(), "main/LICENSE"); /// ``` /// -/// /// # Multi-Pattern Resources /// For resources that can map to multiple distinct paths, it may be suitable to use /// multi-pattern resources by passing an array/vec to [`new`][Self::new]. They will be combined @@ -198,7 +193,6 @@ const REGEX_FLAGS: &str = "(?s-m)"; /// assert!(resource.is_match("/index")); /// ``` /// -/// /// # Trailing Slashes /// It should be noted that this library takes no steps to normalize intra-path or trailing slashes. /// As such, all resource definitions implicitly expect a pre-processing step to normalize paths if @@ -212,6 +206,8 @@ const REGEX_FLAGS: &str = "(?s-m)"; /// assert!(!ResourceDef::new("/root/").is_match("/root")); /// assert!(!ResourceDef::prefix("/root/").is_match("/root")); /// ``` +/// +/// [matching behavior section]: #pattern-format-and-matching-behavior #[derive(Clone, Debug)] pub struct ResourceDef { id: u16, @@ -279,7 +275,7 @@ impl ResourceDef { /// ``` pub fn new(paths: T) -> Self { profile_method!(new); - Self::new2(paths, false) + Self::construct(paths, false) } /// Constructs a new resource definition using a pattern that performs prefix matching. @@ -292,7 +288,7 @@ impl ResourceDef { /// resource definition with a tail segment; use [`new`][Self::new] in this case. /// /// # Panics - /// Panics if path regex pattern is malformed. + /// Panics if path pattern is malformed. /// /// # Examples /// ``` @@ -307,14 +303,14 @@ impl ResourceDef { /// ``` pub fn prefix(paths: T) -> Self { profile_method!(prefix); - ResourceDef::new2(paths, true) + ResourceDef::construct(paths, true) } /// Constructs a new resource definition using a string pattern that performs prefix matching, - /// inserting a `/` to beginning of the pattern if absent and pattern is not empty. + /// ensuring a leading `/` if pattern is not empty. /// /// # Panics - /// Panics if path regex pattern is malformed. + /// Panics if path pattern is malformed. /// /// # Examples /// ``` @@ -515,8 +511,8 @@ impl ResourceDef { .collect::>(); match patterns.len() { - 1 => ResourceDef::new2(&patterns[0], other.is_prefix()), - _ => ResourceDef::new2(patterns, other.is_prefix()), + 1 => ResourceDef::construct(&patterns[0], other.is_prefix()), + _ => ResourceDef::construct(patterns, other.is_prefix()), } } @@ -881,8 +877,8 @@ impl ResourceDef { } } - fn new2(paths: T, is_prefix: bool) -> Self { - profile_method!(new2); + fn construct(paths: T, is_prefix: bool) -> Self { + profile_method!(construct); let patterns = paths.patterns(); let (pat_type, segments) = match &patterns { @@ -1814,7 +1810,7 @@ mod tests { #[test] #[should_panic] - fn prefix_plus_tail_match_is_allowed() { + fn prefix_plus_tail_match_disallowed() { ResourceDef::prefix("/user/{id}*"); } } diff --git a/src/resource.rs b/src/resource.rs index d94d2a464..0d82bb004 100644 --- a/src/resource.rs +++ b/src/resource.rs @@ -23,28 +23,25 @@ use crate::{ BoxError, Error, FromRequest, HttpResponse, Responder, }; -/// *Resource* is an entry in resources table which corresponds to requested URL. +/// A collection of [`Route`]s that respond to the same path pattern. /// -/// Resource in turn has at least one route. -/// Route consists of an handlers objects and list of guards -/// (objects that implement `Guard` trait). -/// Resources and routes uses builder-like pattern for configuration. -/// During request handling, resource object iterate through all routes -/// and check guards for specific route, if request matches all -/// guards, route considered matched and route handler get called. +/// Resource in turn has at least one route. Route consists of an handlers objects and list of +/// guards (objects that implement `Guard` trait). Resources and routes uses builder-like pattern +/// for configuration. During request handling, resource object iterate through all routes and check +/// guards for specific route, if request matches all guards, route considered matched and route +/// handler get called. /// +/// # Examples /// ``` /// use actix_web::{web, App, HttpResponse}; /// -/// fn main() { -/// let app = App::new().service( -/// web::resource("/") -/// .route(web::get().to(|| HttpResponse::Ok()))); -/// } +/// let app = App::new().service( +/// web::resource("/") +/// .route(web::get().to(|| HttpResponse::Ok()))); /// ``` /// -/// If no matching route could be found, *405* response code get returned. -/// Default behavior could be overridden with `default_resource()` method. +/// If no matching route could be found, *405* response code get returned. Default behavior could be +/// overridden with `default_resource()` method. pub struct Resource { endpoint: T, rdef: Patterns, diff --git a/src/route.rs b/src/route.rs index 4447bff50..16c01275b 100644 --- a/src/route.rs +++ b/src/route.rs @@ -15,10 +15,10 @@ use crate::{ BoxError, Error, FromRequest, HttpResponse, Responder, }; -/// Resource route definition +/// A request handler with [guards](guard). /// -/// Route uses builder-like pattern for configuration. -/// If handler is not explicitly set, default *404 Not Found* handler is used. +/// Route uses a builder-like pattern for configuration. If handler is not set, a `404 Not Found` +/// handler is used. pub struct Route { service: BoxedHttpServiceFactory, guards: Rc>>, diff --git a/src/scope.rs b/src/scope.rs index 1fd282f61..c3bab8f7e 100644 --- a/src/scope.rs +++ b/src/scope.rs @@ -27,34 +27,36 @@ use crate::{ type Guards = Vec>; -/// Resources scope. +/// A collection of [`Route`]s, [`Resource`]s, or other services that share a common path prefix. /// -/// Scope is a set of resources with common root path. -/// Scopes collect multiple paths under a common path prefix. -/// Scope path can contain variable path segments as resources. -/// Scope prefix is always complete path segment, i.e `/app` would -/// be converted to a `/app/` and it would not match `/app` path. +/// The `Scope`'s path can contain [dynamic segments]. The dynamic segments can be extracted from +/// requests using the [`Path`](crate::web::Path) extractor or +/// with [`HttpRequest::match_info()`](crate::HttpRequest::match_info). /// -/// You can get variable path segments from `HttpRequest::match_info()`. -/// `Path` extractor also is able to extract scope level variable segments. +/// # Avoid Trailing Slashes +/// Avoid using trailing slashes in the scope prefix (e.g., `web::scope("/scope/")`). It will almost +/// certainly not have the expected behavior. See the [documentation on resource definitions][pat] +/// to understand why this is the case and how to correctly construct scope/prefix definitions. /// +/// # Examples /// ``` /// use actix_web::{web, App, HttpResponse}; /// -/// fn main() { -/// let app = App::new().service( -/// web::scope("/{project_id}/") -/// .service(web::resource("/path1").to(|| async { "OK" })) -/// .service(web::resource("/path2").route(web::get().to(|| HttpResponse::Ok()))) -/// .service(web::resource("/path3").route(web::head().to(HttpResponse::MethodNotAllowed))) -/// ); -/// } +/// let app = App::new().service( +/// web::scope("/{project_id}/") +/// .service(web::resource("/path1").to(|| async { "OK" })) +/// .service(web::resource("/path2").route(web::get().to(|| HttpResponse::Ok()))) +/// .service(web::resource("/path3").route(web::head().to(HttpResponse::MethodNotAllowed))) +/// ); /// ``` /// /// In the above example three routes get registered: -/// * /{project_id}/path1 - responds to all http method -/// * /{project_id}/path2 - `GET` requests -/// * /{project_id}/path3 - `HEAD` requests +/// - /{project_id}/path1 - responds to all HTTP methods +/// - /{project_id}/path2 - responds to `GET` requests +/// - /{project_id}/path3 - responds to `HEAD` requests +/// +/// [pat]: crate::dev::ResourceDef#prefix-resources +/// [dynamic segments]: crate::dev::ResourceDef#dynamic-segments pub struct Scope { endpoint: T, rdef: String, @@ -106,16 +108,14 @@ where /// "Welcome!" /// } /// - /// fn main() { - /// let app = App::new().service( - /// web::scope("/app") - /// .guard(guard::Header("content-type", "text/plain")) - /// .route("/test1", web::get().to(index)) - /// .route("/test2", web::post().to(|r: HttpRequest| { - /// HttpResponse::MethodNotAllowed() - /// })) - /// ); - /// } + /// let app = App::new().service( + /// web::scope("/app") + /// .guard(guard::Header("content-type", "text/plain")) + /// .route("/test1", web::get().to(index)) + /// .route("/test2", web::post().to(|r: HttpRequest| { + /// HttpResponse::MethodNotAllowed() + /// })) + /// ); /// ``` pub fn guard(mut self, guard: G) -> Self { self.guards.push(Box::new(guard)); @@ -186,15 +186,13 @@ where /// ); /// } /// - /// fn main() { - /// let app = App::new() - /// .wrap(middleware::Logger::default()) - /// .service( - /// web::scope("/api") - /// .configure(config) - /// ) - /// .route("/index.html", web::get().to(|| HttpResponse::Ok())); - /// } + /// let app = App::new() + /// .wrap(middleware::Logger::default()) + /// .service( + /// web::scope("/api") + /// .configure(config) + /// ) + /// .route("/index.html", web::get().to(|| HttpResponse::Ok())); /// ``` pub fn configure(mut self, cfg_fn: F) -> Self where @@ -233,13 +231,11 @@ where /// "Welcome!" /// } /// - /// fn main() { - /// let app = App::new().service( - /// web::scope("/app").service( - /// web::scope("/v1") - /// .service(web::resource("/test1").to(index))) - /// ); - /// } + /// let app = App::new().service( + /// web::scope("/app").service( + /// web::scope("/v1") + /// .service(web::resource("/test1").to(index))) + /// ); /// ``` pub fn service(mut self, factory: F) -> Self where @@ -263,13 +259,11 @@ where /// "Welcome!" /// } /// - /// fn main() { - /// let app = App::new().service( - /// web::scope("/app") - /// .route("/test1", web::get().to(index)) - /// .route("/test2", web::post().to(|| HttpResponse::MethodNotAllowed())) - /// ); - /// } + /// let app = App::new().service( + /// web::scope("/app") + /// .route("/test1", web::get().to(index)) + /// .route("/test2", web::post().to(|| HttpResponse::MethodNotAllowed())) + /// ); /// ``` pub fn route(self, path: &str, mut route: Route) -> Self { self.service( @@ -355,21 +349,19 @@ where /// "Welcome!" /// } /// - /// fn main() { - /// let app = App::new().service( - /// web::scope("/app") - /// .wrap_fn(|req, srv| { - /// let fut = srv.call(req); - /// async { - /// let mut res = fut.await?; - /// res.headers_mut().insert( - /// CONTENT_TYPE, HeaderValue::from_static("text/plain"), - /// ); - /// Ok(res) - /// } - /// }) - /// .route("/index.html", web::get().to(index))); - /// } + /// let app = App::new().service( + /// web::scope("/app") + /// .wrap_fn(|req, srv| { + /// let fut = srv.call(req); + /// async { + /// let mut res = fut.await?; + /// res.headers_mut().insert( + /// CONTENT_TYPE, HeaderValue::from_static("text/plain"), + /// ); + /// Ok(res) + /// } + /// }) + /// .route("/index.html", web::get().to(index))); /// ``` pub fn wrap_fn( self, diff --git a/src/web.rs b/src/web.rs index 042b8a008..22877692d 100644 --- a/src/web.rs +++ b/src/web.rs @@ -52,11 +52,16 @@ pub fn resource(path: T) -> Resource { /// Scopes collect multiple paths under a common path prefix. The scope's path can contain dynamic /// path segments. /// +/// # Avoid Trailing Slashes +/// Avoid using trailing slashes in the scope prefix (e.g., `web::scope("/scope/")`). It will almost +/// certainly not have the expected behavior. See the [documentation on resource definitions][pat] +/// to understand why this is the case and how to correctly construct scope/prefix definitions. +/// /// # Examples /// In this example, three routes are set up (and will handle any method): -/// * `/{project_id}/path1` -/// * `/{project_id}/path2` -/// * `/{project_id}/path3` +/// - `/{project_id}/path1` +/// - `/{project_id}/path2` +/// - `/{project_id}/path3` /// /// ``` /// use actix_web::{web, App, HttpResponse}; @@ -68,6 +73,8 @@ pub fn resource(path: T) -> Resource { /// .service(web::resource("/path3").to(|| HttpResponse::MethodNotAllowed())) /// ); /// ``` +/// +/// [pat]: crate::dev::ResourceDef#prefix-resources pub fn scope(path: &str) -> Scope { Scope::new(path) }