Skip to main content

domain_check_lib/
types.rs

1//! Core data types for domain availability checking.
2//!
3//! This module defines all the main data structures used throughout the library,
4//! including domain results, configuration options, and output formatting.
5
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8use std::time::Duration;
9
10/// Result of a domain availability check.
11///
12/// Contains all information about a domain's availability status,
13/// registration details, and metadata about the check itself.
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct DomainResult {
16    /// The domain name that was checked (e.g., "example.com")
17    pub domain: String,
18
19    /// Whether the domain is available for registration.
20    /// - `Some(true)`: Domain is available
21    /// - `Some(false)`: Domain is taken/registered  
22    /// - `None`: Status could not be determined
23    pub available: Option<bool>,
24
25    /// Detailed registration information (only available for taken domains)
26    #[serde(skip_serializing_if = "Option::is_none")]
27    pub info: Option<DomainInfo>,
28
29    /// How long the domain check took to complete
30    #[serde(skip_serializing_if = "Option::is_none")]
31    pub check_duration: Option<Duration>,
32
33    /// Which method was used to check the domain
34    pub method_used: CheckMethod,
35
36    /// Any error message if the check failed
37    #[serde(skip_serializing_if = "Option::is_none")]
38    pub error_message: Option<String>,
39}
40
41/// Detailed information about a registered domain.
42///
43/// This information is typically extracted from RDAP responses
44/// and provides insights into the domain's registration details.
45#[derive(Debug, Clone, Serialize, Deserialize, Default)]
46pub struct DomainInfo {
47    /// The registrar that manages this domain
48    #[serde(skip_serializing_if = "Option::is_none")]
49    pub registrar: Option<String>,
50
51    /// When the domain was first registered
52    #[serde(skip_serializing_if = "Option::is_none")]
53    pub creation_date: Option<String>,
54
55    /// When the domain registration expires
56    #[serde(skip_serializing_if = "Option::is_none")]
57    pub expiration_date: Option<String>,
58
59    /// Domain status codes (e.g., "clientTransferProhibited")
60    pub status: Vec<String>,
61
62    /// Last update date of the domain record
63    #[serde(skip_serializing_if = "Option::is_none")]
64    pub updated_date: Option<String>,
65
66    /// Nameservers associated with the domain
67    pub nameservers: Vec<String>,
68}
69
70/// Configuration options for domain checking operations.
71///
72/// This struct allows fine-tuning of the domain checking behavior,
73/// including performance, timeout, and protocol preferences.
74#[derive(Debug, Clone, Serialize, Deserialize)]
75pub struct CheckConfig {
76    /// Maximum number of concurrent domain checks
77    /// Default: 10, Range: 1-100
78    pub concurrency: usize,
79
80    /// Timeout for each individual domain check
81    /// Default: 5 seconds
82    #[serde(skip)] // Don't serialize Duration directly
83    pub timeout: Duration,
84
85    /// Whether to automatically fall back to WHOIS when RDAP fails
86    /// Default: true
87    pub enable_whois_fallback: bool,
88
89    /// Whether to use IANA bootstrap registry for unknown TLDs
90    /// Default: false (uses built-in registry only)
91    pub enable_bootstrap: bool,
92
93    /// Whether to extract detailed domain information for taken domains
94    /// Default: false (just availability status)
95    pub detailed_info: bool,
96
97    /// List of TLDs to check for base domain names
98    /// If None, defaults to ["com"]
99    pub tlds: Option<Vec<String>>,
100
101    /// Custom timeout for RDAP requests (separate from overall timeout)
102    /// Default: 3 seconds
103    #[serde(skip)] // Don't serialize Duration directly
104    pub rdap_timeout: Duration,
105
106    /// Custom timeout for WHOIS requests
107    /// Default: 5 seconds  
108    #[serde(skip)] // Don't serialize Duration directly
109    pub whois_timeout: Duration,
110
111    /// Custom user-defined TLD presets from config files
112    /// Default: empty
113    #[serde(skip)] // Handled separately in config merging
114    pub custom_presets: HashMap<String, Vec<String>>,
115}
116
117/// Method used to check domain availability.
118///
119/// This helps users understand which protocol was used
120/// and can be useful for debugging or performance analysis.
121#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
122pub enum CheckMethod {
123    /// Domain checked via RDAP protocol
124    #[serde(rename = "rdap")]
125    Rdap,
126
127    /// Domain checked via WHOIS protocol
128    #[serde(rename = "whois")]
129    Whois,
130
131    /// RDAP endpoint discovered via IANA bootstrap registry
132    #[serde(rename = "bootstrap")]
133    Bootstrap,
134
135    /// Check failed or method unknown
136    #[serde(rename = "unknown")]
137    Unknown,
138}
139
140/// Output mode for displaying results.
141///
142/// This controls how and when results are presented to the user,
143/// affecting both performance perception and data formatting.
144#[derive(Debug, Clone, PartialEq)]
145pub enum OutputMode {
146    /// Stream results as they become available (good for interactive use)
147    Streaming,
148
149    /// Collect all results before displaying (good for formatting/sorting)
150    Collected,
151
152    /// Automatically choose based on context (terminal vs pipe, etc.)
153    Auto,
154}
155
156impl Default for CheckConfig {
157    /// Create a sensible default configuration.
158    ///
159    /// These defaults are chosen to work well for most use cases
160    /// while being conservative about resource usage.
161    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, // Will default to ["com"] when needed
169            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    /// Create a new configuration with custom concurrency.
178    ///
179    /// Automatically caps concurrency at 100 to prevent resource exhaustion.
180    pub fn with_concurrency(mut self, concurrency: usize) -> Self {
181        self.concurrency = concurrency.clamp(1, 100);
182        self
183    }
184
185    /// Set custom timeout for domain checks.
186    pub fn with_timeout(mut self, timeout: Duration) -> Self {
187        self.timeout = timeout;
188        self
189    }
190
191    /// Enable or disable WHOIS fallback.
192    pub fn with_whois_fallback(mut self, enabled: bool) -> Self {
193        self.enable_whois_fallback = enabled;
194        self
195    }
196
197    /// Enable or disable IANA bootstrap registry.
198    pub fn with_bootstrap(mut self, enabled: bool) -> Self {
199        self.enable_bootstrap = enabled;
200        self
201    }
202
203    /// Enable detailed domain information extraction.
204    pub fn with_detailed_info(mut self, enabled: bool) -> Self {
205        self.detailed_info = enabled;
206        self
207    }
208
209    /// Set TLDs to check for base domain names.
210    pub fn with_tlds(mut self, tlds: Vec<String>) -> Self {
211        self.tlds = Some(tlds);
212        self
213    }
214}
215
216/// Configuration for domain name generation.
217///
218/// Controls pattern expansion, prefix/suffix permutation, and the generation pipeline.
219/// Used by the `generate` module to produce base domain names before TLD expansion.
220#[derive(Debug, Clone, Default)]
221pub struct GenerateConfig {
222    /// Patterns to expand (e.g., "test\d\d", "app?")
223    /// Supports: \w (a-z + hyphen), \d (0-9), ? (alphanumeric + hyphen), literals
224    pub patterns: Vec<String>,
225
226    /// Prefixes to prepend to base names (e.g., ["get", "my", "try"])
227    pub prefixes: Vec<String>,
228
229    /// Suffixes to append to base names (e.g., ["hub", "ly", "ify"])
230    pub suffixes: Vec<String>,
231
232    /// Whether to include the bare base name when prefixes/suffixes are provided.
233    /// Default: true. When false, only affixed variants are generated.
234    pub include_bare: bool,
235}
236
237/// Result of the domain name generation pipeline.
238#[derive(Debug, Clone)]
239pub struct GenerationResult {
240    /// Generated base names (validated, ready for TLD expansion)
241    pub names: Vec<String>,
242
243    /// Pre-filter estimate of how many names the patterns would produce.
244    /// May be higher than `names.len()` due to validation filtering.
245    pub estimated_count: usize,
246}
247
248impl GenerateConfig {
249    /// Create a new GenerateConfig with default settings.
250    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    /// Returns true if this config would actually generate anything.
260    pub fn has_generation(&self) -> bool {
261        !self.patterns.is_empty()
262    }
263
264    /// Returns true if affixes are configured.
265    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    // ── CheckConfig defaults ────────────────────────────────────────────
296
297    #[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    // ── Builder methods ─────────────────────────────────────────────────
312
313    #[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        // Only concurrency changed; everything else should be default
394        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    // ── GenerateConfig ──────────────────────────────────────────────────
402
403    #[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    // ── Display impls ───────────────────────────────────────────────────
446
447    #[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    // ── Serialization ───────────────────────────────────────────────────
463
464    #[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        // None fields with skip_serializing_if should be absent
490        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}