1use crate::config::{ConfigSource, ConfigError};
2use std::env;
3use std::str::FromStr;
4use std::collections::HashMap;
5
6pub trait AppConfigTrait: Sized {
8 fn from_env() -> Result<Self, ConfigError>;
10
11 fn validate(&self) -> Result<(), ConfigError>;
13
14 fn config_sources(&self) -> HashMap<String, ConfigSource>;
16}
17
18#[derive(Debug, Clone, PartialEq, Eq)]
20pub enum Environment {
21 Development,
22 Testing,
23 Production,
24}
25
26impl FromStr for Environment {
27 type Err = ConfigError;
28
29 fn from_str(s: &str) -> Result<Self, Self::Err> {
30 match s.to_lowercase().as_str() {
31 "development" | "dev" => Ok(Environment::Development),
32 "testing" | "test" => Ok(Environment::Testing),
33 "production" | "prod" => Ok(Environment::Production),
34 _ => Err(ConfigError::InvalidValue {
35 field: "environment".to_string(),
36 value: s.to_string(),
37 expected: "development, testing, or production".to_string(),
38 }),
39 }
40 }
41}
42
43impl std::fmt::Display for Environment {
44 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
45 let env_str = match self {
46 Environment::Development => "development",
47 Environment::Testing => "testing",
48 Environment::Production => "production",
49 };
50 write!(f, "{}", env_str)
51 }
52}
53
54impl Environment {
55 pub fn is_development(&self) -> bool {
57 matches!(self, Environment::Development)
58 }
59
60 pub fn is_testing(&self) -> bool {
62 matches!(self, Environment::Testing)
63 }
64
65 pub fn is_production(&self) -> bool {
67 matches!(self, Environment::Production)
68 }
69
70 pub fn debug_mode(&self) -> bool {
72 !self.is_production()
73 }
74}
75
76#[derive(Debug, Clone)]
78pub struct AppConfig {
79 pub environment: Environment,
80 pub debug: bool,
81 pub port: u16,
82 pub host: String,
83 pub database_url: Option<String>,
84 pub redis_url: Option<String>,
85 pub log_level: String,
86 pub secret_key: Option<String>,
87}
88
89impl AppConfig {
90 pub fn new() -> Self {
92 Self {
93 environment: Environment::Development,
94 debug: true,
95 port: 3000,
96 host: "127.0.0.1".to_string(),
97 database_url: None,
98 redis_url: None,
99 log_level: "info".to_string(),
100 secret_key: None,
101 }
102 }
103
104 pub fn development() -> Self {
106 Self {
107 environment: Environment::Development,
108 debug: true,
109 port: 3000,
110 host: "127.0.0.1".to_string(),
111 database_url: Some("postgres://localhost/elif_dev".to_string()),
112 redis_url: Some("redis://localhost:6379".to_string()),
113 log_level: "debug".to_string(),
114 secret_key: Some("dev_secret_key".to_string()),
115 }
116 }
117
118 pub fn testing() -> Self {
120 Self {
121 environment: Environment::Testing,
122 debug: true,
123 port: 0, host: "127.0.0.1".to_string(),
125 database_url: Some("postgres://localhost/elif_test".to_string()),
126 redis_url: Some("redis://localhost:6379/1".to_string()),
127 log_level: "warn".to_string(),
128 secret_key: Some("test_secret_key".to_string()),
129 }
130 }
131
132 pub fn production() -> Self {
134 Self {
135 environment: Environment::Production,
136 debug: false,
137 port: 8080,
138 host: "0.0.0.0".to_string(),
139 database_url: None, redis_url: None, log_level: "info".to_string(),
142 secret_key: None, }
144 }
145
146 pub fn bind_address(&self) -> String {
148 format!("{}:{}", self.host, self.port)
149 }
150
151 pub fn has_database(&self) -> bool {
153 self.database_url.is_some()
154 }
155
156 pub fn has_redis(&self) -> bool {
158 self.redis_url.is_some()
159 }
160}
161
162impl Default for AppConfig {
163 fn default() -> Self {
164 Self::new()
165 }
166}
167
168impl AppConfigTrait for AppConfig {
169 fn from_env() -> Result<Self, ConfigError> {
170 let mut config = Self::new();
171
172 if let Ok(env_str) = env::var("ENVIRONMENT") {
174 config.environment = env_str.parse()?;
175 }
176
177 if let Ok(debug_str) = env::var("DEBUG") {
179 config.debug = debug_str.parse().unwrap_or(config.environment.debug_mode());
180 } else {
181 config.debug = config.environment.debug_mode();
182 }
183
184 if let Ok(port_str) = env::var("PORT") {
186 config.port = port_str.parse().map_err(|_| ConfigError::InvalidValue {
187 field: "port".to_string(),
188 value: port_str,
189 expected: "valid port number (0-65535)".to_string(),
190 })?;
191 }
192
193 if let Ok(host) = env::var("HOST") {
195 config.host = host;
196 }
197
198 config.database_url = env::var("DATABASE_URL").ok();
200
201 config.redis_url = env::var("REDIS_URL").ok();
203
204 if let Ok(log_level) = env::var("LOG_LEVEL") {
206 config.log_level = log_level;
207 }
208
209 config.secret_key = env::var("SECRET_KEY").ok();
211
212 config.validate()?;
213 Ok(config)
214 }
215
216 fn validate(&self) -> Result<(), ConfigError> {
217 if !self.environment.is_testing() && self.port == 0 {
220 return Err(ConfigError::InvalidValue {
221 field: "port".to_string(),
222 value: self.port.to_string(),
223 expected: "port between 1 and 65535".to_string(),
224 });
225 }
226
227 let valid_levels = ["error", "warn", "info", "debug", "trace"];
229 if !valid_levels.contains(&self.log_level.as_str()) {
230 return Err(ConfigError::InvalidValue {
231 field: "log_level".to_string(),
232 value: self.log_level.clone(),
233 expected: format!("one of: {}", valid_levels.join(", ")),
234 });
235 }
236
237 if self.environment.is_production() {
239 if self.secret_key.is_none() {
240 return Err(ConfigError::MissingRequired {
241 field: "secret_key".to_string(),
242 hint: "SECRET_KEY environment variable is required in production".to_string(),
243 });
244 }
245
246 if self.debug {
247 return Err(ConfigError::InvalidValue {
248 field: "debug".to_string(),
249 value: "true".to_string(),
250 expected: "false in production environment".to_string(),
251 });
252 }
253 }
254
255 Ok(())
256 }
257
258 fn config_sources(&self) -> HashMap<String, ConfigSource> {
259 let mut sources = HashMap::new();
260
261 sources.insert("environment".to_string(),
262 if env::var("ENVIRONMENT").is_ok() {
263 ConfigSource::EnvVar("ENVIRONMENT".to_string())
264 } else {
265 ConfigSource::Default("development".to_string())
266 });
267
268 sources.insert("debug".to_string(),
269 if env::var("DEBUG").is_ok() {
270 ConfigSource::EnvVar("DEBUG".to_string())
271 } else {
272 ConfigSource::Default("based on environment".to_string())
273 });
274
275 sources.insert("port".to_string(),
276 if env::var("PORT").is_ok() {
277 ConfigSource::EnvVar("PORT".to_string())
278 } else {
279 ConfigSource::Default("3000".to_string())
280 });
281
282 sources
285 }
286}
287
288#[cfg(test)]
289mod tests {
290 use super::*;
291
292 #[test]
293 fn test_port_validation_in_testing_environment() {
294 let mut config = AppConfig::testing();
296 config.port = 0;
297 assert!(config.validate().is_ok(), "Port 0 should be allowed in testing environment");
298 }
299
300 #[test]
301 fn test_port_validation_in_non_testing_environment() {
302 let mut config = AppConfig::development();
304 config.port = 0;
305 assert!(config.validate().is_err(), "Port 0 should not be allowed in development environment");
306
307 let mut config = AppConfig::production();
309 config.port = 0;
310 assert!(config.validate().is_err(), "Port 0 should not be allowed in production environment");
311 }
312
313 #[test]
314 fn test_valid_port_numbers() {
315 let mut config = AppConfig::development();
317 config.port = 3000;
318 assert!(config.validate().is_ok());
319
320 config = AppConfig::testing();
321 config.port = 8080;
322 assert!(config.validate().is_ok());
323
324 config = AppConfig::production();
325 config.port = 443;
326 let result = config.validate();
329 if let Err(ConfigError::InvalidValue { field, .. }) = result {
331 assert_ne!(field, "port", "Port validation should not fail for valid port numbers");
332 }
333 }
334}