1
0
mirror of https://github.com/fafhrd91/actix-web synced 2025-02-25 13:22:50 +01:00

Added HttpRequest::url_for_map

This commit is contained in:
Herddex 2024-06-23 08:54:31 +03:00
parent 4222f92bd3
commit abafd90ec6
3 changed files with 173 additions and 27 deletions

View File

@ -2,6 +2,10 @@
## Unreleased ## Unreleased
### Added
- Add `HttpRequest::url_for_map()` that accepts a map of elements by parameter names, an alternative to `HttpRequest::url_for()` (which takes an iterator over elements).
## 4.8.0 ## 4.8.0
### Added ### Added

View File

@ -1,6 +1,10 @@
use std::{ use std::{
borrow,
cell::{Ref, RefCell, RefMut}, cell::{Ref, RefCell, RefMut},
fmt, net, collections::HashMap,
fmt,
hash::{BuildHasher, Hash},
net,
rc::Rc, rc::Rc,
str, str,
}; };
@ -230,7 +234,7 @@ impl HttpRequest {
/// ///
/// let app = App::new() /// let app = App::new()
/// .service(web::resource("/test/{one}/{two}/{three}") /// .service(web::resource("/test/{one}/{two}/{three}")
/// .name("foo") // <- set resource name so it can be used in `url_for` /// .name("foo") // <- set resource name, so it can be used in `url_for`
/// .route(web::get().to(|| HttpResponse::Ok())) /// .route(web::get().to(|| HttpResponse::Ok()))
/// ); /// );
/// ``` /// ```
@ -239,7 +243,46 @@ impl HttpRequest {
U: IntoIterator<Item = I>, U: IntoIterator<Item = I>,
I: AsRef<str>, I: AsRef<str>,
{ {
self.resource_map().url_for(self, name, elements) self.resource_map().url_for_iter(self, name, elements)
}
/// Generates URL for a named resource.
///
/// This substitutes all URL parameters that appear in the resource itself and in
/// parent [scopes](crate::web::scope), if any, with their associated value from the given map.
///
/// It is worth noting that the characters `['/', '%']` are not escaped and therefore a single
/// URL parameter may expand into multiple path segments and `elements` can be percent-encoded
/// beforehand without worrying about double encoding. Any other character that is not valid in
/// a URL path context is escaped using percent-encoding.
///
/// # Examples
/// ```
/// # use std::collections::HashMap;
/// # use actix_web::{web, App, HttpRequest, HttpResponse};
/// fn index(req: HttpRequest) -> HttpResponse {
/// let parameter_map = HashMap::from([("one", "1"), ("two", "2"), ("three", "3")]);
/// let url = req.url_for_map("foo", &parameter_map); // <- generate URL for "foo" resource
/// HttpResponse::Ok().into()
/// }
///
/// let app = App::new()
/// .service(web::resource("/test/{one}/{two}/{three}")
/// .name("foo") // <- set resource name, so it can be used in `url_for`
/// .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: borrow::Borrow<str> + Eq + Hash,
V: AsRef<str>,
S: BuildHasher,
{
self.resource_map().url_for_map(self, name, elements)
} }
/// Generate URL for named resource /// Generate URL for named resource
@ -621,17 +664,30 @@ mod tests {
.rmap(rmap) .rmap(rmap)
.to_http_request(); .to_http_request();
let input_map = HashMap::from([("non-existing", "test")]);
assert_eq!( assert_eq!(
req.url_for("unknown", ["test"]), req.url_for("unknown", ["test"]),
Err(UrlGenerationError::ResourceNotFound) Err(UrlGenerationError::ResourceNotFound)
); );
assert_eq!(
req.url_for_map("unknown", &input_map),
Err(UrlGenerationError::ResourceNotFound)
);
assert_eq!( assert_eq!(
req.url_for("index", ["test"]), req.url_for("index", ["test"]),
Err(UrlGenerationError::NotEnoughElements) Err(UrlGenerationError::NotEnoughElements)
); );
let url = req.url_for("index", ["test", "html"]);
assert_eq!( assert_eq!(
url.ok().unwrap().as_str(), req.url_for_map("index", &input_map),
Err(UrlGenerationError::NotEnoughElements)
);
let input_map = HashMap::from([("name", "test"), ("ext", "html")]);
let url = req.url_for("index", ["test", "html"]);
assert_eq!(url, req.url_for_map("index", &input_map));
assert_eq!(
url.unwrap().as_str(),
"http://www.rust-lang.org/user/test.html" "http://www.rust-lang.org/user/test.html"
); );
} }
@ -685,9 +741,11 @@ mod tests {
rmap.add(&mut rdef, None); rmap.add(&mut rdef, None);
let req = TestRequest::default().rmap(rmap).to_http_request(); let req = TestRequest::default().rmap(rmap).to_http_request();
let input_map = HashMap::from([("video_id", "oHg5SJYRHA0")]);
let url = req.url_for("youtube", ["oHg5SJYRHA0"]); let url = req.url_for("youtube", ["oHg5SJYRHA0"]);
assert_eq!(url, req.url_for_map("youtube", &input_map));
assert_eq!( assert_eq!(
url.ok().unwrap().as_str(), url.unwrap().as_str(),
"https://youtube.com/watch/oHg5SJYRHA0" "https://youtube.com/watch/oHg5SJYRHA0"
); );
} }

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},
}; };
@ -114,10 +116,10 @@ impl ResourceMap {
} }
} }
/// Generate URL for named resource. /// Generate URL for named resource with an iterator over elements.
/// ///
/// Check [`HttpRequest::url_for`] for detailed information. /// Check [`HttpRequest::url_for`] for detailed information.
pub fn url_for<U, I>( pub fn url_for_iter<U, I>(
&self, &self,
req: &HttpRequest, req: &HttpRequest,
name: &str, name: &str,
@ -128,16 +130,48 @@ impl ResourceMap {
I: AsRef<str>, I: AsRef<str>,
{ {
let mut elements = elements.into_iter(); let mut elements = elements.into_iter();
self.url_for(req, name, |mut acc, node: &ResourceMap| {
let path = self
.named
.get(name)
.ok_or(UrlGenerationError::ResourceNotFound)?
.root_rmap_fn(String::with_capacity(AVG_PATH_LEN), |mut acc, node| {
node.pattern node.pattern
.resource_path_from_iter(&mut acc, &mut elements) .resource_path_from_iter(&mut acc, &mut elements)
.then_some(acc) .then_some(acc)
}) })
}
/// Generate URL for named resource with a map of elements by parameter names.
///
/// 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,
{
self.url_for(req, name, |mut acc, node: &ResourceMap| {
node.pattern
.resource_path_from_map(&mut acc, elements)
.then_some(acc)
})
}
fn url_for<F>(
&self,
req: &HttpRequest,
name: &str,
map_fn: F,
) -> Result<Url, UrlGenerationError>
where
F: FnMut(String, &ResourceMap) -> Option<String>,
{
let path = self
.named
.get(name)
.ok_or(UrlGenerationError::ResourceNotFound)?
.root_rmap_fn(String::with_capacity(AVG_PATH_LEN), map_fn)
.ok_or(UrlGenerationError::NotEnoughElements)?; .ok_or(UrlGenerationError::NotEnoughElements)?;
let (base, path): (Cow<'_, _>, _) = if path.starts_with('/') { let (base, path): (Cow<'_, _>, _) = if path.starts_with('/') {
@ -448,13 +482,23 @@ mod tests {
req.set_server_hostname("localhost:8888"); req.set_server_hostname("localhost:8888");
let req = req.to_http_request(); let req = req.to_http_request();
const OUTPUT: &str = "http://localhost:8888/user/u123/post/foobar";
let url = rmap let url = rmap
.url_for(&req, "post", ["u123", "foobar"]) .url_for_iter(&req, "post", ["u123", "foobar"])
.unwrap() .unwrap()
.to_string(); .to_string();
assert_eq!(url, "http://localhost:8888/user/u123/post/foobar"); assert_eq!(url, OUTPUT);
assert!(rmap.url_for(&req, "missing", ["u123"]).is_err()); let input_map = HashMap::from([("user_id", "u123"), ("sub_id", "foobar")]);
let url = rmap
.url_for_map(&req, "post", &input_map)
.unwrap()
.to_string();
assert_eq!(url, OUTPUT);
assert!(rmap.url_for_iter(&req, "missing", ["u123"]).is_err());
assert!(rmap.url_for_map(&req, "missing", &input_map).is_err());
} }
#[test] #[test]
@ -480,17 +524,32 @@ mod tests {
req.set_server_hostname("localhost:8888"); req.set_server_hostname("localhost:8888");
let req = req.to_http_request(); let req = req.to_http_request();
const INPUT: &[&str] = &["a/../quick brown%20fox/%nan?query#frag"]; const INPUT: &str = "a/../quick brown%20fox/%nan?query#frag";
const ITERABLE_INPUT: &[&str] = &[INPUT];
let map_input = HashMap::from([("var", INPUT), ("extra", "")]);
const OUTPUT: &str = "/quick%20brown%20fox/%nan%3Fquery%23frag"; const OUTPUT: &str = "/quick%20brown%20fox/%nan%3Fquery%23frag";
let url = rmap.url_for(&req, "internal", INPUT).unwrap(); let url = rmap.url_for_iter(&req, "internal", ITERABLE_INPUT).unwrap();
assert_eq!(url.path(), OUTPUT);
let url = rmap.url_for_map(&req, "internal", &map_input).unwrap();
assert_eq!(url.path(), OUTPUT); assert_eq!(url.path(), OUTPUT);
let url = rmap.url_for(&req, "external.1", INPUT).unwrap(); let url = rmap
.url_for_iter(&req, "external.1", ITERABLE_INPUT)
.unwrap();
assert_eq!(url.path(), OUTPUT);
let url = rmap.url_for_map(&req, "external.1", &map_input).unwrap();
assert_eq!(url.path(), OUTPUT); assert_eq!(url.path(), OUTPUT);
assert!(rmap.url_for(&req, "external.2", INPUT).is_err()); assert!(rmap
assert!(rmap.url_for(&req, "external.2", [""]).is_err()); .url_for_iter(&req, "external.2", ITERABLE_INPUT)
.is_err());
assert!(rmap.url_for_map(&req, "external.2", &map_input).is_err());
let empty_map: HashMap<&str, &str> = HashMap::new();
assert!(rmap.url_for_iter(&req, "external.2", [""]).is_err());
assert!(rmap.url_for_map(&req, "external.2", &empty_map).is_err());
} }
#[test] #[test]
@ -523,10 +582,22 @@ mod tests {
req.set_server_hostname("localhost:8888"); req.set_server_hostname("localhost:8888");
let req = req.to_http_request(); let req = req.to_http_request();
const OUTPUT: &str = "https://duck.com/abcd";
assert_eq!( assert_eq!(
rmap.url_for(&req, "duck", ["abcd"]).unwrap().to_string(), rmap.url_for_iter(&req, "duck", ["abcd"])
"https://duck.com/abcd" .unwrap()
.to_string(),
OUTPUT
); );
let input_map = HashMap::from([("query", "abcd")]);
assert_eq!(
rmap.url_for_map(&req, "duck", &input_map)
.unwrap()
.to_string(),
OUTPUT
)
} }
#[test] #[test]
@ -552,9 +623,22 @@ mod tests {
let req = crate::test::TestRequest::default().to_http_request(); let req = crate::test::TestRequest::default().to_http_request();
let url = rmap.url_for(&req, "nested", [""; 0]).unwrap().to_string(); const OUTPUT: &str = "http://localhost:8080/bar/nested";
assert_eq!(url, "http://localhost:8080/bar/nested");
assert!(rmap.url_for(&req, "missing", ["u123"]).is_err()); let url = rmap
.url_for_iter(&req, "nested", [""; 0])
.unwrap()
.to_string();
assert_eq!(url, OUTPUT);
let empty_map: HashMap<&str, &str> = HashMap::new();
let url = rmap
.url_for_map(&req, "nested", &empty_map)
.unwrap()
.to_string();
assert_eq!(url, OUTPUT);
assert!(rmap.url_for_iter(&req, "missing", ["u123"]).is_err());
assert!(rmap.url_for_map(&req, "missing", &empty_map).is_err());
} }
} }