Skip to main content

ggen_core/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 crate::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 crate::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 crate::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_core::utils::error::Result;
325/// use ggen_core::bail;
326///
327/// fn validate_positive(n: i32) -> Result<()> {
328///     if n < 0 {
329///         bail!("Number must be positive, got {}", n);
330///     }
331///     Ok(())
332/// }
333/// ```
334#[macro_export]
335macro_rules! bail {
336    ($msg:literal $(,)?) => {
337        return Err($crate::utils::error::Error::new($msg))
338    };
339    ($fmt:expr, $($arg:tt)*) => {
340        return Err($crate::utils::error::Error::new(&format!($fmt, $($arg)*)))
341    };
342}
343
344/// Ensure a condition is true, or return early with an error
345///
346/// This macro is similar to `anyhow::ensure!` and provides a convenient way to
347/// check conditions and return early with an error if they fail.
348///
349/// # Examples
350///
351/// ```rust
352/// use ggen_core::utils::error::Result;
353/// use ggen_core::ensure;
354///
355/// fn divide(a: i32, b: i32) -> Result<i32> {
356///     ensure!(b != 0, "Division by zero");
357///     Ok(a / b)
358/// }
359/// ```
360#[macro_export]
361macro_rules! ensure {
362    ($condition:expr, $msg:literal $(,)?) => {
363        if !$condition {
364            $crate::bail!($msg);
365        }
366    };
367    ($condition:expr, $fmt:expr, $($arg:tt)*) => {
368        if !$condition {
369            $crate::bail!($fmt, $($arg)*);
370        }
371    };
372}
373
374/// Create a new Error from a format string or literal
375#[macro_export]
376macro_rules! ggen_error {
377    ($msg:literal $(,)?) => {
378        $crate::utils::error::Error::new($msg)
379    };
380    ($fmt:expr, $($arg:tt)*) => {
381        $crate::utils::error::Error::new(&format!($fmt, $($arg)*))
382    };
383}
384
385pub use crate::ggen_error;
386
387impl Error {
388    /// Create an invalid input error
389    #[must_use]
390    pub fn invalid_input(message: impl Into<String>) -> Self {
391        let msg = message.into();
392        Self::new(&format!("Invalid input: {}", msg))
393    }
394
395    /// Create a network error
396    #[must_use]
397    pub fn network_error(message: impl Into<String>) -> Self {
398        let msg = message.into();
399        Self::new(&format!("Network error: {}", msg))
400    }
401
402    /// Create a feature not enabled error
403    #[must_use]
404    pub fn feature_not_enabled(feature: &str, help: &str) -> Self {
405        Self::new(&format!("Feature '{}' not enabled. {}", feature, help))
406    }
407
408    /// Create a file not found error
409    #[must_use]
410    pub fn file_not_found(path: std::path::PathBuf) -> Self {
411        Self::new(&format!("File not found: {}", path.display()))
412    }
413
414    /// Create an IO error
415    #[must_use]
416    pub fn io_error(message: impl Into<String>) -> Self {
417        let msg = message.into();
418        Self::new(&format!("IO error: {}", msg))
419    }
420
421    /// Create an internal error
422    #[must_use]
423    pub fn internal_error(message: impl Into<String>) -> Self {
424        let msg = message.into();
425        Self::new(&format!("Internal error: {}", msg))
426    }
427
428    /// Create an invalid state error
429    #[must_use]
430    pub fn invalid_state(message: impl Into<String>) -> Self {
431        let msg = message.into();
432        Self::new(&format!("Invalid state: {}", msg))
433    }
434}
435
436#[cfg(test)]
437mod tests {
438    use super::*;
439
440    #[test]
441    fn test_error_creation() {
442        let error = Error::new("Test error message");
443        assert_eq!(error.message, "Test error message");
444    }
445
446    #[test]
447    fn test_error_display() {
448        let error = Error::new("Test error message");
449        let display = format!("{error}");
450        assert_eq!(display, "Test error message");
451    }
452
453    #[test]
454    fn test_error_debug() {
455        let error = Error::new("Test error message");
456        let debug = format!("{error:?}");
457        assert!(debug.contains("Test error message"));
458    }
459
460    #[test]
461    fn test_error_from_io_error_old() {
462        let io_error = std::io::Error::new(std::io::ErrorKind::NotFound, "File not found");
463        let error: Error = io_error.into();
464
465        assert!(error.to_string().contains("File not found"));
466    }
467
468    #[test]
469    fn test_error_from_yaml_error() {
470        let yaml_content = "invalid: yaml: content: [";
471        let yaml_error = serde_yaml::from_str::<serde_yaml::Value>(yaml_content).unwrap_err();
472        let error: Error = yaml_error.into();
473
474        assert!(!error.to_string().is_empty());
475    }
476
477    #[test]
478    fn test_error_from_json_error() {
479        let json_content = "invalid json content";
480        let json_error = serde_json::from_str::<serde_json::Value>(json_content).unwrap_err();
481        let error: Error = json_error.into();
482
483        assert!(!error.to_string().is_empty());
484    }
485
486    #[test]
487    #[ignore]
488    fn test_error_from_tera_error() {
489        let template_content = "{{ invalid template syntax";
490        let tera_error = tera::Tera::new("templates/**/*")
491            .unwrap()
492            .render_str(template_content, &tera::Context::new())
493            .unwrap_err();
494        let error: Error = tera_error.into();
495
496        assert!(!error.to_string().is_empty());
497    }
498
499    #[test]
500    fn test_result_type() {
501        fn success_function() -> String {
502            "success".to_string()
503        }
504
505        fn error_function() -> Result<String> {
506            Err(Error::new("error"))
507        }
508
509        assert_eq!(success_function(), "success");
510
511        assert!(error_function().is_err());
512        assert_eq!(error_function().unwrap_err().to_string(), "error");
513    }
514
515    #[test]
516    fn test_error_chain() {
517        let io_error =
518            std::io::Error::new(std::io::ErrorKind::PermissionDenied, "Permission denied");
519        let error: Error = io_error.into();
520
521        // Test that the error can be used as std::error::Error
522        let error_ref: &dyn std::error::Error = &error;
523        assert!(!error_ref.to_string().is_empty());
524    }
525
526    #[test]
527    fn test_context_trait() {
528        let result: Result<()> = Err(Error::new("Original error"));
529        let result_with_context = result.context("Failed to process");
530        assert!(result_with_context.is_err());
531        let err = result_with_context.unwrap_err();
532        assert!(err.to_string().contains("Failed to process"));
533    }
534
535    #[test]
536    fn test_with_context_trait() {
537        let result: Result<()> = Err(Error::new("Original error"));
538        let result_with_context = result.with_context(|| format!("Failed at step {}", 1));
539        assert!(result_with_context.is_err());
540        let err = result_with_context.unwrap_err();
541        assert!(err.to_string().contains("Failed at step 1"));
542    }
543
544    #[test]
545    fn test_error_context_method() {
546        let error = Error::new("Original error");
547        let error_with_context = error.context("Additional context");
548        assert!(error_with_context
549            .to_string()
550            .contains("Additional context"));
551    }
552}