Skip to main content

rustlavel_core/
context.rs

1//! The application context: configuration plus shared, typed state.
2//!
3//! This is what replaces Laravel's service container. Laravel resolves services
4//! by name at runtime; here a handler asks for a type and the compiler proves it
5//! was registered — `req.state::<Database>()` cannot typo its way into a
6//! runtime failure.
7
8use crate::config::Config;
9use std::any::{Any, TypeId};
10use std::collections::HashMap;
11use std::sync::Arc;
12
13/// Shared state handed to every request, cheap to clone.
14#[derive(Clone)]
15pub struct Context {
16    inner: Arc<Inner>,
17}
18
19struct Inner {
20    config: Config,
21    state: HashMap<TypeId, Box<dyn Any + Send + Sync>>,
22}
23
24impl Context {
25    pub fn builder() -> ContextBuilder {
26        ContextBuilder { config: Config::with_defaults(), state: HashMap::new() }
27    }
28
29    pub fn config(&self) -> &Config {
30        &self.inner.config
31    }
32
33    /// Fetch a registered service by type.
34    pub fn state<T: Send + Sync + 'static>(&self) -> Option<&T> {
35        self.inner.state.get(&TypeId::of::<T>()).and_then(|value| value.downcast_ref::<T>())
36    }
37
38    /// Fetch a registered service, panicking with an actionable message if the
39    /// application forgot to register it. Intended for framework packages that
40    /// cannot proceed without their own state.
41    pub fn expect_state<T: Send + Sync + 'static>(&self) -> &T {
42        self.state::<T>().unwrap_or_else(|| {
43            panic!(
44                "`{}` was never registered on the application. \
45                 Add `.state(...)` for it in main.rs, or enable the package that provides it.",
46                std::any::type_name::<T>()
47            )
48        })
49    }
50}
51
52impl Default for Context {
53    fn default() -> Self {
54        Context::builder().build()
55    }
56}
57
58pub struct ContextBuilder {
59    config: Config,
60    state: HashMap<TypeId, Box<dyn Any + Send + Sync>>,
61}
62
63impl ContextBuilder {
64    pub fn config(mut self, config: Config) -> Self {
65        self.config = config;
66        self
67    }
68
69    /// Register a service. One value per type; registering twice replaces.
70    pub fn state<T: Send + Sync + 'static>(mut self, value: T) -> Self {
71        self.state.insert(TypeId::of::<T>(), Box::new(value));
72        self
73    }
74
75    /// Whether something of this type has already been registered.
76    ///
77    /// For the case where the framework would otherwise supply a default and
78    /// quietly replace what the application built for itself — see how `App`
79    /// decides whether to construct a view engine.
80    pub fn has_state<T: Send + Sync + 'static>(&self) -> bool {
81        self.state.contains_key(&TypeId::of::<T>())
82    }
83
84    pub fn build(self) -> Context {
85        Context { inner: Arc::new(Inner { config: self.config, state: self.state }) }
86    }
87}
88
89#[cfg(test)]
90mod tests {
91    use super::*;
92
93    struct Database(&'static str);
94    struct Cache;
95
96    #[test]
97    fn resolves_registered_state_by_type() {
98        let context = Context::builder().state(Database("postgres")).build();
99
100        assert_eq!(context.state::<Database>().unwrap().0, "postgres");
101        assert!(context.state::<Cache>().is_none());
102    }
103
104    #[test]
105    fn carries_configuration() {
106        let config = Config::new();
107        config.set("app.name", "Rustlavel");
108        let context = Context::builder().config(config).build();
109
110        assert_eq!(context.config().string("app.name", ""), "Rustlavel");
111    }
112}