1
0
mirror of https://github.com/fafhrd91/actix-web synced 2024-11-28 01:52:57 +01:00

rework ResourceMap internals (#2337)

This commit is contained in:
Ali MJ Al-Nasrawy 2021-09-01 06:48:43 +03:00 committed by GitHub
parent 7d01ece355
commit 373b3f91df
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 233 additions and 202 deletions

View File

@ -79,7 +79,7 @@ where
.into_iter() .into_iter()
.for_each(|mut srv| srv.register(&mut config)); .for_each(|mut srv| srv.register(&mut config));
let mut rmap = ResourceMap::new(ResourceDef::new("")); let mut rmap = ResourceMap::new(ResourceDef::prefix(""));
let (config, services) = config.into_services(); let (config, services) = config.into_services();
@ -104,7 +104,7 @@ where
// complete ResourceMap tree creation // complete ResourceMap tree creation
let rmap = Rc::new(rmap); let rmap = Rc::new(rmap);
rmap.finish(rmap.clone()); ResourceMap::finish(&rmap);
// construct all async data factory futures // construct all async data factory futures
let factory_futs = join_all(self.async_data_factories.iter().map(|f| f())); let factory_futs = join_all(self.async_data_factories.iter().map(|f| f()));

View File

@ -511,7 +511,7 @@ mod tests {
let mut res = ResourceDef::new("/user/{name}.{ext}"); let mut res = ResourceDef::new("/user/{name}.{ext}");
res.set_name("index"); res.set_name("index");
let mut rmap = ResourceMap::new(ResourceDef::new("")); let mut rmap = ResourceMap::new(ResourceDef::prefix(""));
rmap.add(&mut res, None); rmap.add(&mut res, None);
assert!(rmap.has_resource("/user/test.html")); assert!(rmap.has_resource("/user/test.html"));
assert!(!rmap.has_resource("/test/unknown")); assert!(!rmap.has_resource("/test/unknown"));
@ -541,7 +541,7 @@ mod tests {
let mut rdef = ResourceDef::new("/index.html"); let mut rdef = ResourceDef::new("/index.html");
rdef.set_name("index"); rdef.set_name("index");
let mut rmap = ResourceMap::new(ResourceDef::new("")); let mut rmap = ResourceMap::new(ResourceDef::prefix(""));
rmap.add(&mut rdef, None); rmap.add(&mut rdef, None);
assert!(rmap.has_resource("/index.html")); assert!(rmap.has_resource("/index.html"));
@ -562,7 +562,7 @@ mod tests {
let mut rdef = ResourceDef::new("/index.html"); let mut rdef = ResourceDef::new("/index.html");
rdef.set_name("index"); rdef.set_name("index");
let mut rmap = ResourceMap::new(ResourceDef::new("")); let mut rmap = ResourceMap::new(ResourceDef::prefix(""));
rmap.add(&mut rdef, None); rmap.add(&mut rdef, None);
assert!(rmap.has_resource("/index.html")); assert!(rmap.has_resource("/index.html"));
@ -581,9 +581,8 @@ mod tests {
rdef.set_name("youtube"); rdef.set_name("youtube");
let mut rmap = ResourceMap::new(ResourceDef::new("")); let mut rmap = ResourceMap::new(ResourceDef::prefix(""));
rmap.add(&mut rdef, None); rmap.add(&mut rdef, None);
assert!(rmap.has_resource("https://youtube.com/watch/unknown"));
let req = TestRequest::default().rmap(rmap).to_http_request(); let req = TestRequest::default().rmap(rmap).to_http_request();
let url = req.url_for("youtube", &["oHg5SJYRHA0"]); let url = req.url_for("youtube", &["oHg5SJYRHA0"]);

View File

@ -10,43 +10,75 @@ use crate::request::HttpRequest;
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct ResourceMap { pub struct ResourceMap {
root: ResourceDef, pattern: ResourceDef,
/// Named resources within the tree or, for external resources,
/// it points to isolated nodes outside the tree.
named: AHashMap<String, Rc<ResourceMap>>,
parent: RefCell<Weak<ResourceMap>>, parent: RefCell<Weak<ResourceMap>>,
named: AHashMap<String, ResourceDef>,
patterns: Vec<(ResourceDef, Option<Rc<ResourceMap>>)>, /// Must be `None` for "edge" nodes.
nodes: Option<Vec<Rc<ResourceMap>>>,
} }
impl ResourceMap { impl ResourceMap {
/// Creates a _container_ node in the `ResourceMap` tree.
pub fn new(root: ResourceDef) -> Self { pub fn new(root: ResourceDef) -> Self {
ResourceMap { ResourceMap {
root, pattern: root,
parent: RefCell::new(Weak::new()),
named: AHashMap::default(), named: AHashMap::default(),
patterns: Vec::new(), parent: RefCell::new(Weak::new()),
nodes: Some(Vec::new()),
} }
} }
/// Adds a (possibly nested) resource.
///
/// To add a non-prefix pattern, `nested` must be `None`.
/// To add external resource, supply a pattern without a leading `/`.
/// The root pattern of `nested`, if present, should match `pattern`.
pub fn add(&mut self, pattern: &mut ResourceDef, nested: Option<Rc<ResourceMap>>) { pub fn add(&mut self, pattern: &mut ResourceDef, nested: Option<Rc<ResourceMap>>) {
pattern.set_id(self.patterns.len() as u16); pattern.set_id(self.nodes.as_ref().unwrap().len() as u16);
self.patterns.push((pattern.clone(), nested));
if let Some(name) = pattern.name() { if let Some(new_node) = nested {
self.named.insert(name.to_owned(), pattern.clone()); assert_eq!(&new_node.pattern, pattern, "`patern` and `nested` mismatch");
self.named.extend(new_node.named.clone().into_iter());
self.nodes.as_mut().unwrap().push(new_node);
} else {
let new_node = Rc::new(ResourceMap {
pattern: pattern.clone(),
named: AHashMap::default(),
parent: RefCell::new(Weak::new()),
nodes: None,
});
if let Some(name) = pattern.name() {
self.named.insert(name.to_owned(), Rc::clone(&new_node));
}
let is_external = match pattern.pattern() {
Some(p) => !p.is_empty() && !p.starts_with('/'),
None => false,
};
// Don't add external resources to the tree
if !is_external {
self.nodes.as_mut().unwrap().push(new_node);
}
} }
} }
pub(crate) fn finish(&self, current: Rc<ResourceMap>) { pub(crate) fn finish(self: &Rc<Self>) {
for (_, nested) in &self.patterns { for node in self.nodes.iter().flatten() {
if let Some(ref nested) = nested { node.parent.replace(Rc::downgrade(self));
*nested.parent.borrow_mut() = Rc::downgrade(&current); ResourceMap::finish(node);
nested.finish(nested.clone());
}
} }
} }
/// Generate url for named resource /// Generate url for named resource
/// ///
/// Check [`HttpRequest::url_for()`](../struct.HttpRequest.html#method. /// Check [`HttpRequest::url_for`] for detailed information.
/// url_for) for detailed information.
pub fn url_for<U, I>( pub fn url_for<U, I>(
&self, &self,
req: &HttpRequest, req: &HttpRequest,
@ -57,197 +89,97 @@ impl ResourceMap {
U: IntoIterator<Item = I>, U: IntoIterator<Item = I>,
I: AsRef<str>, I: AsRef<str>,
{ {
let mut path = String::new();
let mut elements = elements.into_iter(); let mut elements = elements.into_iter();
if self.patterns_for(name, &mut path, &mut elements)?.is_some() { let path = self
if path.starts_with('/') { .named
let conn = req.connection_info(); .get(name)
Ok(Url::parse(&format!( .ok_or(UrlGenerationError::ResourceNotFound)?
"{}://{}{}", .root_rmap_fn(String::with_capacity(24), |mut acc, node| {
conn.scheme(), node.pattern
conn.host(), .resource_path_from_iter(&mut acc, &mut elements)
path .then(|| acc)
))?) })
} else { .ok_or(UrlGenerationError::NotEnoughElements)?;
Ok(Url::parse(&path)?)
} if path.starts_with('/') {
let conn = req.connection_info();
Ok(Url::parse(&format!(
"{}://{}{}",
conn.scheme(),
conn.host(),
path
))?)
} else { } else {
Err(UrlGenerationError::ResourceNotFound) Ok(Url::parse(&path)?)
} }
} }
pub fn has_resource(&self, path: &str) -> bool { pub fn has_resource(&self, path: &str) -> bool {
let path = if path.is_empty() { "/" } else { path }; self.find_matching_node(path).is_some()
for (pattern, rmap) in &self.patterns {
if let Some(ref rmap) = rmap {
if let Some(pat_len) = pattern.find_match(path) {
return rmap.has_resource(&path[pat_len..]);
}
} else if pattern.is_match(path) || pattern.pattern() == Some("") && path == "/" {
return true;
}
}
false
} }
/// Returns the name of the route that matches the given path or None if no full match /// Returns the name of the route that matches the given path or None if no full match
/// is possible. /// is possible or the matching resource is not named.
pub fn match_name(&self, path: &str) -> Option<&str> { pub fn match_name(&self, path: &str) -> Option<&str> {
let path = if path.is_empty() { "/" } else { path }; self.find_matching_node(path)?.pattern.name()
for (pattern, rmap) in &self.patterns {
if let Some(ref rmap) = rmap {
if let Some(plen) = pattern.find_match(path) {
return rmap.match_name(&path[plen..]);
}
} else if pattern.is_match(path) {
return pattern.name();
}
}
None
} }
/// Returns the full resource pattern matched against a path or None if no full match /// Returns the full resource pattern matched against a path or None if no full match
/// is possible. /// is possible.
pub fn match_pattern(&self, path: &str) -> Option<String> { pub fn match_pattern(&self, path: &str) -> Option<String> {
let path = if path.is_empty() { "/" } else { path }; self.find_matching_node(path)?.root_rmap_fn(
String::with_capacity(24),
// ensure a full match exists |mut acc, node| {
if !self.has_resource(path) { acc.push_str(node.pattern.pattern()?);
return None; Some(acc)
} },
)
Some(self.traverse_resource_pattern(path))
} }
/// Takes remaining path and tries to match it up against a resource definition within the fn find_matching_node(&self, path: &str) -> Option<&ResourceMap> {
/// current resource map recursively, returning a concatenation of all resource prefixes and self._find_matching_node(path).flatten()
/// patterns matched in the tree.
///
/// Should only be used after checking the resource exists in the map so that partial match
/// patterns are not returned.
fn traverse_resource_pattern(&self, remaining: &str) -> String {
for (pattern, rmap) in &self.patterns {
if let Some(ref rmap) = rmap {
if let Some(prefix_len) = pattern.find_match(remaining) {
// TODO: think about unwrap_or
let prefix = pattern.pattern().unwrap_or("").to_owned();
return [
prefix,
rmap.traverse_resource_pattern(&remaining[prefix_len..]),
]
.concat();
}
} else if pattern.is_match(remaining) {
// TODO: think about unwrap_or
return pattern.pattern().unwrap_or("").to_owned();
}
}
String::new()
} }
fn patterns_for<U, I>( /// Returns `None` if root pattern doesn't match;
&self, /// `Some(None)` if root pattern matches but there is no matching child pattern.
name: &str, /// Don't search sideways when `Some(none)` is returned.
path: &mut String, fn _find_matching_node(&self, path: &str) -> Option<Option<&ResourceMap>> {
elements: &mut U, let matched_len = self.pattern.find_match(path)?;
) -> Result<Option<()>, UrlGenerationError> let path = &path[matched_len..];
Some(match &self.nodes {
// find first sub-node to match remaining path
Some(nodes) => nodes
.iter()
.filter_map(|node| node._find_matching_node(path))
.next()
.flatten(),
// only terminate at edge nodes
None => Some(self),
})
}
/// Find `self`'s highest ancestor and then run `F`, providing `B`, in that rmap context.
fn root_rmap_fn<F, B>(&self, init: B, mut f: F) -> Option<B>
where where
U: Iterator<Item = I>, F: FnMut(B, &ResourceMap) -> Option<B>,
I: AsRef<str>,
{ {
if self.pattern_for(name, path, elements)?.is_some() { self._root_rmap_fn(init, &mut f)
Ok(Some(()))
} else {
self.parent_pattern_for(name, path, elements)
}
} }
fn pattern_for<U, I>( /// Run `F`, providing `B`, if `self` is top-level resource map, else recurse to parent map.
&self, fn _root_rmap_fn<F, B>(&self, init: B, f: &mut F) -> Option<B>
name: &str,
path: &mut String,
elements: &mut U,
) -> Result<Option<()>, UrlGenerationError>
where where
U: Iterator<Item = I>, F: FnMut(B, &ResourceMap) -> Option<B>,
I: AsRef<str>,
{ {
if let Some(pattern) = self.named.get(name) { let data = match self.parent.borrow().upgrade() {
if pattern Some(ref parent) => parent._root_rmap_fn(init, f)?,
.pattern() None => init,
.map(|pat| pat.starts_with('/')) };
.unwrap_or(false)
{
self.fill_root(path, elements)?;
}
if pattern.resource_path_from_iter(path, elements) { f(data, self)
Ok(Some(()))
} else {
Err(UrlGenerationError::NotEnoughElements)
}
} else {
for (_, rmap) in &self.patterns {
if let Some(ref rmap) = rmap {
if rmap.pattern_for(name, path, elements)?.is_some() {
return Ok(Some(()));
}
}
}
Ok(None)
}
}
fn fill_root<U, I>(
&self,
path: &mut String,
elements: &mut U,
) -> Result<(), UrlGenerationError>
where
U: Iterator<Item = I>,
I: AsRef<str>,
{
if let Some(ref parent) = self.parent.borrow().upgrade() {
parent.fill_root(path, elements)?;
}
if self.root.resource_path_from_iter(path, elements) {
Ok(())
} else {
Err(UrlGenerationError::NotEnoughElements)
}
}
fn parent_pattern_for<U, I>(
&self,
name: &str,
path: &mut String,
elements: &mut U,
) -> Result<Option<()>, UrlGenerationError>
where
U: Iterator<Item = I>,
I: AsRef<str>,
{
if let Some(ref parent) = self.parent.borrow().upgrade() {
if let Some(pattern) = parent.named.get(name) {
self.fill_root(path, elements)?;
if pattern.resource_path_from_iter(path, elements) {
Ok(Some(()))
} else {
Err(UrlGenerationError::NotEnoughElements)
}
} else {
parent.parent_pattern_for(name, path, elements)
}
} else {
Ok(None)
}
} }
} }
@ -259,7 +191,7 @@ mod tests {
fn extract_matched_pattern() { fn extract_matched_pattern() {
let mut root = ResourceMap::new(ResourceDef::root_prefix("")); let mut root = ResourceMap::new(ResourceDef::root_prefix(""));
let mut user_map = ResourceMap::new(ResourceDef::root_prefix("")); let mut user_map = ResourceMap::new(ResourceDef::root_prefix("/user/{id}"));
user_map.add(&mut ResourceDef::new("/"), None); user_map.add(&mut ResourceDef::new("/"), None);
user_map.add(&mut ResourceDef::new("/profile"), None); user_map.add(&mut ResourceDef::new("/profile"), None);
user_map.add(&mut ResourceDef::new("/article/{id}"), None); user_map.add(&mut ResourceDef::new("/article/{id}"), None);
@ -275,9 +207,10 @@ mod tests {
&mut ResourceDef::root_prefix("/user/{id}"), &mut ResourceDef::root_prefix("/user/{id}"),
Some(Rc::new(user_map)), Some(Rc::new(user_map)),
); );
root.add(&mut ResourceDef::new("/info"), None);
let root = Rc::new(root); let root = Rc::new(root);
root.finish(Rc::clone(&root)); ResourceMap::finish(&root);
// sanity check resource map setup // sanity check resource map setup
@ -288,7 +221,7 @@ mod tests {
assert!(root.has_resource("/v2")); assert!(root.has_resource("/v2"));
assert!(!root.has_resource("/v33")); assert!(!root.has_resource("/v33"));
assert!(root.has_resource("/user/22")); assert!(!root.has_resource("/user/22"));
assert!(root.has_resource("/user/22/")); assert!(root.has_resource("/user/22/"));
assert!(root.has_resource("/user/22/profile")); assert!(root.has_resource("/user/22/profile"));
@ -336,7 +269,7 @@ mod tests {
rdef.set_name("root_info"); rdef.set_name("root_info");
root.add(&mut rdef, None); root.add(&mut rdef, None);
let mut user_map = ResourceMap::new(ResourceDef::root_prefix("")); let mut user_map = ResourceMap::new(ResourceDef::root_prefix("/user/{id}"));
let mut rdef = ResourceDef::new("/"); let mut rdef = ResourceDef::new("/");
user_map.add(&mut rdef, None); user_map.add(&mut rdef, None);
@ -350,14 +283,14 @@ mod tests {
); );
let root = Rc::new(root); let root = Rc::new(root);
root.finish(Rc::clone(&root)); ResourceMap::finish(&root);
// sanity check resource map setup // sanity check resource map setup
assert!(root.has_resource("/info")); assert!(root.has_resource("/info"));
assert!(!root.has_resource("/bar")); assert!(!root.has_resource("/bar"));
assert!(root.has_resource("/user/22")); assert!(!root.has_resource("/user/22"));
assert!(root.has_resource("/user/22/")); assert!(root.has_resource("/user/22/"));
assert!(root.has_resource("/user/22/post/55")); assert!(root.has_resource("/user/22/post/55"));
@ -377,7 +310,7 @@ mod tests {
// ref: https://github.com/actix/actix-web/issues/1582 // ref: https://github.com/actix/actix-web/issues/1582
let mut root = ResourceMap::new(ResourceDef::root_prefix("")); let mut root = ResourceMap::new(ResourceDef::root_prefix(""));
let mut user_map = ResourceMap::new(ResourceDef::root_prefix("")); let mut user_map = ResourceMap::new(ResourceDef::root_prefix("/user/{id}"));
user_map.add(&mut ResourceDef::new("/"), None); user_map.add(&mut ResourceDef::new("/"), None);
user_map.add(&mut ResourceDef::new("/profile"), None); user_map.add(&mut ResourceDef::new("/profile"), None);
user_map.add(&mut ResourceDef::new("/article/{id}"), None); user_map.add(&mut ResourceDef::new("/article/{id}"), None);
@ -393,20 +326,119 @@ mod tests {
); );
let root = Rc::new(root); let root = Rc::new(root);
root.finish(Rc::clone(&root)); ResourceMap::finish(&root);
// check root has no parent // check root has no parent
assert!(root.parent.borrow().upgrade().is_none()); assert!(root.parent.borrow().upgrade().is_none());
// check child has parent reference // check child has parent reference
assert!(root.patterns[0].1.is_some()); assert!(root.nodes.as_ref().unwrap()[0]
.parent
.borrow()
.upgrade()
.is_some());
// check child's parent root id matches root's root id // check child's parent root id matches root's root id
assert_eq!( assert!(Rc::ptr_eq(
root.patterns[0].1.as_ref().unwrap().root.id(), &root.nodes.as_ref().unwrap()[0]
root.root.id() .parent
); .borrow()
.upgrade()
.unwrap(),
&root
));
let output = format!("{:?}", root); let output = format!("{:?}", root);
assert!(output.starts_with("ResourceMap {")); assert!(output.starts_with("ResourceMap {"));
assert!(output.ends_with(" }")); assert!(output.ends_with(" }"));
} }
#[test]
fn short_circuit() {
let mut root = ResourceMap::new(ResourceDef::prefix(""));
let mut user_root = ResourceDef::prefix("/user");
let mut user_map = ResourceMap::new(user_root.clone());
user_map.add(&mut ResourceDef::new("/u1"), None);
user_map.add(&mut ResourceDef::new("/u2"), None);
root.add(&mut ResourceDef::new("/user/u3"), None);
root.add(&mut user_root, Some(Rc::new(user_map)));
root.add(&mut ResourceDef::new("/user/u4"), None);
let rmap = Rc::new(root);
ResourceMap::finish(&rmap);
assert!(rmap.has_resource("/user/u1"));
assert!(rmap.has_resource("/user/u2"));
assert!(rmap.has_resource("/user/u3"));
assert!(!rmap.has_resource("/user/u4"));
}
#[test]
fn url_for() {
let mut root = ResourceMap::new(ResourceDef::prefix(""));
let mut user_scope_rdef = ResourceDef::prefix("/user");
let mut user_scope_map = ResourceMap::new(user_scope_rdef.clone());
let mut user_rdef = ResourceDef::new("/{user_id}");
let mut user_map = ResourceMap::new(user_rdef.clone());
let mut post_rdef = ResourceDef::new("/post/{sub_id}");
post_rdef.set_name("post");
user_map.add(&mut post_rdef, None);
user_scope_map.add(&mut user_rdef, Some(Rc::new(user_map)));
root.add(&mut user_scope_rdef, Some(Rc::new(user_scope_map)));
let rmap = Rc::new(root);
ResourceMap::finish(&rmap);
let mut req = crate::test::TestRequest::default();
req.set_server_hostname("localhost:8888");
let req = req.to_http_request();
let url = rmap
.url_for(&req, "post", &["u123", "foobar"])
.unwrap()
.to_string();
assert_eq!(url, "http://localhost:8888/user/u123/post/foobar");
assert!(rmap.url_for(&req, "missing", &["u123"]).is_err());
}
#[test]
fn external_resource_with_no_name() {
let mut root = ResourceMap::new(ResourceDef::prefix(""));
let mut rdef = ResourceDef::new("https://duck.com/{query}");
root.add(&mut rdef, None);
let rmap = Rc::new(root);
ResourceMap::finish(&rmap);
assert!(!rmap.has_resource("https://duck.com/abc"));
}
#[test]
fn external_resource_with_name() {
let mut root = ResourceMap::new(ResourceDef::prefix(""));
let mut rdef = ResourceDef::new("https://duck.com/{query}");
rdef.set_name("duck");
root.add(&mut rdef, None);
let rmap = Rc::new(root);
ResourceMap::finish(&rmap);
assert!(!rmap.has_resource("https://duck.com/abc"));
let mut req = crate::test::TestRequest::default();
req.set_server_hostname("localhost:8888");
let req = req.to_http_request();
assert_eq!(
rmap.url_for(&req, "duck", &["abcd"]).unwrap().to_string(),
"https://duck.com/abcd"
);
}
} }