Skip to content

Commit

Permalink
refactor(ssg): 🎨 allow RssError to be cloneable while maintaining t…
Browse files Browse the repository at this point in the history
…he essential error information
  • Loading branch information
sebastienrousseau committed Sep 24, 2024
1 parent 035e138 commit 8ef2809
Showing 1 changed file with 135 additions and 75 deletions.
210 changes: 135 additions & 75 deletions ssg-rss/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,57 +2,151 @@
// SPDX-License-Identifier: Apache-2.0 OR MIT

use quick_xml;
use std::io;
use std::error::Error;
use std::fmt;
use std::string::FromUtf8Error;
use thiserror::Error;

/// Errors that can occur when generating RSS feeds.
#[derive(Error, Debug)]
///
/// This enum represents various error types that may occur during RSS feed generation,
/// including XML writing errors, UTF-8 conversion errors, missing required fields,
/// and general I/O errors.
#[derive(Debug)]

Check warning on line 14 in ssg-rss/src/error.rs

View check run for this annotation

Codecov / codecov/patch

ssg-rss/src/error.rs#L14

Added line #L14 was not covered by tests
pub enum RssError {
/// Error occurred while writing XML.
#[error("XML writing error: {0}")]
XmlWriteError(#[from] quick_xml::Error),
XmlWriteError(quick_xml::Error),

Check warning on line 17 in ssg-rss/src/error.rs

View check run for this annotation

Codecov / codecov/patch

ssg-rss/src/error.rs#L17

Added line #L17 was not covered by tests

/// Error occurred during UTF-8 conversion.
#[error("UTF-8 conversion error: {0}")]
Utf8Error(#[from] FromUtf8Error),
Utf8Error(FromUtf8Error),

Check warning on line 20 in ssg-rss/src/error.rs

View check run for this annotation

Codecov / codecov/patch

ssg-rss/src/error.rs#L20

Added line #L20 was not covered by tests

/// Error indicating a required field is missing.
#[error("Missing required field: {0}")]
MissingField(String),

Check warning on line 23 in ssg-rss/src/error.rs

View check run for this annotation

Codecov / codecov/patch

ssg-rss/src/error.rs#L23

Added line #L23 was not covered by tests

/// Error for any I/O operations.
#[error("I/O error: {0}")]
IoError(#[from] io::Error),
IoError(String),

Check warning on line 26 in ssg-rss/src/error.rs

View check run for this annotation

Codecov / codecov/patch

ssg-rss/src/error.rs#L26

Added line #L26 was not covered by tests

/// Error for invalid input data.
InvalidInput,
}

/// Custom implementation to avoid leaking sensitive information in error messages
impl fmt::Display for RssError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
RssError::XmlWriteError(_) => {
write!(f, "XML writing error occurred")
}
RssError::Utf8Error(_) => {
write!(f, "UTF-8 conversion error occurred")
}
RssError::MissingField(_) => {
write!(f, "A required field is missing")
}
RssError::IoError(_) => write!(f, "An I/O error occurred"),
RssError::InvalidInput => {
write!(f, "Invalid input data provided")
}
}
}
}

impl Error for RssError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
RssError::XmlWriteError(e) => Some(e),
RssError::Utf8Error(e) => Some(e),

Check warning on line 57 in ssg-rss/src/error.rs

View check run for this annotation

Codecov / codecov/patch

ssg-rss/src/error.rs#L57

Added line #L57 was not covered by tests
RssError::IoError(_) => None,
RssError::MissingField(_) | RssError::InvalidInput => None,

Check warning on line 59 in ssg-rss/src/error.rs

View check run for this annotation

Codecov / codecov/patch

ssg-rss/src/error.rs#L59

Added line #L59 was not covered by tests
}
}
}

impl Clone for RssError {
fn clone(&self) -> Self {
match self {
RssError::XmlWriteError(e) => {
RssError::XmlWriteError(e.clone())
}
RssError::Utf8Error(e) => RssError::Utf8Error(e.clone()),

Check warning on line 70 in ssg-rss/src/error.rs

View check run for this annotation

Codecov / codecov/patch

ssg-rss/src/error.rs#L67-L70

Added lines #L67 - L70 were not covered by tests
RssError::MissingField(s) => {
RssError::MissingField(s.clone())
}
RssError::IoError(s) => RssError::IoError(s.clone()),
RssError::InvalidInput => RssError::InvalidInput,

Check warning on line 75 in ssg-rss/src/error.rs

View check run for this annotation

Codecov / codecov/patch

ssg-rss/src/error.rs#L74-L75

Added lines #L74 - L75 were not covered by tests
}
}
}

/// Result type for RSS operations.
///
/// This type alias provides a convenient way to return results from RSS operations,
/// where the error type is always `RssError`.
pub type Result<T> = std::result::Result<T, RssError>;

impl RssError {
/// Creates a new `RssError::MissingField` error.
///
/// This method provides a convenient way to create a `MissingField` error
/// with a given field name.
///
/// # Arguments
///
/// * `field_name` - The name of the missing field.
///
/// # Returns
///
/// Returns a new `RssError::MissingField` instance.
///
/// # Examples
///
/// ```
/// use ssg_rss::error::RssError;
///
/// let error = RssError::missing_field("title");
/// assert_eq!(error.to_string(), "A required field is missing");
/// ```
pub fn missing_field<S: Into<String>>(field_name: S) -> Self {
RssError::MissingField(field_name.into())
}

/// Securely logs an error without exposing sensitive details.
///
/// This method should be used to log errors in a way that doesn't reveal
/// sensitive information to log files or monitoring systems.
pub fn log(&self) {

Check warning on line 116 in ssg-rss/src/error.rs

View check run for this annotation

Codecov / codecov/patch

ssg-rss/src/error.rs#L116

Added line #L116 was not covered by tests
// Implement secure logging here. For example:
// log::error!("RSS Error occurred: {}", self);
}

Check warning on line 119 in ssg-rss/src/error.rs

View check run for this annotation

Codecov / codecov/patch

ssg-rss/src/error.rs#L119

Added line #L119 was not covered by tests
}

