dipper 0.5.3

An out-of-the-box modular dependency injection web application framework.
Documentation
use core::time::Duration;
use std::{cell::Cell, env, sync::Arc};

use humantime::format_duration;
use tokio::{net::ToSocketAddrs, signal};

use crate::{
    catcher::Catcher,
    dyn_mod::{DmRouter, Module, ModuleBuilder},
    http::{Mime, uri::Scheme},
    mode::{Mode, current_mode},
    mw::*,
    otel,
    prelude::*,
};

#[macro_export]
macro_rules! new_app {
    ($($module:ty $(, $module_more:ty)*)*) => {
        {
            $crate::dyn_mod::dyn_modules![$($module $(, $module_more)*)*];
            App::<DynModules>::new(
                env!("CARGO_PKG_NAME"),
                env!("CARGO_PKG_VERSION"),
                DynModules::builder(),
            )
        }
    };
}

/// An environment variable of type u16, representing the work_id of SULID.
/// The default value is 0.
pub const ENV_DIPPER_WORKER_ID: &str = "DIPPER_WORKER_ID";
/// Timeout seconds environment variable for graceful shutdown.
/// 1. If not set or set to 0, it means that graceful shutdown is not enabled.
/// 2. Setting it to a negative number (such as -1) means that the timeout
///    period is infinitely long.
/// 3. A positive number indicates the number of seconds for timeout.
pub const ENV_DIPPER_SHUTDOWN_SECS: &str = "DIPPER_SHUTDOWN_SECS";

pub struct App<M: Module> {
    name: &'static str,
    version: &'static str,
    router: Router,
    module_builder: Cell<Option<ModuleBuilder<M>>>,
    service: Service,
    unroutable_scribe: Arc<dyn UnroutableScribe>,
    routable_preset: bool,
    router_path_prefix: String,
}

impl<M: Module> App<M> {
    /// It is recommended to use the `new_app![ModuleType,...]` macro to create
    /// an App. This method is mainly provided for use by this macro.
    pub fn new(name: &'static str, version: &'static str, module_builder: ModuleBuilder<M>) -> Self {
        Self {
            name,
            version,
            router: Router::new(),
            module_builder: Cell::new(Some(module_builder)),
            service: Service::new(Router::new()),
            unroutable_scribe: Arc::new(DefaultUnroutableScribe),
            routable_preset: false,
            router_path_prefix: String::new(),
        }
    }

    /// Set the parameters of the specified component. If the parameters are not
    /// manually set, the defaults will be used.
    pub fn with_component_parameters<C: Component<M>>(self, params: C::Parameters) -> Self
    where
        M: HasComponent<C::Interface>,
    {
        self.set_module_builder(self.take_module_builder().with_component_parameters::<C>(params));
        self
    }

    /// Override a component implementation. This method is best used when the
    /// overriding component has no injected dependencies.
    pub fn with_component_override<I: Interface + ?Sized>(self, component: Box<I>) -> Self
    where
        M: HasComponent<I>,
    {
        self.set_module_builder(self.take_module_builder().with_component_override::<I>(component));
        self
    }

    /// Override a component implementation. This method is best used when the
    /// overriding component has injected dependencies.
    pub fn with_component_override_fn<I: Interface + ?Sized>(self, component_fn: ComponentFn<M, I>) -> Self
    where
        M: HasComponent<I>,
    {
        self.set_module_builder(self.take_module_builder().with_component_override_fn::<I>(component_fn));
        self
    }

    fn take_module_builder(&self) -> ModuleBuilder<M> {
        self.module_builder.take().unwrap()
    }

    fn set_module_builder(&self, module_builder: ModuleBuilder<M>) {
        self.module_builder.set(Some(module_builder))
    }

    /// Set the preset value of routable.
    pub const fn with_routable_preset(mut self, routable_preset: bool) -> Self {
        self.routable_preset = routable_preset;
        self
    }

    /// Set up a scribe that provides a response when the module is not allowed
    /// to be accessed.
    pub fn with_unroutable_scribe(mut self, unroutable_scribe: Arc<dyn UnroutableScribe>) -> Self {
        self.unroutable_scribe = unroutable_scribe;
        self
    }

