arcature 2026.2.0

Arcature application framework: a high-level Application facade over the certified Arcature subsystems, with the low-level Axum/Tower escape hatch preserved.
Documentation
//! The error-mapping layer — applies an application-owned response
//! transformation to every response (A10).
//!
//! The error-mapping layer wraps the inner service and passes each
//! response through a type-erased `Fn(Response) -> Response`. The
//! application installs it via `Application::error_mapping(...)`. The
//! function typically checks the status code and reformats error
//! responses (e.g., converting all 5xx into RFC 9457 Problem Details).
//!
//! # Ordering
//!
//! The error-mapping layer is a post-routing global layer, applied
//! **inside** Inertia and maintenance (it runs after them on the request
//! path, so Inertia/maintenance short-circuits are already handled). It
//! wraps the handler + observe route-layers, so it sees the handler's
//! response and can reformat it before Inertia processes it on the
//! response path.
//!
//! # No information disclosure
//!
//! The `ErrorMapFn` implementation must not leak internal error details
//! to the client. The dogfood pattern logs the error to `eprintln!` and
//! returns a generic `500 INTERNAL_SERVER_ERROR`.

use std::sync::Arc;

use crate::axum::body::Body;
use crate::axum::extract::Request;
use crate::axum::response::Response;

/// The global error-mapping function: a synchronous function from
/// `Response` to `Response`. The pipeline applies it to every response
/// after the handler has run.
///
/// Stored as a newtype wrapper so Arcature can implement `Clone` without
/// orphan-rule issues (the inner `Arc<dyn Fn>` is a foreign type).
#[derive(Clone)]
pub struct ErrorMapFn(Arc<dyn Fn(Response) -> Response + Send + Sync + 'static>);

impl ErrorMapFn {
    /// Create a new error-mapping function from a closure or function.
    #[must_use]
    pub fn new<F>(f: F) -> Self
    where
        F: Fn(Response) -> Response + Send + Sync + 'static,
    {
        Self(Arc::new(f))
    }

    /// Apply the mapping to a response.
    #[must_use]
    pub fn map(&self, response: Response) -> Response {
        (self.0)(response)
    }
}

impl std::fmt::Debug for ErrorMapFn {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_tuple("ErrorMapFn").finish_non_exhaustive()
    }
}

/// A Tower layer that applies an error-mapping function to every response.
///
/// Created from an `Option<ErrorMapFn>` by [`ErrorMappingLayer::new`].
/// When `None`, the layer is a zero-overhead pass-through (the service is
/// returned unchanged).
#[derive(Clone)]
pub struct ErrorMappingLayer {
    map: Option<ErrorMapFn>,
}

impl ErrorMappingLayer {
    /// Create a new layer from an optional error-mapping function.
    #[must_use]
    pub fn new(map: Option<ErrorMapFn>) -> Self {
        Self { map }
    }
}

impl<S> tower::Layer<S> for ErrorMappingLayer {
    type Service = ErrorMappingService<S>;
    fn layer(&self, inner: S) -> Self::Service {
        ErrorMappingService {
            inner,
            map: self.map.clone(),
        }
    }
}

/// The service produced by [`ErrorMappingLayer`]. When no mapping
/// function is installed, it delegates directly to the inner service.
#[derive(Clone)]
pub struct ErrorMappingService<S> {
    inner: S,
    map: Option<ErrorMapFn>,
}

impl<S> tower::Service<Request<Body>> for ErrorMappingService<S>
where
    S: tower::Service<Request<Body>, Response = Response, Error = std::convert::Infallible>
        + Clone
        + Send
        + 'static,
    S::Future: Send + 'static,
{
    type Response = Response;
    type Error = std::convert::Infallible;
    type Future = std::pin::Pin<
        Box<dyn std::future::Future<Output = Result<Response, std::convert::Infallible>> + Send>,
    >;

    fn poll_ready(
        &mut self,
        cx: &mut std::task::Context<'_>,
    ) -> std::task::Poll<Result<(), Self::Error>> {
        self.inner.poll_ready(cx)
    }

    fn call(&mut self, req: Request<Body>) -> Self::Future {
        // Tower requires `&mut self` for `call`, but we need the inner
        // service to be movable into the async block. Clone and swap, as
        // Axum and Tower's ready-service helpers do.
        let clone = self.inner.clone();
        let mut inner = std::mem::replace(&mut self.inner, clone);
        let map = self.map.clone();

        Box::pin(async move {
            let response = inner.call(req).await.unwrap_or_else(|inf| match inf {});
            match map {
                Some(f) => Ok(f.map(response)),
                None => Ok(response),
            }
        })
    }
}