ntex 4.0.0-beta.11

Framework for composable network services
Documentation
use std::{marker::PhantomData, rc::Rc};

use crate::error::{Failure, IntoFailure};
use crate::router::{IntoPattern, ResourceDef};
use crate::service::{IntoServiceFactory, ServiceFactory, boxed};

use super::error::{WebError, WebResponseError};
use super::guard::{AllGuard, Guard};
use super::{AppState, HttpService, WebRequest, WebResponse, dev::insert_slash, rmap::ResourceMap};

pub trait WebServiceFactory<St: AppState, In>: 'static {
    fn register(self, config: &mut WebServiceConfig<St, In>);
}

pub(super) trait AppServiceFactory<St: AppState, In> {
    fn register(&mut self, config: &mut WebServiceConfig<St, In>);
}

pub(super) struct ServiceFactoryWrapper<T> {
    factory: Option<T>,
}

impl<T> ServiceFactoryWrapper<T> {
    pub(super) fn new(factory: T) -> Self {
        Self {
            factory: Some(factory),
        }
    }
}

impl<T, St, In> AppServiceFactory<St, In> for ServiceFactoryWrapper<T>
where
    T: WebServiceFactory<St, In>,
    St: AppState,
{
    fn register(&mut self, config: &mut WebServiceConfig<St, In>) {
        if let Some(item) = self.factory.take() {
            item.register(config);
        }
    }
}

type Guards = Vec<Box<dyn Guard>>;

/// Application service configuration
#[derive(derive_more::Debug)]
#[debug("WebServiceConfig")]
pub struct WebServiceConfig<St: AppState, In = ()> {
    root: bool,
    services: Vec<(
        ResourceDef,
        HttpService<St, In>,
        Option<Guards>,
        Option<Rc<ResourceMap>>,
    )>,
}

impl<St: AppState, In: 'static> WebServiceConfig<St, In> {
    /// Crate server settings instance
    pub(crate) fn new() -> Self {
        WebServiceConfig {
            root: true,
            services: Vec::new(),
        }
    }

    /// Check if root is beeing configured
    pub fn is_root(&self) -> bool {
        self.root
    }

    pub(crate) fn into_services(
        self,
    ) -> Vec<(
        ResourceDef,
        HttpService<St, In>,
        Option<Guards>,
        Option<Rc<ResourceMap>>,
    )> {
        self.services
    }

    /// Register http service
    pub fn register_service<S>(
        &mut self,
        rdef: ResourceDef,
        guards: Option<Vec<Box<dyn Guard>>>,
        nested: Option<Rc<ResourceMap>>,
        factory: impl IntoServiceFactory<S, St, WebRequest<In>>,
    ) where
        S: ServiceFactory<
                St,
                WebRequest<In>,
                Res = WebResponse,
                Error = WebError<St, St::Error>,
                InitError = Failure,
            > + 'static,
    {
        self.services
            .push((rdef, boxed::factory(factory.into_factory()), guards, nested));
    }
}

/// Create service adapter for a specific path.
///
/// ```rust
/// use std::convert::Infallible;
/// use ntex::web::{self, guard, App, HttpResponse};
///
/// async fn my_service(req: web::WebRequest<()>) -> Result<web::WebResponse, Infallible> {
///     Ok(req.into_response(HttpResponse::Ok().build()))
/// }
///
/// let app = App::default().service(
///     web::service("/users/*")
///         .guard(guard::Header("content-type", "text/plain"))
///         .build(my_service)
/// );
/// ```
#[derive(Debug)]
pub struct WebServiceAdapter {
    rdef: Vec<String>,
    name: Option<String>,
    guards: AllGuard,
}

impl WebServiceAdapter {
    #[must_use]
    #[allow(clippy::needless_pass_by_value)]
    /// Create new `WebServiceAdapter` instance.
    pub fn new<T: IntoPattern>(path: T) -> Self {
        WebServiceAdapter {
            rdef: path.patterns(),
            name: None,
            guards: AllGuard::default(),
        }
    }

