1use config::{Config, Environment, File};
9use serde::de::DeserializeOwned;
10
11pub use config::ConfigError;
12
13pub fn load_config<T: DeserializeOwned>() -> Result<T, ConfigError> {
20 dotenvy::dotenv().ok();
22
23 let environment = std::env::var("ARO_ENV").unwrap_or_else(|_| "development".to_string());
24
25 let config = Config::builder()
26 .add_source(File::with_name(&format!("config/{environment}")).required(false))
27 .add_source(
28 Environment::with_prefix("APP")
29 .prefix_separator("_")
30 .separator("__"),
31 )
32 .build()?;
33
34 config.try_deserialize()
35}
36
37#[cfg(test)]
38mod tests {
39 use super::*;
40 use serde::Deserialize;
41
42 #[test]
43 fn missing_dotenv_does_not_error() {
44 #[derive(Debug, Deserialize)]
45 struct Empty {}
46
47 let result: Result<Empty, _> = load_config();
48 assert!(result.is_ok());
49 }
50
51 #[test]
52 fn config_returns_result_on_success() {
53 use crate::App;
54
55 #[derive(Debug, Deserialize)]
56 struct Empty {}
57
58 let app = App::new().config::<Empty>();
59 assert!(app.is_ok());
60 }
61
62 #[test]
63 fn config_returns_err_when_required_field_missing() {
64 use crate::App;
65
66 #[derive(Debug, Deserialize)]
67 #[expect(
68 dead_code,
69 reason = "field exists only to force deserialization failure"
70 )]
71 struct RequiresField {
72 aro_config_test_required_secret: String,
74 }
75
76 let result = App::new().config::<RequiresField>();
77 assert!(result.is_err());
78 }
79
80 #[test]
81 fn config_available_via_dep() {
82 use crate::dep::Dep;
83 use crate::state::AroState;
84 use axum::extract::FromRef;
85 use std::sync::Arc;
86
87 #[derive(Debug, Deserialize, PartialEq)]
88 struct MyConfig {
89 name: String,
90 }
91
92 let state = AroState::builder()
93 .register::<MyConfig>(Arc::new(MyConfig {
94 name: "test".to_string(),
95 }))
96 .build();
97
98 let dep = Dep::<MyConfig>::from_ref(&state);
99 assert_eq!(dep.name, "test");
100 }
101}