Skip to main content

mdx_gen/
error.rs

1//! Error handling for the MDX Gen library.
2
3use crate::validation::ValidationError;
4
5/// Represents all the errors that can occur during Markdown processing.
6#[derive(thiserror::Error, Debug)]
7pub enum MarkdownError {
8    /// An error occurred while parsing the Markdown content.
9    #[error("Failed to parse Markdown: {0}")]
10    ParseError(String),
11
12    /// An error occurred while converting Markdown to HTML.
13    #[error("Failed to convert Markdown to HTML: {0}")]
14    ConversionError(String),
15
16    /// An error occurred while processing a custom block.
17    #[error("Failed to process custom block: {0}")]
18    CustomBlockError(String),
19
20    /// An error occurred while applying syntax highlighting.
21    #[error("Syntax highlighting error: {0}")]
22    SyntaxHighlightError(String),
23
24    /// An error occurred due to invalid options.
25    #[error("Invalid Markdown options: {0}")]
26    InvalidOptionsError(String),
27
28    /// An error occurred while loading a syntax set.
29    #[error("Failed to load syntax set: {0}")]
30    SyntaxSetError(String),
31
32    /// The input exceeds the configured maximum size.
33    #[error(
34        "Input too large: {size} bytes exceeds limit of {limit} bytes"
35    )]
36    InputTooLarge {
37        /// Actual input size in bytes.
38        size: usize,
39        /// Configured maximum in bytes.
40        limit: usize,
41    },
42
43    /// An error occurred while rendering HTML.
44    #[error("HTML rendering error: {0}")]
45    RenderError(String),
46
47    /// An error occurred while writing output to a `Write` sink.
48    #[error("Output write error: {0}")]
49    IoError(#[from] std::io::Error),
50}
51
52/// Map a single [`ValidationError`] into a domain
53/// [`MarkdownError::InvalidOptionsError`].
54impl From<ValidationError> for MarkdownError {
55    fn from(err: ValidationError) -> Self {
56        MarkdownError::InvalidOptionsError(err.to_string())
57    }
58}
59
60/// Map the multi-error form produced by
61/// [`Validator::finish`](crate::validation::Validator::finish) into a
62/// domain [`MarkdownError::InvalidOptionsError`]. Every failing check
63/// is joined into a single human-readable message with the field name
64/// preserved.
65///
66/// This is what
67/// [`MarkdownOptions::validate`](crate::MarkdownOptions::validate)
68/// returns; the pipeline converts via `?`.
69impl From<Vec<(String, ValidationError)>> for MarkdownError {
70    fn from(errors: Vec<(String, ValidationError)>) -> Self {
71        let msg = errors
72            .iter()
73            .map(|(field, err)| format!("{field}: {err}"))
74            .collect::<Vec<_>>()
75            .join("; ");
76        MarkdownError::InvalidOptionsError(msg)
77    }
78}
79
80#[cfg(test)]
81mod tests {
82    use super::*;
83
84    #[test]
85    fn test_error_display() {
86        let cases: Vec<(MarkdownError, &str)> = vec![
87            (
88                MarkdownError::ParseError("bad input".into()),
89                "Failed to parse Markdown: bad input",
90            ),
91            (
92                MarkdownError::ConversionError("failed".into()),
93                "Failed to convert Markdown to HTML: failed",
94            ),
95            (
96                MarkdownError::InputTooLarge {
97                    size: 2_000_000,
98                    limit: 1_000_000,
99                },
100                "Input too large: 2000000 bytes exceeds limit of 1000000 bytes",
101            ),
102            (
103                MarkdownError::RenderError("fmt".into()),
104                "HTML rendering error: fmt",
105            ),
106        ];
107
108        for (error, expected) in cases {
109            assert_eq!(format!("{error}"), expected);
110        }
111    }
112
113    #[test]
114    fn test_from_validation_error() {
115        // Exact-string assertion on Display (which `thiserror` derives
116        // from the `#[error(...)]` attribute on `InvalidOptionsError`)
117        // implicitly proves the variant is `InvalidOptionsError` — no
118        // pattern-match branch whose no-match arm would be
119        // uncoverable.
120        let err: MarkdownError = ValidationError::Empty.into();
121        assert_eq!(
122            err.to_string(),
123            "Invalid Markdown options: Value cannot be empty"
124        );
125    }
126
127    #[test]
128    fn test_from_validation_error_vec_joins_fields() {
129        let errs = vec![
130            ("name".into(), ValidationError::Empty),
131            (
132                "pattern".into(),
133                ValidationError::InvalidPattern {
134                    pattern: "email".into(),
135                },
136            ),
137        ];
138        let err: MarkdownError = errs.into();
139        let msg = err.to_string();
140        assert!(
141            msg.starts_with("Invalid Markdown options: "),
142            "expected InvalidOptionsError Display prefix, got: {msg}"
143        );
144        assert!(msg.contains("name: Value cannot be empty"));
145        assert!(
146            msg.contains("pattern: Value doesn't match pattern: email")
147        );
148        assert!(msg.contains("; "));
149    }
150}