sword_ai/
config.rs

1//! Application configuration module.
2//!
3//! Provides [`AppConfig`] for loading configuration from environment variables.
4//!
5//! ## Environment Variables
6//!
7//! | Variable | Description | Default |
8//! |----------|-------------|---------|
9//! | `DATABASE_URL` | PostgreSQL connection string | **Required** |
10//! | `APP_HOST` | Server bind host | `0.0.0.0` |
11//! | `APP_PORT` | Server bind port | `3000` |
12//! | `DB_MAX_CONNECTIONS` | Maximum database connections | `100` |
13//! | `DB_MIN_CONNECTIONS` | Minimum database connections | `5` |
14//! | `DB_CONNECT_TIMEOUT` | Connection timeout in seconds | `8` |
15//! | `DB_IDLE_TIMEOUT` | Idle connection timeout in seconds | `600` |
16//! | `DB_MAX_LIFETIME` | Maximum connection lifetime in seconds | `1800` |
17//!
18//! ## Example
19//!
20//! ```rust,ignore
21//! use sword_ai::AppConfig;
22//!
23//! let config = AppConfig::from_env()?;
24//! println!("Binding to {}", config.bind_address());
25//! ```
26
27use std::env;
28
29/// Application configuration loaded from environment variables.
30#[derive(Debug, Clone)]
31pub struct AppConfig {
32    /// Server bind host (from `APP_HOST`, default: `0.0.0.0`).
33    pub host: String,
34    /// Server bind port (from `APP_PORT`, default: `3000`).
35    pub port: u16,
36    /// PostgreSQL connection string (from `DATABASE_URL`, required).
37    pub database_url: String,
38    /// Maximum database connections (from `DB_MAX_CONNECTIONS`, default: `100`).
39    pub db_max_connections: u32,
40    /// Minimum database connections (from `DB_MIN_CONNECTIONS`, default: `5`).
41    pub db_min_connections: u32,
42    /// Connection timeout in seconds (from `DB_CONNECT_TIMEOUT`, default: `8`).
43    pub db_connect_timeout: u64,
44    /// Idle connection timeout in seconds (from `DB_IDLE_TIMEOUT`, default: `600`).
45    pub db_idle_timeout: u64,
46    /// Maximum connection lifetime in seconds (from `DB_MAX_LIFETIME`, default: `1800`).
47    pub db_max_lifetime: u64,
48}
49
50impl AppConfig {
51    /// Loads configuration from environment variables.
52    ///
53    /// # Errors
54    ///
55    /// Returns an error if `DATABASE_URL` is not set or if any numeric
56    /// environment variable cannot be parsed.
57    pub fn from_env() -> anyhow::Result<Self> {
58        let host = env::var("APP_HOST").unwrap_or_else(|_| "0.0.0.0".to_string());
59        let port = env::var("APP_PORT")
60            .unwrap_or_else(|_| "3000".to_string())
61            .parse::<u16>()?;
62        let database_url =
63            env::var("DATABASE_URL").map_err(|_| anyhow::anyhow!("DATABASE_URL must be set"))?;
64
65        let db_max_connections = env::var("DB_MAX_CONNECTIONS")
66            .unwrap_or_else(|_| "100".to_string())
67            .parse::<u32>()?;
68        let db_min_connections = env::var("DB_MIN_CONNECTIONS")
69            .unwrap_or_else(|_| "5".to_string())
70            .parse::<u32>()?;
71        let db_connect_timeout = env::var("DB_CONNECT_TIMEOUT")
72            .unwrap_or_else(|_| "8".to_string())
73            .parse::<u64>()?;
74        let db_idle_timeout = env::var("DB_IDLE_TIMEOUT")
75            .unwrap_or_else(|_| "600".to_string())
76            .parse::<u64>()?;
77        let db_max_lifetime = env::var("DB_MAX_LIFETIME")
78            .unwrap_or_else(|_| "1800".to_string())
79            .parse::<u64>()?;
80
81        Ok(Self {
82            host,
83            port,
84            database_url,
85            db_max_connections,
86            db_min_connections,
87            db_connect_timeout,
88            db_idle_timeout,
89            db_max_lifetime,
90        })
91    }
92
93    /// Returns the server bind address in `host:port` format.
94    pub fn bind_address(&self) -> String {
95        format!("{}:{}", self.host, self.port)
96    }
97}
98
99#[cfg(test)]
100mod tests {
101    use super::*;
102
103    fn default_config() -> AppConfig {
104        AppConfig {
105            host: "127.0.0.1".to_string(),
106            port: 8080,
107            database_url: "postgres://localhost/test".to_string(),
108            db_max_connections: 100,
109            db_min_connections: 5,
110            db_connect_timeout: 8,
111            db_idle_timeout: 600,
112            db_max_lifetime: 1800,
113        }
114    }
115
116    #[test]
117    fn test_bind_address() {
118        let config = default_config();
119        assert_eq!(config.bind_address(), "127.0.0.1:8080");
120    }
121
122    #[test]
123    fn test_bind_address_default_format() {
124        let mut config = default_config();
125        config.host = "0.0.0.0".to_string();
126        config.port = 3000;
127        assert_eq!(config.bind_address(), "0.0.0.0:3000");
128    }
129
130    #[test]
131    fn test_config_clone() {
132        let config = default_config();
133        let cloned = config.clone();
134        assert_eq!(config.host, cloned.host);
135        assert_eq!(config.port, cloned.port);
136        assert_eq!(config.database_url, cloned.database_url);
137        assert_eq!(config.db_max_connections, cloned.db_max_connections);
138    }
139}