Skip to main content

iii_sdk/
error.rs

1use schemars::JsonSchema;
2use serde::{Deserialize, Serialize};
3use thiserror::Error;
4
5/// Errors returned by the III SDK.
6#[derive(Debug, Error, Clone, Serialize, JsonSchema)]
7pub enum Error {
8    #[error("iii is not connected")]
9    NotConnected,
10    #[error("invocation timed out")]
11    Timeout,
12    #[error("runtime error: {0}")]
13    Runtime(String),
14    #[error("remote error ({code}): {message}")]
15    Remote {
16        code: String,
17        message: String,
18        stacktrace: Option<String>,
19    },
20    #[error("handler error: {0}")]
21    Handler(String),
22    #[error("serialization error: {0}")]
23    Serde(String),
24    #[error("websocket error: {0}")]
25    WebSocket(String),
26}
27
28impl From<serde_json::Error> for Error {
29    fn from(err: serde_json::Error) -> Self {
30        Error::Serde(err.to_string())
31    }
32}
33
34impl From<String> for Error {
35    fn from(msg: String) -> Self {
36        Error::Handler(msg)
37    }
38}
39
40impl From<&str> for Error {
41    fn from(msg: &str) -> Self {
42        Error::Handler(msg.to_string())
43    }
44}
45
46impl From<tokio_tungstenite::tungstenite::Error> for Error {
47    fn from(err: tokio_tungstenite::tungstenite::Error) -> Self {
48        Error::WebSocket(err.to_string())
49    }
50}
51
52/// Structured invocation failure, mirroring the Node and Python `InvocationError`.
53///
54/// Produced from the [`Error::Remote`] variant via [`Error::invocation_error`].
55/// `function_id` is `None` from that accessor because the wire `Remote` payload
56/// does not carry it.
57#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
58pub struct InvocationError {
59    pub code: String,
60    pub message: String,
61    #[serde(skip_serializing_if = "Option::is_none")]
62    pub function_id: Option<String>,
63    #[serde(skip_serializing_if = "Option::is_none")]
64    pub stacktrace: Option<String>,
65}
66
67impl std::fmt::Display for InvocationError {
68    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
69        write!(f, "{}: {}", self.code, self.message)
70    }
71}
72
73impl std::error::Error for InvocationError {}
74
75impl Error {
76    /// If this is a remote invocation failure (`Error::Remote`), return its
77    /// structured form. Returns `None` for transport/serde/handler errors.
78    pub fn invocation_error(&self) -> Option<InvocationError> {
79        match self {
80            Error::Remote {
81                code,
82                message,
83                stacktrace,
84            } => Some(InvocationError {
85                code: code.clone(),
86                message: message.clone(),
87                function_id: None,
88                stacktrace: stacktrace.clone(),
89            }),
90            _ => None,
91        }
92    }
93}
94
95#[cfg(test)]
96mod invocation_error_tests {
97    use super::*;
98
99    #[test]
100    fn remote_error_yields_invocation_error() {
101        let err = Error::Remote {
102            code: "FORBIDDEN".into(),
103            message: "nope".into(),
104            stacktrace: Some("trace".into()),
105        };
106        let inv = err.invocation_error().expect("remote -> invocation");
107        assert_eq!(inv.code, "FORBIDDEN");
108        assert_eq!(inv.message, "nope");
109        assert_eq!(inv.stacktrace.as_deref(), Some("trace"));
110        assert_eq!(inv.to_string(), "FORBIDDEN: nope");
111    }
112
113    #[test]
114    fn non_remote_error_yields_none() {
115        assert!(Error::Timeout.invocation_error().is_none());
116    }
117}