diff --git a/README.md b/README.md index 52ceeeb..82e9b03 100644 --- a/README.md +++ b/README.md @@ -29,62 +29,91 @@ The router supports dynamic route segments. These can either be named or catch-a Named parameters like `/{id}` match anything until the next static segment or the end of the path. ```rust,ignore -let mut m = Router::new(); -m.insert("/users/{id}", true)?; +let mut router = Router::new(); +router.insert("/users/{id}", 42)?; -assert_eq!(m.at("/users/1")?.params.get("id"), Some("1")); -assert_eq!(m.at("/users/23")?.params.get("id"), Some("23")); -assert!(m.at("/users").is_err()); +let matched = router.at("/users/1")?; +assert_eq!(matched.params.get("id"), Some("1")); + +let matched = router.at("/users/23")?; +assert_eq!(matched.params.get("id"), Some("23")); + +assert!(router.at("/users").is_err()); ``` Prefixes and suffixes within a segment are also supported. However, there may only be a single named parameter per route segment. ```rust,ignore -let mut m = Router::new(); -m.insert("/images/img{id}.png", true)?; +let mut router = Router::new(); +router.insert("/images/img-{id}.png", true)?; + +let matched = router.at("/images/img-1.png")?; +assert_eq!(matched.params.get("id"), Some("1")); -assert_eq!(m.at("/images/img1.png")?.params.get("id"), Some("1")); -assert!(m.at("/images/img1.jpg").is_err()); +assert!(router.at("/images/img-1.jpg").is_err()); ``` -Catch-all parameters start with `*` and match anything until the end of the path. They must always be at the **end** of the route. +Catch-all parameters start with a `*` and match anything until the end of the path. They must always be at the *end* of the route. ```rust,ignore -let mut m = Router::new(); -m.insert("/{*p}", true)?; +let mut router = Router::new(); +router.insert("/{*rest}", true)?; -assert_eq!(m.at("/foo.js")?.params.get("p"), Some("foo.js")); -assert_eq!(m.at("/c/bar.css")?.params.get("p"), Some("c/bar.css")); +let matched = router.at("/foo.html")?; +assert_eq!(matched.params.get("rest"), Some("foo.html")); -// Note that this would lead to an empty parameter. -assert!(m.at("/").is_err()); +let matched = router.at("/static/bar.css")?; +assert_eq!(matched.params.get("rest"), Some("static/bar.css")); + +// Note that this would lead to an empty parameter value. +assert!(router.at("/").is_err()); ``` The literal characters `{` and `}` may be included in a static route by escaping them with the same character. -For example, the `{` character is escaped with `{{` and the `}` character is escaped with `}}`. +For example, the `{` character is escaped with `{{`, and the `}` character is escaped with `}}`. ```rust,ignore -let mut m = Router::new(); -m.insert("/{{hello}}", true)?; -m.insert("/{hello}", true)?; +let mut router = Router::new(); +router.insert("/{{hello}}", true)?; +router.insert("/{hello}", true)?; // Match the static route. -assert!(m.at("/{hello}")?.value); +let matched = router.at("/{hello}")?; +assert!(matched.params.is_empty()); // Match the dynamic route. -assert_eq!(m.at("/hello")?.params.get("hello"), Some("hello")); +let matched = router.at("/hello")?; +assert_eq!(matched.params.get("hello"), Some("hello")); ``` -## Routing Priority +## Conflict Rules Static and dynamic route segments are allowed to overlap. If they do, static segments will be given higher priority: ```rust,ignore -let mut m = Router::new(); -m.insert("/", "Welcome!").unwrap(); // Priority: 1 -m.insert("/about", "About Me").unwrap(); // Priority: 1 -m.insert("/{*filepath}", "...").unwrap(); // Priority: 2 +let mut router = Router::new(); +router.insert("/", "Welcome!").unwrap(); // Priority: 1 +router.insert("/about", "About Me").unwrap(); // Priority: 1 +router.insert("/{*filepath}", "...").unwrap(); // Priority: 2 ``` +Formally, a route consists of a list of segments separated by `/`, with an optional leading and trailing slash: `(/)/.../(/)`. + +Given set of routes, their overlapping segments may include, in order of priority: + +- Any number of static segments (`/a`, `/b`, ...). +- *One* of the following: + - Any number of route parameters with a suffix (`/{x}a`, `/{x}b`, ...), prioritizing the longest suffix. + - Any number of route parameters with a prefix (`/a{x}`, `/b{x}`, ...), prioritizing the longest prefix. + - A single route parameter with both a prefix and a suffix (`/a{x}b`). +- *One* of the following; + - A single standalone parameter (`/{x}`). + - A single standalone catch-all parameter (`/{*rest}`). Note this only applies to the final route segment. + +Any other combination of route segments is considered ambiguous, and attempting to insert such a route will result in an error. + +The one exception to the above set of rules is that catch-all parameters are always considered to conflict with suffixed route parameters, i.e. that `/{*rest}` +and `/{x}suffix` are overlapping. This is due to an implementation detail of the routing tree that may be relaxed in the future. + ## How does it work? The router takes advantage of the fact that URL routes generally follow a hierarchical structure. diff --git a/src/error.rs b/src/error.rs index fd9feed..1c18afc 100644 --- a/src/error.rs +++ b/src/error.rs @@ -79,7 +79,7 @@ impl InsertError { route.append(¤t.prefix); } - // Add the prefixes of any conflicting children. + // Add the prefixes of the first conflicting child. let mut child = current.children.first(); while let Some(node) = child { route.append(&node.prefix); diff --git a/src/lib.rs b/src/lib.rs index c36d18b..6ef310b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -26,12 +26,16 @@ Named parameters like `/{id}` match anything until the next static segment or th ```rust # use matchit::Router; # fn main() -> Result<(), Box> { -let mut m = Router::new(); -m.insert("/users/{id}", true)?; +let mut router = Router::new(); +router.insert("/users/{id}", 42)?; -assert_eq!(m.at("/users/1")?.params.get("id"), Some("1")); -assert_eq!(m.at("/users/23")?.params.get("id"), Some("23")); -assert!(m.at("/users").is_err()); +let matched = router.at("/users/1")?; +assert_eq!(matched.params.get("id"), Some("1")); + +let matched = router.at("/users/23")?; +assert_eq!(matched.params.get("id"), Some("23")); + +assert!(router.at("/users").is_err()); # Ok(()) # } ``` @@ -40,65 +44,90 @@ Prefixes and suffixes within a segment are also supported. However, there may on ```rust # use matchit::Router; # fn main() -> Result<(), Box> { -let mut m = Router::new(); -m.insert("/images/img{id}.png", true)?; +let mut router = Router::new(); +router.insert("/images/img-{id}.png", true)?; + +let matched = router.at("/images/img-1.png")?; +assert_eq!(matched.params.get("id"), Some("1")); -assert_eq!(m.at("/images/img1.png")?.params.get("id"), Some("1")); -assert!(m.at("/images/img1.jpg").is_err()); +assert!(router.at("/images/img-1.jpg").is_err()); # Ok(()) # } ``` -Catch-all parameters start with `*` and match anything until the end of the path. They must always be at the **end** of the route. +Catch-all parameters start with a `*` and match anything until the end of the path. They must always be at the *end* of the route. ```rust # use matchit::Router; # fn main() -> Result<(), Box> { -let mut m = Router::new(); -m.insert("/{*p}", true)?; +let mut router = Router::new(); +router.insert("/{*rest}", true)?; -assert_eq!(m.at("/foo.js")?.params.get("p"), Some("foo.js")); -assert_eq!(m.at("/c/bar.css")?.params.get("p"), Some("c/bar.css")); +let matched = router.at("/foo.html")?; +assert_eq!(matched.params.get("rest"), Some("foo.html")); -// Note that this would lead to an empty parameter. -assert!(m.at("/").is_err()); +let matched = router.at("/static/bar.css")?; +assert_eq!(matched.params.get("rest"), Some("static/bar.css")); + +// Note that this would lead to an empty parameter value. +assert!(router.at("/").is_err()); # Ok(()) # } ``` -The literal characters `{` and `}` may be included in a static route by escaping them with the same character. For example, the `{` character is escaped with `{{` and the `}` character is escaped with `}}`. +The literal characters `{` and `}` may be included in a static route by escaping them with the same character. For example, the `{` character is escaped with `{{`, and the `}` character is escaped with `}}`. ```rust # use matchit::Router; # fn main() -> Result<(), Box> { -let mut m = Router::new(); -m.insert("/{{hello}}", true)?; -m.insert("/{hello}", true)?; +let mut router = Router::new(); +router.insert("/{{hello}}", true)?; +router.insert("/{hello}", true)?; // Match the static route. -assert!(m.at("/{hello}")?.value); +let matched = router.at("/{hello}")?; +assert!(matched.params.is_empty()); // Match the dynamic route. -assert_eq!(m.at("/hello")?.params.get("hello"), Some("hello")); +let matched = router.at("/hello")?; +assert_eq!(matched.params.get("hello"), Some("hello")); # Ok(()) # } ``` -# Routing Priority +# Conflict Rules Static and dynamic route segments are allowed to overlap. If they do, static segments will be given higher priority: ```rust # use matchit::Router; # fn main() -> Result<(), Box> { -let mut m = Router::new(); -m.insert("/", "Welcome!").unwrap(); // Priority: 1 -m.insert("/about", "About Me").unwrap(); // Priority: 1 -m.insert("/{*filepath}", "...").unwrap(); // Priority: 2 +let mut router = Router::new(); +router.insert("/", "Welcome!").unwrap(); // Priority: 1 +router.insert("/about", "About Me").unwrap(); // Priority: 1 +router.insert("/{*filepath}", "...").unwrap(); // Priority: 2 # Ok(()) # } ``` +Formally, a route consists of a list of segments separated by `/`, with an optional leading and trailing slash: `(/)/.../(/)`. + +Given set of routes, their overlapping segments may include, in order of priority: + +- Any number of static segments (`/a`, `/b`, ...). +- *One* of the following: + - Any number of route parameters with a suffix (`/{x}a`, `/{x}b`, ...), prioritizing the longest suffix. + - Any number of route parameters with a prefix (`/a{x}`, `/b{x}`, ...), prioritizing the longest prefix. + - A single route parameter with both a prefix and a suffix (`/a{x}b`). +- *One* of the following; + - A single standalone parameter (`/{x}`). + - A single standalone catch-all parameter (`/{*rest}`). Note this only applies to the final route segment. + +Any other combination of route segments is considered ambiguous, and attempting to insert such a route will result in an error. + +The one exception to the above set of rules is that catch-all parameters are always considered to conflict with suffixed route parameters, i.e. that `/{*rest}` +and `/{x}suffix` are overlapping. This is due to an implementation detail of the routing tree that may be relaxed in the future. + # How does it work? The router takes advantage of the fact that URL routes generally follow a hierarchical structure. Routes are stored them in a radix trie that makes heavy use of common prefixes. diff --git a/src/router.rs b/src/router.rs index ef29a45..fed5385 100644 --- a/src/router.rs +++ b/src/router.rs @@ -125,7 +125,7 @@ impl Router { /// /// router.insert("/home/{id}/", "Hello!"); /// // Invalid route. - /// assert_eq!(router.remove("/home/{id"), None); + /// assert_eq!(router.remove("/home/{id}"), None); /// assert_eq!(router.remove("/home/{id}/"), Some("Hello!")); /// ``` pub fn remove(&mut self, path: impl Into) -> Option { diff --git a/src/tree.rs b/src/tree.rs index 4ad1f99..3d5e3b1 100644 --- a/src/tree.rs +++ b/src/tree.rs @@ -34,7 +34,7 @@ pub struct Node { // The value stored at this node. // // See `Node::at` for why an `UnsafeCell` is necessary. - value: Option>, + pub(crate) value: Option>, // A parameter name remapping, stored at nodes that hold values. pub(crate) remapping: ParamRemapping, @@ -71,6 +71,52 @@ pub(crate) enum NodeType { unsafe impl Send for Node {} unsafe impl Sync for Node {} +/// Tracks the current node and its parent during insertion. +struct InsertState<'node, T> { + parent: &'node mut Node, + child: Option, +} + +impl<'node, T> InsertState<'node, T> { + /// Returns a reference to the parent node for the traversal. + fn parent(&self) -> Option<&Node> { + match self.child { + None => None, + Some(_) => Some(self.parent), + } + } + + /// Returns a reference to the current node in the traversal. + fn node(&self) -> &Node { + match self.child { + None => self.parent, + Some(i) => &self.parent.children[i], + } + } + + /// Returns a mutable reference to the current node in the traversal. + fn node_mut(&mut self) -> &mut Node { + match self.child { + None => self.parent, + Some(i) => &mut self.parent.children[i], + } + } + + /// Move the current node to its i'th child. + fn set_child(self, i: usize) -> InsertState<'node, T> { + match self.child { + None => InsertState { + parent: self.parent, + child: Some(i), + }, + Some(prev) => InsertState { + parent: &mut self.parent.children[prev], + child: Some(i), + }, + } + } +} + impl Node { // Insert a route into the tree. pub fn insert(&mut self, route: String, val: T) -> Result<(), InsertError> { @@ -88,21 +134,27 @@ impl Node { return Ok(()); } - let mut node = self; + let mut state = InsertState { + parent: self, + child: None, + }; + 'walk: loop { // Find the common prefix between the route and the current node. - let len = min(remaining.len(), node.prefix.len()); + let len = min(remaining.len(), state.node().prefix.len()); let common_prefix = (0..len) .find(|&i| { - remaining[i] != node.prefix[i] + remaining[i] != state.node().prefix[i] // Make sure not confuse the start of a wildcard with an escaped `{`. - || remaining.is_escaped(i) != node.prefix.is_escaped(i) + || remaining.is_escaped(i) != state.node().prefix.is_escaped(i) }) .unwrap_or(len); // If this node has a longer prefix than we need, we have to fork and extract the // common prefix into a shared parent. - if node.prefix.len() > common_prefix { + if state.node().prefix.len() > common_prefix { + let node = state.node_mut(); + // Move the non-matching suffix into a child node. let suffix = node.prefix.as_ref().slice_off(common_prefix).to_owned(); let child = Node { @@ -125,6 +177,8 @@ impl Node { } if remaining.len() == common_prefix { + let node = state.node_mut(); + // This node must not already contain a value. if node.value.is_some() { return Err(InsertError::conflict(&route, remaining, node)); @@ -136,15 +190,16 @@ impl Node { return Ok(()); } + let common_remaining = remaining; + // Otherwise, the route has a remaining non-matching suffix. // // We have to search deeper. remaining = remaining.slice_off(common_prefix); let next = remaining[0]; - // For parameters with a suffix, we have to find the matching suffix or - // create a new child node. - if matches!(node.node_type, NodeType::Param { .. }) { + // For parameters with a suffix, we have to find the matching suffix or create a new child node. + if matches!(state.node().node_type, NodeType::Param { .. }) { let terminator = remaining .iter() .position(|&b| b == b'/') @@ -153,13 +208,32 @@ impl Node { let suffix = remaining.slice_until(terminator); - for (i, child) in node.children.iter().enumerate() { + let mut extra_trailing_slash = false; + for (i, child) in state.node().children.iter().enumerate() { // Find a matching suffix. if *child.prefix == **suffix { - node = &mut node.children[i]; - node.priority += 1; + state = state.set_child(i); + state.node_mut().priority += 1; continue 'walk; } + + // The suffix matches except for an extra trailing slash. + if child.prefix.len() <= suffix.len() { + let (common, remaining) = suffix.split_at(child.prefix.len()); + if *common == *child.prefix && remaining == *b"/" { + extra_trailing_slash = true; + } + } + } + + // If we are inserting a conflicting suffix, and there is a static prefix that + // already leads to this route parameter, we have a prefix-suffix conflict. + if !extra_trailing_slash && !matches!(*suffix, b"" | b"/") { + if let Some(parent) = state.parent() { + if parent.prefix_wild_child_in_segment() { + return Err(InsertError::conflict(&route, common_remaining, parent)); + } + } } // Multiple parameters within the same segment, e.g. `/{foo}{bar}`. @@ -168,19 +242,20 @@ impl Node { } // If there is no matching suffix, create a new suffix node. - let child = node.add_suffix_child(Node { + let child = state.node_mut().add_suffix_child(Node { prefix: suffix.to_owned(), node_type: NodeType::Static, priority: 1, ..Node::default() }); - node.node_type = NodeType::Param { suffix: true }; - node = &mut node.children[child]; + let has_suffix = !matches!(*suffix, b"" | b"/"); + state.node_mut().node_type = NodeType::Param { suffix: has_suffix }; + state = state.set_child(child); // If this is the final route segment, insert the value. if terminator == remaining.len() { - node.value = Some(UnsafeCell::new(val)); - node.remapping = remapping; + state.node_mut().value = Some(UnsafeCell::new(val)); + state.node_mut().remapping = remapping; return Ok(()); } @@ -190,32 +265,32 @@ impl Node { // Create a static node unless we are inserting a parameter. if remaining[0] != b'{' || remaining.is_escaped(0) { - let child = node.add_child(Node { + let child = state.node_mut().add_child(Node { node_type: NodeType::Static, priority: 1, ..Node::default() }); - node.indices.push(remaining[0]); - node = &mut node.children[child]; + state.node_mut().indices.push(remaining[0]); + state = state.set_child(child); } // Insert the remaining route. - let last = node.insert_route(remaining, val)?; + let last = state.node_mut().insert_route(remaining, val)?; last.remapping = remapping; return Ok(()); } // Find a child node that matches the next character in the route. - for mut i in 0..node.indices.len() { - if next == node.indices[i] { + for mut i in 0..state.node().indices.len() { + if next == state.node().indices[i] { // Make sure not confuse the start of a wildcard with an escaped `{` or `}`. if matches!(next, b'{' | b'}') && !remaining.is_escaped(0) { continue; } // Continue searching in the child. - i = node.update_child_priority(i); - node = &mut node.children[i]; + i = state.node_mut().update_child_priority(i); + state = state.set_child(i); continue 'walk; } } @@ -223,7 +298,31 @@ impl Node { // We couldn't find a matching child. // // If we're not inserting a wildcard we have to create a static child. - if (next != b'{' || remaining.is_escaped(0)) && node.node_type != NodeType::CatchAll { + if (next != b'{' || remaining.is_escaped(0)) + && state.node().node_type != NodeType::CatchAll + { + let node = state.node_mut(); + + let terminator = remaining + .iter() + .position(|&b| b == b'/') + .unwrap_or(remaining.len()); + + if let Ok(Some(wildcard)) = find_wildcard(remaining.slice_until(terminator)) { + // If we are inserting a parameter prefix and this node already has a parameter suffix, + // we have a prefix-suffix conflict. + if wildcard.start > 0 && node.suffix_wild_child_in_segment() { + return Err(InsertError::conflict(&route, remaining, node)); + } + + // Similarly, we are inserting a parameter suffix and this node already has a parameter + // prefix, we have a prefix-suffix conflict. + let suffix = remaining.slice_off(wildcard.end); + if !matches!(*suffix, b"" | b"/") && node.prefix_wild_child_in_segment() { + return Err(InsertError::conflict(&route, remaining, node)); + } + } + node.indices.push(next); let child = node.add_child(Node::default()); let child = node.update_child_priority(child); @@ -237,34 +336,116 @@ impl Node { // We're trying to insert a wildcard. // // If this node already has a wildcard child, we have to make sure it matches. - if node.wild_child { + if state.node().wild_child { // Wildcards are always the last child. - node = node.children.last_mut().unwrap(); - node.priority += 1; + let wild_child = state.node().children.len() - 1; + state = state.set_child(wild_child); + state.node_mut().priority += 1; // Make sure the route parameter matches. - if let Some(wildcard) = remaining.get(..node.prefix.len()) { - if *wildcard != *node.prefix { - return Err(InsertError::conflict(&route, remaining, node)); + if let Some(wildcard) = remaining.get(..state.node().prefix.len()) { + if *wildcard != *state.node().prefix { + return Err(InsertError::conflict(&route, remaining, state.node())); } } // Catch-all routes cannot have children. - if node.node_type == NodeType::CatchAll { - return Err(InsertError::conflict(&route, remaining, node)); + if state.node().node_type == NodeType::CatchAll { + return Err(InsertError::conflict(&route, remaining, state.node())); + } + + if let Some(parent) = state.parent() { + // If there is a route with both a prefix and a suffix, and we are inserting a route with + // a matching prefix but _without_ a suffix, we have a prefix-suffix conflict. + if !parent.prefix.ends_with(b"/") + && matches!(state.node().node_type, NodeType::Param { suffix: true }) + { + let terminator = remaining + .iter() + .position(|&b| b == b'/') + .map(|b| b + 1) + .unwrap_or(remaining.len()); + + if let Ok(Some(wildcard)) = find_wildcard(remaining.slice_until(terminator)) + { + let suffix = remaining.slice_off(wildcard.end); + if matches!(*suffix, b"" | b"/") { + return Err(InsertError::conflict(&route, remaining, parent)); + } + } + } } // Continue with the wildcard node. continue 'walk; } + if let Ok(Some(wildcard)) = find_wildcard(remaining) { + let node = state.node(); + let suffix = remaining.slice_off(wildcard.end); + + // If we are inserting a suffix and there is a static prefix that already leads to this + // route parameter, we have a prefix-suffix conflict. + if !matches!(*suffix, b"" | b"/") && node.prefix_wild_child_in_segment() { + return Err(InsertError::conflict(&route, remaining, node)); + } + + // Similarly, if we are inserting a longer prefix, and there is a route that leads to this + // parameter that includes a suffix, we have a prefix-suffix conflicts. + if common_remaining[common_prefix - 1] != b'/' + && node.suffix_wild_child_in_segment() + { + return Err(InsertError::conflict(&route, remaining, node)); + } + } + // Otherwise, create a new node for the wildcard and insert the route. - let last = node.insert_route(remaining, val)?; + let last = state.node_mut().insert_route(remaining, val)?; last.remapping = remapping; return Ok(()); } } + /// Returns `true` if there is a wildcard node that contains a prefix within the current route segment, + /// i.e. before the next trailing slash + fn prefix_wild_child_in_segment(&self) -> bool { + if self.prefix.ends_with(b"/") { + self.children.iter().any(Node::prefix_wild_child_in_segment) + } else { + self.children.iter().any(Node::wild_child_in_segment) + } + } + + /// Returns `true` if there is a wildcard node within the current route segment, i.e. before the + /// next trailing slash. + fn wild_child_in_segment(&self) -> bool { + if self.prefix.contains(&b'/') { + return false; + } + + if matches!(self.node_type, NodeType::Param { .. }) { + return true; + } + + self.children.iter().any(Node::wild_child_in_segment) + } + + /// Returns `true` if there is a wildcard parameter node that contains a suffix within the current route + /// segment, i.e. before a trailing slash. + fn suffix_wild_child_in_segment(&self) -> bool { + if matches!(self.node_type, NodeType::Param { suffix: true }) { + return true; + } + + self.children.iter().any(|child| { + if child.prefix.contains(&b'/') { + return false; + } + + child.suffix_wild_child_in_segment() + }) + } + // Insert a route at this node. // // If the route starts with a wildcard, a child node will be created for the parameter @@ -322,7 +503,6 @@ impl Node { let terminator = prefix .iter() .position(|&b| b == b'/') - // Include the '/' in the suffix. .map(|b| b + 1) .unwrap_or(prefix.len()); diff --git a/tests/insert.rs b/tests/insert.rs index 640e2f7..b01ddbe 100644 --- a/tests/insert.rs +++ b/tests/insert.rs @@ -52,23 +52,199 @@ fn wildcard_conflict() { ("/x/{id}/", Ok(())), ("/x/{id}y", Ok(())), ("/x/{id}y/", Ok(())), - ("/x/x{id}", Ok(())), - ("/x/x{id}y", Ok(())), - ("/qux/id", Ok(())), - ("/qux/{id}y", Ok(())), - ("/qux/{id}", Ok(())), - ("/qux/{id}/", Ok(())), - ("/qux/{id}x", Ok(())), - ("/qux/x{id}y", Ok(())), - ("/qux/x{id}", Ok(())), - ("/qux/x{id}", Err(conflict("/qux/x{id}"))), - ("/qux/x{id}y", Err(conflict("/qux/x{id}y"))), + ("/x/{id}y", Err(conflict("/x/{id}y"))), + ("/x/x{id}", Err(conflict("/x/{id}y/"))), + ("/x/x{id}y", Err(conflict("/x/{id}y/"))), + ("/y/{id}", Ok(())), + ("/y/{id}/", Ok(())), + ("/y/y{id}", Ok(())), + ("/y/y{id}/", Ok(())), + ("/y/{id}y", Err(conflict("/y/y{id}/"))), + ("/y/{id}y/", Err(conflict("/y/y{id}/"))), + ("/y/x{id}y", Err(conflict("/y/y{id}/"))), + ("/z/x{id}y", Ok(())), + ("/z/{id}", Ok(())), + ("/z/{id}y", Err(conflict("/z/x{id}y"))), + ("/z/x{id}", Err(conflict("/z/x{id}y"))), + ("/z/y{id}", Err(conflict("/z/x{id}y"))), + ("/z/x{id}z", Err(conflict("/z/x{id}y"))), + ("/z/z{id}y", Err(conflict("/z/x{id}y"))), ("/bar/{id}", Ok(())), ("/bar/x{id}y", Ok(())), ]) .run() } +#[test] +fn prefix_suffix_conflict() { + InsertTest(vec![ + ("/x1/{a}suffix", Ok(())), + ("/x1/prefix{a}", Err(conflict("/x1/{a}suffix"))), + ("/x1/prefix{a}suffix", Err(conflict("/x1/{a}suffix"))), + ("/x1/suffix{a}prefix", Err(conflict("/x1/{a}suffix"))), + ("/x1", Ok(())), + ("/x1/", Ok(())), + ("/x1/{a}", Ok(())), + ("/x1/{a}/", Ok(())), + ("/x1/{a}suffix/", Ok(())), + ("/x2/{a}suffix", Ok(())), + ("/x2/{a}", Ok(())), + ("/x2/prefix{a}", Err(conflict("/x2/{a}suffix"))), + ("/x2/prefix{a}suff", Err(conflict("/x2/{a}suffix"))), + ("/x2/prefix{a}suffix", Err(conflict("/x2/{a}suffix"))), + ("/x2/prefix{a}suffixy", Err(conflict("/x2/{a}suffix"))), + ("/x2", Ok(())), + ("/x2/", Ok(())), + ("/x2/{a}suffix/", Ok(())), + ("/x3/prefix{a}", Ok(())), + ("/x3/{a}suffix", Err(conflict("/x3/prefix{a}"))), + ("/x3/prefix{a}suffix", Err(conflict("/x3/prefix{a}"))), + ("/x3/prefix{a}/", Ok(())), + ("/x3/{a}", Ok(())), + ("/x3/{a}/", Ok(())), + ("/x4/prefix{a}", Ok(())), + ("/x4/{a}", Ok(())), + ("/x4/{a}suffix", Err(conflict("/x4/prefix{a}"))), + ("/x4/suffix{a}p", Err(conflict("/x4/prefix{a}"))), + ("/x4/suffix{a}prefix", Err(conflict("/x4/prefix{a}"))), + ("/x4/prefix{a}/", Ok(())), + ("/x4/{a}/", Ok(())), + ("/x5/prefix1{a}", Ok(())), + ("/x5/prefix2{a}", Ok(())), + ("/x5/{a}suffix", Err(conflict("/x5/prefix1{a}"))), + ("/x5/prefix{a}suffix", Err(conflict("/x5/prefix1{a}"))), + ("/x5/prefix1{a}suffix", Err(conflict("/x5/prefix1{a}"))), + ("/x5/prefix2{a}suffix", Err(conflict("/x5/prefix2{a}"))), + ("/x5/prefix3{a}suffix", Err(conflict("/x5/prefix1{a}"))), + ("/x5/prefix1{a}/", Ok(())), + ("/x5/prefix2{a}/", Ok(())), + ("/x5/prefix3{a}/", Ok(())), + ("/x5/{a}", Ok(())), + ("/x5/{a}/", Ok(())), + ("/x6/prefix1{a}", Ok(())), + ("/x6/prefix2{a}", Ok(())), + ("/x6/{a}", Ok(())), + ("/x6/{a}suffix", Err(conflict("/x6/prefix1{a}"))), + ("/x6/prefix{a}suffix", Err(conflict("/x6/prefix1{a}"))), + ("/x6/prefix1{a}suffix", Err(conflict("/x6/prefix1{a}"))), + ("/x6/prefix2{a}suffix", Err(conflict("/x6/prefix2{a}"))), + ("/x6/prefix3{a}suffix", Err(conflict("/x6/prefix1{a}"))), + ("/x6/prefix1{a}/", Ok(())), + ("/x6/prefix2{a}/", Ok(())), + ("/x6/prefix3{a}/", Ok(())), + ("/x6/{a}/", Ok(())), + ("/x7/prefix{a}suffix", Ok(())), + ("/x7/{a}suff", Err(conflict("/x7/prefix{a}suffix"))), + ("/x7/{a}suffix", Err(conflict("/x7/prefix{a}suffix"))), + ("/x7/{a}suffixy", Err(conflict("/x7/prefix{a}suffix"))), + ("/x7/{a}prefix", Err(conflict("/x7/prefix{a}suffix"))), + ("/x7/suffix{a}prefix", Err(conflict("/x7/prefix{a}suffix"))), + ("/x7/prefix{a}", Err(conflict("/x7/prefix{a}suffix"))), + ("/x7/another{a}", Err(conflict("/x7/prefix{a}suffix"))), + ("/x7/suffix{a}", Err(conflict("/x7/prefix{a}suffix"))), + ("/x7/prefix{a}/", Err(conflict("/x7/prefix{a}suffix"))), + ("/x7/prefix{a}suff", Err(conflict("/x7/prefix{a}suffix"))), + ("/x7/prefix{a}suffix", Err(conflict("/x7/prefix{a}suffix"))), + ("/x7/prefix{a}suffixy", Err(conflict("/x7/prefix{a}suffix"))), + ("/x7/prefix1{a}", Err(conflict("/x7/prefix{a}suffix"))), + ("/x7/prefix{a}/", Err(conflict("/x7/prefix{a}suffix"))), + ("/x7/{a}suffix/", Err(conflict("/x7/prefix{a}suffix"))), + ("/x7/prefix{a}suffix/", Ok(())), + ("/x7/{a}", Ok(())), + ("/x7/{a}/", Ok(())), + ("/x8/prefix{a}suffix", Ok(())), + ("/x8/{a}", Ok(())), + ("/x8/{a}suff", Err(conflict("/x8/prefix{a}suffix"))), + ("/x8/{a}suffix", Err(conflict("/x8/prefix{a}suffix"))), + ("/x8/{a}suffixy", Err(conflict("/x8/prefix{a}suffix"))), + ("/x8/prefix{a}", Err(conflict("/x8/prefix{a}suffix"))), + ("/x8/prefix{a}/", Err(conflict("/x8/prefix{a}suffix"))), + ("/x8/prefix{a}suff", Err(conflict("/x8/prefix{a}suffix"))), + ("/x8/prefix{a}suffix", Err(conflict("/x8/prefix{a}suffix"))), + ("/x8/prefix{a}suffixy", Err(conflict("/x8/prefix{a}suffix"))), + ("/x8/prefix1{a}", Err(conflict("/x8/prefix{a}suffix"))), + ("/x8/prefix{a}/", Err(conflict("/x8/prefix{a}suffix"))), + ("/x8/{a}suffix/", Err(conflict("/x8/prefix{a}suffix"))), + ("/x8/prefix{a}suffix/", Ok(())), + ("/x8/{a}/", Ok(())), + ("/x9/prefix{a}", Ok(())), + ("/x9/{a}suffix", Err(conflict("/x9/prefix{a}"))), + ("/x9/prefix{a}suffix", Err(conflict("/x9/prefix{a}"))), + ("/x9/prefixabc{a}suffix", Err(conflict("/x9/prefix{a}"))), + ("/x9/pre{a}suffix", Err(conflict("/x9/prefix{a}"))), + ("/x10/{a}", Ok(())), + ("/x10/prefix{a}", Ok(())), + ("/x10/{a}suffix", Err(conflict("/x10/prefix{a}"))), + ("/x10/prefix{a}suffix", Err(conflict("/x10/prefix{a}"))), + ("/x10/prefixabc{a}suffix", Err(conflict("/x10/prefix{a}"))), + ("/x10/pre{a}suffix", Err(conflict("/x10/prefix{a}"))), + ("/x11/{a}", Ok(())), + ("/x11/{a}suffix", Ok(())), + ("/x11/prx11fix{a}", Err(conflict("/x11/{a}suffix"))), + ("/x11/prx11fix{a}suff", Err(conflict("/x11/{a}suffix"))), + ("/x11/prx11fix{a}suffix", Err(conflict("/x11/{a}suffix"))), + ("/x11/prx11fix{a}suffixabc", Err(conflict("/x11/{a}suffix"))), + ("/x12/prefix{a}suffix", Ok(())), + ("/x12/pre{a}", Err(conflict("/x12/prefix{a}suffix"))), + ("/x12/prefix{a}", Err(conflict("/x12/prefix{a}suffix"))), + ("/x12/prefixabc{a}", Err(conflict("/x12/prefix{a}suffix"))), + ("/x12/pre{a}suffix", Err(conflict("/x12/prefix{a}suffix"))), + ( + "/x12/prefix{a}suffix", + Err(conflict("/x12/prefix{a}suffix")), + ), + ( + "/x12/prefixabc{a}suffix", + Err(conflict("/x12/prefix{a}suffix")), + ), + ("/x12/prefix{a}suff", Err(conflict("/x12/prefix{a}suffix"))), + ( + "/x12/prefix{a}suffix", + Err(conflict("/x12/prefix{a}suffix")), + ), + ( + "/x12/prefix{a}suffixabc", + Err(conflict("/x12/prefix{a}suffix")), + ), + ("/x12/{a}suff", Err(conflict("/x12/prefix{a}suffix"))), + ("/x12/{a}suffix", Err(conflict("/x12/prefix{a}suffix"))), + ("/x12/{a}suffixabc", Err(conflict("/x12/prefix{a}suffix"))), + ("/x13/{a}", Ok(())), + ("/x13/prefix{a}suffix", Ok(())), + ("/x13/pre{a}", Err(conflict("/x13/prefix{a}suffix"))), + ("/x13/prefix{a}", Err(conflict("/x13/prefix{a}suffix"))), + ("/x13/prefixabc{a}", Err(conflict("/x13/prefix{a}suffix"))), + ("/x13/pre{a}suffix", Err(conflict("/x13/prefix{a}suffix"))), + ( + "/x13/prefix{a}suffix", + Err(conflict("/x13/prefix{a}suffix")), + ), + ( + "/x13/prefixabc{a}suffix", + Err(conflict("/x13/prefix{a}suffix")), + ), + ("/x13/prefix{a}suff", Err(conflict("/x13/prefix{a}suffix"))), + ( + "/x13/prefix{a}suffix", + Err(conflict("/x13/prefix{a}suffix")), + ), + ( + "/x13/prefix{a}suffixabc", + Err(conflict("/x13/prefix{a}suffix")), + ), + ("/x13/{a}suff", Err(conflict("/x13/prefix{a}suffix"))), + ("/x13/{a}suffix", Err(conflict("/x13/prefix{a}suffix"))), + ("/x13/{a}suffixabc", Err(conflict("/x13/prefix{a}suffix"))), + ("/x15/{*rest}", Ok(())), + ("/x15/{a}suffix", Err(conflict("/x15/{*rest}"))), + ("/x15/{a}suffix", Err(conflict("/x15/{*rest}"))), + ("/x15/prefix{a}", Ok(())), + ("/x16/{*rest}", Ok(())), + ("/x16/prefix{a}suffix", Ok(())), + ]) + .run() +} + #[test] fn invalid_catchall() { InsertTest(vec![ diff --git a/tests/match.rs b/tests/match.rs index bb68dbe..1edcb92 100644 --- a/tests/match.rs +++ b/tests/match.rs @@ -61,9 +61,9 @@ fn overlapping_param_backtracking() { assert_eq!(matched.params.get("id"), Some("978")); } +#[allow(clippy::type_complexity)] struct MatchTest { routes: Vec<&'static str>, - #[allow(clippy::type_complexity)] matches: Vec<( &'static str, &'static str, @@ -617,15 +617,7 @@ fn empty_param() { #[test] fn wildcard_suffix() { MatchTest { - routes: vec![ - "/", - "/{foo}x", - "/foox", - "/{foo}x/bar", - "/{foo}x/bar/baz", - "/x{foo}", - "/x{foo}/bar", - ], + routes: vec!["/", "/{foo}x", "/foox", "/{foo}x/bar", "/{foo}x/bar/baz"], matches: vec![ ("/", "/", p! {}), ("/foox", "/foox", p! {}), @@ -636,9 +628,9 @@ fn wildcard_suffix() { ("/mx/bar", "/{foo}x/bar", p! { "foo" => "m" }), ("/mxm/bar", "", Err(())), ("/x", "", Err(())), - ("/xfoo", "/x{foo}", p! { "foo" => "foo" }), - ("/xfoox", "/x{foo}", p! { "foo" => "foox" }), - ("/xfoox/bar", "/x{foo}/bar", p! { "foo" => "foox" }), + ("/xfoo", "", Err(())), + ("/xfoox", "/{foo}x", p! { "foo" => "xfoo" }), + ("/xfoox/bar", "/{foo}x/bar", p! { "foo" => "xfoo" }), ("/xfoox/bar/baz", "/{foo}x/bar/baz", p! { "foo" => "xfoo" }), ], }