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 reference to a registered value by its
8//!   type.
9//! - **Request context** is scoped to a single request and dropped when the request ends. Within a
10//!   request, [`request_context`] retrieves a reference to a registered value by its type.
11
12use std::any::{Any, type_name};
13
14use crate::context::Cx;
15
16/// Returns a reference to the app context value of type `T` registered on the
17/// router.
18///
19/// The lookup is keyed by `T`'s [`TypeId`](std::any::TypeId), so each type may have at most one
20/// registered value.
21///
22/// # Panics
23///
24/// Panics if no value of type `T` has been registered.
25///
26/// # Examples
27///
28/// ```rust
29/// # struct User;
30/// # impl Database {
31/// #     async fn fetch_user(&self, id: u64) -> User { User }
32/// # }
33/// use topcoat::context::{Cx, app_context};
34///
35/// struct Database {/* ... */}
36///
37/// async fn load_user(cx: &Cx, id: u64) -> User {
38///     let db: &Database = app_context(cx);
39///     db.fetch_user(id).await
40/// }
41/// ```
42pub fn app_context<T>(cx: &Cx) -> &T
43where
44    T: Any + Send + Sync,
45{
46    match cx.app_context.get::<T>() {
47        Some(value) => value,
48        None => panic!(
49            "attempted to access app context of type `{:?}`, but this type was not registered for this context",
50            type_name::<T>()
51        ),
52    }
53}
54
55/// Returns a reference to the request context value of type `T` registered on
56/// the current request's [`Cx`].
57///
58/// The lookup is keyed by `T`'s [`TypeId`](std::any::TypeId), so each type may have at most one
59/// registered value per request. Request context lives only for the duration of
60/// the request that owns it; once the request completes, every value is
61/// dropped.
62///
63/// # Panics
64///
65/// Panics if no value of type `T` has been registered on this request's `Cx`.
66///
67/// # Examples
68///
69/// ```rust
70/// use topcoat::context::{Cx, request_context};
71///
72/// struct RequestId(String);
73///
74/// async fn current_request_id(cx: &Cx) -> &str {
75///     let id: &RequestId = request_context(cx);
76///     &id.0
77/// }
78/// ```
79pub fn request_context<T>(cx: &Cx) -> &T
80where
81    T: Any + Send + Sync,
82{
83    match cx.request_context.get::<T>() {
84        Some(value) => value,
85        None => panic!(
86            "attempted to access request context of type `{:?}`, but this type was not registered for this context",
87            type_name::<T>()
88        ),
89    }
90}
91
92/// A type-keyed container of values.
93///
94/// Each registered value is stored under its [`TypeId`](std::any::TypeId), so a given type can
95/// only be registered once per `ContextMap`. Used by [`Cx`] to hold both the
96/// router-wide app context and the per-request request context; values are
97/// retrieved within a request via [`app_context`] or [`request_context`].
98#[derive(Default, Debug)]
99pub struct ContextMap {
100    entries: anymap3::Map<dyn Any + Send + Sync>,
101}
102
103impl ContextMap {
104    /// Creates an empty `ContextMap`.
105    #[must_use]
106    pub fn new() -> Self {
107        Self::default()
108    }
109
110    /// Registers `value` under its concrete type `T`, returning the value
111    /// previously registered for `T`, if any.
112    ///
113    /// A type can hold only one value at a time, so registering a type that is
114    /// already present replaces it and hands back the displaced value.
115    pub fn insert<T>(&mut self, value: T) -> Option<T>
116    where
117        T: Any + Send + Sync,
118    {
119        self.entries.insert::<T>(value)
120    }
121
122    /// Returns `true` if a value of type `T` has been registered.
123    #[must_use]
124    pub fn contains<T>(&self) -> bool
125    where
126        T: Any + Send + Sync,
127    {
128        self.entries.contains::<T>()
129    }
130
131    /// Returns a reference to the registered value of type `T`, or `None` if
132    /// no such value has been registered.
133    ///
134    /// Within a request, prefer the [`app_context`] and [`request_context`] free
135    /// functions over reaching for this directly.
136    #[must_use]
137    pub fn get<T>(&self) -> Option<&T>
138    where
139        T: Any + Send + Sync,
140    {
141        self.entries.get::<T>()
142    }
143
144    /// Returns a mutable reference to the registered value of type `T`, or
145    /// `None` if no such value has been registered.
146    #[must_use]
147    pub fn get_mut<T>(&mut self) -> Option<&mut T>
148    where
149        T: Any + Send + Sync,
150    {
151        self.entries.get_mut::<T>()
152    }
153}
154
155#[cfg(test)]
156mod tests {
157    use super::*;
158    use crate::context::CxTestBuilder;
159
160    #[derive(Debug, PartialEq)]
161    struct Database(&'static str);
162
163    #[derive(Debug, PartialEq)]
164    struct Config(u32);
165
166    #[test]
167    fn register_and_get_returns_value() {
168        let mut context = ContextMap::new();
169        context.insert(Database("primary"));
170
171        assert_eq!(context.get::<Database>(), Some(&Database("primary")));
172    }
173
174    #[test]
175    fn get_returns_none_for_unregistered_type() {
176        let context = ContextMap::new();
177        assert_eq!(context.get::<Database>(), None);
178    }
179
180    #[test]
181    fn multiple_types_coexist() {
182        let mut context = ContextMap::new();
183        context.insert(Database("primary"));
184        context.insert(Config(42));
185
186        assert_eq!(context.get::<Database>(), Some(&Database("primary")));
187        assert_eq!(context.get::<Config>(), Some(&Config(42)));
188    }
189
190    #[test]
191    fn insert_replaces_and_returns_the_displaced_value() {
192        let mut context = ContextMap::new();
193        assert_eq!(context.insert(Database("primary")), None);
194        assert_eq!(
195            context.insert(Database("replica")),
196            Some(Database("primary"))
197        );
198        assert_eq!(context.get::<Database>(), Some(&Database("replica")));
199    }
200
201    #[test]
202    fn contains_reports_registered_types() {
203        let mut context = ContextMap::new();
204        assert!(!context.contains::<Database>());
205        context.insert(Database("primary"));
206        assert!(context.contains::<Database>());
207        assert!(!context.contains::<Config>());
208    }
209
210    #[test]
211    fn get_mut_allows_mutation_in_place() {
212        let mut context = ContextMap::new();
213        context.insert(Config(1));
214        context.get_mut::<Config>().unwrap().0 = 42;
215        assert_eq!(context.get::<Config>(), Some(&Config(42)));
216        assert_eq!(context.get_mut::<Database>(), None);
217    }
218
219    #[test]
220    fn app_context_returns_registered_value() {
221        let cx = CxTestBuilder::new()
222            .app_context(Database("primary"))
223            .build();
224
225        let db: &Database = app_context(&cx);
226        assert_eq!(db, &Database("primary"));
227    }
228
229    #[test]
230    #[should_panic(expected = "attempted to access app context")]
231    fn app_context_panics_for_unregistered_type() {
232        let cx = Cx::default();
233        let _: &Database = app_context(&cx);
234    }
235
236    #[test]
237    fn request_context_returns_registered_value() {
238        let cx = CxTestBuilder::new()
239            .request_context(Database("primary"))
240            .build();
241
242        let db: &Database = request_context(&cx);
243        assert_eq!(db, &Database("primary"));
244    }
245
246    #[test]
247    #[should_panic(expected = "attempted to access request context")]
248    fn request_context_panics_for_unregistered_type() {
249        let cx = Cx::default();
250        let _: &Database = request_context(&cx);
251    }
252}