// Implement From for RssError to allow ? operator usage
impl From<std::string::FromUtf8Error> for RssError {
fn from(error: std::string::FromUtf8Error) -> Self {
RssError::Utf8Error(error)
}
}

impl From<quick_xml::Error> for RssError {
fn from(error: quick_xml::Error) -> Self {
RssError::XmlWriteError(error)
}
}

impl From<std::io::Error> for RssError {
fn from(error: std::io::Error) -> Self {
RssError::IoError(error.to_string())
}
}

#[cfg(test)]
mod tests {
use super::*;
use std::error::Error;
use std::io;

#[test]
fn test_rss_error_display() {
let error = RssError::missing_field("title");
assert_eq!(error.to_string(), "Missing required field: title");
assert_eq!(error.to_string(), "A required field is missing");
}

#[test]
Expand All @@ -61,25 +155,26 @@ mod tests {
io::Error::new(io::ErrorKind::Other, "XML error"),
));
let error = RssError::XmlWriteError(xml_error);
assert!(error.to_string().starts_with("XML writing error:"));
assert_eq!(error.to_string(), "XML writing error occurred");
}

#[test]
fn test_utf8_error() {
let utf8_error =
String::from_utf8(vec![0, 159, 146, 150]).unwrap_err();
let error = RssError::Utf8Error(utf8_error);
assert!(error
.to_string()
.starts_with("UTF-8 conversion error:"));
assert_eq!(
error.to_string(),
"UTF-8 conversion error occurred"
);
}

#[test]
fn test_io_error() {
let io_error =
io::Error::new(io::ErrorKind::NotFound, "File not found");
let error = RssError::IoError(io_error);
assert!(error.to_string().starts_with("I/O error:"));
let error = RssError::IoError(io_error.to_string());
assert_eq!(error.to_string(), "An I/O error occurred");
}

#[test]
Expand All @@ -90,56 +185,26 @@ mod tests {

#[test]
fn test_error_source() {
let io_error =
io::Error::new(io::ErrorKind::NotFound, "File not found");
let error = RssError::IoError(io_error);
let xml_error = quick_xml::Error::Io(std::sync::Arc::new(
io::Error::new(io::ErrorKind::NotFound, "File not found"),
));
let error = RssError::XmlWriteError(xml_error);
assert!(error.source().is_some());
}

// New tests start here
let error = RssError::IoError("File not found".to_string());
assert!(error.source().is_none());
}

#[test]
fn test_missing_field_with_string() {
let error = RssError::missing_field(String::from("author"));
assert_eq!(error.to_string(), "Missing required field: author");
assert_eq!(error.to_string(), "A required field is missing");
}

#[test]
fn test_missing_field_with_str() {
let error = RssError::missing_field("description");
assert_eq!(
error.to_string(),
"Missing required field: description"
);
}

#[test]
fn test_xml_write_error_details() {
let xml_error =
quick_xml::Error::Io(std::sync::Arc::new(io::Error::new(
io::ErrorKind::PermissionDenied,
"Permission denied",
)));
let error = RssError::XmlWriteError(xml_error);
assert!(error.to_string().contains("Permission denied"));
}

#[test]
fn test_utf8_error_details() {
let utf8_error =
String::from_utf8(vec![0xFF, 0xFF]).unwrap_err();
let error = RssError::Utf8Error(utf8_error);
assert!(error.to_string().contains("invalid utf-8 sequence"));
}

#[test]
fn test_io_error_details() {
let io_error = io::Error::new(
io::ErrorKind::AddrInUse,
"Address already in use",
);
let error = RssError::IoError(io_error);
assert!(error.to_string().contains("Address already in use"));
assert_eq!(error.to_string(), "A required field is missing");
}

#[test]
Expand All @@ -150,24 +215,6 @@ mod tests {
assert!(downcast_result.is_ok());
}

#[test]
fn test_error_chain() {
let io_error =
io::Error::new(io::ErrorKind::Other, "Underlying IO error");
let xml_error =
quick_xml::Error::Io(std::sync::Arc::new(io_error));
let error = RssError::XmlWriteError(xml_error);

let mut error_chain = error.source();
assert!(error_chain.is_some());
error_chain = error_chain.unwrap().source();
assert!(error_chain.is_some());
assert_eq!(
error_chain.unwrap().to_string(),
"Underlying IO error"
);
}

#[test]
fn test_from_quick_xml_error() {
let xml_error =
Expand All @@ -194,4 +241,17 @@ mod tests {
let error: RssError = io_error.into();
assert!(matches!(error, RssError::IoError(_)));
}

#[test]
fn test_invalid_input_error() {
let error = RssError::InvalidInput;
assert_eq!(error.to_string(), "Invalid input data provided");
}

#[test]
fn test_error_clone() {
let error = RssError::missing_field("title");
let cloned_error = error.clone();
assert_eq!(error.to_string(), cloned_error.to_string());
}
}

0 comments on commit 8ef2809

Please sign in to comment.