    /// Set service name.
    ///
    /// Name is used for url generation.
    #[must_use]
    pub fn name(mut self, name: &str) -> Self {
        self.name = Some(name.to_string());
        self
    }

    /// Add match guard to a web service.
    ///
    /// ```rust
    /// use std::convert::Infallible;
    /// use ntex::web::{self, guard, App, WebError, HttpResponse};
    ///
    /// async fn index(req: web::WebRequest<()>) -> Result<web::WebResponse, Infallible> {
    ///     Ok(req.into_response(HttpResponse::Ok().build()))
    /// }
    ///
    /// fn main() {
    ///     let app = App::default()
    ///         .service(
    ///             web::service("/app")
    ///                 .guard(guard::Header("content-type", "text/plain"))
    ///                 .build(index)
    ///         );
    /// }
    /// ```
    #[must_use]
    pub fn guard<G: Guard + 'static>(mut self, guard: G) -> Self {
        self.guards.add(guard);
        self
    }

    /// Set a service factory implementation and generate web service.
    pub fn build<Sf, St, In, F>(self, service: F) -> impl WebServiceFactory<St, In>
    where
        St: AppState,
        In: 'static,
        F: IntoServiceFactory<Sf, St, WebRequest<In>>,
        Sf: ServiceFactory<St, WebRequest<In>, Res = WebResponse> + 'static,
        Sf::Error: WebResponseError<St, St::Error>,
        Sf::InitError: IntoFailure,
    {
        WebServiceImpl {
            srv: service
                .into_factory()
                .map_err(WebError::from_err)
                .map_init_err(IntoFailure::fail),
            rdef: self.rdef,
            name: self.name,
            guards: self.guards,
            ph: PhantomData,
        }
    }
}

struct WebServiceImpl<Sf, In> {
    srv: Sf,
    rdef: Vec<String>,
    name: Option<String>,
    guards: AllGuard,
    ph: PhantomData<In>,
}

impl<Sf, In, St> WebServiceFactory<St, In> for WebServiceImpl<Sf, In>
where
    St: AppState,
    In: 'static,
    Sf: ServiceFactory<
            St,
            WebRequest<In>,
            Res = WebResponse,
            Error = WebError<St, St::Error>,
            InitError = Failure,
        > + 'static,
{
    fn register(mut self, config: &mut WebServiceConfig<St, In>) {
        let guards = if self.guards.0.is_empty() {
            None
        } else {
            Some(std::mem::take(&mut self.guards.0))
        };

        let mut rdef = if config.is_root() || !self.rdef.is_empty() {
            ResourceDef::new(insert_slash(self.rdef))
        } else {
            ResourceDef::new(self.rdef)
        };
        if let Some(ref name) = self.name {
            rdef.name_mut().clone_from(name);
        }
        config.register_service(rdef, guards, None, self.srv);
    }
}

#[allow(unused_parens)]
impl<T, St, In> WebServiceFactory<St, In> for Vec<T>
where
    T: WebServiceFactory<St, In> + 'static,
    St: AppState,
{
    fn register(mut self, config: &mut WebServiceConfig<St, In>) {
        for service in self.drain(..) {
            service.register(config);
        }
    }
}

