Skip to main content

arcly_http_core/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::ops::Deref;
9use std::str::FromStr;
10use std::sync::Arc;
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.
19///
20/// Holds an `Arc<T>` cloned from the container's singleton. Cloning the wrapper
21/// (or resolving one at request entry) is a single atomic refcount bump — no
22/// lock, no heap allocation — preserving the framework's zero-lock /
23/// zero-alloc request-path guarantee while keeping the type fully safe (no
24/// `'static` lifetime laundering).
25pub struct Inject<T: Send + Sync + 'static> {
26    inner: Arc<T>,
27}
28
29impl<T: Send + Sync + 'static> Inject<T> {
30    #[inline]
31    pub fn from_ctx(ctx: &RequestContext) -> Self {
32        Self {
33            inner: ctx.inject_arc::<T>(),
34        }
35    }
36    /// Construct from a pre-resolved `Arc<T>`. Used by `#[Injectable]` to
37    /// populate fields during topological provider construction.
38    #[doc(hidden)]
39    #[inline]
40    pub fn __from_arc(inner: Arc<T>) -> Self {
41        Self { inner }
42    }
43    /// Borrow the injected singleton. Tied to `self`'s lifetime; for an owned
44    /// handle that outlives the wrapper, clone via [`arc`](Self::arc).
45    #[inline]
46    pub fn get(&self) -> &T {
47        &self.inner
48    }
49    /// Clone the underlying `Arc<T>` (one atomic increment).
50    #[inline]
51    pub fn arc(&self) -> Arc<T> {
52        Arc::clone(&self.inner)
53    }
54}
55
56impl<T: Send + Sync + 'static> Deref for Inject<T> {
57    type Target = T;
58    #[inline]
59    fn deref(&self) -> &T {
60        &self.inner
61    }
62}
63
64impl<T: Send + Sync + 'static> Clone for Inject<T> {
65    #[inline]
66    fn clone(&self) -> Self {
67        Self {
68            inner: Arc::clone(&self.inner),
69        }
70    }
71}
72
73// ─── Path / Query / Body / Header helpers ────────────────────────────────
74//
75// These are free functions instead of `FromRequest`-style traits because
76// macro-generated code stays simpler and the type-inference story is sharper.
77
78#[inline]
79pub fn extract_param<T: FromStr>(ctx: &RequestContext, name: &'static str) -> Result<T, Error> {
80    let raw = ctx
81        .param(name)
82        .ok_or(Error::BadRequest("missing path parameter"))?;
83    raw.parse::<T>()
84        .map_err(|_| Error::BadRequest("invalid path parameter"))
85}
86
87#[inline]
88pub fn extract_query<T: DeserializeOwned>(ctx: &RequestContext) -> Result<T, Error> {
89    let raw = ctx.query_string().unwrap_or("");
90    serde_urlencoded::from_str::<T>(raw).map_err(|_| Error::BadRequest("invalid query string"))
91}
92
93#[inline]
94pub fn extract_body_json<T: DeserializeOwned>(ctx: &RequestContext) -> Result<T, Error> {
95    serde_json::from_slice::<T>(ctx.body()).map_err(|_| Error::BadRequest("invalid JSON body"))
96}
97
98#[inline]
99pub fn extract_header<'a>(ctx: &'a RequestContext, name: &'static str) -> Result<&'a str, Error> {
100    ctx.header(name)
101        .ok_or(Error::BadRequest("missing required header"))
102}
103
104// ─── Validated variants ──────────────────────────────────────────────────
105//
106// Deserialize, then run `validator::Validate::validate`. Any field-level
107// constraint failure surfaces as `Error::Validation(Vec<FieldError>)` →
108// RFC 7807 422 with full per-field detail.
109
110#[inline]
111pub fn extract_body_validated<T: DeserializeOwned + Validate>(
112    ctx: &RequestContext,
113) -> Result<T, Error> {
114    let v: T = extract_body_json(ctx)?;
115    v.validate().map_err(Error::from)?;
116    Ok(v)
117}
118
119#[inline]
120pub fn extract_query_validated<T: DeserializeOwned + Validate>(
121    ctx: &RequestContext,
122) -> Result<T, Error> {
123    let v: T = extract_query(ctx)?;
124    v.validate().map_err(Error::from)?;
125    Ok(v)
126}