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}