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}