Skip to main content

dbrest_core/app/
state.rs

1//! Application state shared across all handlers.
2//!
3//! [`AppState`] is the central struct holding the database backend,
4//! configuration, schema cache, authentication state, metrics, and
5//! database version info. It is cheaply cloneable (all fields are
6//! `Arc`-wrapped) and passed to every axum handler via `State<AppState>`.
7
8use std::sync::Arc;
9
10use arc_swap::ArcSwap;
11
12use crate::auth::JwtCache;
13use crate::auth::middleware::AuthState;
14use crate::backend::{DatabaseBackend, DbVersion, SqlDialect};
15use crate::config::AppConfig;
16use crate::error::Error;
17use crate::schema_cache::SchemaCache;
18
19// Keep PgVersion as a compatibility alias during migration
20/// PostgreSQL server version.
21///
22/// Deprecated — use [`DbVersion`] from the backend module instead.
23#[derive(Debug, Clone, PartialEq, Eq)]
24pub struct PgVersion {
25    pub major: u32,
26    pub minor: u32,
27    pub patch: u32,
28}
29
30impl std::fmt::Display for PgVersion {
31    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
32        write!(f, "{}.{}.{}", self.major, self.minor, self.patch)
33    }
34}
35
36impl From<&DbVersion> for PgVersion {
37    fn from(v: &DbVersion) -> Self {
38        Self {
39            major: v.major,
40            minor: v.minor,
41            patch: v.patch,
42        }
43    }
44}
45
46/// Central application state.
47///
48/// Constructed once at startup and shared across all handlers.
49/// The `config` and `schema_cache` fields use `ArcSwap` for
50/// lock-free reads and atomic replacement during live reload.
51#[derive(Clone)]
52pub struct AppState {
53    /// Database backend (connection pool + execution + introspection).
54    pub db: Arc<dyn DatabaseBackend>,
55    /// SQL dialect for the active backend.
56    pub dialect: Arc<dyn SqlDialect>,
57    /// Atomically swappable configuration.
58    pub config: Arc<ArcSwap<AppConfig>>,
59    /// Atomically swappable schema cache.
60    pub schema_cache: Arc<ArcSwap<Option<SchemaCache>>>,
61    /// Authentication state (JWT cache + config reference).
62    pub auth: AuthState,
63    /// JWT validation cache (shared with AuthState).
64    pub jwt_cache: JwtCache,
65    /// Database version.
66    pub db_version: DbVersion,
67    /// PostgreSQL version (legacy — prefer `db_version` field).
68    pub pg_version: PgVersion,
69}
70
71impl AppState {
72    /// Create a new `AppState` from a database backend, dialect, config, and version.
73    ///
74    /// The schema cache starts as `None` — call [`Self::reload_schema_cache`]
75    /// after construction.
76    pub fn new_with_backend(
77        db: Arc<dyn DatabaseBackend>,
78        dialect: Arc<dyn SqlDialect>,
79        config: AppConfig,
80        db_version: DbVersion,
81    ) -> Self {
82        let pg_version = PgVersion::from(&db_version);
83        let config_swap = Arc::new(ArcSwap::new(Arc::new(config)));
84        let auth = AuthState::with_shared_config(config_swap.clone());
85        let jwt_cache = auth.cache.clone();
86        Self {
87            db,
88            dialect,
89            config: config_swap,
90            schema_cache: Arc::new(ArcSwap::new(Arc::new(None))),
91            auth,
92            jwt_cache,
93            db_version,
94            pg_version,
95        }
96    }
97
98    /// Get the current config snapshot.
99    pub fn config(&self) -> arc_swap::Guard<Arc<AppConfig>> {
100        self.config.load()
101    }
102
103    /// Get the current schema cache (may be `None` if not loaded).
104    pub fn schema_cache_guard(&self) -> arc_swap::Guard<Arc<Option<SchemaCache>>> {
105        self.schema_cache.load()
106    }
107
108    /// Reload configuration from file and environment.
109    #[tracing::instrument(name = "reload_config", skip(self))]
110    pub async fn reload_config(&self) -> Result<(), Error> {
111        let current = self.config.load();
112        let file_path = current.config_file_path.clone();
113        let new_config =
114            crate::config::load_config(file_path.as_deref(), std::collections::HashMap::new())
115                .await
116                .map_err(|e| Error::InvalidConfig {
117                    message: e.to_string(),
118                })?;
119
120        self.config.store(Arc::new(new_config));
121
122        tracing::info!("Configuration reloaded successfully");
123        Ok(())
124    }
125
126    /// Load or reload the schema cache from the database.
127    #[tracing::instrument(name = "reload_schema_cache", skip(self))]
128    pub async fn reload_schema_cache(&self) -> Result<(), Error> {
129        let config = self.config.load();
130        let introspector = self.db.introspector();
131        let cache = SchemaCache::load(&*introspector, &config).await?;
132
133        metrics::counter!("schema_cache.reload.total").increment(1);
134        self.schema_cache.store(Arc::new(Some(cache)));
135
136        tracing::info!("Schema cache reloaded successfully");
137        Ok(())
138    }
139}