1use std::path::PathBuf;
6use std::time::Duration;
7
8use serde::{Deserialize, Serialize};
9use tracing::debug;
10
11#[derive(Debug, Clone, Serialize, Deserialize)]
13#[serde(default)]
14pub struct SeerConfig {
15 pub output_format: String,
17 pub nameserver: Option<String>,
19 pub timeouts: TimeoutConfig,
21 pub bulk: BulkConfig,
23}
24
25#[derive(Debug, Clone, Serialize, Deserialize)]
26#[serde(default)]
27pub struct TimeoutConfig {
28 pub whois_secs: u64,
30 pub rdap_secs: u64,
32 pub dns_secs: u64,
34 pub http_secs: u64,
36}
37
38#[derive(Debug, Clone, Serialize, Deserialize)]
39#[serde(default)]
40pub struct BulkConfig {
41 pub concurrency: usize,
43 pub rate_limit_ms: u64,
45}
46
47impl Default for SeerConfig {
48 fn default() -> Self {
49 Self {
50 output_format: "human".to_string(),
51 nameserver: None,
52 timeouts: TimeoutConfig::default(),
53 bulk: BulkConfig::default(),
54 }
55 }
56}
57
58impl Default for TimeoutConfig {
59 fn default() -> Self {
60 Self {
61 whois_secs: 15,
62 rdap_secs: 30,
63 dns_secs: 5,
64 http_secs: 10,
65 }
66 }
67}
68
69impl Default for BulkConfig {
70 fn default() -> Self {
71 Self {
72 concurrency: 10,
73 rate_limit_ms: 100,
74 }
75 }
76}
77
78impl SeerConfig {
79 pub fn config_path() -> Option<PathBuf> {
81 dirs::home_dir().map(|home| home.join(".seer").join("config.toml"))
82 }
83
84 pub fn load() -> Self {
86 let Some(path) = Self::config_path() else {
87 return Self::default();
88 };
89
90 if !path.exists() {
91 return Self::default();
92 }
93
94 match std::fs::read_to_string(&path) {
95 Ok(content) => match toml::from_str::<SeerConfig>(&content) {
96 Ok(config) => {
97 debug!(?path, "Loaded config");
98 config.clamped()
99 }
100 Err(e) => {
101 tracing::warn!(?path, error = %e, "Failed to parse config, using defaults");
102 Self::default()
103 }
104 },
105 Err(e) => {
106 debug!(?path, error = %e, "Could not read config, using defaults");
107 Self::default()
108 }
109 }
110 }
111
112 fn clamped(mut self) -> Self {
120 self.bulk.concurrency = self.bulk.concurrency.clamp(1, 50);
121 self.timeouts.whois_secs = self.timeouts.whois_secs.clamp(1, 300);
122 self.timeouts.rdap_secs = self.timeouts.rdap_secs.clamp(1, 300);
123 self.timeouts.dns_secs = self.timeouts.dns_secs.clamp(1, 60);
124 self.timeouts.http_secs = self.timeouts.http_secs.clamp(1, 120);
125 self
126 }
127
128 pub fn whois_timeout(&self) -> Duration {
130 Duration::from_secs(self.timeouts.whois_secs)
131 }
132
133 pub fn rdap_timeout(&self) -> Duration {
135 Duration::from_secs(self.timeouts.rdap_secs)
136 }
137
138 pub fn dns_timeout(&self) -> Duration {
140 Duration::from_secs(self.timeouts.dns_secs)
141 }
142
143 pub fn http_timeout(&self) -> Duration {
145 Duration::from_secs(self.timeouts.http_secs)
146 }
147
148 pub fn default_toml() -> String {
150 toml::to_string_pretty(&Self::default()).unwrap_or_else(|_| String::new())
151 }
152}
153
154#[cfg(test)]
155mod tests {
156 use super::*;
157
158 #[test]
159 fn test_default_config() {
160 let config = SeerConfig::default();
161 assert_eq!(config.output_format, "human");
162 assert_eq!(config.timeouts.whois_secs, 15);
163 assert_eq!(config.timeouts.rdap_secs, 30);
164 assert_eq!(config.timeouts.dns_secs, 5);
165 assert_eq!(config.bulk.concurrency, 10);
166 }
167
168 #[test]
169 fn test_parse_config_toml() {
170 let toml_str = r#"
171output_format = "json"
172nameserver = "1.1.1.1"
173
174[timeouts]
175whois_secs = 20
176rdap_secs = 45
177
178[bulk]
179concurrency = 20
180"#;
181 let config: SeerConfig = toml::from_str(toml_str).unwrap();
182 assert_eq!(config.output_format, "json");
183 assert_eq!(config.nameserver, Some("1.1.1.1".to_string()));
184 assert_eq!(config.timeouts.whois_secs, 20);
185 assert_eq!(config.timeouts.rdap_secs, 45);
186 assert_eq!(config.timeouts.dns_secs, 5); assert_eq!(config.bulk.concurrency, 20);
188 }
189
190 #[test]
191 fn test_default_toml_roundtrip() {
192 let toml_str = SeerConfig::default_toml();
193 let config: SeerConfig = toml::from_str(&toml_str).unwrap();
194 assert_eq!(config.output_format, "human");
195 }
196
197 #[test]
198 fn test_timeout_durations() {
199 let config = SeerConfig::default();
200 assert_eq!(config.whois_timeout(), Duration::from_secs(15));
201 assert_eq!(config.rdap_timeout(), Duration::from_secs(30));
202 assert_eq!(config.dns_timeout(), Duration::from_secs(5));
203 assert_eq!(config.http_timeout(), Duration::from_secs(10));
204 }
205}