Skip to main content

headless_lms_chatbot/
chatbot_error.rs

1/*!
2Contains error and result types for all the chatbot functions.
3*/
4
5use std::fmt::Display;
6
7use backtrace::Backtrace;
8use headless_lms_models::ModelError;
9use tracing_error::SpanTrace;
10
11use headless_lms_base::error::backend_error::BackendError;
12
13/**
14Used as the result types for all of chatbot.
15*/
16pub type ChatbotResult<T> = Result<T, ChatbotError>;
17
18/// The type of [ChatbotError] that occured.
19#[derive(Debug)]
20pub enum ChatbotErrorType {
21    InvalidMessageShape,
22    InvalidToolName,
23    InvalidToolArguments,
24    ChatbotModelError,
25    ChatbotMessageSuggestError,
26    UrlParse,
27    TokioIo,
28    SerdeJson,
29    Other,
30    DeserializationError,
31}
32
33/**
34Error type used in [ChatbotError], which is used for errors related to chatbot functionality.
35
36All the information in the error is meant to be seen by the user. The type of error is determined by the [ChatbotErrorType] enum, which is stored inside this struct.
37
38## Examples
39
40### Usage without source error
41
42```no_run
43# use headless_lms_chatbot::prelude::*;
44# fn random_function() -> ChatbotResult<()> {
45#    let erroneous_condition = 1 == 1;
46if erroneous_condition {
47    return Err(ChatbotError::new(
48        ChatbotErrorType::Other,
49        "File not found".to_string(),
50        None,
51    ));
52}
53# Ok(())
54# }
55```
56
57### Usage with a source error
58
59Used when calling a function that returns an error that cannot be automatically converted to an ChatbotError. (See `impl From<X>` implementations on this struct.)
60
61```no_run
62# use headless_lms_chatbot::prelude::*;
63# fn some_function_returning_an_error() -> ChatbotResult<()> {
64#    return Err(ChatbotError::new(
65#        ChatbotErrorType::Other,
66#        "File not found".to_string(),
67#        None,
68#    ));
69# }
70#
71# fn random_function() -> ChatbotResult<()> {
72#    let erroneous_condition = 1 == 1;
73some_function_returning_an_error().map_err(|original_error| {
74    ChatbotError::new(
75        ChatbotErrorType::Other,
76        "Library x failed to do y".to_string(),
77        Some(original_error.into()),
78    )
79})?;
80# Ok(())
81# }
82```
83*/
84#[derive(Debug)]
85pub struct ChatbotError {
86    error_type: <ChatbotError as BackendError>::ErrorType,
87    message: String,
88    /// Original error that caused this error.
89    source: Option<anyhow::Error>,
90    /// A trace of tokio tracing spans, generated automatically when the error is generated.
91    span_trace: Box<SpanTrace>,
92    /// Stack trace, generated automatically when the error is created.
93    backtrace: Box<Backtrace>,
94}
95
96impl std::error::Error for ChatbotError {
97    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
98        self.source.as_ref().and_then(|o| o.source())
99    }
100
101    fn cause(&self) -> Option<&dyn std::error::Error> {
102        self.source()
103    }
104}
105
106impl Display for ChatbotError {
107    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
108        write!(f, "ChatbotError {:?} {:?}", self.error_type, self.message)
109    }
110}
111
112impl BackendError for ChatbotError {
113    type ErrorType = ChatbotErrorType;
114
115    fn new<M: Into<String>, S: Into<Option<anyhow::Error>>>(
116        error_type: Self::ErrorType,
117        message: M,
118        source_error: S,
119    ) -> Self {
120        Self::new_with_traces(
121            error_type,
122            message,
123            source_error,
124            Backtrace::new(),
125            SpanTrace::capture(),
126        )
127    }
128
129    fn backtrace(&self) -> Option<&Backtrace> {
130        Some(&self.backtrace)
131    }
132
133    fn error_type(&self) -> &Self::ErrorType {
134        &self.error_type
135    }
136
137    fn message(&self) -> &str {
138        &self.message
139    }
140
141    fn span_trace(&self) -> &SpanTrace {
142        &self.span_trace
143    }
144
145    fn new_with_traces<M: Into<String>, S: Into<Option<anyhow::Error>>>(
146        error_type: Self::ErrorType,
147        message: M,
148        source_error: S,
149        backtrace: Backtrace,
150        span_trace: SpanTrace,
151    ) -> Self {
152        Self {
153            error_type,
154            message: message.into(),
155            source: source_error.into(),
156            span_trace: Box::new(span_trace),
157            backtrace: Box::new(backtrace),
158        }
159    }
160}
161
162impl From<url::ParseError> for ChatbotError {
163    fn from(source: url::ParseError) -> Self {
164        ChatbotError::new(
165            ChatbotErrorType::UrlParse,
166            source.to_string(),
167            Some(source.into()),
168        )
169    }
170}
171
172impl From<tokio::io::Error> for ChatbotError {
173    fn from(source: tokio::io::Error) -> Self {
174        ChatbotError::new(
175            ChatbotErrorType::TokioIo,
176            source.to_string(),
177            Some(source.into()),
178        )
179    }
180}
181
182impl From<serde_json::Error> for ChatbotError {
183    fn from(source: serde_json::Error) -> Self {
184        ChatbotError::new(
185            ChatbotErrorType::SerdeJson,
186            source.to_string(),
187            Some(source.into()),
188        )
189    }
190}
191
192impl From<anyhow::Error> for ChatbotError {
193    fn from(err: anyhow::Error) -> ChatbotError {
194        Self::new(ChatbotErrorType::Other, err.to_string(), Some(err))
195    }
196}
197
198impl From<ModelError> for ChatbotError {
199    fn from(err: ModelError) -> ChatbotError {
200        Self::new(
201            ChatbotErrorType::ChatbotModelError,
202            err.to_string(),
203            Some(err.into()),
204        )
205    }
206}
207
208// Generate error creation macros for ChatbotError
209headless_lms_utils::define_err_macro!(
210    chatbot_err,
211    ChatbotError,
212    ChatbotErrorType,
213    ChatbotErrorType,
214    "Create a ChatbotError with less boilerplate."
215);
216
217/// Helper function for `.map_err()` chains to wrap any error as ChatbotError.
218///
219/// This function creates a closure that converts any error into a `ChatbotError`
220/// with the specified error type and message, including the original error as the source.
221///
222/// # Examples
223///
224/// ```ignore
225/// // Instead of:
226/// .map_err(|e| ChatbotError::new(ChatbotErrorType::Other, e.to_string(), Some(e.into())))?
227///
228/// // You can write:
229/// .map_err(as_chatbot_error(ChatbotErrorType::Other, "Failed to process".to_string()))?
230/// ```
231pub fn as_chatbot_error<E>(
232    error_type: ChatbotErrorType,
233    message: impl Into<String>,
234) -> impl FnOnce(E) -> ChatbotError
235where
236    E: Into<anyhow::Error>,
237{
238    let msg = message.into();
239    move |e| ChatbotError::new(error_type, msg, Some(e.into()))
240}
241
242/// Helper function for `.ok_or_else()` to create ChatbotError on None.
243///
244/// This function creates a closure that generates a `ChatbotError` with the
245/// specified error type and message when called.
246///
247/// # Examples
248///
249/// ```ignore
250/// // Instead of:
251/// .ok_or_else(|| ChatbotError::new(ChatbotErrorType::Other, "Item not found".to_string(), None))
252///
253/// // You can write:
254/// .ok_or_else(missing_chatbot_error(ChatbotErrorType::Other, "Item not found".to_string()))
255/// ```
256pub fn missing_chatbot_error(
257    error_type: ChatbotErrorType,
258    message: impl Into<String>,
259) -> impl FnOnce() -> ChatbotError {
260    let msg = message.into();
261    move || ChatbotError::new(error_type, msg, None)
262}
263
264#[cfg(test)]
265mod tests {
266    use super::*;
267
268    #[test]
269    fn test_chatbot_err_macro_without_source() {
270        let err = chatbot_err!(Other, "Test error message".to_string());
271        assert_eq!(err.message(), "Test error message");
272        assert!(matches!(err.error_type(), ChatbotErrorType::Other));
273    }
274
275    #[test]
276    fn test_chatbot_err_macro_with_source() {
277        let source_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
278        let err = chatbot_err!(TokioIo, "Wrapped error".to_string(), source_err);
279        assert_eq!(err.message(), "Wrapped error");
280    }
281
282    #[test]
283    fn test_as_chatbot_error_helper() {
284        let result: Result<(), std::io::Error> = Err(std::io::Error::new(
285            std::io::ErrorKind::NotFound,
286            "test error",
287        ));
288        let chatbot_result = result.map_err(as_chatbot_error(
289            ChatbotErrorType::Other,
290            "Failed to process".to_string(),
291        ));
292
293        assert!(chatbot_result.is_err());
294        let err = chatbot_result.unwrap_err();
295        assert_eq!(err.message(), "Failed to process");
296        assert!(matches!(err.error_type(), ChatbotErrorType::Other));
297    }
298
299    #[test]
300    fn test_missing_chatbot_error_helper() {
301        let option: Option<String> = None;
302        let result = option.ok_or_else(missing_chatbot_error(
303            ChatbotErrorType::InvalidMessageShape,
304            "Message not found".to_string(),
305        ));
306
307        assert!(result.is_err());
308        let err = result.unwrap_err();
309        assert_eq!(err.message(), "Message not found");
310        assert!(matches!(
311            err.error_type(),
312            ChatbotErrorType::InvalidMessageShape
313        ));
314    }
315
316    #[test]
317    fn test_chatbot_err_with_format() {
318        let tool_name = "test_tool";
319        let err = chatbot_err!(InvalidToolName, format!("Unknown tool: {}", tool_name));
320        assert_eq!(err.message(), "Unknown tool: test_tool");
321    }
322}