headless_lms_utils/error/
util_error.rs1use std::fmt::Display;
6use std::panic::Location;
7
8use backtrace::Backtrace;
9use headless_lms_base::error::backend_error::BackendError;
10use tracing_error::SpanTrace;
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 SisuClientError(SisuErrorVariant),
33}
34#[derive(Debug)]
35
36pub enum SisuErrorVariant {
37 GenericSisuError,
38 InvalidCourseCode,
39 SisuResourceNotFound,
40}
41
42pub struct UtilError {
94 error_type: <UtilError as BackendError>::ErrorType,
95 message: String,
96 source: Option<anyhow::Error>,
98 span_trace: Box<SpanTrace>,
100 backtrace: Box<Backtrace>,
102 location: Option<&'static Location<'static>>,
104}
105
106impl std::error::Error for UtilError {
107 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
108 self.source
109 .as_deref()
110 .map(|e| e as &(dyn std::error::Error + 'static))
111 }
112
113 fn cause(&self) -> Option<&dyn std::error::Error> {
114 self.source()
115 }
116}
117
118headless_lms_base::impl_clean_debug!(UtilError, [UtilError]);
120
121impl Display for UtilError {
122 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
123 write!(f, "UtilError {:?} {:?}", self.error_type, self.message)
124 }
125}
126
127impl BackendError for UtilError {
128 type ErrorType = UtilErrorType;
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 location(&self) -> Option<&'static Location<'static>> {
147 self.location
148 }
149
150 fn new_with_traces_and_location<M: Into<String>, S: Into<Option<anyhow::Error>>>(
151 error_type: Self::ErrorType,
152 message: M,
153 source_error: S,
154 backtrace: Backtrace,
155 span_trace: SpanTrace,
156 location: Option<&'static Location<'static>>,
157 ) -> Self {
158 Self {
159 error_type,
160 message: message.into(),
161 source: source_error.into(),
162 span_trace: Box::new(span_trace),
163 backtrace: Box::new(backtrace),
164 location,
165 }
166 }
167}
168
169impl From<url::ParseError> for UtilError {
170 fn from(source: url::ParseError) -> Self {
171 UtilError::new(
172 UtilErrorType::UrlParse,
173 source.to_string(),
174 Some(source.into()),
175 )
176 }
177}
178
179impl From<walkdir::Error> for UtilError {
180 fn from(source: walkdir::Error) -> Self {
181 UtilError::new(
182 UtilErrorType::Walkdir,
183 source.to_string(),
184 Some(source.into()),
185 )
186 }
187}
188
189impl From<std::path::StripPrefixError> for UtilError {
190 fn from(source: std::path::StripPrefixError) -> Self {
191 UtilError::new(
192 UtilErrorType::StripPrefix,
193 source.to_string(),
194 Some(source.into()),
195 )
196 }
197}
198
199impl From<tokio::io::Error> for UtilError {
200 fn from(source: tokio::io::Error) -> Self {
201 UtilError::new(
202 UtilErrorType::TokioIo,
203 source.to_string(),
204 Some(source.into()),
205 )
206 }
207}
208
209impl From<serde_json::Error> for UtilError {
210 fn from(source: serde_json::Error) -> Self {
211 UtilError::new(
212 UtilErrorType::SerdeJson,
213 source.to_string(),
214 Some(source.into()),
215 )
216 }
217}
218
219impl From<google_cloud_storage::Error> for UtilError {
220 fn from(source: google_cloud_storage::Error) -> Self {
221 UtilError::new(
222 UtilErrorType::CloudStorage,
223 source.to_string(),
224 Some(source.into()),
225 )
226 }
227}
228
229impl From<anyhow::Error> for UtilError {
230 fn from(err: anyhow::Error) -> UtilError {
231 Self::new(UtilErrorType::Other, err.to_string(), Some(err))
232 }
233}
234
235crate::define_err_macro!(
237 util_err,
238 UtilError,
239 UtilErrorType,
240 UtilErrorType,
241 "Create a UtilError with less boilerplate."
242);
243
244pub fn as_util_error<E>(
259 error_type: UtilErrorType,
260 message: impl Into<String>,
261) -> impl FnOnce(E) -> UtilError
262where
263 E: Into<anyhow::Error>,
264{
265 let msg = message.into();
266 move |e| UtilError::new(error_type, msg, Some(e.into()))
267}
268
269pub fn missing_util_error(
284 error_type: UtilErrorType,
285 message: impl Into<String>,
286) -> impl FnOnce() -> UtilError {
287 let msg = message.into();
288 move || UtilError::new(error_type, msg, None)
289}
290
291#[cfg(test)]
292mod tests {
293 use super::*;
294
295 #[test]
296 fn test_util_err_macro_without_source() {
297 let err = util_err!(Other, "Test error message".to_string());
298 assert_eq!(err.message(), "Test error message");
299 assert!(matches!(err.error_type(), UtilErrorType::Other));
300 }
301
302 #[test]
303 fn test_util_err_macro_with_source() {
304 let source_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
305 let err = util_err!(TokioIo, "Wrapped error".to_string(), source_err);
306 assert_eq!(err.message(), "Wrapped error");
307 }
308
309 #[test]
310 fn test_as_util_error_helper() {
311 let result: Result<(), std::io::Error> = Err(std::io::Error::new(
312 std::io::ErrorKind::NotFound,
313 "test error",
314 ));
315 let util_result = result.map_err(as_util_error(
316 UtilErrorType::TokioIo,
317 "Failed to read file".to_string(),
318 ));
319
320 assert!(util_result.is_err());
321 let err = util_result.unwrap_err();
322 assert_eq!(err.message(), "Failed to read file");
323 assert!(matches!(err.error_type(), UtilErrorType::TokioIo));
324 }
325
326 #[test]
327 fn test_missing_util_error_helper() {
328 let option: Option<String> = None;
329 let result = option.ok_or_else(missing_util_error(
330 UtilErrorType::Other,
331 "Item not found".to_string(),
332 ));
333
334 assert!(result.is_err());
335 let err = result.unwrap_err();
336 assert_eq!(err.message(), "Item not found");
337 assert!(matches!(err.error_type(), UtilErrorType::Other));
338 }
339
340 #[test]
341 fn test_util_err_with_format() {
342 let path = "/tmp/test.txt";
343 let err = util_err!(Other, format!("Failed to process file: {}", path));
344 assert_eq!(err.message(), "Failed to process file: /tmp/test.txt");
345 }
346
347 #[test]
350 fn err_macro_captures_the_real_call_site() {
351 let expected_line = line!() + 1;
352 let err = util_err!(Other, "boom".to_string());
353 let location = err.location().expect("location should be captured");
354 assert_eq!(location.line(), expected_line, "file: {}", location.file());
355 assert!(
356 location.file().ends_with("util_error.rs"),
357 "expected the call site, got: {}",
358 location.file()
359 );
360 assert!(
361 !location.file().contains("macros.rs"),
362 "got: {}",
363 location.file()
364 );
365 }
366
367 #[test]
369 fn debug_uses_clean_format() {
370 let err = util_err!(Other, "boom".to_string());
371 let debug = format!("{err:?}");
372 assert!(debug.contains("UtilError ยท Other: boom"), "got: {debug}");
373 assert!(!debug.contains("backend_error.rs"), "got: {debug}");
374 }
375}