rss_gen/
error.rs

1// Copyright © 2024 RSS Gen. All rights reserved.
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3
4// src/error.rs
5
6use log;
7use quick_xml;
8use std::string::FromUtf8Error;
9use thiserror::Error;
10
11/// Errors that can occur when generating or parsing RSS feeds.
12#[derive(Debug, Error)]
13#[non_exhaustive]
14pub enum RssError {
15    /// Error occurred while writing XML.
16    #[error("XML error occurred: {0}")]
17    XmlWriteError(#[from] quick_xml::Error),
18
19    /// Error occurred during XML parsing.
20    #[error("XML parse error occurred: {0}")]
21    XmlParseError(quick_xml::Error),
22
23    /// Error occurred during UTF-8 conversion.
24    #[error("UTF-8 conversion error occurred: {0}")]
25    Utf8Error(#[from] FromUtf8Error),
26
27    /// Error indicating a required field is missing.
28    #[error("A required field is missing: {0}")]
29    MissingField(String),
30
31    /// Error indicating a date parsing failure.
32    #[error("Date parse error: {0}")]
33    DateParseError(String),
34
35    /// General I/O error.
36    #[error("I/O error occurred: {0}")]
37    IoError(#[from] std::io::Error),
38
39    /// Error for invalid input data.
40    #[error("Invalid input data provided: {0}")]
41    InvalidInput(String),
42
43    /// Error for invalid URL provided.
44    #[error("Invalid URL provided: {0}")]
45    InvalidUrl(String),
46
47    /// Error for unknown XML elements encountered during parsing.
48    #[error("Unknown XML element found: {0}")]
49    UnknownElement(String),
50
51    /// Error for validation errors.
52    #[error("Validation errors: {0:?}")]
53    ValidationErrors(Vec<String>),
54
55    /// Error for date sort errors.
56    #[error("Date sort error: {0:?}")]
57    DateSortError(Vec<DateSortError>),
58
59    /// Error for item validation errors.
60    #[error("Item validation error: {0}")]
61    ItemValidationError(String),
62
63    /// Error for unknown field encountered during parsing.
64    #[error("Unknown field encountered: {0}")]
65    UnknownField(String),
66
67    /// Custom error for unforeseen scenarios.
68    #[error("Custom error: {0}")]
69    Custom(String),
70
71    /// Error for invalid RSS version.
72    #[error("Invalid RSS version: {0}")]
73    InvalidRssVersion(String),
74    // #[error("Unknown RSS element: {0}")]
75    // UnknownElement(String),
76
77    // #[error("XML parsing error: {0}")]
78    // XmlParseError(#[from] quick_xml::Error),
79
80    // #[error("IO error: {0}")]
81    // IoError(#[from] std::io::Error),
82}
83
84/// Represents a specific validation error.
85#[derive(Debug, Error)]
86#[non_exhaustive]
87#[error("Validation error: {message}")]
88pub struct ValidationError {
89    /// The field that failed validation.
90    pub field: String,
91    /// The error message.
92    pub message: String,
93}
94
95/// Represents a specific date sorting error.
96#[derive(Debug, Error)]
97#[non_exhaustive]
98#[error("Date sort error: {message}")]
99pub struct DateSortError {
100    /// The index of the item with the date sort error.
101    pub index: usize,
102    /// The error message.
103    pub message: String,
104}
105
106/// Result type for RSS operations.
107///
108/// This type alias provides a convenient way to return results from RSS operations,
109/// where the error type is always `RssError`.
110pub type Result<T> = std::result::Result<T, RssError>;
111
112impl RssError {
113    /// Creates a new `RssError::MissingField` error.
114    ///
115    /// # Arguments
116    ///
117    /// * `field_name` - The name of the missing field.
118    ///
119    /// # Returns
120    ///
121    /// Returns a new `RssError::MissingField` instance.
122    pub fn missing_field<S: Into<String>>(field_name: S) -> Self {
123        RssError::MissingField(field_name.into())
124    }
125
126    /// Creates a new `DateSortError`.
127    ///
128    /// # Arguments
129    ///
130    /// * `index` - The index of the item with the date sort error.
131    /// * `message` - The error message.
132    ///
133    /// # Returns
134    ///
135    /// Returns a new `DateSortError` instance.
136    pub fn date_sort_error<S: Into<String>>(
137        index: usize,
138        message: S,
139    ) -> DateSortError {
140        DateSortError {
141            index,
142            message: message.into(),
143        }
144    }
145
146    /// Creates a new `RssError::InvalidInput` error.
147    ///
148    /// # Arguments
149    ///
150    /// * `message` - A description of why the input is invalid.
151    ///
152    /// # Returns
153    ///
154    /// Returns a new `RssError::InvalidInput` instance.
155    pub fn invalid_input<S: Into<String>>(message: S) -> Self {
156        RssError::InvalidInput(message.into())
157    }
158
159    /// Creates a new `RssError::Custom` error.
160    ///
161    /// # Arguments
162    ///
163    /// * `message` - A custom error message.
164    ///
165    /// # Returns
166    ///
167    /// Returns a new `RssError::Custom` instance.
168    pub fn custom<S: Into<String>>(message: S) -> Self {
169        RssError::Custom(message.into())
170    }
171
172    /// Logs the error using the `log` crate.
173    ///
174    /// This method logs the error at the error level. It uses the `log` crate,
175    /// so the application using this library should configure a logger.
176    pub fn log(&self) {
177        log::error!("RSS Error occurred: {}", self);
178    }
179
180    /// Converts the `RssError` into an appropriate HTTP status code.
181    ///
182    /// This method is useful when the library is used in web services.
183    ///
184    /// # Returns
185    ///
186    /// Returns a `u16` representing an HTTP status code.
187    #[must_use]
188    pub fn to_http_status(&self) -> u16 {
189        match self {
190            // Combine all cases that map to 500
191            RssError::XmlWriteError(_)
192            | RssError::XmlParseError(_)
193            | RssError::Utf8Error(_)
194            | RssError::IoError(_)
195            | RssError::UnknownElement(_)
196            | RssError::DateSortError(_)
197            | RssError::UnknownField(_)
198            | RssError::Custom(_) => 500,
199
200            // Combine all cases that map to 400
201            RssError::MissingField(_)
202            | RssError::InvalidInput(_)
203            | RssError::DateParseError(_)
204            | RssError::InvalidUrl(_)
205            | RssError::ValidationErrors(_)
206            | RssError::ItemValidationError(_)
207            | RssError::InvalidRssVersion(_) => 400,
208        }
209    }
210}
211
212#[cfg(test)]
213mod tests {
214    use super::*;
215    use std::error::Error;
216    use std::io;
217
218    #[test]
219    fn test_rss_error_display() {
220        let error = RssError::missing_field("title");
221        assert_eq!(
222            error.to_string(),
223            "A required field is missing: title"
224        );
225    }
226
227    #[test]
228    fn test_xml_write_error() {
229        let xml_error = quick_xml::Error::Io(std::sync::Arc::new(
230            io::Error::new(io::ErrorKind::Other, "XML error"),
231        ));
232        let error = RssError::XmlWriteError(xml_error);
233        assert_eq!(
234            error.to_string(),
235            "XML error occurred: I/O error: XML error"
236        );
237    }
238
239    #[test]
240    fn test_utf8_error() {
241        let utf8_error =
242            String::from_utf8(vec![0, 159, 146, 150]).unwrap_err();
243        let error = RssError::Utf8Error(utf8_error);
244        assert_eq!(error.to_string(), "UTF-8 conversion error occurred: invalid utf-8 sequence of 1 bytes from index 1");
245    }
246
247    #[test]
248    fn test_io_error() {
249        let io_error =
250            io::Error::new(io::ErrorKind::NotFound, "File not found");
251        let error: RssError = io_error.into();
252        assert_eq!(
253            error.to_string(),
254            "I/O error occurred: File not found"
255        );
256    }
257
258    #[test]
259    fn test_error_is_send_and_sync() {
260        fn assert_send_sync<T: Send + Sync>() {}
261        assert_send_sync::<RssError>();
262    }
263
264    #[test]
265    fn test_error_source() {
266        let xml_error = quick_xml::Error::Io(std::sync::Arc::new(
267            io::Error::new(io::ErrorKind::NotFound, "File not found"),
268        ));
269        let error = RssError::XmlWriteError(xml_error);
270        assert!(error.source().is_some());
271
272        let io_error: RssError =
273            io::Error::new(io::ErrorKind::NotFound, "File not found")
274                .into();
275        assert!(io_error.source().is_some());
276    }
277
278    #[test]
279    fn test_missing_field_with_string() {
280        let error = RssError::missing_field(String::from("author"));
281        assert_eq!(
282            error.to_string(),
283            "A required field is missing: author"
284        );
285    }
286
287    #[test]
288    fn test_missing_field_with_str() {
289        let error = RssError::missing_field("description");
290        assert_eq!(
291            error.to_string(),
292            "A required field is missing: description"
293        );
294    }
295
296    #[test]
297    fn test_error_downcast() {
298        let error: Box<dyn Error> =
299            Box::new(RssError::missing_field("category"));
300        let downcast_result = error.downcast::<RssError>();
301        assert!(downcast_result.is_ok());
302    }
303
304    #[test]
305    fn test_invalid_input_error() {
306        let error = RssError::invalid_input("Invalid date format");
307        assert_eq!(
308            error.to_string(),
309            "Invalid input data provided: Invalid date format"
310        );
311    }
312
313    #[test]
314    fn test_custom_error() {
315        let error = RssError::custom("Unforeseen error occurred");
316        assert_eq!(
317            error.to_string(),
318            "Custom error: Unforeseen error occurred"
319        );
320    }
321
322    #[test]
323    fn test_to_http_status() {
324        assert_eq!(
325            RssError::missing_field("title").to_http_status(),
326            400
327        );
328        assert_eq!(
329            RssError::XmlWriteError(quick_xml::Error::Io(
330                std::sync::Arc::new(io::Error::new(
331                    io::ErrorKind::Other,
332                    "XML error"
333                ))
334            ))
335            .to_http_status(),
336            500
337        );
338        assert_eq!(
339            RssError::InvalidInput("Bad input".to_string())
340                .to_http_status(),
341            400
342        );
343    }
344
345    #[test]
346    fn test_validation_error() {
347        let error = ValidationError {
348            field: "some_field".to_string(),
349            message: "Invalid field".to_string(),
350        };
351        assert_eq!(
352            error.to_string(),
353            "Validation error: Invalid field"
354        );
355    }
356
357    #[test]
358    fn test_date_sort_error() {
359        let error = DateSortError {
360            index: 0,
361            message: "Invalid date".to_string(),
362        };
363        assert_eq!(error.to_string(), "Date sort error: Invalid date");
364    }
365
366    #[test]
367    fn test_missing_field_error() {
368        let rss_error = RssError::MissingField("title".to_string());
369
370        assert_eq!(
371            format!("{}", rss_error),
372            "A required field is missing: title"
373        );
374    }
375
376    #[test]
377    fn test_date_parse_error() {
378        let rss_error =
379            RssError::DateParseError("Invalid date format".to_string());
380
381        assert_eq!(
382            format!("{}", rss_error),
383            "Date parse error: Invalid date format"
384        );
385    }
386
387    #[test]
388    fn test_invalid_url_error() {
389        let rss_error =
390            RssError::InvalidUrl("https://invalid-url".to_string());
391
392        assert_eq!(
393            format!("{}", rss_error),
394            "Invalid URL provided: https://invalid-url"
395        );
396    }
397
398    #[test]
399    fn test_unknown_element_error() {
400        let rss_error =
401            RssError::UnknownElement("unknown-element".to_string());
402
403        assert_eq!(
404            format!("{}", rss_error),
405            "Unknown XML element found: unknown-element"
406        );
407    }
408
409    #[test]
410    fn test_validation_errors() {
411        let validation_errors = vec![
412            "Title is missing".to_string(),
413            "Invalid pub date".to_string(),
414        ];
415        let rss_error =
416            RssError::ValidationErrors(validation_errors.clone());
417
418        assert_eq!(
419            format!("{}", rss_error),
420            format!("Validation errors: {:?}", validation_errors)
421        );
422    }
423}