Skip to main content

frontmatter_gen/
error.rs

1//! Error handling for the frontmatter-gen crate.
2//!
3//! This module provides a comprehensive set of error types to handle various
4//! failure scenarios that may occur during front matter parsing, conversion,
5//! and extraction operations. Each error variant includes detailed error
6//! messages and context to aid in debugging and error handling.
7//!
8//! # Error Handling Strategies
9//!
10//! The error system provides several ways to handle errors:
11//!
12//! - **Context-aware errors**: Use `Context` to add line/column information
13//! - **Categorized errors**: Group errors by type using `Category`
14//! - **Error conversion**: Convert from standard errors using `From` implementations
15//! - **Rich error messages**: Detailed error descriptions with context
16//!
17//! # Features
18//!
19//! - Type-safe error handling with descriptive messages
20//! - Support for YAML, TOML, and JSON parsing errors
21//! - Content validation errors with size and depth checks
22//! - Format-specific error handling
23//! - Extraction and conversion error handling
24//!
25//! # Examples
26//!
27//! ```rust
28//! use frontmatter_gen::error::Error;
29//!
30//! fn example() -> Result<(), Error> {
31//!     // Example of handling YAML parsing errors
32//!     let invalid_yaml = "invalid: : yaml";
33//!     match noyalib::from_str::<noyalib::Value>(invalid_yaml) {
34//!         Ok(_) => Ok(()),
35//!         Err(e) => Err(Error::YamlParseError { source: e.into() }),
36//!     }
37//! }
38//! ```
39
40use noyalib::Error as YamlError;
41use serde_json::Error as JsonError;
42use std::sync::Arc;
43use thiserror::Error;
44
45/// Provides additional context for front matter errors.
46#[derive(Debug, Clone)]
47pub struct Context {
48    /// Line number where the error occurred.
49    pub line: Option<usize>,
50    /// Column number where the error occurred.
51    pub column: Option<usize>,
52    /// Snippet of the content where the error occurred.
53    pub snippet: Option<String>,
54}
55
56impl std::fmt::Display for Context {
57    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
58        match &self.snippet {
59            Some(snippet) => write!(
60                f,
61                "at {}:{} near '{}'",
62                self.line.unwrap_or(0),
63                self.column.unwrap_or(0),
64                snippet
65            ),
66            None => write!(
67                f,
68                "at {}:{}",
69                self.line.unwrap_or(0),
70                self.column.unwrap_or(0)
71            ),
72        }
73    }
74}
75
76/// Represents errors that can occur during front matter operations.
77///
78/// This enumeration uses the `thiserror` crate to provide structured error
79/// messages, improving the ease of debugging and handling errors encountered
80/// in front matter processing.
81///
82/// Each variant represents a specific type of error that may occur during
83/// front matter operations, with appropriate context and error details.
84#[derive(Error, Debug)]
85#[non_exhaustive]
86pub enum Error {
87    /// Content exceeds the maximum allowed size.
88    ///
89    /// This error occurs when the content size is larger than the configured
90    /// maximum limit.
91    ///
92    /// # Fields
93    ///
94    /// * `size` - The actual size of the content
95    /// * `max` - The maximum allowed size
96    #[error("Your front matter contains too many fields ({size}). The maximum allowed is {max}.")]
97    ContentTooLarge {
98        /// The actual size of the content
99        size: usize,
100        /// The maximum allowed size
101        max: usize,
102    },
103
104    /// Nesting depth exceeds the maximum allowed.
105    ///
106    /// This error occurs when the structure's nesting depth is greater than
107    /// the configured maximum depth.
108    #[error(
109        "Your front matter is nested too deeply ({depth} levels). The maximum allowed nesting depth is {max}."
110    )]
111    NestingTooDeep {
112        /// The actual nesting depth
113        depth: usize,
114        /// The maximum allowed depth
115        max: usize,
116    },
117
118    /// Error occurred whilst parsing YAML content.
119    ///
120    /// This error occurs when the YAML parser encounters invalid syntax or
121    /// structure.
122    #[error("Failed to parse YAML: {source}")]
123    YamlParseError {
124        /// The original error from the YAML parser
125        source: Arc<YamlError>,
126    },
127
128    /// Error occurred whilst parsing TOML content.
129    ///
130    /// This error occurs when the TOML parser encounters invalid syntax or
131    /// structure.
132    #[error("Failed to parse TOML: {0}")]
133    TomlParseError(#[from] toml::de::Error),
134
135    /// Error occurred whilst parsing JSON content.
136    ///
137    /// This error occurs when the JSON parser encounters invalid syntax or
138    /// structure.
139    #[error("Failed to parse JSON: {0}")]
140    JsonParseError(Arc<JsonError>),
141
142    /// The front matter format is invalid or unsupported.
143    ///
144    /// This error occurs when the front matter format cannot be determined or
145    /// is not supported by the library.
146    #[error("Invalid front matter format")]
147    InvalidFormat,
148
149    /// Error occurred during conversion between formats.
150    ///
151    /// This error occurs when converting front matter from one format to another
152    /// fails.
153    #[error("Failed to convert front matter: {0}")]
154    ConversionError(String),
155
156    /// Generic error during parsing.
157    ///
158    /// This error occurs when a parsing operation fails with a generic error.
159    #[error("Failed to parse front matter: {0}")]
160    ParseError(String),
161
162    /// Unsupported or unknown front matter format was detected.
163    ///
164    /// This error occurs when an unsupported front matter format is encountered
165    /// at a specific line.
166    #[error("Unsupported front matter format detected at line {line}")]
167    UnsupportedFormat {
168        /// The line number where the unsupported format was encountered
169        line: usize,
170    },
171
172    /// No front matter content was found.
173    ///
174    /// This error occurs when attempting to extract front matter from content
175    /// that does not contain any front matter section.
176    #[error("No front matter found in the content")]
177    NoFrontmatterFound,
178
179    /// Invalid JSON front matter.
180    ///
181    /// This error occurs when the JSON front matter is malformed or invalid.
182    #[error(
183        "Invalid JSON front matter: malformed or invalid structure."
184    )]
185    InvalidJson,
186
187    /// Invalid URL format.
188    ///
189    /// This error occurs when an invalid URL is encountered in the front matter.
190    #[error(
191        "Invalid URL: {0}. Ensure the URL is well-formed and valid."
192    )]
193    InvalidUrl(String),
194
195    /// Invalid TOML front matter.
196    ///
197    /// This error occurs when the TOML front matter is malformed or invalid.
198    #[error(
199        "Invalid TOML front matter: malformed or invalid structure."
200    )]
201    InvalidToml,
202
203    /// Invalid YAML front matter.
204    ///
205    /// This error occurs when the YAML front matter is malformed or invalid.
206    #[error(
207        "Invalid YAML front matter: malformed or invalid structure."
208    )]
209    InvalidYaml,
210
211    /// Invalid language code.
212    ///
213    /// This error occurs when an invalid language code is encountered in the
214    /// front matter.
215    #[error("Invalid language code: {0}")]
216    InvalidLanguage(String),
217
218    /// JSON front matter exceeds maximum nesting depth.
219    ///
220    /// This error occurs when the JSON front matter structure exceeds the
221    /// maximum allowed nesting depth.
222    #[error("JSON front matter exceeds maximum nesting depth")]
223    JsonDepthLimitExceeded,
224
225    /// Error during front matter extraction.
226    ///
227    /// This error occurs when there is a problem extracting front matter from
228    /// the content.
229    #[error("Extraction error: {0}")]
230    ExtractionError(String),
231
232    /// Serialization or deserialization error.
233    ///
234    /// This error occurs when there is a problem serializing or deserializing
235    /// content.
236    #[error("Serialization or deserialization error: {source}")]
237    SerdeError {
238        /// The original error from the serde library
239        source: Arc<serde_json::Error>,
240    },
241
242    /// Input validation error.
243    ///
244    /// This error occurs when the input fails validation checks.
245    #[error("Input validation error: {0}")]
246    ValidationError(String),
247
248    /// Generic error with a custom message.
249    ///
250    /// This error occurs when a generic error is encountered with a custom message.
251    #[error("Generic error: {0}")]
252    Other(String),
253}
254
255impl Clone for Error {
256    fn clone(&self) -> Self {
257        match self {
258            Self::ContentTooLarge { size, max } => {
259                Self::ContentTooLarge {
260                    size: *size,
261                    max: *max,
262                }
263            }
264            Self::NestingTooDeep { depth, max } => {
265                Self::NestingTooDeep {
266                    depth: *depth,
267                    max: *max,
268                }
269            }
270            Self::YamlParseError { source } => Self::YamlParseError {
271                source: Arc::clone(source),
272            },
273            Self::JsonParseError(err) => {
274                Self::JsonParseError(Arc::<serde_json::Error>::clone(
275                    err,
276                ))
277            }
278            Self::TomlParseError(err) => {
279                Self::TomlParseError(err.clone())
280            }
281            Self::SerdeError { source } => Self::SerdeError {
282                source: Arc::clone(source),
283            },
284            Self::ConversionError(msg) => {
285                Self::ConversionError(msg.clone())
286            }
287            Self::ParseError(msg) => Self::ParseError(msg.clone()),
288            Self::UnsupportedFormat { line } => {
289                Self::UnsupportedFormat { line: *line }
290            }
291            Self::NoFrontmatterFound => Self::NoFrontmatterFound,
292            Self::InvalidJson => Self::InvalidJson,
293            Self::InvalidToml => Self::InvalidToml,
294            Self::InvalidYaml => Self::InvalidYaml,
295            Self::JsonDepthLimitExceeded => {
296                Self::JsonDepthLimitExceeded
297            }
298            Self::ExtractionError(msg) => {
299                Self::ExtractionError(msg.clone())
300            }
301            Self::ValidationError(msg) => {
302                Self::ValidationError(msg.clone())
303            }
304            Self::InvalidUrl(msg) => Self::InvalidUrl(msg.clone()),
305            Self::InvalidLanguage(msg) => {
306                Self::InvalidLanguage(msg.clone())
307            }
308            Self::Other(msg) => Self::Other(msg.clone()),
309            Self::InvalidFormat => Self::InvalidFormat,
310        }
311    }
312}
313
314/// Categories of front matter errors.
315///
316/// This enumeration defines the main categories of errors that can occur
317/// during front matter operations.
318#[derive(Debug, Copy, Clone, Eq, PartialEq)]
319pub enum Category {
320    /// Parsing-related errors.
321    Parsing,
322    /// Validation-related errors.
323    Validation,
324    /// Conversion-related errors.
325    Conversion,
326    /// Configuration-related errors.
327    Configuration,
328}
329
330impl Error {
331    /// Returns the category of the error.
332    ///
333    /// # Returns
334    ///
335    /// Returns the `Category` that best describes this error.
336    #[must_use]
337    pub const fn category(&self) -> Category {
338        match self {
339            Self::YamlParseError { .. }
340            | Self::TomlParseError(_)
341            | Self::JsonParseError(_)
342            | Self::SerdeError { .. }
343            | Self::ParseError(_)
344            | Self::InvalidFormat
345            | Self::UnsupportedFormat { .. }
346            | Self::NoFrontmatterFound
347            | Self::InvalidJson
348            | Self::InvalidToml
349            | Self::InvalidYaml
350            | Self::JsonDepthLimitExceeded
351            | Self::ExtractionError(_)
352            | Self::InvalidUrl(_)
353            | Self::InvalidLanguage(_) => Category::Parsing,
354            Self::ValidationError(_) => Category::Validation,
355            Self::ConversionError(_) => Category::Conversion,
356            Self::ContentTooLarge { .. }
357            | Self::NestingTooDeep { .. }
358            | Self::Other(_) => Category::Configuration,
359        }
360    }
361
362    /// Creates a generic parse error with a custom message.
363    ///
364    /// # Arguments
365    ///
366    /// * `message` - A string slice containing the error message.
367    ///
368    /// # Examples
369    ///
370    /// ```rust
371    /// use frontmatter_gen::error::Error;
372    ///
373    /// let error = Error::generic_parse_error("Invalid syntax");
374    /// assert!(matches!(error, Error::ParseError(_)));
375    /// ```
376    #[must_use]
377    pub fn generic_parse_error(message: &str) -> Self {
378        Self::ParseError(message.to_string())
379    }
380
381    /// Creates an unsupported format error for a specific line.
382    ///
383    /// # Arguments
384    ///
385    /// * `line` - The line number where the unsupported format was detected.
386    ///
387    /// # Examples
388    ///
389    /// ```rust
390    /// use frontmatter_gen::error::Error;
391    ///
392    /// let error = Error::unsupported_format(42);
393    /// assert!(matches!(error, Error::UnsupportedFormat { line: 42 }));
394    /// ```
395    #[must_use]
396    pub const fn unsupported_format(line: usize) -> Self {
397        Self::UnsupportedFormat { line }
398    }
399
400    /// Creates a validation error with a custom message.
401    ///
402    /// # Arguments
403    ///
404    /// * `message` - A string slice containing the validation error message.
405    ///
406    /// # Examples
407    ///
408    /// ```rust
409    /// use frontmatter_gen::error::Error;
410    ///
411    /// let error = Error::validation_error("Invalid character in title");
412    /// assert!(matches!(error, Error::ValidationError(_)));
413    /// ```
414    #[must_use]
415    pub fn validation_error(message: &str) -> Self {
416        Self::ValidationError(message.to_string())
417    }
418
419    /// Adds context to an error.
420    ///
421    /// # Arguments
422    ///
423    /// * `context` - Additional context information about the error.
424    ///
425    /// # Examples
426    ///
427    /// ```rust
428    /// use frontmatter_gen::error::{Error, Context};
429    ///
430    /// let context = Context {
431    ///     line: Some(42),
432    ///     column: Some(10),
433    ///     snippet: Some("invalid content".to_string()),
434    /// };
435    ///
436    /// let error = Error::ParseError("Invalid syntax".to_string())
437    ///     .with_context(&context);
438    /// ```
439    #[must_use]
440    pub fn with_context(self, context: &Context) -> Self {
441        let context_info = format!(
442            " (line: {}, column: {})",
443            context.line.unwrap_or(0),
444            context.column.unwrap_or(0)
445        );
446        let snippet_info = context
447            .snippet
448            .as_ref()
449            .map(|s| format!(" near '{}'", s))
450            .unwrap_or_default();
451
452        match self {
453            Self::ParseError(mut msg) => {
454                msg.push_str(&context_info);
455                msg.push_str(&snippet_info);
456                Self::ParseError(msg)
457            }
458            Self::YamlParseError { source } => {
459                Self::YamlParseError { source }
460            }
461            _ => self, // For unsupported variants
462        }
463    }
464}
465
466/// Errors that can occur during site generation.
467///
468/// This enum is used to represent higher-level errors encountered during site
469/// generation processes, such as template rendering, file system operations,
470/// and metadata processing.
471#[derive(Error, Debug)]
472pub enum EngineError {
473    /// Error occurred during content processing.
474    #[error("Content processing error: {0}")]
475    ContentError(String),
476
477    /// Error occurred during template processing.
478    #[error("Template processing error: {0}")]
479    TemplateError(String),
480
481    /// Error occurred during asset processing.
482    #[error("Asset processing error: {0}")]
483    AssetError(String),
484
485    /// Error occurred during file system operations.
486    #[error("File system error: {source}")]
487    FileSystemError {
488        /// The original IO error that caused this error.
489        source: std::io::Error,
490        /// Additional context information about the error.
491        context: String,
492    },
493
494    /// Error occurred during metadata processing.
495    #[error("Metadata error: {0}")]
496    MetadataError(String),
497}
498
499/// Manual `Clone` implementation for `EngineError`.
500impl Clone for EngineError {
501    fn clone(&self) -> Self {
502        match self {
503            Self::ContentError(msg) => Self::ContentError(msg.clone()),
504            Self::TemplateError(msg) => {
505                Self::TemplateError(msg.clone())
506            }
507            Self::AssetError(msg) => Self::AssetError(msg.clone()),
508            Self::FileSystemError { source, context } => {
509                Self::FileSystemError {
510                    source: std::io::Error::new(
511                        source.kind(),
512                        source.to_string(),
513                    ),
514                    context: context.clone(),
515                }
516            }
517            Self::MetadataError(msg) => {
518                Self::MetadataError(msg.clone())
519            }
520        }
521    }
522}
523
524/// Converts an `EngineError` into an `Error`.
525///
526/// This allows engine errors to be converted into front matter errors when needed,
527/// preserving the error context and message.
528///
529/// Every variant of `EngineError` maps to `Error::ParseError` with a descriptive
530/// prefix indicating the subsystems that reported the failure.
531///
532/// # Examples
533///
534/// ```rust
535/// use frontmatter_gen::error::{EngineError, Error};
536/// use std::io;
537///
538/// let engine_error = EngineError::ContentError("content processing failed".to_string());
539/// let frontmatter_error: Error = engine_error.into();
540/// assert!(matches!(frontmatter_error, Error::ParseError(_)));
541/// ```
542impl From<EngineError> for Error {
543    fn from(err: EngineError) -> Self {
544        match err {
545            EngineError::ContentError(msg) => {
546                Self::ParseError(format!("Content error: {}", msg))
547            }
548            EngineError::TemplateError(msg) => {
549                Self::ParseError(format!("Template error: {}", msg))
550            }
551            EngineError::AssetError(msg) => {
552                Self::ParseError(format!("Asset error: {}", msg))
553            }
554            EngineError::FileSystemError { source, context } => {
555                Self::ParseError(format!(
556                    "File system error: {} ({})",
557                    source, context
558                ))
559            }
560            EngineError::MetadataError(msg) => {
561                Self::ParseError(format!("Metadata error: {}", msg))
562            }
563        }
564    }
565}
566
567/// Converts an IO error (`std::io::Error`) into a front matter `Error`.
568impl From<std::io::Error> for Error {
569    fn from(err: std::io::Error) -> Self {
570        Self::ParseError(err.to_string())
571    }
572}
573
574/// Converts a front matter `Error` into a string.
575///
576/// Uses the `Display` formatting of `Error` to produce an owned `String`.
577impl From<Error> for String {
578    fn from(err: Error) -> Self {
579        err.to_string()
580    }
581}
582
583#[cfg(test)]
584mod tests {
585    /// Tests for the `Error` enum and its associated methods.
586    mod error_tests {
587        use super::super::*;
588
589        /// Test the `ContentTooLarge` error variant.
590        #[test]
591        fn test_content_too_large_error() {
592            let error = Error::ContentTooLarge {
593                size: 1000,
594                max: 500,
595            };
596            assert_eq!(
597                error.to_string(),
598                "Your front matter contains too many fields (1000). The maximum allowed is 500."
599            );
600        }
601
602        /// Test the `NestingTooDeep` error variant.
603        #[test]
604        fn test_nesting_too_deep_error() {
605            let error = Error::NestingTooDeep { depth: 10, max: 5 };
606            assert_eq!(
607                error.to_string(),
608                "Your front matter is nested too deeply (10 levels). The maximum allowed nesting depth is 5."
609            );
610        }
611
612        /// Test the `JsonParseError` error variant.
613        #[test]
614        fn test_json_parse_error() {
615            let json_data = r#"{"key": invalid}"#;
616            let result: Result<serde_json::Value, _> =
617                serde_json::from_str(json_data);
618            assert!(result.is_err());
619            let error =
620                Error::JsonParseError(Arc::new(result.unwrap_err()));
621            assert!(matches!(error, Error::JsonParseError(_)));
622        }
623
624        /// Test the `InvalidFormat` error variant.
625        #[test]
626        fn test_invalid_format_error() {
627            let error = Error::InvalidFormat;
628            assert_eq!(
629                error.to_string(),
630                "Invalid front matter format"
631            );
632        }
633
634        /// Test the `ConversionError` error variant.
635        #[test]
636        fn test_conversion_error() {
637            let error =
638                Error::ConversionError("Conversion failed".to_string());
639            assert_eq!(
640                error.to_string(),
641                "Failed to convert front matter: Conversion failed"
642            );
643        }
644
645        /// Test the `UnsupportedFormat` error variant.
646        #[test]
647        fn test_unsupported_format() {
648            let error = Error::unsupported_format(42);
649            assert!(matches!(
650                error,
651                Error::UnsupportedFormat { line: 42 }
652            ));
653            assert_eq!(
654                error.to_string(),
655                "Unsupported front matter format detected at line 42"
656            );
657        }
658
659        /// Test the `NoFrontmatterFound` error variant.
660        #[test]
661        fn test_no_frontmatter_found() {
662            let error = Error::NoFrontmatterFound;
663            assert_eq!(
664                error.to_string(),
665                "No front matter found in the content"
666            );
667        }
668
669        /// Test the `InvalidJson` error variant.
670        #[test]
671        fn test_invalid_json_error() {
672            let error = Error::InvalidJson;
673            assert_eq!(error.to_string(), "Invalid JSON front matter: malformed or invalid structure.");
674        }
675
676        /// Test the `InvalidUrl` error variant.
677        #[test]
678        fn test_invalid_url_error() {
679            let error =
680                Error::InvalidUrl("http:// invalid.url".to_string());
681            assert_eq!(
682                error.to_string(),
683                "Invalid URL: http:// invalid.url. Ensure the URL is well-formed and valid."
684            );
685        }
686
687        /// Test the `InvalidYaml` error variant.
688        #[test]
689        fn test_invalid_yaml_error() {
690            let error = Error::InvalidYaml;
691            assert_eq!(
692                error.to_string(),
693                "Invalid YAML front matter: malformed or invalid structure."
694            );
695        }
696
697        /// Test the `ValidationError` error variant.
698        #[test]
699        fn test_validation_error() {
700            let error =
701                Error::ValidationError("Invalid title".to_string());
702            assert_eq!(
703                error.to_string(),
704                "Input validation error: Invalid title"
705            );
706        }
707
708        /// Test the `JsonDepthLimitExceeded` error variant.
709        #[test]
710        fn test_json_depth_limit_exceeded() {
711            let error = Error::JsonDepthLimitExceeded;
712            assert_eq!(
713                error.to_string(),
714                "JSON front matter exceeds maximum nesting depth"
715            );
716        }
717
718        /// Test the `category` method for different error variants.
719        #[test]
720        fn test_category_method() {
721            let validation_error =
722                Error::ValidationError("Invalid field".to_string());
723            assert_eq!(
724                validation_error.category(),
725                Category::Validation
726            );
727
728            let conversion_error =
729                Error::ConversionError("Conversion failed".to_string());
730            assert_eq!(
731                conversion_error.category(),
732                Category::Conversion
733            );
734
735            let config_error =
736                Error::ContentTooLarge { size: 100, max: 50 };
737            assert_eq!(
738                config_error.category(),
739                Category::Configuration
740            );
741        }
742
743        /// Test the `Clone` implementation for `Error`.
744        #[test]
745        fn test_error_clone() {
746            let original = Error::ContentTooLarge {
747                size: 200,
748                max: 100,
749            };
750            let cloned = original.clone();
751            assert!(
752                matches!(cloned, Error::ContentTooLarge { size, max } if size == 200 && max == 100)
753            );
754        }
755    }
756
757    /// Tests for the `EngineError` enum.
758    mod engine_error_tests {
759        use super::super::*;
760        use std::io;
761
762        /// Test the `ContentError` variant.
763        #[test]
764        fn test_content_error() {
765            let error = EngineError::ContentError(
766                "Processing failed".to_string(),
767            );
768            assert_eq!(
769                error.to_string(),
770                "Content processing error: Processing failed"
771            );
772        }
773
774        /// Test `EngineError::FileSystemError` conversion to `Error`.
775        #[test]
776        fn test_engine_error_to_error_conversion() {
777            let io_error = io::Error::other("disk full");
778            let engine_error = EngineError::FileSystemError {
779                source: io_error,
780                context: "Saving file".to_string(),
781            };
782            let converted: Error = engine_error.into();
783            assert!(converted.to_string().contains("disk full"));
784            assert!(converted.to_string().contains("Saving file"));
785        }
786    }
787
788    /// Tests for the `Context` struct.
789    mod context_tests {
790        use super::super::*;
791
792        /// Test the `Display` implementation of `Context`.
793        #[test]
794        fn test_context_display() {
795            let context = Context {
796                line: Some(42),
797                column: Some(10),
798                snippet: Some("invalid key".to_string()),
799            };
800            assert_eq!(
801                context.to_string(),
802                "at 42:10 near 'invalid key'"
803            );
804        }
805
806        /// Test missing fields in `Context`.
807        #[test]
808        fn test_context_missing_fields() {
809            let context = Context {
810                line: None,
811                column: None,
812                snippet: Some("example snippet".to_string()),
813            };
814            assert_eq!(
815                context.to_string(),
816                "at 0:0 near 'example snippet'"
817            );
818        }
819    }
820
821    /// Tests for conversions.
822    mod conversion_tests {
823        use super::super::*;
824        use std::io;
825
826        /// Test the conversion from `std::io::Error` to `Error`.
827        #[test]
828        fn test_io_error_conversion() {
829            let io_error =
830                io::Error::new(io::ErrorKind::NotFound, "file missing");
831            let error: Error = io_error.into();
832            assert!(matches!(error, Error::ParseError(_)));
833            assert!(error.to_string().contains("file missing"));
834        }
835    }
836
837    /// Test the conversion of `EngineError` to `Error`.
838    #[test]
839    fn test_engine_error_conversion() {
840        let engine_error = crate::error::EngineError::ContentError(
841            "content failed".to_string(),
842        );
843        let error: crate::Error = engine_error.into();
844        assert!(matches!(error, crate::Error::ParseError(_)));
845        assert!(error.to_string().contains("content failed"));
846    }
847}
848
849#[cfg(test)]
850mod exhaustive_error_tests {
851    //! Every `Error` and `EngineError` variant through `Clone`,
852    //! `Display`, `category()` and the `EngineError -> Error`
853    //! conversion.
854    //!
855    //! `Clone` is hand-written here because several variants hold
856    //! `Arc`-wrapped or non-`Clone` sources, and a hand-written `Clone`
857    //! with twenty arms is exactly the code where one arm silently
858    //! returns the wrong variant. Each arm is checked to round-trip to
859    //! its own variant with its own payload.
860
861    use super::*;
862    use std::sync::Arc;
863
864    fn one_of_every_error() -> Vec<Error> {
865        let json_err = serde_json::from_str::<serde_json::Value>("{")
866            .expect_err("invalid JSON");
867        let toml_err = toml::from_str::<toml::Value>("a = ")
868            .expect_err("invalid TOML");
869        let yaml_err = noyalib::from_str::<noyalib::Value>("a: [")
870            .expect_err("invalid YAML");
871        let serde_err = serde_json::from_str::<serde_json::Value>("]")
872            .expect_err("invalid JSON");
873
874        vec![
875            Error::ContentTooLarge { size: 10, max: 5 },
876            Error::NestingTooDeep { depth: 9, max: 3 },
877            Error::YamlParseError {
878                source: Arc::new(yaml_err),
879            },
880            Error::TomlParseError(toml_err),
881            Error::JsonParseError(Arc::new(json_err)),
882            Error::SerdeError {
883                source: Arc::new(serde_err),
884            },
885            Error::InvalidFormat,
886            Error::ConversionError("conversion".into()),
887            Error::ParseError("parse".into()),
888            Error::UnsupportedFormat { line: 42 },
889            Error::NoFrontmatterFound,
890            Error::InvalidJson,
891            Error::InvalidToml,
892            Error::InvalidYaml,
893            Error::InvalidUrl("url".into()),
894            Error::InvalidLanguage("lang".into()),
895            Error::JsonDepthLimitExceeded,
896            Error::ExtractionError("extract".into()),
897            Error::ValidationError("validate".into()),
898            Error::Other("other".into()),
899        ]
900    }
901
902    /// A discriminant name for a variant, so a clone that lands on the
903    /// wrong arm is caught even when both arms carry the same payload.
904    fn variant_of(e: &Error) -> &'static str {
905        match e {
906            Error::ContentTooLarge { .. } => "ContentTooLarge",
907            Error::NestingTooDeep { .. } => "NestingTooDeep",
908            Error::YamlParseError { .. } => "YamlParseError",
909            Error::TomlParseError(_) => "TomlParseError",
910            Error::JsonParseError(_) => "JsonParseError",
911            Error::SerdeError { .. } => "SerdeError",
912            Error::InvalidFormat => "InvalidFormat",
913            Error::ConversionError(_) => "ConversionError",
914            Error::ParseError(_) => "ParseError",
915            Error::UnsupportedFormat { .. } => "UnsupportedFormat",
916            Error::NoFrontmatterFound => "NoFrontmatterFound",
917            Error::InvalidJson => "InvalidJson",
918            Error::InvalidToml => "InvalidToml",
919            Error::InvalidYaml => "InvalidYaml",
920            Error::InvalidUrl(_) => "InvalidUrl",
921            Error::InvalidLanguage(_) => "InvalidLanguage",
922            Error::JsonDepthLimitExceeded => "JsonDepthLimitExceeded",
923            Error::ExtractionError(_) => "ExtractionError",
924            Error::ValidationError(_) => "ValidationError",
925            Error::Other(_) => "Other",
926        }
927    }
928
929    #[test]
930    fn clone_round_trips_every_variant() {
931        for error in one_of_every_error() {
932            let cloned = error.clone();
933            assert_eq!(
934                variant_of(&error),
935                variant_of(&cloned),
936                "clone changed the variant of {error:?}"
937            );
938            assert_eq!(
939                error.to_string(),
940                cloned.to_string(),
941                "clone changed the message of {error:?}"
942            );
943        }
944    }
945
946    #[test]
947    fn display_is_non_empty_and_names_its_payload() {
948        for error in one_of_every_error() {
949            let text = error.to_string();
950            let variant = variant_of(&error);
951            assert!(!text.is_empty(), "{variant} has no message");
952        }
953        assert!(Error::ContentTooLarge { size: 10, max: 5 }
954            .to_string()
955            .contains("10"));
956        assert!(Error::NestingTooDeep { depth: 9, max: 3 }
957            .to_string()
958            .contains('9'));
959        assert!(Error::UnsupportedFormat { line: 42 }
960            .to_string()
961            .contains("42"));
962    }
963
964    #[test]
965    fn every_variant_has_a_category() {
966        for error in one_of_every_error() {
967            // The point is that `category()` is total: a new variant
968            // added without a category arm fails to compile, and one
969            // mapped to the wrong group is visible here.
970            let _ = error.category();
971        }
972        assert_eq!(
973            Error::ValidationError("x".into()).category(),
974            Category::Validation
975        );
976        assert_eq!(
977            Error::ConversionError("x".into()).category(),
978            Category::Conversion
979        );
980        assert_eq!(
981            Error::NoFrontmatterFound.category(),
982            Category::Parsing
983        );
984        assert_eq!(
985            Error::ContentTooLarge { size: 1, max: 0 }.category(),
986            Category::Configuration
987        );
988    }
989
990    #[test]
991    fn context_is_added_to_parse_errors_and_ignored_elsewhere() {
992        let context = Context {
993            line: Some(7),
994            column: Some(3),
995            snippet: Some("bad: [".into()),
996        };
997
998        let with =
999            Error::ParseError("broken".into()).with_context(&context);
1000        let text = with.to_string();
1001        assert!(text.contains("line: 7"), "{text}");
1002        assert!(text.contains("column: 3"), "{text}");
1003        assert!(text.contains("near 'bad: ['"), "{text}");
1004
1005        // A context with no snippet still carries the position.
1006        let bare = Context {
1007            line: None,
1008            column: None,
1009            snippet: None,
1010        };
1011        let text = Error::ParseError("broken".into())
1012            .with_context(&bare)
1013            .to_string();
1014        assert!(text.contains("line: 0"), "{text}");
1015        assert!(!text.contains("near"), "{text}");
1016
1017        // Variants the function does not rewrite come back unchanged.
1018        let untouched = Error::NoFrontmatterFound
1019            .with_context(&context)
1020            .to_string();
1021        assert_eq!(untouched, Error::NoFrontmatterFound.to_string());
1022    }
1023
1024    #[test]
1025    fn helper_constructors_build_the_variant_they_name() {
1026        assert!(matches!(
1027            Error::generic_parse_error("x"),
1028            Error::ParseError(_)
1029        ));
1030        assert!(matches!(
1031            Error::validation_error("x"),
1032            Error::ValidationError(_)
1033        ));
1034    }
1035
1036    #[test]
1037    fn context_displays_all_three_fields() {
1038        let full = Context {
1039            line: Some(1),
1040            column: Some(2),
1041            snippet: Some("s".into()),
1042        };
1043        assert!(!full.to_string().is_empty());
1044        let empty = Context {
1045            line: None,
1046            column: None,
1047            snippet: None,
1048        };
1049        assert!(!empty.to_string().is_empty());
1050    }
1051
1052    fn one_of_every_engine_error() -> Vec<EngineError> {
1053        vec![
1054            EngineError::ContentError("content".into()),
1055            EngineError::TemplateError("template".into()),
1056            EngineError::AssetError("asset".into()),
1057            EngineError::FileSystemError {
1058                source: std::io::Error::new(
1059                    std::io::ErrorKind::NotFound,
1060                    "gone",
1061                ),
1062                context: "reading a template".into(),
1063            },
1064            EngineError::MetadataError("metadata".into()),
1065        ]
1066    }
1067
1068    #[test]
1069    fn engine_error_clones_and_converts_every_variant() {
1070        for error in one_of_every_engine_error() {
1071            let cloned = error.clone();
1072            assert_eq!(error.to_string(), cloned.to_string());
1073
1074            let converted: Error = error.into();
1075            assert!(
1076                matches!(converted, Error::ParseError(_)),
1077                "every EngineError converts to a ParseError: {converted:?}"
1078            );
1079            assert!(!converted.to_string().is_empty());
1080        }
1081    }
1082
1083    #[test]
1084    fn filesystem_engine_error_keeps_its_kind_and_context() {
1085        let error = EngineError::FileSystemError {
1086            source: std::io::Error::new(
1087                std::io::ErrorKind::PermissionDenied,
1088                "nope",
1089            ),
1090            context: "writing output".into(),
1091        };
1092        let cloned = error.clone();
1093        assert!(matches!(
1094            &cloned,
1095            EngineError::FileSystemError { source, context }
1096                if source.kind() == std::io::ErrorKind::PermissionDenied
1097                    && context == "writing output"
1098        ));
1099
1100        let yaml_err = noyalib::from_str::<noyalib::Value>(":\n  - [")
1101            .expect_err("invalid yaml");
1102        let wrapped = Error::YamlParseError {
1103            source: yaml_err.into(),
1104        }
1105        .with_context(&Context {
1106            line: Some(1),
1107            column: Some(2),
1108            snippet: None,
1109        });
1110        assert!(matches!(wrapped, Error::YamlParseError { .. }));
1111        let as_str: String = Error::InvalidFormat.into();
1112        assert!(!as_str.is_empty());
1113    }
1114}