1use thiserror::Error;
112
113#[derive(Debug, Error)]
115pub enum MdBookLintError {
116 #[error("IO error: {0}")]
118 Io(#[from] std::io::Error),
119
120 #[error("Parse error at line {line}, column {column}: {message}")]
122 Parse {
123 line: usize,
124 column: usize,
125 message: String,
126 },
127
128 #[error("Configuration error: {0}")]
130 Config(String),
131
132 #[error("Rule error in {rule_id}: {message}")]
134 Rule { rule_id: String, message: String },
135
136 #[error("Plugin error: {0}")]
138 Plugin(String),
139
140 #[error("Document error: {0}")]
142 Document(String),
143
144 #[error("Registry error: {0}")]
146 Registry(String),
147
148 #[error("JSON error: {0}")]
150 Json(#[from] serde_json::Error),
151
152 #[error("YAML error: {0}")]
154 Yaml(#[from] serde_yaml::Error),
155
156 #[error("TOML error: {0}")]
158 Toml(#[from] toml::de::Error),
159
160 #[error("Directory traversal error: {0}")]
162 WalkDir(#[from] walkdir::Error),
163}
164
165#[derive(Debug, Error)]
167pub enum RuleError {
168 #[error("Rule not found: {rule_id}")]
170 NotFound { rule_id: String },
171
172 #[error("Rule execution failed: {message}")]
174 ExecutionFailed { message: String },
175
176 #[error("Invalid rule configuration: {message}")]
178 InvalidConfig { message: String },
179
180 #[error("Rule dependency not satisfied: {rule_id} requires {dependency}")]
182 DependencyNotMet { rule_id: String, dependency: String },
183
184 #[error("Rule registration conflict: rule {rule_id} already exists")]
186 RegistrationConflict { rule_id: String },
187}
188
189#[derive(Debug, Error)]
191pub enum DocumentError {
192 #[error("Failed to read document: {path}")]
194 ReadFailed { path: String },
195
196 #[error("Invalid document format")]
198 InvalidFormat,
199
200 #[error("Document too large: {size} bytes (max: {max_size})")]
202 TooLarge { size: usize, max_size: usize },
203
204 #[error("Failed to parse document: {reason}")]
206 ParseFailed { reason: String },
207
208 #[error("Invalid encoding in document: {path}")]
210 InvalidEncoding { path: String },
211}
212
213#[derive(Debug, Error)]
215pub enum ConfigError {
216 #[error("Configuration file not found: {path}")]
218 NotFound { path: String },
219
220 #[error("Invalid configuration format: {message}")]
222 InvalidFormat { message: String },
223
224 #[error("Configuration validation failed: {field} - {message}")]
226 ValidationFailed { field: String, message: String },
227
228 #[error("Unsupported configuration version: {version} (supported: {supported})")]
230 UnsupportedVersion { version: String, supported: String },
231}
232
233#[derive(Debug, Error)]
235pub enum PluginError {
236 #[error("Plugin not found: {plugin_id}")]
238 NotFound { plugin_id: String },
239
240 #[error("Failed to load plugin {plugin_id}: {reason}")]
242 LoadFailed { plugin_id: String, reason: String },
243
244 #[error("Plugin initialization failed: {plugin_id}")]
246 InitializationFailed { plugin_id: String },
247
248 #[error("Plugin version incompatible: {plugin_id} version {version} (required: {required})")]
250 VersionIncompatible {
251 plugin_id: String,
252 version: String,
253 required: String,
254 },
255}
256
257pub type Result<T> = std::result::Result<T, MdBookLintError>;
259
260impl MdBookLintError {
262 pub fn parse_error(line: usize, column: usize, message: impl Into<String>) -> Self {
264 Self::Parse {
265 line,
266 column,
267 message: message.into(),
268 }
269 }
270
271 pub fn rule_error(rule_id: impl Into<String>, message: impl Into<String>) -> Self {
273 Self::Rule {
274 rule_id: rule_id.into(),
275 message: message.into(),
276 }
277 }
278
279 pub fn config_error(message: impl Into<String>) -> Self {
281 Self::Config(message.into())
282 }
283
284 pub fn document_error(message: impl Into<String>) -> Self {
286 Self::Document(message.into())
287 }
288
289 pub fn plugin_error(message: impl Into<String>) -> Self {
291 Self::Plugin(message.into())
292 }
293
294 pub fn registry_error(message: impl Into<String>) -> Self {
296 Self::Registry(message.into())
297 }
298}
299
300impl RuleError {
302 pub fn not_found(rule_id: impl Into<String>) -> Self {
304 Self::NotFound {
305 rule_id: rule_id.into(),
306 }
307 }
308
309 pub fn execution_failed(message: impl Into<String>) -> Self {
311 Self::ExecutionFailed {
312 message: message.into(),
313 }
314 }
315
316 pub fn invalid_config(message: impl Into<String>) -> Self {
318 Self::InvalidConfig {
319 message: message.into(),
320 }
321 }
322
323 pub fn dependency_not_met(rule_id: impl Into<String>, dependency: impl Into<String>) -> Self {
325 Self::DependencyNotMet {
326 rule_id: rule_id.into(),
327 dependency: dependency.into(),
328 }
329 }
330
331 pub fn registration_conflict(rule_id: impl Into<String>) -> Self {
333 Self::RegistrationConflict {
334 rule_id: rule_id.into(),
335 }
336 }
337}
338
339impl DocumentError {
341 pub fn read_failed(path: impl Into<String>) -> Self {
343 Self::ReadFailed { path: path.into() }
344 }
345
346 pub fn parse_failed(reason: impl Into<String>) -> Self {
348 Self::ParseFailed {
349 reason: reason.into(),
350 }
351 }
352
353 pub fn too_large(size: usize, max_size: usize) -> Self {
355 Self::TooLarge { size, max_size }
356 }
357
358 pub fn invalid_encoding(path: impl Into<String>) -> Self {
360 Self::InvalidEncoding { path: path.into() }
361 }
362}
363
364pub trait ErrorContext<T> {
366 fn with_rule_context(self, rule_id: &str) -> Result<T>;
368
369 fn with_document_context(self, path: &str) -> Result<T>;
371
372 fn with_plugin_context(self, plugin_id: &str) -> Result<T>;
374
375 fn with_config_context(self, field: &str) -> Result<T>;
377}
378
379impl<T> ErrorContext<T> for std::result::Result<T, MdBookLintError> {
380 fn with_rule_context(self, rule_id: &str) -> Result<T> {
381 self.map_err(|e| match e {
382 MdBookLintError::Rule { message, .. } => MdBookLintError::Rule {
383 rule_id: rule_id.to_string(),
384 message,
385 },
386 other => other,
387 })
388 }
389
390 fn with_document_context(self, path: &str) -> Result<T> {
391 self.map_err(|e| match e {
392 MdBookLintError::Document(message) => {
393 MdBookLintError::Document(format!("{path}: {message}"))
394 }
395 other => other,
396 })
397 }
398
399 fn with_plugin_context(self, plugin_id: &str) -> Result<T> {
400 self.map_err(|e| match e {
401 MdBookLintError::Plugin(message) => {
402 MdBookLintError::Plugin(format!("{plugin_id}: {message}"))
403 }
404 other => other,
405 })
406 }
407
408 fn with_config_context(self, field: &str) -> Result<T> {
409 self.map_err(|e| match e {
410 MdBookLintError::Config(message) => {
411 MdBookLintError::Config(format!("{field}: {message}"))
412 }
413 other => other,
414 })
415 }
416}
417
418pub trait IntoMdBookLintError<T> {
420 fn into_mdbook_lint_error(self) -> Result<T>;
422}
423
424impl<T> IntoMdBookLintError<T> for std::result::Result<T, RuleError> {
425 fn into_mdbook_lint_error(self) -> Result<T> {
426 self.map_err(|e| match e {
427 RuleError::NotFound { rule_id } => {
428 MdBookLintError::rule_error(rule_id, "Rule not found")
429 }
430 RuleError::ExecutionFailed { message } => {
431 MdBookLintError::rule_error("unknown", message)
432 }
433 RuleError::InvalidConfig { message } => MdBookLintError::config_error(message),
434 RuleError::DependencyNotMet {
435 rule_id,
436 dependency,
437 } => MdBookLintError::rule_error(rule_id, format!("Dependency not met: {dependency}")),
438 RuleError::RegistrationConflict { rule_id } => {
439 MdBookLintError::registry_error(format!("Rule already exists: {rule_id}"))
440 }
441 })
442 }
443}
444
445impl<T> IntoMdBookLintError<T> for std::result::Result<T, DocumentError> {
446 fn into_mdbook_lint_error(self) -> Result<T> {
447 self.map_err(|e| match e {
448 DocumentError::ReadFailed { path } => {
449 MdBookLintError::document_error(format!("Failed to read: {path}"))
450 }
451 DocumentError::InvalidFormat => {
452 MdBookLintError::document_error("Invalid document format")
453 }
454 DocumentError::TooLarge { size, max_size } => MdBookLintError::document_error(format!(
455 "Document too large: {size} bytes (max: {max_size})"
456 )),
457 DocumentError::ParseFailed { reason } => {
458 MdBookLintError::document_error(format!("Parse failed: {reason}"))
459 }
460 DocumentError::InvalidEncoding { path } => {
461 MdBookLintError::document_error(format!("Invalid encoding: {path}"))
462 }
463 })
464 }
465}
466
467impl<T> IntoMdBookLintError<T> for std::result::Result<T, ConfigError> {
468 fn into_mdbook_lint_error(self) -> Result<T> {
469 self.map_err(|e| match e {
470 ConfigError::NotFound { path } => {
471 MdBookLintError::config_error(format!("Configuration not found: {path}"))
472 }
473 ConfigError::InvalidFormat { message } => {
474 MdBookLintError::config_error(format!("Invalid format: {message}"))
475 }
476 ConfigError::ValidationFailed { field, message } => {
477 MdBookLintError::config_error(format!("Validation failed for {field}: {message}"))
478 }
479 ConfigError::UnsupportedVersion { version, supported } => {
480 MdBookLintError::config_error(format!(
481 "Unsupported version: {version} (supported: {supported})"
482 ))
483 }
484 })
485 }
486}
487
488impl<T> IntoMdBookLintError<T> for std::result::Result<T, PluginError> {
489 fn into_mdbook_lint_error(self) -> Result<T> {
490 self.map_err(|e| match e {
491 PluginError::NotFound { plugin_id } => {
492 MdBookLintError::plugin_error(format!("Plugin not found: {plugin_id}"))
493 }
494 PluginError::LoadFailed { plugin_id, reason } => {
495 MdBookLintError::plugin_error(format!("Failed to load {plugin_id}: {reason}"))
496 }
497 PluginError::InitializationFailed { plugin_id } => {
498 MdBookLintError::plugin_error(format!("Initialization failed: {plugin_id}"))
499 }
500 PluginError::VersionIncompatible {
501 plugin_id,
502 version,
503 required,
504 } => MdBookLintError::plugin_error(format!(
505 "Version incompatible: {plugin_id} version {version} (required: {required})"
506 )),
507 })
508 }
509}
510
511impl From<anyhow::Error> for MdBookLintError {
513 fn from(err: anyhow::Error) -> Self {
514 MdBookLintError::Document(err.to_string())
515 }
516}
517
518pub type MdlntError = MdBookLintError;
523
524#[cfg(test)]
525mod tests {
526 use super::*;
527
528 #[test]
529 fn test_error_creation() {
530 let err = MdBookLintError::parse_error(10, 5, "Invalid syntax");
531 assert!(matches!(
532 err,
533 MdBookLintError::Parse {
534 line: 10,
535 column: 5,
536 ..
537 }
538 ));
539 assert!(err.to_string().contains("line 10, column 5"));
540 }
541
542 #[test]
543 fn test_all_error_variants() {
544 let config_err = MdBookLintError::config_error("Invalid config");
546 assert!(matches!(config_err, MdBookLintError::Config(_)));
547
548 let rule_err = MdBookLintError::rule_error("MD001", "Rule failed");
550 assert!(matches!(rule_err, MdBookLintError::Rule { .. }));
551
552 let plugin_err = MdBookLintError::plugin_error("Plugin failed");
554 assert!(matches!(plugin_err, MdBookLintError::Plugin(_)));
555
556 let doc_err = MdBookLintError::document_error("Document error");
558 assert!(matches!(doc_err, MdBookLintError::Document(_)));
559
560 let registry_err = MdBookLintError::registry_error("Registry error");
562 assert!(matches!(registry_err, MdBookLintError::Registry(_)));
563 }
564
565 #[test]
566 fn test_rule_error_variants() {
567 let not_found = RuleError::not_found("MD999");
568 assert!(matches!(not_found, RuleError::NotFound { .. }));
569 assert!(not_found.to_string().contains("MD999"));
570
571 let exec_failed = RuleError::execution_failed("Test execution failed");
572 assert!(matches!(exec_failed, RuleError::ExecutionFailed { .. }));
573
574 let invalid_config = RuleError::invalid_config("Invalid rule config");
575 assert!(matches!(invalid_config, RuleError::InvalidConfig { .. }));
576
577 let dep_not_met = RuleError::dependency_not_met("MD001", "MD002");
578 assert!(matches!(dep_not_met, RuleError::DependencyNotMet { .. }));
579
580 let reg_conflict = RuleError::registration_conflict("MD001");
581 assert!(matches!(
582 reg_conflict,
583 RuleError::RegistrationConflict { .. }
584 ));
585 }
586
587 #[test]
588 fn test_document_error_variants() {
589 let read_failed = DocumentError::read_failed("test.md");
590 assert!(matches!(read_failed, DocumentError::ReadFailed { .. }));
591
592 let parse_failed = DocumentError::parse_failed("Parse error");
593 assert!(matches!(parse_failed, DocumentError::ParseFailed { .. }));
594
595 let too_large = DocumentError::too_large(1000, 500);
596 assert!(matches!(too_large, DocumentError::TooLarge { .. }));
597
598 let invalid_encoding = DocumentError::invalid_encoding("test.md");
599 assert!(matches!(
600 invalid_encoding,
601 DocumentError::InvalidEncoding { .. }
602 ));
603 }
604
605 #[test]
606 fn test_config_error_variants() {
607 let not_found = ConfigError::NotFound {
608 path: "config.toml".to_string(),
609 };
610 assert!(not_found.to_string().contains("config.toml"));
611
612 let invalid_format = ConfigError::InvalidFormat {
613 message: "Bad YAML".to_string(),
614 };
615 assert!(invalid_format.to_string().contains("Bad YAML"));
616
617 let validation_failed = ConfigError::ValidationFailed {
618 field: "rules".to_string(),
619 message: "Invalid rule".to_string(),
620 };
621 assert!(validation_failed.to_string().contains("rules"));
622
623 let unsupported_version = ConfigError::UnsupportedVersion {
624 version: "2.0".to_string(),
625 supported: "1.0-1.5".to_string(),
626 };
627 assert!(unsupported_version.to_string().contains("2.0"));
628 }
629
630 #[test]
631 fn test_plugin_error_variants() {
632 let not_found = PluginError::NotFound {
633 plugin_id: "test-plugin".to_string(),
634 };
635 assert!(not_found.to_string().contains("test-plugin"));
636
637 let load_failed = PluginError::LoadFailed {
638 plugin_id: "test-plugin".to_string(),
639 reason: "Missing file".to_string(),
640 };
641 assert!(load_failed.to_string().contains("Missing file"));
642
643 let init_failed = PluginError::InitializationFailed {
644 plugin_id: "test-plugin".to_string(),
645 };
646 assert!(init_failed.to_string().contains("test-plugin"));
647
648 let version_incompatible = PluginError::VersionIncompatible {
649 plugin_id: "test-plugin".to_string(),
650 version: "2.0".to_string(),
651 required: "1.0".to_string(),
652 };
653 assert!(version_incompatible.to_string().contains("2.0"));
654 }
655
656 #[test]
657 fn test_error_context() {
658 let result: Result<()> = Err(MdBookLintError::document_error("Something went wrong"));
659 let with_context = result.with_document_context("test.md");
660
661 assert!(with_context.is_err());
662 assert!(with_context.unwrap_err().to_string().contains("test.md"));
663 }
664
665 #[test]
666 fn test_all_error_contexts() {
667 let base_err = MdBookLintError::document_error("Base error");
668
669 let result: Result<()> = Err(MdBookLintError::document_error("Base error"));
670 let with_rule = result.with_rule_context("MD001");
671 assert!(with_rule.is_err());
672
673 let result: Result<()> = Err(MdBookLintError::document_error("Base error"));
674 let with_doc = result.with_document_context("test.md");
675 assert!(with_doc.is_err());
676
677 let result: Result<()> = Err(MdBookLintError::document_error("Base error"));
678 let with_plugin = result.with_plugin_context("test-plugin");
679 assert!(with_plugin.is_err());
680
681 let result: Result<()> = Err(base_err);
682 let with_config = result.with_config_context("config.toml");
683 assert!(with_config.is_err());
684 }
685
686 #[test]
687 fn test_rule_error_conversion() {
688 let rule_err = RuleError::not_found("MD001");
689 let result: std::result::Result<(), _> = Err(rule_err);
690 let result = result.into_mdbook_lint_error();
691
692 assert!(result.is_err());
693 assert!(result.unwrap_err().to_string().contains("MD001"));
694 }
695
696 #[test]
697 fn test_all_error_conversions() {
698 let doc_err = DocumentError::parse_failed("Parse failed");
700 let result: std::result::Result<(), _> = Err(doc_err);
701 let converted = result.into_mdbook_lint_error();
702 assert!(converted.is_err());
703 assert!(matches!(
704 converted.unwrap_err(),
705 MdBookLintError::Document(_)
706 ));
707
708 let config_err = ConfigError::InvalidFormat {
710 message: "Bad format".to_string(),
711 };
712 let result: std::result::Result<(), _> = Err(config_err);
713 let converted = result.into_mdbook_lint_error();
714 assert!(converted.is_err());
715 assert!(matches!(converted.unwrap_err(), MdBookLintError::Config(_)));
716
717 let plugin_err = PluginError::NotFound {
719 plugin_id: "missing".to_string(),
720 };
721 let result: std::result::Result<(), _> = Err(plugin_err);
722 let converted = result.into_mdbook_lint_error();
723 assert!(converted.is_err());
724 assert!(matches!(converted.unwrap_err(), MdBookLintError::Plugin(_)));
725 }
726
727 #[test]
728 fn test_anyhow_compatibility() {
729 let anyhow_err = anyhow::anyhow!("Test error");
730 let mdbook_lint_err: MdBookLintError = anyhow_err.into();
731 let back_to_anyhow = anyhow::Error::from(mdbook_lint_err);
733
734 assert!(back_to_anyhow.to_string().contains("Test error"));
735 }
736
737 #[test]
738 fn test_io_error_conversion() {
739 use std::io::{Error, ErrorKind};
740
741 let io_err = Error::new(ErrorKind::NotFound, "File not found");
742 let mdbook_lint_err: MdBookLintError = io_err.into();
743
744 assert!(matches!(mdbook_lint_err, MdBookLintError::Io(_)));
745 assert!(mdbook_lint_err.to_string().contains("File not found"));
746 }
747
748 #[test]
749 fn test_error_chain_context() {
750 let base_err = MdBookLintError::parse_error(1, 1, "Parse error");
752 let result: Result<()> = Err(base_err);
753
754 let chained: Result<()> = result.with_document_context("test.md");
755
756 assert!(chained.is_err());
757 let error_string = chained.unwrap_err().to_string();
758 assert!(
759 error_string.contains("Parse error"),
760 "Error should contain original message"
761 );
762 }
763
764 #[test]
765 fn test_error_source_chain() {
766 use std::error::Error;
767
768 let inner_err = std::io::Error::new(std::io::ErrorKind::NotFound, "File not found");
769 let mdbook_err: MdBookLintError = inner_err.into();
770
771 assert!(mdbook_err.source().is_some());
773 assert!(
774 mdbook_err
775 .source()
776 .unwrap()
777 .to_string()
778 .contains("File not found")
779 );
780 }
781
782 #[test]
783 fn test_mdlnt_error_alias() {
784 let _err: MdlntError = MdBookLintError::document_error("Test");
786 }
788}