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(&content) {
96 Ok(config) => {
97 debug!(?path, "Loaded config");
98 config
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 pub fn whois_timeout(&self) -> Duration {
114 Duration::from_secs(self.timeouts.whois_secs)
115 }
116
117 pub fn rdap_timeout(&self) -> Duration {
119 Duration::from_secs(self.timeouts.rdap_secs)
120 }
121
122 pub fn dns_timeout(&self) -> Duration {
124 Duration::from_secs(self.timeouts.dns_secs)
125 }
126
127 pub fn http_timeout(&self) -> Duration {
129 Duration::from_secs(self.timeouts.http_secs)
130 }
131
132 pub fn default_toml() -> String {
134 toml::to_string_pretty(&Self::default()).unwrap_or_else(|_| String::new())
135 }
136}
137
138#[cfg(test)]
139mod tests {
140 use super::*;
141
142 #[test]
143 fn test_default_config() {
144 let config = SeerConfig::default();
145 assert_eq!(config.output_format, "human");
146 assert_eq!(config.timeouts.whois_secs, 15);
147 assert_eq!(config.timeouts.rdap_secs, 30);
148 assert_eq!(config.timeouts.dns_secs, 5);
149 assert_eq!(config.bulk.concurrency, 10);
150 }
151
152 #[test]
153 fn test_parse_config_toml() {
154 let toml_str = r#"
155output_format = "json"
156nameserver = "1.1.1.1"
157
158[timeouts]
159whois_secs = 20
160rdap_secs = 45
161
162[bulk]
163concurrency = 20
164"#;
165 let config: SeerConfig = toml::from_str(toml_str).unwrap();
166 assert_eq!(config.output_format, "json");
167 assert_eq!(config.nameserver, Some("1.1.1.1".to_string()));
168 assert_eq!(config.timeouts.whois_secs, 20);
169 assert_eq!(config.timeouts.rdap_secs, 45);
170 assert_eq!(config.timeouts.dns_secs, 5); assert_eq!(config.bulk.concurrency, 20);
172 }
173
174 #[test]
175 fn test_default_toml_roundtrip() {
176 let toml_str = SeerConfig::default_toml();
177 let config: SeerConfig = toml::from_str(&toml_str).unwrap();
178 assert_eq!(config.output_format, "human");
179 }
180
181 #[test]
182 fn test_timeout_durations() {
183 let config = SeerConfig::default();
184 assert_eq!(config.whois_timeout(), Duration::from_secs(15));
185 assert_eq!(config.rdap_timeout(), Duration::from_secs(30));
186 assert_eq!(config.dns_timeout(), Duration::from_secs(5));
187 assert_eq!(config.http_timeout(), Duration::from_secs(10));
188 }
189}