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,
33 TmcHttpStatusError(u16),
36 TmcErrorResponse,
37 EmbeddingRequestBuildError,
38 ReqwestError,
39 SisuClientError(SisuErrorVariant),
40 SuotarClientError,
42}
43#[derive(Debug)]
44
45pub enum SisuErrorVariant {
46 GenericSisuError,
47 InvalidCourseCode,
48 SisuResourceNotFound,
49}
50
51pub struct UtilError {
103 error_type: <UtilError as BackendError>::ErrorType,
104 message: String,
105 source: Option<anyhow::Error>,
107 span_trace: Box<SpanTrace>,
109 backtrace: Box<Backtrace>,
111 location: Option<&'static Location<'static>>,
113}
114
115impl std::error::Error for UtilError {
116 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
117 self.source
118 .as_deref()
119 .map(|e| e as &(dyn std::error::Error + 'static))
120 }
121
122 fn cause(&self) -> Option<&dyn std::error::Error> {
123 self.source()
124 }
125}
126
127headless_lms_base::impl_clean_debug!(UtilError, [UtilError]);
129
130impl Display for UtilError {
131 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
132 write!(f, "UtilError {:?} {:?}", self.error_type, self.message)
133 }
134}
135
136impl BackendError for UtilError {
137 type ErrorType = UtilErrorType;
138
139 fn backtrace(&self) -> Option<&Backtrace> {
140 Some(&self.backtrace)
141 }
142
143 fn error_type(&self) -> &Self::ErrorType {
144 &self.error_type
145 }
146
147 fn message(&self) -> &str {
148 &self.message
149 }
150
151 fn span_trace(&self) -> &SpanTrace {
152 &self.span_trace
153 }
154
155 fn location(&self) -> Option<&'static Location<'static>> {
156 self.location
157 }
158
159 fn new_with_traces_and_location<M: Into<String>, S: Into<Option<anyhow::Error>>>(
160 error_type: Self::ErrorType,
161 message: M,
162 source_error: S,
163 backtrace: Backtrace,
164 span_trace: SpanTrace,
165 location: Option<&'static Location<'static>>,
166 ) -> Self {
167 Self {
168 error_type,
169 message: message.into(),
170 source: source_error.into(),
171 span_trace: Box::new(span_trace),
172 backtrace: Box::new(backtrace),
173 location,
174 }
175 }
176}
177
178impl From<url::ParseError> for UtilError {
179 fn from(source: url::ParseError) -> Self {
180 UtilError::new(
181 UtilErrorType::UrlParse,
182 source.to_string(),
183 Some(source.into()),
184 )
185 }
186}
187
188impl From<walkdir::Error> for UtilError {
189 fn from(source: walkdir::Error) -> Self {
190 UtilError::new(
191 UtilErrorType::Walkdir,
192 source.to_string(),
193 Some(source.into()),
194 )
195 }
196}
197
198impl From<reqwest::Error> for UtilError {
199 fn from(err: reqwest::Error) -> UtilError {
200 Self::new(
201 UtilErrorType::ReqwestError,
202 err.to_string(),
203 Some(err.into()),
204 )
205 }
206}
207
208impl From<std::path::StripPrefixError> for UtilError {
209 fn from(source: std::path::StripPrefixError) -> Self {
210 UtilError::new(
211 UtilErrorType::StripPrefix,
212 source.to_string(),
213 Some(source.into()),
214 )
215 }
216}
217
218impl From<tokio::io::Error> for UtilError {
219 fn from(source: tokio::io::Error) -> Self {
220 UtilError::new(
221 UtilErrorType::TokioIo,
222 source.to_string(),
223 Some(source.into()),
224 )
225 }
226}
227
228impl From<serde_json::Error> for UtilError {
229 fn from(source: serde_json::Error) -> Self {
230 UtilError::new(
231 UtilErrorType::SerdeJson,
232 source.to_string(),
233 Some(source.into()),
234 )
235 }
236}
237
238impl From<google_cloud_storage::Error> for UtilError {
239 fn from(source: google_cloud_storage::Error) -> Self {
240 UtilError::new(
241 UtilErrorType::CloudStorage,
242 source.to_string(),
243 Some(source.into()),
244 )
245 }
246}
247
248impl From<anyhow::Error> for UtilError {
249 fn from(err: anyhow::Error) -> UtilError {
250 Self::new(UtilErrorType::Other, err.to_string(), Some(err))
251 }
252}
253
254crate::define_err_macro!(
256 util_err,
257 UtilError,
258 UtilErrorType,
259 UtilErrorType,
260 "Create a UtilError with less boilerplate."
261);
262
263pub fn as_util_error<E>(
278 error_type: UtilErrorType,
279 message: impl Into<String>,
280) -> impl FnOnce(E) -> UtilError
281where
282 E: Into<anyhow::Error>,
283{
284 let msg = message.into();
285 move |e| UtilError::new(error_type, msg, Some(e.into()))
286}
287
288pub fn missing_util_error(
303 error_type: UtilErrorType,
304 message: impl Into<String>,
305) -> impl FnOnce() -> UtilError {
306 let msg = message.into();
307 move || UtilError::new(error_type, msg, None)
308}
309
310#[cfg(test)]
311mod tests {
312 use super::*;
313
314 #[test]
315 fn test_util_err_macro_without_source() {
316 let err = util_err!(Other, "Test error message".to_string());
317 assert_eq!(err.message(), "Test error message");
318 assert!(matches!(err.error_type(), UtilErrorType::Other));
319 }
320
321 #[test]
322 fn test_util_err_macro_with_source() {
323 let source_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
324 let err = util_err!(TokioIo, "Wrapped error".to_string(), source_err);
325 assert_eq!(err.message(), "Wrapped error");
326 }
327
328 #[test]
329 fn test_as_util_error_helper() {
330 let result: Result<(), std::io::Error> = Err(std::io::Error::new(
331 std::io::ErrorKind::NotFound,
332 "test error",
333 ));
334 let util_result = result.map_err(as_util_error(
335 UtilErrorType::TokioIo,
336 "Failed to read file".to_string(),
337 ));
338
339 assert!(util_result.is_err());
340 let err = util_result.unwrap_err();
341 assert_eq!(err.message(), "Failed to read file");
342 assert!(matches!(err.error_type(), UtilErrorType::TokioIo));
343 }
344
345 #[test]
346 fn test_missing_util_error_helper() {
347 let option: Option<String> = None;
348 let result = option.ok_or_else(missing_util_error(
349 UtilErrorType::Other,
350 "Item not found".to_string(),
351 ));
352
353 assert!(result.is_err());
354 let err = result.unwrap_err();
355 assert_eq!(err.message(), "Item not found");
356 assert!(matches!(err.error_type(), UtilErrorType::Other));
357 }
358
359 #[test]
360 fn test_util_err_with_format() {
361 let path = "/tmp/test.txt";
362 let err = util_err!(Other, format!("Failed to process file: {}", path));
363 assert_eq!(err.message(), "Failed to process file: /tmp/test.txt");
364 }
365
366 #[test]
369 fn err_macro_captures_the_real_call_site() {
370 let expected_line = line!() + 1;
371 let err = util_err!(Other, "boom".to_string());
372 let location = err.location().expect("location should be captured");
373 assert_eq!(location.line(), expected_line, "file: {}", location.file());
374 assert!(
375 location.file().ends_with("util_error.rs"),
376 "expected the call site, got: {}",
377 location.file()
378 );
379 assert!(
380 !location.file().contains("macros.rs"),
381 "got: {}",
382 location.file()
383 );
384 }
385
386 #[test]
388 fn debug_uses_clean_format() {
389 let err = util_err!(Other, "boom".to_string());
390 let debug = format!("{err:?}");
391 assert!(debug.contains("UtilError ยท Other: boom"), "got: {debug}");
392 assert!(!debug.contains("backend_error.rs"), "got: {debug}");
393 }
394}