Skip to main content

webx_core/
dispatch_runtime.rs

1//! Host-level DI handles: the service provider and the handler registry.
2//!
3//! Since 1.0 rust-webx passes DI explicitly instead of relying on a task-local
4//! ambient runtime:
5//!
6//! - [`Mediator`](crate::mediator::Mediator) holds the provider and the handler
7//!   registry it was built with;
8//! - [`IHttpContext`](crate::http::IHttpContext) exposes the
9//!   [`DispatchRuntime`] that dispatched the request, so endpoint adapters and
10//!   middleware can open scopes (`provider.scope()`) explicitly;
11//! - background work injects rust-dix's built-in `Arc<dyn ScopeFactory>` and
12//!   calls `ScopeFactory::create_scope()` itself.
13//!
14//! `Host` stores one [`DispatchRuntime`] per instance. There is no process-wide
15//! or task-local fallback: `rust-dix` owns scope lifetime, resolution and
16//! disposal, and rust-webx only keeps the per-host handle.
17
18use std::sync::Arc;
19
20use crate::route::scan::HandlerCache;
21
22/// Provider + handler registry for one host instance.
23///
24/// This is a plain handle: obtaining the provider or registry requires holding
25/// the runtime (or the `Host` that owns it), never an ambient task-local.
26#[derive(Clone)]
27pub struct DispatchRuntime {
28    provider: Arc<rust_dix::ServiceProvider>,
29    handler_cache: Arc<HandlerCache>,
30}
31
32impl DispatchRuntime {
33    pub fn new(provider: Arc<rust_dix::ServiceProvider>, handler_cache: Arc<HandlerCache>) -> Self {
34        Self {
35            provider,
36            handler_cache,
37        }
38    }
39
40    /// The host's root service provider (rust-dix).
41    pub fn provider(&self) -> &Arc<rust_dix::ServiceProvider> {
42        &self.provider
43    }
44
45    /// The compiled handler registry of this host.
46    pub fn handler_cache(&self) -> &Arc<HandlerCache> {
47        &self.handler_cache
48    }
49
50    /// Open a fresh DI scope on this host's provider.
51    ///
52    /// Equivalent to `self.provider().scope()`; exposed for call sites that
53    /// already hold the runtime (HTTP dispatch, middleware, hosted services).
54    pub fn scope(&self) -> rust_dix::Scope {
55        self.provider.scope()
56    }
57}
58
59#[cfg(test)]
60mod tests {
61    use super::*;
62    use rust_dix::ServiceCollection;
63    use std::sync::atomic::{AtomicUsize, Ordering};
64
65    #[derive(Debug)]
66    struct ScopedThing(usize);
67
68    fn runtime_with_scoped_counter() -> (Arc<DispatchRuntime>, Arc<AtomicUsize>) {
69        let created = Arc::new(AtomicUsize::new(0));
70        let counter = Arc::clone(&created);
71        let provider = ServiceCollection::new()
72            .scoped::<ScopedThing>(move |_| {
73                let n = counter.fetch_add(1, Ordering::SeqCst);
74                Arc::new(ScopedThing(n))
75            })
76            .build()
77            .expect("provider");
78        (
79            Arc::new(DispatchRuntime::new(
80                provider,
81                Arc::new(HandlerCache::build()),
82            )),
83            created,
84        )
85    }
86
87    /// rust-dix owns strict scope validation: the runtime's handle resolves
88    /// Scoped services through an explicit scope, and root resolution fails.
89    #[tokio::test]
90    async fn runtime_scope_is_the_explicit_di_entry_point() {
91        let (runtime, created) = runtime_with_scoped_counter();
92
93        let scope = runtime.scope();
94        let first = scope.get::<ScopedThing>().expect("scoped in scope");
95        let second = scope.get::<ScopedThing>().expect("scoped in scope");
96        assert!(
97            Arc::ptr_eq(&first, &second),
98            "Scoped services are cached per scope"
99        );
100        assert_eq!(first.0, 0, "the scope created the service once");
101        assert_eq!(created.load(Ordering::SeqCst), 1);
102
103        let error = runtime
104            .provider()
105            .get::<ScopedThing>()
106            .expect_err("strict scope validation rejects Scoped services from the root");
107        assert!(error.to_string().contains("root provider"), "{error}");
108    }
109}