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    pub fn build(self) -> Context {
76        Context { inner: Arc::new(Inner { config: self.config, state: self.state }) }
77    }
78}
79
80#[cfg(test)]
81mod tests {
82    use super::*;
83
84    struct Database(&'static str);
85    struct Cache;
86
87    #[test]
88    fn resolves_registered_state_by_type() {
89        let context = Context::builder().state(Database("postgres")).build();
90
91        assert_eq!(context.state::<Database>().unwrap().0, "postgres");
92        assert!(context.state::<Cache>().is_none());
93    }
94
95    #[test]
96    fn carries_configuration() {
97        let config = Config::new();
98        config.set("app.name", "Rustlavel");
99        let context = Context::builder().config(config).build();
100
101        assert_eq!(context.config().string("app.name", ""), "Rustlavel");
102    }
103}