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