Skip to main content

strike48_connector/
error.rs

1use thiserror::Error;
2
3/// Result type alias for connector operations
4pub type Result<T> = std::result::Result<T, ConnectorError>;
5
6/// Connector error types
7#[derive(Error, Debug)]
8pub enum ConnectorError {
9    #[error("Connection error: {0}")]
10    ConnectionError(String),
11
12    #[error("Not connected to Strike48 server")]
13    NotConnected,
14
15    #[error("Connector not registered")]
16    NotRegistered,
17
18    #[error("Stream error: {0}")]
19    StreamError(String),
20
21    #[error("Registration error: {0}")]
22    RegistrationError(String),
23
24    #[error("Invalid configuration: {0}")]
25    InvalidConfig(String),
26
27    #[error("Serialization error: {0}")]
28    SerializationError(String),
29
30    #[error("Deserialization error: {0}")]
31    DeserializationError(String),
32
33    #[error("Unsupported encoding: {0}")]
34    UnsupportedEncoding(String),
35
36    #[error("Timeout: {0}")]
37    Timeout(String),
38
39    #[error("Invoke failed: {0}")]
40    InvokeFailed(String),
41
42    #[error("Connector not running")]
43    NotRunning,
44
45    #[error("Already running")]
46    AlreadyRunning,
47
48    #[error("IO error: {0}")]
49    Io(#[source] Box<std::io::Error>),
50
51    #[error("gRPC error: {0}")]
52    Grpc(#[source] Box<tonic::Status>),
53
54    #[error("JSON error: {0}")]
55    Json(#[source] Box<serde_json::Error>),
56
57    #[error("Not implemented: {0}")]
58    NotImplemented(String),
59
60    #[error("Other error: {0}")]
61    Other(String),
62}
63
64impl From<std::io::Error> for ConnectorError {
65    fn from(e: std::io::Error) -> Self {
66        ConnectorError::Io(Box::new(e))
67    }
68}
69
70impl From<tonic::Status> for ConnectorError {
71    fn from(e: tonic::Status) -> Self {
72        ConnectorError::Grpc(Box::new(e))
73    }
74}
75
76impl From<serde_json::Error> for ConnectorError {
77    fn from(e: serde_json::Error) -> Self {
78        ConnectorError::Json(Box::new(e))
79    }
80}
81
82impl ConnectorError {
83    pub fn code(&self) -> &'static str {
84        match self {
85            ConnectorError::ConnectionError(_) => "CONNECTION_ERROR",
86            ConnectorError::NotConnected => "NOT_CONNECTED",
87            ConnectorError::NotRegistered => "NOT_REGISTERED",
88            ConnectorError::StreamError(_) => "STREAM_ERROR",
89            ConnectorError::RegistrationError(_) => "REGISTRATION_ERROR",
90            ConnectorError::InvalidConfig(_) => "INVALID_CONFIG",
91            ConnectorError::SerializationError(_) => "SERIALIZATION_ERROR",
92            ConnectorError::DeserializationError(_) => "DESERIALIZATION_ERROR",
93            ConnectorError::UnsupportedEncoding(_) => "UNSUPPORTED_ENCODING",
94            ConnectorError::Timeout(_) => "TIMEOUT",
95            ConnectorError::InvokeFailed(_) => "INVOKE_FAILED",
96            ConnectorError::NotRunning => "NOT_RUNNING",
97            ConnectorError::AlreadyRunning => "ALREADY_RUNNING",
98            ConnectorError::Io(_) => "IO_ERROR",
99            ConnectorError::Grpc(_) => "GRPC_ERROR",
100            ConnectorError::Json(_) => "JSON_ERROR",
101            ConnectorError::NotImplemented(_) => "NOT_IMPLEMENTED",
102            ConnectorError::Other(_) => "OTHER",
103        }
104    }
105
106    /// Whether this error is transient and reconnection may succeed.
107    ///
108    /// Non-recoverable errors (invalid config, permanent registration
109    /// rejection, serialization bugs) should stop the reconnect loop
110    /// to avoid wasting resources and hiding real problems.
111    pub fn is_recoverable(&self) -> bool {
112        match self {
113            ConnectorError::ConnectionError(_)
114            | ConnectorError::NotConnected
115            | ConnectorError::StreamError(_)
116            | ConnectorError::Timeout(_)
117            | ConnectorError::Io(_)
118            | ConnectorError::Grpc(_) => true,
119
120            ConnectorError::RegistrationError(msg) => {
121                let lower = msg.to_lowercase();
122                // Permanent config/policy errors — no point retrying
123                let permanent = lower.contains("not enabled")
124                    || lower.contains("not supported")
125                    || lower.contains("disabled")
126                    || lower.contains("invalid connector")
127                    || lower.contains("permission denied")
128                    || lower.contains("not allowed");
129                // Auth errors (jwt_invalid, auth_invalid, etc.) are handled
130                // in-stream by the registration failure path which clears
131                // credentials and retries. They should never be considered
132                // permanent shutdowns if they do reach this branch.
133                !permanent
134            }
135
136            ConnectorError::InvalidConfig(_)
137            | ConnectorError::NotRegistered
138            | ConnectorError::SerializationError(_)
139            | ConnectorError::DeserializationError(_)
140            | ConnectorError::UnsupportedEncoding(_)
141            | ConnectorError::NotImplemented(_)
142            | ConnectorError::NotRunning
143            | ConnectorError::AlreadyRunning
144            | ConnectorError::Json(_) => false,
145
146            ConnectorError::InvokeFailed(_) | ConnectorError::Other(_) => true,
147        }
148    }
149}