1use log;
7use quick_xml;
8use std::string::FromUtf8Error;
9use thiserror::Error;
10
11#[derive(Debug, Error)]
13#[non_exhaustive]
14pub enum RssError {
15 #[error("XML error occurred: {0}")]
17 XmlWriteError(#[from] quick_xml::Error),
18
19 #[error("XML parse error occurred: {0}")]
21 XmlParseError(quick_xml::Error),
22
23 #[error("UTF-8 conversion error occurred: {0}")]
25 Utf8Error(#[from] FromUtf8Error),
26
27 #[error("A required field is missing: {0}")]
29 MissingField(String),
30
31 #[error("Date parse error: {0}")]
33 DateParseError(String),
34
35 #[error("I/O error occurred: {0}")]
37 IoError(#[from] std::io::Error),
38
39 #[error("Invalid input data provided: {0}")]
41 InvalidInput(String),
42
43 #[error("Invalid URL provided: {0}")]
45 InvalidUrl(String),
46
47 #[error("Unknown XML element found: {0}")]
49 UnknownElement(String),
50
51 #[error("Validation errors: {0:?}")]
59 ValidationErrors(Vec<ValidationError>),
60
61 #[error("Date sort error: {0:?}")]
63 DateSortError(Vec<DateSortError>),
64
65 #[error("Item validation error: {0}")]
67 ItemValidationError(String),
68
69 #[error("Unknown field encountered: {0}")]
71 UnknownField(String),
72
73 #[error("Custom error: {0}")]
75 Custom(String),
76
77 #[error("Invalid RSS version: {0}")]
79 InvalidRssVersion(String),
80 }
89
90#[derive(Debug, Clone, PartialEq, Eq, Error)]
100#[non_exhaustive]
101#[error("{message}")]
102pub struct ValidationError {
103 pub field: String,
105 pub message: String,
109}
110
111impl ValidationError {
112 #[must_use]
119 pub fn new<F: Into<String>, M: Into<String>>(
120 field: F,
121 message: M,
122 ) -> Self {
123 Self {
124 field: field.into(),
125 message: message.into(),
126 }
127 }
128}
129
130#[derive(Debug, Error)]
132#[non_exhaustive]
133#[error("Date sort error: {message}")]
134pub struct DateSortError {
135 pub index: usize,
137 pub message: String,
139}
140
141pub type Result<T> = std::result::Result<T, RssError>;
146
147impl RssError {
148 pub fn missing_field<S: Into<String>>(field_name: S) -> Self {
158 RssError::MissingField(field_name.into())
159 }
160
161 pub fn date_sort_error<S: Into<String>>(
172 index: usize,
173 message: S,
174 ) -> DateSortError {
175 DateSortError {
176 index,
177 message: message.into(),
178 }
179 }
180
181 pub fn invalid_input<S: Into<String>>(message: S) -> Self {
191 RssError::InvalidInput(message.into())
192 }
193
194 pub fn custom<S: Into<String>>(message: S) -> Self {
204 RssError::Custom(message.into())
205 }
206
207 pub fn log(&self) {
212 log::error!("RSS Error occurred: {self}");
213 }
214
215 #[must_use]
223 pub fn to_http_status(&self) -> u16 {
224 match self {
225 RssError::XmlWriteError(_)
227 | RssError::XmlParseError(_)
228 | RssError::Utf8Error(_)
229 | RssError::IoError(_)
230 | RssError::UnknownElement(_)
231 | RssError::DateSortError(_)
232 | RssError::UnknownField(_)
233 | RssError::Custom(_) => 500,
234
235 RssError::MissingField(_)
237 | RssError::InvalidInput(_)
238 | RssError::DateParseError(_)
239 | RssError::InvalidUrl(_)
240 | RssError::ValidationErrors(_)
241 | RssError::ItemValidationError(_)
242 | RssError::InvalidRssVersion(_) => 400,
243 }
244 }
245}
246
247#[cfg(test)]
248mod tests {
249 use super::*;
250 use std::error::Error;
251 use std::io;
252
253 #[test]
254 fn test_rss_error_display() {
255 let error = RssError::missing_field("title");
256 assert_eq!(
257 error.to_string(),
258 "A required field is missing: title"
259 );
260 }
261
262 #[test]
263 fn test_xml_write_error() {
264 let xml_error = quick_xml::Error::Io(std::sync::Arc::new(
265 io::Error::other("XML error"),
266 ));
267 let error = RssError::XmlWriteError(xml_error);
268 assert_eq!(
269 error.to_string(),
270 "XML error occurred: I/O error: XML error"
271 );
272 }
273
274 #[test]
275 fn test_utf8_error() {
276 let utf8_error =
277 String::from_utf8(vec![0, 159, 146, 150]).unwrap_err();
278 let error = RssError::Utf8Error(utf8_error);
279 assert_eq!(error.to_string(), "UTF-8 conversion error occurred: invalid utf-8 sequence of 1 bytes from index 1");
280 }
281
282 #[test]
283 fn test_io_error() {
284 let io_error =
285 io::Error::new(io::ErrorKind::NotFound, "File not found");
286 let error: RssError = io_error.into();
287 assert_eq!(
288 error.to_string(),
289 "I/O error occurred: File not found"
290 );
291 }
292
293 #[test]
294 fn test_error_is_send_and_sync() {
295 fn assert_send_sync<T: Send + Sync>() {}
296 assert_send_sync::<RssError>();
297 }
298
299 #[test]
300 fn test_error_source() {
301 let xml_error = quick_xml::Error::Io(std::sync::Arc::new(
302 io::Error::new(io::ErrorKind::NotFound, "File not found"),
303 ));
304 let error = RssError::XmlWriteError(xml_error);
305 assert!(error.source().is_some());
306
307 let io_error: RssError =
308 io::Error::new(io::ErrorKind::NotFound, "File not found")
309 .into();
310 assert!(io_error.source().is_some());
311 }
312
313 #[test]
314 fn test_missing_field_with_string() {
315 let error = RssError::missing_field(String::from("author"));
316 assert_eq!(
317 error.to_string(),
318 "A required field is missing: author"
319 );
320 }
321
322 #[test]
323 fn test_missing_field_with_str() {
324 let error = RssError::missing_field("description");
325 assert_eq!(
326 error.to_string(),
327 "A required field is missing: description"
328 );
329 }
330
331 #[test]
332 fn test_error_downcast() {
333 let error: Box<dyn Error> =
334 Box::new(RssError::missing_field("category"));
335 let downcast_result = error.downcast::<RssError>();
336 assert!(downcast_result.is_ok());
337 }
338
339 #[test]
340 fn test_invalid_input_error() {
341 let error = RssError::invalid_input("Invalid date format");
342 assert_eq!(
343 error.to_string(),
344 "Invalid input data provided: Invalid date format"
345 );
346 }
347
348 #[test]
349 fn test_custom_error() {
350 let error = RssError::custom("Unforeseen error occurred");
351 assert_eq!(
352 error.to_string(),
353 "Custom error: Unforeseen error occurred"
354 );
355 }
356
357 #[test]
358 fn test_to_http_status() {
359 assert_eq!(
360 RssError::missing_field("title").to_http_status(),
361 400
362 );
363 assert_eq!(
364 RssError::XmlWriteError(quick_xml::Error::Io(
365 std::sync::Arc::new(io::Error::other("XML error"))
366 ))
367 .to_http_status(),
368 500
369 );
370 assert_eq!(
371 RssError::InvalidInput("Bad input".to_string())
372 .to_http_status(),
373 400
374 );
375 }
376
377 #[test]
378 fn test_validation_error() {
379 let error = ValidationError::new(
385 "channel.title",
386 "channel.title is missing",
387 );
388 assert_eq!(error.to_string(), "channel.title is missing");
389 assert_eq!(error.field, "channel.title");
390 assert_eq!(error.message, "channel.title is missing");
391 }
392
393 #[test]
394 fn test_date_sort_error() {
395 let error = DateSortError {
396 index: 0,
397 message: "Invalid date".to_string(),
398 };
399 assert_eq!(error.to_string(), "Date sort error: Invalid date");
400 }
401
402 #[test]
403 fn test_missing_field_error() {
404 let rss_error = RssError::MissingField("title".to_string());
405
406 assert_eq!(
407 format!("{rss_error}"),
408 "A required field is missing: title"
409 );
410 }
411
412 #[test]
413 fn test_date_parse_error() {
414 let rss_error =
415 RssError::DateParseError("Invalid date format".to_string());
416
417 assert_eq!(
418 format!("{rss_error}"),
419 "Date parse error: Invalid date format"
420 );
421 }
422
423 #[test]
424 fn test_invalid_url_error() {
425 let rss_error =
426 RssError::InvalidUrl("https://invalid-url".to_string());
427
428 assert_eq!(
429 format!("{rss_error}"),
430 "Invalid URL provided: https://invalid-url"
431 );
432 }
433
434 #[test]
435 fn test_unknown_element_error() {
436 let rss_error =
437 RssError::UnknownElement("unknown-element".to_string());
438
439 assert_eq!(
440 format!("{rss_error}"),
441 "Unknown XML element found: unknown-element"
442 );
443 }
444
445 #[test]
446 fn test_validation_errors() {
447 let validation_errors = vec![
448 ValidationError::new(
449 "channel.title",
450 "channel.title is missing",
451 ),
452 ValidationError::new(
453 "channel.pub_date",
454 "Invalid channel.pub_date: 2026/06/28",
455 ),
456 ];
457 let rss_error =
458 RssError::ValidationErrors(validation_errors.clone());
459
460 assert_eq!(
463 validation_errors[0].to_string(),
464 "channel.title is missing"
465 );
466 let rendered = format!("{rss_error}");
469 assert!(rendered.contains("channel.title"));
470 assert!(rendered.contains("Invalid channel.pub_date"));
471 }
472
473 #[test]
474 fn test_validation_error_field_is_accessible() {
475 let err = ValidationError::new(
476 "item.0.link",
477 "item.0.link is missing",
478 );
479 assert_eq!(err.field, "item.0.link");
480 assert_eq!(err.to_string(), "item.0.link is missing");
481 }
482
483 #[test]
484 fn test_error_log() {
485 let error = RssError::missing_field("title");
486 error.log();
488
489 let error = RssError::custom("something went wrong");
490 error.log();
491 }
492
493 #[test]
494 fn test_custom_error_http_status() {
495 assert_eq!(
496 RssError::Custom("err".to_string()).to_http_status(),
497 500
498 );
499 }
500
501 #[test]
502 fn test_date_sort_error_constructor() {
503 let error = RssError::date_sort_error(3, "dates out of order");
504 let DateSortError { index, message } = error;
505 assert_eq!(index, 3);
506 assert_eq!(message, "dates out of order");
507 }
508}