feat(web): implement `HttpRequest::url_for_iter`/`url_for_map` (#3895)

This commit is contained in:
Yuki Okushi 2026-02-01 16:54:47 +09:00 committed by GitHub
parent cf2b097de6
commit 69edde9662
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 185 additions and 2 deletions

View File

@ -3,6 +3,9 @@
## Unreleased ## Unreleased
- Minimum supported Rust version (MSRV) is now 1.88. - 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 ## 4.12.1

View File

@ -1,6 +1,9 @@
use std::{ use std::{
cell::{Ref, RefCell, RefMut}, cell::{Ref, RefCell, RefMut},
fmt, net, collections::HashMap,
fmt,
hash::{BuildHasher, Hash},
net,
rc::Rc, rc::Rc,
str, str,
}; };
@ -242,6 +245,76 @@ impl HttpRequest {
self.resource_map().url_for(self, name, elements) 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", &params); // <- 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<K, V, S>(
&self,
name: &str,
elements: &HashMap<K, V, S>,
) -> Result<url::Url, UrlGenerationError>
where
K: std::borrow::Borrow<str> + Eq + Hash,
V: AsRef<str>,
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<String, String>`) 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<K, V, I>(
&self,
name: &str,
elements: I,
) -> Result<url::Url, UrlGenerationError>
where
I: IntoIterator<Item = (K, V)>,
K: std::borrow::Borrow<str> + Eq + Hash,
V: AsRef<str>,
{
self.resource_map().url_for_iter(self, name, elements)
}
/// Generate URL for named resource /// Generate URL for named resource
/// ///
/// This method is similar to `HttpRequest::url_for()` but it can be used /// This method is similar to `HttpRequest::url_for()` but it can be used
@ -550,6 +623,8 @@ impl HttpRequestPool {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use std::collections::HashMap;
use bytes::Bytes; use bytes::Bytes;
use super::*; 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", &params);
assert_eq!(
url.ok().unwrap().as_str(),
"http://www.actix.rs/user/test.html"
);
params.remove("ext");
assert_eq!(
req.url_for_map("index", &params),
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] #[test]
fn test_url_for_static() { fn test_url_for_static() {
let mut rdef = ResourceDef::new("/index.html"); let mut rdef = ResourceDef::new("/index.html");

View File

@ -1,7 +1,9 @@
use std::{ use std::{
borrow::Cow, borrow::{Borrow, Cow},
cell::RefCell, cell::RefCell,
collections::HashMap,
fmt::Write as _, fmt::Write as _,
hash::{BuildHasher, Hash},
rc::{Rc, Weak}, rc::{Rc, Weak},
}; };
@ -140,6 +142,56 @@ impl ResourceMap {
}) })
.ok_or(UrlGenerationError::NotEnoughElements)?; .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<K, V, S>(
&self,
req: &HttpRequest,
name: &str,
elements: &HashMap<K, V, S>,
) -> Result<Url, UrlGenerationError>
where
K: Borrow<str> + Eq + Hash,
V: AsRef<str>,
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<K, V, I>(
&self,
req: &HttpRequest,
name: &str,
elements: I,
) -> Result<Url, UrlGenerationError>
where
I: IntoIterator<Item = (K, V)>,
K: Borrow<str> + Eq + Hash,
V: AsRef<str>,
{
let elements = elements.into_iter().collect::<FoldHashMap<K, V>>();
self.url_for_map(req, name, &elements)
}
fn url_from_path(&self, req: &HttpRequest, path: String) -> Result<Url, UrlGenerationError> {
let (base, path): (Cow<'_, _>, _) = if path.starts_with('/') { let (base, path): (Cow<'_, _>, _) = if path.starts_with('/') {
// build full URL from connection info parts and resource path // build full URL from connection info parts and resource path
let conn = req.connection_info(); let conn = req.connection_info();