    /// Set the global route path prefix.
    ///
    /// # Panics
    ///
    /// Panics if path value is not in correct format.
    #[inline]
    pub fn path_prefix(mut self, path: impl Into<String>) -> Self {
        let path: String = path.into();
        let s = path.trim_ascii().trim_matches('/');
        self.router_path_prefix = if s.is_empty() { s.to_owned() } else { "/".to_owned() + s };
        self
    }

    /// Push a router as child of current router.
    pub fn router_push(mut self, router: Router) -> Self {
        self.router = self.router.push(router);
        self
    }

    /// Add middleware for the routes of dynamic modules.
    #[inline]
    pub fn dm_router_hoop<H: Handler>(mut self, hoop: H) -> Self {
        self.router = self.router.hoop(hoop);
        self
    }

    /// Add middleware for the routes of dynamic modules.
    /// This middleware only effective when the filter return true.
    #[inline]
    pub fn dm_router_hoop_when<H, F>(mut self, hoop: H, filter: F) -> Self
    where
        H: Handler,
        F: Fn(&Request, &Depot) -> bool + Send + Sync + 'static,
    {
        self.router = self.router.hoop_when(hoop, filter);
        self
    }

    /// When you want write router chain, this function will be useful,
    /// You can write your custom logic in FnOnce.
    #[inline]
    pub fn router_then<F>(mut self, func: F) -> Self
    where
        F: FnOnce(Router) -> Router,
    {
        self.router = self.router.then(func);
        self
    }

    /// Add a [`SchemeFilter`] to current router.
    ///
    /// [`SchemeFilter`]: salvo::routing::filters::HostFilter
    #[inline]
    pub fn router_scheme(mut self, scheme: Scheme) -> Self {
        self.router = self.router.scheme(scheme);
        self
    }

    /// Add a [`HostFilter`] to current router.
    ///
    /// [`HostFilter`]: salvo::routing::filters::HostFilter
    #[inline]
    pub fn router_host(mut self, host: impl Into<String>) -> Self {
        self.router = self.router.host(host);
        self
    }

    /// When the response code is 400-600 and the body is empty, capture and set
    /// the error page content. If catchers is not set, the default error
    /// page will be used.
    ///
    /// # Example
    ///
    /// ```
    /// use dipper::{catcher::Catcher, prelude::*};
    ///
    /// #[handler]
    /// async fn handle404(
    ///     &self,
    ///     _req: &Request,
    ///     _depot: &Depot,
    ///     res: &mut Response,
    ///     ctrl: &mut FlowCtrl,
    /// ) {
    ///     if let Some(StatusCode::NOT_FOUND) = res.status_code {
    ///         res.render("Custom 404 Error Page");
    ///         ctrl.skip_rest();
    ///     }
    /// }
    ///
    /// #[tokio::main]
    /// async fn main() {
    ///     Service::new(Router::new()).catcher(Catcher::default().hoop(handle404));
    /// }
    /// ```
    #[inline]
    pub fn service_catcher(mut self, catcher: impl Into<Arc<Catcher>>) -> Self {
        self.service = self.service.catcher(catcher);
        self
    }

    /// Add a handler as middleware, it will run the handler when request
    /// received.
    #[inline]
    pub fn service_hoop<H: Handler>(mut self, hoop: H) -> Self {
        self.service = self.service.hoop(hoop);
        self
    }

    /// Add a handler as middleware, it will run the handler when request
    /// received.
    ///
    /// This middleware only effective when the filter return true.
    #[inline]
    pub fn service_hoop_when<H, F>(mut self, hoop: H, filter: F) -> Self
    where
        H: Handler,
        F: Fn(&Request, &Depot) -> bool + Send + Sync + 'static,
    {
        self.service = self.service.hoop_when(hoop, filter);
        self
    }

    /// Sets allowed media types list and returns `Self` for write code chained.
    ///
    /// # Example
    ///
    /// ```
    /// # use dipper::prelude::*;
    ///
    /// # #[tokio::main]
    /// # async fn main() {
    /// let service = Service::new(Router::new()).allowed_media_types(vec![mime::TEXT_PLAIN]);
    /// # }
    /// ```
    #[inline]
    pub fn allowed_media_types<T>(mut self, allowed_media_types: T) -> Self
    where
        T: Into<Arc<Vec<Mime>>>,
    {
        self.service = self.service.allowed_media_types(allowed_media_types);
        self
    }

