arcly-http 0.1.0

Enterprise-grade NestJS-inspired web framework on axum: zero-lock DI, declarative controllers, multi-tenant data routing, transactional outbox, ABAC, and a self-documenting OpenAPI surface
Documentation
//! Boundary adapter: converts an axum `Request` into the opaque
//! `RequestContext` and dispatches to the macro-generated thunk.
//!
//! This is the *only* place in the framework where axum extraction primitives
//! are touched outside of the launch path. `assemble_context` is the single
//! request→context pipeline — plugin routes (`web::plugin_routes`) reuse it,
//! so body limits, trace propagation, and credential extraction can never
//! drift between entry points.

use axum::body::Body;
use axum::extract::{RawPathParams, Request, State};
use axum::http::request::Parts;
use axum::response::Response;
use axum::routing::{on, MethodFilter, MethodRouter};
use smallvec::SmallVec;
use smol_str::SmolStr;

use crate::auth::extract::extract_auth;
use crate::core::engine::{FrozenDiContainer, RouteDescriptor, RouteSpec};
use crate::observability::lean_telemetry::on_request_start;
use crate::observability::propagation::extract_trace_context;
use crate::web::context::RequestContext;

/// Maximum request body size — protects against memory exhaustion.
const MAX_BODY: usize = 8 * 1024 * 1024;

/// RAII guard that tracks the in-flight request count in the Prometheus gauge.
/// Pairs with `lean_telemetry::RequestGuard` which tracks the same count in the
/// raw atomic used by health endpoints.
pub(crate) struct InFlightGuard;
impl InFlightGuard {
    #[inline]
    pub(crate) fn new() -> Self {
        metrics::gauge!("http_requests_in_flight").increment(1.0);
        Self
    }
}
impl Drop for InFlightGuard {
    #[inline]
    fn drop(&mut self) {
        metrics::gauge!("http_requests_in_flight").decrement(1.0);
    }
}

/// The single request→`RequestContext` pipeline:
/// body read (capped) → W3C trace propagation → credential extraction
/// (Bearer / cookie / session) → context construction.
///
/// Every HTTP entry point — macro routes here, plugin routes in
/// `web::plugin_routes` — goes through this function.
pub(crate) async fn assemble_context(
    parts: Parts,
    body: Body,
    params: SmallVec<[(SmolStr, SmolStr); 4]>,
    container: &'static FrozenDiContainer,
    route_pattern: &'static str,
    route_spec: Option<&'static RouteSpec>,
) -> RequestContext {
    let bytes = axum::body::to_bytes(body, MAX_BODY)
        .await
        .unwrap_or_default();

    let trace = extract_trace_context(&parts.headers);
    let auth = extract_auth(&parts.headers, container).await;
    // Frozen-map lookup — no I/O, no locks. None when no registry is provided.
    let tenant = container
        .try_get::<crate::web::tenant::TenantRegistry>()
        .and_then(|tr| tr.resolve(&parts.headers));

    RequestContext::__new(
        parts.method,
        SmolStr::new(parts.uri.path()),
        SmolStr::new(parts.uri.query().unwrap_or("")),
        params,
        parts.headers,
        bytes,
        trace.trace_id,
        trace.span_id,
        trace.parent_span_id,
        container,
        route_pattern,
        route_spec,
    )
    .__with_claims(auth.claims)
    .__with_session(auth.session)
    .__with_tenant(tenant)
}

/// Wrap a macro-generated route descriptor in an axum `MethodRouter`.
/// The HTTP method filter is baked in so the caller can drop the result
/// straight into `Router::route`.
pub fn adapt(rt: &'static RouteDescriptor) -> MethodRouter<&'static FrozenDiContainer> {
    let filter = MethodFilter::try_from(axum::http::Method::from(rt.method))
        .expect("Arcly: unsupported HTTP method");

    let handler = move |State(container): State<&'static FrozenDiContainer>,
                        raw_params: RawPathParams,
                        req: Request| async move {
        let params: SmallVec<[(SmolStr, SmolStr); 4]> = raw_params
            .iter()
            .map(|(k, v)| (SmolStr::new(k), SmolStr::new(v)))
            .collect();

        let (parts, body) = req.into_parts();
        let ctx = assemble_context(parts, body, params, container, rt.path, Some(rt.spec)).await;

        let _guard = on_request_start();
        let _in_flight = InFlightGuard::new();
        let resp: Response = (rt.handler)(ctx).await;
        drop(_guard);

        let (mut p, b) = resp.into_parts();
        // RFC 8594: announce deprecation + sunset date on versioned routes
        // marked #[Deprecated(sunset = "…")].
        if !rt.spec.sunset.is_empty() {
            p.headers
                .insert("deprecation", axum::http::HeaderValue::from_static("true"));
            if let Ok(v) = axum::http::HeaderValue::from_str(rt.spec.sunset) {
                p.headers.insert("sunset", v);
            }
        }
        Response::from_parts(p, Body::new(b))
    };

    on(filter, handler)
}