use std::path::PathBuf;
use std::time::Duration;
use serde::{Deserialize, Serialize};
use tracing::debug;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct SeerConfig {
pub output_format: String,
pub nameserver: Option<String>,
pub timeouts: TimeoutConfig,
pub bulk: BulkConfig,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct TimeoutConfig {
pub whois_secs: u64,
pub rdap_secs: u64,
pub dns_secs: u64,
pub http_secs: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct BulkConfig {
pub concurrency: usize,
pub rate_limit_ms: u64,
}
impl Default for SeerConfig {
fn default() -> Self {
Self {
output_format: "human".to_string(),
nameserver: None,
timeouts: TimeoutConfig::default(),
bulk: BulkConfig::default(),
}
}
}
impl Default for TimeoutConfig {
fn default() -> Self {
Self {
whois_secs: 15,
rdap_secs: 30,
dns_secs: 5,
http_secs: 10,
}
}
}
impl Default for BulkConfig {
fn default() -> Self {
Self {
concurrency: 10,
rate_limit_ms: 100,
}
}
}
impl SeerConfig {
pub fn config_path() -> Option<PathBuf> {
dirs::home_dir().map(|home| home.join(".seer").join("config.toml"))
}
pub fn load() -> Self {
let Some(path) = Self::config_path() else {
return Self::default();
};
if !path.exists() {
return Self::default();
}
match std::fs::read_to_string(&path) {
Ok(content) => match toml::from_str::<SeerConfig>(&content) {
Ok(config) => {
debug!(?path, "Loaded config");
config.clamped()
}
Err(e) => {
tracing::warn!(?path, error = %e, "Failed to parse config, using defaults");
Self::default()
}
},
Err(e) => {
debug!(?path, error = %e, "Could not read config, using defaults");
Self::default()
}
}
}
fn clamped(mut self) -> Self {
self.bulk.concurrency = self.bulk.concurrency.clamp(1, 50);
self.timeouts.whois_secs = self.timeouts.whois_secs.clamp(1, 300);
self.timeouts.rdap_secs = self.timeouts.rdap_secs.clamp(1, 300);
self.timeouts.dns_secs = self.timeouts.dns_secs.clamp(1, 60);
self.timeouts.http_secs = self.timeouts.http_secs.clamp(1, 120);
self
}
pub fn whois_timeout(&self) -> Duration {
Duration::from_secs(self.timeouts.whois_secs)
}
pub fn rdap_timeout(&self) -> Duration {
Duration::from_secs(self.timeouts.rdap_secs)
}
pub fn dns_timeout(&self) -> Duration {
Duration::from_secs(self.timeouts.dns_secs)
}
pub fn http_timeout(&self) -> Duration {
Duration::from_secs(self.timeouts.http_secs)
}
pub fn default_toml() -> String {
toml::to_string_pretty(&Self::default()).unwrap_or_else(|_| String::new())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_config() {
let config = SeerConfig::default();
assert_eq!(config.output_format, "human");
assert_eq!(config.timeouts.whois_secs, 15);
assert_eq!(config.timeouts.rdap_secs, 30);
assert_eq!(config.timeouts.dns_secs, 5);
assert_eq!(config.bulk.concurrency, 10);
}
#[test]
fn test_parse_config_toml() {
let toml_str = r#"
output_format = "json"
nameserver = "1.1.1.1"
[timeouts]
whois_secs = 20
rdap_secs = 45
[bulk]
concurrency = 20
"#;
let config: SeerConfig = toml::from_str(toml_str).unwrap();
assert_eq!(config.output_format, "json");
assert_eq!(config.nameserver, Some("1.1.1.1".to_string()));
assert_eq!(config.timeouts.whois_secs, 20);
assert_eq!(config.timeouts.rdap_secs, 45);
assert_eq!(config.timeouts.dns_secs, 5); assert_eq!(config.bulk.concurrency, 20);
}
#[test]
fn test_default_toml_roundtrip() {
let toml_str = SeerConfig::default_toml();
let config: SeerConfig = toml::from_str(&toml_str).unwrap();
assert_eq!(config.output_format, "human");
}
#[test]
fn test_timeout_durations() {
let config = SeerConfig::default();
assert_eq!(config.whois_timeout(), Duration::from_secs(15));
assert_eq!(config.rdap_timeout(), Duration::from_secs(30));
assert_eq!(config.dns_timeout(), Duration::from_secs(5));
assert_eq!(config.http_timeout(), Duration::from_secs(10));
}
}