Skip to main content

arc_web/helpers/
config.rs

1use std::env;
2
3/// Default database file path used when DATABASE_URL is not set
4pub const DEFAULT_DATABASE_URL: &str = "database/database.sqlite";
5
6/// Default SQLite file backing the JWT session-revocation registry when the
7/// primary driver is not SQLite.
8pub const DEFAULT_SESSION_DATABASE_URL: &str = "database/sessions.sqlite";
9
10/// Default database connection pool size
11pub const DEFAULT_POOL_LIMIT: u32 = 10;
12
13/// Default number of new aggregate events between best-effort user snapshots.
14pub const DEFAULT_USER_SNAPSHOT_INTERVAL_EVENTS: i64 = 50;
15
16/// Storage backend selected at startup behind the `EventStore` /
17/// `ReadModelStore` traits. Parsing is always available; constructing the
18/// Postgres stores additionally requires the `postgres` cargo feature.
19#[derive(Clone, Copy, Debug, PartialEq, Eq)]
20pub enum DatabaseDriver {
21    Sqlite,
22    Postgres,
23}
24
25impl DatabaseDriver {
26    /// Read `DATABASE_DRIVER` from the environment, defaulting to SQLite.
27    pub fn from_env() -> Self {
28        Self::parse(&env::var("DATABASE_DRIVER").unwrap_or_else(|_| "sqlite".to_string()))
29    }
30
31    /// Parse a driver name. Anything other than a recognized Postgres alias
32    /// falls back to SQLite, matching the historical default.
33    pub fn parse(value: &str) -> Self {
34        match value.trim().to_ascii_lowercase().as_str() {
35            "postgres" | "postgresql" | "pg" => DatabaseDriver::Postgres,
36            _ => DatabaseDriver::Sqlite,
37        }
38    }
39
40    pub fn as_str(self) -> &'static str {
41        match self {
42            DatabaseDriver::Sqlite => "sqlite",
43            DatabaseDriver::Postgres => "postgres",
44        }
45    }
46
47    /// Whether `DATABASE_URL` names a local filesystem path the startup health
48    /// check can stat. Postgres uses a connection string, not a file.
49    pub fn is_file_backed(self) -> bool {
50        matches!(self, DatabaseDriver::Sqlite)
51    }
52}
53
54/// Get the database URL from environment or use default
55pub fn database_url() -> String {
56    env::var("DATABASE_URL").unwrap_or_else(|_| DEFAULT_DATABASE_URL.to_string())
57}
58
59/// SQLite URL backing the JWT session-revocation registry. The registry is
60/// SQLite-only today; under a non-SQLite primary driver it keeps its own local
61/// SQLite database (overridable via `SESSION_DATABASE_URL`) so session
62/// revocation still functions.
63pub fn session_store_url(driver: DatabaseDriver) -> String {
64    match driver {
65        DatabaseDriver::Sqlite => database_url(),
66        DatabaseDriver::Postgres => env::var("SESSION_DATABASE_URL")
67            .unwrap_or_else(|_| DEFAULT_SESSION_DATABASE_URL.to_string()),
68    }
69}
70
71/// Optional HMAC key enabling HIPAA-5 event integrity signing/enforcement.
72/// The storage layer validates length and rejects keys shorter than 32 bytes.
73pub fn event_integrity_key() -> Option<Vec<u8>> {
74    env::var("EVENT_INTEGRITY_KEY")
75        .ok()
76        .filter(|value| !value.trim().is_empty())
77        .map(|value| value.into_bytes())
78}
79
80/// Identifier stored with each event signature so future key rotation can
81/// distinguish which key signed a row.
82pub fn event_integrity_key_id() -> String {
83    env::var("EVENT_INTEGRITY_KEY_ID").unwrap_or_else(|_| "default".to_string())
84}
85
86/// Number of new events between best-effort aggregate snapshots.
87/// Values <= 0 disable user snapshots without changing command correctness.
88pub fn user_snapshot_interval_events() -> Option<i64> {
89    let interval = env::var("USER_SNAPSHOT_INTERVAL_EVENTS")
90        .ok()
91        .filter(|value| !value.trim().is_empty())
92        .map(|value| {
93            value
94                .parse()
95                .expect("USER_SNAPSHOT_INTERVAL_EVENTS must be a number")
96        })
97        .unwrap_or(DEFAULT_USER_SNAPSHOT_INTERVAL_EVENTS);
98
99    (interval > 0).then_some(interval)
100}
101
102/// Get the database pool limit from environment or use default
103pub fn database_pool_limit() -> u32 {
104    env::var("DATABASE_POOL_LIMIT")
105        .unwrap_or_else(|_| DEFAULT_POOL_LIMIT.to_string())
106        .parse()
107        .expect("DATABASE_POOL_LIMIT must be a number")
108}
109
110#[cfg(test)]
111mod tests {
112    use super::*;
113    use serial_test::serial;
114
115    #[test]
116    #[serial]
117    fn user_snapshot_interval_defaults_to_50() {
118        env::remove_var("USER_SNAPSHOT_INTERVAL_EVENTS");
119        assert_eq!(
120            user_snapshot_interval_events(),
121            Some(DEFAULT_USER_SNAPSHOT_INTERVAL_EVENTS)
122        );
123    }
124
125    #[test]
126    #[serial]
127    fn user_snapshot_interval_reads_env_override() {
128        env::set_var("USER_SNAPSHOT_INTERVAL_EVENTS", "25");
129        assert_eq!(user_snapshot_interval_events(), Some(25));
130        env::remove_var("USER_SNAPSHOT_INTERVAL_EVENTS");
131    }
132
133    #[test]
134    #[serial]
135    fn user_snapshot_interval_non_positive_disables_snapshots() {
136        env::set_var("USER_SNAPSHOT_INTERVAL_EVENTS", "0");
137        assert_eq!(user_snapshot_interval_events(), None);
138        env::remove_var("USER_SNAPSHOT_INTERVAL_EVENTS");
139    }
140}