topcoat_core/context/context_map.rs
1//! Type-keyed values made available through the request context.
2//!
3//! [`ContextMap`] is a type-keyed map of values, looked up by their [`TypeId`](std::any::TypeId).
4//! Each [`Cx`] carries two of them:
5//!
6//! - **App context** is registered once at startup and shared across every request handled by the
7//! router. Within a request, [`app_context`] retrieves a required value and [`try_app_context`]
8//! retrieves an optional value by its type.
9//! - **Request context** is scoped to a single request and dropped when the request ends. Within a
10//! request, [`request_context`] retrieves a required value and [`try_request_context`] retrieves
11//! an optional value by its type.
12
13use std::any::{Any, type_name};
14
15use crate::context::Cx;
16
17/// Returns a reference to the app context value of type `T` registered on the
18/// router, or `None` if no such value has been registered.
19///
20/// The lookup is keyed by `T`'s [`TypeId`](std::any::TypeId), so each type may
21/// have at most one registered value.
22///
23/// # Examples
24///
25/// ```rust
26/// use topcoat::context::{Cx, try_app_context};
27///
28/// struct FeatureConfig;
29///
30/// fn feature_config(cx: &Cx) -> Option<&FeatureConfig> {
31/// try_app_context(cx)
32/// }
33/// ```
34#[must_use]
35pub fn try_app_context<T>(cx: &Cx) -> Option<&T>
36where
37 T: Any + Send + Sync,
38{
39 cx.app_context.get::<T>()
40}
41
42/// Returns a reference to the app context value of type `T` registered on the
43/// router.
44///
45/// The lookup is keyed by `T`'s [`TypeId`](std::any::TypeId), so each type may have at most one
46/// registered value.
47///
48/// # Panics
49///
50/// Panics if no value of type `T` has been registered.
51///
52/// # Examples
53///
54/// ```rust
55/// # struct User;
56/// # impl Database {
57/// # async fn fetch_user(&self, id: u64) -> User { User }
58/// # }
59/// use topcoat::context::{Cx, app_context};
60///
61/// struct Database {/* ... */}
62///
63/// async fn load_user(cx: &Cx, id: u64) -> User {
64/// let db: &Database = app_context(cx);
65/// db.fetch_user(id).await
66/// }
67/// ```
68pub fn app_context<T>(cx: &Cx) -> &T
69where
70 T: Any + Send + Sync,
71{
72 match try_app_context(cx) {
73 Some(value) => value,
74 None => panic!(
75 "attempted to access app context of type `{:?}`, but this type was not registered for this context",
76 type_name::<T>()
77 ),
78 }
79}
80
81/// Returns a reference to the request context value of type `T` registered on
82/// the current request's [`Cx`], or `None` if no such value has been registered.
83///
84/// The lookup is keyed by `T`'s [`TypeId`](std::any::TypeId), so each type may
85/// have at most one registered value per request. Request context lives only
86/// for the duration of the request that owns it; once the request completes,
87/// every value is dropped.
88///
89/// # Examples
90///
91/// ```rust
92/// use topcoat::context::{Cx, try_request_context};
93///
94/// struct Customer;
95///
96/// fn current_customer(cx: &Cx) -> Option<&Customer> {
97/// try_request_context(cx)
98/// }
99/// ```
100#[must_use]
101pub fn try_request_context<T>(cx: &Cx) -> Option<&T>
102where
103 T: Any + Send + Sync,
104{
105 cx.request_context.get::<T>()
106}
107
108/// Returns a reference to the request context value of type `T` registered on
109/// the current request's [`Cx`].
110///
111/// The lookup is keyed by `T`'s [`TypeId`](std::any::TypeId), so each type may have at most one
112/// registered value per request. Request context lives only for the duration of
113/// the request that owns it; once the request completes, every value is
114/// dropped.
115///
116/// # Panics
117///
118/// Panics if no value of type `T` has been registered on this request's `Cx`.
119///
120/// # Examples
121///
122/// ```rust
123/// use topcoat::context::{Cx, request_context};
124///
125/// struct RequestId(String);
126///
127/// async fn current_request_id(cx: &Cx) -> &str {
128/// let id: &RequestId = request_context(cx);
129/// &id.0
130/// }
131/// ```
132pub fn request_context<T>(cx: &Cx) -> &T
133where
134 T: Any + Send + Sync,
135{
136 match try_request_context(cx) {
137 Some(value) => value,
138 None => panic!(
139 "attempted to access request context of type `{:?}`, but this type was not registered for this context",
140 type_name::<T>()
141 ),
142 }
143}
144
145/// A type-keyed container of values.
146///
147/// Each registered value is stored under its [`TypeId`](std::any::TypeId), so a given type can
148/// only be registered once per `ContextMap`. Used by [`Cx`] to hold both the
149/// router-wide app context and the per-request request context; values are
150/// retrieved within a request via [`app_context`], [`try_app_context`],
151/// [`request_context`], or [`try_request_context`].
152#[derive(Default, Debug)]
153pub struct ContextMap {
154 entries: anymap3::Map<dyn Any + Send + Sync>,
155}
156
157impl ContextMap {
158 /// Creates an empty `ContextMap`.
159 #[must_use]
160 pub fn new() -> Self {
161 Self::default()
162 }
163
164 /// Registers `value` under its concrete type `T`, returning the value
165 /// previously registered for `T`, if any.
166 ///
167 /// A type can hold only one value at a time, so registering a type that is
168 /// already present replaces it and hands back the displaced value.
169 pub fn insert<T>(&mut self, value: T) -> Option<T>
170 where
171 T: Any + Send + Sync,
172 {
173 self.entries.insert::<T>(value)
174 }
175
176 /// Returns `true` if a value of type `T` has been registered.
177 #[must_use]
178 pub fn contains<T>(&self) -> bool
179 where
180 T: Any + Send + Sync,
181 {
182 self.entries.contains::<T>()
183 }
184
185 /// Returns a reference to the registered value of type `T`, or `None` if
186 /// no such value has been registered.
187 ///
188 /// Within a request, prefer the [`app_context`] and [`request_context`] free
189 /// functions over reaching for this directly.
190 #[must_use]
191 pub fn get<T>(&self) -> Option<&T>
192 where
193 T: Any + Send + Sync,
194 {
195 self.entries.get::<T>()
196 }
197
198 /// Returns a mutable reference to the registered value of type `T`, or
199 /// `None` if no such value has been registered.
200 #[must_use]
201 pub fn get_mut<T>(&mut self) -> Option<&mut T>
202 where
203 T: Any + Send + Sync,
204 {
205 self.entries.get_mut::<T>()
206 }
207}
208
209#[cfg(test)]
210mod tests {
211 use super::*;
212 use crate::context::CxTestBuilder;
213
214 #[derive(Debug, PartialEq)]
215 struct Database(&'static str);
216
217 #[derive(Debug, PartialEq)]
218 struct Config(u32);
219
220 #[test]
221 fn register_and_get_returns_value() {
222 let mut context = ContextMap::new();
223 context.insert(Database("primary"));
224
225 assert_eq!(context.get::<Database>(), Some(&Database("primary")));
226 }
227
228 #[test]
229 fn get_returns_none_for_unregistered_type() {
230 let context = ContextMap::new();
231 assert_eq!(context.get::<Database>(), None);
232 }
233
234 #[test]
235 fn multiple_types_coexist() {
236 let mut context = ContextMap::new();
237 context.insert(Database("primary"));
238 context.insert(Config(42));
239
240 assert_eq!(context.get::<Database>(), Some(&Database("primary")));
241 assert_eq!(context.get::<Config>(), Some(&Config(42)));
242 }
243
244 #[test]
245 fn insert_replaces_and_returns_the_displaced_value() {
246 let mut context = ContextMap::new();
247 assert_eq!(context.insert(Database("primary")), None);
248 assert_eq!(
249 context.insert(Database("replica")),
250 Some(Database("primary"))
251 );
252 assert_eq!(context.get::<Database>(), Some(&Database("replica")));
253 }
254
255 #[test]
256 fn contains_reports_registered_types() {
257 let mut context = ContextMap::new();
258 assert!(!context.contains::<Database>());
259 context.insert(Database("primary"));
260 assert!(context.contains::<Database>());
261 assert!(!context.contains::<Config>());
262 }
263
264 #[test]
265 fn get_mut_allows_mutation_in_place() {
266 let mut context = ContextMap::new();
267 context.insert(Config(1));
268 context.get_mut::<Config>().unwrap().0 = 42;
269 assert_eq!(context.get::<Config>(), Some(&Config(42)));
270 assert_eq!(context.get_mut::<Database>(), None);
271 }
272
273 #[test]
274 fn app_context_returns_registered_value() {
275 let cx = CxTestBuilder::new()
276 .app_context(Database("primary"))
277 .build();
278
279 let db: &Database = app_context(&cx);
280 assert_eq!(db, &Database("primary"));
281 }
282
283 #[test]
284 fn try_app_context_returns_registered_value() {
285 let cx = CxTestBuilder::new()
286 .app_context(Database("primary"))
287 .build();
288
289 assert_eq!(try_app_context::<Database>(&cx), Some(&Database("primary")));
290 }
291
292 #[test]
293 fn try_app_context_returns_none_for_unregistered_type() {
294 let cx = Cx::default();
295 assert_eq!(try_app_context::<Database>(&cx), None);
296 }
297
298 #[test]
299 #[should_panic(expected = "attempted to access app context")]
300 fn app_context_panics_for_unregistered_type() {
301 let cx = Cx::default();
302 let _: &Database = app_context(&cx);
303 }
304
305 #[test]
306 fn request_context_returns_registered_value() {
307 let cx = CxTestBuilder::new()
308 .request_context(Database("primary"))
309 .build();
310
311 let db: &Database = request_context(&cx);
312 assert_eq!(db, &Database("primary"));
313 }
314
315 #[test]
316 fn try_request_context_returns_registered_value() {
317 let cx = CxTestBuilder::new()
318 .request_context(Database("primary"))
319 .build();
320
321 assert_eq!(
322 try_request_context::<Database>(&cx),
323 Some(&Database("primary"))
324 );
325 }
326
327 #[test]
328 fn try_request_context_returns_none_for_unregistered_type() {
329 let cx = Cx::default();
330 assert_eq!(try_request_context::<Database>(&cx), None);
331 }
332
333 #[test]
334 #[should_panic(expected = "attempted to access request context")]
335 fn request_context_panics_for_unregistered_type() {
336 let cx = Cx::default();
337 let _: &Database = request_context(&cx);
338 }
339}