1use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8use std::time::Duration;
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct DomainResult {
16 pub domain: String,
18
19 pub available: Option<bool>,
24
25 #[serde(skip_serializing_if = "Option::is_none")]
27 pub info: Option<DomainInfo>,
28
29 #[serde(skip_serializing_if = "Option::is_none")]
31 pub check_duration: Option<Duration>,
32
33 pub method_used: CheckMethod,
35
36 #[serde(skip_serializing_if = "Option::is_none")]
38 pub error_message: Option<String>,
39}
40
41#[derive(Debug, Clone, Serialize, Deserialize, Default)]
46pub struct DomainInfo {
47 #[serde(skip_serializing_if = "Option::is_none")]
49 pub registrar: Option<String>,
50
51 #[serde(skip_serializing_if = "Option::is_none")]
53 pub creation_date: Option<String>,
54
55 #[serde(skip_serializing_if = "Option::is_none")]
57 pub expiration_date: Option<String>,
58
59 pub status: Vec<String>,
61
62 #[serde(skip_serializing_if = "Option::is_none")]
64 pub updated_date: Option<String>,
65
66 pub nameservers: Vec<String>,
68}
69
70#[derive(Debug, Clone, Serialize, Deserialize)]
75pub struct CheckConfig {
76 pub concurrency: usize,
79
80 #[serde(skip)] pub timeout: Duration,
84
85 pub enable_whois_fallback: bool,
88
89 pub enable_bootstrap: bool,
92
93 pub detailed_info: bool,
96
97 pub tlds: Option<Vec<String>>,
100
101 #[serde(skip)] pub rdap_timeout: Duration,
105
106 #[serde(skip)] pub whois_timeout: Duration,
110
111 #[serde(skip)] pub custom_presets: HashMap<String, Vec<String>>,
115}
116
117#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
122pub enum CheckMethod {
123 #[serde(rename = "rdap")]
125 Rdap,
126
127 #[serde(rename = "whois")]
129 Whois,
130
131 #[serde(rename = "bootstrap")]
133 Bootstrap,
134
135 #[serde(rename = "unknown")]
137 Unknown,
138}
139
140#[derive(Debug, Clone, PartialEq)]
145pub enum OutputMode {
146 Streaming,
148
149 Collected,
151
152 Auto,
154}
155
156impl Default for CheckConfig {
157 fn default() -> Self {
162 Self {
163 concurrency: 20,
164 timeout: Duration::from_secs(5),
165 enable_whois_fallback: true,
166 enable_bootstrap: true,
167 detailed_info: false,
168 tlds: None, rdap_timeout: Duration::from_secs(3),
170 whois_timeout: Duration::from_secs(5),
171 custom_presets: HashMap::new(),
172 }
173 }
174}
175
176impl CheckConfig {
177 pub fn with_concurrency(mut self, concurrency: usize) -> Self {
181 self.concurrency = concurrency.clamp(1, 100);
182 self
183 }
184
185 pub fn with_timeout(mut self, timeout: Duration) -> Self {
187 self.timeout = timeout;
188 self
189 }
190
191 pub fn with_whois_fallback(mut self, enabled: bool) -> Self {
193 self.enable_whois_fallback = enabled;
194 self
195 }
196
197 pub fn with_bootstrap(mut self, enabled: bool) -> Self {
199 self.enable_bootstrap = enabled;
200 self
201 }
202
203 pub fn with_detailed_info(mut self, enabled: bool) -> Self {
205 self.detailed_info = enabled;
206 self
207 }
208
209 pub fn with_tlds(mut self, tlds: Vec<String>) -> Self {
211 self.tlds = Some(tlds);
212 self
213 }
214}
215
216#[derive(Debug, Clone, Default)]
221pub struct GenerateConfig {
222 pub patterns: Vec<String>,
225
226 pub prefixes: Vec<String>,
228
229 pub suffixes: Vec<String>,
231
232 pub include_bare: bool,
235}
236
237#[derive(Debug, Clone)]
239pub struct GenerationResult {
240 pub names: Vec<String>,
242
243 pub estimated_count: usize,
246}
247
248impl GenerateConfig {
249 pub fn new() -> Self {
251 Self {
252 patterns: Vec::new(),
253 prefixes: Vec::new(),
254 suffixes: Vec::new(),
255 include_bare: true,
256 }
257 }
258
259 pub fn has_generation(&self) -> bool {
261 !self.patterns.is_empty()
262 }
263
264 pub fn has_affixes(&self) -> bool {
266 !self.prefixes.is_empty() || !self.suffixes.is_empty()
267 }
268}
269
270impl std::fmt::Display for CheckMethod {
271 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
272 match self {
273 CheckMethod::Rdap => write!(f, "RDAP"),
274 CheckMethod::Whois => write!(f, "WHOIS"),
275 CheckMethod::Bootstrap => write!(f, "Bootstrap"),
276 CheckMethod::Unknown => write!(f, "Unknown"),
277 }
278 }
279}
280
281impl std::fmt::Display for OutputMode {
282 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
283 match self {
284 OutputMode::Streaming => write!(f, "Streaming"),
285 OutputMode::Collected => write!(f, "Collected"),
286 OutputMode::Auto => write!(f, "Auto"),
287 }
288 }
289}
290
291#[cfg(test)]
292mod tests {
293 use super::*;
294
295 #[test]
298 fn test_check_config_defaults() {
299 let config = CheckConfig::default();
300 assert_eq!(config.concurrency, 20);
301 assert_eq!(config.timeout, Duration::from_secs(5));
302 assert!(config.enable_whois_fallback);
303 assert!(config.enable_bootstrap);
304 assert!(!config.detailed_info);
305 assert!(config.tlds.is_none());
306 assert_eq!(config.rdap_timeout, Duration::from_secs(3));
307 assert_eq!(config.whois_timeout, Duration::from_secs(5));
308 assert!(config.custom_presets.is_empty());
309 }
310
311 #[test]
314 fn test_with_concurrency_normal() {
315 let config = CheckConfig::default().with_concurrency(50);
316 assert_eq!(config.concurrency, 50);
317 }
318
319 #[test]
320 fn test_with_concurrency_clamps_to_max() {
321 let config = CheckConfig::default().with_concurrency(200);
322 assert_eq!(config.concurrency, 100);
323 }
324
325 #[test]
326 fn test_with_concurrency_clamps_to_min() {
327 let config = CheckConfig::default().with_concurrency(0);
328 assert_eq!(config.concurrency, 1);
329 }
330
331 #[test]
332 fn test_with_concurrency_boundary_values() {
333 assert_eq!(CheckConfig::default().with_concurrency(1).concurrency, 1);
334 assert_eq!(
335 CheckConfig::default().with_concurrency(100).concurrency,
336 100
337 );
338 }
339
340 #[test]
341 fn test_with_timeout() {
342 let config = CheckConfig::default().with_timeout(Duration::from_secs(30));
343 assert_eq!(config.timeout, Duration::from_secs(30));
344 }
345
346 #[test]
347 fn test_with_whois_fallback() {
348 let config = CheckConfig::default().with_whois_fallback(false);
349 assert!(!config.enable_whois_fallback);
350 }
351
352 #[test]
353 fn test_with_bootstrap() {
354 let config = CheckConfig::default().with_bootstrap(false);
355 assert!(!config.enable_bootstrap);
356 }
357
358 #[test]
359 fn test_with_detailed_info() {
360 let config = CheckConfig::default().with_detailed_info(true);
361 assert!(config.detailed_info);
362 }
363
364 #[test]
365 fn test_with_tlds() {
366 let config = CheckConfig::default().with_tlds(vec!["com".into(), "org".into()]);
367 assert_eq!(
368 config.tlds,
369 Some(vec!["com".to_string(), "org".to_string()])
370 );
371 }
372
373 #[test]
374 fn test_builder_chaining_order_independent() {
375 let a = CheckConfig::default()
376 .with_concurrency(50)
377 .with_timeout(Duration::from_secs(10))
378 .with_bootstrap(false);
379
380 let b = CheckConfig::default()
381 .with_bootstrap(false)
382 .with_timeout(Duration::from_secs(10))
383 .with_concurrency(50);
384
385 assert_eq!(a.concurrency, b.concurrency);
386 assert_eq!(a.timeout, b.timeout);
387 assert_eq!(a.enable_bootstrap, b.enable_bootstrap);
388 }
389
390 #[test]
391 fn test_builder_preserves_other_defaults() {
392 let config = CheckConfig::default().with_concurrency(50);
393 assert_eq!(config.timeout, Duration::from_secs(5));
395 assert!(config.enable_whois_fallback);
396 assert!(config.enable_bootstrap);
397 assert!(!config.detailed_info);
398 assert!(config.tlds.is_none());
399 }
400
401 #[test]
404 fn test_generate_config_new_defaults() {
405 let config = GenerateConfig::new();
406 assert!(config.patterns.is_empty());
407 assert!(config.prefixes.is_empty());
408 assert!(config.suffixes.is_empty());
409 assert!(config.include_bare);
410 }
411
412 #[test]
413 fn test_generate_config_has_generation_empty() {
414 let config = GenerateConfig::new();
415 assert!(!config.has_generation());
416 }
417
418 #[test]
419 fn test_generate_config_has_generation_with_pattern() {
420 let mut config = GenerateConfig::new();
421 config.patterns.push("test\\d".to_string());
422 assert!(config.has_generation());
423 }
424
425 #[test]
426 fn test_generate_config_has_affixes_none() {
427 let config = GenerateConfig::new();
428 assert!(!config.has_affixes());
429 }
430
431 #[test]
432 fn test_generate_config_has_affixes_prefix_only() {
433 let mut config = GenerateConfig::new();
434 config.prefixes.push("get".to_string());
435 assert!(config.has_affixes());
436 }
437
438 #[test]
439 fn test_generate_config_has_affixes_suffix_only() {
440 let mut config = GenerateConfig::new();
441 config.suffixes.push("ly".to_string());
442 assert!(config.has_affixes());
443 }
444
445 #[test]
448 fn test_check_method_display() {
449 assert_eq!(format!("{}", CheckMethod::Rdap), "RDAP");
450 assert_eq!(format!("{}", CheckMethod::Whois), "WHOIS");
451 assert_eq!(format!("{}", CheckMethod::Bootstrap), "Bootstrap");
452 assert_eq!(format!("{}", CheckMethod::Unknown), "Unknown");
453 }
454
455 #[test]
456 fn test_output_mode_display() {
457 assert_eq!(format!("{}", OutputMode::Streaming), "Streaming");
458 assert_eq!(format!("{}", OutputMode::Collected), "Collected");
459 assert_eq!(format!("{}", OutputMode::Auto), "Auto");
460 }
461
462 #[test]
465 fn test_check_method_serialization() {
466 let json = serde_json::to_string(&CheckMethod::Rdap).unwrap();
467 assert_eq!(json, "\"rdap\"");
468 let json = serde_json::to_string(&CheckMethod::Whois).unwrap();
469 assert_eq!(json, "\"whois\"");
470 }
471
472 #[test]
473 fn test_check_method_deserialization() {
474 let method: CheckMethod = serde_json::from_str("\"bootstrap\"").unwrap();
475 assert_eq!(method, CheckMethod::Bootstrap);
476 }
477
478 #[test]
479 fn test_domain_result_json_skip_none_fields() {
480 let result = DomainResult {
481 domain: "test.com".to_string(),
482 available: Some(true),
483 info: None,
484 check_duration: None,
485 method_used: CheckMethod::Rdap,
486 error_message: None,
487 };
488 let json = serde_json::to_string(&result).unwrap();
489 assert!(!json.contains("info"));
491 assert!(!json.contains("check_duration"));
492 assert!(!json.contains("error_message"));
493 assert!(json.contains("\"domain\":\"test.com\""));
494 assert!(json.contains("\"available\":true"));
495 }
496
497 #[test]
498 fn test_domain_info_default() {
499 let info = DomainInfo::default();
500 assert!(info.registrar.is_none());
501 assert!(info.creation_date.is_none());
502 assert!(info.expiration_date.is_none());
503 assert!(info.status.is_empty());
504 assert!(info.updated_date.is_none());
505 assert!(info.nameservers.is_empty());
506 }
507}