Skip to main content

malbox_plugin_sdk/
error.rs

1//! Error types for the plugin SDK.
2//!
3//! [`SdkError`] covers everything that can go wrong during plugin
4//! initialization, task execution, and result transport. It is
5//! `#[non_exhaustive]` so new variants can be added without breaking
6//! downstream code.
7
8use thiserror::Error;
9
10/// Errors that can occur within the plugin SDK or be propagated from plugins.
11#[derive(Debug, Error)]
12#[non_exhaustive]
13pub enum SdkError {
14    /// An error originating from the IPC or gRPC transport layer.
15    #[error("transport error: {0}")]
16    Transport(#[from] malbox_plugin_transport::error::TransportError),
17
18    /// A failure during plugin or runtime initialization.
19    #[error("initialization error: {0}")]
20    Init(String),
21
22    /// A blocking_send / try_send on a result or event channel failed.
23    /// Almost always means the receiver was dropped (daemon disconnected).
24    #[error("channel send failed: {0}")]
25    Channel(String),
26
27    /// An operation timed out waiting for an external signal.
28    #[error("{operation} timed out after {elapsed:?}")]
29    Timeout {
30        operation: &'static str,
31        elapsed: std::time::Duration,
32    },
33
34    /// A `Context` method was called from a place where the required
35    /// state isn't set up (e.g. `push_result` outside `on_task`,
36    /// `wait_for_execution` in a lifecycle handler).
37    #[error("invalid context: {0}")]
38    InvalidContext(&'static str),
39
40    /// A JSON serialization or deserialization error (e.g. config parsing).
41    #[error("serialization error: {0}")]
42    Serialization(#[from] serde_json::Error),
43
44    /// A standard I/O error (e.g. reading a sample file).
45    #[error("io error: {0}")]
46    Io(#[from] std::io::Error),
47
48    /// An opaque error returned by plugin handler code.
49    #[error("plugin error: {0}")]
50    Plugin(Box<dyn std::error::Error + Send + Sync>),
51}
52
53/// A [`Result`](std::result::Result) alias using [`SdkError`] as the error type.
54pub type Result<T> = std::result::Result<T, SdkError>;
55
56#[cfg(test)]
57mod tests {
58    use super::*;
59
60    #[test]
61    fn io_error_converts_to_sdk_error() {
62        let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
63        let sdk_err: SdkError = io_err.into();
64        assert!(matches!(sdk_err, SdkError::Io(_)));
65    }
66
67    #[test]
68    fn plugin_variant_wraps_custom_error() {
69        #[derive(Debug)]
70        struct MyErr(&'static str);
71        impl std::fmt::Display for MyErr {
72            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
73                write!(f, "{}", self.0)
74            }
75        }
76        impl std::error::Error for MyErr {}
77
78        let err: SdkError = SdkError::Plugin(Box::new(MyErr("custom boom")));
79        assert!(err.to_string().contains("custom boom"));
80    }
81
82    #[test]
83    fn serde_error_converts_to_sdk_error() {
84        let json_err = serde_json::from_str::<String>("not json").unwrap_err();
85        let sdk_err: SdkError = json_err.into();
86        assert!(matches!(sdk_err, SdkError::Serialization(_)));
87    }
88
89    use std::time::Duration;
90
91    #[test]
92    fn channel_variant_formats_correctly() {
93        let err = SdkError::Channel("push_result: receiver dropped".to_string());
94        assert!(err.to_string().contains("channel send failed"));
95        assert!(err.to_string().contains("push_result"));
96    }
97
98    #[test]
99    fn timeout_variant_formats_correctly() {
100        let err = SdkError::Timeout {
101            operation: "some_operation",
102            elapsed: Duration::from_secs(5),
103        };
104        let s = err.to_string();
105        assert!(s.contains("some_operation"));
106        assert!(s.contains("timed out"));
107    }
108
109    #[test]
110    fn invalid_context_variant_formats_correctly() {
111        let err = SdkError::InvalidContext("push_result called outside on_task");
112        assert!(err.to_string().contains("invalid context"));
113        assert!(err.to_string().contains("push_result"));
114    }
115}