icarus_core/
error.rs

1//! Error types for the Icarus SDK
2
3use serde::{Deserialize, Serialize};
4use thiserror::Error;
5
6/// Main error type for Icarus operations
7#[derive(Error, Debug)]
8pub enum IcarusError {
9    /// Error from the underlying MCP implementation
10    #[error("MCP error: {0}")]
11    Mcp(String),
12
13    /// Error from ICP canister operations
14    #[error("Canister error: {0}")]
15    Canister(String),
16
17    /// Serialization/deserialization errors
18    #[error("Serialization error: {0}")]
19    Serialization(#[from] serde_json::Error),
20
21    /// State management errors
22    #[error("State error: {0}")]
23    State(String),
24
25    /// Protocol translation errors
26    #[error("Protocol error: {0}")]
27    Protocol(String),
28
29    /// Tool execution errors
30    #[error("Tool error: {0}")]
31    Tool(#[from] ToolError),
32
33    /// Generic errors
34    #[error("{0}")]
35    Other(String),
36}
37
38/// Result type alias for Icarus operations
39pub type Result<T> = std::result::Result<T, IcarusError>;
40
41impl From<candid::Error> for IcarusError {
42    fn from(err: candid::Error) -> Self {
43        IcarusError::Canister(err.to_string())
44    }
45}
46
47/// Specialized error type for tool execution
48#[derive(Error, Debug, Clone, Serialize, Deserialize)]
49pub enum ToolError {
50    /// Invalid input provided to the tool
51    #[error("Invalid input: {0}")]
52    InvalidInput(String),
53
54    /// Requested resource was not found
55    #[error("Not found: {0}")]
56    NotFound(String),
57
58    /// Permission denied for the operation
59    #[error("Permission denied: {0}")]
60    PermissionDenied(String),
61
62    /// Operation failed for a specific reason
63    #[error("Operation failed: {0}")]
64    OperationFailed(String),
65
66    /// Internal error occurred
67    #[error("Internal error: {0}")]
68    InternalError(String),
69}
70
71impl ToolError {
72    /// Create an InvalidInput error
73    pub fn invalid_input(msg: impl Into<String>) -> Self {
74        Self::InvalidInput(msg.into())
75    }
76
77    /// Create a NotFound error
78    pub fn not_found(msg: impl Into<String>) -> Self {
79        Self::NotFound(msg.into())
80    }
81
82    /// Create a PermissionDenied error
83    pub fn permission_denied(msg: impl Into<String>) -> Self {
84        Self::PermissionDenied(msg.into())
85    }
86
87    /// Create an OperationFailed error
88    pub fn operation_failed(msg: impl Into<String>) -> Self {
89        Self::OperationFailed(msg.into())
90    }
91
92    /// Create an InternalError
93    pub fn internal(msg: impl Into<String>) -> Self {
94        Self::InternalError(msg.into())
95    }
96}
97
98#[cfg(test)]
99mod tests {
100    use super::*;
101
102    #[test]
103    fn test_icarus_error_display() {
104        let err = IcarusError::Mcp("connection failed".to_string());
105        assert_eq!(err.to_string(), "MCP error: connection failed");
106
107        let err = IcarusError::Canister("out of cycles".to_string());
108        assert_eq!(err.to_string(), "Canister error: out of cycles");
109
110        let err = IcarusError::State("invalid state".to_string());
111        assert_eq!(err.to_string(), "State error: invalid state");
112
113        let err = IcarusError::Protocol("invalid message".to_string());
114        assert_eq!(err.to_string(), "Protocol error: invalid message");
115
116        let err = IcarusError::Other("something went wrong".to_string());
117        assert_eq!(err.to_string(), "something went wrong");
118    }
119
120    #[test]
121    fn test_tool_error_helpers() {
122        let err = ToolError::invalid_input("bad input");
123        assert_eq!(err.to_string(), "Invalid input: bad input");
124
125        let err = ToolError::not_found("resource missing");
126        assert_eq!(err.to_string(), "Not found: resource missing");
127
128        let err = ToolError::permission_denied("unauthorized");
129        assert_eq!(err.to_string(), "Permission denied: unauthorized");
130
131        let err = ToolError::operation_failed("failed to process");
132        assert_eq!(err.to_string(), "Operation failed: failed to process");
133
134        let err = ToolError::internal("internal error");
135        assert_eq!(err.to_string(), "Internal error: internal error");
136    }
137
138    #[test]
139    fn test_tool_error_conversion() {
140        let tool_err = ToolError::invalid_input("bad data");
141        let icarus_err: IcarusError = tool_err.into();
142        assert_eq!(
143            icarus_err.to_string(),
144            "Tool error: Invalid input: bad data"
145        );
146    }
147
148    #[test]
149    fn test_candid_error_conversion() {
150        // We can't easily create a real candid::Error, but we can test the conversion exists
151        // by checking that the From trait is implemented
152        fn _test_conversion_exists() {
153            let _: fn(candid::Error) -> IcarusError = IcarusError::from;
154        }
155    }
156
157    #[test]
158    fn test_tool_error_serialization() {
159        let err = ToolError::not_found("item");
160        let serialized = serde_json::to_string(&err).unwrap();
161        let deserialized: ToolError = serde_json::from_str(&serialized).unwrap();
162        assert_eq!(err.to_string(), deserialized.to_string());
163    }
164
165    #[test]
166    fn test_tool_error_clone() {
167        let err1 = ToolError::permission_denied("access denied");
168        let err2 = err1.clone();
169        assert_eq!(err1.to_string(), err2.to_string());
170    }
171}