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 noyalib::Error as SerdeYmlError;
7use serde::de::Error as SerdeError;
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 = noyalib::Error::custom("YAML structure error");
319        let error: MetadataError = yaml_error.into();
320        assert!(error.to_string().contains("YAML parsing error"));
321    }
322
323    #[test]
324    fn test_json_error() {
325        let json_error =
326            serde_json::Error::custom("Invalid JSON format");
327        let error: MetadataError = json_error.into();
328        assert_eq!(
329            error.to_string(),
330            "JSON parsing error: Invalid JSON format"
331        );
332    }
333
334    #[test]
335    fn test_toml_error() {
336        let toml_error =
337            toml::de::Error::custom("Invalid TOML structure");
338        let error: MetadataError = toml_error.into();
339        assert!(error.to_string().contains("TOML parsing error"));
340    }
341
342    #[test]
343    fn test_unsupported_format_error() {
344        let error =
345            MetadataError::UnsupportedFormatError("XML".to_string());
346        assert_eq!(
347            error.to_string(),
348            "Unsupported metadata format: XML"
349        );
350    }
351
352    #[test]
353    fn test_validation_error() {
354        let error = MetadataError::new_validation_error(
355            "title",
356            "Title must not be empty",
357        );
358        match error {
359            MetadataError::ValidationError { field, message } => {
360                assert_eq!(field, "title");
361                assert_eq!(message, "Title must not be empty");
362            }
363            _ => panic!("Unexpected error variant"),
364        }
365    }
366
367    #[test]
368    #[allow(invalid_from_utf8)]
369    fn test_utf8_error() {
370        let invalid_bytes: &[u8] = &[0xFF, 0xFF];
371        let utf8_error =
372            std::str::from_utf8(invalid_bytes).unwrap_err();
373        let error: MetadataError = utf8_error.into();
374        assert!(matches!(error, MetadataError::Utf8Error(..)));
375        assert!(error.to_string().starts_with("UTF-8 decoding error:"));
376    }
377
378    #[test]
379    fn test_other_error() {
380        use std::error::Error;
381
382        #[derive(Debug)]
383        struct CustomError;
384
385        impl std::fmt::Display for CustomError {
386            fn fmt(
387                &self,
388                f: &mut std::fmt::Formatter<'_>,
389            ) -> std::fmt::Result {
390                write!(f, "Custom error occurred")
391            }
392        }
393
394        impl Error for CustomError {}
395
396        let custom_error = CustomError;
397        let error = MetadataError::Other(Box::new(custom_error));
398
399        assert!(matches!(error, MetadataError::Other(..)));
400        assert_eq!(
401            error.to_string(),
402            "Unexpected error: Custom error occurred"
403        );
404    }
405
406    #[test]
407    fn test_extraction_error_with_empty_message() {
408        let error = MetadataError::new_extraction_error("");
409        assert_eq!(error.to_string(), "Failed to extract metadata: ");
410    }
411
412    #[test]
413    fn test_processing_error_with_empty_message() {
414        let error = MetadataError::new_processing_error("");
415        assert_eq!(error.to_string(), "Failed to process metadata: ");
416    }
417
418    #[test]
419    fn test_validation_error_with_empty_field_and_message() {
420        let error = MetadataError::new_validation_error("", "");
421        match error {
422            MetadataError::ValidationError { field, message } => {
423                assert_eq!(field, "");
424                assert_eq!(message, "");
425            }
426            _ => panic!("Unexpected error variant"),
427        }
428    }
429
430    #[test]
431    fn test_unsupported_format_error_with_empty_format() {
432        let error =
433            MetadataError::UnsupportedFormatError("".to_string());
434        assert_eq!(error.to_string(), "Unsupported metadata format: ");
435    }
436
437    #[test]
438    fn test_yaml_error_with_custom_message() {
439        // Custom YAML error message
440        let yaml_error =
441            noyalib::Error::custom("Custom YAML error occurred");
442        let error: MetadataError = yaml_error.into();
443        assert!(error.to_string().contains(
444            "YAML parsing error: Custom YAML error occurred"
445        ));
446    }
447
448    #[test]
449    fn test_json_error_with_custom_message() {
450        // Custom JSON error message
451        let json_error = serde_json::Error::custom("Custom JSON error");
452        let error: MetadataError = json_error.into();
453        assert_eq!(
454            error.to_string(),
455            "JSON parsing error: Custom JSON error"
456        );
457    }
458
459    #[test]
460    fn test_toml_error_with_custom_message() {
461        // Custom TOML error message
462        let toml_error = toml::de::Error::custom("Custom TOML error");
463        let error: MetadataError = toml_error.into();
464        assert!(error
465            .to_string()
466            .contains("TOML parsing error: Custom TOML error"));
467    }
468
469    #[test]
470    #[allow(invalid_from_utf8)]
471    fn test_utf8_error_with_specific_invalid_bytes() {
472        let invalid_bytes: &[u8] = &[0xC0, 0x80]; // Overlong encoding, invalid UTF-8
473        let utf8_error =
474            std::str::from_utf8(invalid_bytes).unwrap_err();
475        let error: MetadataError = utf8_error.into();
476        assert!(matches!(error, MetadataError::Utf8Error(..)));
477        assert!(error.to_string().starts_with("UTF-8 decoding error:"));
478    }
479
480    #[test]
481    fn test_io_error_with_custom_message() {
482        let io_error = std::io::Error::new(
483            std::io::ErrorKind::PermissionDenied,
484            "Permission denied",
485        );
486        let error: MetadataError = io_error.into();
487        assert_eq!(error.to_string(), "I/O error: Permission denied");
488    }
489
490    #[test]
491    fn test_extraction_error_to_debug() {
492        let error = MetadataError::new_extraction_error(
493            "Failed to extract metadata",
494        );
495        assert_eq!(
496            format!("{:?}", error),
497            r#"ExtractionError { message: "Failed to extract metadata" }"#
498        );
499    }
500
501    #[test]
502    fn test_processing_error_to_debug() {
503        let error =
504            MetadataError::new_processing_error("Processing failed");
505        assert_eq!(
506            format!("{:?}", error),
507            r#"ProcessingError { message: "Processing failed" }"#
508        );
509    }
510
511    #[test]
512    fn test_validation_error_to_debug() {
513        let error = MetadataError::new_validation_error(
514            "title",
515            "Title cannot be empty",
516        );
517        assert_eq!(
518            format!("{:?}", error),
519            r#"ValidationError { field: "title", message: "Title cannot be empty" }"#
520        );
521    }
522
523    #[test]
524    fn test_other_error_to_debug() {
525        #[derive(Debug)]
526        struct CustomError;
527
528        impl std::fmt::Display for CustomError {
529            fn fmt(
530                &self,
531                f: &mut std::fmt::Formatter<'_>,
532            ) -> std::fmt::Result {
533                write!(f, "A custom error occurred")
534            }
535        }
536
537        impl std::error::Error for CustomError {}
538
539        let custom_error = CustomError;
540        let error = MetadataError::Other(Box::new(custom_error));
541
542        // Ensure the debug output is correctly formatted
543        assert!(format!("{:?}", error).contains("Other("));
544    }
545
546    #[test]
547    fn test_context_error() {
548        let error =
549            MetadataError::new_extraction_error("Failed to parse YAML")
550                .context("Processing file 'example.md'");
551        assert_eq!(
552            error.to_string(),
553            "Failed to extract metadata: Processing file 'example.md': Failed to parse YAML"
554        );
555    }
556
557    #[test]
558    fn test_nested_context_error() {
559        let error =
560            MetadataError::new_extraction_error("Failed to parse YAML")
561                .context("Processing file 'example.md'")
562                .context("Metadata extraction process");
563        assert_eq!(
564            error.to_string(),
565            "Failed to extract metadata: Metadata extraction process: Processing file 'example.md': Failed to parse YAML"
566        );
567    }
568
569    #[test]
570    fn test_extraction_error_empty_message() {
571        let error = MetadataError::ExtractionError {
572            message: "".to_string(),
573        };
574        assert_eq!(error.to_string(), "Failed to extract metadata: ");
575    }
576
577    #[test]
578    fn test_processing_error_empty_message() {
579        let error = MetadataError::ProcessingError {
580            message: "".to_string(),
581        };
582        assert_eq!(error.to_string(), "Failed to process metadata: ");
583    }
584
585    #[test]
586    fn test_missing_field_error_empty_message() {
587        let error = MetadataError::MissingFieldError("".to_string());
588        assert_eq!(
589            error.to_string(),
590            "Missing required metadata field: "
591        );
592    }
593
594    #[test]
595    fn test_date_parse_error_empty_message() {
596        let error = MetadataError::DateParseError("".to_string());
597        assert_eq!(error.to_string(), "Failed to parse date: ");
598    }
599
600    #[test]
601    fn test_extraction_error_debug() {
602        let error = MetadataError::ExtractionError {
603            message: "Error extracting metadata".to_string(),
604        };
605        // The correct Debug output for the struct variant should include the field name
606        assert_eq!(
607            format!("{:?}", error),
608            r#"ExtractionError { message: "Error extracting metadata" }"#
609        );
610    }
611
612    #[test]
613    fn test_processing_error_debug() {
614        let error = MetadataError::ProcessingError {
615            message: "Error processing metadata".to_string(),
616        };
617        // The correct Debug output for the struct variant should include the field name
618        assert_eq!(
619            format!("{:?}", error),
620            r#"ProcessingError { message: "Error processing metadata" }"#
621        );
622    }
623
624    #[test]
625    fn test_io_error_propagation() {
626        let io_error =
627            io::Error::new(io::ErrorKind::NotFound, "file not found");
628        let error: MetadataError = io_error.into();
629        assert_eq!(error.to_string(), "I/O error: file not found");
630        assert!(matches!(error, MetadataError::IoError(_)));
631    }
632
633    #[test]
634    fn test_yaml_error_propagation() {
635        let yaml_error = noyalib::Error::custom("Custom YAML error");
636        let error: MetadataError = yaml_error.into();
637        assert_eq!(
638            error.to_string(),
639            "YAML parsing error: Custom YAML error"
640        );
641        assert!(matches!(error, MetadataError::YamlError(_)));
642    }
643
644    #[test]
645    fn test_json_error_propagation() {
646        let json_error = serde_json::Error::custom("Custom JSON error");
647        let error: MetadataError = json_error.into();
648        assert_eq!(
649            error.to_string(),
650            "JSON parsing error: Custom JSON error"
651        );
652        assert!(matches!(error, MetadataError::JsonError(_)));
653    }
654
655    #[test]
656    fn test_toml_error_propagation() {
657        let toml_error = toml::de::Error::custom("Custom TOML error");
658        let error: MetadataError = toml_error.into();
659        assert_eq!(
660            error.to_string(),
661            "TOML parsing error: Custom TOML error\n"
662        );
663        assert!(matches!(error, MetadataError::TomlError(_)));
664    }
665
666    #[test]
667    fn test_missing_field_error_debug() {
668        let error =
669            MetadataError::MissingFieldError("title".to_string());
670        assert_eq!(
671            format!("{:?}", error),
672            r#"MissingFieldError("title")"#
673        );
674    }
675
676    #[test]
677    fn test_date_parse_error_debug() {
678        let error = MetadataError::DateParseError(
679            "Invalid date format".to_string(),
680        );
681        assert_eq!(
682            format!("{:?}", error),
683            r#"DateParseError("Invalid date format")"#
684        );
685    }
686
687    #[test]
688    fn test_empty_yaml_error_message() {
689        let yaml_error = noyalib::Error::custom("");
690        let error: MetadataError = yaml_error.into();
691        assert_eq!(error.to_string(), "YAML parsing error: ");
692    }
693
694    #[test]
695    fn test_empty_json_error_message() {
696        let json_error = serde_json::Error::custom("");
697        let error: MetadataError = json_error.into();
698        assert_eq!(error.to_string(), "JSON parsing error: ");
699    }
700
701    #[test]
702    fn test_empty_toml_error_message() {
703        let toml_error = toml::de::Error::custom("");
704        let error: MetadataError = toml_error.into();
705        assert_eq!(error.to_string(), "TOML parsing error: \n");
706    }
707
708    // A custom error for testing purposes
709    #[derive(Debug)]
710    struct CustomError;
711
712    impl fmt::Display for CustomError {
713        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
714            write!(f, "Custom error occurred")
715        }
716    }
717
718    impl Error for CustomError {}
719
720    #[test]
721    fn test_context_error_fmt() {
722        let custom_error = CustomError;
723        let context_error = ContextError {
724            context: "An error occurred while processing".to_string(),
725            source: Box::new(custom_error),
726        };
727
728        let formatted = format!("{}", context_error);
729        assert_eq!(
730            formatted,
731            "An error occurred while processing: Custom error occurred"
732        );
733    }
734
735    #[test]
736    fn test_context_error_source() {
737        let custom_error = CustomError;
738        let context_error = ContextError {
739            context: "Error with context".to_string(),
740            source: Box::new(custom_error),
741        };
742
743        // The source method should return a reference to the original error (custom_error in this case)
744        let source = context_error.source().unwrap();
745        assert_eq!(source.to_string(), "Custom error occurred");
746    }
747
748    #[test]
749    fn test_context_error_debug() {
750        let custom_error = CustomError;
751        let context_error = ContextError {
752            context: "Error during processing".to_string(),
753            source: Box::new(custom_error),
754        };
755
756        let debug_output = format!("{:?}", context_error);
757
758        // Ensure the debug output includes the "ContextError" struct and its fields
759        assert!(debug_output.contains("ContextError"));
760        assert!(debug_output.contains("Error during processing"));
761        assert!(debug_output.contains("CustomError"));
762    }
763}