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 Internal { message: String },
63}
64
65impl DomainCheckError {
66 pub fn invalid_domain<D: Into<String>, R: Into<String>>(domain: D, reason: R) -> Self {
68 Self::InvalidDomain {
69 domain: domain.into(),
70 reason: reason.into(),
71 }
72 }
73
74 pub fn network<M: Into<String>>(message: M) -> Self {
76 Self::NetworkError {
77 message: message.into(),
78 source: None,
79 }
80 }
81
82 pub fn network_with_source<M: Into<String>, S: Into<String>>(message: M, source: S) -> Self {
84 Self::NetworkError {
85 message: message.into(),
86 source: Some(source.into()),
87 }
88 }
89
90 pub fn rdap<D: Into<String>, M: Into<String>>(domain: D, message: M) -> Self {
92 Self::RdapError {
93 domain: domain.into(),
94 message: message.into(),
95 status_code: None,
96 }
97 }
98
99 pub fn rdap_with_status<D: Into<String>, M: Into<String>>(
101 domain: D,
102 message: M,
103 status_code: u16,
104 ) -> Self {
105 Self::RdapError {
106 domain: domain.into(),
107 message: message.into(),
108 status_code: Some(status_code),
109 }
110 }
111
112 pub fn whois<D: Into<String>, M: Into<String>>(domain: D, message: M) -> Self {
114 Self::WhoisError {
115 domain: domain.into(),
116 message: message.into(),
117 }
118 }
119
120 pub fn bootstrap<T: Into<String>, M: Into<String>>(tld: T, message: M) -> Self {
122 Self::BootstrapError {
123 tld: tld.into(),
124 message: message.into(),
125 }
126 }
127
128 pub fn timeout<O: Into<String>>(operation: O, duration: std::time::Duration) -> Self {
130 Self::Timeout {
131 operation: operation.into(),
132 duration,
133 }
134 }
135
136 pub fn internal<M: Into<String>>(message: M) -> Self {
138 Self::Internal {
139 message: message.into(),
140 }
141 }
142
143 pub fn file_error<P: Into<String>, M: Into<String>>(path: P, message: M) -> Self {
145 Self::FileError {
146 path: path.into(),
147 message: message.into(),
148 }
149 }
150
151 pub fn indicates_available(&self) -> bool {
155 match self {
156 Self::RdapError {
157 status_code: Some(404),
158 ..
159 } => true,
160 Self::WhoisError { message, .. } => {
161 let msg = message.to_lowercase();
162 msg.contains("not found")
163 || msg.contains("no match")
164 || msg.contains("no data found")
165 || msg.contains("domain available")
166 }
167 _ => false,
168 }
169 }
170
171 pub fn is_retryable(&self) -> bool {
173 matches!(
174 self,
175 Self::NetworkError { .. }
176 | Self::Timeout { .. }
177 | Self::RateLimited { .. }
178 | Self::RdapError {
179 status_code: Some(500..=599),
180 ..
181 }
182 )
183 }
184}
185
186impl fmt::Display for DomainCheckError {
187 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
188 match self {
189 Self::InvalidDomain { domain, reason } => {
190 write!(f, "❌ '{}' is not a valid domain name: {}\n 💡 Try something like 'example.com' or use a different domain", domain, reason)
191 }
192 Self::NetworkError { message, source } => {
193 if message.to_lowercase().contains("connection") || message.to_lowercase().contains("connect") {
194 write!(f, "🌐 Cannot connect to the internet\n 💡 Please check your network connection and try again")
195 } else if message.to_lowercase().contains("timeout") {
196 write!(f, "⏱️ Request timed out\n 💡 Your internet connection may be slow. Try again or check fewer domains at once")
197 } else {
198 match source {
199 Some(_) => write!(f, "🌐 Network error: {}\n 💡 Please check your internet connection", message),
200 None => write!(f, "🌐 Network error: {}\n 💡 Please check your internet connection", message),
201 }
202 }
203 }
204 Self::RdapError { domain, message, status_code } => {
205 match status_code {
206 Some(404) => write!(f, "✅ {}: Domain appears to be available", domain),
207 Some(429) => write!(f, "⏳ {}: Registry is rate limiting requests\n 💡 Please wait a moment and try again", domain),
208 Some(500..=599) => write!(f, "⚠️ {}: Registry server is temporarily unavailable\n 💡 Trying backup method...", domain),
209 Some(code) => write!(f, "⚠️ {}: Registry returned error (HTTP {})\n 💡 This domain registry may be temporarily unavailable", domain, code),
210 None => write!(f, "⚠️ {}: {}\n 💡 Trying alternative checking method...", domain, message),
211 }
212 }
213 Self::WhoisError { domain, message } => {
214 if message.to_lowercase().contains("not found") || message.to_lowercase().contains("no match") {
215 write!(f, "✅ {}: Domain appears to be available", domain)
216 } else if message.to_lowercase().contains("rate limit") || message.to_lowercase().contains("too many") {
217 write!(f, "⏳ {}: WHOIS server is rate limiting requests\n 💡 Please wait a moment and try again", domain)
218 } else if message.to_lowercase().contains("whois") && message.to_lowercase().contains("not found") {
219 write!(f, "⚠️ {}: WHOIS command not found on this system\n 💡 Please install whois or use online domain checkers", domain)
220 } else {
221 write!(f, "⚠️ {}: WHOIS lookup failed\n 💡 This may indicate the domain is available or the server is busy", domain)
222 }
223 }
224 Self::BootstrapError { tld, message: _ } => {
225 write!(f, "❓ Unknown domain extension '.{}'\n 💡 This TLD may not support automated checking. Try manually checking with a registrar", tld)
226 }
227 Self::ParseError { message: _, content: _ } => {
228 write!(f, "⚠️ Unable to understand server response\n 💡 The domain registry may be experiencing issues. Please try again later")
229 }
230 Self::ConfigError { message } => {
231 write!(f, "⚙️ Configuration error: {}\n 💡 Please check your command line arguments or configuration file values", message)
232 }
233 Self::FileError { path, message } => {
234 if message.to_lowercase().contains("not found") || message.to_lowercase().contains("no such file") {
235 write!(f, "📁 File not found: {}\n 💡 Please check the file path and make sure the file exists", path)
236 } else if message.to_lowercase().contains("permission") {
237 write!(f, "🔒 Permission denied: {}\n 💡 Please check file permissions or try running with appropriate access", path)
238 } else if message.to_lowercase().contains("no valid domains") {
239 write!(f, "📄 No valid domains found in: {}\n 💡 Make sure the file contains domain names (one per line) and check the format", path)
240 } else {
241 write!(f, "📁 File error ({}): {}\n 💡 Please check the file and try again", path, message)
242 }
243 }
244 Self::Timeout { operation, duration } => {
245 write!(f, "⏱️ Operation timed out after {:?}: {}\n 💡 Try reducing the number of domains or check your internet connection", duration, operation)
246 }
247 Self::RateLimited { service, message, retry_after } => {
248 match retry_after {
249 Some(retry) => write!(f, "⏳ Rate limited by {}: {}\n 💡 Please wait {:?} and try again", service, message, retry),
250 None => write!(f, "⏳ Rate limited by {}: {}\n 💡 Please wait a moment and try again", service, message),
251 }
252 }
253 Self::Internal { message } => {
254 write!(f, "🔧 Internal error: {}\n 💡 This is unexpected. Please try again or report this issue", message)
255 }
256 }
257 }
258}
259
260impl std::error::Error for DomainCheckError {}
261
262impl From<reqwest::Error> for DomainCheckError {
264 fn from(err: reqwest::Error) -> Self {
265 if err.is_timeout() {
266 Self::timeout("HTTP request", std::time::Duration::from_secs(30))
267 } else if err.is_connect() {
268 Self::network_with_source("Connection failed", err.to_string())
269 } else {
270 Self::network_with_source("HTTP request failed", err.to_string())
271 }
272 }
273}
274
275impl From<serde_json::Error> for DomainCheckError {
276 fn from(err: serde_json::Error) -> Self {
277 Self::ParseError {
278 message: format!("JSON parsing failed: {}", err),
279 content: None,
280 }
281 }
282}
283
284impl From<std::io::Error> for DomainCheckError {
285 fn from(err: std::io::Error) -> Self {
286 Self::Internal {
287 message: format!("I/O error: {}", err),
288 }
289 }
290}
291
292impl From<regex::Error> for DomainCheckError {
293 fn from(err: regex::Error) -> Self {
294 Self::Internal {
295 message: format!("Regex error: {}", err),
296 }
297 }
298}