Skip to main content

nidus_http/
request.rs

1//! Request extractors and helpers.
2
3use std::{future::Future, ops::Deref, sync::Arc};
4
5use axum::{Json, extract::FromRequestParts, response::IntoResponse};
6use http::{StatusCode, request::Parts};
7use nidus_core::{Inject, NidusError, SharedRequestScope};
8use serde::Serialize;
9
10/// Axum extractor for a provider resolved from the active Nidus request scope.
11///
12/// Attach [`crate::middleware::request_scope_layer`] with the application
13/// [`nidus_core::Container`] before using this extractor. The requested type
14/// must be registered in the container, commonly with
15/// `Container::register_request` or `Container::register_request_scoped`.
16///
17/// Missing middleware rejects with `500 Internal Server Error` and
18/// `request_scope_unavailable`. A provider resolution failure also returns
19/// `500`, with `request_scope_resolution_failed`.
20///
21/// `RequestScoped<T>` dereferences to `T` for handler reads. Use
22/// [`Self::into_inner`] when you need the shared [`Arc<T>`], or
23/// [`Self::into_inject`] when passing the value to APIs that expect Nidus'
24/// [`Inject<T>`] wrapper.
25///
26/// ```ignore
27/// use std::sync::Arc;
28/// use axum::{Router, routing::get};
29/// use nidus_core::Container;
30/// use nidus_http::{RequestScoped, middleware::request_scope_layer};
31///
32/// struct CurrentTenant(String);
33///
34/// async fn handler(tenant: RequestScoped<CurrentTenant>) -> String {
35///     tenant.0.clone()
36/// }
37///
38/// let mut container = Container::new();
39/// container.register_request::<CurrentTenant, _>(|_container| {
40///     Ok(CurrentTenant("demo".to_owned()))
41/// })?;
42///
43/// let app = Router::new()
44///     .route("/tenant", get(handler))
45///     .layer(request_scope_layer(Arc::new(container)));
46/// # Ok::<(), nidus_core::NidusError>(())
47/// ```
48#[derive(Clone, Debug)]
49pub struct RequestScoped<T: Send + Sync + 'static>(Inject<T>);
50
51impl<T> RequestScoped<T>
52where
53    T: Send + Sync + 'static,
54{
55    /// Creates a request-scoped extractor value from an injected dependency.
56    ///
57    /// Most application code receives this from Axum extraction rather than
58    /// constructing it manually.
59    pub fn new(value: Inject<T>) -> Self {
60        Self(value)
61    }
62
63    /// Returns the underlying injected dependency wrapper.
64    ///
65    /// Use this when downstream Nidus APIs need the injection wrapper rather
66    /// than a borrowed `T` or shared [`Arc<T>`].
67    pub fn into_inject(self) -> Inject<T> {
68        self.0
69    }
70
71    /// Returns a cloned shared pointer to the resolved dependency.
72    ///
73    /// This is useful when spawning work that must own the provider beyond the
74    /// handler's borrow.
75    pub fn into_inner(self) -> Arc<T> {
76        self.0.into_inner()
77    }
78}
79
80impl<T> Deref for RequestScoped<T>
81where
82    T: Send + Sync + 'static,
83{
84    type Target = T;
85
86    fn deref(&self) -> &Self::Target {
87        &self.0
88    }
89}
90
91impl<S, T> FromRequestParts<S> for RequestScoped<T>
92where
93    S: Send + Sync,
94    T: Send + Sync + 'static,
95{
96    type Rejection = RequestScopeRejection;
97
98    fn from_request_parts(
99        parts: &mut Parts,
100        _state: &S,
101    ) -> impl Future<Output = Result<Self, Self::Rejection>> + Send {
102        let scope = parts.extensions.get::<SharedRequestScope>().cloned();
103        async move {
104            let scope = scope.ok_or(RequestScopeRejection::MissingScope)?;
105            scope
106                .inject::<T>()
107                .map(Self::new)
108                .map_err(RequestScopeRejection::ResolutionFailed)
109        }
110    }
111}
112
113/// Rejection returned when a request-scoped provider cannot be extracted.
114#[derive(Debug, thiserror::Error)]
115pub enum RequestScopeRejection {
116    /// The request did not contain a Nidus request scope.
117    #[error("request scope is not available; attach request_scope_layer to the router")]
118    MissingScope,
119    /// The request scope failed to resolve the requested provider.
120    #[error("request-scoped provider resolution failed: {0}")]
121    ResolutionFailed(#[source] NidusError),
122}
123
124impl IntoResponse for RequestScopeRejection {
125    fn into_response(self) -> axum::response::Response {
126        let (code, message) = match self {
127            Self::MissingScope => (
128                "request_scope_unavailable",
129                "request scope is not available; attach request_scope_layer to the router"
130                    .to_owned(),
131            ),
132            Self::ResolutionFailed(error) => (
133                "request_scope_resolution_failed",
134                format!("request-scoped provider resolution failed: {error}"),
135            ),
136        };
137        (
138            StatusCode::INTERNAL_SERVER_ERROR,
139            Json(ErrorBody {
140                error: ErrorDetails { code, message },
141            }),
142        )
143            .into_response()
144    }
145}
146
147#[derive(Debug, Serialize)]
148struct ErrorBody {
149    error: ErrorDetails,
150}
151
152#[derive(Debug, Serialize)]
153struct ErrorDetails {
154    code: &'static str,
155    message: String,
156}