seer_core/
availability.rs1use serde::{Deserialize, Serialize};
7use tracing::{debug, instrument};
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 #[instrument(skip(self), fields(domain = %domain))]
51 pub async fn check(&self, domain: &str) -> Result<AvailabilityResult> {
52 let domain = crate::validation::normalize_domain(domain)?;
53 debug!(domain = %domain, "Checking domain availability");
54
55 match self.rdap_client.lookup_domain(&domain).await {
57 Ok(response) => {
58 let statuses: Vec<String> = response.status.clone();
60 let is_redemption = statuses
61 .iter()
62 .any(|s| s.contains("redemption") || s.contains("pending delete"));
63
64 if is_redemption {
65 return Ok(AvailabilityResult {
66 domain,
67 available: false,
68 confidence: "medium".to_string(),
69 method: "rdap".to_string(),
70 details: Some("Domain is in redemption/pending delete period".to_string()),
71 });
72 }
73
74 Ok(AvailabilityResult {
75 domain,
76 available: false,
77 confidence: "high".to_string(),
78 method: "rdap".to_string(),
79 details: Some(format!(
80 "Domain is registered (status: {})",
81 statuses.join(", ")
82 )),
83 })
84 }
85 Err(rdap_err) => {
86 debug!(error = %rdap_err, "RDAP lookup failed, falling back to WHOIS");
87 match self.whois_client.lookup(&domain).await {
88 Ok(whois_response) => {
89 if whois_response.is_available() {
90 Ok(AvailabilityResult {
91 domain,
92 available: true,
93 confidence: "high".to_string(),
94 method: "whois".to_string(),
95 details: Some(
96 "WHOIS indicates domain is not registered".to_string(),
97 ),
98 })
99 } else {
100 Ok(AvailabilityResult {
101 domain,
102 available: false,
103 confidence: "high".to_string(),
104 method: "whois".to_string(),
105 details: whois_response
106 .registrar
107 .map(|r| format!("Registered with {}", r)),
108 })
109 }
110 }
111 Err(whois_err) => {
112 let whois_msg = whois_err.to_string().to_lowercase();
114 let likely_available = whois_msg.contains("no match")
115 || whois_msg.contains("not found")
116 || whois_msg.contains("no data found")
117 || whois_msg.contains("no entries found");
118
119 if likely_available {
120 Ok(AvailabilityResult {
121 domain,
122 available: true,
123 confidence: "medium".to_string(),
124 method: "whois_error".to_string(),
125 details: Some(
126 "WHOIS server indicates no matching records".to_string(),
127 ),
128 })
129 } else {
130 let rdap_detail = rdap_err.to_string();
136 let whois_detail = whois_err.to_string();
137 Ok(AvailabilityResult {
138 domain,
139 available: false,
140 confidence: "none".to_string(),
141 method: "inconclusive".to_string(),
142 details: Some(format!(
143 "Could not determine availability. RDAP: {}. WHOIS: {}",
144 rdap_detail, whois_detail
145 )),
146 })
147 }
148 }
149 }
150 }
151 }
152 }
153}
154
155#[cfg(test)]
156mod tests {
157 use super::*;
158
159 #[test]
160 fn test_availability_result_serialization() {
161 let result = AvailabilityResult {
162 domain: "example.com".to_string(),
163 available: false,
164 confidence: "high".to_string(),
165 method: "rdap".to_string(),
166 details: Some("Domain is registered".to_string()),
167 };
168 let json = serde_json::to_string(&result).unwrap();
169 assert!(json.contains("\"available\":false"));
170 assert!(json.contains("\"confidence\":\"high\""));
171 }
172}