1
0
mirror of https://github.com/fafhrd91/actix-web synced 2024-11-27 17:52:56 +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
### 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
### Added

View File

@ -1,6 +1,10 @@
use std::{
borrow,
cell::{Ref, RefCell, RefMut},
fmt, net,
collections::HashMap,
fmt,
hash::{BuildHasher, Hash},
net,
rc::Rc,
str,
};
@ -230,7 +234,7 @@ impl HttpRequest {
///
/// let app = App::new()
/// .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()))
/// );
/// ```
@ -239,7 +243,46 @@ impl HttpRequest {
U: IntoIterator<Item = I>,
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
@ -621,17 +664,30 @@ mod tests {
.rmap(rmap)
.to_http_request();
let input_map = HashMap::from([("non-existing", "test")]);
assert_eq!(
req.url_for("unknown", ["test"]),
Err(UrlGenerationError::ResourceNotFound)
);
assert_eq!(
req.url_for_map("unknown", &input_map),
Err(UrlGenerationError::ResourceNotFound)
);
assert_eq!(
req.url_for("index", ["test"]),
Err(UrlGenerationError::NotEnoughElements)
);
let url = req.url_for("index", ["test", "html"]);
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"
);
}
@ -685,9 +741,11 @@ mod tests {
rmap.add(&mut rdef, None);
let req = TestRequest::default().rmap(rmap).to_http_request();
let input_map = HashMap::from([("video_id", "oHg5SJYRHA0")]);
let url = req.url_for("youtube", ["oHg5SJYRHA0"]);
assert_eq!(url, req.url_for_map("youtube", &input_map));
assert_eq!(
url.ok().unwrap().as_str(),
url.unwrap().as_str(),
"https://youtube.com/watch/oHg5SJYRHA0"
);
}

View File

@ -1,7 +1,9 @@
use std::{
borrow::Cow,
borrow::{Borrow, Cow},
cell::RefCell,
collections::HashMap,
fmt::Write as _,
hash::{BuildHasher, Hash},
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.
pub fn url_for<U, I>(
pub fn url_for_iter<U, I>(
&self,
req: &HttpRequest,
name: &str,
@ -128,16 +130,48 @@ impl ResourceMap {
I: AsRef<str>,
{
let mut elements = elements.into_iter();
let path = self
.named
.get(name)
.ok_or(UrlGenerationError::ResourceNotFound)?
.root_rmap_fn(String::with_capacity(AVG_PATH_LEN), |mut acc, node| {
self.url_for(req, name, |mut acc, node: &ResourceMap| {
node.pattern
.resource_path_from_iter(&mut acc, &mut elements)
.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)?;
let (base, path): (Cow<'_, _>, _) = if path.starts_with('/') {
@ -448,13 +482,23 @@ mod tests {
req.set_server_hostname("localhost:8888");
let req = req.to_http_request();
const OUTPUT: &str = "http://localhost:8888/user/u123/post/foobar";
let url = rmap
.url_for(&req, "post", ["u123", "foobar"])
.url_for_iter(&req, "post", ["u123", "foobar"])
.unwrap()
.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]
@ -480,17 +524,32 @@ mod tests {
req.set_server_hostname("localhost:8888");
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";
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);
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!(rmap.url_for(&req, "external.2", INPUT).is_err());
assert!(rmap.url_for(&req, "external.2", [""]).is_err());
assert!(rmap
.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]
@ -523,10 +582,22 @@ mod tests {
req.set_server_hostname("localhost:8888");
let req = req.to_http_request();
const OUTPUT: &str = "https://duck.com/abcd";
assert_eq!(
rmap.url_for(&req, "duck", ["abcd"]).unwrap().to_string(),
"https://duck.com/abcd"
rmap.url_for_iter(&req, "duck", ["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]
@ -552,9 +623,22 @@ mod tests {
let req = crate::test::TestRequest::default().to_http_request();
let url = rmap.url_for(&req, "nested", [""; 0]).unwrap().to_string();
assert_eq!(url, "http://localhost:8080/bar/nested");
const OUTPUT: &str = "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());
}
}