use config::{Config, Environment, File};
use serde::de::DeserializeOwned;
pub use config::ConfigError;
pub fn load_config<T: DeserializeOwned>() -> Result<T, ConfigError> {
dotenvy::dotenv().ok();
let environment = std::env::var("ARO_ENV").unwrap_or_else(|_| "development".to_string());
let config = Config::builder()
.add_source(File::with_name(&format!("config/{environment}")).required(false))
.add_source(
Environment::with_prefix("APP")
.prefix_separator("_")
.separator("__"),
)
.build()?;
config.try_deserialize()
}
#[cfg(test)]
mod tests {
use super::*;
use serde::Deserialize;
#[test]
fn missing_dotenv_does_not_error() {
#[derive(Debug, Deserialize)]
struct Empty {}
let result: Result<Empty, _> = load_config();
assert!(result.is_ok());
}
#[test]
fn config_returns_result_on_success() {
use crate::App;
#[derive(Debug, Deserialize)]
struct Empty {}
let app = App::new().config::<Empty>();
assert!(app.is_ok());
}
#[test]
fn config_returns_err_when_required_field_missing() {
use crate::App;
#[derive(Debug, Deserialize)]
#[expect(
dead_code,
reason = "field exists only to force deserialization failure"
)]
struct RequiresField {
aro_config_test_required_secret: String,
}
let result = App::new().config::<RequiresField>();
assert!(result.is_err());
}
#[test]
fn config_available_via_dep() {
use crate::dep::Dep;
use crate::state::AroState;
use axum::extract::FromRef;
use std::sync::Arc;
#[derive(Debug, Deserialize, PartialEq)]
struct MyConfig {
name: String,
}
let state = AroState::builder()
.register::<MyConfig>(Arc::new(MyConfig {
name: "test".to_string(),
}))
.build();
let dep = Dep::<MyConfig>::from_ref(&state);
assert_eq!(dep.name, "test");
}
}