use thiserror::Error;
#[derive(Debug, Error)]
pub enum MdBookLintError {
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("Parse error at line {line}, column {column}: {message}")]
Parse {
line: usize,
column: usize,
message: String,
},
#[error("Configuration error: {0}")]
Config(String),
#[error("Rule error in {rule_id}: {message}")]
Rule { rule_id: String, message: String },
#[error("Plugin error: {0}")]
Plugin(String),
#[error("Document error: {0}")]
Document(String),
#[error("Registry error: {0}")]
Registry(String),
#[error("JSON error: {0}")]
Json(#[from] serde_json::Error),
#[error("YAML error: {0}")]
Yaml(#[from] serde_yaml::Error),
#[error("TOML error: {0}")]
Toml(#[from] toml::de::Error),
#[error("Directory traversal error: {0}")]
WalkDir(#[from] walkdir::Error),
}
#[derive(Debug, Error)]
pub enum RuleError {
#[error("Rule not found: {rule_id}")]
NotFound { rule_id: String },
#[error("Rule execution failed: {message}")]
ExecutionFailed { message: String },
#[error("Invalid rule configuration: {message}")]
InvalidConfig { message: String },
#[error("Rule dependency not satisfied: {rule_id} requires {dependency}")]
DependencyNotMet { rule_id: String, dependency: String },
#[error("Rule registration conflict: rule {rule_id} already exists")]
RegistrationConflict { rule_id: String },
}
#[derive(Debug, Error)]
pub enum DocumentError {
#[error("Failed to read document: {path}")]
ReadFailed { path: String },
#[error("Invalid document format")]
InvalidFormat,
#[error("Document too large: {size} bytes (max: {max_size})")]
TooLarge { size: usize, max_size: usize },
#[error("Failed to parse document: {reason}")]
ParseFailed { reason: String },
#[error("Invalid encoding in document: {path}")]
InvalidEncoding { path: String },
}
#[derive(Debug, Error)]
pub enum ConfigError {
#[error("Configuration file not found: {path}")]
NotFound { path: String },
#[error("Invalid configuration format: {message}")]
InvalidFormat { message: String },
#[error("Configuration validation failed: {field} - {message}")]
ValidationFailed { field: String, message: String },
#[error("Unsupported configuration version: {version} (supported: {supported})")]
UnsupportedVersion { version: String, supported: String },
}
#[derive(Debug, Error)]
pub enum PluginError {
#[error("Plugin not found: {plugin_id}")]
NotFound { plugin_id: String },
#[error("Failed to load plugin {plugin_id}: {reason}")]
LoadFailed { plugin_id: String, reason: String },
#[error("Plugin initialization failed: {plugin_id}")]
InitializationFailed { plugin_id: String },
#[error("Plugin version incompatible: {plugin_id} version {version} (required: {required})")]
VersionIncompatible {
plugin_id: String,
version: String,
required: String,
},
}
pub type Result<T> = std::result::Result<T, MdBookLintError>;
impl MdBookLintError {
pub fn parse_error(line: usize, column: usize, message: impl Into<String>) -> Self {
Self::Parse {
line,
column,
message: message.into(),
}
}
pub fn rule_error(rule_id: impl Into<String>, message: impl Into<String>) -> Self {
Self::Rule {
rule_id: rule_id.into(),
message: message.into(),
}
}
pub fn config_error(message: impl Into<String>) -> Self {
Self::Config(message.into())
}
pub fn document_error(message: impl Into<String>) -> Self {
Self::Document(message.into())
}
pub fn plugin_error(message: impl Into<String>) -> Self {
Self::Plugin(message.into())
}
pub fn registry_error(message: impl Into<String>) -> Self {
Self::Registry(message.into())
}
}
impl RuleError {
pub fn not_found(rule_id: impl Into<String>) -> Self {
Self::NotFound {
rule_id: rule_id.into(),
}
}
pub fn execution_failed(message: impl Into<String>) -> Self {
Self::ExecutionFailed {
message: message.into(),
}
}
pub fn invalid_config(message: impl Into<String>) -> Self {
Self::InvalidConfig {
message: message.into(),
}
}
pub fn dependency_not_met(rule_id: impl Into<String>, dependency: impl Into<String>) -> Self {
Self::DependencyNotMet {
rule_id: rule_id.into(),
dependency: dependency.into(),
}
}
pub fn registration_conflict(rule_id: impl Into<String>) -> Self {
Self::RegistrationConflict {
rule_id: rule_id.into(),
}
}
}
impl DocumentError {
pub fn read_failed(path: impl Into<String>) -> Self {
Self::ReadFailed { path: path.into() }
}
pub fn parse_failed(reason: impl Into<String>) -> Self {
Self::ParseFailed {
reason: reason.into(),
}
}
pub fn too_large(size: usize, max_size: usize) -> Self {
Self::TooLarge { size, max_size }
}
pub fn invalid_encoding(path: impl Into<String>) -> Self {
Self::InvalidEncoding { path: path.into() }
}
}
pub trait ErrorContext<T> {
fn with_rule_context(self, rule_id: &str) -> Result<T>;
fn with_document_context(self, path: &str) -> Result<T>;
fn with_plugin_context(self, plugin_id: &str) -> Result<T>;
fn with_config_context(self, field: &str) -> Result<T>;
}
impl<T> ErrorContext<T> for std::result::Result<T, MdBookLintError> {
fn with_rule_context(self, rule_id: &str) -> Result<T> {
self.map_err(|e| match e {
MdBookLintError::Rule { message, .. } => MdBookLintError::Rule {
rule_id: rule_id.to_string(),
message,
},
other => other,
})
}
fn with_document_context(self, path: &str) -> Result<T> {
self.map_err(|e| match e {
MdBookLintError::Document(message) => {
MdBookLintError::Document(format!("{path}: {message}"))
}
other => other,
})
}
fn with_plugin_context(self, plugin_id: &str) -> Result<T> {
self.map_err(|e| match e {
MdBookLintError::Plugin(message) => {
MdBookLintError::Plugin(format!("{plugin_id}: {message}"))
}
other => other,
})
}
fn with_config_context(self, field: &str) -> Result<T> {
self.map_err(|e| match e {
MdBookLintError::Config(message) => {
MdBookLintError::Config(format!("{field}: {message}"))
}
other => other,
})
}
}
pub trait IntoMdBookLintError<T> {
fn into_mdbook_lint_error(self) -> Result<T>;
}
impl<T> IntoMdBookLintError<T> for std::result::Result<T, RuleError> {
fn into_mdbook_lint_error(self) -> Result<T> {
self.map_err(|e| match e {
RuleError::NotFound { rule_id } => {
MdBookLintError::rule_error(rule_id, "Rule not found")
}
RuleError::ExecutionFailed { message } => {
MdBookLintError::rule_error("unknown", message)
}
RuleError::InvalidConfig { message } => MdBookLintError::config_error(message),
RuleError::DependencyNotMet {
rule_id,
dependency,
} => MdBookLintError::rule_error(rule_id, format!("Dependency not met: {dependency}")),
RuleError::RegistrationConflict { rule_id } => {
MdBookLintError::registry_error(format!("Rule already exists: {rule_id}"))
}
})
}
}
impl<T> IntoMdBookLintError<T> for std::result::Result<T, DocumentError> {
fn into_mdbook_lint_error(self) -> Result<T> {
self.map_err(|e| match e {
DocumentError::ReadFailed { path } => {
MdBookLintError::document_error(format!("Failed to read: {path}"))
}
DocumentError::InvalidFormat => {
MdBookLintError::document_error("Invalid document format")
}
DocumentError::TooLarge { size, max_size } => MdBookLintError::document_error(format!(
"Document too large: {size} bytes (max: {max_size})"
)),
DocumentError::ParseFailed { reason } => {
MdBookLintError::document_error(format!("Parse failed: {reason}"))
}
DocumentError::InvalidEncoding { path } => {
MdBookLintError::document_error(format!("Invalid encoding: {path}"))
}
})
}
}
impl<T> IntoMdBookLintError<T> for std::result::Result<T, ConfigError> {
fn into_mdbook_lint_error(self) -> Result<T> {
self.map_err(|e| match e {
ConfigError::NotFound { path } => {
MdBookLintError::config_error(format!("Configuration not found: {path}"))
}
ConfigError::InvalidFormat { message } => {
MdBookLintError::config_error(format!("Invalid format: {message}"))
}
ConfigError::ValidationFailed { field, message } => {
MdBookLintError::config_error(format!("Validation failed for {field}: {message}"))
}
ConfigError::UnsupportedVersion { version, supported } => {
MdBookLintError::config_error(format!(
"Unsupported version: {version} (supported: {supported})"
))
}
})
}
}
impl<T> IntoMdBookLintError<T> for std::result::Result<T, PluginError> {
fn into_mdbook_lint_error(self) -> Result<T> {
self.map_err(|e| match e {
PluginError::NotFound { plugin_id } => {
MdBookLintError::plugin_error(format!("Plugin not found: {plugin_id}"))
}
PluginError::LoadFailed { plugin_id, reason } => {
MdBookLintError::plugin_error(format!("Failed to load {plugin_id}: {reason}"))
}
PluginError::InitializationFailed { plugin_id } => {
MdBookLintError::plugin_error(format!("Initialization failed: {plugin_id}"))
}
PluginError::VersionIncompatible {
plugin_id,
version,
required,
} => MdBookLintError::plugin_error(format!(
"Version incompatible: {plugin_id} version {version} (required: {required})"
)),
})
}
}
impl From<anyhow::Error> for MdBookLintError {
fn from(err: anyhow::Error) -> Self {
MdBookLintError::Document(err.to_string())
}
}
pub type MdlntError = MdBookLintError;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_error_creation() {
let err = MdBookLintError::parse_error(10, 5, "Invalid syntax");
assert!(matches!(
err,
MdBookLintError::Parse {
line: 10,
column: 5,
..
}
));
assert!(err.to_string().contains("line 10, column 5"));
}
#[test]
fn test_error_context() {
let result: Result<()> = Err(MdBookLintError::document_error("Something went wrong"));
let with_context = result.with_document_context("test.md");
assert!(with_context.is_err());
assert!(with_context.unwrap_err().to_string().contains("test.md"));
}
#[test]
fn test_rule_error_conversion() {
let rule_err = RuleError::not_found("MD001");
let result: std::result::Result<(), _> = Err(rule_err);
let result = result.into_mdbook_lint_error();
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("MD001"));
}
#[test]
fn test_anyhow_compatibility() {
let anyhow_err = anyhow::anyhow!("Test error");
let mdbook_lint_err: MdBookLintError = anyhow_err.into();
let back_to_anyhow = anyhow::Error::from(mdbook_lint_err);
assert!(back_to_anyhow.to_string().contains("Test error"));
}
#[test]
fn test_io_error_conversion() {
use std::io::{Error, ErrorKind};
let io_err = Error::new(ErrorKind::NotFound, "File not found");
let mdbook_lint_err: MdBookLintError = io_err.into();
assert!(matches!(mdbook_lint_err, MdBookLintError::Io(_)));
assert!(mdbook_lint_err.to_string().contains("File not found"));
}
}