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;