use std::time::Duration;
use serde::{Deserialize, Serialize};
use crate::builder::strategy::QueryStrategy;
#[derive(Debug, thiserror::Error)]
pub enum ConfigError {
#[error("Missing required configuration: {0}")]
MissingRequired(String),
#[error("Invalid configuration value: {0}")]
InvalidValue(String),
#[error("No upstreams configured - at least one upstream server is required")]
NoUpstreams,
#[error("Invalid timeout: {0}")]
InvalidTimeout(String),
#[error("Invalid retry count: {0}")]
InvalidRetryCount(String),
#[error("Invalid buffer size: {0}")]
InvalidBufferSize(String),
#[error("Invalid port: {0}")]
InvalidPort(String),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UpstreamSpec {
pub address: String,
pub protocol: String,
pub weight: u32,
pub enabled: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StrictDnsConfig {
pub strategy: QueryStrategy,
pub default_timeout: Duration,
pub retry_count: usize,
pub enable_cache: bool,
pub max_cache_ttl: Duration,
pub enable_upstream_monitoring: bool,
pub upstream_monitoring_interval: Duration,
pub port: u16,
pub concurrent_queries: usize,
pub buffer_size: usize,
pub upstreams: Vec<UpstreamSpec>,
pub enable_stats: bool,
pub emergency_threshold: f64,
}
pub struct StrictConfigBuilder {
strategy: Option<QueryStrategy>,
default_timeout: Option<Duration>,
retry_count: Option<usize>,
enable_cache: Option<bool>,
max_cache_ttl: Option<Duration>,
enable_upstream_monitoring: Option<bool>,
upstream_monitoring_interval: Option<Duration>,
port: Option<u16>,
concurrent_queries: Option<usize>,
buffer_size: Option<usize>,
upstreams: Vec<UpstreamSpec>,
enable_stats: Option<bool>,
emergency_threshold: Option<f64>,
}
impl StrictConfigBuilder {
pub fn new() -> Self {
Self {
strategy: None,
default_timeout: None,
retry_count: None,
enable_cache: None,
max_cache_ttl: None,
enable_upstream_monitoring: None,
upstream_monitoring_interval: None,
port: None,
concurrent_queries: None,
buffer_size: None,
upstreams: Vec::new(),
enable_stats: None,
emergency_threshold: None,
}
}
pub fn strategy(mut self, strategy: QueryStrategy) -> Self {
self.strategy = Some(strategy);
self
}
pub fn timeout(mut self, timeout: Duration) -> Self {
self.default_timeout = Some(timeout);
self
}
pub fn retry_count(mut self, count: usize) -> Self {
self.retry_count = Some(count);
self
}
pub fn enable_cache(mut self, enable: bool) -> Self {
self.enable_cache = Some(enable);
self
}
pub fn cache_ttl(mut self, ttl: Duration) -> Self {
self.max_cache_ttl = Some(ttl);
self
}
pub fn enable_upstream_monitoring(mut self, enable: bool) -> Self {
self.enable_upstream_monitoring = Some(enable);
self
}
pub fn upstream_monitoring_interval(mut self, interval: Duration) -> Self {
self.upstream_monitoring_interval = Some(interval);
self
}
pub fn port(mut self, port: u16) -> Self {
self.port = Some(port);
self
}
pub fn concurrent_queries(mut self, count: usize) -> Self {
self.concurrent_queries = Some(count);
self
}
pub fn buffer_size(mut self, size: usize) -> Self {
self.buffer_size = Some(size);
self
}
pub fn add_upstream(mut self, spec: UpstreamSpec) -> Self {
self.upstreams.push(spec);
self
}
pub fn enable_stats(mut self, enable: bool) -> Self {
self.enable_stats = Some(enable);
self
}
pub fn emergency_threshold(mut self, threshold: f64) -> Self {
self.emergency_threshold = Some(threshold);
self
}
pub fn build(self) -> Result<StrictDnsConfig, ConfigError> {
let config = StrictDnsConfig {
strategy: self.strategy.ok_or_else(||
ConfigError::MissingRequired("strategy".to_string()))?,
default_timeout: self.default_timeout.ok_or_else(||
ConfigError::MissingRequired("default_timeout".to_string()))?,
retry_count: self.retry_count.ok_or_else(||
ConfigError::MissingRequired("retry_count".to_string()))?,
enable_cache: self.enable_cache.ok_or_else(||
ConfigError::MissingRequired("enable_cache".to_string()))?,
max_cache_ttl: self.max_cache_ttl.ok_or_else(||
ConfigError::MissingRequired("max_cache_ttl".to_string()))?,
enable_upstream_monitoring: self.enable_upstream_monitoring.ok_or_else(||
ConfigError::MissingRequired("enable_upstream_monitoring".to_string()))?,
upstream_monitoring_interval: self.upstream_monitoring_interval.ok_or_else(||
ConfigError::MissingRequired("upstream_monitoring_interval".to_string()))?,
port: self.port.ok_or_else(||
ConfigError::MissingRequired("port".to_string()))?,
concurrent_queries: self.concurrent_queries.ok_or_else(||
ConfigError::MissingRequired("concurrent_queries".to_string()))?,
buffer_size: self.buffer_size.ok_or_else(||
ConfigError::MissingRequired("buffer_size".to_string()))?,
enable_stats: self.enable_stats.ok_or_else(||
ConfigError::MissingRequired("enable_stats".to_string()))?,
emergency_threshold: self.emergency_threshold.ok_or_else(||
ConfigError::MissingRequired("emergency_threshold".to_string()))?,
upstreams: if self.upstreams.is_empty() {
return Err(ConfigError::NoUpstreams);
} else {
self.upstreams
},
};
config.validate()?;
Ok(config)
}
}
impl StrictDnsConfig {
pub fn builder() -> StrictConfigBuilder {
StrictConfigBuilder::new()
}
pub fn validate(&self) -> Result<(), ConfigError> {
if self.default_timeout.as_millis() == 0 {
return Err(ConfigError::InvalidTimeout(
"Timeout cannot be zero".to_string()));
}
if self.default_timeout.as_secs() > 300 {
return Err(ConfigError::InvalidTimeout(
"Timeout cannot exceed 300 seconds".to_string()));
}
if self.retry_count == 0 {
return Err(ConfigError::InvalidRetryCount(
"Retry count cannot be zero".to_string()));
}
if self.retry_count > 10 {
return Err(ConfigError::InvalidRetryCount(
"Retry count cannot exceed 10".to_string()));
}
if self.port == 0 {
return Err(ConfigError::InvalidPort(
"Port cannot be zero".to_string()));
}
if self.concurrent_queries == 0 {
return Err(ConfigError::InvalidValue(
"Concurrent queries cannot be zero".to_string()));
}
if self.concurrent_queries > 1000 {
return Err(ConfigError::InvalidValue(
"Concurrent queries cannot exceed 1000".to_string()));
}
if self.buffer_size < 512 {
return Err(ConfigError::InvalidBufferSize(
"Buffer size must be at least 512 bytes".to_string()));
}
if self.buffer_size > 65536 {
return Err(ConfigError::InvalidBufferSize(
"Buffer size cannot exceed 65536 bytes".to_string()));
}
if self.upstreams.is_empty() {
return Err(ConfigError::NoUpstreams);
}
for (i, upstream) in self.upstreams.iter().enumerate() {
if upstream.address.is_empty() {
return Err(ConfigError::InvalidValue(
format!("Upstream {} address cannot be empty", i)));
}
if !upstream.address.contains(':') {
return Err(ConfigError::InvalidValue(
format!("Upstream {} address must include port (e.g., '8.8.8.8:53')", i)));
}
if upstream.protocol.is_empty() {
return Err(ConfigError::InvalidValue(
format!("Upstream {} protocol cannot be empty", i)));
}
if upstream.weight == 0 {
return Err(ConfigError::InvalidValue(
format!("Upstream {} weight cannot be zero", i)));
}
}
if self.emergency_threshold < 0.0 || self.emergency_threshold > 1.0 {
return Err(ConfigError::InvalidValue(
"Emergency threshold must be between 0.0 and 1.0".to_string()));
}
if self.enable_cache && self.max_cache_ttl.as_secs() == 0 {
return Err(ConfigError::InvalidValue(
"Cache TTL cannot be zero when cache is enabled".to_string()));
}
if self.enable_upstream_monitoring && self.upstream_monitoring_interval.as_secs() == 0 {
return Err(ConfigError::InvalidValue(
"Upstream monitoring interval cannot be zero when upstream monitoring is enabled".to_string()));
}
Ok(())
}
pub fn enabled_upstreams(&self) -> Vec<&UpstreamSpec> {
self.upstreams.iter().filter(|u| u.enabled).collect()
}
pub fn is_smart_mode(&self) -> bool {
matches!(self.strategy, QueryStrategy::Smart)
}
}
impl UpstreamSpec {
pub fn new(address: String, protocol: String, weight: u32) -> Self {
Self {
address,
protocol,
weight,
enabled: true,
}
}
pub fn disabled(address: String, protocol: String, weight: u32) -> Self {
Self {
address,
protocol,
weight,
enabled: false,
}
}
pub fn parse_address(&self) -> Result<(String, u16), ConfigError> {
let parts: Vec<&str> = self.address.split(':').collect();
if parts.len() != 2 {
return Err(ConfigError::InvalidValue(
format!("Invalid address format: '{}'. Expected 'host:port'", self.address)));
}
let host = parts[0].to_string();
let port = parts[1].parse::<u16>()
.map_err(|_| ConfigError::InvalidValue(
format!("Invalid port in address: '{}'", self.address)))?;
Ok((host, port))
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::Duration;
#[test]
fn test_strict_config_builder_missing_required() {
let result = StrictDnsConfig::builder().build();
assert!(result.is_err());
if let Err(ConfigError::MissingRequired(field)) = result {
assert_eq!(field, "strategy");
} else {
panic!("Expected MissingRequired error");
}
}
#[test]
fn test_strict_config_builder_success() {
let upstream = UpstreamSpec::new(
"8.8.8.8:53".to_string(),
"udp".to_string(),
1
);
let config = StrictDnsConfig::builder()
.strategy(QueryStrategy::Smart)
.timeout(Duration::from_secs(5))
.retry_count(3)
.enable_cache(true)
.cache_ttl(Duration::from_secs(3600))
.enable_upstream_monitoring(true)
.upstream_monitoring_interval(Duration::from_secs(30))
.port(53)
.concurrent_queries(10)
.buffer_size(4096)
.enable_stats(true)
.emergency_threshold(0.3)
.add_upstream(upstream)
.build();
assert!(config.is_ok());
}
#[test]
fn test_upstream_spec_parse_address() {
let upstream = UpstreamSpec::new(
"8.8.8.8:53".to_string(),
"udp".to_string(),
1
);
let (host, port) = upstream.parse_address().unwrap();
assert_eq!(host, "8.8.8.8");
assert_eq!(port, 53);
}
#[test]
fn test_upstream_spec_invalid_address() {
let upstream = UpstreamSpec::new(
"8.8.8.8".to_string(), "udp".to_string(),
1
);
let result = upstream.parse_address();
assert!(result.is_err());
}
}