Skip to main content

anvil_core/
container.rs

1//! The service container. Two layers:
2//! - Typed fields on `Container` (pool, mailer, cache, queue) — primary mechanism.
3//! - Typemap for user-registered bindings.
4//!
5//! Also exposes a task-local context for facade-style access (`cache::get(...)`).
6
7use std::any::{Any, TypeId};
8use std::collections::HashMap;
9use std::sync::Arc;
10
11use parking_lot::RwLock;
12
13use crate::auth::AuthManager;
14use crate::cache::CacheStore;
15use crate::config::{AppConfig, DatabaseConfig, MailConfig, QueueConfig, SessionConfig};
16use crate::event::EventBus;
17use crate::mail::MailerHandle;
18use crate::queue::QueueHandle;
19use crate::storage::StorageManager;
20
21/// Backward-compat alias: the default Container pool is still `sqlx::PgPool`.
22/// For multi-driver access, use `c.driver_pool()` (returns `cast_core::Pool` enum).
23pub type Pool = sqlx::PgPool;
24pub use cast_core::ConnectionManager;
25
26#[derive(Clone)]
27pub struct Container {
28    inner: Arc<ContainerInner>,
29}
30
31pub struct ContainerInner {
32    pub app: AppConfig,
33    pub db: DatabaseConfig,
34    pub session_cfg: SessionConfig,
35    pub mail_cfg: MailConfig,
36    pub queue_cfg: QueueConfig,
37    pub connections: ConnectionManager,
38    /// Cached PgPool for back-compat `c.pool()`. `None` when the default
39    /// connection is MySQL or SQLite — in that case `c.pool()` panics, and
40    /// users should call `c.driver_pool()` / `c.connection(name)` instead.
41    pub default_pool: Option<Pool>,
42    pub cache: CacheStore,
43    pub mailer: MailerHandle,
44    pub queue: QueueHandle,
45    pub storage: StorageManager,
46    pub events: EventBus,
47    pub auth: AuthManager,
48    bindings: RwLock<HashMap<TypeId, Arc<dyn Any + Send + Sync>>>,
49}
50
51impl Container {
52    pub fn app(&self) -> &AppConfig {
53        &self.inner.app
54    }
55    /// The default connection's write pool. Returns `&sqlx::PgPool` for
56    /// backward compat — panics if the default connection isn't Postgres.
57    /// For multi-driver code, use `driver_pool()` instead.
58    pub fn pool(&self) -> &Pool {
59        self.inner.default_pool.as_ref().unwrap_or_else(|| {
60            panic!(
61                "c.pool() called but default connection is not Postgres ({:?}). \
62                 Use c.driver_pool() or c.connection(name) instead.",
63                self.driver()
64            )
65        })
66    }
67
68    /// Same as `pool()` but returns `Option<&PgPool>` instead of panicking.
69    pub fn try_pool(&self) -> Option<&Pool> {
70        self.inner.default_pool.as_ref()
71    }
72
73    /// The default connection's pool as the `cast::Pool` enum — Postgres / MySQL / SQLite.
74    /// Multi-driver code should use this and dispatch via `match` or `.as_postgres()`.
75    pub fn driver_pool(&self) -> cast_core::Pool {
76        self.inner.connections.default_pool()
77    }
78
79    /// Which driver the default connection is using.
80    pub fn driver(&self) -> cast_core::Driver {
81        self.inner.connections.default_driver()
82    }
83
84    /// Resolve a named connection. Returns `None` if not configured.
85    pub fn connection(&self, name: &str) -> Option<cast_core::Connection> {
86        self.inner.connections.get(name)
87    }
88    /// The connection manager itself, for advanced cases.
89    pub fn connections(&self) -> &ConnectionManager {
90        &self.inner.connections
91    }
92    pub fn cache(&self) -> &CacheStore {
93        &self.inner.cache
94    }
95    pub fn mailer(&self) -> &MailerHandle {
96        &self.inner.mailer
97    }
98    pub fn queue(&self) -> &QueueHandle {
99        &self.inner.queue
100    }
101    pub fn storage(&self) -> &StorageManager {
102        &self.inner.storage
103    }
104    pub fn events(&self) -> &EventBus {
105        &self.inner.events
106    }
107    pub fn auth(&self) -> &AuthManager {
108        &self.inner.auth
109    }
110
111    /// Resolve a user-registered binding by type. Returns `None` if not bound.
112    pub fn resolve<T: Send + Sync + 'static>(&self) -> Option<Arc<T>> {
113        let bindings = self.inner.bindings.read();
114        bindings
115            .get(&TypeId::of::<T>())
116            .and_then(|v| v.clone().downcast::<T>().ok())
117    }
118
119    /// Bind a value into the runtime typemap. Last-write-wins.
120    pub fn bind<T: Send + Sync + 'static>(&self, value: T) {
121        let mut bindings = self.inner.bindings.write();
122        bindings.insert(TypeId::of::<T>(), Arc::new(value));
123    }
124}
125
126pub struct ContainerBuilder {
127    pub app: AppConfig,
128    pub db: DatabaseConfig,
129    pub session_cfg: SessionConfig,
130    pub mail_cfg: MailConfig,
131    pub queue_cfg: QueueConfig,
132    pub connections: Option<ConnectionManager>,
133    pub cache: Option<CacheStore>,
134    pub mailer: Option<MailerHandle>,
135    pub queue: Option<QueueHandle>,
136    pub storage: Option<StorageManager>,
137    pub events: Option<EventBus>,
138    pub auth: Option<AuthManager>,
139}
140
141impl ContainerBuilder {
142    pub fn from_env() -> Self {
143        Self {
144            app: AppConfig::from_env(),
145            db: DatabaseConfig::from_env(),
146            session_cfg: SessionConfig::from_env(),
147            mail_cfg: MailConfig::from_env(),
148            queue_cfg: QueueConfig::from_env(),
149            connections: None,
150            cache: None,
151            mailer: None,
152            queue: None,
153            storage: None,
154            events: None,
155            auth: None,
156        }
157    }
158
159    /// Wrap a single Postgres pool as the default connection. Convenience for
160    /// single-DB apps using Postgres.
161    pub fn pool(mut self, pool: Pool) -> Self {
162        self.connections = Some(ConnectionManager::from_pool(cast_core::Pool::Postgres(
163            pool,
164        )));
165        self
166    }
167
168    /// Wrap a `cast::Pool` (any driver) as the default connection.
169    pub fn driver_pool(mut self, pool: cast_core::Pool) -> Self {
170        self.connections = Some(ConnectionManager::from_pool(pool));
171        self
172    }
173
174    /// Provide a fully-built `ConnectionManager` (multi-connection apps).
175    pub fn connections(mut self, manager: ConnectionManager) -> Self {
176        self.connections = Some(manager);
177        self
178    }
179
180    pub fn cache(mut self, c: CacheStore) -> Self {
181        self.cache = Some(c);
182        self
183    }
184    pub fn mailer(mut self, m: MailerHandle) -> Self {
185        self.mailer = Some(m);
186        self
187    }
188    pub fn queue(mut self, q: QueueHandle) -> Self {
189        self.queue = Some(q);
190        self
191    }
192    pub fn storage(mut self, s: StorageManager) -> Self {
193        self.storage = Some(s);
194        self
195    }
196    pub fn events(mut self, e: EventBus) -> Self {
197        self.events = Some(e);
198        self
199    }
200    pub fn auth(mut self, a: AuthManager) -> Self {
201        self.auth = Some(a);
202        self
203    }
204
205    pub fn build(self) -> Container {
206        let connections = self
207            .connections
208            .expect("ContainerBuilder requires a database connection — call .pool(pool) or .connections(manager)");
209        let default_driver_pool = connections.default_pool();
210        let pg_default = default_driver_pool.as_postgres().cloned();
211        let queue = self.queue.unwrap_or_else(|| match &pg_default {
212            Some(pg) => QueueHandle::in_memory(pg.clone()),
213            None => QueueHandle::in_memory_no_pool(),
214        });
215        if pg_default.is_none() {
216            tracing::debug!(
217                driver = ?default_driver_pool.driver(),
218                "default connection is non-Postgres — `c.pool()` will panic; use `c.driver_pool()` instead."
219            );
220        }
221        let default_pool = pg_default;
222        let inner = ContainerInner {
223            app: self.app,
224            db: self.db,
225            session_cfg: self.session_cfg,
226            mail_cfg: self.mail_cfg,
227            queue_cfg: self.queue_cfg,
228            cache: self.cache.unwrap_or_else(CacheStore::null),
229            mailer: self.mailer.unwrap_or_else(MailerHandle::null),
230            queue,
231            storage: self.storage.unwrap_or_else(StorageManager::local_default),
232            events: self.events.unwrap_or_default(),
233            auth: self.auth.unwrap_or_default(),
234            default_pool,
235            connections,
236            bindings: RwLock::new(HashMap::new()),
237        };
238        Container {
239            inner: Arc::new(inner),
240        }
241    }
242}
243
244/// Trait for types that can be resolved from a `Container` reference.
245pub trait FromContainer: Sized {
246    fn from_container(container: &Container) -> Self;
247}
248
249impl FromContainer for Container {
250    fn from_container(container: &Container) -> Self {
251        container.clone()
252    }
253}
254
255impl FromContainer for Pool {
256    fn from_container(container: &Container) -> Self {
257        container.pool().clone()
258    }
259}
260
261tokio::task_local! {
262    static CURRENT_CONTAINER: Container;
263}
264
265/// Run a future with a container installed in task-local context. Used by
266/// the per-request middleware so facade-style functions can find the container.
267pub async fn with_container<F, T>(container: Container, fut: F) -> T
268where
269    F: std::future::Future<Output = T>,
270{
271    CURRENT_CONTAINER.scope(container, fut).await
272}
273
274/// Access the current request's container from anywhere on the request task.
275/// Panics if called outside a `with_container` scope.
276pub fn current() -> Container {
277    CURRENT_CONTAINER
278        .try_with(|c| c.clone())
279        .expect("container not installed in current task — call inside with_container scope")
280}
281
282pub fn try_current() -> Option<Container> {
283    CURRENT_CONTAINER.try_with(|c| c.clone()).ok()
284}