Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added HttpRequest::url_for_map #3409

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions actix-web/CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
68 changes: 63 additions & 5 deletions actix-web/src/request.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
use std::{
borrow,
cell::{Ref, RefCell, RefMut},
fmt, net,
collections::HashMap,
fmt,
hash::{BuildHasher, Hash},
net,
rc::Rc,
str,
};
Expand Down Expand Up @@ -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()))
/// );
/// ```
Expand All @@ -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
Expand Down Expand Up @@ -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)
);
assert_eq!(
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.ok().unwrap().as_str(),
url.unwrap().as_str(),
"http://www.rust-lang.org/user/test.html"
);
}
Expand Down Expand Up @@ -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"
);
}
Expand Down
126 changes: 105 additions & 21 deletions actix-web/src/rmap.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
use std::{
borrow::Cow,
borrow::{Borrow, Cow},
cell::RefCell,
collections::HashMap,
fmt::Write as _,
hash::{BuildHasher, Hash},
rc::{Rc, Weak},
};

Expand Down Expand Up @@ -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,
Expand All @@ -128,16 +130,48 @@ impl ResourceMap {
I: AsRef<str>,
{
let mut elements = elements.into_iter();
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), |mut acc, node| {
node.pattern
.resource_path_from_iter(&mut acc, &mut elements)
.then_some(acc)
})
.root_rmap_fn(String::with_capacity(AVG_PATH_LEN), map_fn)
.ok_or(UrlGenerationError::NotEnoughElements)?;

let (base, path): (Cow<'_, _>, _) = if path.starts_with('/') {
Expand Down Expand Up @@ -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_iter(&req, "post", ["u123", "foobar"])
.unwrap()
.to_string();
assert_eq!(url, OUTPUT);

let input_map = HashMap::from([("user_id", "u123"), ("sub_id", "foobar")]);
let url = rmap
.url_for(&req, "post", ["u123", "foobar"])
.url_for_map(&req, "post", &input_map)
.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());
assert!(rmap.url_for_iter(&req, "missing", ["u123"]).is_err());
assert!(rmap.url_for_map(&req, "missing", &input_map).is_err());
}

#[test]
Expand All @@ -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]
Expand Down Expand Up @@ -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]
Expand All @@ -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";

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(&req, "missing", ["u123"]).is_err());
assert!(rmap.url_for_iter(&req, "missing", ["u123"]).is_err());
assert!(rmap.url_for_map(&req, "missing", &empty_map).is_err());
}
}
Loading