ggen_utils/
error.rs

1//! Error handling types and utilities
2//!
3//! This module provides the core error handling infrastructure for the ggen project.
4//! It defines a custom `Error` type that supports error chaining, context, and
5//! conversion from common error types.
6//!
7//! ## Features
8//!
9//! - **Error chaining**: Support for source errors with full error chain
10//! - **Context information**: Additional context can be attached to errors
11//! - **Type conversions**: Automatic conversion from common error types
12//! - **Helper methods**: Convenient constructors for common error scenarios
13//!
14//! ## Error Type
15//!
16//! The `Error` type is the primary error type used throughout ggen. It implements
17//! `std::error::Error` and provides:
18//!
19//! - Message: Primary error message
20//! - Context: Optional additional context
21//! - Source: Optional underlying error (for error chaining)
22//!
23//! ## Examples
24//!
25//! ### Creating Errors
26//!
27//! ```rust
28//! use ggen_utils::error::Error;
29//!
30//! # fn main() {
31//! // Simple error
32//! let err = Error::new("Something went wrong");
33//! assert_eq!(err.to_string(), "Something went wrong");
34//!
35//! // Error with context
36//! let err = Error::with_context("Failed to read file", "config.toml");
37//! assert!(err.to_string().contains("Failed to read file"));
38//!
39//! // Error with source
40//! let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "File not found");
41//! let err = Error::with_source("Configuration error", Box::new(io_err));
42//! assert!(err.to_string().contains("Configuration error"));
43//! # }
44//! ```
45//!
46//! ### Using Result Type
47//!
48//! ```rust,no_run
49//! use ggen_utils::error::{Error, Result};
50//!
51//! fn read_config() -> Result<String> {
52//!     std::fs::read_to_string("config.toml")
53//!         .map_err(|e| Error::with_source("Failed to read config", Box::new(e)))
54//! }
55//!
56//! # fn main() -> Result<()> {
57//! let _config = read_config()?;
58//! # Ok(())
59//! # }
60//! ```
61//!
62//! ### Helper Methods
63//!
64//! ```rust
65//! use ggen_utils::error::Error;
66//! use std::path::PathBuf;
67//!
68//! # fn main() {
69//! // Common error types
70//! let err = Error::file_not_found(PathBuf::from("config.toml"));
71//! assert!(err.to_string().contains("config.toml"));
72//!
73//! let err = Error::invalid_input("Invalid project name");
74//! assert!(err.to_string().contains("Invalid project name"));
75//!
76//! let err = Error::network_error("Connection timeout");
77//! assert!(err.to_string().contains("Connection timeout"));
78//! # }
79//! ```
80
81use std::error::Error as StdError;
82use std::fmt;
83
84/// Custom error type for the ggen project
85#[derive(Debug)]
86pub struct Error {
87    message: String,
88    context: Option<String>,
89    source: Option<Box<dyn StdError + Send + Sync>>,
90}
91
92impl Error {
93    /// Create a new error with a message
94    #[must_use]
95    pub fn new(message: &str) -> Self {
96        Self {
97            message: message.to_string(),
98            context: None,
99            source: None,
100        }
101    }
102
103    /// Create a new error with a formatted message
104    #[must_use]
105    pub fn new_fmt(args: std::fmt::Arguments) -> Self {
106        Self {
107            message: args.to_string(),
108            context: None,
109            source: None,
110        }
111    }
112
113    /// Create an error with additional context
114    #[must_use]
115    pub fn with_context(message: &str, context: &str) -> Self {
116        Self {
117            message: message.to_string(),
118            context: Some(context.to_string()),
119            source: None,
120        }
121    }
122
123    /// Create an error with a source error
124    #[must_use]
125    pub fn with_source(message: &str, source: Box<dyn StdError + Send + Sync>) -> Self {
126        Self {
127            message: message.to_string(),
128            context: None,
129            source: Some(source),
130        }
131    }
132
133    /// Add context to an existing error, creating a new error with the context as the message
134    /// and the original error as the source
135    #[must_use]
136    pub fn context<C>(self, context: C) -> Self
137    where
138        C: fmt::Display + Send + Sync + 'static,
139    {
140        Self {
141            message: context.to_string(),
142            context: None,
143            source: Some(Box::new(self)),
144        }
145    }
146
147    /// Add context to an existing error using a closure, creating a new error with the context as the message
148    /// and the original error as the source
149    #[must_use]
150    pub fn with_context_fn<C, F>(self, f: F) -> Self
151    where
152        C: fmt::Display + Send + Sync + 'static,
153        F: FnOnce() -> C,
154    {
155        Self {
156            message: f().to_string(),
157            context: None,
158            source: Some(Box::new(self)),
159        }
160    }
161}
162
163impl fmt::Display for Error {
164    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
165        write!(f, "{}", self.message)?;
166
167        if let Some(context) = &self.context {
168            write!(f, " (context: {context})")?;
169        }
170
171        if let Some(source) = &self.source {
172            write!(f, " (caused by: {source})")?;
173        }
174
175        Ok(())
176    }
177}
178
179impl StdError for Error {
180    fn source(&self) -> Option<&(dyn StdError + 'static)> {
181        self.source
182            .as_ref()
183            .map(|s| s.as_ref() as &(dyn StdError + 'static))
184    }
185}
186
187/// Result type alias for the ggen project
188pub type Result<T> = std::result::Result<T, Error>;
189
190// Implement From for common error types
191impl From<std::io::Error> for Error {
192    fn from(err: std::io::Error) -> Self {
193        Self::new(&err.to_string())
194    }
195}
196
197impl From<serde_yaml::Error> for Error {
198    fn from(err: serde_yaml::Error) -> Self {
199        Self::new(&err.to_string())
200    }
201}
202
203impl From<serde_json::Error> for Error {
204    fn from(err: serde_json::Error) -> Self {
205        Self::new(&err.to_string())
206    }
207}
208
209impl From<tera::Error> for Error {
210    fn from(err: tera::Error) -> Self {
211        Self::new(&err.to_string())
212    }
213}
214
215impl From<config::ConfigError> for Error {
216    fn from(err: config::ConfigError) -> Self {
217        Self::new(&err.to_string())
218    }
219}
220
221impl From<log::SetLoggerError> for Error {
222    fn from(err: log::SetLoggerError) -> Self {
223        Self::new(&err.to_string())
224    }
225}
226
227impl<T> From<std::sync::PoisonError<T>> for Error {
228    fn from(err: std::sync::PoisonError<T>) -> Self {
229        Self::new(&err.to_string())
230    }
231}
232
233impl From<anyhow::Error> for Error {
234    fn from(err: anyhow::Error) -> Self {
235        Self::new(&err.to_string())
236    }
237}
238
239impl From<toml::de::Error> for Error {
240    fn from(err: toml::de::Error) -> Self {
241        Self::new(&err.to_string())
242    }
243}
244
245impl From<String> for Error {
246    fn from(err: String) -> Self {
247        Self::new(&err)
248    }
249}
250
251impl From<&str> for Error {
252    fn from(err: &str) -> Self {
253        Self::new(err)
254    }
255}
256
257// Oxigraph error types
258impl From<oxigraph::store::StorageError> for Error {
259    fn from(err: oxigraph::store::StorageError) -> Self {
260        Self::new(&err.to_string())
261    }
262}
263
264impl From<oxigraph::sparql::QueryEvaluationError> for Error {
265    fn from(err: oxigraph::sparql::QueryEvaluationError) -> Self {
266        Self::new(&err.to_string())
267    }
268}
269
270impl From<oxigraph::store::SerializerError> for Error {
271    fn from(err: oxigraph::store::SerializerError) -> Self {
272        Self::new(&err.to_string())
273    }
274}
275
276impl From<toml::ser::Error> for Error {
277    fn from(err: toml::ser::Error) -> Self {
278        Self::new(&err.to_string())
279    }
280}
281
282/// GgenError type alias for backwards compatibility with P2P module
283pub type GgenError = Error;
284
285/// Extension trait for adding context to Results, similar to anyhow::Context
286pub trait Context<T> {
287    /// Add context to an error result
288    fn context<C>(self, context: C) -> Result<T>
289    where
290        C: fmt::Display + Send + Sync + 'static;
291
292    /// Add context to an error result using a closure
293    fn with_context<C, F>(self, f: F) -> Result<T>
294    where
295        C: fmt::Display + Send + Sync + 'static,
296        F: FnOnce() -> C;
297}
298
299impl<T> Context<T> for Result<T> {
300    fn context<C>(self, context: C) -> Result<T>
301    where
302        C: fmt::Display + Send + Sync + 'static,
303    {
304        self.map_err(|e| e.context(context))
305    }
306
307    fn with_context<C, F>(self, f: F) -> Result<T>
308    where
309        C: fmt::Display + Send + Sync + 'static,
310        F: FnOnce() -> C,
311    {
312        self.map_err(|e| e.with_context_fn(f))
313    }
314}
315
316/// Return early with an error
317///
318/// This macro is similar to `anyhow::bail!` and provides a convenient way to
319/// return early from a function with an error.
320///
321/// # Examples
322///
323/// ```rust
324/// use ggen_utils::error::{Result, bail};
325///
326/// fn validate_positive(n: i32) -> Result<()> {
327///     if n < 0 {
328///         bail!("Number must be positive, got {}", n);
329///     }
330///     Ok(())
331/// }
332/// ```
333#[macro_export]
334macro_rules! bail {
335    ($msg:literal $(,)?) => {
336        return Err($crate::error::Error::new($msg));
337    };
338    ($fmt:expr, $($arg:tt)*) => {
339        return Err($crate::error::Error::new(&format!($fmt, $($arg)*)));
340    };
341}
342
343/// Ensure a condition is true, or return early with an error
344///
345/// This macro is similar to `anyhow::ensure!` and provides a convenient way to
346/// check conditions and return early with an error if they fail.
347///
348/// # Examples
349///
350/// ```rust
351/// use ggen_utils::error::{Result, ensure};
352///
353/// fn divide(a: i32, b: i32) -> Result<i32> {
354///     ensure!(b != 0, "Division by zero");
355///     Ok(a / b)
356/// }
357/// ```
358#[macro_export]
359macro_rules! ensure {
360    ($condition:expr, $msg:literal $(,)?) => {
361        if !$condition {
362            $crate::bail!($msg);
363        }
364    };
365    ($condition:expr, $fmt:expr, $($arg:tt)*) => {
366        if !$condition {
367            $crate::bail!($fmt, $($arg)*);
368        }
369    };
370}
371
372impl Error {
373    /// Create an invalid input error
374    #[must_use]
375    pub fn invalid_input(message: impl Into<String>) -> Self {
376        let msg = message.into();
377        Self::new(&format!("Invalid input: {}", msg))
378    }
379
380    /// Create a network error
381    #[must_use]
382    pub fn network_error(message: impl Into<String>) -> Self {
383        let msg = message.into();
384        Self::new(&format!("Network error: {}", msg))
385    }
386
387    /// Create a feature not enabled error
388    #[must_use]
389    pub fn feature_not_enabled(feature: &str, help: &str) -> Self {
390        Self::new(&format!("Feature '{}' not enabled. {}", feature, help))
391    }
392
393    /// Create a file not found error
394    #[must_use]
395    pub fn file_not_found(path: std::path::PathBuf) -> Self {
396        Self::new(&format!("File not found: {}", path.display()))
397    }
398
399    /// Create an IO error
400    #[must_use]
401    pub fn io_error(message: impl Into<String>) -> Self {
402        let msg = message.into();
403        Self::new(&format!("IO error: {}", msg))
404    }
405
406    /// Create an internal error
407    #[must_use]
408    pub fn internal_error(message: impl Into<String>) -> Self {
409        let msg = message.into();
410        Self::new(&format!("Internal error: {}", msg))
411    }
412
413    /// Create an invalid state error
414    #[must_use]
415    pub fn invalid_state(message: impl Into<String>) -> Self {
416        let msg = message.into();
417        Self::new(&format!("Invalid state: {}", msg))
418    }
419}
420
421#[cfg(test)]
422mod tests {
423    use super::*;
424
425    #[test]
426    fn test_error_creation() {
427        let error = Error::new("Test error message");
428        assert_eq!(error.message, "Test error message");
429    }
430
431    #[test]
432    fn test_error_display() {
433        let error = Error::new("Test error message");
434        let display = format!("{error}");
435        assert_eq!(display, "Test error message");
436    }
437
438    #[test]
439    fn test_error_debug() {
440        let error = Error::new("Test error message");
441        let debug = format!("{error:?}");
442        assert!(debug.contains("Test error message"));
443    }
444
445    #[test]
446    fn test_error_from_io_error() {
447        let io_error = std::io::Error::new(std::io::ErrorKind::NotFound, "File not found");
448        let error: Error = io_error.into();
449
450        assert!(error.to_string().contains("File not found"));
451    }
452
453    #[test]
454    fn test_error_from_yaml_error() {
455        let yaml_content = "invalid: yaml: content: [";
456        let yaml_error = serde_yaml::from_str::<serde_yaml::Value>(yaml_content).unwrap_err();
457        let error: Error = yaml_error.into();
458
459        assert!(!error.to_string().is_empty());
460    }
461
462    #[test]
463    fn test_error_from_json_error() {
464        let json_content = "invalid json content";
465        let json_error = serde_json::from_str::<serde_json::Value>(json_content).unwrap_err();
466        let error: Error = json_error.into();
467
468        assert!(!error.to_string().is_empty());
469    }
470
471    #[test]
472    fn test_error_from_tera_error() {
473        let template_content = "{{ invalid template syntax";
474        let tera_error = tera::Tera::new("templates/**/*")
475            .unwrap()
476            .render_str(template_content, &tera::Context::new())
477            .unwrap_err();
478        let error: Error = tera_error.into();
479
480        assert!(!error.to_string().is_empty());
481    }
482
483    #[test]
484    fn test_result_type() {
485        fn success_function() -> String {
486            "success".to_string()
487        }
488
489        fn error_function() -> Result<String> {
490            Err(Error::new("error"))
491        }
492
493        assert_eq!(success_function(), "success");
494
495        assert!(error_function().is_err());
496        assert_eq!(error_function().unwrap_err().to_string(), "error");
497    }
498
499    #[test]
500    fn test_error_chain() {
501        let io_error =
502            std::io::Error::new(std::io::ErrorKind::PermissionDenied, "Permission denied");
503        let error: Error = io_error.into();
504
505        // Test that the error can be used as std::error::Error
506        let error_ref: &dyn std::error::Error = &error;
507        assert!(!error_ref.to_string().is_empty());
508    }
509
510    #[test]
511    fn test_context_trait() {
512        let result: Result<()> = Err(Error::new("Original error"));
513        let result_with_context = result.context("Failed to process");
514        assert!(result_with_context.is_err());
515        let err = result_with_context.unwrap_err();
516        assert!(err.to_string().contains("Failed to process"));
517    }
518
519    #[test]
520    fn test_with_context_trait() {
521        let result: Result<()> = Err(Error::new("Original error"));
522        let result_with_context = result.with_context(|| format!("Failed at step {}", 1));
523        assert!(result_with_context.is_err());
524        let err = result_with_context.unwrap_err();
525        assert!(err.to_string().contains("Failed at step 1"));
526    }
527
528    #[test]
529    fn test_error_context_method() {
530        let error = Error::new("Original error");
531        let error_with_context = error.context("Additional context");
532        assert!(error_with_context
533            .to_string()
534            .contains("Additional context"));
535    }
536}