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}