Skip to main content

topcoat_core/
context.rs

1mod app_context;
2mod id;
3mod request_context;
4mod tracking;
5
6use std::{any::Any, panic::Location, sync::Arc};
7
8pub use app_context::*;
9pub use id::*;
10pub use request_context::*;
11pub(crate) use tracking::*;
12
13pub use crate::memoize::MemoizeAsRef;
14use crate::{
15    abort::AbortStore,
16    identity::{AmbiguousIdentityError, Identity, IdentityKey, SiteKey},
17    memoize::MemoizeCache,
18};
19
20/// The request context.
21///
22/// Add `cx: &Cx` to a handler or component to access context. Topcoat supplies
23/// it automatically. Read shared application values with [`app_context`] and
24/// values scoped to the request with [`request_context`].
25///
26/// Clones share the request's state. Use [`with`](Self::with) or
27/// [`with_many`](Self::with_many) to add context values in a child scope
28/// without changing the parent. Move an owned clone into work that outlives
29/// the handler, such as a streaming response.
30#[derive(Debug, Default, Clone)]
31pub struct Cx {
32    /// The state shared by handles in the same context scope.
33    state: Arc<CxState>,
34    /// The identity of this handle's scope.
35    identity: Identity,
36}
37
38impl Cx {
39    /// Creates the context for one request over the shared app context, with an
40    /// empty request context.
41    #[must_use]
42    pub fn new(app_context: Arc<AppContext>) -> Self {
43        Self::from_parts(app_context, RequestContext::new())
44    }
45
46    /// Creates a `Cx` from the given app and request contexts.
47    fn from_parts(app_context: Arc<AppContext>, request_context: RequestContext) -> Self {
48        Self {
49            state: Arc::new(CxState {
50                shared: Arc::new(RequestShared {
51                    id: CxId::new(),
52                    app_context,
53                    memoize_cache: MemoizeCache::new(),
54                    abort_store: AbortStore::new(),
55                }),
56                request_context: Arc::new(request_context),
57                tracker: None,
58            }),
59            identity: Identity::ROOT,
60        }
61    }
62
63    /// Returns this request's unique [`CxId`].
64    #[inline]
65    #[must_use]
66    pub fn id(&self) -> CxId {
67        self.state.shared.id
68    }
69
70    /// Returns a child context with a key derived from the parent's key,
71    /// this call's source location, and `key`.
72    ///
73    /// Use `()` to distinguish separate call locations, or an item key to
74    /// distinguish repeated calls at one location:
75    ///
76    /// ```
77    /// # use topcoat_core::context::Cx;
78    /// # let cx = Cx::default();
79    /// let first = cx.keyed(());
80    /// let second = cx.keyed(());
81    ///
82    /// for id in [1, 2, 3] {
83    ///     let child = cx.keyed(id);
84    /// }
85    /// ```
86    ///
87    /// The same inputs produce the same key. Moving the call in source
88    /// changes it. The child shares the parent's request state and context
89    /// values.
90    #[must_use]
91    #[track_caller]
92    pub fn keyed(&self, key: impl IdentityKey) -> Self {
93        let site = SiteKey::from_location(Location::caller());
94        Self {
95            identity: self.identity.keyed_child(site, key),
96            ..self.clone()
97        }
98    }
99
100    /// Returns the request context visible to this handle's scope.
101    #[inline]
102    pub(crate) fn request_context(&self) -> &RequestContext {
103        &self.state.request_context
104    }
105
106    /// Returns the tracker recording this handle's request context reads, if
107    /// one is installed.
108    #[inline]
109    pub(crate) fn tracker(&self) -> Option<&ContextTracker> {
110        self.state.tracker.as_deref()
111    }
112
113    /// Returns a child handle whose request context also holds `value`.
114    ///
115    /// The child inherits other context values and shares the request's state.
116    /// If this type is already registered, the child sees `value` while the
117    /// parent still sees the original.
118    #[must_use]
119    pub fn with<T>(&self, value: T) -> Cx
120    where
121        T: Any + Send + Sync,
122    {
123        let mut request_context = (*self.state.request_context).clone();
124        request_context.insert(value);
125        self.scope(request_context)
126    }
127
128    /// Returns a child handle whose request context also holds every value in
129    /// `values`, a tuple of context values or a [`RequestContext`].
130    ///
131    /// Behaves like chained [`with`](Self::with) calls, but builds the child's
132    /// request context in one step.
133    #[must_use]
134    pub fn with_many<V>(&self, values: V) -> Cx
135    where
136        V: ContextValues,
137    {
138        let mut request_context = (*self.state.request_context).clone();
139        values.install(&mut request_context);
140        self.scope(request_context)
141    }
142
143    /// Wraps a derived request context into a child handle sharing this
144    /// request's state.
145    fn scope(&self, request_context: RequestContext) -> Cx {
146        Cx {
147            state: Arc::new(CxState {
148                shared: Arc::clone(&self.state.shared),
149                request_context: Arc::new(request_context),
150                tracker: self.state.tracker.clone(),
151            }),
152            identity: self.identity,
153        }
154    }
155
156    /// Returns a child handle whose request context reads are recorded, along
157    /// with the tracker collecting them.
158    ///
159    /// The child shares this handle's scope, and that scope is also the
160    /// tracker's entry scope. A tracker inherited from an enclosing `track`
161    /// call is replaced, not stacked: reads made through the child and its
162    /// descendants are recorded by the new tracker only.
163    pub(crate) fn track(&self) -> (Cx, Arc<ContextTracker>) {
164        let tracker = Arc::new(ContextTracker::new(Arc::clone(&self.state.request_context)));
165        let child = Cx {
166            state: Arc::new(CxState {
167                shared: Arc::clone(&self.state.shared),
168                request_context: Arc::clone(&self.state.request_context),
169                tracker: Some(Arc::clone(&tracker)),
170            }),
171            identity: self.identity,
172        };
173        (child, tracker)
174    }
175}
176
177/// The bindings and tracker shared by handles in one context scope.
178#[derive(Debug, Default)]
179struct CxState {
180    shared: Arc<RequestShared>,
181    request_context: Arc<RequestContext>,
182    tracker: Option<Arc<ContextTracker>>,
183}
184
185/// The state shared by every handle to one request's [`Cx`].
186#[derive(Debug, Default)]
187struct RequestShared {
188    id: CxId,
189    app_context: Arc<AppContext>,
190    memoize_cache: MemoizeCache,
191    abort_store: AbortStore,
192}
193
194/// Builds a [`Cx`] with app and request context values for a test.
195///
196/// Use this when a test needs context without handling an HTTP request.
197#[derive(Debug, Default)]
198pub struct CxTestBuilder {
199    app_context: AppContext,
200    request_context: RequestContext,
201}
202
203impl CxTestBuilder {
204    /// Creates an empty builder.
205    #[must_use]
206    pub fn new() -> Self {
207        Self::default()
208    }
209
210    /// Registers `value` on the app context.
211    #[must_use]
212    pub fn app_context<T>(mut self, value: T) -> Self
213    where
214        T: Any + Send + Sync,
215    {
216        self.app_context.insert(value);
217        self
218    }
219
220    /// Registers `value` on the request context.
221    #[must_use]
222    pub fn request_context<T>(mut self, value: T) -> Self
223    where
224        T: Any + Send + Sync,
225    {
226        self.request_context.insert(value);
227        self
228    }
229
230    /// Consumes the builder, returning the assembled [`Cx`].
231    #[must_use]
232    pub fn build(self) -> Cx {
233        Cx::from_parts(Arc::new(self.app_context), self.request_context)
234    }
235}
236
237/// Returns the identity of this context's scope.
238///
239/// # Panics
240///
241/// Panics if `cx` belongs to a memoized call or an enclosing scope introduced
242/// ambiguity. Use [`try_identity`] when an identity may be ambiguous.
243#[must_use]
244#[track_caller]
245pub fn identity(cx: &Cx) -> Identity {
246    match try_identity(cx) {
247        Ok(identity) => identity,
248        Err(error) => panic!("{error}"),
249    }
250}
251
252/// Returns this context's identity, or the ambiguity inherited by it.
253///
254/// # Errors
255///
256/// Returns an error naming the scope that introduced ambiguity.
257///
258/// # Panics
259///
260/// Panics if `cx` belongs to a memoized call. To memoize a value that depends
261/// on identity, read the identity before the call and pass it as an argument.
262#[track_caller]
263pub fn try_identity(cx: &Cx) -> Result<Identity, AmbiguousIdentityError> {
264    assert!(
265        cx.state.tracker.is_none(),
266        "identity cannot be read inside memoized functions"
267    );
268    cx.identity.checked()
269}
270
271/// Reads the identity for derivation without checking ambiguity.
272#[doc(hidden)]
273#[must_use]
274pub fn identity_raw(cx: &Cx) -> Identity {
275    cx.identity
276}
277
278/// Sets the identity of an owned context.
279#[doc(hidden)]
280#[must_use]
281pub fn with_identity(cx: Cx, identity: Identity) -> Cx {
282    Cx { identity, ..cx }
283}
284
285#[inline]
286#[must_use]
287#[doc(hidden)]
288pub fn memoize_cache(cx: &Cx) -> &MemoizeCache {
289    &cx.state.shared.memoize_cache
290}
291
292#[inline]
293#[must_use]
294#[doc(hidden)]
295pub fn abort_store(cx: &Cx) -> &AbortStore {
296    &cx.state.shared.abort_store
297}
298
299#[cfg(test)]
300mod tests {
301    use super::*;
302
303    #[derive(Debug, PartialEq)]
304    struct Marker(u32);
305
306    #[derive(Debug, PartialEq)]
307    struct Other(&'static str);
308
309    #[test]
310    fn a_fresh_context_has_a_unique_id() {
311        let first = Cx::new(Arc::new(AppContext::new()));
312        let second = Cx::new(Arc::new(AppContext::new()));
313        assert_ne!(first.id(), second.id());
314    }
315
316    #[test]
317    fn keys_distinguish_locations_and_repetitions() {
318        fn child(cx: &Cx, key: u32) -> Cx {
319            cx.keyed(key)
320        }
321
322        let cx = Cx::default();
323        assert_ne!(identity(&cx.keyed(())), identity(&cx.keyed(())));
324        assert_eq!(
325            identity(&child(&cx, 1)),
326            identity(&child(&Cx::default(), 1))
327        );
328        assert_ne!(identity(&child(&cx, 1)), identity(&child(&cx, 2)));
329        assert_ne!(
330            identity(&child(&child(&cx, 1), 2)),
331            identity(&child(&cx, 2))
332        );
333        assert_eq!(identity(&cx), Identity::ROOT);
334    }
335
336    #[test]
337    fn keyed_contexts_share_request_state_and_preserve_their_scope() {
338        let cx = Cx::default().with(Marker(7));
339        let child = cx.keyed("child");
340        assert!(Arc::ptr_eq(&child.state, &cx.state));
341        assert_eq!(child.id(), cx.id());
342        assert!(std::ptr::eq(memoize_cache(&child), memoize_cache(&cx)));
343        assert!(std::ptr::eq(
344            request_context::<Marker>(&child),
345            request_context::<Marker>(&cx)
346        ));
347
348        let expected = identity(&child);
349        assert_eq!(identity(&child.clone()), expected);
350        assert_eq!(identity(&child.with(Other("value"))), expected);
351        assert_eq!(identity(&child.with_many((Other("value"),))), expected);
352        assert_eq!(identity_raw(&child.track().0), expected);
353    }
354
355    #[test]
356    #[should_panic(expected = "identity cannot be read inside memoized functions")]
357    fn memoized_functions_cannot_read_identity() {
358        let cx = Cx::default();
359        memoize_cache(&cx).memoize(&cx, (), (), |cx, ()| {
360            identity(&cx.keyed(()).with(Marker(7)))
361        });
362    }
363
364    #[test]
365    #[should_panic(expected = "identity cannot be read inside memoized functions")]
366    fn memoized_functions_cannot_try_identity() {
367        let cx = Cx::default();
368        memoize_cache(&cx).memoize(&cx, (), (), |cx, ()| try_identity(&cx));
369    }
370
371    #[tokio::test]
372    #[should_panic(expected = "identity cannot be read inside memoized functions")]
373    async fn memoized_functions_cannot_read_identity_after_suspension() {
374        let cx = Cx::default();
375        memoize_cache(&cx)
376            .memoize_async(&cx, (), (), |cx, ()| async move {
377                tokio::task::yield_now().await;
378                identity(&cx)
379            })
380            .await;
381    }
382
383    #[test]
384    fn with_registers_a_value_on_the_child() {
385        let cx = Cx::default();
386        let child = cx.with(Marker(1));
387
388        assert_eq!(try_request_context::<Marker>(&cx), None);
389        assert_eq!(request_context::<Marker>(&child), &Marker(1));
390    }
391
392    #[test]
393    fn with_shadows_without_touching_the_parent() {
394        let cx = Cx::default().with(Marker(1));
395        let child = cx.with(Marker(2));
396
397        assert_eq!(request_context::<Marker>(&cx), &Marker(1));
398        assert_eq!(request_context::<Marker>(&child), &Marker(2));
399    }
400
401    #[test]
402    fn a_child_inherits_the_parent_context() {
403        let cx = CxTestBuilder::new()
404            .app_context(Other("app"))
405            .request_context(Marker(7))
406            .build();
407        let child = cx.with(Other("request"));
408
409        assert_eq!(request_context::<Marker>(&child), &Marker(7));
410        assert_eq!(request_context::<Other>(&child), &Other("request"));
411        assert_eq!(app_context::<Other>(&child), &Other("app"));
412    }
413
414    #[test]
415    fn with_many_registers_every_value() {
416        let cx = Cx::default().with_many((Marker(1), Other("many")));
417
418        assert_eq!(request_context::<Marker>(&cx), &Marker(1));
419        assert_eq!(request_context::<Other>(&cx), &Other("many"));
420    }
421
422    #[test]
423    fn a_child_shares_the_request_state() {
424        let cx = Cx::default();
425        let child = cx.with(Marker(1));
426
427        assert_eq!(child.id(), cx.id());
428        assert!(std::ptr::eq(memoize_cache(&child), memoize_cache(&cx)));
429        assert!(std::ptr::eq(abort_store(&child), abort_store(&cx)));
430    }
431
432    #[test]
433    fn clones_outlive_the_original() {
434        let cx = CxTestBuilder::new().request_context(Marker(7)).build();
435        let id = cx.id();
436        let handle = cx.clone();
437        drop(cx);
438
439        assert_eq!(request_context::<Marker>(&handle).0, 7);
440        assert_eq!(handle.id(), id);
441    }
442
443    #[test]
444    fn handles_are_send_and_sync() {
445        fn assert_send_sync<T: Send + Sync>() {}
446        assert_send_sync::<Cx>();
447    }
448}