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}