Skip to main content

arcly_http/web/
extract.rs

1//! Parameter-driven extraction for handler arguments.
2//!
3//! The route attribute macro inspects each function parameter and emits a
4//! call to one of the helpers below. Result: handlers receive their
5//! dependencies and request data as typed arguments — no `ctx.inject()`
6//! service-locator pattern, no manual `ctx.param(...).parse()`.
7
8use std::marker::PhantomData;
9use std::ops::Deref;
10use std::str::FromStr;
11
12use serde::de::DeserializeOwned;
13use validator::Validate;
14
15use crate::web::{Error, RequestContext};
16
17/// Constructor-style dependency injection wrapper. Resolved from the frozen
18/// DI container at request entry; zero allocation, zero lock.
19pub struct Inject<T: Send + Sync + 'static> {
20    inner: &'static T,
21    _marker: PhantomData<T>,
22}
23
24impl<T: Send + Sync + 'static> Inject<T> {
25    #[inline]
26    pub fn from_ctx(ctx: &RequestContext) -> Self {
27        Self {
28            inner: ctx.inject::<T>(),
29            _marker: PhantomData,
30        }
31    }
32    /// Construct from a pre-resolved `&'static T`. Used by `#[Injectable]` to
33    /// populate fields during topological provider construction.
34    #[doc(hidden)]
35    #[inline]
36    pub fn __from_static(inner: &'static T) -> Self {
37        Self {
38            inner,
39            _marker: PhantomData,
40        }
41    }
42    #[inline]
43    pub fn get(&self) -> &'static T {
44        self.inner
45    }
46}
47
48impl<T: Send + Sync + 'static> Deref for Inject<T> {
49    type Target = T;
50    #[inline]
51    fn deref(&self) -> &T {
52        self.inner
53    }
54}
55
56impl<T: Send + Sync + 'static> Clone for Inject<T> {
57    #[inline]
58    fn clone(&self) -> Self {
59        Self {
60            inner: self.inner,
61            _marker: PhantomData,
62        }
63    }
64}
65
66// ─── Path / Query / Body / Header helpers ────────────────────────────────
67//
68// These are free functions instead of `FromRequest`-style traits because
69// macro-generated code stays simpler and the type-inference story is sharper.
70
71#[inline]
72pub fn extract_param<T: FromStr>(ctx: &RequestContext, name: &'static str) -> Result<T, Error> {
73    let raw = ctx
74        .param(name)
75        .ok_or(Error::BadRequest("missing path parameter"))?;
76    raw.parse::<T>()
77        .map_err(|_| Error::BadRequest("invalid path parameter"))
78}
79
80#[inline]
81pub fn extract_query<T: DeserializeOwned>(ctx: &RequestContext) -> Result<T, Error> {
82    let raw = ctx.query_string().unwrap_or("");
83    serde_urlencoded::from_str::<T>(raw).map_err(|_| Error::BadRequest("invalid query string"))
84}
85
86#[inline]
87pub fn extract_body_json<T: DeserializeOwned>(ctx: &RequestContext) -> Result<T, Error> {
88    serde_json::from_slice::<T>(ctx.body()).map_err(|_| Error::BadRequest("invalid JSON body"))
89}
90
91#[inline]
92pub fn extract_header<'a>(ctx: &'a RequestContext, name: &'static str) -> Result<&'a str, Error> {
93    ctx.header(name)
94        .ok_or(Error::BadRequest("missing required header"))
95}
96
97// ─── Validated variants ──────────────────────────────────────────────────
98//
99// Deserialize, then run `validator::Validate::validate`. Any field-level
100// constraint failure surfaces as `Error::Validation(Vec<FieldError>)` →
101// RFC 7807 422 with full per-field detail.
102
103#[inline]
104pub fn extract_body_validated<T: DeserializeOwned + Validate>(
105    ctx: &RequestContext,
106) -> Result<T, Error> {
107    let v: T = extract_body_json(ctx)?;
108    v.validate().map_err(Error::from)?;
109    Ok(v)
110}
111
112#[inline]
113pub fn extract_query_validated<T: DeserializeOwned + Validate>(
114    ctx: &RequestContext,
115) -> Result<T, Error> {
116    let v: T = extract_query(ctx)?;
117    v.validate().map_err(Error::from)?;
118    Ok(v)
119}