Skip to main content

unifly_api/
core_error.rs

1// ── Core error types ──
2//
3// User-facing errors from unifly-core. These are NOT API-specific --
4// consumers never see HTTP status codes or JSON parse failures directly.
5// The `From<crate::error::Error>` impl translates transport-layer errors
6// into domain-appropriate variants.
7
8use thiserror::Error;
9
10/// Unified error type for the core crate.
11#[derive(Debug, Error)]
12pub enum CoreError {
13    // ── Connection errors ────────────────────────────────────────────
14    #[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    // ── Data errors ──────────────────────────────────────────────────
27    #[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    // ── Operation errors ─────────────────────────────────────────────
46    #[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    // ── API errors (wrapped, not exposed raw) ────────────────────────
59    #[error("API error: {message}")]
60    Api {
61        message: String,
62        /// The API-specific error code (e.g., "api.authentication.missing-credentials").
63        code: Option<String>,
64        /// HTTP status code (if applicable).
65        status: Option<u16>,
66    },
67
68    // ── Configuration errors ─────────────────────────────────────────
69    #[error("Configuration error: {message}")]
70    Config { message: String },
71
72    // ── Internal errors ──────────────────────────────────────────────
73    #[error("Internal error: {0}")]
74    Internal(String),
75}
76
77// ── Conversion from transport-layer errors ───────────────────────────
78
79impl 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}