use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::time::Duration;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DomainResult {
pub domain: String,
pub available: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub info: Option<DomainInfo>,
#[serde(skip_serializing_if = "Option::is_none")]
pub check_duration: Option<Duration>,
pub method_used: CheckMethod,
#[serde(skip_serializing_if = "Option::is_none")]
pub error_message: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct DomainInfo {
#[serde(skip_serializing_if = "Option::is_none")]
pub registrar: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub creation_date: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub expiration_date: Option<String>,
pub status: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub updated_date: Option<String>,
pub nameservers: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CheckConfig {
pub concurrency: usize,
#[serde(skip)] pub timeout: Duration,
pub enable_whois_fallback: bool,
pub enable_bootstrap: bool,
pub detailed_info: bool,
pub tlds: Option<Vec<String>>,
#[serde(skip)] pub rdap_timeout: Duration,
#[serde(skip)] pub whois_timeout: Duration,
#[serde(skip)] pub custom_presets: HashMap<String, Vec<String>>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum CheckMethod {
#[serde(rename = "rdap")]
Rdap,
#[serde(rename = "whois")]
Whois,
#[serde(rename = "bootstrap")]
Bootstrap,
#[serde(rename = "unknown")]
Unknown,
}
#[derive(Debug, Clone, PartialEq)]
pub enum OutputMode {
Streaming,
Collected,
Auto,
}
impl Default for CheckConfig {
fn default() -> Self {
Self {
concurrency: 20,
timeout: Duration::from_secs(5),
enable_whois_fallback: true,
enable_bootstrap: true,
detailed_info: false,
tlds: None, rdap_timeout: Duration::from_secs(3),
whois_timeout: Duration::from_secs(5),
custom_presets: HashMap::new(),
}
}
}
impl CheckConfig {
pub fn with_concurrency(mut self, concurrency: usize) -> Self {
self.concurrency = concurrency.clamp(1, 100);
self
}
pub fn with_timeout(mut self, timeout: Duration) -> Self {
self.timeout = timeout;
self
}
pub fn with_whois_fallback(mut self, enabled: bool) -> Self {
self.enable_whois_fallback = enabled;
self
}
pub fn with_bootstrap(mut self, enabled: bool) -> Self {
self.enable_bootstrap = enabled;
self
}
pub fn with_detailed_info(mut self, enabled: bool) -> Self {
self.detailed_info = enabled;
self
}
pub fn with_tlds(mut self, tlds: Vec<String>) -> Self {
self.tlds = Some(tlds);
self
}
}
#[derive(Debug, Clone, Default)]
pub struct GenerateConfig {
pub patterns: Vec<String>,
pub prefixes: Vec<String>,
pub suffixes: Vec<String>,
pub include_bare: bool,
}
#[derive(Debug, Clone)]
pub struct GenerationResult {
pub names: Vec<String>,
pub estimated_count: usize,
}
impl GenerateConfig {
pub fn new() -> Self {
Self {
patterns: Vec::new(),
prefixes: Vec::new(),
suffixes: Vec::new(),
include_bare: true,
}
}
pub fn has_generation(&self) -> bool {
!self.patterns.is_empty()
}
pub fn has_affixes(&self) -> bool {
!self.prefixes.is_empty() || !self.suffixes.is_empty()
}
}
impl std::fmt::Display for CheckMethod {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
CheckMethod::Rdap => write!(f, "RDAP"),
CheckMethod::Whois => write!(f, "WHOIS"),
CheckMethod::Bootstrap => write!(f, "Bootstrap"),
CheckMethod::Unknown => write!(f, "Unknown"),
}
}
}
impl std::fmt::Display for OutputMode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
OutputMode::Streaming => write!(f, "Streaming"),
OutputMode::Collected => write!(f, "Collected"),
OutputMode::Auto => write!(f, "Auto"),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_check_config_defaults() {
let config = CheckConfig::default();
assert_eq!(config.concurrency, 20);
assert_eq!(config.timeout, Duration::from_secs(5));
assert!(config.enable_whois_fallback);
assert!(config.enable_bootstrap);
assert!(!config.detailed_info);
assert!(config.tlds.is_none());
assert_eq!(config.rdap_timeout, Duration::from_secs(3));
assert_eq!(config.whois_timeout, Duration::from_secs(5));
assert!(config.custom_presets.is_empty());
}
#[test]
fn test_with_concurrency_normal() {
let config = CheckConfig::default().with_concurrency(50);
assert_eq!(config.concurrency, 50);
}
#[test]
fn test_with_concurrency_clamps_to_max() {
let config = CheckConfig::default().with_concurrency(200);
assert_eq!(config.concurrency, 100);
}
#[test]
fn test_with_concurrency_clamps_to_min() {
let config = CheckConfig::default().with_concurrency(0);
assert_eq!(config.concurrency, 1);
}
#[test]
fn test_with_concurrency_boundary_values() {
assert_eq!(CheckConfig::default().with_concurrency(1).concurrency, 1);
assert_eq!(
CheckConfig::default().with_concurrency(100).concurrency,
100
);
}
#[test]
fn test_with_timeout() {
let config = CheckConfig::default().with_timeout(Duration::from_secs(30));
assert_eq!(config.timeout, Duration::from_secs(30));
}
#[test]
fn test_with_whois_fallback() {
let config = CheckConfig::default().with_whois_fallback(false);
assert!(!config.enable_whois_fallback);
}
#[test]
fn test_with_bootstrap() {
let config = CheckConfig::default().with_bootstrap(false);
assert!(!config.enable_bootstrap);
}
#[test]
fn test_with_detailed_info() {
let config = CheckConfig::default().with_detailed_info(true);
assert!(config.detailed_info);
}
#[test]
fn test_with_tlds() {
let config = CheckConfig::default().with_tlds(vec!["com".into(), "org".into()]);
assert_eq!(
config.tlds,
Some(vec!["com".to_string(), "org".to_string()])
);
}
#[test]
fn test_builder_chaining_order_independent() {
let a = CheckConfig::default()
.with_concurrency(50)
.with_timeout(Duration::from_secs(10))
.with_bootstrap(false);
let b = CheckConfig::default()
.with_bootstrap(false)
.with_timeout(Duration::from_secs(10))
.with_concurrency(50);
assert_eq!(a.concurrency, b.concurrency);
assert_eq!(a.timeout, b.timeout);
assert_eq!(a.enable_bootstrap, b.enable_bootstrap);
}
#[test]
fn test_builder_preserves_other_defaults() {
let config = CheckConfig::default().with_concurrency(50);
assert_eq!(config.timeout, Duration::from_secs(5));
assert!(config.enable_whois_fallback);
assert!(config.enable_bootstrap);
assert!(!config.detailed_info);
assert!(config.tlds.is_none());
}
#[test]
fn test_generate_config_new_defaults() {
let config = GenerateConfig::new();
assert!(config.patterns.is_empty());
assert!(config.prefixes.is_empty());
assert!(config.suffixes.is_empty());
assert!(config.include_bare);
}
#[test]
fn test_generate_config_has_generation_empty() {
let config = GenerateConfig::new();
assert!(!config.has_generation());
}
#[test]
fn test_generate_config_has_generation_with_pattern() {
let mut config = GenerateConfig::new();
config.patterns.push("test\\d".to_string());
assert!(config.has_generation());
}
#[test]
fn test_generate_config_has_affixes_none() {
let config = GenerateConfig::new();
assert!(!config.has_affixes());
}
#[test]
fn test_generate_config_has_affixes_prefix_only() {
let mut config = GenerateConfig::new();
config.prefixes.push("get".to_string());
assert!(config.has_affixes());
}
#[test]
fn test_generate_config_has_affixes_suffix_only() {
let mut config = GenerateConfig::new();
config.suffixes.push("ly".to_string());
assert!(config.has_affixes());
}
#[test]
fn test_check_method_display() {
assert_eq!(format!("{}", CheckMethod::Rdap), "RDAP");
assert_eq!(format!("{}", CheckMethod::Whois), "WHOIS");
assert_eq!(format!("{}", CheckMethod::Bootstrap), "Bootstrap");
assert_eq!(format!("{}", CheckMethod::Unknown), "Unknown");
}
#[test]
fn test_output_mode_display() {
assert_eq!(format!("{}", OutputMode::Streaming), "Streaming");
assert_eq!(format!("{}", OutputMode::Collected), "Collected");
assert_eq!(format!("{}", OutputMode::Auto), "Auto");
}
#[test]
fn test_check_method_serialization() {
let json = serde_json::to_string(&CheckMethod::Rdap).unwrap();
assert_eq!(json, "\"rdap\"");
let json = serde_json::to_string(&CheckMethod::Whois).unwrap();
assert_eq!(json, "\"whois\"");
}
#[test]
fn test_check_method_deserialization() {
let method: CheckMethod = serde_json::from_str("\"bootstrap\"").unwrap();
assert_eq!(method, CheckMethod::Bootstrap);
}
#[test]
fn test_domain_result_json_skip_none_fields() {
let result = DomainResult {
domain: "test.com".to_string(),
available: Some(true),
info: None,
check_duration: None,
method_used: CheckMethod::Rdap,
error_message: None,
};
let json = serde_json::to_string(&result).unwrap();
assert!(!json.contains("info"));
assert!(!json.contains("check_duration"));
assert!(!json.contains("error_message"));
assert!(json.contains("\"domain\":\"test.com\""));
assert!(json.contains("\"available\":true"));
}
#[test]
fn test_domain_info_default() {
let info = DomainInfo::default();
assert!(info.registrar.is_none());
assert!(info.creation_date.is_none());
assert!(info.expiration_date.is_none());
assert!(info.status.is_empty());
assert!(info.updated_date.is_none());
assert!(info.nameservers.is_empty());
}
}