mirror of https://github.com/fafhrd91/actix-web
feat(web): implement `HttpRequest::url_for_iter`/`url_for_map` (#3895)
This commit is contained in:
parent
cf2b097de6
commit
69edde9662
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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", ¶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<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", ¶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]
|
#[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");
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue