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