headless_lms_utils/error/
util_error.rs1use std::fmt::Display;
6
7use backtrace::Backtrace;
8use headless_lms_base::error::backend_error::BackendError;
9use tracing_error::SpanTrace;
10
11pub type UtilResult<T> = Result<T, UtilError>;
17
18#[derive(Debug)]
20pub enum UtilErrorType {
21 UrlParse,
22 Walkdir,
23 StripPrefix,
24 TokioIo,
25 SerdeJson,
26 CloudStorage,
27 Other,
28 Unavailable,
29 DeserializationError,
30 TmcHttpError,
31 TmcErrorResponse,
32}
33
34#[derive(Debug)]
86pub struct UtilError {
87 error_type: <UtilError as BackendError>::ErrorType,
88 message: String,
89 source: Option<anyhow::Error>,
91 span_trace: Box<SpanTrace>,
93 backtrace: Box<Backtrace>,
95}
96
97impl std::error::Error for UtilError {
98 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
99 self.source.as_ref().and_then(|o| o.source())
100 }
101
102 fn cause(&self) -> Option<&dyn std::error::Error> {
103 self.source()
104 }
105}
106
107impl Display for UtilError {
108 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
109 write!(f, "UtilError {:?} {:?}", self.error_type, self.message)
110 }
111}
112
113impl BackendError for UtilError {
114 type ErrorType = UtilErrorType;
115
116 fn new<M: Into<String>, S: Into<Option<anyhow::Error>>>(
117 error_type: Self::ErrorType,
118 message: M,
119 source_error: S,
120 ) -> Self {
121 Self::new_with_traces(
122 error_type,
123 message,
124 source_error,
125 Backtrace::new(),
126 SpanTrace::capture(),
127 )
128 }
129
130 fn backtrace(&self) -> Option<&Backtrace> {
131 Some(&self.backtrace)
132 }
133
134 fn error_type(&self) -> &Self::ErrorType {
135 &self.error_type
136 }
137
138 fn message(&self) -> &str {
139 &self.message
140 }
141
142 fn span_trace(&self) -> &SpanTrace {
143 &self.span_trace
144 }
145
146 fn new_with_traces<M: Into<String>, S: Into<Option<anyhow::Error>>>(
147 error_type: Self::ErrorType,
148 message: M,
149 source_error: S,
150 backtrace: Backtrace,
151 span_trace: SpanTrace,
152 ) -> Self {
153 Self {
154 error_type,
155 message: message.into(),
156 source: source_error.into(),
157 span_trace: Box::new(span_trace),
158 backtrace: Box::new(backtrace),
159 }
160 }
161}
162
163impl From<url::ParseError> for UtilError {
164 fn from(source: url::ParseError) -> Self {
165 UtilError::new(
166 UtilErrorType::UrlParse,
167 source.to_string(),
168 Some(source.into()),
169 )
170 }
171}
172
173impl From<walkdir::Error> for UtilError {
174 fn from(source: walkdir::Error) -> Self {
175 UtilError::new(
176 UtilErrorType::Walkdir,
177 source.to_string(),
178 Some(source.into()),
179 )
180 }
181}
182
183impl From<std::path::StripPrefixError> for UtilError {
184 fn from(source: std::path::StripPrefixError) -> Self {
185 UtilError::new(
186 UtilErrorType::StripPrefix,
187 source.to_string(),
188 Some(source.into()),
189 )
190 }
191}
192
193impl From<tokio::io::Error> for UtilError {
194 fn from(source: tokio::io::Error) -> Self {
195 UtilError::new(
196 UtilErrorType::TokioIo,
197 source.to_string(),
198 Some(source.into()),
199 )
200 }
201}
202
203impl From<serde_json::Error> for UtilError {
204 fn from(source: serde_json::Error) -> Self {
205 UtilError::new(
206 UtilErrorType::SerdeJson,
207 source.to_string(),
208 Some(source.into()),
209 )
210 }
211}
212
213impl From<google_cloud_storage::Error> for UtilError {
214 fn from(source: google_cloud_storage::Error) -> Self {
215 UtilError::new(
216 UtilErrorType::CloudStorage,
217 source.to_string(),
218 Some(source.into()),
219 )
220 }
221}
222
223impl From<anyhow::Error> for UtilError {
224 fn from(err: anyhow::Error) -> UtilError {
225 Self::new(UtilErrorType::Other, err.to_string(), Some(err))
226 }
227}
228
229crate::define_err_macro!(
231 util_err,
232 UtilError,
233 UtilErrorType,
234 UtilErrorType,
235 "Create a UtilError with less boilerplate."
236);
237
238pub fn as_util_error<E>(
253 error_type: UtilErrorType,
254 message: impl Into<String>,
255) -> impl FnOnce(E) -> UtilError
256where
257 E: Into<anyhow::Error>,
258{
259 let msg = message.into();
260 move |e| UtilError::new(error_type, msg, Some(e.into()))
261}
262
263pub fn missing_util_error(
278 error_type: UtilErrorType,
279 message: impl Into<String>,
280) -> impl FnOnce() -> UtilError {
281 let msg = message.into();
282 move || UtilError::new(error_type, msg, None)
283}
284
285#[cfg(test)]
286mod tests {
287 use super::*;
288
289 #[test]
290 fn test_util_err_macro_without_source() {
291 let err = util_err!(Other, "Test error message".to_string());
292 assert_eq!(err.message(), "Test error message");
293 assert!(matches!(err.error_type(), UtilErrorType::Other));
294 }
295
296 #[test]
297 fn test_util_err_macro_with_source() {
298 let source_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
299 let err = util_err!(TokioIo, "Wrapped error".to_string(), source_err);
300 assert_eq!(err.message(), "Wrapped error");
301 }
302
303 #[test]
304 fn test_as_util_error_helper() {
305 let result: Result<(), std::io::Error> = Err(std::io::Error::new(
306 std::io::ErrorKind::NotFound,
307 "test error",
308 ));
309 let util_result = result.map_err(as_util_error(
310 UtilErrorType::TokioIo,
311 "Failed to read file".to_string(),
312 ));
313
314 assert!(util_result.is_err());
315 let err = util_result.unwrap_err();
316 assert_eq!(err.message(), "Failed to read file");
317 assert!(matches!(err.error_type(), UtilErrorType::TokioIo));
318 }
319
320 #[test]
321 fn test_missing_util_error_helper() {
322 let option: Option<String> = None;
323 let result = option.ok_or_else(missing_util_error(
324 UtilErrorType::Other,
325 "Item not found".to_string(),
326 ));
327
328 assert!(result.is_err());
329 let err = result.unwrap_err();
330 assert_eq!(err.message(), "Item not found");
331 assert!(matches!(err.error_type(), UtilErrorType::Other));
332 }
333
334 #[test]
335 fn test_util_err_with_format() {
336 let path = "/tmp/test.txt";
337 let err = util_err!(Other, format!("Failed to process file: {}", path));
338 assert_eq!(err.message(), "Failed to process file: /tmp/test.txt");
339 }
340}