use serde_json::Error as JsonError;
use serde_yml::Error as YamlError;
use std::sync::Arc;
use thiserror::Error;
#[derive(Debug, Clone)]
pub struct Context {
pub line: Option<usize>,
pub column: Option<usize>,
pub snippet: Option<String>,
}
impl std::fmt::Display for Context {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"at {}:{}",
self.line.unwrap_or(0),
self.column.unwrap_or(0)
)?;
if let Some(snippet) = &self.snippet {
write!(f, " near '{}'", snippet)?;
}
Ok(())
}
}
#[derive(Error, Debug)]
#[non_exhaustive]
pub enum Error {
#[error("Your front matter contains too many fields ({size}). The maximum allowed is {max}.")]
ContentTooLarge {
size: usize,
max: usize,
},
#[error(
"Your front matter is nested too deeply ({depth} levels). The maximum allowed nesting depth is {max}."
)]
NestingTooDeep {
depth: usize,
max: usize,
},
#[error("Failed to parse YAML: {source}")]
YamlParseError {
source: Arc<YamlError>,
},
#[error("Failed to parse TOML: {0}")]
TomlParseError(#[from] toml::de::Error),
#[error("Failed to parse JSON: {0}")]
JsonParseError(Arc<JsonError>),
#[error("Invalid front matter format")]
InvalidFormat,
#[error("Failed to convert front matter: {0}")]
ConversionError(String),
#[error("Failed to parse front matter: {0}")]
ParseError(String),
#[error("Unsupported front matter format detected at line {line}")]
UnsupportedFormat {
line: usize,
},
#[error("No front matter found in the content")]
NoFrontmatterFound,
#[error(
"Invalid JSON front matter: malformed or invalid structure."
)]
InvalidJson,
#[error(
"Invalid URL: {0}. Ensure the URL is well-formed and valid."
)]
InvalidUrl(String),
#[error(
"Invalid TOML front matter: malformed or invalid structure."
)]
InvalidToml,
#[error(
"Invalid YAML front matter: malformed or invalid structure."
)]
InvalidYaml,
#[error("Invalid language code: {0}")]
InvalidLanguage(String),
#[error("JSON front matter exceeds maximum nesting depth")]
JsonDepthLimitExceeded,
#[error("Extraction error: {0}")]
ExtractionError(String),
#[error("Serialization or deserialization error: {source}")]
SerdeError {
source: Arc<serde_json::Error>,
},
#[error("Input validation error: {0}")]
ValidationError(String),
#[error("Generic error: {0}")]
Other(String),
}
impl Clone for Error {
fn clone(&self) -> Self {
match self {
Self::ContentTooLarge { size, max } => {
Self::ContentTooLarge {
size: *size,
max: *max,
}
}
Self::NestingTooDeep { depth, max } => {
Self::NestingTooDeep {
depth: *depth,
max: *max,
}
}
Self::YamlParseError { source } => Self::YamlParseError {
source: Arc::clone(source),
},
Self::JsonParseError(err) => {
Self::JsonParseError(Arc::<serde_json::Error>::clone(
err,
))
}
Self::TomlParseError(err) => {
Self::TomlParseError(err.clone())
}
Self::SerdeError { source } => Self::SerdeError {
source: Arc::clone(source),
},
Self::ConversionError(msg) => {
Self::ConversionError(msg.clone())
}
Self::ParseError(msg) => Self::ParseError(msg.clone()),
Self::UnsupportedFormat { line } => {
Self::UnsupportedFormat { line: *line }
}
Self::NoFrontmatterFound => Self::NoFrontmatterFound,
Self::InvalidJson => Self::InvalidJson,
Self::InvalidToml => Self::InvalidToml,
Self::InvalidYaml => Self::InvalidYaml,
Self::JsonDepthLimitExceeded => {
Self::JsonDepthLimitExceeded
}
Self::ExtractionError(msg) => {
Self::ExtractionError(msg.clone())
}
Self::ValidationError(msg) => {
Self::ValidationError(msg.clone())
}
Self::InvalidUrl(msg) => Self::InvalidUrl(msg.clone()),
Self::InvalidLanguage(msg) => {
Self::InvalidLanguage(msg.clone())
}
Self::Other(msg) => Self::Other(msg.clone()),
Self::InvalidFormat => Self::InvalidFormat,
}
}
}
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub enum Category {
Parsing,
Validation,
Conversion,
Configuration,
}
impl Error {
#[must_use]
pub const fn category(&self) -> Category {
match self {
Self::YamlParseError { .. }
| Self::TomlParseError(_)
| Self::JsonParseError(_)
| Self::SerdeError { .. }
| Self::ParseError(_)
| Self::InvalidFormat
| Self::UnsupportedFormat { .. }
| Self::NoFrontmatterFound
| Self::InvalidJson
| Self::InvalidToml
| Self::InvalidYaml
| Self::JsonDepthLimitExceeded
| Self::ExtractionError(_)
| Self::InvalidUrl(_)
| Self::InvalidLanguage(_) => Category::Parsing,
Self::ValidationError(_) => Category::Validation,
Self::ConversionError(_) => Category::Conversion,
Self::ContentTooLarge { .. }
| Self::NestingTooDeep { .. }
| Self::Other(_) => Category::Configuration,
}
}
#[must_use]
pub fn generic_parse_error(message: &str) -> Self {
Self::ParseError(message.to_string())
}
#[must_use]
pub const fn unsupported_format(line: usize) -> Self {
Self::UnsupportedFormat { line }
}
#[must_use]
pub fn validation_error(message: &str) -> Self {
Self::ValidationError(message.to_string())
}
#[must_use]
pub fn with_context(self, context: &Context) -> Self {
let context_info = format!(
" (line: {}, column: {})",
context.line.unwrap_or(0),
context.column.unwrap_or(0)
);
let snippet_info = context
.snippet
.as_ref()
.map(|s| format!(" near '{}'", s))
.unwrap_or_default();
match self {
Self::ParseError(msg) => Self::ParseError(format!(
"{msg}{context_info}{snippet_info}"
)),
Self::YamlParseError { source } => {
Self::YamlParseError { source }
}
_ => self, }
}
}
#[derive(Error, Debug)]
pub enum EngineError {
#[error("Content processing error: {0}")]
ContentError(String),
#[error("Template processing error: {0}")]
TemplateError(String),
#[error("Asset processing error: {0}")]
AssetError(String),
#[error("File system error: {source}")]
FileSystemError {
source: std::io::Error,
context: String,
},
#[error("Metadata error: {0}")]
MetadataError(String),
}
impl Clone for EngineError {
fn clone(&self) -> Self {
match self {
Self::ContentError(msg) => Self::ContentError(msg.clone()),
Self::TemplateError(msg) => {
Self::TemplateError(msg.clone())
}
Self::AssetError(msg) => Self::AssetError(msg.clone()),
Self::FileSystemError { source, context } => {
Self::FileSystemError {
source: std::io::Error::new(
source.kind(),
source.to_string(),
),
context: context.clone(),
}
}
Self::MetadataError(msg) => {
Self::MetadataError(msg.clone())
}
}
}
}
impl From<EngineError> for Error {
fn from(err: EngineError) -> Self {
match err {
EngineError::ContentError(msg) => {
Self::ParseError(format!("Content error: {}", msg))
}
EngineError::TemplateError(msg) => {
Self::ParseError(format!("Template error: {}", msg))
}
EngineError::AssetError(msg) => {
Self::ParseError(format!("Asset error: {}", msg))
}
EngineError::FileSystemError { source, context } => {
Self::ParseError(format!(
"File system error: {} ({})",
source, context
))
}
EngineError::MetadataError(msg) => {
Self::ParseError(format!("Metadata error: {}", msg))
}
}
}
}
impl From<std::io::Error> for Error {
fn from(err: std::io::Error) -> Self {
Self::ParseError(err.to_string())
}
}
impl From<Error> for String {
fn from(err: Error) -> Self {
err.to_string()
}
}
#[cfg(test)]
mod tests {
mod error_tests {
use super::super::*;
#[test]
fn test_content_too_large_error() {
let error = Error::ContentTooLarge {
size: 1000,
max: 500,
};
assert_eq!(
error.to_string(),
"Your front matter contains too many fields (1000). The maximum allowed is 500."
);
}
#[test]
fn test_nesting_too_deep_error() {
let error = Error::NestingTooDeep { depth: 10, max: 5 };
assert_eq!(
error.to_string(),
"Your front matter is nested too deeply (10 levels). The maximum allowed nesting depth is 5."
);
}
#[test]
fn test_json_parse_error() {
let json_data = r#"{"key": invalid}"#;
let result: Result<serde_json::Value, _> =
serde_json::from_str(json_data);
assert!(result.is_err());
let error =
Error::JsonParseError(Arc::new(result.unwrap_err()));
assert!(matches!(error, Error::JsonParseError(_)));
}
#[test]
fn test_invalid_format_error() {
let error = Error::InvalidFormat;
assert_eq!(
error.to_string(),
"Invalid front matter format"
);
}
#[test]
fn test_conversion_error() {
let error =
Error::ConversionError("Conversion failed".to_string());
assert_eq!(
error.to_string(),
"Failed to convert front matter: Conversion failed"
);
}
#[test]
fn test_unsupported_format() {
let error = Error::unsupported_format(42);
assert!(matches!(
error,
Error::UnsupportedFormat { line: 42 }
));
assert_eq!(
error.to_string(),
"Unsupported front matter format detected at line 42"
);
}
#[test]
fn test_no_frontmatter_found() {
let error = Error::NoFrontmatterFound;
assert_eq!(
error.to_string(),
"No front matter found in the content"
);
}
#[test]
fn test_invalid_json_error() {
let error = Error::InvalidJson;
assert_eq!(error.to_string(), "Invalid JSON front matter: malformed or invalid structure.");
}
#[test]
fn test_invalid_url_error() {
let error =
Error::InvalidUrl("http:// invalid.url".to_string());
assert_eq!(
error.to_string(),
"Invalid URL: http:// invalid.url. Ensure the URL is well-formed and valid."
);
}
#[test]
fn test_invalid_yaml_error() {
let error = Error::InvalidYaml;
assert_eq!(
error.to_string(),
"Invalid YAML front matter: malformed or invalid structure."
);
}
#[test]
fn test_validation_error() {
let error =
Error::ValidationError("Invalid title".to_string());
assert_eq!(
error.to_string(),
"Input validation error: Invalid title"
);
}
#[test]
fn test_json_depth_limit_exceeded() {
let error = Error::JsonDepthLimitExceeded;
assert_eq!(
error.to_string(),
"JSON front matter exceeds maximum nesting depth"
);
}
#[test]
fn test_category_method() {
let validation_error =
Error::ValidationError("Invalid field".to_string());
assert_eq!(
validation_error.category(),
Category::Validation
);
let conversion_error =
Error::ConversionError("Conversion failed".to_string());
assert_eq!(
conversion_error.category(),
Category::Conversion
);
let config_error =
Error::ContentTooLarge { size: 100, max: 50 };
assert_eq!(
config_error.category(),
Category::Configuration
);
}
#[test]
fn test_error_clone() {
let original = Error::ContentTooLarge {
size: 200,
max: 100,
};
let cloned = original.clone();
assert!(
matches!(cloned, Error::ContentTooLarge { size, max } if size == 200 && max == 100)
);
}
}
mod engine_error_tests {
use super::super::*;
use std::io;
#[test]
fn test_content_error() {
let error = EngineError::ContentError(
"Processing failed".to_string(),
);
assert_eq!(
error.to_string(),
"Content processing error: Processing failed"
);
}
#[test]
fn test_engine_error_to_error_conversion() {
let io_error =
io::Error::new(io::ErrorKind::Other, "disk full");
let engine_error = EngineError::FileSystemError {
source: io_error,
context: "Saving file".to_string(),
};
let converted: Error = engine_error.into();
assert!(converted.to_string().contains("disk full"));
assert!(converted.to_string().contains("Saving file"));
}
}
mod context_tests {
use super::super::*;
#[test]
fn test_context_display() {
let context = Context {
line: Some(42),
column: Some(10),
snippet: Some("invalid key".to_string()),
};
assert_eq!(
context.to_string(),
"at 42:10 near 'invalid key'"
);
}
#[test]
fn test_context_missing_fields() {
let context = Context {
line: None,
column: None,
snippet: Some("example snippet".to_string()),
};
assert_eq!(
context.to_string(),
"at 0:0 near 'example snippet'"
);
}
}
mod conversion_tests {
use super::super::*;
use std::io;
#[test]
fn test_io_error_conversion() {
let io_error =
io::Error::new(io::ErrorKind::NotFound, "file missing");
let error: Error = io_error.into();
assert!(matches!(error, Error::ParseError(_)));
assert!(error.to_string().contains("file missing"));
}
}
#[test]
fn test_engine_error_conversion() {
let engine_error = crate::error::EngineError::ContentError(
"content failed".to_string(),
);
let error: crate::Error = engine_error.into();
assert!(matches!(error, crate::Error::ParseError(_)));
assert!(error.to_string().contains("content failed"));
}
}