viz-router 0.2.0-alpha

Viz Router
use viz_core::{
    handler::Transform, Body, BoxHandler, Handler, HandlerExt, Next, Request, Response, Result,
};

use crate::{Resource, Route};

#[derive(Clone, Debug)]
pub struct Router {
    pub(crate) routes: Option<Vec<(String, Route)>>,
}

impl Router {
    pub fn new() -> Self {
        Self { routes: None }
    }

    fn push<S>(routes: &mut Vec<(String, Route)>, path: S, route: Route)
    where
        S: AsRef<str>,
    {
        let path = path.as_ref();
        match routes
            .iter_mut()
            .find_map(|(p, r)| if p == path { Some(r) } else { None })
        {
            Some(r) => *r = route,
            None => routes.push((path.to_string(), route)),
        }
    }

    pub fn route<S>(mut self, path: S, route: Route) -> Self
    where
        S: AsRef<str>,
    {
        Self::push(
            self.routes.get_or_insert_with(Vec::new),
            path.as_ref().trim_start_matches('/'),
            route,
        );
        self
    }

    pub fn resource<S>(self, path: S, resource: Resource) -> Self
    where
        S: AsRef<str>,
    {
        let mut path = path.as_ref().to_string();
        if !path.ends_with('/') {
            path.push('/');
        }

        resource
            .routes
            .into_iter()
            .fold(self, |router, (mut sp, route)| {
                let is_empty = sp.is_empty();
                sp = path.clone() + &sp;
                if is_empty {
                    sp = sp.trim_end_matches('/').to_string();
                }
                router.route(sp, route)
            })
    }

    pub fn nest<S>(self, path: S, router: Self) -> Self
    where
        S: AsRef<str>,
    {
        let mut path = path.as_ref().to_string();
        if !path.ends_with('/') {
            path.push('/');
        }

        match router.routes {
            Some(routes) => routes.into_iter().fold(self, |router, (mut sp, route)| {
                let is_empty = sp.is_empty();
                sp = path.clone() + &sp;
                if is_empty {
                    sp = sp.trim_end_matches('/').to_string();
                }
                router.route(sp, route)
            }),
            None => self,
        }
    }

    pub fn with<T>(self, t: T) -> Self
    where
        T: Transform<BoxHandler>,
        T::Output: Handler<Request<Body>, Output = Result<Response<Body>>>,
    {
        Self {
            routes: self.routes.map(|routes| {
                routes
                    .into_iter()
                    .map(|(path, route)| {
                        (
                            path,
                            route
                                .into_iter()
                                .map(|(method, handler)| (method, t.transform(handler).boxed()))
                                .collect(),
                        )
                    })
                    .collect()
            }),
        }
    }

    pub fn with_handler<F>(self, f: F) -> Self
    where
        F: Handler<Next<Request<Body>, BoxHandler>, Output = Result<Response<Body>>> + Clone,
    {
        Self {
            routes: self.routes.map(|routes| {
                routes
                    .into_iter()
                    .map(|(path, route)| {
                        (
                            path,
                            route
                                .into_iter()
                                .map(|(method, handler)| {
                                    (method, handler.around(f.clone()).boxed())
                                })
                                .collect(),
                        )
                    })
                    .collect()
            }),
        }
    }

    pub fn map_handler<F>(self, f: F) -> Self
    where
        F: Fn(BoxHandler) -> BoxHandler,
    {
        Self {
            routes: self.routes.map(|routes| {
                routes
                    .into_iter()
                    .map(|(path, route)| {
                        (
                            path,
                            route
                                .into_iter()
                                .map(|(method, handler)| (method, f(handler)))
                                .collect(),
                        )
                    })
                    .collect()
            }),
        }
    }
}

#[cfg(test)]
mod tests {
    use crate::{get, Resource, Route, Router, Tree};
    use viz_core::{
        async_trait, handler::Transform, Body, Handler, HandlerExt, IntoResponse, Method, Next,
        Request, Response, Result, StatusCode,
    };

    #[derive(Clone)]
    struct Logger;

    impl Logger {
        fn new() -> Self {
            Self
        }
    }

    impl<H: Clone> Transform<H> for Logger {
        type Output = LoggerHandler<H>;

        fn transform(&self, h: H) -> Self::Output {
            LoggerHandler(h.clone())
        }
    }

    #[derive(Clone)]
    struct LoggerHandler<H>(H);

    #[async_trait]
    impl<H> Handler<Request<Body>> for LoggerHandler<H>
    where
        H: Handler<Request<Body>> + Clone,
    {
        type Output = H::Output;

        async fn call(&self, req: Request<Body>) -> Self::Output {
            dbg!("before logger");
            let res = self.0.call(req).await;
            dbg!("after logger");
            res
        }
    }

    #[tokio::test]
    async fn router() -> anyhow::Result<()> {
        async fn index(req: Request<Body>) -> Result<Response<Body>> {
            Ok(Response::new("index".into()))
        }

        async fn any(req: Request<Body>) -> Result<Response<Body>> {
            Ok(Response::new("any".into()))
        }

        async fn not_found(req: Request<Body>) -> Result<impl IntoResponse> {
            Ok(StatusCode::NOT_FOUND)
        }

        async fn search(req: Request<Body>) -> Result<Response<Body>> {
            Ok(Response::new("search".into()))
        }

        async fn show(req: Request<Body>) -> Result<Response<Body>> {
            Ok(Response::new("show".into()))
        }

        async fn create(req: Request<Body>) -> Result<Response<Body>> {
            Ok(Response::new("create".into()))
        }

        async fn update(req: Request<Body>) -> Result<Response<Body>> {
            Ok(Response::new("update".into()))
        }

        async fn delete(req: Request<Body>) -> Result<Response<Body>> {
            Ok(Response::new("delete".into()))
        }

        let users = Resource::default()
            .named("user")
            .index(index)
            .create(create.before(|r: Request<Body>| async { Ok(r) }))
            .show(show)
            .update(update)
            .destroy(delete)
            .with(Logger::new());

        let posts = Router::new().route("search", get(search)).resource(
            "",
            Resource::default()
                .named("post")
                .create(create)
                .show(show)
                .update(update)
                .destroy(delete)
                .with(Logger::new()),
        );

        let router = Router::new()
            .route("", get(index))
            .resource("users", users.clone())
            .nest("posts", posts.resource(":post_id/users", users))
            .route("*", Route::new().any(not_found))
            .with(Logger::new());

        dbg!(&router);

        let tree: Tree = router.into();

        Ok(())
    }
}