use serde::{Deserialize, Serialize};
use std::fmt;
fn default_cache_ttl() -> u64 {
300
}
fn default_target_field() -> String {
"name".to_string()
}
fn default_max_entries() -> u64 {
10_000
}
fn default_request_timeout() -> u64 {
30
}
fn default_connect_timeout() -> u64 {
5
}
#[derive(Debug, Deserialize, Serialize, Clone, Copy, PartialEq, Eq, Default)]
#[serde(rename_all = "snake_case")]
pub enum PartialOutagePolicy {
#[default]
Strict,
AnySuccess,
}
fn default_partial_outage_policy() -> PartialOutagePolicy {
PartialOutagePolicy::default()
}
#[derive(Deserialize, Serialize, Clone)]
#[serde(deny_unknown_fields)]
pub struct EcpdsConfig {
pub username: String,
pub password: String,
#[serde(default = "default_target_field")]
pub target_field: String,
pub match_key: String,
#[serde(default = "default_cache_ttl")]
pub cache_ttl_seconds: u64,
#[serde(default = "default_max_entries")]
pub max_entries: u64,
#[serde(default = "default_request_timeout")]
pub request_timeout_seconds: u64,
#[serde(default = "default_connect_timeout")]
pub connect_timeout_seconds: u64,
#[serde(default = "default_partial_outage_policy")]
pub partial_outage_policy: PartialOutagePolicy,
pub servers: Vec<String>,
}
impl fmt::Debug for EcpdsConfig {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("EcpdsConfig")
.field("username", &self.username)
.field("password", &"[REDACTED]")
.field("target_field", &self.target_field)
.field("match_key", &self.match_key)
.field("cache_ttl_seconds", &self.cache_ttl_seconds)
.field("max_entries", &self.max_entries)
.field("request_timeout_seconds", &self.request_timeout_seconds)
.field("connect_timeout_seconds", &self.connect_timeout_seconds)
.field("partial_outage_policy", &self.partial_outage_policy)
.field("servers", &self.servers)
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_password_redacted_in_debug() {
let config = EcpdsConfig {
username: "testuser".to_string(),
password: "super-secret-password".to_string(),
target_field: "name".to_string(),
match_key: "destination".to_string(),
cache_ttl_seconds: 300,
max_entries: 10_000,
request_timeout_seconds: 30,
connect_timeout_seconds: 5,
partial_outage_policy: PartialOutagePolicy::Strict,
servers: vec!["http://server1.example.com".to_string()],
};
let debug_str = format!("{:?}", config);
assert!(debug_str.contains("[REDACTED]"));
assert!(!debug_str.contains("super-secret-password"));
}
#[test]
fn test_defaults_applied() {
let json = r#"{
"username": "testuser",
"password": "testpass",
"match_key": "destination",
"servers": ["http://server1.example.com"]
}"#;
let config: EcpdsConfig = serde_json::from_str(json).expect("should deserialize");
assert_eq!(config.cache_ttl_seconds, 300);
assert_eq!(config.target_field, "name");
assert_eq!(config.max_entries, 10_000);
assert_eq!(config.request_timeout_seconds, 30);
assert_eq!(config.connect_timeout_seconds, 5);
assert_eq!(config.partial_outage_policy, PartialOutagePolicy::Strict);
}
#[test]
fn test_full_deserialization() {
let json = r#"{
"username": "testuser",
"password": "testpass",
"target_field": "custom_field",
"match_key": "destination",
"cache_ttl_seconds": 600,
"max_entries": 5000,
"servers": ["http://server1.example.com", "http://server2.example.com"]
}"#;
let config: EcpdsConfig = serde_json::from_str(json).expect("should deserialize");
assert_eq!(config.username, "testuser");
assert_eq!(config.password, "testpass");
assert_eq!(config.target_field, "custom_field");
assert_eq!(config.match_key, "destination");
assert_eq!(config.cache_ttl_seconds, 600);
assert_eq!(config.max_entries, 5000);
assert_eq!(config.servers.len(), 2);
assert_eq!(config.servers[0], "http://server1.example.com");
assert_eq!(config.servers[1], "http://server2.example.com");
}
#[test]
fn unknown_fields_are_rejected() {
let json = r#"{
"username": "testuser",
"password": "testpass",
"match_key": "destination",
"servers": ["http://server1.example.com"],
"cache_ttl_secondz": 600
}"#;
let err = serde_json::from_str::<EcpdsConfig>(json)
.expect_err("a typoed field name must fail to deserialize");
let msg = err.to_string();
assert!(
msg.contains("cache_ttl_secondz") || msg.contains("unknown field"),
"error must point at the offending field name; got: {msg}"
);
}
}