Skip to main content

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}