1use thiserror::Error;
9
10#[derive(Debug, Error)]
12pub enum CoreError {
13 #[error("Cannot connect to controller at {url}: {reason}")]
15 ConnectionFailed { url: String, reason: String },
16
17 #[error("Authentication failed: {message}")]
18 AuthenticationFailed { message: String },
19
20 #[error("Controller disconnected")]
21 ControllerDisconnected,
22
23 #[error("Controller connection timed out after {timeout_secs}s")]
24 Timeout { timeout_secs: u64 },
25
26 #[error("Device not found: {identifier}")]
28 DeviceNotFound { identifier: String },
29
30 #[error("Client not found: {identifier}")]
31 ClientNotFound { identifier: String },
32
33 #[error("Network not found: {identifier}")]
34 NetworkNotFound { identifier: String },
35
36 #[error("Site not found: {name}")]
37 SiteNotFound { name: String },
38
39 #[error("Entity not found: {entity_type} with id {identifier}")]
40 NotFound {
41 entity_type: String,
42 identifier: String,
43 },
44
45 #[error("Operation not supported: {operation} (requires {required})")]
47 Unsupported { operation: String, required: String },
48
49 #[error("Operation rejected by controller: {message}")]
50 Rejected { message: String },
51
52 #[error("Validation failed: {message}")]
53 ValidationFailed { message: String },
54
55 #[error("Operation failed: {message}")]
56 OperationFailed { message: String },
57
58 #[error("API error: {message}")]
60 Api {
61 message: String,
62 code: Option<String>,
64 status: Option<u16>,
66 },
67
68 #[error("Configuration error: {message}")]
70 Config { message: String },
71
72 #[error("Internal error: {0}")]
74 Internal(String),
75}
76
77impl From<crate::error::Error> for CoreError {
80 fn from(err: crate::error::Error) -> Self {
81 match err {
82 crate::error::Error::Authentication { message } => {
83 CoreError::AuthenticationFailed { message }
84 }
85 crate::error::Error::TwoFactorRequired => CoreError::AuthenticationFailed {
86 message: "Two-factor authentication token required".into(),
87 },
88 crate::error::Error::SessionExpired => CoreError::AuthenticationFailed {
89 message: "Session expired -- re-authentication required".into(),
90 },
91 crate::error::Error::InvalidApiKey => CoreError::AuthenticationFailed {
92 message: "Invalid API key".into(),
93 },
94 crate::error::Error::WrongAuthStrategy { expected, got } => {
95 CoreError::AuthenticationFailed {
96 message: format!("Wrong auth strategy: expected {expected}, got {got}"),
97 }
98 }
99 crate::error::Error::Transport(ref e) => {
100 if e.is_timeout() {
101 CoreError::Timeout { timeout_secs: 0 }
102 } else if e.is_connect() {
103 CoreError::ConnectionFailed {
104 url: e
105 .url()
106 .map_or_else(|| "<unknown>".into(), ToString::to_string),
107 reason: e.to_string(),
108 }
109 } else if e.status().map(|s| s.as_u16()) == Some(404) {
110 CoreError::NotFound {
111 entity_type: "resource".into(),
112 identifier: e.url().map(|u| u.path().to_string()).unwrap_or_default(),
113 }
114 } else {
115 CoreError::Api {
116 message: e.to_string(),
117 code: None,
118 status: e.status().map(|s| s.as_u16()),
119 }
120 }
121 }
122 crate::error::Error::InvalidUrl(e) => CoreError::Config {
123 message: format!("Invalid URL: {e}"),
124 },
125 crate::error::Error::Timeout { timeout_secs } => CoreError::Timeout { timeout_secs },
126 crate::error::Error::Tls(msg) => CoreError::ConnectionFailed {
127 url: String::new(),
128 reason: format!("TLS error: {msg}"),
129 },
130 crate::error::Error::RateLimited { retry_after_secs } => CoreError::Api {
131 message: format!("Rate limited -- retry after {retry_after_secs}s"),
132 code: Some("rate_limited".into()),
133 status: Some(429),
134 },
135 crate::error::Error::Integration {
136 message,
137 code,
138 status,
139 } => CoreError::Api {
140 message,
141 code,
142 status: Some(status),
143 },
144 crate::error::Error::LegacyApi { message } => CoreError::Api {
145 message,
146 code: None,
147 status: None,
148 },
149 crate::error::Error::WebSocketConnect(reason) => CoreError::ConnectionFailed {
150 url: String::new(),
151 reason: format!("WebSocket connection failed: {reason}"),
152 },
153 crate::error::Error::WebSocketClosed { code, reason } => CoreError::ConnectionFailed {
154 url: String::new(),
155 reason: format!("WebSocket closed (code {code}): {reason}"),
156 },
157 crate::error::Error::Deserialization { message, body: _ } => {
158 CoreError::Internal(format!("Deserialization error: {message}"))
159 }
160 crate::error::Error::UnsupportedOperation(op) => CoreError::Unsupported {
161 operation: op.to_string(),
162 required: "a newer controller firmware".into(),
163 },
164 }
165 }
166}