1use serde::Deserialize;
4
5#[derive(Debug, Clone, Deserialize)]
7pub struct Config {
8 #[serde(default = "default_env")]
10 pub env: String,
11
12 #[serde(default)]
14 pub log: LogConfig,
15
16 #[serde(default)]
18 pub gateway: GatewayConfig,
19
20 pub database: DatabaseConfig,
22
23 pub redis: RedisConfig,
25
26 #[serde(default)]
28 pub otel: OtelConfig,
29}
30
31#[derive(Debug, Clone, Deserialize)]
32pub struct LogConfig {
33 #[serde(default = "default_log_level")]
34 pub level: String,
35
36 #[serde(default = "default_log_format")]
37 pub format: String,
38}
39
40impl Default for LogConfig {
41 fn default() -> Self {
42 Self {
43 level: default_log_level(),
44 format: default_log_format(),
45 }
46 }
47}
48
49#[derive(Debug, Clone, Deserialize)]
50pub struct GatewayConfig {
51 #[serde(default = "default_host")]
52 pub host: String,
53
54 #[serde(default = "default_port")]
55 pub port: u16,
56
57 #[serde(default = "default_workers")]
58 pub workers: usize,
59}
60
61impl Default for GatewayConfig {
62 fn default() -> Self {
63 Self {
64 host: default_host(),
65 port: default_port(),
66 workers: default_workers(),
67 }
68 }
69}
70
71#[derive(Debug, Clone, Deserialize)]
72pub struct DatabaseConfig {
73 pub url: String,
74
75 #[serde(default = "default_max_connections")]
76 pub max_connections: u32,
77
78 #[serde(default = "default_min_connections")]
79 pub min_connections: u32,
80}
81
82#[derive(Debug, Clone, Deserialize)]
83pub struct RedisConfig {
84 pub url: String,
85
86 #[serde(default = "default_queue_prefix")]
87 pub queue_prefix: String,
88
89 #[serde(default = "default_cache_prefix")]
90 pub cache_prefix: String,
91}
92
93#[derive(Debug, Clone, Deserialize)]
94pub struct OtelConfig {
95 #[serde(default)]
96 pub enabled: bool,
97
98 pub endpoint: Option<String>,
99
100 #[serde(default = "default_service_name")]
101 pub service_name: String,
102}
103
104impl Default for OtelConfig {
105 fn default() -> Self {
106 Self {
107 enabled: false,
108 endpoint: None,
109 service_name: default_service_name(),
110 }
111 }
112}
113
114fn default_env() -> String {
116 "development".to_string()
117}
118fn default_log_level() -> String {
119 "debug".to_string()
120}
121fn default_log_format() -> String {
122 "pretty".to_string()
123}
124fn default_host() -> String {
125 "0.0.0.0".to_string()
126}
127fn default_port() -> u16 {
128 8080
129}
130fn default_workers() -> usize {
131 4
132}
133fn default_max_connections() -> u32 {
134 20
135}
136fn default_min_connections() -> u32 {
137 5
138}
139fn default_queue_prefix() -> String {
140 "fd:queue:".to_string()
141}
142fn default_cache_prefix() -> String {
143 "fd:cache:".to_string()
144}
145fn default_service_name() -> String {
146 "ferrumdeck".to_string()
147}
148
149impl Config {
150 pub fn load() -> Result<Self, config::ConfigError> {
152 let _ = dotenvy::dotenv();
154
155 let builder = config::Config::builder()
156 .set_default("env", "development")?
158 .add_source(
160 config::Environment::with_prefix("FERRUMDECK")
161 .separator("_")
162 .try_parsing(true),
163 )
164 .add_source(
166 config::Environment::default()
167 .prefix("DATABASE")
168 .separator("_"),
169 )
170 .add_source(
172 config::Environment::default()
173 .prefix("REDIS")
174 .separator("_"),
175 )
176 .add_source(
178 config::Environment::default()
179 .prefix("GATEWAY")
180 .separator("_"),
181 )
182 .add_source(config::Environment::default().prefix("OTEL").separator("_"));
184
185 builder.build()?.try_deserialize()
186 }
187}
188
189#[cfg(test)]
190mod tests {
191 use super::*;
192
193 #[test]
198 fn test_config_struct_has_required_fields() {
199 let config = Config {
201 env: "test".to_string(),
202 log: LogConfig::default(),
203 gateway: GatewayConfig::default(),
204 database: DatabaseConfig {
205 url: "postgres://test".to_string(),
206 max_connections: 10,
207 min_connections: 2,
208 },
209 redis: RedisConfig {
210 url: "redis://test".to_string(),
211 queue_prefix: "test:".to_string(),
212 cache_prefix: "cache:".to_string(),
213 },
214 otel: OtelConfig::default(),
215 };
216 assert_eq!(config.env, "test");
217 }
218
219 #[test]
220 fn test_config_is_cloneable() {
221 let config = Config {
222 env: "test".to_string(),
223 log: LogConfig::default(),
224 gateway: GatewayConfig::default(),
225 database: DatabaseConfig {
226 url: "postgres://test".to_string(),
227 max_connections: 10,
228 min_connections: 2,
229 },
230 redis: RedisConfig {
231 url: "redis://test".to_string(),
232 queue_prefix: "test:".to_string(),
233 cache_prefix: "cache:".to_string(),
234 },
235 otel: OtelConfig::default(),
236 };
237 let cloned = config.clone();
238 assert_eq!(cloned.env, config.env);
239 }
240
241 #[test]
242 fn test_config_is_debuggable() {
243 let config = LogConfig::default();
244 let debug_str = format!("{:?}", config);
245 assert!(debug_str.contains("LogConfig"));
246 }
247
248 #[test]
253 fn test_log_config_default_level() {
254 let config = LogConfig::default();
255 assert_eq!(config.level, "debug");
256 }
257
258 #[test]
259 fn test_log_config_default_format() {
260 let config = LogConfig::default();
261 assert_eq!(config.format, "pretty");
262 }
263
264 #[test]
265 fn test_gateway_config_default_host() {
266 let config = GatewayConfig::default();
267 assert_eq!(config.host, "0.0.0.0");
268 }
269
270 #[test]
271 fn test_gateway_config_default_port() {
272 let config = GatewayConfig::default();
273 assert_eq!(config.port, 8080);
274 }
275
276 #[test]
277 fn test_gateway_config_default_workers() {
278 let config = GatewayConfig::default();
279 assert_eq!(config.workers, 4);
280 }
281
282 #[test]
283 fn test_otel_config_default_disabled() {
284 let config = OtelConfig::default();
285 assert!(!config.enabled);
286 }
287
288 #[test]
289 fn test_otel_config_default_endpoint_none() {
290 let config = OtelConfig::default();
291 assert!(config.endpoint.is_none());
292 }
293
294 #[test]
295 fn test_otel_config_default_service_name() {
296 let config = OtelConfig::default();
297 assert_eq!(config.service_name, "ferrumdeck");
298 }
299
300 #[test]
301 fn test_default_env_is_development() {
302 assert_eq!(default_env(), "development");
303 }
304
305 #[test]
306 fn test_default_max_connections() {
307 assert_eq!(default_max_connections(), 20);
308 }
309
310 #[test]
311 fn test_default_min_connections() {
312 assert_eq!(default_min_connections(), 5);
313 }
314
315 #[test]
316 fn test_default_queue_prefix() {
317 assert_eq!(default_queue_prefix(), "fd:queue:");
318 }
319
320 #[test]
321 fn test_default_cache_prefix() {
322 assert_eq!(default_cache_prefix(), "fd:cache:");
323 }
324
325 #[test]
330 fn test_log_config_deserialize_from_json() {
331 let json = r#"{"level": "info", "format": "json"}"#;
332 let config: LogConfig = serde_json::from_str(json).unwrap();
333 assert_eq!(config.level, "info");
334 assert_eq!(config.format, "json");
335 }
336
337 #[test]
338 fn test_log_config_deserialize_with_defaults() {
339 let json = r#"{}"#;
340 let config: LogConfig = serde_json::from_str(json).unwrap();
341 assert_eq!(config.level, "debug");
342 assert_eq!(config.format, "pretty");
343 }
344
345 #[test]
346 fn test_gateway_config_deserialize_from_json() {
347 let json = r#"{"host": "127.0.0.1", "port": 9000, "workers": 8}"#;
348 let config: GatewayConfig = serde_json::from_str(json).unwrap();
349 assert_eq!(config.host, "127.0.0.1");
350 assert_eq!(config.port, 9000);
351 assert_eq!(config.workers, 8);
352 }
353
354 #[test]
355 fn test_gateway_config_deserialize_with_defaults() {
356 let json = r#"{}"#;
357 let config: GatewayConfig = serde_json::from_str(json).unwrap();
358 assert_eq!(config.host, "0.0.0.0");
359 assert_eq!(config.port, 8080);
360 assert_eq!(config.workers, 4);
361 }
362
363 #[test]
364 fn test_database_config_deserialize_from_json() {
365 let json = r#"{"url": "postgres://user:pass@host/db", "max_connections": 50, "min_connections": 10}"#;
366 let config: DatabaseConfig = serde_json::from_str(json).unwrap();
367 assert_eq!(config.url, "postgres://user:pass@host/db");
368 assert_eq!(config.max_connections, 50);
369 assert_eq!(config.min_connections, 10);
370 }
371
372 #[test]
373 fn test_database_config_requires_url() {
374 let json = r#"{"max_connections": 50}"#;
375 let result: Result<DatabaseConfig, _> = serde_json::from_str(json);
376 assert!(result.is_err());
377 }
378
379 #[test]
380 fn test_redis_config_deserialize_from_json() {
381 let json = r#"{"url": "redis://localhost:6379", "queue_prefix": "myapp:", "cache_prefix": "mycache:"}"#;
382 let config: RedisConfig = serde_json::from_str(json).unwrap();
383 assert_eq!(config.url, "redis://localhost:6379");
384 assert_eq!(config.queue_prefix, "myapp:");
385 assert_eq!(config.cache_prefix, "mycache:");
386 }
387
388 #[test]
389 fn test_redis_config_requires_url() {
390 let json = r#"{"queue_prefix": "test:"}"#;
391 let result: Result<RedisConfig, _> = serde_json::from_str(json);
392 assert!(result.is_err());
393 }
394
395 #[test]
396 fn test_otel_config_deserialize_from_json() {
397 let json =
398 r#"{"enabled": true, "endpoint": "http://jaeger:4317", "service_name": "my-service"}"#;
399 let config: OtelConfig = serde_json::from_str(json).unwrap();
400 assert!(config.enabled);
401 assert_eq!(config.endpoint, Some("http://jaeger:4317".to_string()));
402 assert_eq!(config.service_name, "my-service");
403 }
404
405 #[test]
406 fn test_otel_config_deserialize_with_defaults() {
407 let json = r#"{}"#;
408 let config: OtelConfig = serde_json::from_str(json).unwrap();
409 assert!(!config.enabled);
410 assert!(config.endpoint.is_none());
411 assert_eq!(config.service_name, "ferrumdeck");
412 }
413
414 #[test]
419 fn test_full_config_deserialize() {
420 let json = r#"{
421 "env": "production",
422 "log": {"level": "warn", "format": "json"},
423 "gateway": {"host": "0.0.0.0", "port": 80, "workers": 16},
424 "database": {"url": "postgres://prod/db"},
425 "redis": {"url": "redis://prod:6379"},
426 "otel": {"enabled": true, "endpoint": "http://otel:4317"}
427 }"#;
428 let config: Config = serde_json::from_str(json).unwrap();
429 assert_eq!(config.env, "production");
430 assert_eq!(config.log.level, "warn");
431 assert_eq!(config.gateway.port, 80);
432 assert!(config.otel.enabled);
433 }
434
435 #[test]
440 fn test_gateway_port_accepts_valid_range() {
441 let config = GatewayConfig {
442 host: "localhost".to_string(),
443 port: 1,
444 workers: 1,
445 };
446 assert_eq!(config.port, 1);
447
448 let config = GatewayConfig {
449 host: "localhost".to_string(),
450 port: 65535,
451 workers: 1,
452 };
453 assert_eq!(config.port, 65535);
454 }
455
456 #[test]
457 fn test_workers_accepts_positive_values() {
458 let config = GatewayConfig {
459 host: "localhost".to_string(),
460 port: 8080,
461 workers: 1,
462 };
463 assert_eq!(config.workers, 1);
464
465 let config = GatewayConfig {
466 host: "localhost".to_string(),
467 port: 8080,
468 workers: 128,
469 };
470 assert_eq!(config.workers, 128);
471 }
472
473 #[test]
474 fn test_connection_pool_values() {
475 let config = DatabaseConfig {
476 url: "postgres://test".to_string(),
477 max_connections: 100,
478 min_connections: 5,
479 };
480 assert!(config.max_connections >= config.min_connections);
481 }
482}