Skip to main content

ghost_io_api/
error.rs

1//! Error types for the Ghost API client.
2//!
3//! This module provides the [`GhostError`] enum which encompasses all possible
4//! errors that can occur when interacting with the Ghost API.
5//!
6//! # Examples
7//!
8//! ```
9//! use ghost_io_api::error::{GhostError, Result};
10//!
11//! fn example_function() -> Result<String> {
12//!     // Your code here
13//!     Ok("success".to_string())
14//! }
15//! ```
16
17/// A specialized `Result` type for Ghost API operations.
18///
19/// This type is used throughout the crate for any operation that may produce
20/// a [`GhostError`].
21pub type Result<T> = std::result::Result<T, GhostError>;
22
23/// The error type for Ghost API operations.
24///
25/// This enum represents all possible errors that can occur when interacting
26/// with the Ghost API, including network errors, serialization errors, API
27/// errors, and authentication errors.
28#[derive(thiserror::Error, Debug)]
29pub enum GhostError {
30    /// An error returned by the Ghost API.
31    ///
32    /// Ghost returns structured JSON errors with a message, error type,
33    /// and optional context information.
34    #[error("Ghost API error ({error_type}): {message}")]
35    Api {
36        /// The human-readable error message.
37        message: String,
38        /// The error type identifier (e.g., "NotFoundError", "ValidationError").
39        error_type: String,
40        /// Optional additional context about the error.
41        context: Option<String>,
42    },
43
44    /// An HTTP error from the underlying HTTP client.
45    ///
46    /// This includes network errors, connection timeouts, DNS resolution
47    /// failures, and HTTP status code errors.
48    #[error("HTTP error: {0}")]
49    Http(#[from] reqwest::Error),
50
51    /// A JSON serialization or deserialization error.
52    ///
53    /// This occurs when the response from Ghost cannot be parsed or when
54    /// a request body cannot be serialized.
55    #[error("Serialization error: {0}")]
56    Json(#[from] serde_json::Error),
57
58    /// An authentication error.
59    ///
60    /// This includes JWT generation failures, invalid API keys, or other
61    /// authentication-related issues.
62    #[error("Authentication error: {0}")]
63    Auth(String),
64}
65
66impl GhostError {
67    /// Creates a new API error with the given message and error type.
68    ///
69    /// # Examples
70    ///
71    /// ```
72    /// use ghost_io_api::error::GhostError;
73    ///
74    /// let error = GhostError::api(
75    ///     "Resource not found",
76    ///     "NotFoundError",
77    ///     None,
78    /// );
79    /// ```
80    pub fn api(
81        message: impl Into<String>,
82        error_type: impl Into<String>,
83        context: Option<String>,
84    ) -> Self {
85        Self::Api {
86            message: message.into(),
87            error_type: error_type.into(),
88            context,
89        }
90    }
91
92    /// Creates a new authentication error.
93    ///
94    /// # Examples
95    ///
96    /// ```
97    /// use ghost_io_api::error::GhostError;
98    ///
99    /// let error = GhostError::auth("Invalid API key");
100    /// ```
101    pub fn auth(message: impl Into<String>) -> Self {
102        Self::Auth(message.into())
103    }
104
105    /// Returns `true` if this is an API error.
106    ///
107    /// # Examples
108    ///
109    /// ```
110    /// use ghost_io_api::error::GhostError;
111    ///
112    /// let error = GhostError::api("Not found", "NotFoundError", None);
113    /// assert!(error.is_api_error());
114    /// ```
115    pub fn is_api_error(&self) -> bool {
116        matches!(self, Self::Api { .. })
117    }
118
119    /// Returns `true` if this is an HTTP error.
120    pub fn is_http_error(&self) -> bool {
121        matches!(self, Self::Http(_))
122    }
123
124    /// Returns `true` if this is a JSON serialization error.
125    pub fn is_json_error(&self) -> bool {
126        matches!(self, Self::Json(_))
127    }
128
129    /// Returns `true` if this is an authentication error.
130    pub fn is_auth_error(&self) -> bool {
131        matches!(self, Self::Auth(_))
132    }
133
134    /// Returns the error type for API errors.
135    ///
136    /// Returns `None` for non-API errors.
137    ///
138    /// # Examples
139    ///
140    /// ```
141    /// use ghost_io_api::error::GhostError;
142    ///
143    /// let error = GhostError::api("Not found", "NotFoundError", None);
144    /// assert_eq!(error.api_error_type(), Some("NotFoundError"));
145    /// ```
146    pub fn api_error_type(&self) -> Option<&str> {
147        match self {
148            Self::Api { error_type, .. } => Some(error_type),
149            _ => None,
150        }
151    }
152
153    /// Returns the error message for API errors.
154    ///
155    /// Returns `None` for non-API errors.
156    pub fn api_message(&self) -> Option<&str> {
157        match self {
158            Self::Api { message, .. } => Some(message),
159            _ => None,
160        }
161    }
162
163    /// Returns the context for API errors.
164    ///
165    /// Returns `None` for non-API errors or if no context is available.
166    pub fn api_context(&self) -> Option<&str> {
167        match self {
168            Self::Api { context, .. } => context.as_deref(),
169            _ => None,
170        }
171    }
172}
173
174#[cfg(test)]
175mod tests {
176    use super::*;
177
178    #[test]
179    fn test_api_error_creation() {
180        let error = GhostError::api("Resource not found", "NotFoundError", None);
181        assert!(error.is_api_error());
182        assert_eq!(error.api_error_type(), Some("NotFoundError"));
183        assert_eq!(error.api_message(), Some("Resource not found"));
184        assert_eq!(error.api_context(), None);
185    }
186
187    #[test]
188    fn test_api_error_with_context() {
189        let error = GhostError::api(
190            "Validation failed",
191            "ValidationError",
192            Some("Title is required".to_string()),
193        );
194        assert!(error.is_api_error());
195        assert_eq!(error.api_error_type(), Some("ValidationError"));
196        assert_eq!(error.api_message(), Some("Validation failed"));
197        assert_eq!(error.api_context(), Some("Title is required"));
198    }
199
200    #[test]
201    fn test_auth_error_creation() {
202        let error = GhostError::auth("Invalid API key");
203        assert!(error.is_auth_error());
204        assert!(!error.is_api_error());
205        assert_eq!(error.api_error_type(), None);
206    }
207
208    #[test]
209    fn test_http_error_from_trait() {
210        // Test that From trait works for reqwest::Error
211        // We can't easily create a reqwest::Error without actually making a request,
212        // so we'll test the type signature and behavior through documentation
213        //
214        // In real usage:
215        // let reqwest_error: reqwest::Error = ...;
216        // let ghost_error: GhostError = reqwest_error.into();
217        // assert!(ghost_error.is_http_error());
218
219        // We can verify the From trait exists at compile time
220        fn _assert_from_impl(e: reqwest::Error) -> GhostError {
221            e.into()
222        }
223    }
224
225    #[test]
226    fn test_json_error_conversion() {
227        let json_error = serde_json::from_str::<serde_json::Value>("not valid json").unwrap_err();
228        let error: GhostError = json_error.into();
229        assert!(error.is_json_error());
230        assert!(!error.is_api_error());
231    }
232
233    #[test]
234    fn test_error_display() {
235        let error = GhostError::api("Not found", "NotFoundError", None);
236        let display = format!("{}", error);
237        assert!(display.contains("NotFoundError"));
238        assert!(display.contains("Not found"));
239    }
240
241    #[test]
242    fn test_error_display_with_context() {
243        let error = GhostError::api(
244            "Validation failed",
245            "ValidationError",
246            Some("Title required".to_string()),
247        );
248        let display = format!("{}", error);
249        assert!(display.contains("ValidationError"));
250        assert!(display.contains("Validation failed"));
251    }
252
253    #[test]
254    fn test_auth_error_display() {
255        let error = GhostError::auth("JWT generation failed");
256        let display = format!("{}", error);
257        assert!(display.contains("Authentication error"));
258        assert!(display.contains("JWT generation failed"));
259    }
260
261    #[test]
262    fn test_result_type_alias() {
263        fn returns_result() -> Result<String> {
264            Ok("success".to_string())
265        }
266
267        let result = returns_result();
268        assert!(result.is_ok());
269        assert_eq!(result.unwrap(), "success");
270    }
271
272    #[test]
273    fn test_result_with_error() {
274        fn returns_error() -> Result<String> {
275            Err(GhostError::auth("Failed"))
276        }
277
278        let result = returns_error();
279        assert!(result.is_err());
280        let error = result.unwrap_err();
281        assert!(error.is_auth_error());
282    }
283
284    #[test]
285    fn test_error_type_checks() {
286        let api_error = GhostError::api("Test", "TestError", None);
287        assert!(api_error.is_api_error());
288        assert!(!api_error.is_http_error());
289        assert!(!api_error.is_json_error());
290        assert!(!api_error.is_auth_error());
291
292        let auth_error = GhostError::auth("Test");
293        assert!(!auth_error.is_api_error());
294        assert!(!auth_error.is_http_error());
295        assert!(!auth_error.is_json_error());
296        assert!(auth_error.is_auth_error());
297    }
298
299    #[test]
300    fn test_api_error_accessors() {
301        let error = GhostError::api("Test message", "TestType", Some("Test context".to_string()));
302
303        assert_eq!(error.api_message(), Some("Test message"));
304        assert_eq!(error.api_error_type(), Some("TestType"));
305        assert_eq!(error.api_context(), Some("Test context"));
306    }
307
308    #[test]
309    fn test_non_api_error_accessors_return_none() {
310        let error = GhostError::auth("Test");
311
312        assert_eq!(error.api_message(), None);
313        assert_eq!(error.api_error_type(), None);
314        assert_eq!(error.api_context(), None);
315    }
316
317    #[test]
318    fn test_error_is_send_and_sync() {
319        fn assert_send<T: Send>() {}
320        fn assert_sync<T: Sync>() {}
321
322        assert_send::<GhostError>();
323        assert_sync::<GhostError>();
324    }
325
326    #[test]
327    fn test_error_debug() {
328        let error = GhostError::api("Debug test", "DebugError", None);
329        let debug = format!("{:?}", error);
330        assert!(debug.contains("Api"));
331        assert!(debug.contains("Debug test"));
332        assert!(debug.contains("DebugError"));
333    }
334}