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_all_error_variants() {
let config_err = MdBookLintError::config_error("Invalid config");
assert!(matches!(config_err, MdBookLintError::Config(_)));
let rule_err = MdBookLintError::rule_error("MD001", "Rule failed");
assert!(matches!(rule_err, MdBookLintError::Rule { .. }));
let plugin_err = MdBookLintError::plugin_error("Plugin failed");
assert!(matches!(plugin_err, MdBookLintError::Plugin(_)));
let doc_err = MdBookLintError::document_error("Document error");
assert!(matches!(doc_err, MdBookLintError::Document(_)));
let registry_err = MdBookLintError::registry_error("Registry error");
assert!(matches!(registry_err, MdBookLintError::Registry(_)));
}
#[test]
fn test_rule_error_variants() {
let not_found = RuleError::not_found("MD999");
assert!(matches!(not_found, RuleError::NotFound { .. }));
assert!(not_found.to_string().contains("MD999"));
let exec_failed = RuleError::execution_failed("Test execution failed");
assert!(matches!(exec_failed, RuleError::ExecutionFailed { .. }));
let invalid_config = RuleError::invalid_config("Invalid rule config");
assert!(matches!(invalid_config, RuleError::InvalidConfig { .. }));
let dep_not_met = RuleError::dependency_not_met("MD001", "MD002");
assert!(matches!(dep_not_met, RuleError::DependencyNotMet { .. }));
let reg_conflict = RuleError::registration_conflict("MD001");
assert!(matches!(
reg_conflict,
RuleError::RegistrationConflict { .. }
));
}
#[test]
fn test_document_error_variants() {
let read_failed = DocumentError::read_failed("test.md");
assert!(matches!(read_failed, DocumentError::ReadFailed { .. }));
let parse_failed = DocumentError::parse_failed("Parse error");
assert!(matches!(parse_failed, DocumentError::ParseFailed { .. }));
let too_large = DocumentError::too_large(1000, 500);
assert!(matches!(too_large, DocumentError::TooLarge { .. }));
let invalid_encoding = DocumentError::invalid_encoding("test.md");
assert!(matches!(
invalid_encoding,
DocumentError::InvalidEncoding { .. }
));
}
#[test]
fn test_config_error_variants() {
let not_found = ConfigError::NotFound {
path: "config.toml".to_string(),
};
assert!(not_found.to_string().contains("config.toml"));
let invalid_format = ConfigError::InvalidFormat {
message: "Bad YAML".to_string(),
};
assert!(invalid_format.to_string().contains("Bad YAML"));
let validation_failed = ConfigError::ValidationFailed {
field: "rules".to_string(),
message: "Invalid rule".to_string(),
};
assert!(validation_failed.to_string().contains("rules"));
let unsupported_version = ConfigError::UnsupportedVersion {
version: "2.0".to_string(),
supported: "1.0-1.5".to_string(),
};
assert!(unsupported_version.to_string().contains("2.0"));
}
#[test]
fn test_plugin_error_variants() {
let not_found = PluginError::NotFound {
plugin_id: "test-plugin".to_string(),
};
assert!(not_found.to_string().contains("test-plugin"));
let load_failed = PluginError::LoadFailed {
plugin_id: "test-plugin".to_string(),
reason: "Missing file".to_string(),
};
assert!(load_failed.to_string().contains("Missing file"));
let init_failed = PluginError::InitializationFailed {
plugin_id: "test-plugin".to_string(),
};
assert!(init_failed.to_string().contains("test-plugin"));
let version_incompatible = PluginError::VersionIncompatible {
plugin_id: "test-plugin".to_string(),
version: "2.0".to_string(),
required: "1.0".to_string(),
};
assert!(version_incompatible.to_string().contains("2.0"));
}
#[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_all_error_contexts() {
let base_err = MdBookLintError::document_error("Base error");
let result: Result<()> = Err(MdBookLintError::document_error("Base error"));
let with_rule = result.with_rule_context("MD001");
assert!(with_rule.is_err());
let result: Result<()> = Err(MdBookLintError::document_error("Base error"));
let with_doc = result.with_document_context("test.md");
assert!(with_doc.is_err());
let result: Result<()> = Err(MdBookLintError::document_error("Base error"));
let with_plugin = result.with_plugin_context("test-plugin");
assert!(with_plugin.is_err());
let result: Result<()> = Err(base_err);
let with_config = result.with_config_context("config.toml");
assert!(with_config.is_err());
}
#[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_all_error_conversions() {
let doc_err = DocumentError::parse_failed("Parse failed");
let result: std::result::Result<(), _> = Err(doc_err);
let converted = result.into_mdbook_lint_error();
assert!(converted.is_err());
assert!(matches!(
converted.unwrap_err(),
MdBookLintError::Document(_)
));
let config_err = ConfigError::InvalidFormat {
message: "Bad format".to_string(),
};
let result: std::result::Result<(), _> = Err(config_err);
let converted = result.into_mdbook_lint_error();
assert!(converted.is_err());
assert!(matches!(converted.unwrap_err(), MdBookLintError::Config(_)));
let plugin_err = PluginError::NotFound {
plugin_id: "missing".to_string(),
};
let result: std::result::Result<(), _> = Err(plugin_err);
let converted = result.into_mdbook_lint_error();
assert!(converted.is_err());
assert!(matches!(converted.unwrap_err(), MdBookLintError::Plugin(_)));
}
#[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"));
}
#[test]
fn test_error_chain_context() {
let base_err = MdBookLintError::parse_error(1, 1, "Parse error");
let result: Result<()> = Err(base_err);
let chained: Result<()> = result.with_document_context("test.md");
assert!(chained.is_err());
let error_string = chained.unwrap_err().to_string();
assert!(
error_string.contains("Parse error"),
"Error should contain original message"
);
}
#[test]
fn test_error_source_chain() {
use std::error::Error;
let inner_err = std::io::Error::new(std::io::ErrorKind::NotFound, "File not found");
let mdbook_err: MdBookLintError = inner_err.into();
assert!(mdbook_err.source().is_some());
assert!(
mdbook_err
.source()
.unwrap()
.to_string()
.contains("File not found")
);
}
#[test]
fn test_mdlnt_error_alias() {
let _err: MdlntError = MdBookLintError::document_error("Test");
}
}