seer_core/
availability.rs1use serde::{Deserialize, Serialize};
7use tracing::debug;
8
9use crate::error::Result;
10use crate::rdap::RdapClient;
11use crate::whois::WhoisClient;
12
13#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct AvailabilityResult {
16 pub domain: String,
18 pub available: bool,
20 pub confidence: String,
22 pub method: String,
24 pub details: Option<String>,
26}
27
28#[derive(Debug, Clone)]
30pub struct AvailabilityChecker {
31 rdap_client: RdapClient,
32 whois_client: WhoisClient,
33}
34
35impl Default for AvailabilityChecker {
36 fn default() -> Self {
37 Self::new()
38 }
39}
40
41impl AvailabilityChecker {
42 pub fn new() -> Self {
43 Self {
44 rdap_client: RdapClient::new(),
45 whois_client: WhoisClient::new(),
46 }
47 }
48
49 pub async fn check(&self, domain: &str) -> Result<AvailabilityResult> {
51 let domain = crate::validation::normalize_domain(domain)?;
52 debug!(domain = %domain, "Checking domain availability");
53
54 match self.rdap_client.lookup_domain(&domain).await {
56 Ok(response) => {
57 let statuses: Vec<String> = response.status.clone();
59 let is_redemption = statuses
60 .iter()
61 .any(|s| s.contains("redemption") || s.contains("pending delete"));
62
63 if is_redemption {
64 return Ok(AvailabilityResult {
65 domain,
66 available: false,
67 confidence: "medium".to_string(),
68 method: "rdap".to_string(),
69 details: Some("Domain is in redemption/pending delete period".to_string()),
70 });
71 }
72
73 Ok(AvailabilityResult {
74 domain,
75 available: false,
76 confidence: "high".to_string(),
77 method: "rdap".to_string(),
78 details: Some(format!(
79 "Domain is registered (status: {})",
80 statuses.join(", ")
81 )),
82 })
83 }
84 Err(rdap_err) => {
85 debug!(error = %rdap_err, "RDAP lookup failed, falling back to WHOIS");
86 match self.whois_client.lookup(&domain).await {
87 Ok(whois_response) => {
88 if whois_response.is_available() {
89 Ok(AvailabilityResult {
90 domain,
91 available: true,
92 confidence: "high".to_string(),
93 method: "whois".to_string(),
94 details: Some(
95 "WHOIS indicates domain is not registered".to_string(),
96 ),
97 })
98 } else {
99 Ok(AvailabilityResult {
100 domain,
101 available: false,
102 confidence: "high".to_string(),
103 method: "whois".to_string(),
104 details: whois_response
105 .registrar
106 .map(|r| format!("Registered with {}", r)),
107 })
108 }
109 }
110 Err(whois_err) => {
111 let whois_msg = whois_err.to_string().to_lowercase();
113 let likely_available = whois_msg.contains("no match")
114 || whois_msg.contains("not found")
115 || whois_msg.contains("no data found")
116 || whois_msg.contains("no entries found");
117
118 if likely_available {
119 Ok(AvailabilityResult {
120 domain,
121 available: true,
122 confidence: "medium".to_string(),
123 method: "whois_error".to_string(),
124 details: Some(
125 "WHOIS server indicates no matching records".to_string(),
126 ),
127 })
128 } else {
129 Ok(AvailabilityResult {
130 domain,
131 available: false,
132 confidence: "low".to_string(),
133 method: "inconclusive".to_string(),
134 details: Some("Could not determine availability - both RDAP and WHOIS queries failed".to_string()),
135 })
136 }
137 }
138 }
139 }
140 }
141 }
142}
143
144#[cfg(test)]
145mod tests {
146 use super::*;
147
148 #[test]
149 fn test_availability_result_serialization() {
150 let result = AvailabilityResult {
151 domain: "example.com".to_string(),
152 available: false,
153 confidence: "high".to_string(),
154 method: "rdap".to_string(),
155 details: Some("Domain is registered".to_string()),
156 };
157 let json = serde_json::to_string(&result).unwrap();
158 assert!(json.contains("\"available\":false"));
159 assert!(json.contains("\"confidence\":\"high\""));
160 }
161}