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