nested_router 0.2.1

Nested route-recognizers
Documentation
use std::collections::BTreeMap;

use route_recognizer::Router;

pub const SUB_PATH_WILDCARD_NAME: &str = "_sub_path";

#[derive(Debug, PartialEq, Clone)]
pub struct Route {
    pub path: String,
    pub has_sub_routes: bool,
}

#[derive(Debug, Clone, PartialEq)]
pub struct RouteList {
    pub routes: Vec<Route>,
}

impl RouteList {
    pub fn route(&self, mut rel_path: &str) -> Result<RouteOutput, Error> {
        if rel_path.starts_with('/') {
            return Err(Error::InvalidPath);
        }
        if rel_path.ends_with('/') {
            rel_path = &rel_path[..rel_path.len() - 1];
        }
        let router = {
            let mut router = Router::new();

            for (i, route) in self.routes.iter().enumerate() {
                match route.has_sub_routes {
                    true => router.add(&format!("{}/*{}", &route.path, SUB_PATH_WILDCARD_NAME), i),
                    false => router.add(&route.path, i),
                }
                router.add(&route.path, i);
            }
            router
        };
        let matched = router.recognize(rel_path);
        match matched {
            Ok(matched) => {
                let idx = **matched.handler();
                let route = &self.routes[idx];
                let mut params = BTreeMap::new();
                let mut sub_path = "";

                for (key, value) in matched.params() {
                    if key == SUB_PATH_WILDCARD_NAME {
                        sub_path = value;
                    } else {
                        params.insert(key.to_string(), value.to_string());
                    }
                }

                Ok(RouteOutput {
                    sub_path: sub_path.to_string(),
                    route,
                    params,
                })
            }
            Err(_) => Err(Error::NotFound),
        }
    }
}

#[derive(Debug)]
pub struct RouteOutput<'route_list> {
    pub sub_path: String,
    pub route: &'route_list Route,
    pub params: BTreeMap<String, String>,
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_ok() {
        let (root, sub) = route_list_1();

        {
            let absolute_path = "/123/456";
            let relative_path = &absolute_path[1..];

            let RouteOutput {
                sub_path,
                route,
                params,
            } = root.route(relative_path).unwrap();
            assert_eq!(route.path, ":id");
            assert_eq!(params.get("id"), Some(&"123".to_string()));

            let RouteOutput {
                sub_path,
                route,
                params,
            } = sub.route(&sub_path).unwrap();
            assert_eq!(route.path, ":id");
            assert_eq!(params.get("id"), Some(&"456".to_string()));
            assert_eq!(sub_path, "");
        }

        {
            let absolute_path = "/about";
            let relative_path = &absolute_path[1..];

            let RouteOutput {
                sub_path,
                route,
                params,
            } = root.route(relative_path).unwrap();
            assert_eq!(route.path, "about");
            assert_eq!(sub_path, "");
            assert_eq!(params.len(), 0);
        }

        {
            let absolute_path = "/about/123";
            let relative_path = &absolute_path[1..];

            let RouteOutput {
                sub_path,
                route,
                params,
            } = root.route(relative_path).unwrap();
            assert_eq!(route.path, ":id");
            assert_eq!(params.get("id"), Some(&"about".to_string()));
            assert_eq!(sub_path, "123".to_string());
        }
    }

    #[test]
    fn test_index() {
        let root = route_list_3();

        let absolute_path = "/";
        let relative_path = &absolute_path[1..];

        let RouteOutput {
            sub_path,
            route,
            params,
        } = root.route(relative_path).unwrap();
        assert_eq!(route.path, "");
        assert_eq!(sub_path, "");
        assert_eq!(params.len(), 0);
    }

    #[test]
    fn test_combined_segment() {
        let root = RouteList {
            routes: vec![Route {
                path: "1/2".to_string(),
                has_sub_routes: false,
            }],
        };

        let absolute_path = "/1/2";
        let relative_path = &absolute_path[1..];

        let RouteOutput {
            sub_path,
            route,
            params,
        } = root.route(relative_path).unwrap();
        assert_eq!(route.path, "1/2");
        assert_eq!(params.len(), 0);
        assert_eq!(sub_path, "");
    }

    #[test]
    fn test_not_found() {
        let root = route_list_2();

        {
            let absolute_path = "/about/123";
            let relative_path = &absolute_path[1..];

            assert_eq!(root.route(relative_path).unwrap_err(), Error::NotFound);
        }

        {
            let absolute_path = "/about2";
            let relative_path = &absolute_path[1..];

            assert_eq!(root.route(relative_path).unwrap_err(), Error::NotFound);
        }
    }

    #[test]
    fn test_invalid_path() {
        let root = route_list_2();

        let absolute_path = "/about/123";

        assert_eq!(root.route(absolute_path).unwrap_err(), Error::InvalidPath);
    }

    #[test]
    fn test_sub_index() {
        let root = RouteList {
            routes: vec![
                Route {
                    path: "sub".to_string(),
                    has_sub_routes: true,
                },
                Route {
                    path: "*".to_string(),
                    has_sub_routes: false,
                },
            ],
        };

        {
            let absolute_path = "/sub";
            let relative_path = &absolute_path[1..];

            let RouteOutput {
                sub_path,
                route,
                params,
            } = root.route(relative_path).unwrap();
            assert_eq!(sub_path, "");
            assert_eq!(route.path, "sub");
            assert_eq!(params.len(), 0);
        }

        {
            let absolute_path = "/sub/";
            let relative_path = &absolute_path[1..];

            let RouteOutput {
                sub_path,
                route,
                params,
            } = root.route(relative_path).unwrap();
            assert_eq!(sub_path, "");
            assert_eq!(route.path, "sub");
            assert_eq!(params.len(), 0);
        }
    }

    fn route_list_1() -> (RouteList, RouteList) {
        let sub = RouteList {
            routes: vec![Route {
                path: ":id".to_string(),
                has_sub_routes: false,
            }],
        };
        let root = RouteList {
            routes: vec![
                Route {
                    path: ":id".to_string(),
                    has_sub_routes: true,
                },
                Route {
                    path: "about".to_string(),
                    has_sub_routes: false,
                },
            ],
        };
        (root, sub)
    }

    fn route_list_2() -> RouteList {
        let root = RouteList {
            routes: vec![Route {
                path: "about".to_string(),
                has_sub_routes: false,
            }],
        };
        root
    }

    fn route_list_3() -> RouteList {
        let root = RouteList {
            routes: vec![Route {
                path: "".to_string(),
                has_sub_routes: false,
            }],
        };
        root
    }
}

#[derive(Debug, Clone, PartialEq)]
pub enum Error {
    InvalidPath,
    NotFound,
}