use conflaguration::Error;
use conflaguration::Result;
use conflaguration::Settings;
use conflaguration::Validate;
use conflaguration::ValidationMessage;
#[derive(Default, Debug, PartialEq)]
#[cfg_attr(any(feature = "toml", feature = "json", feature = "yaml"), derive(serde::Deserialize))]
struct TestConfig {
host: String,
port: u16,
}
impl Settings for TestConfig {
const PREFIX: Option<&'static str> = Some("BTEST");
fn from_env() -> Result<Self> {
Ok(Self {
host: conflaguration::resolve_or_parse(&["BTEST_HOST"], "localhost")?,
port: conflaguration::resolve_or(&["BTEST_PORT"], 8080)?,
})
}
fn from_env_with_prefix(prefix: &str) -> Result<Self> {
let host_key = format!("{prefix}_HOST");
let port_key = format!("{prefix}_PORT");
Ok(Self {
host: conflaguration::resolve_or_parse(&[&host_key], "localhost")?,
port: conflaguration::resolve_or(&[&port_key], 8080)?,
})
}
fn override_from_env(&mut self) -> Result<()> {
self.host = conflaguration::resolve_or_parse(&["BTEST_HOST"], &self.host)?;
self.port = conflaguration::resolve_or(&["BTEST_PORT"], self.port)?;
Ok(())
}
fn override_from_env_with_prefix(&mut self, prefix: &str) -> Result<()> {
let host_key = format!("{prefix}_HOST");
let port_key = format!("{prefix}_PORT");
self.host = conflaguration::resolve_or_parse(&[&host_key], &self.host)?;
self.port = conflaguration::resolve_or(&[&port_key], self.port)?;
Ok(())
}
}
impl Validate for TestConfig {
fn validate(&self) -> Result<()> {
let mut errors = vec![];
if self.host.is_empty() {
errors.push(ValidationMessage::new("host", "must not be empty"));
}
if self.port == 0 {
errors.push(ValidationMessage::new("port", "must be > 0"));
}
if errors.is_empty() { Ok(()) } else { Err(Error::Validation { errors }) }
}
}
#[test]
fn env_loads_from_environment() {
temp_env::with_vars([("BTEST_HOST", Some("example.com")), ("BTEST_PORT", Some("9090"))], || {
let config: TestConfig = conflaguration::builder()
.env()
.build()
.unwrap_or_else(|err| panic!("build failed: {err}"));
assert_eq!(config.host, "example.com");
assert_eq!(config.port, 9090);
});
}
#[test]
fn defaults_then_env_overrides() {
temp_env::with_vars([("BTEST_HOST", None::<&str>), ("BTEST_PORT", Some("3000"))], || {
let config: TestConfig = conflaguration::builder()
.defaults()
.env()
.build()
.unwrap_or_else(|err| panic!("build failed: {err}"));
assert_eq!(config.host, "");
assert_eq!(config.port, 3000);
});
}
#[test]
fn apply_mutates_value() {
temp_env::with_vars([("BTEST_HOST", Some("original.com")), ("BTEST_PORT", Some("8080"))], || {
let config: TestConfig = conflaguration::builder()
.env()
.apply(|config: &mut TestConfig| {
config.host = "overridden.com".into();
})
.build()
.unwrap_or_else(|err| panic!("build failed: {err}"));
assert_eq!(config.host, "overridden.com");
assert_eq!(config.port, 8080);
});
}
#[test]
fn validate_passes_valid_config() {
temp_env::with_vars([("BTEST_HOST", Some("localhost")), ("BTEST_PORT", Some("8080"))], || {
let result: Result<TestConfig> = conflaguration::builder().env().validate().build();
assert!(result.is_ok());
});
}
#[test]
fn validate_rejects_invalid_config() {
temp_env::with_vars([("BTEST_HOST", Some("")), ("BTEST_PORT", Some("0"))], || {
let result: Result<TestConfig> = conflaguration::builder().env().validate().build();
assert!(matches!(result, Err(Error::Validation { .. })));
});
}
#[test]
fn build_without_source_returns_no_source() {
let result: Result<TestConfig> = conflaguration::builder().build();
assert!(matches!(result, Err(Error::NoSource)));
}
#[test]
fn error_state_short_circuits() {
temp_env::with_vars([("BTEST_HOST", Some("")), ("BTEST_PORT", Some("0"))], || {
let mut apply_called = false;
let result: Result<TestConfig> = conflaguration::builder()
.env()
.validate()
.apply(|_| {
apply_called = true;
})
.build();
assert!(!apply_called);
assert!(matches!(result, Err(Error::Validation { .. })));
});
}
#[test]
fn env_with_prefix_constructs_from_scratch() {
temp_env::with_vars(
[
("MYAPP_HOST", Some("prefixed.com")),
("MYAPP_PORT", Some("4000")),
("BTEST_HOST", None::<&str>),
("BTEST_PORT", None::<&str>),
],
|| {
let config: TestConfig = conflaguration::builder()
.env_with_prefix("MYAPP")
.build()
.unwrap_or_else(|err| panic!("build failed: {err}"));
assert_eq!(config.host, "prefixed.com");
assert_eq!(config.port, 4000);
},
);
}
#[test]
fn env_with_prefix_overrides_existing() {
temp_env::with_vars(
[
("MYAPP_HOST", Some("prefixed.com")),
("MYAPP_PORT", Some("4000")),
("BTEST_HOST", Some("original.com")),
("BTEST_PORT", Some("8080")),
],
|| {
let config: TestConfig = conflaguration::builder()
.env()
.env_with_prefix("MYAPP")
.build()
.unwrap_or_else(|err| panic!("build failed: {err}"));
assert_eq!(config.host, "prefixed.com");
assert_eq!(config.port, 4000);
},
);
}
#[cfg(feature = "toml")]
#[test]
fn file_then_env_then_apply() {
let dir = tempfile::tempdir().unwrap_or_else(|err| panic!("tempdir failed: {err}"));
let path = dir.path().join("config.toml");
std::fs::write(&path, "host = \"file.com\"\nport = 1234\n").unwrap_or_else(|err| panic!("write failed: {err}"));
temp_env::with_vars([("BTEST_HOST", None::<&str>), ("BTEST_PORT", Some("5555"))], || {
let config: TestConfig = conflaguration::builder()
.file(&path)
.env()
.apply(|config: &mut TestConfig| {
config.host = "cli.com".into();
})
.build()
.unwrap_or_else(|err| panic!("build failed: {err}"));
assert_eq!(config.host, "cli.com");
assert_eq!(config.port, 5555);
});
}
#[cfg(feature = "toml")]
#[test]
fn file_loads_config() {
let dir = tempfile::tempdir().unwrap_or_else(|err| panic!("tempdir failed: {err}"));
let path = dir.path().join("config.toml");
std::fs::write(&path, "host = \"fromfile.com\"\nport = 9999\n").unwrap_or_else(|err| panic!("write failed: {err}"));
temp_env::with_vars([("BTEST_HOST", None::<&str>), ("BTEST_PORT", None::<&str>)], || {
let config: TestConfig = conflaguration::builder()
.file(&path)
.build()
.unwrap_or_else(|err| panic!("build failed: {err}"));
assert_eq!(config.host, "fromfile.com");
assert_eq!(config.port, 9999);
});
}
#[cfg(feature = "toml")]
#[test]
fn env_then_file_overwrites_env_state() {
let dir = tempfile::tempdir().unwrap_or_else(|err| panic!("tempdir failed: {err}"));
let path = dir.path().join("config.toml");
std::fs::write(&path, "host = \"fromfile.com\"\nport = 1111\n").unwrap_or_else(|err| panic!("write failed: {err}"));
temp_env::with_vars([("BTEST_HOST", Some("fromenv.com")), ("BTEST_PORT", Some("2222"))], || {
let config: TestConfig = conflaguration::builder()
.env()
.file(&path)
.build()
.unwrap_or_else(|err| panic!("build failed: {err}"));
assert_eq!(config.host, "fromfile.com", "file() after env() should overwrite");
assert_eq!(config.port, 1111);
});
}
#[cfg(feature = "toml")]
#[test]
fn file_then_env_overwrites_file_state() {
let dir = tempfile::tempdir().unwrap_or_else(|err| panic!("tempdir failed: {err}"));
let path = dir.path().join("config.toml");
std::fs::write(&path, "host = \"fromfile.com\"\nport = 1111\n").unwrap_or_else(|err| panic!("write failed: {err}"));
temp_env::with_vars([("BTEST_HOST", Some("fromenv.com")), ("BTEST_PORT", Some("2222"))], || {
let config: TestConfig = conflaguration::builder()
.file(&path)
.env()
.build()
.unwrap_or_else(|err| panic!("build failed: {err}"));
assert_eq!(config.host, "fromenv.com", "env() after file() should overwrite");
assert_eq!(config.port, 2222);
});
}
#[cfg(feature = "toml")]
#[test]
fn file_error_short_circuits() {
let bad_path = std::path::Path::new("/nonexistent/config.toml");
let result: Result<TestConfig> = conflaguration::builder().file(bad_path).env().build();
assert!(matches!(result, Err(Error::Io(_))));
}
#[test]
fn default_builder_constructs_same_as_new() {
let from_default: conflaguration::ConfigBuilder<TestConfig> = Default::default();
let from_new: conflaguration::ConfigBuilder<TestConfig> = conflaguration::builder();
assert!(matches!(from_default.build(), Err(Error::NoSource)));
assert!(matches!(from_new.build(), Err(Error::NoSource)));
}
#[test]
fn defaults_after_error_preserves_error() {
temp_env::with_vars([("BTEST_HOST", Some("")), ("BTEST_PORT", Some("0"))], || {
let result: Result<TestConfig> = conflaguration::builder().env().validate().defaults().build();
assert!(matches!(result, Err(Error::Validation { .. })));
});
}
#[test]
fn env_after_error_preserves_error() {
temp_env::with_vars([("BTEST_HOST", Some("")), ("BTEST_PORT", Some("0"))], || {
let result: Result<TestConfig> = conflaguration::builder().env().validate().env().build();
assert!(matches!(result, Err(Error::Validation { .. })));
});
}
#[test]
fn env_with_prefix_after_error_preserves_error() {
temp_env::with_vars([("BTEST_HOST", Some("")), ("BTEST_PORT", Some("0"))], || {
let result: Result<TestConfig> = conflaguration::builder().env().validate().env_with_prefix("IGNORED").build();
assert!(matches!(result, Err(Error::Validation { .. })));
});
}
#[test]
fn env_then_defaults_preserves_env_config() {
temp_env::with_vars([("BTEST_HOST", Some("from_env")), ("BTEST_PORT", Some("5555"))], || {
let config: TestConfig = conflaguration::builder()
.env()
.defaults()
.build()
.unwrap_or_else(|err| panic!("build failed: {err}"));
assert_eq!(config.host, "from_env");
assert_eq!(config.port, 5555);
});
}
#[test]
fn defaults_then_env_loads_normally() {
temp_env::with_vars([("BTEST_HOST", Some("env_host")), ("BTEST_PORT", Some("7777"))], || {
let config: TestConfig = conflaguration::builder()
.defaults()
.env()
.build()
.unwrap_or_else(|err| panic!("build failed: {err}"));
assert_eq!(config.host, "env_host");
assert_eq!(config.port, 7777);
});
}
#[test]
fn validate_on_none_state_is_noop() {
temp_env::with_vars([("BTEST_HOST", Some("valid")), ("BTEST_PORT", Some("8080"))], || {
let with_validate: TestConfig = conflaguration::builder()
.validate()
.env()
.build()
.unwrap_or_else(|err| panic!("build failed: {err}"));
let without_validate: TestConfig = conflaguration::builder()
.env()
.build()
.unwrap_or_else(|err| panic!("build failed: {err}"));
assert_eq!(with_validate.host, without_validate.host);
assert_eq!(with_validate.port, without_validate.port);
});
}