use noyalib::Error as SerdeYmlError;
use serde::de::Error as SerdeError;
use std::fmt::Display;
use thiserror::Error;
#[derive(Debug)]
pub struct ContextError {
context: String,
source: Box<dyn std::error::Error + Send + Sync>,
}
impl std::fmt::Display for ContextError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}: {}", self.context, self.source)
}
}
impl std::error::Error for ContextError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
Some(&*self.source)
}
}
#[derive(Error, Debug)]
pub enum MetadataError {
#[error("Failed to extract metadata: {message}")]
ExtractionError {
message: String,
},
#[error("Failed to process metadata: {message}")]
ProcessingError {
message: String,
},
#[error("Missing required metadata field: {0}")]
MissingFieldError(String),
#[error("Failed to parse date: {0}")]
DateParseError(String),
#[error("I/O error: {0}")]
IoError(#[from] std::io::Error),
#[error("YAML parsing error: {0}")]
YamlError(#[from] SerdeYmlError),
#[error("JSON parsing error: {0}")]
JsonError(#[from] serde_json::Error),
#[error("TOML parsing error: {0}")]
TomlError(#[from] toml::de::Error),
#[error("Unsupported metadata format: {0}")]
UnsupportedFormatError(String),
#[error("Metadata validation error: {field} - {message}")]
ValidationError {
field: String,
message: String,
},
#[error("UTF-8 decoding error: {0}")]
Utf8Error(#[from] std::str::Utf8Error),
#[error("Unexpected error: {0}")]
Other(#[from] Box<dyn std::error::Error + Send + Sync>),
}
impl MetadataError {
pub fn new_extraction_error(message: impl Into<String>) -> Self {
Self::ExtractionError {
message: message.into(),
}
}
pub fn new_processing_error(message: impl Into<String>) -> Self {
Self::ProcessingError {
message: message.into(),
}
}
pub fn new_validation_error(
field: impl Into<String>,
message: impl Into<String>,
) -> Self {
Self::ValidationError {
field: field.into(),
message: message.into(),
}
}
pub fn context<C>(self, ctx: C) -> Self
where
C: Display + Send + Sync + 'static,
{
match self {
Self::ExtractionError { message } => {
Self::ExtractionError {
message: format!("{}: {}", ctx, message),
}
}
Self::ProcessingError { message } => {
Self::ProcessingError {
message: format!("{}: {}", ctx, message),
}
}
Self::MissingFieldError(field) => {
Self::MissingFieldError(format!("{}: {}", ctx, field))
}
Self::DateParseError(error) => {
Self::DateParseError(format!("{}: {}", ctx, error))
}
Self::IoError(error) => Self::IoError(std::io::Error::new(
error.kind(),
format!("{}: {}", ctx, error),
)),
Self::YamlError(error) => Self::YamlError(
SerdeYmlError::custom(format!("{}: {}", ctx, error)),
),
Self::JsonError(error) => {
Self::JsonError(serde_json::Error::custom(format!(
"{}: {}",
ctx, error
)))
}
Self::TomlError(error) => Self::TomlError(
toml::de::Error::custom(format!("{}: {}", ctx, error)),
),
Self::UnsupportedFormatError(format) => {
Self::UnsupportedFormatError(format!(
"{}: {}",
ctx, format
))
}
Self::ValidationError { field, message } => {
Self::ValidationError {
field,
message: format!("{}: {}", ctx, message),
}
}
Self::Utf8Error(error) => Self::Utf8Error(error),
Self::Other(error) => Self::Other(Box::new(ContextError {
context: ctx.to_string(),
source: error,
})),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::error::Error;
use std::fmt;
use std::io;
#[test]
fn test_extraction_error() {
let error = MetadataError::new_extraction_error(
"No valid front matter found.",
);
assert_eq!(
error.to_string(),
"Failed to extract metadata: No valid front matter found."
);
}
#[test]
fn test_processing_error() {
let error =
MetadataError::new_processing_error("Unknown field");
assert_eq!(
error.to_string(),
"Failed to process metadata: Unknown field"
);
}
#[test]
fn test_missing_field_error() {
let error =
MetadataError::MissingFieldError("author".to_string());
assert_eq!(
error.to_string(),
"Missing required metadata field: author"
);
}
#[test]
fn test_date_parse_error() {
let error = MetadataError::DateParseError(
"Invalid date format".to_string(),
);
assert_eq!(
error.to_string(),
"Failed to parse date: Invalid date format"
);
}
#[test]
fn test_io_error() {
let io_error =
io::Error::new(io::ErrorKind::NotFound, "File not found");
let error: MetadataError = io_error.into();
assert_eq!(error.to_string(), "I/O error: File not found");
}
#[test]
fn test_yaml_error() {
let yaml_error = noyalib::Error::custom("YAML structure error");
let error: MetadataError = yaml_error.into();
assert!(error.to_string().contains("YAML parsing error"));
}
#[test]
fn test_json_error() {
let json_error =
serde_json::Error::custom("Invalid JSON format");
let error: MetadataError = json_error.into();
assert_eq!(
error.to_string(),
"JSON parsing error: Invalid JSON format"
);
}
#[test]
fn test_toml_error() {
let toml_error =
toml::de::Error::custom("Invalid TOML structure");
let error: MetadataError = toml_error.into();
assert!(error.to_string().contains("TOML parsing error"));
}
#[test]
fn test_unsupported_format_error() {
let error =
MetadataError::UnsupportedFormatError("XML".to_string());
assert_eq!(
error.to_string(),
"Unsupported metadata format: XML"
);
}
#[test]
fn test_validation_error() {
let error = MetadataError::new_validation_error(
"title",
"Title must not be empty",
);
match error {
MetadataError::ValidationError { field, message } => {
assert_eq!(field, "title");
assert_eq!(message, "Title must not be empty");
}
_ => panic!("Unexpected error variant"),
}
}
#[test]
#[allow(invalid_from_utf8)]
fn test_utf8_error() {
let invalid_bytes: &[u8] = &[0xFF, 0xFF];
let utf8_error =
std::str::from_utf8(invalid_bytes).unwrap_err();
let error: MetadataError = utf8_error.into();
assert!(matches!(error, MetadataError::Utf8Error(..)));
assert!(error.to_string().starts_with("UTF-8 decoding error:"));
}
#[test]
fn test_other_error() {
use std::error::Error;
#[derive(Debug)]
struct CustomError;
impl std::fmt::Display for CustomError {
fn fmt(
&self,
f: &mut std::fmt::Formatter<'_>,
) -> std::fmt::Result {
write!(f, "Custom error occurred")
}
}
impl Error for CustomError {}
let custom_error = CustomError;
let error = MetadataError::Other(Box::new(custom_error));
assert!(matches!(error, MetadataError::Other(..)));
assert_eq!(
error.to_string(),
"Unexpected error: Custom error occurred"
);
}
#[test]
fn test_extraction_error_with_empty_message() {
let error = MetadataError::new_extraction_error("");
assert_eq!(error.to_string(), "Failed to extract metadata: ");
}
#[test]
fn test_processing_error_with_empty_message() {
let error = MetadataError::new_processing_error("");
assert_eq!(error.to_string(), "Failed to process metadata: ");
}
#[test]
fn test_validation_error_with_empty_field_and_message() {
let error = MetadataError::new_validation_error("", "");
match error {
MetadataError::ValidationError { field, message } => {
assert_eq!(field, "");
assert_eq!(message, "");
}
_ => panic!("Unexpected error variant"),
}
}
#[test]
fn test_unsupported_format_error_with_empty_format() {
let error =
MetadataError::UnsupportedFormatError("".to_string());
assert_eq!(error.to_string(), "Unsupported metadata format: ");
}
#[test]
fn test_yaml_error_with_custom_message() {
let yaml_error =
noyalib::Error::custom("Custom YAML error occurred");
let error: MetadataError = yaml_error.into();
assert!(error.to_string().contains(
"YAML parsing error: Custom YAML error occurred"
));
}
#[test]
fn test_json_error_with_custom_message() {
let json_error = serde_json::Error::custom("Custom JSON error");
let error: MetadataError = json_error.into();
assert_eq!(
error.to_string(),
"JSON parsing error: Custom JSON error"
);
}
#[test]
fn test_toml_error_with_custom_message() {
let toml_error = toml::de::Error::custom("Custom TOML error");
let error: MetadataError = toml_error.into();
assert!(error
.to_string()
.contains("TOML parsing error: Custom TOML error"));
}
#[test]
#[allow(invalid_from_utf8)]
fn test_utf8_error_with_specific_invalid_bytes() {
let invalid_bytes: &[u8] = &[0xC0, 0x80]; let utf8_error =
std::str::from_utf8(invalid_bytes).unwrap_err();
let error: MetadataError = utf8_error.into();
assert!(matches!(error, MetadataError::Utf8Error(..)));
assert!(error.to_string().starts_with("UTF-8 decoding error:"));
}
#[test]
fn test_io_error_with_custom_message() {
let io_error = std::io::Error::new(
std::io::ErrorKind::PermissionDenied,
"Permission denied",
);
let error: MetadataError = io_error.into();
assert_eq!(error.to_string(), "I/O error: Permission denied");
}
#[test]
fn test_extraction_error_to_debug() {
let error = MetadataError::new_extraction_error(
"Failed to extract metadata",
);
assert_eq!(
format!("{:?}", error),
r#"ExtractionError { message: "Failed to extract metadata" }"#
);
}
#[test]
fn test_processing_error_to_debug() {
let error =
MetadataError::new_processing_error("Processing failed");
assert_eq!(
format!("{:?}", error),
r#"ProcessingError { message: "Processing failed" }"#
);
}
#[test]
fn test_validation_error_to_debug() {
let error = MetadataError::new_validation_error(
"title",
"Title cannot be empty",
);
assert_eq!(
format!("{:?}", error),
r#"ValidationError { field: "title", message: "Title cannot be empty" }"#
);
}
#[test]
fn test_other_error_to_debug() {
#[derive(Debug)]
struct CustomError;
impl std::fmt::Display for CustomError {
fn fmt(
&self,
f: &mut std::fmt::Formatter<'_>,
) -> std::fmt::Result {
write!(f, "A custom error occurred")
}
}
impl std::error::Error for CustomError {}
let custom_error = CustomError;
let error = MetadataError::Other(Box::new(custom_error));
assert!(format!("{:?}", error).contains("Other("));
}
#[test]
fn test_context_error() {
let error =
MetadataError::new_extraction_error("Failed to parse YAML")
.context("Processing file 'example.md'");
assert_eq!(
error.to_string(),
"Failed to extract metadata: Processing file 'example.md': Failed to parse YAML"
);
}
#[test]
fn test_nested_context_error() {
let error =
MetadataError::new_extraction_error("Failed to parse YAML")
.context("Processing file 'example.md'")
.context("Metadata extraction process");
assert_eq!(
error.to_string(),
"Failed to extract metadata: Metadata extraction process: Processing file 'example.md': Failed to parse YAML"
);
}
#[test]
fn test_extraction_error_empty_message() {
let error = MetadataError::ExtractionError {
message: "".to_string(),
};
assert_eq!(error.to_string(), "Failed to extract metadata: ");
}
#[test]
fn test_processing_error_empty_message() {
let error = MetadataError::ProcessingError {
message: "".to_string(),
};
assert_eq!(error.to_string(), "Failed to process metadata: ");
}
#[test]
fn test_missing_field_error_empty_message() {
let error = MetadataError::MissingFieldError("".to_string());
assert_eq!(
error.to_string(),
"Missing required metadata field: "
);
}
#[test]
fn test_date_parse_error_empty_message() {
let error = MetadataError::DateParseError("".to_string());
assert_eq!(error.to_string(), "Failed to parse date: ");
}
#[test]
fn test_extraction_error_debug() {
let error = MetadataError::ExtractionError {
message: "Error extracting metadata".to_string(),
};
assert_eq!(
format!("{:?}", error),
r#"ExtractionError { message: "Error extracting metadata" }"#
);
}
#[test]
fn test_processing_error_debug() {
let error = MetadataError::ProcessingError {
message: "Error processing metadata".to_string(),
};
assert_eq!(
format!("{:?}", error),
r#"ProcessingError { message: "Error processing metadata" }"#
);
}
#[test]
fn test_io_error_propagation() {
let io_error =
io::Error::new(io::ErrorKind::NotFound, "file not found");
let error: MetadataError = io_error.into();
assert_eq!(error.to_string(), "I/O error: file not found");
assert!(matches!(error, MetadataError::IoError(_)));
}
#[test]
fn test_yaml_error_propagation() {
let yaml_error = noyalib::Error::custom("Custom YAML error");
let error: MetadataError = yaml_error.into();
assert_eq!(
error.to_string(),
"YAML parsing error: Custom YAML error"
);
assert!(matches!(error, MetadataError::YamlError(_)));
}
#[test]
fn test_json_error_propagation() {
let json_error = serde_json::Error::custom("Custom JSON error");
let error: MetadataError = json_error.into();
assert_eq!(
error.to_string(),
"JSON parsing error: Custom JSON error"
);
assert!(matches!(error, MetadataError::JsonError(_)));
}
#[test]
fn test_toml_error_propagation() {
let toml_error = toml::de::Error::custom("Custom TOML error");
let error: MetadataError = toml_error.into();
assert_eq!(
error.to_string(),
"TOML parsing error: Custom TOML error\n"
);
assert!(matches!(error, MetadataError::TomlError(_)));
}
#[test]
fn test_missing_field_error_debug() {
let error =
MetadataError::MissingFieldError("title".to_string());
assert_eq!(
format!("{:?}", error),
r#"MissingFieldError("title")"#
);
}
#[test]
fn test_date_parse_error_debug() {
let error = MetadataError::DateParseError(
"Invalid date format".to_string(),
);
assert_eq!(
format!("{:?}", error),
r#"DateParseError("Invalid date format")"#
);
}
#[test]
fn test_empty_yaml_error_message() {
let yaml_error = noyalib::Error::custom("");
let error: MetadataError = yaml_error.into();
assert_eq!(error.to_string(), "YAML parsing error: ");
}
#[test]
fn test_empty_json_error_message() {
let json_error = serde_json::Error::custom("");
let error: MetadataError = json_error.into();
assert_eq!(error.to_string(), "JSON parsing error: ");
}
#[test]
fn test_empty_toml_error_message() {
let toml_error = toml::de::Error::custom("");
let error: MetadataError = toml_error.into();
assert_eq!(error.to_string(), "TOML parsing error: \n");
}
#[derive(Debug)]
struct CustomError;
impl fmt::Display for CustomError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Custom error occurred")
}
}
impl Error for CustomError {}
#[test]
fn test_context_error_fmt() {
let custom_error = CustomError;
let context_error = ContextError {
context: "An error occurred while processing".to_string(),
source: Box::new(custom_error),
};
let formatted = format!("{}", context_error);
assert_eq!(
formatted,
"An error occurred while processing: Custom error occurred"
);
}
#[test]
fn test_context_error_source() {
let custom_error = CustomError;
let context_error = ContextError {
context: "Error with context".to_string(),
source: Box::new(custom_error),
};
let source = context_error.source().unwrap();
assert_eq!(source.to_string(), "Custom error occurred");
}
#[test]
fn test_context_error_debug() {
let custom_error = CustomError;
let context_error = ContextError {
context: "Error during processing".to_string(),
source: Box::new(custom_error),
};
let debug_output = format!("{:?}", context_error);
assert!(debug_output.contains("ContextError"));
assert!(debug_output.contains("Error during processing"));
assert!(debug_output.contains("CustomError"));
}
}
#[cfg(test)]
mod context_tests {
use super::*;
fn assert_prefixed(err: &MetadataError, ctx: &str) {
let text = err.to_string();
assert!(
text.contains(ctx),
"context {ctx:?} missing from {text:?}"
);
}
#[test]
fn context_prefixes_extraction_and_processing() {
let e =
MetadataError::new_extraction_error("bad").context("front");
assert!(
matches!(e, MetadataError::ExtractionError { ref message } if message == "front: bad")
);
let e =
MetadataError::new_processing_error("bad").context("front");
assert!(
matches!(e, MetadataError::ProcessingError { ref message } if message == "front: bad")
);
}
#[test]
fn context_prefixes_string_payload_variants() {
let e = MetadataError::MissingFieldError("title".into())
.context("page");
assert!(
matches!(e, MetadataError::MissingFieldError(ref s) if s == "page: title")
);
let e =
MetadataError::DateParseError("x".into()).context("page");
assert!(
matches!(e, MetadataError::DateParseError(ref s) if s == "page: x")
);
let e = MetadataError::UnsupportedFormatError("ini".into())
.context("page");
assert!(
matches!(e, MetadataError::UnsupportedFormatError(ref s) if s == "page: ini")
);
let e = MetadataError::new_validation_error("title", "empty")
.context("page");
assert!(matches!(
e,
MetadataError::ValidationError { ref field, ref message }
if field == "title" && message == "page: empty"
));
}
#[test]
fn context_wraps_io_error_and_keeps_its_kind() {
let io =
std::io::Error::new(std::io::ErrorKind::NotFound, "gone");
let e = MetadataError::from(io).context("reading");
match e {
MetadataError::IoError(inner) => {
assert_eq!(inner.kind(), std::io::ErrorKind::NotFound);
assert_eq!(inner.to_string(), "reading: gone");
}
other => panic!("expected IoError, got {other:?}"),
}
}
#[test]
fn context_rewrites_parser_errors_as_custom_messages() {
let yaml =
noyalib::from_str::<noyalib::Value>("a: [").unwrap_err();
let e = MetadataError::from(yaml).context("yaml");
assert!(matches!(e, MetadataError::YamlError(_)));
assert_prefixed(&e, "yaml");
let json =
serde_json::from_str::<serde_json::Value>("{").unwrap_err();
let e = MetadataError::from(json).context("json");
assert!(matches!(e, MetadataError::JsonError(_)));
assert_prefixed(&e, "json");
let toml = toml::from_str::<toml::Value>("a = ").unwrap_err();
let e = MetadataError::from(toml).context("toml");
assert!(matches!(e, MetadataError::TomlError(_)));
assert_prefixed(&e, "toml");
}
#[test]
fn context_leaves_utf8_errors_untouched() {
let bytes = vec![0xffu8];
let utf8 = std::str::from_utf8(&bytes).unwrap_err();
let e = MetadataError::from(utf8).context("ignored");
assert!(matches!(e, MetadataError::Utf8Error(_)));
assert!(!e.to_string().contains("ignored"));
}
#[test]
fn context_boxes_other_errors_with_a_source_chain() {
let inner: Box<dyn std::error::Error + Send + Sync> =
"root cause".into();
let e = MetadataError::from(inner).context("outer");
let MetadataError::Other(boxed) = &e else {
panic!("expected Other, got {e:?}");
};
assert_eq!(boxed.to_string(), "outer: root cause");
let source =
boxed.source().expect("ContextError keeps its source");
assert_eq!(source.to_string(), "root cause");
assert_eq!(
e.to_string(),
"Unexpected error: outer: root cause"
);
}
}