use crate::error::{ConfersResult, ConfigResult};
use crate::types::{AnnotatedValue, KeyCachePolicy, SourceKind, ZeroizingBytes};
use std::collections::HashMap;
use std::path::Path;
#[cfg(feature = "progressive-reload")]
use crate::HealthStatus;
#[cfg(feature = "remote")]
use crate::types::SourceId;
pub(crate) mod sealed {
pub trait Sealed {}
}
#[cfg(any(
feature = "remote",
feature = "config-bus",
feature = "encryption",
feature = "watch"
))]
pub use async_traits_impl::*;
#[cfg(any(
feature = "remote",
feature = "config-bus",
feature = "encryption",
feature = "watch"
))]
mod async_traits_impl {
use super::sealed::Sealed;
use super::*;
use async_trait::async_trait;
use std::time::Duration;
#[async_trait]
pub trait ConfigReader: Sealed + Send + Sync {
async fn get_raw(&self, key: &str) -> ConfersResult<Option<AnnotatedValue>>;
async fn keys(&self) -> ConfersResult<Vec<String>>;
async fn has(&self, key: &str) -> ConfersResult<bool> {
Ok(self.get_raw(key).await?.is_some())
}
async fn get_string(&self, key: &str) -> ConfersResult<Option<String>> {
#[allow(deprecated)]
Ok(self.get_raw(key).await?.and_then(|v| v.as_string()))
}
async fn get_i64(&self, key: &str) -> ConfersResult<Option<i64>> {
Ok(self.get_raw(key).await?.and_then(|v| v.as_i64()))
}
async fn get_u64(&self, key: &str) -> ConfersResult<Option<u64>> {
Ok(self.get_raw(key).await?.and_then(|v| v.as_u64()))
}
async fn get_f64(&self, key: &str) -> ConfersResult<Option<f64>> {
Ok(self.get_raw(key).await?.and_then(|v| v.as_f64()))
}
async fn get_bool(&self, key: &str) -> ConfersResult<Option<bool>> {
Ok(self.get_raw(key).await?.and_then(|v| v.as_bool()))
}
}
#[async_trait]
pub trait ConfigWriter: Sealed + Send + Sync {
async fn set(&self, key: &str, value: AnnotatedValue) -> ConfersResult<()>;
async fn delete(&self, key: &str) -> ConfersResult<bool>;
async fn clear(&self) -> ConfersResult<()>;
}
#[async_trait]
pub trait ConfigConnector: ConfigReader + ConfigWriter + Sealed + Send + Sync {
async fn health_check(&self) -> crate::error::ConfersResult<()>;
async fn shutdown(&self);
}
#[async_trait]
pub trait AsyncConfigProvider: Send + Sync {
async fn get_string_async(&self, key: &str) -> ConfigResult<Option<String>>;
async fn get_typed_async<T>(&self, key: &str) -> ConfigResult<T>
where
T: std::str::FromStr + Default + Send,
T::Err: std::fmt::Display + Send;
async fn refresh(&self) -> ConfigResult<()>;
}
#[async_trait]
pub trait AsyncKeyProvider: Send + Sync {
async fn get_key(&self) -> ConfigResult<ZeroizingBytes>;
fn provider_type(&self) -> &'static str;
fn ttl(&self) -> Option<Duration> {
None
}
fn cache_policy(&self) -> KeyCachePolicy {
KeyCachePolicy::default()
}
}
}
#[cfg(not(any(
feature = "remote",
feature = "config-bus",
feature = "encryption",
feature = "watch"
)))]
pub use sync_traits::*;
#[cfg(not(any(
feature = "remote",
feature = "config-bus",
feature = "encryption",
feature = "watch"
)))]
mod sync_traits {
use super::sealed::Sealed;
use super::*;
pub trait ConfigReader: Sealed + Send + Sync {
fn get_raw(&self, key: &str) -> ConfersResult<Option<AnnotatedValue>>;
fn keys(&self) -> ConfersResult<Vec<String>>;
fn has(&self, key: &str) -> ConfersResult<bool> {
Ok(self.get_raw(key)?.is_some())
}
fn get_string(&self, key: &str) -> ConfersResult<Option<String>> {
#[allow(deprecated)]
Ok(self.get_raw(key)?.and_then(|v| v.as_string()))
}
fn get_i64(&self, key: &str) -> ConfersResult<Option<i64>> {
Ok(self.get_raw(key)?.and_then(|v| v.as_i64()))
}
fn get_u64(&self, key: &str) -> ConfersResult<Option<u64>> {
Ok(self.get_raw(key)?.and_then(|v| v.as_u64()))
}
fn get_f64(&self, key: &str) -> ConfersResult<Option<f64>> {
Ok(self.get_raw(key)?.and_then(|v| v.as_f64()))
}
fn get_bool(&self, key: &str) -> ConfersResult<Option<bool>> {
Ok(self.get_raw(key)?.and_then(|v| v.as_bool()))
}
}
pub trait ConfigWriter: Sealed + Send + Sync {
fn set(&self, key: &str, value: AnnotatedValue) -> ConfersResult<()>;
fn delete(&self, key: &str) -> ConfersResult<bool>;
fn clear(&self) -> ConfersResult<()>;
}
pub trait ConfigConnector: ConfigReader + ConfigWriter + Sealed + Send + Sync {
fn health_check(&self) -> crate::error::ConfersResult<()>;
fn shutdown(&self);
}
}
pub trait ConfigProvider: Send + Sync {
fn get_raw(&self, key: &str) -> Option<&AnnotatedValue>;
fn keys(&self) -> Vec<String>;
fn has(&self, key: &str) -> bool {
self.get_raw(key).is_some()
}
#[cfg(debug_assertions)]
fn keys_all(&self) -> Vec<String> {
self.keys()
}
}
pub trait ConfigProviderExt: ConfigProvider {
fn get_string(&self, key: &str) -> Option<String> {
#[allow(deprecated)]
self.get_raw(key).and_then(|v| v.as_string())
}
fn get_int(&self, key: &str) -> Option<i64> {
self.get_raw(key).and_then(|v| v.as_i64())
}
fn get_uint(&self, key: &str) -> Option<u64> {
self.get_raw(key).and_then(|v| v.as_u64())
}
fn get_float(&self, key: &str) -> Option<f64> {
self.get_raw(key).and_then(|v| v.as_f64())
}
fn get_bool(&self, key: &str) -> Option<bool> {
self.get_raw(key).and_then(|v| v.as_bool())
}
fn get_typed<T>(&self, key: &str) -> ConfigResult<T>
where
T: std::str::FromStr + Default,
T::Err: std::fmt::Display,
{
let value = self
.get_raw(key)
.ok_or_else(|| crate::error::ConfigError::InvalidValue {
key: key.to_string(),
expected_type: std::any::type_name::<T>().to_string(),
message: "key not found".to_string(),
})?;
let s = {
#[allow(deprecated)]
let s = value.as_string();
s
}
.ok_or_else(|| crate::error::ConfigError::InvalidValue {
key: key.to_string(),
expected_type: std::any::type_name::<T>().to_string(),
message: "value is not a string".to_string(),
})?;
s.parse::<T>()
.map_err(|e| crate::error::ConfigError::InvalidValue {
key: key.to_string(),
expected_type: std::any::type_name::<T>().to_string(),
message: e.to_string(),
})
}
fn get_many<'a>(&self, keys: &[&'a str]) -> HashMap<&'a str, Option<&AnnotatedValue>> {
keys.iter().map(|&k| (k, self.get_raw(k))).collect()
}
fn get_by_path(&self, path: &[&str]) -> Option<&AnnotatedValue> {
if path.is_empty() {
return None;
}
let key = path.join(".");
self.get_raw(&key)
}
}
impl<T: ConfigProvider + ?Sized> ConfigProviderExt for T {}
pub fn filter_sensitive_keys(keys: Vec<String>, sensitive_paths: &[&str]) -> Vec<String> {
keys.into_iter()
.filter(|key| {
!sensitive_paths
.iter()
.any(|s| key == s || key.starts_with(s))
})
.collect()
}
pub trait KeyProvider: Send + Sync {
fn get_key(&self) -> ConfigResult<ZeroizingBytes>;
fn provider_type(&self) -> &'static str;
fn cache_policy(&self) -> KeyCachePolicy {
KeyCachePolicy::default()
}
}
#[cfg(feature = "progressive-reload")]
#[cfg_attr(docsrs, doc(cfg(feature = "progressive-reload")))]
#[async_trait::async_trait]
pub trait ReloadHealthCheck: Send + Sync {
async fn check(&self) -> HealthStatus;
}
pub trait MetricsBackend: Send + Sync {
fn counter(&self, name: &str, labels: &[(&str, &str)]);
fn histogram(&self, name: &str, value: f64, labels: &[(&str, &str)]);
}
pub trait Versioned {
const VERSION: u32;
}
pub trait Source: Send + Sync {
fn collect(&self) -> ConfigResult<AnnotatedValue>;
fn priority(&self) -> u8;
fn name(&self) -> &str;
fn source_kind(&self) -> SourceKind;
fn is_optional(&self) -> bool {
false
}
fn file_path(&self) -> Option<&Path> {
None
}
}
#[cfg(feature = "remote")]
#[async_trait::async_trait]
pub trait AsyncSource: Send + Sync {
async fn load(&self) -> ConfigResult<AnnotatedValue>;
fn source_id(&self) -> &SourceId;
fn priority(&self) -> u8 {
50
}
fn name(&self) -> &str;
}
#[cfg(feature = "context-aware")]
#[cfg_attr(docsrs, doc(cfg(feature = "context-aware")))]
pub trait ContextAwareProvider: Send + Sync {
type Context: Clone + Send + Sync;
fn get_with_context(&self, key: &str, context: &Self::Context) -> Option<AnnotatedValue>;
fn context_dependent_keys(&self) -> Vec<String>;
fn requires_context(&self, key: &str) -> bool;
}
#[cfg(feature = "progressive-reload")]
#[cfg_attr(docsrs, doc(cfg(feature = "progressive-reload")))]
#[async_trait::async_trait]
pub trait AsyncPreloadValidator: Send + Sync {
async fn validate(&self, config: &impl ConfigProvider) -> ConfigResult<()>;
fn name(&self) -> &'static str;
fn priority(&self) -> u8 {
100
}
fn is_optional(&self) -> bool {
false
}
}
#[derive(Debug, Clone)]
pub struct TypedConfigKey<T> {
path: &'static str,
description: Option<&'static str>,
_marker: std::marker::PhantomData<T>,
}
impl<T> TypedConfigKey<T> {
pub const fn new(path: &'static str) -> Self {
Self {
path,
description: None,
_marker: std::marker::PhantomData,
}
}
pub const fn with_description(mut self, description: &'static str) -> Self {
self.description = Some(description);
self
}
pub fn path(&self) -> &'static str {
self.path
}
pub fn description(&self) -> Option<&'static str> {
self.description
}
}
impl<T: Clone + Send + Sync + 'static> TypedConfigKey<T> {
pub fn get<'a>(&self, provider: &'a impl ConfigProvider) -> Option<&'a AnnotatedValue> {
provider.get_raw(self.path)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_typed_config_key() {
static DB_HOST: TypedConfigKey<String> =
TypedConfigKey::new("database.host").with_description("Database hostname");
assert_eq!(DB_HOST.path(), "database.host");
assert_eq!(DB_HOST.description(), Some("Database hostname"));
}
#[test]
#[cfg(feature = "progressive-reload")]
fn test_health_status() {
let healthy = HealthStatus::Healthy;
assert!(healthy.is_healthy());
assert!(!healthy.requires_rollback());
let critical = HealthStatus::Critical {
reason: "test".to_string(),
};
assert!(!critical.is_healthy());
assert!(critical.requires_rollback());
}
#[test]
fn test_key_cache_policy() {
assert_eq!(
KeyCachePolicy::default(),
KeyCachePolicy::CacheWithTtl(std::time::Duration::from_secs(3600))
);
}
#[test]
fn test_zeroizing_bytes() {
let bytes = ZeroizingBytes::new(vec![1, 2, 3, 4]);
assert_eq!(bytes.len(), 4);
assert_eq!(bytes.as_slice(), &[1, 2, 3, 4]);
assert!(!bytes.is_empty());
}
#[test]
fn test_noop_metrics() {
use crate::types::NoOpMetrics;
let metrics = NoOpMetrics;
metrics.counter("test", &[("status", "ok")]);
metrics.histogram("duration", 1.5, &[("source", "file")]);
}
#[cfg(feature = "migration")]
#[test]
fn test_versioned() {
use crate::migration::Versioned;
struct MyConfig;
impl Versioned for MyConfig {
const VERSION: u32 = 2;
}
assert_eq!(MyConfig::VERSION, 2);
}
#[test]
fn test_filter_sensitive_keys_exact_match() {
let keys = vec!["host".into(), "password".into(), "port".into()];
let sensitive = &["password"];
let filtered = filter_sensitive_keys(keys, sensitive);
assert_eq!(filtered, vec!["host", "port"]);
}
#[test]
fn test_filter_sensitive_keys_prefix_match() {
let keys = vec!["db.host".into(), "db.password".into(), "db.port".into()];
let sensitive = &["db.password"];
let filtered = filter_sensitive_keys(keys, sensitive);
assert_eq!(filtered, vec!["db.host", "db.port"]);
}
#[test]
fn test_filter_sensitive_keys_nested_path() {
let keys = vec![
"server.host".into(),
"server.tls.key".into(),
"server.tls.cert".into(),
"server.port".into(),
];
let sensitive = &["server.tls"];
let filtered = filter_sensitive_keys(keys, sensitive);
assert_eq!(filtered, vec!["server.host", "server.port"]);
}
#[test]
fn test_filter_sensitive_keys_no_match() {
let keys = vec!["host".into(), "port".into()];
let sensitive = &["password"];
let filtered = filter_sensitive_keys(keys, sensitive);
assert_eq!(filtered, vec!["host", "port"]);
}
#[test]
fn test_filter_sensitive_keys_empty() {
let keys: Vec<String> = vec![];
let sensitive = &["password"];
let filtered = filter_sensitive_keys(keys, sensitive);
assert!(filtered.is_empty());
}
#[test]
fn test_config_provider_ext_get_typed_not_found() {
use crate::types::{AnnotatedValue, ConfigValue, SourceId};
use std::collections::HashMap;
struct SimpleProvider(HashMap<String, AnnotatedValue>);
impl ConfigProvider for SimpleProvider {
fn get_raw(&self, key: &str) -> Option<&AnnotatedValue> {
self.0.get(key)
}
fn keys(&self) -> Vec<String> {
self.0.keys().cloned().collect()
}
}
let mut map = HashMap::new();
map.insert(
"host".into(),
AnnotatedValue::new(
ConfigValue::string("localhost"),
SourceId::new("test"),
"host",
),
);
let provider = SimpleProvider(map);
let result: Result<String, crate::error::ConfigError> = provider.get_typed("nonexistent");
assert!(result.is_err());
}
#[test]
fn test_config_provider_ext_get_many() {
use crate::types::{AnnotatedValue, ConfigValue, SourceId};
use std::collections::HashMap;
struct SimpleProvider(HashMap<String, AnnotatedValue>);
impl ConfigProvider for SimpleProvider {
fn get_raw(&self, key: &str) -> Option<&AnnotatedValue> {
self.0.get(key)
}
fn keys(&self) -> Vec<String> {
self.0.keys().cloned().collect()
}
}
let mut map = HashMap::new();
map.insert(
"a".into(),
AnnotatedValue::new(ConfigValue::string("1"), SourceId::new("test"), "a"),
);
map.insert(
"b".into(),
AnnotatedValue::new(ConfigValue::string("2"), SourceId::new("test"), "b"),
);
let provider = SimpleProvider(map);
let result = provider.get_many(&["a", "c"]);
assert!(result.get("a").unwrap().is_some());
assert!(result.get("c").unwrap().is_none());
}
#[test]
fn test_config_provider_ext_get_by_path_joins_segments() {
use crate::types::{AnnotatedValue, ConfigValue, SourceId};
use std::collections::HashMap;
struct FlatProvider(HashMap<String, AnnotatedValue>);
impl ConfigProvider for FlatProvider {
fn get_raw(&self, key: &str) -> Option<&AnnotatedValue> {
self.0.get(key)
}
fn keys(&self) -> Vec<String> {
self.0.keys().cloned().collect()
}
}
let mut map = HashMap::new();
map.insert(
"db.host".into(),
AnnotatedValue::new(
ConfigValue::string("localhost"),
SourceId::new("test"),
"db.host",
),
);
let provider = FlatProvider(map);
let result = provider.get_by_path(&["db", "host"]);
assert!(result.is_some());
assert_eq!(result.unwrap().as_str(), Some("localhost"));
}
#[test]
fn test_config_provider_ext_has() {
use crate::types::{AnnotatedValue, ConfigValue, SourceId};
use std::collections::HashMap;
struct P(HashMap<String, AnnotatedValue>);
impl ConfigProvider for P {
fn get_raw(&self, key: &str) -> Option<&AnnotatedValue> {
self.0.get(key)
}
fn keys(&self) -> Vec<String> {
self.0.keys().cloned().collect()
}
}
let mut map = HashMap::new();
map.insert(
"x".into(),
AnnotatedValue::new(ConfigValue::string("1"), SourceId::new("t"), "x"),
);
let p = P(map);
assert!(p.has("x"));
assert!(!p.has("y"));
}
#[test]
fn test_config_provider_ext_get_typed_success() {
use crate::types::{AnnotatedValue, ConfigValue, SourceId};
use std::collections::HashMap;
struct P(HashMap<String, AnnotatedValue>);
impl ConfigProvider for P {
fn get_raw(&self, key: &str) -> Option<&AnnotatedValue> {
self.0.get(key)
}
fn keys(&self) -> Vec<String> {
self.0.keys().cloned().collect()
}
}
let mut map = HashMap::new();
map.insert(
"port".into(),
AnnotatedValue::new(ConfigValue::string("8080"), SourceId::new("t"), "port"),
);
let p = P(map);
let result: Result<u16, crate::error::ConfigError> = p.get_typed("port");
assert_eq!(result.unwrap(), 8080);
}
#[test]
fn test_config_provider_ext_get_string() {
use crate::types::{AnnotatedValue, ConfigValue, SourceId};
use std::collections::HashMap;
struct P(HashMap<String, AnnotatedValue>);
impl ConfigProvider for P {
fn get_raw(&self, key: &str) -> Option<&AnnotatedValue> {
self.0.get(key)
}
fn keys(&self) -> Vec<String> {
self.0.keys().cloned().collect()
}
}
let mut map = HashMap::new();
map.insert(
"host".into(),
AnnotatedValue::new(ConfigValue::string("localhost"), SourceId::new("t"), "host"),
);
let p = P(map);
assert_eq!(p.get_string("host"), Some("localhost".into()));
assert_eq!(p.get_string("missing"), None);
}
#[test]
fn test_config_provider_ext_get_int() {
use crate::types::{AnnotatedValue, ConfigValue, SourceId};
use std::collections::HashMap;
struct P(HashMap<String, AnnotatedValue>);
impl ConfigProvider for P {
fn get_raw(&self, key: &str) -> Option<&AnnotatedValue> {
self.0.get(key)
}
fn keys(&self) -> Vec<String> {
self.0.keys().cloned().collect()
}
}
let mut map = HashMap::new();
map.insert(
"count".into(),
AnnotatedValue::new(ConfigValue::integer(42), SourceId::new("t"), "count"),
);
let p = P(map);
assert_eq!(p.get_int("count"), Some(42));
assert_eq!(p.get_uint("count"), Some(42));
assert_eq!(p.get_float("count"), Some(42.0));
assert_eq!(p.get_bool("count"), None);
}
#[test]
fn test_typed_config_key_without_description() {
static PORT: TypedConfigKey<u16> = TypedConfigKey::new("server.port");
assert_eq!(PORT.path(), "server.port");
assert_eq!(PORT.description(), None);
}
#[test]
fn test_typed_config_key_get_method() {
use crate::types::{AnnotatedValue, ConfigValue, SourceId};
use std::collections::HashMap;
struct MapProvider(HashMap<String, AnnotatedValue>);
impl ConfigProvider for MapProvider {
fn get_raw(&self, key: &str) -> Option<&AnnotatedValue> {
self.0.get(key)
}
fn keys(&self) -> Vec<String> {
self.0.keys().cloned().collect()
}
}
static HOST_KEY: TypedConfigKey<String> = TypedConfigKey::new("db.host");
let mut map = HashMap::new();
map.insert(
"db.host".into(),
AnnotatedValue::new(
ConfigValue::string("localhost"),
SourceId::new("test"),
"db.host",
),
);
let provider = MapProvider(map);
let value = HOST_KEY.get(&provider);
assert!(value.is_some());
assert_eq!(value.unwrap().as_str(), Some("localhost"));
static MISSING_KEY: TypedConfigKey<String> = TypedConfigKey::new("db.missing");
assert!(MISSING_KEY.get(&provider).is_none());
}
#[test]
fn test_config_provider_ext_get_float() {
use crate::types::{AnnotatedValue, ConfigValue, SourceId};
use std::collections::HashMap;
struct P(HashMap<String, AnnotatedValue>);
impl ConfigProvider for P {
fn get_raw(&self, key: &str) -> Option<&AnnotatedValue> {
self.0.get(key)
}
fn keys(&self) -> Vec<String> {
self.0.keys().cloned().collect()
}
}
let mut map = HashMap::new();
map.insert(
"ratio".into(),
AnnotatedValue::new(ConfigValue::float(2.5), SourceId::new("t"), "ratio"),
);
let p = P(map);
assert_eq!(p.get_float("ratio"), Some(2.5));
assert_eq!(p.get_float("missing"), None);
}
#[test]
fn test_config_provider_ext_get_bool() {
use crate::types::{AnnotatedValue, ConfigValue, SourceId};
use std::collections::HashMap;
struct P(HashMap<String, AnnotatedValue>);
impl ConfigProvider for P {
fn get_raw(&self, key: &str) -> Option<&AnnotatedValue> {
self.0.get(key)
}
fn keys(&self) -> Vec<String> {
self.0.keys().cloned().collect()
}
}
let mut map = HashMap::new();
map.insert(
"enabled".into(),
AnnotatedValue::new(ConfigValue::bool(true), SourceId::new("t"), "enabled"),
);
let p = P(map);
assert_eq!(p.get_bool("enabled"), Some(true));
assert_eq!(p.get_bool("missing"), None);
}
#[test]
fn test_config_provider_ext_get_uint() {
use crate::types::{AnnotatedValue, ConfigValue, SourceId};
use std::collections::HashMap;
struct P(HashMap<String, AnnotatedValue>);
impl ConfigProvider for P {
fn get_raw(&self, key: &str) -> Option<&AnnotatedValue> {
self.0.get(key)
}
fn keys(&self) -> Vec<String> {
self.0.keys().cloned().collect()
}
}
let mut map = HashMap::new();
map.insert(
"neg".into(),
AnnotatedValue::new(ConfigValue::integer(-5), SourceId::new("t"), "neg"),
);
let p = P(map);
assert_eq!(p.get_uint("neg"), None);
}
#[test]
fn test_config_provider_ext_get_string_returns_none_for_non_string() {
use crate::types::{AnnotatedValue, ConfigValue, SourceId};
use std::collections::HashMap;
struct P(HashMap<String, AnnotatedValue>);
impl ConfigProvider for P {
fn get_raw(&self, key: &str) -> Option<&AnnotatedValue> {
self.0.get(key)
}
fn keys(&self) -> Vec<String> {
self.0.keys().cloned().collect()
}
}
let mut map = HashMap::new();
map.insert(
"num".into(),
AnnotatedValue::new(ConfigValue::integer(42), SourceId::new("t"), "num"),
);
let p = P(map);
assert_eq!(p.get_string("num"), None);
}
#[test]
fn test_config_provider_ext_get_by_path_empty_returns_none() {
use crate::types::AnnotatedValue;
use std::collections::HashMap;
struct P(HashMap<String, AnnotatedValue>);
impl ConfigProvider for P {
fn get_raw(&self, key: &str) -> Option<&AnnotatedValue> {
self.0.get(key)
}
fn keys(&self) -> Vec<String> {
self.0.keys().cloned().collect()
}
}
let map: HashMap<String, AnnotatedValue> = HashMap::new();
let p = P(map);
assert!(p.get_by_path(&[]).is_none());
}
#[test]
fn test_config_provider_ext_get_by_path_single_segment() {
use crate::types::{AnnotatedValue, ConfigValue, SourceId};
use std::collections::HashMap;
struct P(HashMap<String, AnnotatedValue>);
impl ConfigProvider for P {
fn get_raw(&self, key: &str) -> Option<&AnnotatedValue> {
self.0.get(key)
}
fn keys(&self) -> Vec<String> {
self.0.keys().cloned().collect()
}
}
let mut map = HashMap::new();
map.insert(
"simple".into(),
AnnotatedValue::new(ConfigValue::string("v"), SourceId::new("t"), "simple"),
);
let p = P(map);
let result = p.get_by_path(&["simple"]);
assert!(result.is_some());
}
#[test]
fn test_config_provider_ext_get_typed_non_string_value() {
use crate::types::{AnnotatedValue, ConfigValue, SourceId};
use std::collections::HashMap;
struct P(HashMap<String, AnnotatedValue>);
impl ConfigProvider for P {
fn get_raw(&self, key: &str) -> Option<&AnnotatedValue> {
self.0.get(key)
}
fn keys(&self) -> Vec<String> {
self.0.keys().cloned().collect()
}
}
let mut map = HashMap::new();
map.insert(
"num".into(),
AnnotatedValue::new(ConfigValue::integer(42), SourceId::new("t"), "num"),
);
let p = P(map);
let result: Result<u16, _> = p.get_typed("num");
assert!(result.is_err());
let err = result.unwrap_err();
assert!(matches!(
err,
crate::error::ConfigError::InvalidValue { .. }
));
let msg = err.user_message();
assert!(msg.contains("not a string"));
}
#[test]
fn test_config_provider_ext_get_typed_parse_failure() {
use crate::types::{AnnotatedValue, ConfigValue, SourceId};
use std::collections::HashMap;
struct P(HashMap<String, AnnotatedValue>);
impl ConfigProvider for P {
fn get_raw(&self, key: &str) -> Option<&AnnotatedValue> {
self.0.get(key)
}
fn keys(&self) -> Vec<String> {
self.0.keys().cloned().collect()
}
}
let mut map = HashMap::new();
map.insert(
"port".into(),
AnnotatedValue::new(
ConfigValue::string("not_a_number"),
SourceId::new("t"),
"port",
),
);
let p = P(map);
let result: Result<u16, _> = p.get_typed("port");
assert!(result.is_err());
let err = result.unwrap_err();
assert!(matches!(
err,
crate::error::ConfigError::InvalidValue { .. }
));
}
#[test]
fn test_source_trait_defaults() {
use crate::types::{AnnotatedValue, ConfigValue, SourceId, SourceKind};
struct DummySource;
impl Source for DummySource {
fn collect(&self) -> ConfigResult<AnnotatedValue> {
Ok(AnnotatedValue::new(
ConfigValue::string("v"),
SourceId::new("dummy"),
"root",
))
}
fn priority(&self) -> u8 {
10
}
fn name(&self) -> &str {
"dummy"
}
fn source_kind(&self) -> SourceKind {
SourceKind::Memory
}
}
let s = DummySource;
assert!(!s.is_optional());
assert!(s.file_path().is_none());
assert_eq!(s.priority(), 10);
assert_eq!(s.name(), "dummy");
assert_eq!(s.source_kind(), SourceKind::Memory);
let collected = s.collect().expect("collect succeeds");
assert_eq!(collected.as_str(), Some("v"));
}
#[test]
fn test_source_trait_overridden_defaults() {
use crate::types::{AnnotatedValue, ConfigValue, SourceId, SourceKind};
use std::path::Path;
struct FileSource;
impl Source for FileSource {
fn collect(&self) -> ConfigResult<AnnotatedValue> {
Ok(AnnotatedValue::new(
ConfigValue::string("file"),
SourceId::new("file"),
"root",
))
}
fn priority(&self) -> u8 {
100
}
fn name(&self) -> &str {
"file_source"
}
fn source_kind(&self) -> SourceKind {
SourceKind::File
}
fn is_optional(&self) -> bool {
true
}
fn file_path(&self) -> Option<&Path> {
Some(Path::new("/etc/config.toml"))
}
}
let s = FileSource;
assert!(s.is_optional());
assert_eq!(s.file_path(), Some(Path::new("/etc/config.toml")));
}
#[test]
fn test_key_provider_default_cache_policy() {
use crate::types::{KeyCachePolicy, ZeroizingBytes};
struct StaticKeyProvider;
impl KeyProvider for StaticKeyProvider {
fn get_key(&self) -> ConfigResult<ZeroizingBytes> {
Ok(ZeroizingBytes::new(vec![0u8; 32]))
}
fn provider_type(&self) -> &'static str {
"static"
}
}
let p = StaticKeyProvider;
assert_eq!(p.provider_type(), "static");
assert_eq!(
p.cache_policy(),
KeyCachePolicy::CacheWithTtl(std::time::Duration::from_secs(3600))
);
let key = p.get_key().expect("get_key succeeds");
assert_eq!(key.len(), 32);
}
#[test]
fn test_key_provider_overridden_cache_policy() {
use crate::types::{KeyCachePolicy, ZeroizingBytes};
struct NoCacheProvider;
impl KeyProvider for NoCacheProvider {
fn get_key(&self) -> ConfigResult<ZeroizingBytes> {
Ok(ZeroizingBytes::new(vec![1, 2, 3]))
}
fn provider_type(&self) -> &'static str {
"nocache"
}
fn cache_policy(&self) -> KeyCachePolicy {
KeyCachePolicy::NoCache
}
}
let p = NoCacheProvider;
assert_eq!(p.cache_policy(), KeyCachePolicy::NoCache);
}
#[test]
fn test_metrics_backend_custom_impl() {
use std::sync::atomic::{AtomicUsize, Ordering};
struct CountingMetrics {
counters: AtomicUsize,
histograms: AtomicUsize,
}
impl MetricsBackend for CountingMetrics {
fn counter(&self, _name: &str, _labels: &[(&str, &str)]) {
self.counters.fetch_add(1, Ordering::SeqCst);
}
fn histogram(&self, _name: &str, _value: f64, _labels: &[(&str, &str)]) {
self.histograms.fetch_add(1, Ordering::SeqCst);
}
}
let m = CountingMetrics {
counters: AtomicUsize::new(0),
histograms: AtomicUsize::new(0),
};
m.counter("requests", &[("status", "ok")]);
m.counter("requests", &[("status", "err")]);
m.histogram("latency", 0.5, &[("endpoint", "/api")]);
assert_eq!(m.counters.load(Ordering::SeqCst), 2);
assert_eq!(m.histograms.load(Ordering::SeqCst), 1);
}
#[test]
fn test_versioned_trait_interface() {
struct AppConfig;
impl Versioned for AppConfig {
const VERSION: u32 = 5;
}
assert_eq!(AppConfig::VERSION, 5);
struct LegacyConfig;
impl Versioned for LegacyConfig {
const VERSION: u32 = 1;
}
assert_eq!(LegacyConfig::VERSION, 1);
assert_ne!(AppConfig::VERSION, LegacyConfig::VERSION);
}
#[test]
fn test_filter_sensitive_keys_substring_not_prefix() {
let keys = vec!["db.password".into(), "password".into(), "host".into()];
let sensitive = &["password"];
let filtered = filter_sensitive_keys(keys, sensitive);
assert_eq!(filtered, vec!["db.password", "host"]);
}
#[test]
fn test_filter_sensitive_keys_multiple_sensitive_paths() {
let keys = vec![
"host".into(),
"password".into(),
"api_key".into(),
"token".into(),
"port".into(),
];
let sensitive = &["password", "api_key", "token"];
let filtered = filter_sensitive_keys(keys, sensitive);
assert_eq!(filtered, vec!["host", "port"]);
}
#[test]
fn test_filter_sensitive_keys_all_filtered() {
let keys = vec!["password".into(), "api_key".into()];
let sensitive = &["password", "api_key"];
let filtered = filter_sensitive_keys(keys, sensitive);
assert!(filtered.is_empty());
}
#[test]
fn test_filter_sensitive_keys_no_sensitive_paths() {
let keys = vec!["host".into(), "port".into()];
let sensitive: &[&str] = &[];
let filtered = filter_sensitive_keys(keys, sensitive);
assert_eq!(filtered, vec!["host", "port"]);
}
#[test]
#[cfg(debug_assertions)]
fn test_config_provider_keys_all_debug() {
use crate::types::{AnnotatedValue, ConfigValue, SourceId};
use std::collections::HashMap;
struct P(HashMap<String, AnnotatedValue>);
impl ConfigProvider for P {
fn get_raw(&self, key: &str) -> Option<&AnnotatedValue> {
self.0.get(key)
}
fn keys(&self) -> Vec<String> {
self.0.keys().cloned().collect()
}
}
let mut map = HashMap::new();
map.insert(
"a".into(),
AnnotatedValue::new(ConfigValue::string("1"), SourceId::new("t"), "a"),
);
map.insert(
"b".into(),
AnnotatedValue::new(ConfigValue::string("2"), SourceId::new("t"), "b"),
);
let p = P(map);
let all = p.keys_all();
assert_eq!(all.len(), 2);
}
#[test]
fn test_config_provider_has_default_impl() {
use crate::types::{AnnotatedValue, ConfigValue, SourceId};
use std::collections::HashMap;
struct P(HashMap<String, AnnotatedValue>);
impl ConfigProvider for P {
fn get_raw(&self, key: &str) -> Option<&AnnotatedValue> {
self.0.get(key)
}
fn keys(&self) -> Vec<String> {
self.0.keys().cloned().collect()
}
}
let mut map = HashMap::new();
map.insert(
"exists".into(),
AnnotatedValue::new(ConfigValue::string("v"), SourceId::new("t"), "exists"),
);
let p = P(map);
assert!(p.has("exists"));
assert!(!p.has("missing"));
}
#[test]
fn test_source_kind_variants() {
use crate::types::SourceKind;
assert_eq!(SourceKind::File, SourceKind::File);
assert_eq!(SourceKind::Environment, SourceKind::Environment);
assert_eq!(SourceKind::CommandLine, SourceKind::CommandLine);
assert_eq!(SourceKind::Default, SourceKind::Default);
assert_eq!(SourceKind::Memory, SourceKind::Memory);
assert_ne!(SourceKind::File, SourceKind::Memory);
}
#[test]
#[cfg(feature = "remote")]
fn test_async_source_priority_default() {
use crate::interface::AsyncSource;
use crate::types::{AnnotatedValue, ConfigValue, SourceId};
struct DummyAsyncSource {
id: SourceId,
}
#[async_trait::async_trait]
impl AsyncSource for DummyAsyncSource {
async fn load(&self) -> ConfigResult<AnnotatedValue> {
Ok(AnnotatedValue::new(
ConfigValue::string("v"),
self.id.clone(),
"root",
))
}
fn source_id(&self) -> &SourceId {
&self.id
}
fn name(&self) -> &str {
"dummy_async"
}
}
let s = DummyAsyncSource {
id: SourceId::new("dummy"),
};
assert_eq!(s.priority(), 50);
assert_eq!(s.name(), "dummy_async");
}
}