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: 15,
65 dns_secs: 5,
66 http_secs: 10,
67 }
68 }
69}
70
71impl Default for BulkConfig {
72 fn default() -> Self {
73 Self {
74 concurrency: 10,
75 rate_limit_ms: 100,
76 }
77 }
78}
79
80impl SeerConfig {
81 pub fn config_path() -> Option<PathBuf> {
83 dirs::home_dir().map(|home| home.join(".seer").join("config.toml"))
84 }
85
86 pub fn load() -> Self {
88 let Some(path) = Self::config_path() else {
89 return Self::default();
90 };
91
92 if !path.exists() {
93 return Self::default();
94 }
95
96 match std::fs::read_to_string(&path) {
97 Ok(content) => match toml::from_str::<SeerConfig>(&content) {
98 Ok(config) => {
99 debug!(?path, "Loaded config");
100 config.clamped()
101 }
102 Err(e) => {
103 tracing::warn!(?path, error = %e, "Failed to parse config, using defaults");
104 Self::default()
105 }
106 },
107 Err(e) => {
108 debug!(?path, error = %e, "Could not read config, using defaults");
109 Self::default()
110 }
111 }
112 }
113
114 fn clamped(mut self) -> Self {
123 self.bulk.concurrency = self.bulk.concurrency.clamp(1, 50);
124 self.bulk.rate_limit_ms = self.bulk.rate_limit_ms.min(60_000);
127 self.timeouts.whois_secs = self.timeouts.whois_secs.clamp(1, 300);
128 self.timeouts.rdap_secs = self.timeouts.rdap_secs.clamp(1, 300);
129 self.timeouts.dns_secs = self.timeouts.dns_secs.clamp(1, 60);
130 self.timeouts.http_secs = self.timeouts.http_secs.clamp(1, 120);
131 self
132 }
133
134 pub fn whois_timeout(&self) -> Duration {
136 Duration::from_secs(self.timeouts.whois_secs)
137 }
138
139 pub fn rdap_timeout(&self) -> Duration {
141 Duration::from_secs(self.timeouts.rdap_secs)
142 }
143
144 pub fn dns_timeout(&self) -> Duration {
146 Duration::from_secs(self.timeouts.dns_secs)
147 }
148
149 pub fn http_timeout(&self) -> Duration {
151 Duration::from_secs(self.timeouts.http_secs)
152 }
153
154 pub fn bulk_rate_limit(&self) -> Duration {
156 Duration::from_millis(self.bulk.rate_limit_ms)
157 }
158
159 pub fn default_toml() -> String {
161 toml::to_string_pretty(&Self::default()).unwrap_or_else(|_| String::new())
162 }
163}
164
165#[cfg(test)]
166mod tests {
167 use super::*;
168
169 #[test]
170 fn test_default_config() {
171 let config = SeerConfig::default();
172 assert_eq!(config.output_format, "human");
173 assert_eq!(config.timeouts.whois_secs, 15);
174 assert_eq!(config.timeouts.rdap_secs, 15);
175 assert_eq!(config.timeouts.dns_secs, 5);
176 assert_eq!(config.bulk.concurrency, 10);
177 }
178
179 #[test]
180 fn test_parse_config_toml() {
181 let toml_str = r#"
182output_format = "json"
183nameserver = "1.1.1.1"
184
185[timeouts]
186whois_secs = 20
187rdap_secs = 45
188
189[bulk]
190concurrency = 20
191"#;
192 let config: SeerConfig = toml::from_str(toml_str).unwrap();
193 assert_eq!(config.output_format, "json");
194 assert_eq!(config.nameserver, Some("1.1.1.1".to_string()));
195 assert_eq!(config.timeouts.whois_secs, 20);
196 assert_eq!(config.timeouts.rdap_secs, 45);
197 assert_eq!(config.timeouts.dns_secs, 5); assert_eq!(config.bulk.concurrency, 20);
199 }
200
201 #[test]
202 fn test_default_toml_roundtrip() {
203 let toml_str = SeerConfig::default_toml();
204 let config: SeerConfig = toml::from_str(&toml_str).unwrap();
205 assert_eq!(config.output_format, "human");
206 }
207
208 #[test]
209 fn test_timeout_durations() {
210 let config = SeerConfig::default();
211 assert_eq!(config.whois_timeout(), Duration::from_secs(15));
212 assert_eq!(config.rdap_timeout(), Duration::from_secs(15));
213 assert_eq!(config.dns_timeout(), Duration::from_secs(5));
214 assert_eq!(config.http_timeout(), Duration::from_secs(10));
215 }
216
217 #[test]
218 fn clamp_bounds_rate_limit_ms() {
219 let mut config = SeerConfig::default();
222 config.bulk.rate_limit_ms = 10_000_000;
223 let config = config.clamped();
224 assert!(config.bulk.rate_limit_ms <= 60_000);
225 }
226}