-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathtraceable.rs
More file actions
190 lines (176 loc) · 5.81 KB
/
Copy pathtraceable.rs
File metadata and controls
190 lines (176 loc) · 5.81 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
//! Traceable trait for error formatting and trace generation
//!
//! The `Traceable` trait allows errors to provide detailed trace information
//! without requiring them to implement `Serialize`. This decouples error types
//! from serialization constraints and allows custom formatting per error type.
use super::ErrorKind;
/// Trait for errors that can generate detailed traces
///
/// This trait enables errors to provide custom formatted trace entries and
/// maintain error chain information. Unlike requiring `Serialize`, this approach
/// allows errors to contain non-serializable data while still generating
/// comprehensive trace files.
///
/// # Example
///
/// ```rust
/// use torrust_tracker_deployer_lib::shared::error::Traceable;
/// use torrust_tracker_deployer_lib::shared::ErrorKind;
///
/// #[derive(Debug, thiserror::Error)]
/// enum MyError {
/// #[error("Operation failed: {reason}")]
/// OperationFailed {
/// reason: String,
/// #[source]
/// source: std::io::Error,
/// },
/// }
///
/// impl Traceable for MyError {
/// fn trace_format(&self) -> String {
/// match self {
/// Self::OperationFailed { reason, .. } => {
/// format!("MyError: Operation failed - {}", reason)
/// }
/// }
/// }
///
/// fn trace_source(&self) -> Option<&dyn Traceable> {
/// match self {
/// Self::OperationFailed { source, .. } => {
/// // Would return Some if source implemented Traceable
/// None
/// }
/// }
/// }
///
/// fn error_kind(&self) -> ErrorKind {
/// match self {
/// Self::OperationFailed { .. } => ErrorKind::FileSystem,
/// }
/// }
/// }
/// ```
pub trait Traceable: std::error::Error {
/// Generate a formatted trace entry for this error
///
/// This method should return a human-readable string describing the error
/// with relevant context. It will be used to build the error chain in
/// trace files.
///
/// # Returns
///
/// A formatted string representing this error in the trace
fn trace_format(&self) -> String;
/// Get the underlying source error that implements Traceable, if any
///
/// This method enables walking the error chain to capture complete
/// error information in trace files. Return `Some` if the source error
/// implements `Traceable`, `None` otherwise.
///
/// # Returns
///
/// An optional reference to the source error as a `Traceable` trait object
fn trace_source(&self) -> Option<&dyn Traceable>;
/// Get the error kind for high-level categorization
///
/// Returns a high-level category for this error, used in trace files
/// and failure context for debugging and potential recovery strategies.
///
/// Error kinds provide an easy way to understand what type of error
/// occurred without parsing detailed trace files. They serve as a
/// high-level summary that can be:
///
/// - Displayed to users without technical details
/// - Used for filtering/grouping errors
/// - Foundation for future retry/recovery strategies based on error category
///
/// # Returns
///
/// An `ErrorKind` variant representing this error's category
///
/// # Example
///
/// ```rust
/// use torrust_tracker_deployer_lib::shared::{Traceable, ErrorKind};
///
/// fn handle_error<E: Traceable>(error: &E) {
/// let kind = error.error_kind();
/// println!("Error category: {:?}", kind);
/// }
/// ```
fn error_kind(&self) -> ErrorKind;
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Debug, thiserror::Error)]
enum TestError {
#[error("Root error: {message}")]
Root { message: String },
#[error("Wrapped error: {context}")]
Wrapped {
context: String,
#[source]
source: Box<TestError>,
},
}
impl Traceable for TestError {
fn trace_format(&self) -> String {
match self {
Self::Root { message } => format!("TestError::Root - {message}"),
Self::Wrapped { context, .. } => format!("TestError::Wrapped - {context}"),
}
}
fn trace_source(&self) -> Option<&dyn Traceable> {
match self {
Self::Root { .. } => None,
Self::Wrapped { source, .. } => Some(source.as_ref()),
}
}
fn error_kind(&self) -> ErrorKind {
// Test errors are categorized as general errors
ErrorKind::CommandExecution
}
}
#[test]
fn it_should_format_root_error() {
let error = TestError::Root {
message: "test message".to_string(),
};
assert_eq!(error.trace_format(), "TestError::Root - test message");
}
#[test]
fn it_should_format_wrapped_error() {
let root = TestError::Root {
message: "root cause".to_string(),
};
let wrapped = TestError::Wrapped {
context: "additional context".to_string(),
source: Box::new(root),
};
assert_eq!(
wrapped.trace_format(),
"TestError::Wrapped - additional context"
);
}
#[test]
fn it_should_provide_source_for_wrapped_error() {
let root = TestError::Root {
message: "root cause".to_string(),
};
let wrapped = TestError::Wrapped {
context: "context".to_string(),
source: Box::new(root),
};
assert!(wrapped.trace_source().is_some());
}
#[test]
fn it_should_not_provide_source_for_root_error() {
let error = TestError::Root {
message: "test".to_string(),
};
assert!(error.trace_source().is_none());
}
}