use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use std::path::PathBuf;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct DaemonConfig {
pub bind_address: SocketAddr,
pub data_dir: PathBuf,
pub enable_control: bool,
pub metrics_bind: Option<SocketAddr>,
}
impl Default for DaemonConfig {
fn default() -> Self {
Self {
bind_address: SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 8787),
data_dir: PathBuf::from("~/.actionqueue/data"),
enable_control: false,
metrics_bind: Some(SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 9090)),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ConfigError {
pub code: ConfigErrorCode,
pub message: String,
}
impl std::fmt::Display for ConfigError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}: {}", self.code, self.message)
}
}
impl std::error::Error for ConfigError {}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum ConfigErrorCode {
InvalidBindAddress,
InvalidDataDir,
InvalidMetricsBind,
PortConflict,
}
impl std::fmt::Display for ConfigErrorCode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ConfigErrorCode::InvalidBindAddress => write!(f, "invalid_bind_address"),
ConfigErrorCode::InvalidDataDir => write!(f, "invalid_data_dir"),
ConfigErrorCode::InvalidMetricsBind => write!(f, "invalid_metrics_bind"),
ConfigErrorCode::PortConflict => write!(f, "port_conflict"),
}
}
}
impl DaemonConfig {
pub fn validate(&self) -> Result<(), ConfigError> {
if self.bind_address.port() == 0 {
return Err(ConfigError {
code: ConfigErrorCode::InvalidBindAddress,
message: "bind address port must be non-zero".to_string(),
});
}
if let Some(ref metrics_addr) = self.metrics_bind {
if metrics_addr.port() == 0 {
return Err(ConfigError {
code: ConfigErrorCode::InvalidMetricsBind,
message: "metrics bind address port must be non-zero".to_string(),
});
}
if *metrics_addr == self.bind_address {
return Err(ConfigError {
code: ConfigErrorCode::PortConflict,
message: format!(
"metrics_bind {metrics_addr} conflicts with bind_address (same address \
and port)",
),
});
}
}
if self.data_dir.as_os_str().is_empty() {
return Err(ConfigError {
code: ConfigErrorCode::InvalidDataDir,
message: "data directory path must not be empty".to_string(),
});
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_config() {
let config = DaemonConfig::default();
assert_eq!(config.bind_address.ip(), IpAddr::V4(Ipv4Addr::LOCALHOST));
assert_eq!(config.bind_address.port(), 8787);
assert_eq!(config.data_dir, PathBuf::from("~/.actionqueue/data"));
assert!(!config.enable_control);
assert!(config.metrics_bind.is_some());
let metrics_addr = config.metrics_bind.unwrap();
assert_eq!(metrics_addr.ip(), IpAddr::V4(Ipv4Addr::LOCALHOST));
assert_eq!(metrics_addr.port(), 9090);
}
#[test]
fn test_validate_valid_config() {
let config = DaemonConfig::default();
assert!(config.validate().is_ok());
}
#[test]
fn test_validate_zero_port_bind_address() {
let config = DaemonConfig {
bind_address: SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0),
..Default::default()
};
assert_eq!(config.validate().unwrap_err().code, ConfigErrorCode::InvalidBindAddress);
}
#[test]
fn test_validate_zero_port_metrics_bind() {
let config = DaemonConfig {
metrics_bind: Some(SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0)),
..Default::default()
};
assert_eq!(config.validate().unwrap_err().code, ConfigErrorCode::InvalidMetricsBind);
}
#[test]
fn test_validate_port_conflict() {
let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 8787);
let config =
DaemonConfig { bind_address: addr, metrics_bind: Some(addr), ..Default::default() };
let err = config.validate().unwrap_err();
assert_eq!(err.code, ConfigErrorCode::PortConflict);
assert!(err.message.contains("conflicts with bind_address"));
}
#[test]
fn test_validate_different_ports_ok() {
let config = DaemonConfig {
bind_address: SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 8787),
metrics_bind: Some(SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 9090)),
..Default::default()
};
assert!(config.validate().is_ok());
}
#[test]
fn test_validate_empty_data_dir() {
let config = DaemonConfig { data_dir: PathBuf::new(), ..Default::default() };
assert_eq!(config.validate().unwrap_err().code, ConfigErrorCode::InvalidDataDir);
}
#[test]
fn test_config_equality() {
let config1 = DaemonConfig::default();
let config2 = DaemonConfig::default();
assert_eq!(config1, config2);
}
#[test]
fn test_config_cloning() {
let config = DaemonConfig::default();
let cloned = config.clone();
assert_eq!(config, cloned);
}
}