Skip to main content

html_generator/
error.rs

1// Copyright © 2025 HTML Generator. All rights reserved.
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3
4//! Error types for HTML generation and processing.
5//!
6//! This module defines custom error types used throughout the HTML generation library.
7//! It provides a centralized location for all error definitions, making it easier to manage and handle errors consistently across the codebase.
8
9use std::io;
10use thiserror::Error;
11
12/// Enum to represent various errors that can occur during HTML generation, processing, or optimization.
13///
14/// # Examples
15///
16/// ```
17/// use html_generator::error::HtmlError;
18///
19/// let err = HtmlError::InvalidInput("empty document".into());
20/// assert!(err.to_string().contains("Invalid input"));
21/// ```
22#[derive(Error, Debug)]
23pub enum HtmlError {
24    /// Error that occurs when a regular expression fails to compile.
25    ///
26    /// This variant contains the underlying error from the `regex` crate.
27    #[error("Failed to compile regex: {0}")]
28    RegexCompilationError(#[from] regex::Error),
29
30    /// Error indicating failure in extracting front matter from the input content.
31    ///
32    /// This variant is used when there is an issue parsing the front matter of a document.
33    /// The associated string provides details about the error.
34    #[error("Failed to extract front matter: {0}")]
35    FrontMatterExtractionError(String),
36
37    /// Error indicating a failure in formatting an HTML header.
38    ///
39    /// This variant is used when the header cannot be formatted correctly. The associated string provides more details.
40    #[error("Failed to format header: {0}")]
41    HeaderFormattingError(String),
42
43    /// Error that occurs when parsing a selector fails.
44    ///
45    /// This variant is used when a CSS or HTML selector cannot be parsed.
46    /// The first string is the selector, and the second string provides additional context.
47    #[error("Failed to parse selector '{0}': {1}")]
48    SelectorParseError(String, String),
49
50    /// Error indicating failure to minify HTML content.
51    ///
52    /// This variant is used when there is an issue during the HTML minification process. The associated string provides details.
53    #[error("Failed to minify HTML: {0}")]
54    MinificationError(String),
55
56    /// Error that occurs during the conversion of Markdown to HTML.
57    ///
58    /// This variant is used when the Markdown conversion process encounters an issue. The associated string provides more information.
59    #[error("Failed to convert Markdown to HTML: {message}")]
60    MarkdownConversion {
61        /// The error message
62        message: String,
63        /// The source error, if available
64        #[source]
65        source: Option<io::Error>,
66    },
67
68    /// SEO-related errors.
69    #[error("SEO optimization failed: {kind}: {message}")]
70    Seo {
71        /// The kind of SEO error
72        kind: SeoErrorKind,
73        /// The error message
74        message: String,
75        /// The problematic element, if available
76        element: Option<String>,
77    },
78
79    /// Accessibility-related errors.
80    #[error("Accessibility check failed: {kind}: {message}")]
81    Accessibility {
82        /// The kind of accessibility error
83        kind: ErrorKind,
84        /// The error message
85        message: String,
86        /// The relevant WCAG guideline, if available
87        wcag_guideline: Option<String>,
88    },
89
90    /// Error indicating that a required HTML element is missing.
91    ///
92    /// This variant is used when a necessary HTML element (like a title tag) is not found.
93    #[error("Missing required HTML element: {0}")]
94    MissingHtmlElement(String),
95
96    /// Error that occurs when structured data is invalid.
97    ///
98    /// This variant is used when JSON-LD or other structured data does not meet the expected format or requirements.
99    #[error("Invalid structured data: {0}")]
100    InvalidStructuredData(String),
101
102    /// Input/Output errors
103    ///
104    /// This variant is used when an IO operation fails (e.g., reading or writing files).
105    #[error("IO error: {0}")]
106    Io(#[from] io::Error),
107
108    /// Error indicating an invalid input.
109    ///
110    /// This variant is used when the input content is invalid or does not meet the expected criteria.
111    #[error("Invalid input: {0}")]
112    InvalidInput(String),
113
114    /// Error indicating an invalid front matter format.
115    ///
116    /// This variant is used when the front matter of a document does not follow the expected format.
117    #[error("Invalid front matter format: {0}")]
118    InvalidFrontMatterFormat(String),
119
120    /// Error indicating an input that is too large.
121    ///
122    /// This variant is used when the input content exceeds a certain size limit.
123    #[error("Input too large: size {0} bytes")]
124    InputTooLarge(usize),
125
126    /// Error indicating an invalid header format.
127    ///
128    /// This variant is used when an HTML header does not conform to the expected format.
129    #[error("Invalid header format: {0}")]
130    InvalidHeaderFormat(String),
131
132    /// Error that occurs when converting from UTF-8 fails.
133    ///
134    /// This variant wraps errors that occur when converting a byte sequence to a UTF-8 string.
135    #[error("UTF-8 conversion error: {0}")]
136    Utf8ConversionError(#[from] std::string::FromUtf8Error),
137
138    /// Error indicating a failure during parsing.
139    ///
140    /// This variant is used for general parsing errors where the specific source of the issue isn't covered by other variants.
141    #[error("Parsing error: {0}")]
142    ParsingError(String),
143
144    /// Errors that occur during template rendering.
145    #[error("Template rendering failed: {message}")]
146    TemplateRendering {
147        /// The error message
148        message: String,
149        /// The source error, if available
150        #[source]
151        source: Box<dyn std::error::Error + Send + Sync>,
152    },
153
154    /// Error indicating a validation failure.
155    ///
156    /// This variant is used when a validation step fails, such as schema validation or data integrity checks.
157    #[error("Validation error: {0}")]
158    ValidationError(String),
159
160    /// A catch-all error for unexpected failures.
161    ///
162    /// This variant is used for errors that do not fit into other categories.
163    #[error("Unexpected error: {0}")]
164    UnexpectedError(String),
165}
166
167/// Types of SEO-related errors
168///
169/// # Examples
170///
171/// ```
172/// use html_generator::error::SeoErrorKind;
173///
174/// assert_eq!(SeoErrorKind::MissingTitle.to_string(), "Missing title");
175/// ```
176#[derive(Debug, Copy, Clone, PartialEq, Eq)]
177pub enum SeoErrorKind {
178    /// Missing required meta tags
179    MissingMetaTags,
180    /// Invalid input
181    InvalidInput,
182    /// Invalid structured data
183    InvalidStructuredData,
184    /// Missing title
185    MissingTitle,
186    /// Missing description
187    MissingDescription,
188    /// Other SEO-related errors
189    Other,
190}
191
192/// Types of accessibility-related errors
193///
194/// # Examples
195///
196/// ```
197/// use html_generator::error::ErrorKind;
198///
199/// assert_eq!(
200///     ErrorKind::MissingAriaAttributes.to_string(),
201///     "Missing ARIA attributes"
202/// );
203/// ```
204#[derive(Debug, Copy, Clone, PartialEq, Eq)]
205pub enum ErrorKind {
206    /// Missing ARIA attributes
207    MissingAriaAttributes,
208    /// Invalid ARIA attribute values
209    InvalidAriaValue,
210    /// Missing alternative text
211    MissingAltText,
212    /// Incorrect heading structure
213    HeadingStructure,
214    /// Missing form labels
215    MissingFormLabels,
216    /// Other accessibility-related errors
217    Other,
218}
219
220impl std::fmt::Display for ErrorKind {
221    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
222        match self {
223            ErrorKind::MissingAriaAttributes => {
224                write!(f, "Missing ARIA attributes")
225            }
226            ErrorKind::InvalidAriaValue => {
227                write!(f, "Invalid ARIA attribute values")
228            }
229            ErrorKind::MissingAltText => {
230                write!(f, "Missing alternative text")
231            }
232            ErrorKind::HeadingStructure => {
233                write!(f, "Incorrect heading structure")
234            }
235            ErrorKind::MissingFormLabels => {
236                write!(f, "Missing form labels")
237            }
238            ErrorKind::Other => {
239                write!(f, "Other accessibility-related errors")
240            }
241        }
242    }
243}
244
245impl std::fmt::Display for SeoErrorKind {
246    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
247        match self {
248            SeoErrorKind::MissingMetaTags => {
249                write!(f, "Missing required meta tags")
250            }
251            SeoErrorKind::InvalidStructuredData => {
252                write!(f, "Invalid structured data")
253            }
254            SeoErrorKind::MissingTitle => write!(f, "Missing title"),
255            SeoErrorKind::InvalidInput => write!(f, "Invalid input"),
256            SeoErrorKind::MissingDescription => {
257                write!(f, "Missing description")
258            }
259            SeoErrorKind::Other => {
260                write!(f, "Other SEO-related errors")
261            }
262        }
263    }
264}
265
266impl HtmlError {
267    /// Creates a new `InvalidInput` error.
268    ///
269    /// # Examples
270    ///
271    /// ```
272    /// use html_generator::error::HtmlError;
273    ///
274    /// let err = HtmlError::invalid_input("empty document", None);
275    /// assert!(matches!(err, HtmlError::InvalidInput(_)));
276    /// ```
277    pub fn invalid_input(
278        message: impl Into<String>,
279        _input: Option<String>,
280    ) -> Self {
281        Self::InvalidInput(message.into())
282    }
283
284    /// Creates a new `InputTooLarge` error.
285    ///
286    /// # Examples
287    ///
288    /// ```
289    /// use html_generator::error::HtmlError;
290    ///
291    /// let err = HtmlError::input_too_large(1_048_576);
292    /// assert!(matches!(err, HtmlError::InputTooLarge(1_048_576)));
293    /// ```
294    pub fn input_too_large(size: usize) -> Self {
295        Self::InputTooLarge(size)
296    }
297
298    /// Creates a new `Seo` error.
299    ///
300    /// # Examples
301    ///
302    /// ```
303    /// use html_generator::error::{HtmlError, SeoErrorKind};
304    ///
305    /// let err = HtmlError::seo(SeoErrorKind::MissingTitle, "no <h1>", None);
306    /// assert!(matches!(err, HtmlError::Seo { .. }));
307    /// ```
308    pub fn seo(
309        kind: SeoErrorKind,
310        message: impl Into<String>,
311        element: Option<String>,
312    ) -> Self {
313        Self::Seo {
314            kind,
315            message: message.into(),
316            element,
317        }
318    }
319
320    /// Creates a new `Accessibility` error.
321    ///
322    /// # Examples
323    ///
324    /// ```
325    /// use html_generator::error::{ErrorKind, HtmlError};
326    ///
327    /// let err = HtmlError::accessibility(
328    ///     ErrorKind::MissingAriaAttributes,
329    ///     "button without aria-label",
330    ///     Some("WCAG 4.1.2".into()),
331    /// );
332    /// assert!(matches!(err, HtmlError::Accessibility { .. }));
333    /// ```
334    pub fn accessibility(
335        kind: ErrorKind,
336        message: impl Into<String>,
337        wcag_guideline: Option<String>,
338    ) -> Self {
339        Self::Accessibility {
340            kind,
341            message: message.into(),
342            wcag_guideline,
343        }
344    }
345
346    /// Creates a new `MarkdownConversion` error.
347    ///
348    /// # Examples
349    ///
350    /// ```
351    /// use html_generator::error::HtmlError;
352    ///
353    /// let err = HtmlError::markdown_conversion("comrak failed", None);
354    /// assert!(matches!(err, HtmlError::MarkdownConversion { .. }));
355    /// ```
356    pub fn markdown_conversion(
357        message: impl Into<String>,
358        source: Option<io::Error>,
359    ) -> Self {
360        Self::MarkdownConversion {
361            message: message.into(),
362            source,
363        }
364    }
365}
366
367/// Type alias for a result using the `HtmlError` error type.
368///
369/// This type alias makes it more convenient to work with Results throughout the library,
370/// reducing boilerplate and improving readability.
371///
372/// # Examples
373///
374/// ```
375/// use html_generator::error::{HtmlError, Result};
376///
377/// fn parse(input: &str) -> Result<usize> {
378///     if input.is_empty() {
379///         return Err(HtmlError::InvalidInput("empty".into()));
380///     }
381///     Ok(input.len())
382/// }
383///
384/// assert_eq!(parse("hi").unwrap(), 2);
385/// assert!(parse("").is_err());
386/// ```
387pub type Result<T> = std::result::Result<T, HtmlError>;
388
389#[cfg(test)]
390mod tests {
391    use super::*;
392
393    // Basic Error Creation Tests
394    mod basic_errors {
395        use super::*;
396
397        #[test]
398        fn test_regex_compilation_error() {
399            let regex_error =
400                regex::Error::Syntax("invalid regex".to_string());
401            let error: HtmlError = regex_error.into();
402            assert!(matches!(
403                error,
404                HtmlError::RegexCompilationError(_)
405            ));
406            assert!(error
407                .to_string()
408                .contains("Failed to compile regex"));
409        }
410
411        #[test]
412        fn test_front_matter_extraction_error() {
413            let error = HtmlError::FrontMatterExtractionError(
414                "Missing delimiter".to_string(),
415            );
416            assert_eq!(
417                error.to_string(),
418                "Failed to extract front matter: Missing delimiter"
419            );
420        }
421
422        #[test]
423        fn test_header_formatting_error() {
424            let error = HtmlError::HeaderFormattingError(
425                "Invalid header level".to_string(),
426            );
427            assert_eq!(
428                error.to_string(),
429                "Failed to format header: Invalid header level"
430            );
431        }
432
433        #[test]
434        fn test_selector_parse_error() {
435            let error = HtmlError::SelectorParseError(
436                "div>".to_string(),
437                "Unexpected end".to_string(),
438            );
439            assert_eq!(
440                error.to_string(),
441                "Failed to parse selector 'div>': Unexpected end"
442            );
443        }
444
445        #[test]
446        fn test_minification_error() {
447            let error = HtmlError::MinificationError(
448                "Syntax error".to_string(),
449            );
450            assert_eq!(
451                error.to_string(),
452                "Failed to minify HTML: Syntax error"
453            );
454        }
455    }
456
457    // Structured Error Tests
458    mod structured_errors {
459        use super::*;
460
461        #[test]
462        fn test_markdown_conversion_with_source() {
463            let source = io::Error::other("source error");
464            let error = HtmlError::markdown_conversion(
465                "Conversion failed",
466                Some(source),
467            );
468            assert!(error
469                .to_string()
470                .contains("Failed to convert Markdown to HTML"));
471        }
472
473        #[test]
474        fn test_markdown_conversion_without_source() {
475            let error = HtmlError::markdown_conversion(
476                "Conversion failed",
477                None,
478            );
479            assert!(error.to_string().contains("Conversion failed"));
480        }
481    }
482
483    // SEO Error Tests
484    mod seo_errors {
485        use super::*;
486
487        #[test]
488        fn test_seo_error_missing_meta_tags() {
489            let error = HtmlError::seo(
490                SeoErrorKind::MissingMetaTags,
491                "Required meta tags missing",
492                Some("head".to_string()),
493            );
494            assert!(error
495                .to_string()
496                .contains("Missing required meta tags"));
497        }
498
499        #[test]
500        fn test_seo_error_without_element() {
501            let error = HtmlError::seo(
502                SeoErrorKind::MissingTitle,
503                "Title not found",
504                None,
505            );
506            assert!(error.to_string().contains("Missing title"));
507        }
508
509        #[test]
510        fn test_all_seo_error_kinds() {
511            let kinds = [
512                SeoErrorKind::MissingMetaTags,
513                SeoErrorKind::InvalidStructuredData,
514                SeoErrorKind::MissingTitle,
515                SeoErrorKind::MissingDescription,
516                SeoErrorKind::Other,
517            ];
518            for kind in kinds {
519                assert!(!kind.to_string().is_empty());
520            }
521        }
522    }
523
524    // Accessibility Error Tests
525    mod accessibility_errors {
526        use super::*;
527
528        #[test]
529        fn test_accessibility_error_with_guideline() {
530            let error = HtmlError::accessibility(
531                ErrorKind::MissingAltText,
532                "Images must have alt text",
533                Some("WCAG 1.1.1".to_string()),
534            );
535            assert!(error
536                .to_string()
537                .contains("Missing alternative text"));
538        }
539
540        #[test]
541        fn test_accessibility_error_without_guideline() {
542            let error = HtmlError::accessibility(
543                ErrorKind::InvalidAriaValue,
544                "Invalid ARIA value",
545                None,
546            );
547            assert!(error
548                .to_string()
549                .contains("Invalid ARIA attribute values"));
550        }
551
552        #[test]
553        fn test_all_accessibility_error_kinds() {
554            let kinds = [
555                ErrorKind::MissingAriaAttributes,
556                ErrorKind::InvalidAriaValue,
557                ErrorKind::MissingAltText,
558                ErrorKind::HeadingStructure,
559                ErrorKind::MissingFormLabels,
560                ErrorKind::Other,
561            ];
562            for kind in kinds {
563                assert!(!kind.to_string().is_empty());
564            }
565        }
566    }
567
568    // Input/Output Error Tests
569    mod io_errors {
570        use super::*;
571
572        #[test]
573        fn test_io_error_kinds() {
574            let error_kinds = [
575                io::ErrorKind::NotFound,
576                io::ErrorKind::PermissionDenied,
577                io::ErrorKind::ConnectionRefused,
578                io::ErrorKind::ConnectionReset,
579                io::ErrorKind::ConnectionAborted,
580                io::ErrorKind::NotConnected,
581                io::ErrorKind::AddrInUse,
582                io::ErrorKind::AddrNotAvailable,
583                io::ErrorKind::BrokenPipe,
584                io::ErrorKind::AlreadyExists,
585                io::ErrorKind::WouldBlock,
586                io::ErrorKind::InvalidInput,
587                io::ErrorKind::InvalidData,
588                io::ErrorKind::TimedOut,
589                io::ErrorKind::WriteZero,
590                io::ErrorKind::Interrupted,
591                io::ErrorKind::Unsupported,
592                io::ErrorKind::UnexpectedEof,
593                io::ErrorKind::OutOfMemory,
594                io::ErrorKind::Other,
595            ];
596
597            for kind in error_kinds {
598                let io_error = io::Error::new(kind, "test error");
599                let html_error: HtmlError = io_error.into();
600                assert!(matches!(html_error, HtmlError::Io(_)));
601            }
602        }
603    }
604
605    // Helper Method Tests
606    mod helper_methods {
607        use super::*;
608
609        #[test]
610        fn test_invalid_input_with_content() {
611            let error = HtmlError::invalid_input(
612                "Bad input",
613                Some("problematic content".to_string()),
614            );
615            assert!(error.to_string().contains("Invalid input"));
616        }
617
618        #[test]
619        fn test_input_too_large() {
620            let error = HtmlError::input_too_large(1024);
621            assert!(error.to_string().contains("1024 bytes"));
622        }
623
624        #[test]
625        fn test_template_rendering_error() {
626            let source_error =
627                Box::new(io::Error::other("render failed"));
628            let error = HtmlError::TemplateRendering {
629                message: "Template error".to_string(),
630                source: source_error,
631            };
632            assert!(error
633                .to_string()
634                .contains("Template rendering failed"));
635        }
636    }
637
638    // Miscellaneous Error Tests
639    mod misc_errors {
640        use super::*;
641
642        #[test]
643        fn test_missing_html_element() {
644            let error =
645                HtmlError::MissingHtmlElement("title".to_string());
646            assert!(error
647                .to_string()
648                .contains("Missing required HTML element"));
649        }
650
651        #[test]
652        fn test_invalid_structured_data() {
653            let error = HtmlError::InvalidStructuredData(
654                "Invalid JSON-LD".to_string(),
655            );
656            assert!(error
657                .to_string()
658                .contains("Invalid structured data"));
659        }
660
661        #[test]
662        fn test_invalid_front_matter_format() {
663            let error = HtmlError::InvalidFrontMatterFormat(
664                "Missing closing delimiter".to_string(),
665            );
666            assert!(error
667                .to_string()
668                .contains("Invalid front matter format"));
669        }
670
671        #[test]
672        fn test_parsing_error() {
673            let error =
674                HtmlError::ParsingError("Unexpected token".to_string());
675            assert!(error.to_string().contains("Parsing error"));
676        }
677
678        #[test]
679        fn test_validation_error() {
680            let error = HtmlError::ValidationError(
681                "Schema validation failed".to_string(),
682            );
683            assert!(error.to_string().contains("Validation error"));
684        }
685
686        #[test]
687        fn test_unexpected_error() {
688            let error = HtmlError::UnexpectedError(
689                "Something went wrong".to_string(),
690            );
691            assert!(error.to_string().contains("Unexpected error"));
692        }
693    }
694}