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 crate::metadata::FrontMatterFormat;
7use alloc::boxed::Box;
8use alloc::format;
9use alloc::string::{String, ToString};
10use core::fmt::Display;
11use core::ops::Range;
12#[cfg(feature = "yaml")]
13use noyalib::Error as SerdeYmlError;
14#[cfg(any(feature = "yaml", feature = "toml", feature = "json"))]
15use serde::de::Error as SerdeError;
16use thiserror::Error;
17
18/// A boxed error that can cross threads; the payload of
19/// [`MetadataError::Other`] and the `source` of [`MetadataError::Parse`].
20pub type BoxedError =
21    Box<dyn core::error::Error + Send + Sync + 'static>;
22
23/// A custom error type to add context to the `Other` variant of `MetadataError`.
24///
25/// This struct wraps another error and provides additional context information.
26#[derive(Debug)]
27pub struct ContextError {
28    /// The context message providing additional information about the error.
29    context: String,
30    /// The source error that this `ContextError` is wrapping.
31    source: BoxedError,
32}
33
34/// Displays the context error as `"context: source"`.
35impl core::fmt::Display for ContextError {
36    fn fmt(
37        &self,
38        f: &mut core::fmt::Formatter<'_>,
39    ) -> core::fmt::Result {
40        write!(f, "{}: {}", self.context, self.source)
41    }
42}
43
44/// Provides access to the underlying source error.
45impl core::error::Error for ContextError {
46    fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
47        Some(&*self.source)
48    }
49}
50
51/// Custom error types for the metadata-gen library.
52///
53/// This enum encompasses all possible errors that can occur during
54/// metadata extraction, processing, and related operations.
55#[derive(Error, Debug)]
56#[non_exhaustive]
57pub enum MetadataError {
58    /// A front-matter block was found but its parser rejected it.
59    ///
60    /// `span` is the byte range inside the raw block the parser pointed
61    /// at, when it reported one (TOML and JSON always do; YAML reports
62    /// a line and column that are mapped to an offset). `source` is the
63    /// parser's own error, reachable through `Error::source`.
64    #[error("{format} front matter failed to parse{}: {source}", span_suffix(.span))]
65    Parse {
66        /// Which parser rejected the block.
67        format: FrontMatterFormat,
68        /// Byte range inside the raw front-matter block, if the parser gave one.
69        span: Option<Range<usize>>,
70        /// The parser's error.
71        #[source]
72        source: BoxedError,
73    },
74
75    /// Error occurred while extracting metadata.
76    #[error("Failed to extract metadata: {message}")]
77    ExtractionError {
78        /// A descriptive message about the extraction error.
79        message: String,
80    },
81
82    /// Error occurred while processing metadata.
83    #[error("Failed to process metadata: {message}")]
84    ProcessingError {
85        /// A descriptive message about the processing error.
86        message: String,
87    },
88
89    /// Error occurred due to missing required field.
90    #[error("Missing required metadata field: {0}")]
91    MissingFieldError(String),
92
93    /// Error occurred while parsing date.
94    #[error("Failed to parse date: {0}")]
95    DateParseError(String),
96
97    /// I/O error.
98    #[cfg(feature = "std")]
99    #[cfg_attr(docsrs, doc(cfg(feature = "std")))]
100    #[error("I/O error: {0}")]
101    IoError(#[from] std::io::Error),
102
103    /// YAML parsing error.
104    #[cfg(feature = "yaml")]
105    #[cfg_attr(docsrs, doc(cfg(feature = "yaml")))]
106    #[error("YAML parsing error: {0}")]
107    YamlError(#[from] SerdeYmlError),
108
109    /// JSON parsing error.
110    #[cfg(feature = "json")]
111    #[cfg_attr(docsrs, doc(cfg(feature = "json")))]
112    #[error("JSON parsing error: {0}")]
113    JsonError(#[from] serde_json::Error),
114
115    /// TOML parsing error.
116    #[cfg(feature = "toml")]
117    #[cfg_attr(docsrs, doc(cfg(feature = "toml")))]
118    #[error("TOML parsing error: {0}")]
119    TomlError(#[from] toml::de::Error),
120
121    /// Unsupported metadata format error.
122    #[error("Unsupported metadata format: {0}")]
123    UnsupportedFormatError(String),
124
125    /// Validation error for metadata fields.
126    #[error("Metadata validation error: {field} - {message}")]
127    ValidationError {
128        /// The field that failed validation.
129        field: String,
130        /// A descriptive message about the validation error.
131        message: String,
132    },
133
134    /// UTF-8 decoding error.
135    ///
136    /// No function in the crate returns it: every entry point takes
137    /// `&str`, and the readers in `metadata_gen::io` report invalid UTF-8 as
138    /// an I/O error. Kept for code that matches on it or converts a
139    /// `core::str::Utf8Error` with `?`.
140    #[deprecated(
141        since = "0.0.8",
142        note = "never produced by the crate; match on `IoError` for invalid UTF-8 from a reader"
143    )]
144    #[error("UTF-8 decoding error: {0}")]
145    Utf8Error(#[from] core::str::Utf8Error),
146
147    /// Catch-all for unexpected errors.
148    ///
149    /// There is deliberately no `From<BoxedError>`: a boxed error never
150    /// becomes a `MetadataError` by accident through `?`. Build it with
151    /// [`MetadataError::other`] where the coercion is intended.
152    #[error("Unexpected error: {0}")]
153    Other(BoxedError),
154}
155
156/// Renders ` at bytes a..b` for the `Parse` display, or nothing.
157fn span_suffix(span: &Option<Range<usize>>) -> String {
158    match span {
159        Some(r) => format!(" at bytes {}..{}", r.start, r.end),
160        None => String::new(),
161    }
162}
163
164impl MetadataError {
165    /// Wraps any error as [`MetadataError::Other`].
166    ///
167    /// # Example
168    ///
169    /// ```
170    /// use metadata_gen::error::MetadataError;
171    ///
172    /// let err = MetadataError::other(core::fmt::Error);
173    /// assert!(matches!(err, MetadataError::Other(_)));
174    /// ```
175    pub fn other<E>(error: E) -> Self
176    where
177        E: core::error::Error + Send + Sync + 'static,
178    {
179        Self::Other(Box::new(error))
180    }
181
182    /// Builds a [`MetadataError::Parse`] from a parser's error.
183    #[cfg(any(feature = "yaml", feature = "toml", feature = "json"))]
184    pub(crate) fn parse<E>(
185        format: FrontMatterFormat,
186        span: Option<Range<usize>>,
187        source: E,
188    ) -> Self
189    where
190        E: core::error::Error + Send + Sync + 'static,
191    {
192        Self::Parse {
193            format,
194            span,
195            source: Box::new(source),
196        }
197    }
198
199    /// Creates a new `ExtractionError` with the given message.
200    ///
201    /// # Arguments
202    ///
203    /// * `message` - A descriptive message about the extraction error.
204    ///
205    /// # Returns
206    ///
207    /// A new `MetadataError::ExtractionError` variant.
208    ///
209    /// # Example
210    ///
211    /// ```
212    /// use metadata_gen::error::MetadataError;
213    ///
214    /// let error = MetadataError::new_extraction_error("Failed to extract title");
215    /// assert!(matches!(error, MetadataError::ExtractionError { .. }));
216    /// ```
217    pub fn new_extraction_error(message: impl Into<String>) -> Self {
218        Self::ExtractionError {
219            message: message.into(),
220        }
221    }
222
223    /// Creates a new `ProcessingError` with the given message.
224    ///
225    /// # Arguments
226    ///
227    /// * `message` - A descriptive message about the processing error.
228    ///
229    /// # Returns
230    ///
231    /// A new `MetadataError::ProcessingError` variant.
232    ///
233    /// # Example
234    ///
235    /// ```
236    /// use metadata_gen::error::MetadataError;
237    ///
238    /// let error = MetadataError::new_processing_error("Failed to process metadata");
239    /// assert!(matches!(error, MetadataError::ProcessingError { .. }));
240    /// ```
241    pub fn new_processing_error(message: impl Into<String>) -> Self {
242        Self::ProcessingError {
243            message: message.into(),
244        }
245    }
246
247    /// Creates a new `ValidationError` with the given field and message.
248    ///
249    /// # Arguments
250    ///
251    /// * `field` - The name of the field that failed validation.
252    /// * `message` - A descriptive message about the validation error.
253    ///
254    /// # Returns
255    ///
256    /// A new `MetadataError::ValidationError` variant.
257    ///
258    /// # Example
259    ///
260    /// ```
261    /// use metadata_gen::error::MetadataError;
262    ///
263    /// let error = MetadataError::new_validation_error("title", "Title must not be empty");
264    /// assert!(matches!(error, MetadataError::ValidationError { .. }));
265    /// ```
266    pub fn new_validation_error(
267        field: impl Into<String>,
268        message: impl Into<String>,
269    ) -> Self {
270        Self::ValidationError {
271            field: field.into(),
272            message: message.into(),
273        }
274    }
275
276    /// Adds context to an existing error.
277    ///
278    /// This method wraps the current error with additional context information.
279    ///
280    /// # Arguments
281    ///
282    /// * `ctx` - The context to add to the error.
283    ///
284    /// # Returns
285    ///
286    /// A new `MetadataError` with the added context.
287    ///
288    /// # Example
289    ///
290    /// ```
291    /// use metadata_gen::error::MetadataError;
292    ///
293    /// let error = MetadataError::new_extraction_error("Failed to parse YAML")
294    ///     .context("Processing file 'example.md'");
295    /// assert_eq!(error.to_string(), "Failed to extract metadata: Processing file 'example.md': Failed to parse YAML");
296    /// ```
297    pub fn context<C>(self, ctx: C) -> Self
298    where
299        C: Display + Send + Sync + 'static,
300    {
301        match self {
302            #[cfg(feature = "std")]
303            Self::IoError(error) => Self::IoError(std::io::Error::new(
304                error.kind(),
305                format!("{}: {}", ctx, error),
306            )),
307            #[cfg(feature = "yaml")]
308            Self::YamlError(error) => Self::YamlError(
309                SerdeYmlError::custom(format!("{}: {}", ctx, error)),
310            ),
311            #[cfg(feature = "json")]
312            Self::JsonError(error) => {
313                Self::JsonError(serde_json::Error::custom(format!(
314                    "{}: {}",
315                    ctx, error
316                )))
317            }
318            #[cfg(feature = "toml")]
319            Self::TomlError(error) => Self::TomlError(
320                toml::de::Error::custom(format!("{}: {}", ctx, error)),
321            ),
322            Self::Parse {
323                format,
324                span,
325                source,
326            } => Self::Parse {
327                format,
328                span,
329                source: Box::new(ContextError {
330                    context: ctx.to_string(),
331                    source,
332                }),
333            },
334            Self::Other(error) => Self::Other(Box::new(ContextError {
335                context: ctx.to_string(),
336                source: error,
337            })),
338            other => other.prefix_message(&ctx),
339        }
340    }
341
342    /// Prefixes `ctx` onto the variants whose payload is a plain message.
343    /// Variants that wrap a foreign error are handled by [`Self::context`].
344    fn prefix_message<C: Display>(self, ctx: &C) -> Self {
345        match self {
346            Self::ExtractionError { message } => {
347                Self::ExtractionError {
348                    message: format!("{}: {}", ctx, message),
349                }
350            }
351            Self::ProcessingError { message } => {
352                Self::ProcessingError {
353                    message: format!("{}: {}", ctx, message),
354                }
355            }
356            Self::MissingFieldError(field) => {
357                Self::MissingFieldError(format!("{}: {}", ctx, field))
358            }
359            Self::DateParseError(error) => {
360                Self::DateParseError(format!("{}: {}", ctx, error))
361            }
362            Self::UnsupportedFormatError(format) => {
363                Self::UnsupportedFormatError(format!(
364                    "{}: {}",
365                    ctx, format
366                ))
367            }
368            Self::ValidationError { field, message } => {
369                Self::ValidationError {
370                    field,
371                    message: format!("{}: {}", ctx, message),
372                }
373            }
374            other => other,
375        }
376    }
377}
378
379#[cfg(all(
380    test,
381    feature = "std",
382    feature = "yaml",
383    feature = "toml",
384    feature = "json"
385))]
386mod tests;
387
388#[cfg(all(
389    test,
390    feature = "std",
391    feature = "yaml",
392    feature = "toml",
393    feature = "json"
394))]
395mod context_tests;