1use std::any::{Any, TypeId};
37use std::cell::RefCell;
38use std::collections::HashMap;
39use std::future::Future;
40
41type Values = HashMap<TypeId, Box<dyn Any + Send + Sync>>;
42
43tokio::task_local! {
44 static CONTEXT: RefCell<Values>;
45}
46
47pub fn set<T: Clone + Send + Sync + 'static>(value: T) -> bool {
51 CONTEXT
52 .try_with(|values| {
53 values
54 .borrow_mut()
55 .insert(TypeId::of::<T>(), Box::new(value));
56 })
57 .is_ok()
58}
59
60pub fn get<T: Clone + Send + Sync + 'static>() -> Option<T> {
62 CONTEXT
63 .try_with(|values| {
64 values
65 .borrow()
66 .get(&TypeId::of::<T>())
67 .and_then(|value| value.downcast_ref::<T>())
68 .cloned()
69 })
70 .ok()
71 .flatten()
72}
73
74pub fn remove<T: Send + Sync + 'static>() {
76 let _ = CONTEXT.try_with(|values| values.borrow_mut().remove(&TypeId::of::<T>()));
77}
78
79pub async fn scope<F: Future>(fut: F) -> F::Output {
81 CONTEXT.scope(RefCell::new(Values::new()), fut).await
82}
83
84#[derive(Debug, Clone)]
101pub struct Current<T>(pub T);
102
103impl<T, S> axum::extract::FromRequestParts<S> for Current<T>
104where
105 T: Clone + Send + Sync + 'static,
106 S: Send + Sync,
107{
108 type Rejection = crate::Error;
109
110 async fn from_request_parts(
111 _: &mut axum::http::request::Parts,
112 _: &S,
113 ) -> Result<Self, crate::Error> {
114 get::<T>().map(Current).ok_or_else(|| {
115 anyhow::anyhow!(
116 "no `{}` in the request's context: set it in a middleware with renox::context::set",
117 std::any::type_name::<T>()
118 )
119 .into()
120 })
121 }
122}
123
124impl<T, S> axum::extract::OptionalFromRequestParts<S> for Current<T>
125where
126 T: Clone + Send + Sync + 'static,
127 S: Send + Sync,
128{
129 type Rejection = std::convert::Infallible;
130
131 async fn from_request_parts(
132 _: &mut axum::http::request::Parts,
133 _: &S,
134 ) -> Result<Option<Self>, std::convert::Infallible> {
135 Ok(get::<T>().map(Current))
136 }
137}
138
139pub fn app() -> Option<crate::AppState> {
143 get::<crate::AppState>()
144}
145
146pub(crate) async fn scope_app<F: Future>(state: crate::AppState, fut: F) -> F::Output {
148 scope(async move {
149 set(state);
150 fut.await
151 })
152 .await
153}
154
155pub(crate) async fn middleware(
157 axum::extract::State(state): axum::extract::State<crate::AppState>,
158 req: axum::extract::Request,
159 next: axum::middleware::Next,
160) -> axum::response::Response {
161 let info = RequestInfo {
162 method: req.method().to_string(),
163 path: req.uri().path().to_owned(),
164 id: req
165 .extensions()
166 .get::<crate::RequestId>()
167 .map(|id| id.0.clone())
168 .unwrap_or_default(),
169 ip: crate::ClientIp::of(&req).map(|ip| ip.to_string()),
170 };
171 scope_app(state, async move {
172 set(info);
173 next.run(req).await
174 })
175 .await
176}
177
178#[derive(Debug, Clone)]
180pub(crate) struct RequestInfo {
181 pub method: String,
182 pub path: String,
183 pub id: String,
184 pub ip: Option<String>,
185}
186
187#[cfg(test)]
188mod tests {
189 use super::*;
190
191 #[derive(Clone, Debug, PartialEq)]
192 struct Team(i64);
193
194 #[tokio::test]
195 async fn values_live_in_their_scope() {
196 assert!(!set(Team(1)), "no context outside a scope");
197 assert_eq!(get::<Team>(), None);
198 scope(async {
199 assert!(set(Team(1)));
200 set(Team(2));
201 assert_eq!(get::<Team>(), Some(Team(2)));
202 assert_eq!(get::<String>(), None);
203 scope(async { assert_eq!(get::<Team>(), None, "a new scope starts empty") }).await;
204 remove::<Team>();
205 assert_eq!(get::<Team>(), None);
206 })
207 .await;
208 }
209}