use serde::{Deserialize, Serialize};
use serial_test::serial;
use settings_loader::LoadingOptions;
use std::fs;
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, Default)]
struct TestConfig {
#[serde(default)]
app_name: String,
#[serde(default)]
port: u16,
#[serde(default)]
debug: bool,
#[serde(default)]
database: DatabaseConfig,
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, Default)]
struct DatabaseConfig {
#[serde(default)]
host: String,
#[serde(default)]
port: u16,
#[serde(default)]
user: String,
}
#[derive(Debug, Clone, Default)]
struct DefaultOptions;
impl LoadingOptions for DefaultOptions {
type Error = settings_loader::SettingsError;
fn config_path(&self) -> Option<std::path::PathBuf> {
None
}
fn secrets_path(&self) -> Option<std::path::PathBuf> {
None
}
fn implicit_search_paths(&self) -> Vec<std::path::PathBuf> {
Vec::new()
}
}
#[derive(Debug, Clone, Default)]
struct TurtleOptions;
impl LoadingOptions for TurtleOptions {
type Error = settings_loader::SettingsError;
fn config_path(&self) -> Option<std::path::PathBuf> {
None
}
fn secrets_path(&self) -> Option<std::path::PathBuf> {
None
}
fn implicit_search_paths(&self) -> Vec<std::path::PathBuf> {
Vec::new()
}
fn env_prefix() -> &'static str {
"TURTLE"
}
}
#[derive(Debug, Clone, Default)]
struct CustomSeparatorOptions;
impl LoadingOptions for CustomSeparatorOptions {
type Error = settings_loader::SettingsError;
fn config_path(&self) -> Option<std::path::PathBuf> {
None
}
fn secrets_path(&self) -> Option<std::path::PathBuf> {
None
}
fn implicit_search_paths(&self) -> Vec<std::path::PathBuf> {
Vec::new()
}
fn env_separator() -> &'static str {
"___"
}
}
#[derive(Debug, Clone, Default)]
struct FullyCustomOptions;
impl LoadingOptions for FullyCustomOptions {
type Error = settings_loader::SettingsError;
fn config_path(&self) -> Option<std::path::PathBuf> {
None
}
fn secrets_path(&self) -> Option<std::path::PathBuf> {
None
}
fn implicit_search_paths(&self) -> Vec<std::path::PathBuf> {
Vec::new()
}
fn env_prefix() -> &'static str {
"CUSTOM"
}
fn env_separator() -> &'static str {
"_"
}
}
#[test]
fn test_default_env_prefix() {
let prefix = DefaultOptions::env_prefix();
assert_eq!(prefix, "APP", "Default prefix should be 'APP'");
}
#[test]
fn test_default_env_separator() {
let separator = DefaultOptions::env_separator();
assert_eq!(separator, "__", "Default separator should be '__' (double underscore)");
}
#[test]
fn test_custom_env_prefix() {
let prefix = TurtleOptions::env_prefix();
assert_eq!(prefix, "TURTLE", "TurtleOptions should override prefix to 'TURTLE'");
}
#[test]
fn test_custom_env_separator() {
let separator = CustomSeparatorOptions::env_separator();
assert_eq!(
separator, "___",
"CustomSeparatorOptions should override separator to '___'"
);
}
#[test]
fn test_custom_prefix_and_separator() {
assert_eq!(FullyCustomOptions::env_prefix(), "CUSTOM", "Prefix should be 'CUSTOM'");
assert_eq!(
FullyCustomOptions::env_separator(),
"_",
"Separator should be '_' (single underscore)"
);
}
#[test]
#[serial]
fn test_env_vars_with_custom_prefix() {
let temp_dir = tempfile::tempdir().unwrap();
let config_path = temp_dir.path().join("config.yaml");
fs::write(
&config_path,
"app_name: BaseApp\nport: 8000\ndebug: false\ndatabase:\n host: localhost\n port: 5432\n user: default",
)
.unwrap();
std::env::set_var("TURTLE__PORT", "9000");
std::env::set_var("TURTLE__DATABASE__HOST", "prod.example.com");
let builder = settings_loader::LayerBuilder::new()
.with_path(&config_path)
.with_env_vars("TURTLE", "__");
let config_builder = builder.build().unwrap();
let config = config_builder.build().unwrap();
let result: TestConfig = config.try_deserialize().unwrap();
assert_eq!(result.port, 9000, "Port from TURTLE__PORT should override base config");
assert_eq!(
result.database.host, "prod.example.com",
"Database host from TURTLE__DATABASE__HOST should override"
);
std::env::remove_var("TURTLE__PORT");
std::env::remove_var("TURTLE__DATABASE__HOST");
}
#[test]
#[serial]
fn test_env_vars_with_custom_separator() {
let temp_dir = tempfile::tempdir().unwrap();
let config_path = temp_dir.path().join("config.yaml");
fs::write(
&config_path,
"app_name: SeparatorApp\nport: 8000\ndebug: false\ndatabase:\n host: localhost\n port: 5432\n user: default",
)
.unwrap();
std::env::set_var("CUSTOM_PORT", "7000");
std::env::set_var("CUSTOM_DATABASE_HOST", "sep.example.com");
let builder = settings_loader::LayerBuilder::new()
.with_path(&config_path)
.with_env_vars("CUSTOM", "_");
let config_builder = builder.build().unwrap();
let config = config_builder.build().unwrap();
let result: TestConfig = config.try_deserialize().unwrap();
assert_eq!(
result.port, 7000,
"Port from CUSTOM_PORT should override with single separator"
);
assert_eq!(
result.database.host, "sep.example.com",
"Database host from CUSTOM_DATABASE_HOST should work with custom separator"
);
std::env::remove_var("CUSTOM_PORT");
std::env::remove_var("CUSTOM_DATABASE_HOST");
}
#[test]
fn test_turtle_style_naming_convention() {
let prefix = TurtleOptions::env_prefix();
let separator = TurtleOptions::env_separator();
assert_eq!(prefix, "TURTLE", "Turtle prefix incorrect");
assert_eq!(separator, "__", "Turtle separator incorrect");
}
#[test]
#[serial]
fn test_env_var_loading_with_custom_convention() {
let temp_dir = tempfile::tempdir().unwrap();
let config_path = temp_dir.path().join("config.yaml");
fs::write(
&config_path,
"app_name: TurtleApp\nport: 8000\ndebug: false\ndatabase:\n host: localhost\n port: 5432\n user: turtle",
)
.unwrap();
std::env::set_var("TURTLE__APP_NAME", "TurtleCustom");
std::env::set_var("TURTLE__PORT", "9999");
std::env::set_var("TURTLE__DEBUG", "true");
std::env::set_var("TURTLE__DATABASE__HOST", "turtle.db.local");
std::env::set_var("TURTLE__DATABASE__USER", "turtle_admin");
let builder = settings_loader::LayerBuilder::new()
.with_path(&config_path)
.with_env_vars("TURTLE", "__");
let config_builder = builder.build().unwrap();
let config = config_builder.build().unwrap();
let result: TestConfig = config.try_deserialize().unwrap();
assert_eq!(result.app_name, "TurtleCustom");
assert_eq!(result.port, 9999);
assert!(result.debug);
assert_eq!(result.database.host, "turtle.db.local");
assert_eq!(result.database.user, "turtle_admin");
std::env::remove_var("TURTLE__APP_NAME");
std::env::remove_var("TURTLE__PORT");
std::env::remove_var("TURTLE__DEBUG");
std::env::remove_var("TURTLE__DATABASE__HOST");
std::env::remove_var("TURTLE__DATABASE__USER");
}
#[test]
#[serial]
fn test_backward_compatibility_default_prefix() {
let temp_dir = tempfile::tempdir().unwrap();
let config_path = temp_dir.path().join("config.yaml");
fs::write(
&config_path,
"app_name: LegacyApp\nport: 8000\ndebug: false\ndatabase:\n host: localhost\n port: 5432\n user: legacy",
)
.unwrap();
std::env::set_var("APP__PORT", "8888");
std::env::set_var("APP__DATABASE__HOST", "legacy.db.local");
let builder = settings_loader::LayerBuilder::new()
.with_path(&config_path)
.with_env_vars("APP", "__");
let config_builder = builder.build().unwrap();
let config = config_builder.build().unwrap();
let result: TestConfig = config.try_deserialize().unwrap();
assert_eq!(result.port, 8888);
assert_eq!(result.database.host, "legacy.db.local");
std::env::remove_var("APP__PORT");
std::env::remove_var("APP__DATABASE__HOST");
}
#[test]
#[serial]
fn test_backward_compatibility_default_separator() {
assert_eq!(
DefaultOptions::env_separator(),
"__",
"Default separator must remain '__' for backward compatibility"
);
let temp_dir = tempfile::tempdir().unwrap();
let config_path = temp_dir.path().join("config.yaml");
fs::write(
&config_path,
"app_name: LegacyApp\ndatabase:\n host: localhost\n port: 5432\n user: default",
)
.unwrap();
std::env::set_var("APP__DATABASE__PORT", "3306");
let builder = settings_loader::LayerBuilder::new()
.with_path(&config_path)
.with_env_vars("APP", "__");
let config_builder = builder.build().unwrap();
let config = config_builder.build().unwrap();
let result: TestConfig = config.try_deserialize().unwrap();
assert_eq!(result.database.port, 3306);
std::env::remove_var("APP__DATABASE__PORT");
}
#[test]
fn test_multiple_custom_implementations() {
let turtle_prefix = TurtleOptions::env_prefix();
assert_eq!(turtle_prefix, "TURTLE");
let custom_prefix = FullyCustomOptions::env_prefix();
assert_eq!(custom_prefix, "CUSTOM");
let default_sep = DefaultOptions::env_separator();
let custom_sep = FullyCustomOptions::env_separator();
assert_eq!(default_sep, "__");
assert_eq!(custom_sep, "_");
}