arcly-http 0.2.2

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
//! Parameter-driven extraction for handler arguments.
//!
//! The route attribute macro inspects each function parameter and emits a
//! call to one of the helpers below. Result: handlers receive their
//! dependencies and request data as typed arguments — no `ctx.inject()`
//! service-locator pattern, no manual `ctx.param(...).parse()`.

use std::marker::PhantomData;
use std::ops::Deref;
use std::str::FromStr;

use serde::de::DeserializeOwned;
use validator::Validate;

use crate::web::{Error, RequestContext};

/// Constructor-style dependency injection wrapper. Resolved from the frozen
/// DI container at request entry; zero allocation, zero lock.
pub struct Inject<T: Send + Sync + 'static> {
    inner: &'static T,
    _marker: PhantomData<T>,
}

impl<T: Send + Sync + 'static> Inject<T> {
    #[inline]
    pub fn from_ctx(ctx: &RequestContext) -> Self {
        Self {
            inner: ctx.inject::<T>(),
            _marker: PhantomData,
        }
    }
    /// Construct from a pre-resolved `&'static T`. Used by `#[Injectable]` to
    /// populate fields during topological provider construction.
    #[doc(hidden)]
    #[inline]
    pub fn __from_static(inner: &'static T) -> Self {
        Self {
            inner,
            _marker: PhantomData,
        }
    }
    #[inline]
    pub fn get(&self) -> &'static T {
        self.inner
    }
}

impl<T: Send + Sync + 'static> Deref for Inject<T> {
    type Target = T;
    #[inline]
    fn deref(&self) -> &T {
        self.inner
    }
}

impl<T: Send + Sync + 'static> Clone for Inject<T> {
    #[inline]
    fn clone(&self) -> Self {
        Self {
            inner: self.inner,
            _marker: PhantomData,
        }
    }
}

// ─── Path / Query / Body / Header helpers ────────────────────────────────
//
// These are free functions instead of `FromRequest`-style traits because
// macro-generated code stays simpler and the type-inference story is sharper.

#[inline]
pub fn extract_param<T: FromStr>(ctx: &RequestContext, name: &'static str) -> Result<T, Error> {
    let raw = ctx
        .param(name)
        .ok_or(Error::BadRequest("missing path parameter"))?;
    raw.parse::<T>()
        .map_err(|_| Error::BadRequest("invalid path parameter"))
}

#[inline]
pub fn extract_query<T: DeserializeOwned>(ctx: &RequestContext) -> Result<T, Error> {
    let raw = ctx.query_string().unwrap_or("");
    serde_urlencoded::from_str::<T>(raw).map_err(|_| Error::BadRequest("invalid query string"))
}

#[inline]
pub fn extract_body_json<T: DeserializeOwned>(ctx: &RequestContext) -> Result<T, Error> {
    serde_json::from_slice::<T>(ctx.body()).map_err(|_| Error::BadRequest("invalid JSON body"))
}

#[inline]
pub fn extract_header<'a>(ctx: &'a RequestContext, name: &'static str) -> Result<&'a str, Error> {
    ctx.header(name)
        .ok_or(Error::BadRequest("missing required header"))
}

// ─── Validated variants ──────────────────────────────────────────────────
//
// Deserialize, then run `validator::Validate::validate`. Any field-level
// constraint failure surfaces as `Error::Validation(Vec<FieldError>)` →
// RFC 7807 422 with full per-field detail.

#[inline]
pub fn extract_body_validated<T: DeserializeOwned + Validate>(
    ctx: &RequestContext,
) -> Result<T, Error> {
    let v: T = extract_body_json(ctx)?;
    v.validate().map_err(Error::from)?;
    Ok(v)
}

#[inline]
pub fn extract_query_validated<T: DeserializeOwned + Validate>(
    ctx: &RequestContext,
) -> Result<T, Error> {
    let v: T = extract_query(ctx)?;
    v.validate().map_err(Error::from)?;
    Ok(v)
}