    /// Run server.
    /// `local_addr` can be `"0.0.0.0:8888"`.
    #[allow(static_mut_refs)]
    pub async fn run(mut self, local_addr: impl ToSocketAddrs + Send) {
        // Create routes for dynamic modules and transfer the registered middleware to
        // this dynamic module. That is, other routes do not register these
        // middleware.
        let dm_router = DmRouter::new(std::mem::take(&mut self.router.hoops), self.unroutable_scribe);
        let _ = self
            .module_builder
            .take()
            .unwrap()
            .with_router(dm_router.clone())
            .with_routable_preset(self.routable_preset)
            .build();
        unsafe {
            debug!("{}", MANIFEST_LIST.to_xml_string());
        };

        let mut root_router = Router::new();
        if !self.router_path_prefix.is_empty() {
            root_router = root_router.path(&self.router_path_prefix);
        }
        root_router = root_router.push(dm_router.into_inner()).push(self.router);

        match current_mode() {
            Mode::Prod => {}
            _ => {
                root_router = root_router.push(Router::with_path("/api-err.json").get(api_err_list));
                let doc =
                    OpenApi::new(format!("{} API", self.name.to_uppercase()), self.version).merge_router(&root_router);
                let doc_router_path = "/api-doc/openapi.json";
                root_router = root_router
                    .push(doc.into_router(doc_router_path))
                    .push(SwaggerUi::new(self.router_path_prefix.clone() + doc_router_path).into_router("/api-doc"))
            }
        }

        self.service.router = root_router.into();
        let worker_id: u16 = env::var(ENV_DIPPER_WORKER_ID).map_or(0u16, |v| {
            v.parse::<u16>()
                .expect("The env 'DIPPER_WORKER_ID' must be 'u16' integer")
        });
        let mut hoops: Vec<Arc<dyn Handler>> = vec![
            Arc::new(TraceHoop::new()),
            Arc::new(Metrics::new(self.name.to_owned())),
            Arc::new(RequestIdHoop::new(worker_id)),
            Arc::new(CatchPanic::new()),
            Arc::new(affix_state::inject(unsafe { &MANIFEST_LIST })),
        ];
        hoops.append(&mut self.service.hoops);
        self.service.hoops = hoops;

        let acceptor = TcpListener::new(local_addr).bind().await;
        let server = Server::new(acceptor);

        let shutdown_secs = env::var(ENV_DIPPER_SHUTDOWN_SECS).map_or(0i64, |v| {
            v.parse().expect("The env 'DIPPER_SHUTDOWN_SECS' must be integer.")
        });
        if shutdown_secs != 0 {
            let handle = server.handle();
            let shutdown_secs = if shutdown_secs > 0 {
                #[allow(clippy::cast_sign_loss)]
                Some(Duration::from_secs(shutdown_secs as u64))
            } else {
                None
            };
            tokio::spawn(async move {
                if let Some(shutdown_secs) = shutdown_secs {
                    info!(
                        "Enable graceful shutdown with a timeout of {}.",
                        format_duration(shutdown_secs)
                    );
                } else {
                    info!("Enable graceful shutdown without timeout.");
                }
                #[cfg(not(windows))]
                {
                    let mut sigint_stream = signal::unix::signal(signal::unix::SignalKind::interrupt()).unwrap();
                    let mut sigterm_stream = signal::unix::signal(signal::unix::SignalKind::terminate()).unwrap();
                    tokio::select! {
                        _ = sigint_stream.recv() => {
                            println!("Received SIGINT");
                        },
                        _ = sigterm_stream.recv() => {
                            println!("Received SIGTERM");
                        },
                    }
                }
                #[cfg(windows)]
                {
                    signal::ctrl_c().await.expect("failed to listen for ctrl-c event");
                    println!("Received ctrl-c event");
                }

                handle.stop_graceful(shutdown_secs);
                otel::shutdown_all_providers();
            });
        } else {
            info!("Disable graceful shutdown.");
        }

        server.serve(self.service).await;
    }
}