use qubit_config::field::ConfigField;
use qubit_config::options::{BlankStringPolicy, ConfigReadOptions};
use qubit_config::{Config, ConfigError, ConfigPrefixView, ConfigReader};
use qubit_datatype::DataType;
use serde::Deserialize;
fn create_test_config() -> Config {
let mut config = Config::new();
config.set("string_value", "test").unwrap();
config.set("int_value", 42).unwrap();
config.set("bool_value", true).unwrap();
config.set("float_value", 3.5).unwrap();
config
}
#[cfg(test)]
mod test_config_reader_smoke {
#[allow(unused_imports)]
use super::{
BlankStringPolicy, Config, ConfigError, ConfigField, ConfigPrefixView, ConfigReadOptions,
ConfigReader, DataType, Deserialize, create_test_config,
};
#[test]
fn config_exposes_config_reader_string_api() {
let c = create_test_config();
assert_eq!(
ConfigReader::get_string(&c, "string_value").unwrap(),
"test"
);
}
}
#[cfg(test)]
mod test_config_reader {
#[allow(unused_imports)]
use super::{
BlankStringPolicy, Config, ConfigError, ConfigField, ConfigPrefixView, ConfigReadOptions,
ConfigReader, DataType, Deserialize, create_test_config,
};
fn read_host(reader: &impl ConfigReader) -> String {
reader.get_string("host").unwrap()
}
fn read_http_host_via_prefix_view(reader: &impl ConfigReader) -> String {
reader.prefix_view("http").get_string("host").unwrap()
}
#[test]
fn test_config_implements_config_reader() {
let mut config = Config::new();
config.set("host", "localhost").unwrap();
assert_eq!(read_host(&config), "localhost");
}
#[test]
fn test_config_reader_prefix_view_on_config_and_nested_view() {
let mut config = Config::new();
config.set("http.host", "localhost").unwrap();
config.set("http.proxy.addr", "127.0.0.1").unwrap();
assert_eq!(read_http_host_via_prefix_view(&config), "localhost");
let http = ConfigReader::prefix_view(&config, "http");
let addr: String = ConfigReader::prefix_view(&http, "proxy")
.get_string("addr")
.unwrap();
assert_eq!(addr, "127.0.0.1");
}
#[test]
fn test_trait_default_methods_with_substitution_disabled_branches() {
let mut config = Config::new();
config.set_enable_variable_substitution(false);
config.set("name", "${who}").unwrap();
config.set("names", vec!["${a}", "${b}"]).unwrap();
let name = <Config as ConfigReader>::get_string(&config, "name").unwrap();
let names = <Config as ConfigReader>::get_string_list(&config, "names").unwrap();
assert_eq!(name, "${who}");
assert_eq!(names, vec!["${a}".to_string(), "${b}".to_string()]);
}
#[test]
fn test_trait_default_methods_on_config_reader() {
let mut config = Config::new();
config.set("name", "alice").unwrap();
config.set("names", vec!["alice", "bob"]).unwrap();
config.set("url", "http://${name}").unwrap();
let reader = &config;
assert_eq!(
<Config as ConfigReader>::get_string_or(reader, "missing", "fallback").unwrap(),
"fallback"
);
assert_eq!(
<Config as ConfigReader>::get_string_list_or(reader, "missing.list", &["x", "y"])
.unwrap(),
vec!["x".to_string(), "y".to_string()]
);
assert_eq!(
<Config as ConfigReader>::get_optional_string(reader, "name").unwrap(),
Some("alice".to_string())
);
assert_eq!(
<Config as ConfigReader>::get_optional_string(reader, "missing").unwrap(),
None
);
assert_eq!(
<Config as ConfigReader>::get_optional_string_list(reader, "names").unwrap(),
Some(vec!["alice".to_string(), "bob".to_string()])
);
assert_eq!(
<Config as ConfigReader>::get_optional_string_list(reader, "missing.names").unwrap(),
None
);
assert_eq!(
<Config as ConfigReader>::get_string_or(reader, "url", "fallback").unwrap(),
"http://alice"
);
}
#[test]
fn test_trait_default_methods_on_view_and_forwarding() {
let mut config = Config::new();
config.set("http.host", "localhost").unwrap();
config.set("http.names", vec!["a", "b"]).unwrap();
config.set("http.timeout", 30).unwrap();
config.set("http.url", "http://${host}").unwrap();
let view = config.prefix_view("http");
assert_eq!(
<ConfigPrefixView<'_> as ConfigReader>::get_string_or(&view, "missing", "fallback")
.unwrap(),
"fallback"
);
assert_eq!(
<ConfigPrefixView<'_> as ConfigReader>::get_string_list_or(&view, "missing", &["m"])
.unwrap(),
vec!["m".to_string()]
);
assert_eq!(
<ConfigPrefixView<'_> as ConfigReader>::get_optional_string(&view, "host").unwrap(),
Some("localhost".to_string())
);
assert_eq!(
<ConfigPrefixView<'_> as ConfigReader>::get_optional_string_list(&view, "names")
.unwrap(),
Some(vec!["a".to_string(), "b".to_string()])
);
let timeout: i32 = <ConfigPrefixView<'_> as ConfigReader>::get(&view, "timeout").unwrap();
assert_eq!(timeout, 30);
let timeout_list: Vec<i32> =
<ConfigPrefixView<'_> as ConfigReader>::get_list(&view, "timeout").unwrap();
assert_eq!(timeout_list, vec![30]);
assert!(<ConfigPrefixView<'_> as ConfigReader>::contains_prefix(
&view, "na"
));
let keys: Vec<&str> = <ConfigPrefixView<'_> as ConfigReader>::iter_prefix(&view, "h")
.map(|(k, _)| k)
.collect();
assert_eq!(keys, vec!["host"]);
assert_eq!(
<ConfigPrefixView<'_> as ConfigReader>::get_string_or(&view, "url", "fallback")
.unwrap(),
"http://localhost"
);
}
#[test]
fn test_config_reader_get_optional_on_config_and_prefix_view() {
let mut config = Config::new();
config.set("http.ipv4_only", true).unwrap();
config.set("http.port", 8080i32).unwrap();
assert_eq!(
<Config as ConfigReader>::get_optional::<bool>(&config, "missing").unwrap(),
None
);
assert_eq!(
<Config as ConfigReader>::get_optional::<bool>(&config, "http.ipv4_only").unwrap(),
Some(true)
);
let http = config.prefix_view("http");
assert_eq!(
<ConfigPrefixView<'_> as ConfigReader>::get_optional::<bool>(&http, "ipv4_only")
.unwrap(),
Some(true)
);
assert_eq!(
<ConfigPrefixView<'_> as ConfigReader>::get_optional::<i32>(&http, "port").unwrap(),
Some(8080)
);
assert_eq!(
<ConfigPrefixView<'_> as ConfigReader>::get_optional::<i32>(&http, "nope").unwrap(),
None
);
}
#[test]
fn test_config_reader_uses_conversion_and_exposes_strict_reads() {
let mut config = Config::new();
config.set("http.enabled", "1").unwrap();
config.set("http.flags", vec!["true", "0"]).unwrap();
assert!(<Config as ConfigReader>::get::<bool>(&config, "http.enabled").unwrap());
assert_eq!(
<Config as ConfigReader>::get_list::<bool>(&config, "http.flags").unwrap(),
vec![true, false]
);
assert!(<Config as ConfigReader>::get_strict::<bool>(&config, "http.enabled").is_err());
assert!(<Config as ConfigReader>::get_list_strict::<bool>(&config, "http.flags").is_err());
let http = config.prefix_view("http");
assert!(<ConfigPrefixView<'_> as ConfigReader>::get::<bool>(&http, "enabled").unwrap());
assert_eq!(
<ConfigPrefixView<'_> as ConfigReader>::get_list::<bool>(&http, "flags").unwrap(),
vec![true, false]
);
assert!(
<ConfigPrefixView<'_> as ConfigReader>::get_strict::<bool>(&http, "enabled").is_err()
);
assert!(
<ConfigPrefixView<'_> as ConfigReader>::get_list_strict::<bool>(&http, "flags")
.is_err()
);
}
#[test]
fn test_config_reader_forwarding_for_config_impl() {
let mut config = Config::new();
config.set("db.host", "127.0.0.1").unwrap();
config.set("db.port", 5432).unwrap();
let reader = &config;
assert!(<Config as ConfigReader>::contains(reader, "db.host"));
assert!(<Config as ConfigReader>::contains_prefix(reader, "db."));
let host: String = <Config as ConfigReader>::get(reader, "db.host").unwrap();
let port_list: Vec<i32> = <Config as ConfigReader>::get_list(reader, "db.port").unwrap();
assert_eq!(host, "127.0.0.1");
assert_eq!(port_list, vec![5432]);
let keys: Vec<&str> = <Config as ConfigReader>::iter_prefix(reader, "db.")
.map(|(k, _)| k)
.collect();
assert_eq!(keys.len(), 2);
}
}
#[cfg(test)]
mod test_config_reader_extended_surface {
#[allow(unused_imports)]
use super::{
BlankStringPolicy, Config, ConfigError, ConfigField, ConfigPrefixView, ConfigReadOptions,
ConfigReader, DataType, Deserialize, create_test_config,
};
#[derive(Deserialize, Debug, PartialEq, Eq)]
struct Server {
host: String,
port: i32,
}
#[test]
fn description_matches_underlying_config_on_root_and_prefix_view() {
let config = Config::with_description("app");
assert_eq!(<Config as ConfigReader>::description(&config), Some("app"));
let view = config.prefix_view("any");
assert_eq!(
<ConfigPrefixView<'_> as ConfigReader>::description(&view),
Some("app")
);
}
#[test]
fn resolve_key_returns_root_relative_paths() {
let mut config = Config::new();
config.set("http.proxy.host", "proxy").unwrap();
assert_eq!(<Config as ConfigReader>::resolve_key(&config, "k"), "k");
assert_eq!(<Config as ConfigReader>::resolve_key(&config, ""), "");
let http = config.prefix_view("http");
assert_eq!(
<ConfigPrefixView<'_> as ConfigReader>::resolve_key(&http, "proxy.host"),
"http.proxy.host"
);
assert_eq!(
<ConfigPrefixView<'_> as ConfigReader>::resolve_key(&http, ""),
"http"
);
let proxy = http.prefix_view("proxy");
assert_eq!(
<ConfigPrefixView<'_> as ConfigReader>::resolve_key(&proxy, "host"),
"http.proxy.host"
);
assert_eq!(
<ConfigPrefixView<'_> as ConfigReader>::resolve_key(&proxy, ""),
"http.proxy"
);
}
#[test]
fn get_property_len_keys_iter_get_or_is_null_and_optional_string_empty() {
let mut config = Config::new();
config.set("k", 1i32).unwrap();
config.set_null("nullish", DataType::Int32).unwrap();
config.set_null("empty.names", DataType::String).unwrap();
assert!(<Config as ConfigReader>::get_property(&config, "k").is_some());
assert!(<Config as ConfigReader>::get_property(&config, "nullish").is_some());
assert!(<Config as ConfigReader>::is_null(&config, "nullish"));
assert!(!<Config as ConfigReader>::is_null(&config, "k"));
assert!(!<Config as ConfigReader>::is_null(&config, "missing"));
assert_eq!(<Config as ConfigReader>::len(&config), 3);
assert!(!<Config as ConfigReader>::is_empty(&config));
let mut keys = <Config as ConfigReader>::keys(&config);
keys.sort();
assert_eq!(
keys,
vec![
"empty.names".to_string(),
"k".to_string(),
"nullish".to_string(),
],
);
assert_eq!(<Config as ConfigReader>::iter(&config).count(), 3);
assert_eq!(
<Config as ConfigReader>::get_or(&config, "k", 0i32).unwrap(),
1
);
assert_eq!(
<Config as ConfigReader>::get_or(&config, "missing", 99i32).unwrap(),
99
);
assert_eq!(
<Config as ConfigReader>::get_optional_string(&config, "nullish").unwrap(),
None
);
assert_eq!(
<Config as ConfigReader>::get_optional_string_list(&config, "empty.names").unwrap(),
None
);
}
#[test]
fn prefix_view_len_keys_iter_get_property_is_null_subconfig() {
let mut config = Config::new();
config.set("a.x", 1i32).unwrap();
config.set("a.y", 2i32).unwrap();
config.set("b.z", 3i32).unwrap();
config.set_null("a.empty", DataType::String).unwrap();
let view = config.prefix_view("a");
assert_eq!(<ConfigPrefixView<'_> as ConfigReader>::len(&view), 3);
let mut keys = <ConfigPrefixView<'_> as ConfigReader>::keys(&view);
keys.sort();
assert_eq!(
keys,
vec!["empty".to_string(), "x".to_string(), "y".to_string()]
);
assert_eq!(
<ConfigPrefixView<'_> as ConfigReader>::iter(&view).count(),
3
);
assert!(<ConfigPrefixView<'_> as ConfigReader>::get_property(&view, "x").is_some());
assert!(<ConfigPrefixView<'_> as ConfigReader>::is_null(
&view, "empty"
));
assert_eq!(
<ConfigPrefixView<'_> as ConfigReader>::get_optional_string(&view, "empty").unwrap(),
None
);
let sub = <ConfigPrefixView<'_> as ConfigReader>::subconfig(&view, "", true).unwrap();
assert!(sub.contains("x") && sub.contains("y") && sub.contains("empty"));
assert!(!sub.contains("a.x"));
let nested =
<ConfigPrefixView<'_> as ConfigReader>::subconfig(&view, "nope", true).unwrap();
assert!(nested.is_empty());
}
#[test]
fn test_config_reader_config_get_optional_list_and_subconfig_forwarding() {
let mut config = Config::new();
config.set("svc.ports", vec![8080i32, 8081]).unwrap();
config.set("other.port", 9090i32).unwrap();
assert_eq!(
<Config as ConfigReader>::get_optional_list::<i32>(&config, "svc.ports").unwrap(),
Some(vec![8080, 8081])
);
assert_eq!(
<Config as ConfigReader>::get_optional_list::<i32>(&config, "missing").unwrap(),
None
);
let sub = <Config as ConfigReader>::subconfig(&config, "svc", true).unwrap();
assert_eq!(sub.get_list::<i32>("ports").unwrap(), vec![8080, 8081]);
assert!(!sub.contains("other.port"));
}
#[test]
fn test_config_reader_prefix_view_empty_and_optional_list_forwarding() {
let mut config = Config::new();
config.set("svc.ports", vec![8080i32, 8081]).unwrap();
let missing = config.prefix_view("missing");
assert!(<ConfigPrefixView<'_> as ConfigReader>::is_empty(&missing));
let view = config.prefix_view("svc");
assert!(!<ConfigPrefixView<'_> as ConfigReader>::is_empty(&view));
assert_eq!(
<ConfigPrefixView<'_> as ConfigReader>::get_optional_list::<i32>(&view, "ports")
.unwrap(),
Some(vec![8080, 8081])
);
assert_eq!(
<ConfigPrefixView<'_> as ConfigReader>::get_optional_list::<i32>(&view, "missing")
.unwrap(),
None
);
let root_view = config.prefix_view("");
let sub =
<ConfigPrefixView<'_> as ConfigReader>::subconfig(&root_view, "svc", true).unwrap();
assert_eq!(sub.get_list::<i32>("ports").unwrap(), vec![8080, 8081]);
}
#[test]
fn deserialize_via_trait_on_config_and_prefix_view() {
let mut config = Config::new();
config.set("srv.host", "h").unwrap();
config.set("srv.port", 5i32).unwrap();
let server: Server = <Config as ConfigReader>::deserialize(&config, "srv").unwrap();
assert_eq!(
server,
Server {
host: "h".to_string(),
port: 5,
}
);
let view = config.prefix_view("srv");
let again: Server = <ConfigPrefixView<'_> as ConfigReader>::deserialize(&view, "").unwrap();
assert_eq!(again, server);
}
}
#[cfg(test)]
mod test_config_reader_alias_reads {
#[allow(unused_imports)]
use super::{
BlankStringPolicy, Config, ConfigError, ConfigField, ConfigPrefixView, ConfigReadOptions,
ConfigReader, DataType, Deserialize, create_test_config,
};
#[test]
fn test_get_any_or_uses_default_only_when_all_names_missing_or_empty() {
let config = Config::new();
let value = config
.get_any_or::<i32>(&["new.port", "old.port"], 8080)
.expect("missing aliases should use default");
assert_eq!(value, 8080);
}
#[test]
fn test_get_any_and_get_optional_any_use_alias_order() {
let mut config = Config::new();
config
.set("PORT", "8080")
.expect("setting alias value should succeed");
let value = config
.get_any::<u16>(&["server.port", "PORT"])
.expect("alias value should parse");
let optional = config
.get_optional_any::<u16>(&["server.port", "PORT"])
.expect("optional alias value should parse");
let missing = config.get_any::<u16>(&["server.host", "HOST"]);
assert_eq!(value, 8080);
assert_eq!(optional, Some(8080));
assert!(matches!(missing, Err(ConfigError::PropertyNotFound(_))));
}
#[test]
fn test_get_any_or_returns_error_for_present_invalid_value_before_default() {
let mut config = Config::new();
config
.set("feature.enabled", "maybe")
.expect("setting test config should succeed");
config
.set("FEATURE_ENABLED", "true")
.expect("setting fallback config should succeed");
let result = config
.with_read_options(ConfigReadOptions::env_friendly())
.get_any_or::<bool>(&["feature.enabled", "FEATURE_ENABLED"], false);
assert!(matches!(
result,
Err(ConfigError::ConversionError { key, .. }) if key == "feature.enabled"
));
}
#[test]
fn test_get_reports_missing_for_blank_string_when_policy_treats_missing() {
let mut config = Config::new();
config
.set("server.host", " ")
.expect("setting blank value should succeed");
let result = config
.with_read_options(
ConfigReadOptions::default()
.with_blank_string_policy(BlankStringPolicy::TreatAsMissing),
)
.get::<String>("server.host");
assert!(matches!(
result,
Err(ConfigError::PropertyHasNoValue(key)) if key == "server.host"
));
}
#[test]
fn test_get_optional_reports_rejected_blank_string() {
let mut config = Config::new();
config
.set("server.host", " ")
.expect("setting blank value should succeed");
let result = config
.with_read_options(
ConfigReadOptions::default().with_blank_string_policy(BlankStringPolicy::Reject),
)
.get_optional::<String>("server.host");
assert!(matches!(
result,
Err(ConfigError::ConversionError { key, message })
if key == "server.host" && message == "blank string is not allowed"
));
}
#[test]
fn test_get_or_returns_error_for_present_invalid_value() {
let mut config = Config::new();
config
.set("server.port", "invalid")
.expect("setting invalid value should succeed");
let result = config.get_or::<u16>("server.port", 8080);
assert!(matches!(
result,
Err(ConfigError::ConversionError { key, .. }) if key == "server.port"
));
}
#[test]
fn test_read_optional_uses_alias_and_default() {
let mut config = Config::new();
config
.set("APP_PORT", "8080")
.expect("setting alias value should succeed");
let alias_value = config
.read_optional(
ConfigField::<u16>::builder()
.name("server.port")
.alias("APP_PORT")
.build(),
)
.expect("optional alias field should parse");
let default_value = config
.read_optional(
ConfigField::<u16>::builder()
.name("server.timeout")
.default(30)
.build(),
)
.expect("optional field default should parse");
assert_eq!(alias_value, Some(8080));
assert_eq!(default_value, Some(30));
}
#[test]
fn test_read_uses_global_options_and_reports_missing_field() {
let mut config = Config::new();
config
.set_read_options(ConfigReadOptions::env_friendly())
.set("FEATURE_ENABLED", "yes")
.expect("setting alias value should succeed");
let enabled = config
.read(
ConfigField::<bool>::builder()
.name("feature.enabled")
.alias("FEATURE_ENABLED")
.build(),
)
.expect("field should use global read options");
let missing = config.read(
ConfigField::<u16>::builder()
.name("server.port")
.alias("PORT")
.build(),
);
assert!(enabled);
assert!(matches!(missing, Err(ConfigError::PropertyNotFound(_))));
}
#[test]
fn test_get_string_any_or_applies_alias_order_and_substitution() {
let mut config = Config::new();
config
.set("host", "localhost")
.expect("setting host should succeed");
config
.set("SERVICE_URL", "http://${host}:8080")
.expect("setting alias value should succeed");
let value = config
.get_string_any_or(&["service.url", "SERVICE_URL"], "http://fallback")
.expect("string alias should resolve");
assert_eq!(value, "http://localhost:8080");
}
#[test]
fn test_get_string_any_and_optional_string_any_use_alias_order() {
let mut config = Config::new();
config
.set("host", "localhost")
.expect("setting host should succeed");
config
.set("SERVICE_URL", "http://${host}:8080")
.expect("setting alias value should succeed");
let value = config
.get_string_any(&["service.url", "SERVICE_URL"])
.expect("string alias should resolve");
let optional = config
.get_optional_string_any(&["service.url", "SERVICE_URL"])
.expect("optional string alias should resolve");
let missing = config
.get_optional_string_any(&["server.url", "SERVER_URL"])
.expect("missing optional string aliases should not fail");
assert_eq!(value, "http://localhost:8080");
assert_eq!(optional.as_deref(), Some("http://localhost:8080"));
assert_eq!(missing, None);
}
#[test]
fn test_get_list_strict_returns_empty_vector_for_null_property() {
let mut config = Config::new();
config
.set_null("empty.list", DataType::String)
.expect("setting null value should succeed");
let values = config
.get_list_strict::<String>("empty.list")
.expect("null list should parse as empty vector");
assert!(values.is_empty());
}
#[test]
fn test_get_list_strict_reports_missing_key() {
let config = Config::new();
let result = config.get_list_strict::<String>("missing.list");
assert!(matches!(result, Err(ConfigError::PropertyNotFound(key)) if key == "missing.list"));
}
#[test]
fn test_prefix_view_optional_error_uses_root_relative_key() {
let mut config = Config::new();
config
.set("db.port", "invalid")
.expect("setting invalid value should succeed");
let db = config.prefix_view("db");
let result = db.get_optional::<u16>("port");
assert!(matches!(
result,
Err(ConfigError::ConversionError { key, .. }) if key == "db.port"
));
}
}