1use std::fmt;
7
8#[derive(Debug, Clone)]
13pub enum DomainCheckError {
14 InvalidDomain { domain: String, reason: String },
16
17 NetworkError {
19 message: String,
20 source: Option<String>,
21 },
22
23 RdapError {
25 domain: String,
26 message: String,
27 status_code: Option<u16>,
28 },
29
30 WhoisError { domain: String, message: String },
32
33 BootstrapError { tld: String, message: String },
35
36 ParseError {
38 message: String,
39 content: Option<String>,
40 },
41
42 ConfigError { message: String },
44
45 FileError { path: String, message: String },
47
48 Timeout {
50 operation: String,
51 duration: std::time::Duration,
52 },
53
54 RateLimited {
56 service: String,
57 message: String,
58 retry_after: Option<std::time::Duration>,
59 },
60
61 InvalidPattern { pattern: String, reason: String },
63
64 Internal { message: String },
66}
67
68impl DomainCheckError {
69 pub fn invalid_domain<D: Into<String>, R: Into<String>>(domain: D, reason: R) -> Self {
71 Self::InvalidDomain {
72 domain: domain.into(),
73 reason: reason.into(),
74 }
75 }
76
77 pub fn network<M: Into<String>>(message: M) -> Self {
79 Self::NetworkError {
80 message: message.into(),
81 source: None,
82 }
83 }
84
85 pub fn network_with_source<M: Into<String>, S: Into<String>>(message: M, source: S) -> Self {
87 Self::NetworkError {
88 message: message.into(),
89 source: Some(source.into()),
90 }
91 }
92
93 pub fn rdap<D: Into<String>, M: Into<String>>(domain: D, message: M) -> Self {
95 Self::RdapError {
96 domain: domain.into(),
97 message: message.into(),
98 status_code: None,
99 }
100 }
101
102 pub fn rdap_with_status<D: Into<String>, M: Into<String>>(
104 domain: D,
105 message: M,
106 status_code: u16,
107 ) -> Self {
108 Self::RdapError {
109 domain: domain.into(),
110 message: message.into(),
111 status_code: Some(status_code),
112 }
113 }
114
115 pub fn whois<D: Into<String>, M: Into<String>>(domain: D, message: M) -> Self {
117 Self::WhoisError {
118 domain: domain.into(),
119 message: message.into(),
120 }
121 }
122
123 pub fn bootstrap<T: Into<String>, M: Into<String>>(tld: T, message: M) -> Self {
125 Self::BootstrapError {
126 tld: tld.into(),
127 message: message.into(),
128 }
129 }
130
131 pub fn timeout<O: Into<String>>(operation: O, duration: std::time::Duration) -> Self {
133 Self::Timeout {
134 operation: operation.into(),
135 duration,
136 }
137 }
138
139 pub fn invalid_pattern<P: Into<String>, R: Into<String>>(pattern: P, reason: R) -> Self {
141 Self::InvalidPattern {
142 pattern: pattern.into(),
143 reason: reason.into(),
144 }
145 }
146
147 pub fn internal<M: Into<String>>(message: M) -> Self {
149 Self::Internal {
150 message: message.into(),
151 }
152 }
153
154 pub fn file_error<P: Into<String>, M: Into<String>>(path: P, message: M) -> Self {
156 Self::FileError {
157 path: path.into(),
158 message: message.into(),
159 }
160 }
161
162 pub fn indicates_available(&self) -> bool {
166 match self {
167 Self::RdapError {
168 status_code: Some(404),
169 ..
170 } => true,
171 Self::WhoisError { message, .. } => {
172 let msg = message.to_lowercase();
173 msg.contains("not found")
174 || msg.contains("no match")
175 || msg.contains("no data found")
176 || msg.contains("domain available")
177 }
178 _ => false,
179 }
180 }
181
182 pub fn is_retryable(&self) -> bool {
184 matches!(
185 self,
186 Self::NetworkError { .. }
187 | Self::Timeout { .. }
188 | Self::RateLimited { .. }
189 | Self::RdapError {
190 status_code: Some(500..=599),
191 ..
192 }
193 )
194 }
196}
197
198impl fmt::Display for DomainCheckError {
199 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
200 match self {
201 Self::InvalidDomain { domain, reason } => {
202 write!(f, "❌ '{}' is not a valid domain name: {}\n 💡 Try something like 'example.com' or use a different domain", domain, reason)
203 }
204 Self::NetworkError { message, source } => {
205 if message.to_lowercase().contains("connection") || message.to_lowercase().contains("connect") {
206 write!(f, "🌐 Cannot connect to the internet\n 💡 Please check your network connection and try again")
207 } else if message.to_lowercase().contains("timeout") {
208 write!(f, "⏱️ Request timed out\n 💡 Your internet connection may be slow. Try again or check fewer domains at once")
209 } else {
210 match source {
211 Some(_) => write!(f, "🌐 Network error: {}\n 💡 Please check your internet connection", message),
212 None => write!(f, "🌐 Network error: {}\n 💡 Please check your internet connection", message),
213 }
214 }
215 }
216 Self::RdapError { domain, message, status_code } => {
217 match status_code {
218 Some(404) => write!(f, "✅ {}: Domain appears to be available", domain),
219 Some(429) => write!(f, "⏳ {}: Registry is rate limiting requests\n 💡 Please wait a moment and try again", domain),
220 Some(500..=599) => write!(f, "⚠️ {}: Registry server is temporarily unavailable\n 💡 Trying backup method...", domain),
221 Some(code) => write!(f, "⚠️ {}: Registry returned error (HTTP {})\n 💡 This domain registry may be temporarily unavailable", domain, code),
222 None => write!(f, "⚠️ {}: {}\n 💡 Trying alternative checking method...", domain, message),
223 }
224 }
225 Self::WhoisError { domain, message } => {
226 if message.to_lowercase().contains("not found") || message.to_lowercase().contains("no match") {
227 write!(f, "✅ {}: Domain appears to be available", domain)
228 } else if message.to_lowercase().contains("rate limit") || message.to_lowercase().contains("too many") {
229 write!(f, "⏳ {}: WHOIS server is rate limiting requests\n 💡 Please wait a moment and try again", domain)
230 } else if message.to_lowercase().contains("whois") && message.to_lowercase().contains("not found") {
231 write!(f, "⚠️ {}: WHOIS command not found on this system\n 💡 Please install whois or use online domain checkers", domain)
232 } else {
233 write!(f, "⚠️ {}: WHOIS lookup failed\n 💡 This may indicate the domain is available or the server is busy", domain)
234 }
235 }
236 Self::BootstrapError { tld, message: _ } => {
237 write!(f, "❓ Unknown domain extension '.{}'\n 💡 This TLD may not support automated checking. Try manually checking with a registrar", tld)
238 }
239 Self::ParseError { message: _, content: _ } => {
240 write!(f, "⚠️ Unable to understand server response\n 💡 The domain registry may be experiencing issues. Please try again later")
241 }
242 Self::ConfigError { message } => {
243 write!(f, "⚙️ Configuration error: {}\n 💡 Please check your command line arguments or configuration file values", message)
244 }
245 Self::FileError { path, message } => {
246 if message.to_lowercase().contains("not found") || message.to_lowercase().contains("no such file") {
247 write!(f, "📁 File not found: {}\n 💡 Please check the file path and make sure the file exists", path)
248 } else if message.to_lowercase().contains("permission") {
249 write!(f, "🔒 Permission denied: {}\n 💡 Please check file permissions or try running with appropriate access", path)
250 } else if message.to_lowercase().contains("no valid domains") {
251 write!(f, "📄 No valid domains found in: {}\n 💡 Make sure the file contains domain names (one per line) and check the format", path)
252 } else {
253 write!(f, "📁 File error ({}): {}\n 💡 Please check the file and try again", path, message)
254 }
255 }
256 Self::Timeout { operation, duration } => {
257 write!(f, "⏱️ Operation timed out after {:?}: {}\n 💡 Try reducing the number of domains or check your internet connection", duration, operation)
258 }
259 Self::RateLimited { service, message, retry_after } => {
260 match retry_after {
261 Some(retry) => write!(f, "⏳ Rate limited by {}: {}\n 💡 Please wait {:?} and try again", service, message, retry),
262 None => write!(f, "⏳ Rate limited by {}: {}\n 💡 Please wait a moment and try again", service, message),
263 }
264 }
265 Self::InvalidPattern { pattern, reason } => {
266 write!(f, "⚙️ Invalid pattern '{}': {}\n 💡 Supported: \\w (letters+hyphen), \\d (digits), ? (alphanumeric), literal characters", pattern, reason)
267 }
268 Self::Internal { message } => {
269 write!(f, "🔧 Internal error: {}\n 💡 This is unexpected. Please try again or report this issue", message)
270 }
271 }
272 }
273}
274
275impl std::error::Error for DomainCheckError {}
276
277impl From<reqwest::Error> for DomainCheckError {
279 fn from(err: reqwest::Error) -> Self {
280 if err.is_timeout() {
281 Self::timeout("HTTP request", std::time::Duration::from_secs(30))
282 } else if err.is_connect() {
283 Self::network_with_source("Connection failed", err.to_string())
284 } else {
285 Self::network_with_source("HTTP request failed", err.to_string())
286 }
287 }
288}
289
290impl From<serde_json::Error> for DomainCheckError {
291 fn from(err: serde_json::Error) -> Self {
292 Self::ParseError {
293 message: format!("JSON parsing failed: {}", err),
294 content: None,
295 }
296 }
297}
298
299impl From<std::io::Error> for DomainCheckError {
300 fn from(err: std::io::Error) -> Self {
301 Self::Internal {
302 message: format!("I/O error: {}", err),
303 }
304 }
305}
306
307impl From<regex::Error> for DomainCheckError {
308 fn from(err: regex::Error) -> Self {
309 Self::Internal {
310 message: format!("Regex error: {}", err),
311 }
312 }
313}
314
315#[cfg(test)]
316mod tests {
317 use super::*;
318
319 #[test]
320 fn test_invalid_pattern_display() {
321 let err = DomainCheckError::invalid_pattern("test\\x", "unknown escape sequence '\\x'");
322 let msg = format!("{}", err);
323 assert!(msg.contains("test\\x"));
324 assert!(msg.contains("unknown escape sequence"));
325 assert!(msg.contains("\\w")); assert!(msg.contains("\\d")); }
328
329 #[test]
330 fn test_invalid_pattern_not_retryable() {
331 let err = DomainCheckError::invalid_pattern("bad", "reason");
332 assert!(!err.is_retryable());
333 }
334
335 #[test]
336 fn test_invalid_pattern_not_available() {
337 let err = DomainCheckError::invalid_pattern("bad", "reason");
338 assert!(!err.indicates_available());
339 }
340
341 #[test]
342 fn test_rdap_404_indicates_available() {
343 let err = DomainCheckError::rdap_with_status("test.com", "not found", 404);
344 assert!(err.indicates_available());
345 }
346
347 #[test]
348 fn test_network_error_is_retryable() {
349 let err = DomainCheckError::network("connection refused");
350 assert!(err.is_retryable());
351 }
352
353 #[test]
354 fn test_timeout_is_retryable() {
355 let err = DomainCheckError::timeout("test", std::time::Duration::from_secs(5));
356 assert!(err.is_retryable());
357 }
358
359 #[test]
360 fn test_config_error_not_retryable() {
361 let err = DomainCheckError::ConfigError {
362 message: "bad config".to_string(),
363 };
364 assert!(!err.is_retryable());
365 }
366
367 #[test]
368 fn test_rdap_500_is_retryable() {
369 let err = DomainCheckError::rdap_with_status("test.com", "server error", 500);
370 assert!(err.is_retryable());
371 }
372
373 #[test]
374 fn test_rdap_403_not_retryable() {
375 let err = DomainCheckError::rdap_with_status("test.com", "forbidden", 403);
376 assert!(!err.is_retryable());
377 }
378}