1use std::error::Error as StdError;
6use std::fmt;
7
8pub trait ErrorContextProvider {
10 fn file(&self) -> Option<&str>;
11 fn row(&self) -> Option<usize>;
12 fn column(&self) -> Option<usize>;
13 fn cell_ref(&self) -> Option<&str>;
14 fn column_name(&self) -> Option<&str>;
15}
16
17pub trait UserFriendlyError {
19 fn user_message(&self) -> String;
20 fn suggestion(&self) -> Option<String>;
21}
22
23pub trait RecoverableError {
25 fn can_recover(&self) -> bool;
26 fn recovery_action(&self) -> Option<String>;
27}
28
29pub trait ErrorCategory {
31 fn category(&self) -> ErrorCategoryType;
32 fn severity(&self) -> ErrorSeverity;
33}
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37pub enum ErrorCategoryType {
38 Validation,
39 IO,
40 Format,
41 Type,
42 Calculation,
43 Configuration,
44 Network,
45 Other,
46}
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
50pub enum ErrorSeverity {
51 Info,
52 Warning,
53 Error,
54 Critical,
55}
56
57#[derive(Debug)]
59pub struct TraitBasedError {
60 pub message: String,
61 pub category: ErrorCategoryType,
62 pub severity: ErrorSeverity,
63 pub context: ErrorContext,
64 pub suggestion: Option<String>,
65 pub recovery: Option<String>,
66}
67
68#[derive(Debug, Clone, Default)]
69pub struct ErrorContext {
70 pub file: Option<String>,
71 pub row: Option<usize>,
72 pub column: Option<usize>,
73 pub cell_ref: Option<String>,
74 pub column_name: Option<String>,
75}
76
77impl ErrorContextProvider for TraitBasedError {
78 fn file(&self) -> Option<&str> {
79 self.context.file.as_deref()
80 }
81
82 fn row(&self) -> Option<usize> {
83 self.context.row
84 }
85
86 fn column(&self) -> Option<usize> {
87 self.context.column
88 }
89
90 fn cell_ref(&self) -> Option<&str> {
91 self.context.cell_ref.as_deref()
92 }
93
94 fn column_name(&self) -> Option<&str> {
95 self.context.column_name.as_deref()
96 }
97}
98
99impl UserFriendlyError for TraitBasedError {
100 fn user_message(&self) -> String {
101 let mut msg = self.message.clone();
102
103 if let Some(file) = &self.context.file {
104 msg.push_str(&format!(" (file: {})", file));
105 }
106
107 if let Some(row) = self.context.row {
108 msg.push_str(&format!(" (row: {})", row + 1));
109 }
110
111 if let Some(col) = self.context.column {
112 msg.push_str(&format!(" (column: {})", col + 1));
113 }
114
115 msg
116 }
117
118 fn suggestion(&self) -> Option<String> {
119 self.suggestion.clone()
120 }
121}
122
123impl RecoverableError for TraitBasedError {
124 fn can_recover(&self) -> bool {
125 self.recovery.is_some()
126 }
127
128 fn recovery_action(&self) -> Option<String> {
129 self.recovery.clone()
130 }
131}
132
133impl ErrorCategory for TraitBasedError {
134 fn category(&self) -> ErrorCategoryType {
135 self.category
136 }
137
138 fn severity(&self) -> ErrorSeverity {
139 self.severity
140 }
141}
142
143impl fmt::Display for TraitBasedError {
144 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
145 write!(f, "{}", self.user_message())
146 }
147}
148
149impl StdError for TraitBasedError {}
150
151impl TraitBasedError {
152 pub fn new(message: String, category: ErrorCategoryType, severity: ErrorSeverity) -> Self {
153 Self {
154 message,
155 category,
156 severity,
157 context: ErrorContext::default(),
158 suggestion: None,
159 recovery: None,
160 }
161 }
162
163 pub fn with_context(mut self, context: ErrorContext) -> Self {
164 self.context = context;
165 self
166 }
167
168 pub fn with_suggestion(mut self, suggestion: String) -> Self {
169 self.suggestion = Some(suggestion);
170 self
171 }
172
173 pub fn with_recovery(mut self, recovery: String) -> Self {
174 self.recovery = Some(recovery);
175 self
176 }
177}
178
179pub trait ToTraitBasedError {
181 fn to_trait_error(
182 self,
183 category: ErrorCategoryType,
184 severity: ErrorSeverity,
185 ) -> TraitBasedError;
186}
187
188impl ToTraitBasedError for anyhow::Error {
189 fn to_trait_error(
190 self,
191 category: ErrorCategoryType,
192 severity: ErrorSeverity,
193 ) -> TraitBasedError {
194 TraitBasedError::new(self.to_string(), category, severity)
195 }
196}