rust-webx-host 0.3.0

rust-webx HTTP layer: Host builder, middleware pipeline, Trie-based router, hyper integration
Documentation
//! Endpoint —IEndpoint implementations for dual-mode dispatch.

use rust_webx_core::error::Result;
use rust_webx_core::http::IHttpContext;
use rust_webx_core::routing::IEndpoint;
use serde_json;
use std::sync::Arc;

use crate::authz::AuthorizerSet;
use crate::problem_response::{write_forbidden, write_problem};
use std::collections::HashMap;

/// Endpoint that wraps a boxed async handler.
#[allow(clippy::type_complexity)]
pub struct RequestEndpoint {
    handler: Box<
        dyn Fn(
                &mut dyn IHttpContext,
            )
                -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<()>> + Send + '_>>
            + Send
            + Sync,
    >,
}

#[async_trait::async_trait]
impl IEndpoint for RequestEndpoint {
    async fn handle(&self, ctx: &mut dyn IHttpContext) -> Result<()> {
        (self.handler)(ctx).await
    }
}

impl RequestEndpoint {
    pub fn new<F>(f: F) -> Self
    where
        F: for<'a> Fn(
                &'a mut dyn IHttpContext,
            ) -> std::pin::Pin<
                Box<dyn std::future::Future<Output = Result<()>> + Send + 'a>,
            > + Send
            + Sync
            + 'static,
    {
        Self {
            handler: Box::new(f),
        }
    }
}

/// A route endpoint that dispatches to the registered handler via cache.
///
/// When a dispatch function is available (generated by the endpoint macros),
/// it constructs the request from the HTTP request data, calls the handler
/// via the HandlerCache call bridge, and writes the JSON response.
/// Falls back to a descriptive stub message when no dispatch function is registered.
#[allow(clippy::type_complexity)]
pub struct StubEndpoint {
    pub method: &'static str,
    pub path: &'static str,
    pub handler_type: &'static str,
    pub dispatch_fn: Option<
        fn(
            Vec<u8>,
            std::collections::HashMap<String, String>,
            std::collections::HashMap<String, String>,
            Option<Box<dyn rust_webx_core::auth::IClaims>>,
        ) -> std::pin::Pin<
            Box<dyn std::future::Future<Output = Result<rust_webx_core::route::scan::ResponseData>> + Send>,
        >,
    >,
    pub auth_required_role: &'static str,
    /// Dynamic authorizers (from `IDynamicAuthorizer` DI registrations).
    /// Checked after static `#[authorize]` and before handler dispatch.
    /// When `None`, no dynamic authorization checks run (pass-through).
    pub authorizers: Option<Arc<AuthorizerSet>>,
}

#[async_trait::async_trait]
impl IEndpoint for StubEndpoint {
    async fn handle(&self, ctx: &mut dyn IHttpContext) -> Result<()> {
        if let Some(dispatch) = self.dispatch_fn {
            let body_bytes = ctx.request().body_bytes().await.unwrap_or_default();
            let route_params = ctx.request().route_params().clone();
            let query_params = ctx.request().query().clone();

            // Extract claims from the context so they can be forwarded explicitly
            // to the handler via the dispatch function (fearless concurrency).
            let claims = ctx.claims().map(|c| c.clone_box());

            // ── Authorization check (declared on route via #[authorize]) ──
            if !self.auth_required_role.is_empty() {
                match ctx.claims() {
                    None => {
                        write_problem(ctx, 401, "Authentication required").await;
                        return Ok(());
                    }
                    Some(claims) => {
                        if self.auth_required_role != "authenticated"
                            && !claims
                                .roles()
                                .iter()
                                .any(|r| r.as_str() == self.auth_required_role)
                        {
                            let mut ext = HashMap::new();
                            ext.insert(
                                "required_role".into(),
                                serde_json::Value::String(self.auth_required_role.to_string()),
                            );
                            write_forbidden(
                                ctx,
                                "Forbidden: insufficient permissions",
                                ext,
                            )
                            .await;
                            return Ok(());
                        }
                    }
                }
            }

            // ── Dynamic authorization via IDynamicAuthorizer ──
            if !self.auth_required_role.is_empty() {
                if let Some(ref authorizers) = self.authorizers {
                    if !authorizers.is_empty() {
                        if let Some(claims) = ctx.claims() {
                            let resource_key = ctx.request().route_pattern().unwrap_or(self.path);
                            let method = ctx.request().method();
                            authorizers.authorize(claims, resource_key, method).await?;
                        }
                    }
                }
            }

            let response = dispatch(body_bytes, route_params, query_params, claims).await?;

            ctx.response_mut().set_status(response.status);
            ctx.response_mut()
                .set_header("content-type", &response.content_type);
            ctx.response_mut().write_bytes(response.body).await?;
            return Ok(());
        }

        // Fallback: stub message (when no dispatch function is registered)
        ctx.response_mut().set_status(200);
        ctx.response_mut()
            .write_text(&format!(
                "Matched route: {} {} (handler: {})",
                self.method, self.path, self.handler_type
            ))
            .await?;
        Ok(())
    }
}

/// Endpoint for controller-based methods.
#[allow(clippy::type_complexity)]
pub struct ControllerEndpoint {
    handler: Box<
        dyn Fn(
                &mut dyn IHttpContext,
            )
                -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<()>> + Send + '_>>
            + Send
            + Sync,
    >,
}

#[async_trait::async_trait]
impl IEndpoint for ControllerEndpoint {
    async fn handle(&self, ctx: &mut dyn IHttpContext) -> Result<()> {
        (self.handler)(ctx).await
    }
}

impl ControllerEndpoint {
    pub fn new<F>(f: F) -> Self
    where
        F: for<'a> Fn(
                &'a mut dyn IHttpContext,
            ) -> std::pin::Pin<
                Box<dyn std::future::Future<Output = Result<()>> + Send + 'a>,
            > + Send
            + Sync
            + 'static,
    {
        Self {
            handler: Box::new(f),
        }
    }
}

/// Endpoint that serves a static JSON payload.
///
/// Used for built-in endpoints like `/openapi.json` and `/health`.
/// The `content_type` field allows RFC 8407 `application/health+json`
/// for health endpoints while defaulting to `application/json`.
pub struct StaticJsonEndpoint {
    pub body: Vec<u8>,
    pub content_type: &'static str,
}

impl StaticJsonEndpoint {
    /// Create a new endpoint with the default `application/json` content type.
    pub fn new(body: Vec<u8>) -> Self {
        Self {
            body,
            content_type: "application/json",
        }
    }
}

#[async_trait::async_trait]
impl IEndpoint for StaticJsonEndpoint {
    async fn handle(&self, ctx: &mut dyn IHttpContext) -> Result<()> {
        ctx.response_mut().set_status(200);
        ctx.response_mut()
            .set_header("content-type", self.content_type);
        ctx.response_mut().write_bytes(self.body.clone()).await
    }
}

/// Endpoint that serves a static HTML payload.
///
/// Used for built-in endpoints like `/api/docs`.
pub struct StaticHtmlEndpoint {
    pub body: &'static str,
}

#[async_trait::async_trait]
impl IEndpoint for StaticHtmlEndpoint {
    async fn handle(&self, ctx: &mut dyn IHttpContext) -> Result<()> {
        ctx.response_mut().set_status(200);
        ctx.response_mut()
            .set_header("content-type", "text/html; charset=utf-8");
        ctx.response_mut()
            .write_bytes(self.body.as_bytes().to_vec())
            .await
    }
}