-
Notifications
You must be signed in to change notification settings - Fork 15
Expand file tree
/
Copy pathderive_more_interop.rs
More file actions
201 lines (176 loc) · 6.94 KB
/
derive_more_interop.rs
File metadata and controls
201 lines (176 loc) · 6.94 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
191
192
193
194
195
196
197
198
199
200
201
//! Using derive_more errors with rootcause.
//!
//! Three patterns for integrating derive_more-generated errors:
//!
//! 1. Type-nesting with `#[from]`: Traditional derive_more pattern
//! - One location captured per error
//! - Minimal migration from plain derive_more
//!
//! 2. Early Report creation: Return `Report<E>` from lower functions
//! - Locations captured closer to the error source
//! - Easier to capture multiple locations
//! - Use `.context_transform()` or `ReportConversion` trait
//!
//! 3. Flat enums with Report nesting: Categories with child Reports
//! - Multiple locations captured
//! - Flexible categorization via `.context()` or `ReportConversion`
use derive_more::{Display, Error, From};
use rootcause::{ReportConversion, markers, prelude::*};
// Shared error types used across patterns
#[derive(Error, Debug, Display)]
#[expect(dead_code, reason = "example code")]
enum DatabaseError {
#[display("Connection timeout after {seconds}s")]
ConnectionTimeout { seconds: u64 },
#[display("Query timeout after {seconds}s")]
QueryTimeout { seconds: u64 },
}
#[derive(Error, Debug, Display)]
#[expect(dead_code, reason = "example code")]
enum ConfigError {
#[display("Invalid format in {file}")]
InvalidFormat { file: String },
}
// ============================================================================
// Pattern 1: Type-nesting with #[from]
// ============================================================================
#[derive(Error, Debug, Display, From)]
enum AppError1 {
#[display("Database error")]
Database(DatabaseError),
}
fn query_plain(_id: u32) -> Result<String, DatabaseError> {
Err(DatabaseError::ConnectionTimeout { seconds: 30 })
}
// Only ONE location captured (here)
fn process_type_nested(user_id: u32) -> Result<String, Report<AppError1>> {
let data = query_plain(user_id).map_err(AppError1::from)?;
Ok(data)
}
// Pattern matching on typed reports enables selective handling
fn handle_typed_error(user_id: u32) -> Result<String, Report<AppError1>> {
match process_type_nested(user_id) {
Ok(data) => Ok(data),
Err(report) => {
// Access the typed error for decision-making
match report.current_context() {
AppError1::Database(DatabaseError::ConnectionTimeout { .. }) => {
println!(" → Detected connection timeout, could retry here");
Err(report)
}
AppError1::Database(DatabaseError::QueryTimeout { .. }) => {
println!(" → Query timeout, could adjust query parameters");
Err(report)
}
}
}
}
}
// ============================================================================
// Pattern 2: Early Report creation
// ============================================================================
#[derive(Error, Debug, Display, From)]
enum AppError2 {
#[display("Database error")]
Database(DatabaseError),
}
// Locations captured close to the error source
fn query_report(_id: u32) -> Result<String, Report<DatabaseError>> {
Err(report!(DatabaseError::ConnectionTimeout { seconds: 30 }))
}
// This still captures only ONE location per error, but it's close to the source
fn process_early_report(user_id: u32) -> Result<String, Report<AppError2>> {
let data = query_report(user_id).context_transform(AppError2::Database)?;
Ok(data)
}
// If we want to capture multiple locations, we can use context_transform_nested
// instead
fn process_early_report_multiple_locations(user_id: u32) -> Result<String, Report<AppError2>> {
let data = query_report(user_id).context_transform_nested(AppError2::Database)?;
Ok(data)
}
// Systematic conversion with ReportConversion trait
impl<T> ReportConversion<DatabaseError, markers::Mutable, T> for AppError2
where
AppError2: markers::ObjectMarkerFor<T>,
{
fn convert_report(
report: Report<DatabaseError, markers::Mutable, T>,
) -> Report<Self, markers::Mutable, T> {
report.context_transform(AppError2::Database)
}
}
fn process_with_conversion(user_id: u32) -> Result<String, Report<AppError2>> {
let data = query_report(user_id).context_to::<AppError2>()?;
Ok(data)
}
// ============================================================================
// Pattern 3: Flat enums with Report nesting (flexible categorization)
// ============================================================================
//
// Unlike Patterns 1-2 (which require 1-to-1 mapping with #[from]), Pattern 3
// lets you design categories independently of error type structure:
// - Split: one error type into multiple categories based on variants
// - Merge: multiple error types into one category
#[derive(Error, Debug, Display)]
enum AppError3 {
#[display("Database connection issue")]
DatabaseConnection,
#[display("Database operation failed")]
DatabaseOther,
#[display("System error")]
System,
}
// Split: one DatabaseError type into different categories
impl<T> ReportConversion<DatabaseError, markers::Mutable, T> for AppError3
where
AppError3: markers::ObjectMarkerFor<T>,
{
fn convert_report(
report: Report<DatabaseError, markers::Mutable, T>,
) -> Report<Self, markers::Mutable, T> {
match report.current_context() {
DatabaseError::ConnectionTimeout { .. } => report.context(Self::DatabaseConnection),
DatabaseError::QueryTimeout { .. } => report.context(Self::DatabaseOther),
}
}
}
// Merge: different error types into one category
impl<T> ReportConversion<ConfigError, markers::Mutable, T> for AppError3
where
AppError3: markers::ObjectMarkerFor<T>,
{
fn convert_report(
report: Report<ConfigError, markers::Mutable, T>,
) -> Report<Self, markers::Mutable, T> {
report.context(Self::System)
}
}
fn main() {
println!("\n## Pattern 1: Type-nesting with #[from]\n");
println!("Only one location captured and it's not at the source:");
if let Err(report) = process_type_nested(123) {
eprintln!("{report}\n");
}
println!("Pattern matching on typed reports:");
let _ = handle_typed_error(123);
println!();
println!("\n## Pattern 2: Early Report creation\n");
println!("Locations captured close to the error source:");
if let Err(report) = process_early_report(123) {
eprintln!("{report}\n");
}
println!("Multiple locations captured using context_transform_nested:");
if let Err(report) = process_early_report_multiple_locations(123) {
eprintln!("{report}\n");
}
println!("Using ReportConversion trait:");
if let Err(report) = process_with_conversion(123) {
eprintln!("{report}\n");
}
println!("\n## Pattern 3: Flexible categorization\n");
println!("The data type of AppError3 does not have to be 1-to-1 with the underlying errors:");
if let Err(report) = query_report(123).context_to::<AppError3>() {
eprintln!("{report}\n");
}
}