macro_rules! tuple_web_service(
    {$(#[$meta:meta])* $(($n:tt, $T:ident)),+} => {

        $(#[$meta])*
        impl<St: AppState, In, $($T: WebServiceFactory<St, In> + 'static),+> WebServiceFactory<St, In> for ($($T,)+) {
            fn register(self, config: &mut WebServiceConfig<St, In>) {
                $(
                    self.$n.register(config);
                )+
            }
        }
    }
);

impl<St, In, T, const N: usize> WebServiceFactory<St, In> for [T; N]
where
    St: AppState,
    T: WebServiceFactory<St, In> + 'static,
{
    fn register(self, config: &mut WebServiceConfig<St, In>) {
        for t in self {
            t.register(config);
        }
    }
}

#[allow(non_snake_case, clippy::wildcard_imports)]
#[rustfmt::skip]
mod m {
    use super::*;
    use variadics_please::all_tuples_enumerated;

    all_tuples_enumerated!(#[doc(fake_variadic)] tuple_web_service, 1, 24, T);
}

#[cfg(test)]
mod tests {
    use std::convert::Infallible;

    use super::*;
    use crate::http::{Method, StatusCode};
    use crate::web::test::{TestRequest, init_service};
    use crate::web::{self, App, DefaultError, HttpResponse, guard};

    #[crate::rt_test]
    async fn test_service() {
        let srv = init_service(App::new().service(web::service("/test").name("test").build(
            async move |req: WebRequest<()>| {
                Ok::<_, Infallible>(req.into_response(HttpResponse::Ok().build()))
            },
        )))
        .await;
        let req = TestRequest::with_uri("/test").to_request();
        let resp = srv.call(req).await.unwrap();
        assert_eq!(resp.status(), StatusCode::OK);

        let srv = init_service(
            App::new().service(web::service("/test").guard(guard::Get()).build(
                async move |req: WebRequest<()>| {
                    Ok::<_, DefaultError>(req.into_response(HttpResponse::Ok().build()))
                },
            )),
        )
        .await;
        let req = TestRequest::with_uri("/test")
            .method(Method::PUT)
            .to_request();
        let resp = srv.call(req).await.unwrap();
        assert_eq!(resp.status(), StatusCode::NOT_FOUND);
    }

    #[crate::rt_test]
    async fn test_multi() {
        let srv = init_service(App::new().service([
            web::resource("/test1").to(async || HttpResponse::Ok()),
            web::resource("/test2").to(async || HttpResponse::Ok()),
        ]))
        .await;
        let req = TestRequest::with_uri("/test1").to_request();
        let resp = srv.call(req).await.unwrap();
        assert_eq!(resp.status(), StatusCode::OK);
        let req = TestRequest::with_uri("/test2").to_request();
        let resp = srv.call(req).await.unwrap();
        assert_eq!(resp.status(), StatusCode::OK);

        let srv = init_service(App::new().service((
            web::resource("/test1").to(async || HttpResponse::Ok()),
            web::resource("/test2").to(async || HttpResponse::Ok()),
        )))
        .await;
        let req = TestRequest::with_uri("/test1").to_request();
        let resp = srv.call(req).await.unwrap();
        assert_eq!(resp.status(), StatusCode::OK);
        let req = TestRequest::with_uri("/test2").to_request();
        let resp = srv.call(req).await.unwrap();
        assert_eq!(resp.status(), StatusCode::OK);

        let srv = init_service(App::new().service(vec![
            web::resource("/test1").to(async || HttpResponse::Ok()),
            web::resource("/test2").to(async || HttpResponse::Ok()),
        ]))
        .await;
        let req = TestRequest::with_uri("/test1").to_request();
        let resp = srv.call(req).await.unwrap();
        assert_eq!(resp.status(), StatusCode::OK);
        let req = TestRequest::with_uri("/test2").to_request();
        let resp = srv.call(req).await.unwrap();
        assert_eq!(resp.status(), StatusCode::OK);
    }

    #[test]
    fn test_fmt_debug() {
        let req = TestRequest::get()
            .uri("/index.html?test=1")
            .header("x-test", "111")
            .to_srv_request();
        let s = format!("{req:?}");
        assert!(s.contains("WebRequest"));
        assert!(s.contains("test=1"));
        assert!(s.contains("x-test"));

        let res = HttpResponse::Ok().header("x-test", "111").build();
        let res = TestRequest::post()
            .uri("/index.html?test=1")
            .to_srv_response(res);

        let s = format!("{res:?}");
        assert!(s.contains("WebResponse"));
        assert!(s.contains("x-test"));
    }
}