rust-webx-host 1.0.0

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

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

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 live HTTP context, calls the handler
/// via the HandlerCache call bridge, and writes the response.
/// Falls back to a descriptive stub message when no dispatch function is registered.
pub struct StubEndpoint {
    pub method: &'static str,
    pub path: &'static str,
    pub handler_type: &'static str,
    pub dispatch_fn: Option<webx_core::route::scan::RouteDispatchFn>,
    /// Rules collected from `#[authorize]`; empty means the endpoint is public.
    /// Every rule must be satisfied.
    pub authorization: &'static [webx_core::route::scan::AuthorizeRule],
    /// Dynamic authorizers (from `IDynamicAuthorizer` DI registrations).
    /// Checked after the declarative rules 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 {
            // ── Declarative authorization from `#[authorize]` ──
            if !self.authorization.is_empty() {
                let denied = match ctx.claims() {
                    None => {
                        // Single 401 write path: bubble the error up to the host
                        // so every unauthenticated response (declarative
                        // `#[authorize]`, binding layer, middleware) is written
                        // by one place — including the `WWW-Authenticate`
                        // challenge header.
                        return Err(webx_core::error::Error::Unauthorized(
                            "authentication required".to_string(),
                        ));
                    }
                    Some(claims) => self
                        .authorization
                        .iter()
                        .find(|rule| !rule.matches(claims.roles(), claims.permissions()))
                        .map(|rule| (rule.roles, rule.permissions)),
                };

                if let Some((roles, permissions)) = denied {
                    let mut ext = HashMap::new();
                    ext.insert("required_roles".into(), serde_json::json!(roles));
                    ext.insert(
                        "required_permissions".into(),
                        serde_json::json!(permissions),
                    );
                    write_forbidden(ctx, "Forbidden: insufficient permissions", ext).await;
                    return Ok(());
                }

                // ── Dynamic authorization via IDynamicAuthorizer ──
                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(&mut *ctx).await?;

            ctx.response_mut().set_status(response.status);
            if !response.content_type.is_empty() {
                ctx.response_mut()
                    .set_header("content-type", &response.content_type);
            }
            for (key, value) in &response.headers {
                ctx.response_mut().set_header(key, value);
            }
            ctx.response_mut().write_body(response.body).await?;
            return Ok(());
        }

        // Missing dispatch — route registered but handler bridge not linked.
        write_problem(
            ctx,
            501,
            &format!(
                "Route handler not implemented: {} {} (request type: {})",
                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
    }
}