diff --git a/actix-web/CHANGES.md b/actix-web/CHANGES.md index cf5971e9f..47624967d 100644 --- a/actix-web/CHANGES.md +++ b/actix-web/CHANGES.md @@ -3,6 +3,9 @@ ## Unreleased - Minimum supported Rust version (MSRV) is now 1.88. +- Add `HttpRequest::url_for_map` and `HttpRequest::url_for_iter` methods for named URL parameters. [#3895] + +[#3895]: https://github.com/actix/actix-web/pull/3895 ## 4.12.1 diff --git a/actix-web/src/request.rs b/actix-web/src/request.rs index a49a55bd0..3a3e539bf 100644 --- a/actix-web/src/request.rs +++ b/actix-web/src/request.rs @@ -1,6 +1,9 @@ use std::{ cell::{Ref, RefCell, RefMut}, - fmt, net, + collections::HashMap, + fmt, + hash::{BuildHasher, Hash}, + net, rc::Rc, str, }; @@ -242,6 +245,76 @@ impl HttpRequest { self.resource_map().url_for(self, name, elements) } + /// Generates URL for a named resource using a map of dynamic segment values. + /// + /// This substitutes URL parameters by name from `elements`, including parameters from parent + /// scopes. + /// + /// # Examples + /// ``` + /// # use std::collections::HashMap; + /// # use actix_web::{web, App, HttpRequest, HttpResponse}; + /// fn index(req: HttpRequest) -> HttpResponse { + /// let mut params = HashMap::new(); + /// params.insert("one", "1"); + /// params.insert("two", "2"); + /// let url = req.url_for_map("foo", ¶ms); // <- generate URL for "foo" resource + /// HttpResponse::Ok().into() + /// } + /// + /// let app = App::new() + /// .service(web::resource("/test/{one}/{two}") + /// .name("foo") // <- set resource name so it can be used in `url_for_map` + /// .route(web::get().to(|| HttpResponse::Ok())) + /// ); + /// ``` + pub fn url_for_map( + &self, + name: &str, + elements: &HashMap, + ) -> Result + where + K: std::borrow::Borrow + Eq + Hash, + V: AsRef, + S: BuildHasher, + { + self.resource_map().url_for_map(self, name, elements) + } + + /// Generates URL for a named resource using an iterator of key-value pairs. + /// + /// This is a convenience wrapper around [`HttpRequest::url_for_map`]. + /// + /// Note: passing a borrowed map (e.g. `&HashMap`) directly does not satisfy the + /// trait bounds because the iterator yields `(&String, &String)`. Prefer `url_for_map` for + /// borrowed maps, or map entries to `&str`: + /// + /// ``` + /// # use std::collections::HashMap; + /// # use actix_web::{web, App, HttpRequest, HttpResponse}; + /// fn index(req: HttpRequest) -> HttpResponse { + /// let mut params = HashMap::new(); + /// params.insert("one".to_string(), "1".to_string()); + /// params.insert("two".to_string(), "2".to_string()); + /// + /// let iter = params.iter().map(|(k, v)| (k.as_str(), v.as_str())); + /// let url = req.url_for_iter("foo", iter); + /// HttpResponse::Ok().into() + /// } + /// ``` + pub fn url_for_iter( + &self, + name: &str, + elements: I, + ) -> Result + where + I: IntoIterator, + K: std::borrow::Borrow + Eq + Hash, + V: AsRef, + { + self.resource_map().url_for_iter(self, name, elements) + } + /// Generate URL for named resource /// /// This method is similar to `HttpRequest::url_for()` but it can be used @@ -550,6 +623,8 @@ impl HttpRequestPool { #[cfg(test)] mod tests { + use std::collections::HashMap; + use bytes::Bytes; use super::*; @@ -638,6 +713,59 @@ mod tests { ); } + #[test] + fn test_url_for_map() { + let mut res = ResourceDef::new("/user/{name}.{ext}"); + res.set_name("index"); + + let mut rmap = ResourceMap::new(ResourceDef::prefix("")); + rmap.add(&mut res, None); + + let req = TestRequest::default() + .insert_header((header::HOST, "www.actix.rs")) + .rmap(rmap) + .to_http_request(); + + let mut params = HashMap::new(); + params.insert("name", "test"); + params.insert("ext", "html"); + + let url = req.url_for_map("index", ¶ms); + assert_eq!( + url.ok().unwrap().as_str(), + "http://www.actix.rs/user/test.html" + ); + + params.remove("ext"); + assert_eq!( + req.url_for_map("index", ¶ms), + Err(UrlGenerationError::NotEnoughElements) + ); + } + + #[test] + fn test_url_for_iter() { + let mut res = ResourceDef::new("/user/{name}.{ext}"); + res.set_name("index"); + + let mut rmap = ResourceMap::new(ResourceDef::prefix("")); + rmap.add(&mut res, None); + + let req = TestRequest::default() + .insert_header((header::HOST, "www.actix.rs")) + .rmap(rmap) + .to_http_request(); + + let url = req.url_for_iter("index", [("ext", "html"), ("name", "test")]); + assert_eq!( + url.ok().unwrap().as_str(), + "http://www.actix.rs/user/test.html" + ); + + let url = req.url_for_iter("index", [("name", "test")]); + assert_eq!(url, Err(UrlGenerationError::NotEnoughElements)); + } + #[test] fn test_url_for_static() { let mut rdef = ResourceDef::new("/index.html"); diff --git a/actix-web/src/rmap.rs b/actix-web/src/rmap.rs index b445687ac..ee86d271b 100644 --- a/actix-web/src/rmap.rs +++ b/actix-web/src/rmap.rs @@ -1,7 +1,9 @@ use std::{ - borrow::Cow, + borrow::{Borrow, Cow}, cell::RefCell, + collections::HashMap, fmt::Write as _, + hash::{BuildHasher, Hash}, rc::{Rc, Weak}, }; @@ -140,6 +142,56 @@ impl ResourceMap { }) .ok_or(UrlGenerationError::NotEnoughElements)?; + self.url_from_path(req, path) + } + + /// Generate URL for named resource using map of dynamic segment values. + /// + /// Check [`HttpRequest::url_for_map`] for detailed information. + pub fn url_for_map( + &self, + req: &HttpRequest, + name: &str, + elements: &HashMap, + ) -> Result + where + K: Borrow + Eq + Hash, + V: AsRef, + S: BuildHasher, + { + let path = self + .named + .get(name) + .ok_or(UrlGenerationError::ResourceNotFound)? + .root_rmap_fn(String::with_capacity(AVG_PATH_LEN), |mut acc, node| { + node.pattern + .resource_path_from_map(&mut acc, elements) + .then_some(acc) + }) + .ok_or(UrlGenerationError::NotEnoughElements)?; + + self.url_from_path(req, path) + } + + /// Generate URL for named resource using an iterator of key-value pairs. + /// + /// Check [`HttpRequest::url_for_iter`] for detailed information. + pub fn url_for_iter( + &self, + req: &HttpRequest, + name: &str, + elements: I, + ) -> Result + where + I: IntoIterator, + K: Borrow + Eq + Hash, + V: AsRef, + { + let elements = elements.into_iter().collect::>(); + self.url_for_map(req, name, &elements) + } + + fn url_from_path(&self, req: &HttpRequest, path: String) -> Result { let (base, path): (Cow<'_, _>, _) = if path.starts_with('/') { // build full URL from connection info parts and resource path let conn = req.connection_info();