Skip to main content

metadata_gen/
error.rs

1//! Error types for the metadata-gen library.
2//!
3//! This module defines custom error types used throughout the library,
4//! providing detailed information about various failure scenarios.
5
6use serde::de::Error as SerdeError;
7use serde_yml::Error as SerdeYmlError;
8use std::fmt::Display;
9use thiserror::Error;
10
11/// A custom error type to add context to the `Other` variant of `MetadataError`.
12///
13/// This struct wraps another error and provides additional context information.
14#[derive(Debug)]
15pub struct ContextError {
16    /// The context message providing additional information about the error.
17    context: String,
18    /// The source error that this `ContextError` is wrapping.
19    source: Box<dyn std::error::Error + Send + Sync>,
20}
21
22/// Displays the context error as `"context: source"`.
23impl std::fmt::Display for ContextError {
24    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
25        write!(f, "{}: {}", self.context, self.source)
26    }
27}
28
29/// Provides access to the underlying source error.
30impl std::error::Error for ContextError {
31    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
32        Some(&*self.source)
33    }
34}
35
36/// Custom error types for the metadata-gen library.
37///
38/// This enum encompasses all possible errors that can occur during
39/// metadata extraction, processing, and related operations.
40#[derive(Error, Debug)]
41pub enum MetadataError {
42    /// Error occurred while extracting metadata.
43    #[error("Failed to extract metadata: {message}")]
44    ExtractionError {
45        /// A descriptive message about the extraction error.
46        message: String,
47    },
48
49    /// Error occurred while processing metadata.
50    #[error("Failed to process metadata: {message}")]
51    ProcessingError {
52        /// A descriptive message about the processing error.
53        message: String,
54    },
55
56    /// Error occurred due to missing required field.
57    #[error("Missing required metadata field: {0}")]
58    MissingFieldError(String),
59
60    /// Error occurred while parsing date.
61    #[error("Failed to parse date: {0}")]
62    DateParseError(String),
63
64    /// I/O error.
65    #[error("I/O error: {0}")]
66    IoError(#[from] std::io::Error),
67
68    /// YAML parsing error.
69    #[error("YAML parsing error: {0}")]
70    YamlError(#[from] SerdeYmlError),
71
72    /// JSON parsing error.
73    #[error("JSON parsing error: {0}")]
74    JsonError(#[from] serde_json::Error),
75
76    /// TOML parsing error.
77    #[error("TOML parsing error: {0}")]
78    TomlError(#[from] toml::de::Error),
79
80    /// Unsupported metadata format error.
81    #[error("Unsupported metadata format: {0}")]
82    UnsupportedFormatError(String),
83
84    /// Validation error for metadata fields.
85    #[error("Metadata validation error: {field} - {message}")]
86    ValidationError {
87        /// The field that failed validation.
88        field: String,
89        /// A descriptive message about the validation error.
90        message: String,
91    },
92
93    /// UTF-8 decoding error.
94    #[error("UTF-8 decoding error: {0}")]
95    Utf8Error(#[from] std::str::Utf8Error),
96
97    /// Catch-all for unexpected errors.
98    #[error("Unexpected error: {0}")]
99    Other(#[from] Box<dyn std::error::Error + Send + Sync>),
100}
101
102impl MetadataError {
103    /// Creates a new `ExtractionError` with the given message.
104    ///
105    /// # Arguments
106    ///
107    /// * `message` - A descriptive message about the extraction error.
108    ///
109    /// # Returns
110    ///
111    /// A new `MetadataError::ExtractionError` variant.
112    ///
113    /// # Example
114    ///
115    /// ```
116    /// use metadata_gen::error::MetadataError;
117    ///
118    /// let error = MetadataError::new_extraction_error("Failed to extract title");
119    /// assert!(matches!(error, MetadataError::ExtractionError { .. }));
120    /// ```
121    pub fn new_extraction_error(message: impl Into<String>) -> Self {
122        Self::ExtractionError {
123            message: message.into(),
124        }
125    }
126
127    /// Creates a new `ProcessingError` with the given message.
128    ///
129    /// # Arguments
130    ///
131    /// * `message` - A descriptive message about the processing error.
132    ///
133    /// # Returns
134    ///
135    /// A new `MetadataError::ProcessingError` variant.
136    ///
137    /// # Example
138    ///
139    /// ```
140    /// use metadata_gen::error::MetadataError;
141    ///
142    /// let error = MetadataError::new_processing_error("Failed to process metadata");
143    /// assert!(matches!(error, MetadataError::ProcessingError { .. }));
144    /// ```
145    pub fn new_processing_error(message: impl Into<String>) -> Self {
146        Self::ProcessingError {
147            message: message.into(),
148        }
149    }
150
151    /// Creates a new `ValidationError` with the given field and message.
152    ///
153    /// # Arguments
154    ///
155    /// * `field` - The name of the field that failed validation.
156    /// * `message` - A descriptive message about the validation error.
157    ///
158    /// # Returns
159    ///
160    /// A new `MetadataError::ValidationError` variant.
161    ///
162    /// # Example
163    ///
164    /// ```
165    /// use metadata_gen::error::MetadataError;
166    ///
167    /// let error = MetadataError::new_validation_error("title", "Title must not be empty");
168    /// assert!(matches!(error, MetadataError::ValidationError { .. }));
169    /// ```
170    pub fn new_validation_error(
171        field: impl Into<String>,
172        message: impl Into<String>,
173    ) -> Self {
174        Self::ValidationError {
175            field: field.into(),
176            message: message.into(),
177        }
178    }
179
180    /// Adds context to an existing error.
181    ///
182    /// This method wraps the current error with additional context information.
183    ///
184    /// # Arguments
185    ///
186    /// * `ctx` - The context to add to the error.
187    ///
188    /// # Returns
189    ///
190    /// A new `MetadataError` with the added context.
191    ///
192    /// # Example
193    ///
194    /// ```
195    /// use metadata_gen::error::MetadataError;
196    ///
197    /// let error = MetadataError::new_extraction_error("Failed to parse YAML")
198    ///     .context("Processing file 'example.md'");
199    /// assert_eq!(error.to_string(), "Failed to extract metadata: Processing file 'example.md': Failed to parse YAML");
200    /// ```
201    pub fn context<C>(self, ctx: C) -> Self
202    where
203        C: Display + Send + Sync + 'static,
204    {
205        match self {
206            Self::ExtractionError { message } => {
207                Self::ExtractionError {
208                    message: format!("{}: {}", ctx, message),
209                }
210            }
211            Self::ProcessingError { message } => {
212                Self::ProcessingError {
213                    message: format!("{}: {}", ctx, message),
214                }
215            }
216            Self::MissingFieldError(field) => {
217                Self::MissingFieldError(format!("{}: {}", ctx, field))
218            }
219            Self::DateParseError(error) => {
220                Self::DateParseError(format!("{}: {}", ctx, error))
221            }
222            Self::IoError(error) => Self::IoError(std::io::Error::new(
223                error.kind(),
224                format!("{}: {}", ctx, error),
225            )),
226            Self::YamlError(error) => Self::YamlError(
227                SerdeYmlError::custom(format!("{}: {}", ctx, error)),
228            ),
229            Self::JsonError(error) => {
230                Self::JsonError(serde_json::Error::custom(format!(
231                    "{}: {}",
232                    ctx, error
233                )))
234            }
235            Self::TomlError(error) => Self::TomlError(
236                toml::de::Error::custom(format!("{}: {}", ctx, error)),
237            ),
238            Self::UnsupportedFormatError(format) => {
239                Self::UnsupportedFormatError(format!(
240                    "{}: {}",
241                    ctx, format
242                ))
243            }
244            Self::ValidationError { field, message } => {
245                Self::ValidationError {
246                    field,
247                    message: format!("{}: {}", ctx, message),
248                }
249            }
250            Self::Utf8Error(error) => Self::Utf8Error(error),
251            Self::Other(error) => Self::Other(Box::new(ContextError {
252                context: ctx.to_string(),
253                source: error,
254            })),
255        }
256    }
257}
258
259#[cfg(test)]
260mod tests {
261    use super::*;
262    use std::error::Error;
263    use std::fmt;
264    use std::io;
265
266    #[test]
267    fn test_extraction_error() {
268        let error = MetadataError::new_extraction_error(
269            "No valid front matter found.",
270        );
271        assert_eq!(
272            error.to_string(),
273            "Failed to extract metadata: No valid front matter found."
274        );
275    }
276
277    #[test]
278    fn test_processing_error() {
279        let error =
280            MetadataError::new_processing_error("Unknown field");
281        assert_eq!(
282            error.to_string(),
283            "Failed to process metadata: Unknown field"
284        );
285    }
286
287    #[test]
288    fn test_missing_field_error() {
289        let error =
290            MetadataError::MissingFieldError("author".to_string());
291        assert_eq!(
292            error.to_string(),
293            "Missing required metadata field: author"
294        );
295    }
296
297    #[test]
298    fn test_date_parse_error() {
299        let error = MetadataError::DateParseError(
300            "Invalid date format".to_string(),
301        );
302        assert_eq!(
303            error.to_string(),
304            "Failed to parse date: Invalid date format"
305        );
306    }
307
308    #[test]
309    fn test_io_error() {
310        let io_error =
311            io::Error::new(io::ErrorKind::NotFound, "File not found");
312        let error: MetadataError = io_error.into();
313        assert_eq!(error.to_string(), "I/O error: File not found");
314    }
315
316    #[test]
317    fn test_yaml_error() {
318        let yaml_error =
319            serde_yml::Error::custom("YAML structure error");
320        let error: MetadataError = yaml_error.into();
321        assert!(error.to_string().contains("YAML parsing error"));
322    }
323
324    #[test]
325    fn test_json_error() {
326        let json_error =
327            serde_json::Error::custom("Invalid JSON format");
328        let error: MetadataError = json_error.into();
329        assert_eq!(
330            error.to_string(),
331            "JSON parsing error: Invalid JSON format"
332        );
333    }
334
335    #[test]
336    fn test_toml_error() {
337        let toml_error =
338            toml::de::Error::custom("Invalid TOML structure");
339        let error: MetadataError = toml_error.into();
340        assert!(error.to_string().contains("TOML parsing error"));
341    }
342
343    #[test]
344    fn test_unsupported_format_error() {
345        let error =
346            MetadataError::UnsupportedFormatError("XML".to_string());
347        assert_eq!(
348            error.to_string(),
349            "Unsupported metadata format: XML"
350        );
351    }
352
353    #[test]
354    fn test_validation_error() {
355        let error = MetadataError::new_validation_error(
356            "title",
357            "Title must not be empty",
358        );
359        match error {
360            MetadataError::ValidationError { field, message } => {
361                assert_eq!(field, "title");
362                assert_eq!(message, "Title must not be empty");
363            }
364            _ => panic!("Unexpected error variant"),
365        }
366    }
367
368    #[test]
369    #[allow(invalid_from_utf8)]
370    fn test_utf8_error() {
371        let invalid_bytes: &[u8] = &[0xFF, 0xFF];
372        let utf8_error =
373            std::str::from_utf8(invalid_bytes).unwrap_err();
374        let error: MetadataError = utf8_error.into();
375        assert!(matches!(error, MetadataError::Utf8Error(..)));
376        assert!(error.to_string().starts_with("UTF-8 decoding error:"));
377    }
378
379    #[test]
380    fn test_other_error() {
381        use std::error::Error;
382
383        #[derive(Debug)]
384        struct CustomError;
385
386        impl std::fmt::Display for CustomError {
387            fn fmt(
388                &self,
389                f: &mut std::fmt::Formatter<'_>,
390            ) -> std::fmt::Result {
391                write!(f, "Custom error occurred")
392            }
393        }
394
395        impl Error for CustomError {}
396
397        let custom_error = CustomError;
398        let error = MetadataError::Other(Box::new(custom_error));
399
400        assert!(matches!(error, MetadataError::Other(..)));
401        assert_eq!(
402            error.to_string(),
403            "Unexpected error: Custom error occurred"
404        );
405    }
406
407    #[test]
408    fn test_extraction_error_with_empty_message() {
409        let error = MetadataError::new_extraction_error("");
410        assert_eq!(error.to_string(), "Failed to extract metadata: ");
411    }
412
413    #[test]
414    fn test_processing_error_with_empty_message() {
415        let error = MetadataError::new_processing_error("");
416        assert_eq!(error.to_string(), "Failed to process metadata: ");
417    }
418
419    #[test]
420    fn test_validation_error_with_empty_field_and_message() {
421        let error = MetadataError::new_validation_error("", "");
422        match error {
423            MetadataError::ValidationError { field, message } => {
424                assert_eq!(field, "");
425                assert_eq!(message, "");
426            }
427            _ => panic!("Unexpected error variant"),
428        }
429    }
430
431    #[test]
432    fn test_unsupported_format_error_with_empty_format() {
433        let error =
434            MetadataError::UnsupportedFormatError("".to_string());
435        assert_eq!(error.to_string(), "Unsupported metadata format: ");
436    }
437
438    #[test]
439    fn test_yaml_error_with_custom_message() {
440        // Custom YAML error message
441        let yaml_error =
442            serde_yml::Error::custom("Custom YAML error occurred");
443        let error: MetadataError = yaml_error.into();
444        assert!(error.to_string().contains(
445            "YAML parsing error: Custom YAML error occurred"
446        ));
447    }
448
449    #[test]
450    fn test_json_error_with_custom_message() {
451        // Custom JSON error message
452        let json_error = serde_json::Error::custom("Custom JSON error");
453        let error: MetadataError = json_error.into();
454        assert_eq!(
455            error.to_string(),
456            "JSON parsing error: Custom JSON error"
457        );
458    }
459
460    #[test]
461    fn test_toml_error_with_custom_message() {
462        // Custom TOML error message
463        let toml_error = toml::de::Error::custom("Custom TOML error");
464        let error: MetadataError = toml_error.into();
465        assert!(error
466            .to_string()
467            .contains("TOML parsing error: Custom TOML error"));
468    }
469
470    #[test]
471    #[allow(invalid_from_utf8)]
472    fn test_utf8_error_with_specific_invalid_bytes() {
473        let invalid_bytes: &[u8] = &[0xC0, 0x80]; // Overlong encoding, invalid UTF-8
474        let utf8_error =
475            std::str::from_utf8(invalid_bytes).unwrap_err();
476        let error: MetadataError = utf8_error.into();
477        assert!(matches!(error, MetadataError::Utf8Error(..)));
478        assert!(error.to_string().starts_with("UTF-8 decoding error:"));
479    }
480
481    #[test]
482    fn test_io_error_with_custom_message() {
483        let io_error = std::io::Error::new(
484            std::io::ErrorKind::PermissionDenied,
485            "Permission denied",
486        );
487        let error: MetadataError = io_error.into();
488        assert_eq!(error.to_string(), "I/O error: Permission denied");
489    }
490
491    #[test]
492    fn test_extraction_error_to_debug() {
493        let error = MetadataError::new_extraction_error(
494            "Failed to extract metadata",
495        );
496        assert_eq!(
497            format!("{:?}", error),
498            r#"ExtractionError { message: "Failed to extract metadata" }"#
499        );
500    }
501
502    #[test]
503    fn test_processing_error_to_debug() {
504        let error =
505            MetadataError::new_processing_error("Processing failed");
506        assert_eq!(
507            format!("{:?}", error),
508            r#"ProcessingError { message: "Processing failed" }"#
509        );
510    }
511
512    #[test]
513    fn test_validation_error_to_debug() {
514        let error = MetadataError::new_validation_error(
515            "title",
516            "Title cannot be empty",
517        );
518        assert_eq!(
519            format!("{:?}", error),
520            r#"ValidationError { field: "title", message: "Title cannot be empty" }"#
521        );
522    }
523
524    #[test]
525    fn test_other_error_to_debug() {
526        #[derive(Debug)]
527        struct CustomError;
528
529        impl std::fmt::Display for CustomError {
530            fn fmt(
531                &self,
532                f: &mut std::fmt::Formatter<'_>,
533            ) -> std::fmt::Result {
534                write!(f, "A custom error occurred")
535            }
536        }
537
538        impl std::error::Error for CustomError {}
539
540        let custom_error = CustomError;
541        let error = MetadataError::Other(Box::new(custom_error));
542
543        // Ensure the debug output is correctly formatted
544        assert!(format!("{:?}", error).contains("Other("));
545    }
546
547    #[test]
548    fn test_context_error() {
549        let error =
550            MetadataError::new_extraction_error("Failed to parse YAML")
551                .context("Processing file 'example.md'");
552        assert_eq!(
553            error.to_string(),
554            "Failed to extract metadata: Processing file 'example.md': Failed to parse YAML"
555        );
556    }
557
558    #[test]
559    fn test_nested_context_error() {
560        let error =
561            MetadataError::new_extraction_error("Failed to parse YAML")
562                .context("Processing file 'example.md'")
563                .context("Metadata extraction process");
564        assert_eq!(
565            error.to_string(),
566            "Failed to extract metadata: Metadata extraction process: Processing file 'example.md': Failed to parse YAML"
567        );
568    }
569
570    #[test]
571    fn test_extraction_error_empty_message() {
572        let error = MetadataError::ExtractionError {
573            message: "".to_string(),
574        };
575        assert_eq!(error.to_string(), "Failed to extract metadata: ");
576    }
577
578    #[test]
579    fn test_processing_error_empty_message() {
580        let error = MetadataError::ProcessingError {
581            message: "".to_string(),
582        };
583        assert_eq!(error.to_string(), "Failed to process metadata: ");
584    }
585
586    #[test]
587    fn test_missing_field_error_empty_message() {
588        let error = MetadataError::MissingFieldError("".to_string());
589        assert_eq!(
590            error.to_string(),
591            "Missing required metadata field: "
592        );
593    }
594
595    #[test]
596    fn test_date_parse_error_empty_message() {
597        let error = MetadataError::DateParseError("".to_string());
598        assert_eq!(error.to_string(), "Failed to parse date: ");
599    }
600
601    #[test]
602    fn test_extraction_error_debug() {
603        let error = MetadataError::ExtractionError {
604            message: "Error extracting metadata".to_string(),
605        };
606        // The correct Debug output for the struct variant should include the field name
607        assert_eq!(
608            format!("{:?}", error),
609            r#"ExtractionError { message: "Error extracting metadata" }"#
610        );
611    }
612
613    #[test]
614    fn test_processing_error_debug() {
615        let error = MetadataError::ProcessingError {
616            message: "Error processing metadata".to_string(),
617        };
618        // The correct Debug output for the struct variant should include the field name
619        assert_eq!(
620            format!("{:?}", error),
621            r#"ProcessingError { message: "Error processing metadata" }"#
622        );
623    }
624
625    #[test]
626    fn test_io_error_propagation() {
627        let io_error =
628            io::Error::new(io::ErrorKind::NotFound, "file not found");
629        let error: MetadataError = io_error.into();
630        assert_eq!(error.to_string(), "I/O error: file not found");
631        assert!(matches!(error, MetadataError::IoError(_)));
632    }
633
634    #[test]
635    fn test_yaml_error_propagation() {
636        let yaml_error = serde_yml::Error::custom("Custom YAML error");
637        let error: MetadataError = yaml_error.into();
638        assert_eq!(
639            error.to_string(),
640            "YAML parsing error: Custom YAML error"
641        );
642        assert!(matches!(error, MetadataError::YamlError(_)));
643    }
644
645    #[test]
646    fn test_json_error_propagation() {
647        let json_error = serde_json::Error::custom("Custom JSON error");
648        let error: MetadataError = json_error.into();
649        assert_eq!(
650            error.to_string(),
651            "JSON parsing error: Custom JSON error"
652        );
653        assert!(matches!(error, MetadataError::JsonError(_)));
654    }
655
656    #[test]
657    fn test_toml_error_propagation() {
658        let toml_error = toml::de::Error::custom("Custom TOML error");
659        let error: MetadataError = toml_error.into();
660        assert_eq!(
661            error.to_string(),
662            "TOML parsing error: Custom TOML error\n"
663        );
664        assert!(matches!(error, MetadataError::TomlError(_)));
665    }
666
667    #[test]
668    fn test_missing_field_error_debug() {
669        let error =
670            MetadataError::MissingFieldError("title".to_string());
671        assert_eq!(
672            format!("{:?}", error),
673            r#"MissingFieldError("title")"#
674        );
675    }
676
677    #[test]
678    fn test_date_parse_error_debug() {
679        let error = MetadataError::DateParseError(
680            "Invalid date format".to_string(),
681        );
682        assert_eq!(
683            format!("{:?}", error),
684            r#"DateParseError("Invalid date format")"#
685        );
686    }
687
688    #[test]
689    fn test_empty_yaml_error_message() {
690        let yaml_error = serde_yml::Error::custom("");
691        let error: MetadataError = yaml_error.into();
692        assert_eq!(error.to_string(), "YAML parsing error: ");
693    }
694
695    #[test]
696    fn test_empty_json_error_message() {
697        let json_error = serde_json::Error::custom("");
698        let error: MetadataError = json_error.into();
699        assert_eq!(error.to_string(), "JSON parsing error: ");
700    }
701
702    #[test]
703    fn test_empty_toml_error_message() {
704        let toml_error = toml::de::Error::custom("");
705        let error: MetadataError = toml_error.into();
706        assert_eq!(error.to_string(), "TOML parsing error: \n");
707    }
708
709    // A custom error for testing purposes
710    #[derive(Debug)]
711    struct CustomError;
712
713    impl fmt::Display for CustomError {
714        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
715            write!(f, "Custom error occurred")
716        }
717    }
718
719    impl Error for CustomError {}
720
721    #[test]
722    fn test_context_error_fmt() {
723        let custom_error = CustomError;
724        let context_error = ContextError {
725            context: "An error occurred while processing".to_string(),
726            source: Box::new(custom_error),
727        };
728
729        let formatted = format!("{}", context_error);
730        assert_eq!(
731            formatted,
732            "An error occurred while processing: Custom error occurred"
733        );
734    }
735
736    #[test]
737    fn test_context_error_source() {
738        let custom_error = CustomError;
739        let context_error = ContextError {
740            context: "Error with context".to_string(),
741            source: Box::new(custom_error),
742        };
743
744        // The source method should return a reference to the original error (custom_error in this case)
745        let source = context_error.source().unwrap();
746        assert_eq!(source.to_string(), "Custom error occurred");
747    }
748
749    #[test]
750    fn test_context_error_debug() {
751        let custom_error = CustomError;
752        let context_error = ContextError {
753            context: "Error during processing".to_string(),
754            source: Box::new(custom_error),
755        };
756
757        let debug_output = format!("{:?}", context_error);
758
759        // Ensure the debug output includes the "ContextError" struct and its fields
760        assert!(debug_output.contains("ContextError"));
761        assert!(debug_output.contains("Error during processing"));
762        assert!(debug_output.contains("CustomError"));
763    }
764}