serverkit 0.2.0

A portable Rust HTTP application layer for Workers and native servers
Documentation
use std::{any::TypeId, marker::PhantomData};

use crate::{Handler, Method, Middleware, Router, middleware::MiddlewareEntry, openapi::Operation};

type OperationModifier = Box<dyn FnOnce(&mut Operation)>;

pub struct Route<H, Arguments, Input> {
    path: &'static str,
    method: Method,
    handler: H,
    operation_modifiers: Vec<OperationModifier>,
    middlewares: Vec<MiddlewareEntry>,
    excluded_middlewares: Vec<TypeId>,
    signature: PhantomData<fn() -> (Arguments, Input)>,
}

impl<H, Arguments, Input> Route<H, Arguments, Input> {
    pub fn summary(self, summary: impl Into<String>) -> Self {
        let summary = summary.into();
        self.openapi(move |operation| {
            operation.summary(summary);
        })
    }

    pub fn description(self, description: impl Into<String>) -> Self {
        let description = description.into();
        self.openapi(move |operation| {
            operation.description(description);
        })
    }

    pub fn tag(self, tag: impl Into<String>) -> Self {
        let tag = tag.into();
        self.openapi(move |operation| {
            operation.tag(tag);
        })
    }

    pub fn operation_id(self, operation_id: impl Into<String>) -> Self {
        let operation_id = operation_id.into();
        self.openapi(move |operation| {
            operation.operation_id(operation_id);
        })
    }

    pub fn openapi(mut self, modifier: impl FnOnce(&mut Operation) + 'static) -> Self {
        self.operation_modifiers.push(Box::new(modifier));
        self
    }

    pub fn middleware<M: Middleware>(mut self, middleware: M) -> Self {
        self.middlewares.push(MiddlewareEntry::new(middleware));
        self
    }

    pub fn without_middleware<M: Middleware>(mut self) -> Self {
        let type_id = TypeId::of::<M>();
        if !self.excluded_middlewares.contains(&type_id) {
            self.excluded_middlewares.push(type_id);
        }
        self
    }
}

macro_rules! route_methods {
    ($($method:ident),+ $(,)?) => {
        #[allow(non_snake_case)]
        pub trait RouteMethods {
            fn on<Arguments, Input, H: Handler<Arguments, Input>>(
                self,
                method: Method,
                handler: H,
            ) -> Route<H, Arguments, Input>;

            $(
                fn $method<Arguments, Input, H: Handler<Arguments, Input>>(
                    self,
                    handler: H,
                ) -> Route<H, Arguments, Input>;
            )+
        }

        #[allow(non_snake_case)]
        impl RouteMethods for &'static str {
            fn on<Arguments, Input, H: Handler<Arguments, Input>>(
                self,
                method: Method,
                handler: H,
            ) -> Route<H, Arguments, Input> {
                Route {
                    path: self,
                    method,
                    handler,
                    operation_modifiers: Vec::new(),
                    middlewares: Vec::new(),
                    excluded_middlewares: Vec::new(),
                    signature: PhantomData,
                }
            }

            $(
                fn $method<Arguments, Input, H: Handler<Arguments, Input>>(
                    self,
                    handler: H,
                ) -> Route<H, Arguments, Input> {
                    self.on(Method::$method, handler)
                }
            )+
        }
    };
}

route_methods!(GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS, CONNECT, TRACE);

#[doc(hidden)]
pub trait Routes {
    fn apply(self, router: &mut Router);
}

impl Routes for () {
    fn apply(self, _router: &mut Router) {}
}

impl<Arguments: 'static, Input: 'static, H: Handler<Arguments, Input> + Send + Sync + 'static>
    Routes for Route<H, Arguments, Input>
{
    fn apply(self, router: &mut Router) {
        let mut operation = H::openapi();
        for modifier in self.operation_modifiers {
            modifier(&mut operation);
        }
        router.register(
            self.method,
            self.path,
            self.handler,
            operation,
            self.middlewares,
            self.excluded_middlewares,
        );
    }
}

impl Routes for Router {
    fn apply(self, router: &mut Router) {
        router.register_router(self);
    }
}

macro_rules! impl_route_tuple {
    ($($route:ident),+) => {
        impl<$($route: Routes),+> Routes for ($($route,)+) {
            #[allow(non_snake_case)]
            fn apply(self, router: &mut Router) {
                let ($($route,)+) = self;

                $($route.apply(router);)+
            }
        }
    };
}

serverkit_macros::impl_routes!(16);

#[cfg(test)]
mod tests {
    use crate::{Config, Method, RouteMethods, Router};

    async fn health() -> &'static str {
        "ok"
    }

    async fn version() -> &'static str {
        "0.1.0"
    }

    async fn accepted() -> &'static str {
        "accepted"
    }

    #[test]
    fn routes_can_be_registered_with_an_app() {
        let _application = Router::new(
            Config::new(),
            (
                "/health".GET(health),
                "/version".GET(version),
                "/post".POST(accepted),
                "/put".PUT(accepted),
                "/patch".PATCH(accepted),
                "/delete".DELETE(accepted),
                "/head".HEAD(accepted),
                "/options".OPTIONS(accepted),
                "/connect".CONNECT(accepted),
                "/trace".TRACE(accepted),
                "/propfind".on(Method::from_bytes(b"PROPFIND").unwrap(), accepted),
            ),
        );
    }

    #[test]
    fn generates_route_tuples_through_the_configured_maximum() {
        let _application = Router::new(
            Config::new(),
            (
                "/1".GET(health),
                "/2".GET(health),
                "/3".GET(health),
                "/4".GET(health),
                "/5".GET(health),
                "/6".GET(health),
                "/7".GET(health),
                "/8".GET(health),
                "/9".GET(health),
                "/10".GET(health),
                "/11".GET(health),
                "/12".GET(health),
                "/13".GET(health),
                "/14".GET(health),
                "/15".GET(health),
                "/16".GET(health),
            ),
        );
    }
}