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