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