From 215a8056ff199313f294b06952b1147cf3213873 Mon Sep 17 00:00:00 2001 From: Gameldar Date: Sat, 17 Aug 2019 01:00:50 +0800 Subject: [PATCH 1/3] Add a test after parsing a route to ensure it doesn't contain duplicate parameter names --- src/lib.rs | 60 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index 1156d50..4586069 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,4 @@ +<<<<<<< HEAD use std::{ cmp::Ordering, collections::{btree_map, BTreeMap}, @@ -5,6 +6,15 @@ use std::{ }; use crate::nfa::{CharacterClass, NFA}; +======= +use nfa::CharacterClass; +use nfa::NFA; +use std::cmp::Ordering; +use std::collections::btree_map; +use std::collections::BTreeMap; +use std::collections::HashSet; +use std::ops::Index; +>>>>>>> Add a test after parsing a route to ensure it doesn't contain duplicate parameter names pub mod nfa; @@ -174,6 +184,12 @@ impl Router { metadata.statics += 1; } } + let mut hashes = HashSet::new(); + for name in metadata.param_names.iter() { + if !hashes.insert(name.to_string()) { + panic!("Duplicate name '{}' in route {}", name.to_string(), &route); + } + } nfa.acceptance(state); nfa.metadata(state, metadata); @@ -360,6 +376,7 @@ mod tests { router.add("/a/*b/c", "abc".to_string()); router.add("/a/*b/c/:d", "abcd".to_string()); +<<<<<<< HEAD let m = router.recognize("/a/foo").unwrap(); assert_eq!(*m.handler, "ab".to_string()); assert_eq!(m.params, params("b", "foo")); @@ -379,6 +396,49 @@ mod tests { let m = router.recognize("/a/foo/c/baz").unwrap(); assert_eq!(*m.handler, "abcd".to_string()); assert_eq!(m.params, two_params("b", "foo", "d", "baz")); +======= +#[test] +#[should_panic] +fn duplicate_named_parameter() { + let mut router = Router::new(); + router.add("/foo/:bar/:bar", "test".to_string()); +} + +#[test] +#[should_panic] +fn duplicate_star_parameter() { + let mut router = Router::new(); + router.add("/foo/*bar/*bar", "test".to_string()); +} + +#[test] +#[should_panic] +fn duplicate_mixed_parameter() { + let mut router = Router::new(); + router.add("/foo/*bar/:bar", "test".to_string()); +} + +#[test] +#[should_panic] +fn duplicate_mixed_reversed_parameter() { + let mut router = Router::new(); + router.add("/foo/:bar/*bar", "test".to_string()); +} + +#[test] +#[should_panic] +fn duplicate_separated_parameter() { + let mut router = Router::new(); + router.add("/foo/:bar/bleg/:bar", "test".to_string()); +} + +#[allow(dead_code)] +fn params(key: &str, val: &str) -> Params { + let mut map = Params::new(); + map.insert(key.to_string(), val.to_string()); + map +} +>>>>>>> Add a test after parsing a route to ensure it doesn't contain duplicate parameter names let m = router.recognize("/a/foo/bar/c/baz").unwrap(); assert_eq!(*m.handler, "abcd".to_string()); From 5843a165073db9a7e282ca2dee326cc6607a6ed8 Mon Sep 17 00:00:00 2001 From: Gameldar Date: Mon, 19 Aug 2019 13:23:53 +0800 Subject: [PATCH 2/3] Change add to return a Result to indicate failures when parsing the route --- src/lib.rs | 65 +++++++----------------------------------------------- 1 file changed, 8 insertions(+), 57 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 4586069..8e4a0a7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,20 +1,10 @@ -<<<<<<< HEAD use std::{ cmp::Ordering, - collections::{btree_map, BTreeMap}, + collections::{btree_map, BTreeMap, HashSet}, ops::Index, }; use crate::nfa::{CharacterClass, NFA}; -======= -use nfa::CharacterClass; -use nfa::NFA; -use std::cmp::Ordering; -use std::collections::btree_map; -use std::collections::BTreeMap; -use std::collections::HashSet; -use std::ops::Index; ->>>>>>> Add a test after parsing a route to ensure it doesn't contain duplicate parameter names pub mod nfa; @@ -157,7 +147,7 @@ impl Router { } } - pub fn add(&mut self, mut route: &str, dest: T) { + pub fn add(&mut self, mut route: &str, dest: T) -> Result<(), String> { if !route.is_empty() && route.as_bytes()[0] == b'/' { route = &route[1..]; } @@ -187,13 +177,18 @@ impl Router { let mut hashes = HashSet::new(); for name in metadata.param_names.iter() { if !hashes.insert(name.to_string()) { - panic!("Duplicate name '{}' in route {}", name.to_string(), &route); + return Err(format!( + "Duplicate name '{}' in route {}", + name.to_string(), + &route + )); } } nfa.acceptance(state); nfa.metadata(state, metadata); self.handlers.insert(state, dest); + Ok(()) } pub fn recognize(&self, mut path: &str) -> Result, String> { @@ -376,7 +371,6 @@ mod tests { router.add("/a/*b/c", "abc".to_string()); router.add("/a/*b/c/:d", "abcd".to_string()); -<<<<<<< HEAD let m = router.recognize("/a/foo").unwrap(); assert_eq!(*m.handler, "ab".to_string()); assert_eq!(m.params, params("b", "foo")); @@ -396,49 +390,6 @@ mod tests { let m = router.recognize("/a/foo/c/baz").unwrap(); assert_eq!(*m.handler, "abcd".to_string()); assert_eq!(m.params, two_params("b", "foo", "d", "baz")); -======= -#[test] -#[should_panic] -fn duplicate_named_parameter() { - let mut router = Router::new(); - router.add("/foo/:bar/:bar", "test".to_string()); -} - -#[test] -#[should_panic] -fn duplicate_star_parameter() { - let mut router = Router::new(); - router.add("/foo/*bar/*bar", "test".to_string()); -} - -#[test] -#[should_panic] -fn duplicate_mixed_parameter() { - let mut router = Router::new(); - router.add("/foo/*bar/:bar", "test".to_string()); -} - -#[test] -#[should_panic] -fn duplicate_mixed_reversed_parameter() { - let mut router = Router::new(); - router.add("/foo/:bar/*bar", "test".to_string()); -} - -#[test] -#[should_panic] -fn duplicate_separated_parameter() { - let mut router = Router::new(); - router.add("/foo/:bar/bleg/:bar", "test".to_string()); -} - -#[allow(dead_code)] -fn params(key: &str, val: &str) -> Params { - let mut map = Params::new(); - map.insert(key.to_string(), val.to_string()); - map -} ->>>>>>> Add a test after parsing a route to ensure it doesn't contain duplicate parameter names let m = router.recognize("/a/foo/bar/c/baz").unwrap(); assert_eq!(*m.handler, "abcd".to_string()); From fa0f7d6e50bb97d85ae12401be3cd618d4404baa Mon Sep 17 00:00:00 2001 From: Gameldar Date: Thu, 22 Aug 2019 00:14:01 +0800 Subject: [PATCH 3/3] Split the implementation to add and add_check add calls add_check and unwraps it Also added a bit of documentation around the router and the add methods --- src/lib.rs | 215 ++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 214 insertions(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 8e4a0a7..083c4d8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -133,6 +133,177 @@ impl Match { } } +/// A Router for defining matching rules (``routes``) for paths to a destination (``handler``). +/// One or more routes are added to the router and then paths can be recognized and a match +/// returned. Routes can contain parameters that are then returned as part of the match. +/// +/// # Example +/// +/// ``` +/// use route_recognizer::Router; +/// +/// #[derive(PartialEq)] +/// enum FooBarBaz { +/// FOO, +/// BAR, +/// BAZ, +/// }; +/// +/// let mut router = Router::new(); +/// router.add("/foo", FooBarBaz::FOO); +/// router.add("/foo/:bar", FooBarBaz::BAR); +/// router.add("/foo/:bar/*baz", FooBarBaz::BAZ); +/// +/// let m = router.recognize("/foo").unwrap(); +/// if *m.handler == FooBarBaz::FOO { +/// println!("do some foo"); +/// } +/// +/// let m = router.recognize("/foo/123").unwrap(); +/// if *m.handler == FooBarBaz::BAR { +/// println!("Got a bar of {}", m.params["bar"]); +/// } +/// +/// let m = router.recognize("/foo/123/abc/def").unwrap(); +/// if *m.handler == FooBarBaz::BAZ { +/// println!("Got a bar of {} and a baz of {}", m.params["bar"], m.params["baz"]); +/// } +/// ``` +/// +/// +/// # Route types +/// +/// A ``route`` consists of one or more segments, separated by a ``/``, to be matched against the ``path`` to be +/// recognized. There are three types of segments - *static*, *dynamic*, *star*: +/// +/// 1. *static* - a specific string to match +/// +/// ``` +/// use route_recognizer::Router; +/// let mut router = Router::new(); +/// router.add("/foo", "foo".to_string()); +/// router.add("/foo/bar", "foobar".to_string()); +/// +/// let m = router.recognize("/foo").unwrap(); +/// assert_eq!(*m.handler, "foo"); // foo is matched +/// +/// let m = router.recognize("/foo/bar").unwrap(); +/// assert_eq!(*m.handler, "foobar"); // foobar is matched +/// +/// let m = router.recognize("/foo/bar/baz"); +/// assert!(m.is_err()); // No match is found +/// ``` +/// +/// 2. *dynamic* - a single segment is matched. Dynamic segments start with a ``:`` and can +/// be named to be retrieved as a parameter. +/// +/// ``` +/// use route_recognizer::Router; +/// let mut router = Router::new(); +/// router.add("/foo/:bar", "foobar".to_string()); +/// router.add("/foo/:bar/baz", "foobarbaz".to_string()); +/// +/// let m = router.recognize("/foo"); +/// assert!(m.is_err()); // No match is found +/// +/// let m = router.recognize("/foo/bar").unwrap(); +/// assert_eq!(*m.handler, "foobar"); // foobar is matched +/// assert_eq!(m.params["bar"], "bar"); // parameter 'bar' is set to 'bar' +/// +/// let m = router.recognize("/foo/123").unwrap(); +/// assert_eq!(*m.handler, "foobar"); // foobar is matched +/// assert_eq!(m.params["bar"], "123"); // parameter 'bar' is set to '123' +///``` +/// +/// 3. *star* - matches one or more segments until the end of the path or another +/// defined segment is reached. +/// +/// ``` +/// use route_recognizer::Router; +/// let mut router = Router::new(); +/// router.add("/foo/*bar", "foobar".to_string()); +/// router.add("/foo/*bar/baz", "foobarbaz".to_string()); +/// +/// let m = router.recognize("/foo"); +/// assert!(m.is_err()); // No match is found +/// +/// let m = router.recognize("/foo/123").unwrap(); +/// assert_eq!(*m.handler, "foobar"); // foobar is matched +/// assert_eq!(m.params["bar"], "123"); // parameter 'bar' is set to '123' +/// +/// let m = router.recognize("/foo/123/abc/def").unwrap(); +/// assert_eq!(*m.handler, "foobar"); // foobar is matched +/// assert_eq!(m.params["bar"], "123/abc/def"); // parameter 'bar' is set to '123/abc/def' +/// +/// let m = router.recognize("/foo/123/abc/baz").unwrap(); +/// assert_eq!(*m.handler, "foobarbaz"); // foobar is matched +/// assert_eq!(m.params["bar"], "123/abc"); // parameter 'bar' is set to '123/abc' +///``` +/// +/// # Unnamed parameters +/// +/// Parameters do not need to have a name, but can be indicated just by the leading ``:`` +/// or ``*``. If a name is not defined then the parameter is not captured in the ``params`` +/// field of the match. +/// +/// For example: +/// +/// ``` +/// use route_recognizer::Router; +/// let mut router = Router::new(); +/// router.add("/foo/*", "foo".to_string()); +/// router.add("/bar/:/baz", "barbaz".to_string()); +/// +/// let m = router.recognize("/foo/123").unwrap(); +/// assert_eq!(*m.handler, "foo"); // foo is matched +/// assert_eq!(m.params.iter().next(), None); // but no parameters are found +/// +/// let m = router.recognize("/bar/123/baz").unwrap(); +/// assert_eq!(*m.handler, "barbaz"); // barbaz is matched +/// assert_eq!(m.params.iter().next(), None); // but no parameters are found +/// ``` +/// +/// # Routing precedence +/// +/// Routes can be a combination of all three types and the most specific match will be +/// the result of the precedence of the types where *static* takes precedence over +/// *dynamic*, which in turn takes precedence over *star* segments. For example, if you +/// have the following three routes: +/// +/// ``` +/// use route_recognizer::Router; +/// let mut router = Router::new(); +/// router.add("/foo", "foo".to_string()); +/// router.add("/:bar", "bar".to_string()); +/// router.add("/*baz", "baz".to_string()); +/// +/// let m = router.recognize("/foo").unwrap(); +/// assert_eq!(*m.handler, "foo"); // foo is matched as it is a static match +/// +/// let m = router.recognize("/123").unwrap(); +/// assert_eq!(*m.handler, "bar"); // bar is matched as it is a single segment match, +/// // whereas baz is a star match +/// ``` +/// +/// The precedence rules also apply within a route itself. So if you have a mix of types +/// the static and dynamic parts will take precedence over star rules. For example: +/// +/// ``` +/// use route_recognizer::Router; +/// let mut router = Router::new(); +/// router.add("/foo/*bar/baz/:bay", "foobarbazbay".to_string()); +/// +/// let m = router.recognize("/foo/123/abc/def/baz/xyz").unwrap(); +/// assert_eq!(m.params["bar"], "123/abc/def"); +/// assert_eq!(m.params["bay"], "xyz"); +/// +/// // note that the match will take the right most match when +/// // a star segment is define, so in a path that contains multiple +/// // baz segments it will match on the last one +/// let m = router.recognize("/foo/123/baz/abc/def/baz/xyz").unwrap(); +/// assert_eq!(m.params["bar"], "123/baz/abc/def"); +/// assert_eq!(m.params["bay"], "xyz"); +/// ``` #[derive(Clone)] pub struct Router { nfa: NFA, @@ -147,7 +318,49 @@ impl Router { } } - pub fn add(&mut self, mut route: &str, dest: T) -> Result<(), String> { + /// add a route to the router. + /// + /// # Examples + /// Basic usage: + /// ``` + /// use route_recognizer::Router; + /// let mut router = Router::new(); + /// router.add("/foo/*bar/baz/:bay", "foo".to_string()); + /// ``` + /// + /// # Panics + /// + /// If a duplicate name is detected in the route the function will panic to ensure that data + /// is not lost when a route is recognized. If the earlier parameter is not required an unamed + /// parameter (e.g. ``/a/:/:b`` or ``/a/*/:b``) can be used. + /// + /// If user defined data is being added as a route, consider using [`Router::add_check`] instead. + /// + /// [`Router::add_check`]: struct.Router.html#method.add_check + /// + pub fn add(&mut self, route: &str, dest: T) { + self.add_check(route, dest).unwrap(); + } + + /// add a route to the router returning a result indicating success or failure. + /// + /// # Examples + /// Basic usage: + /// ``` + /// use route_recognizer::Router; + /// let mut router = Router::new(); + /// router.add_check("/foo/*bar/baz/:bay", "foo".to_string()).expect("Failed to add route."); + /// ``` + /// + /// If duplicate parameter names are defined in the route then an ``Error`` is returned: + /// ``` + /// let mut router = route_recognizer::Router::new(); + /// let result = router.add_check("/foo/:bar/abc/:bar", "foobarabcbar".to_string()); + /// assert!(result.is_err()); + /// assert_eq!("Duplicate name 'bar' in route foo/:bar/abc/:bar", result.err().unwrap()); + /// ``` + /// + pub fn add_check(&mut self, mut route: &str, dest: T) -> Result<(), String> { if !route.is_empty() && route.as_bytes()[0] == b'/' { route = &route[1..]; }