reso_client/
error.rs

1// src/error.rs
2
3//! Error types for the RESO client library
4//!
5//! This module defines all error types that can occur when using the RESO client.
6//! Errors are categorized by their source and include detailed context.
7//!
8//! # Examples
9//!
10//! ```no_run
11//! # use reso_client::{ResoClient, QueryBuilder, ResoError};
12//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
13//! let client = ResoClient::from_env()?;
14//! let query = QueryBuilder::new("Property")
15//!     .filter("City eq 'Austin'")
16//!     .build()?;
17//!
18//! match client.execute(&query).await {
19//!     Ok(results) => {
20//!         println!("Success!");
21//!     }
22//!     Err(ResoError::Unauthorized { message, .. }) => {
23//!         eprintln!("Auth failed: {}", message);
24//!     }
25//!     Err(ResoError::NotFound { message, .. }) => {
26//!         eprintln!("Resource not found: {}", message);
27//!     }
28//!     Err(ResoError::Network(msg)) => {
29//!         eprintln!("Network error: {}", msg);
30//!     }
31//!     Err(e) => {
32//!         eprintln!("Other error: {}", e);
33//!     }
34//! }
35//! # Ok(())
36//! # }
37//! ```
38
39use thiserror::Error;
40
41/// Result type alias for RESO client operations
42///
43/// # Examples
44///
45/// ```
46/// # use reso_client::Result;
47/// fn parse_value(s: &str) -> Result<i32> {
48///     s.parse().map_err(|_| {
49///         reso_client::ResoError::Parse("Invalid integer".to_string())
50///     })
51/// }
52/// ```
53pub type Result<T> = std::result::Result<T, ResoError>;
54
55/// OData error response format
56///
57/// The RESO Web API may return structured error responses in this format:
58/// ```json
59/// {
60///   "error": {
61///     "code": "ErrorCode",
62///     "message": "Error description"
63///   }
64/// }
65/// ```
66#[derive(Debug, Clone, serde::Deserialize)]
67pub struct ODataErrorResponse {
68    pub error: ODataErrorDetail,
69}
70
71#[derive(Debug, Clone, serde::Deserialize)]
72pub struct ODataErrorDetail {
73    #[serde(default)]
74    pub code: String,
75    pub message: String,
76}
77
78/// RESO client errors
79///
80/// All errors that can occur when using the RESO client library.
81/// Each variant includes contextual information to help diagnose issues.
82///
83/// # Examples
84///
85/// ```no_run
86/// # use reso_client::{ResoClient, ResoError};
87/// # async fn example() {
88/// let result = ResoClient::from_env();
89/// match result {
90///     Err(ResoError::Config(msg)) => {
91///         eprintln!("Configuration error: {}", msg);
92///         eprintln!("Make sure RESO_BASE_URL and RESO_TOKEN are set");
93///     }
94///     Ok(client) => {
95///         println!("Client created successfully");
96///     }
97///     _ => {}
98/// }
99/// # }
100/// ```
101#[derive(Debug, Error)]
102pub enum ResoError {
103    /// Configuration error
104    #[error("Configuration error: {0}")]
105    Config(String),
106
107    /// Network/HTTP error
108    #[error("Network error: {0}")]
109    Network(String),
110
111    /// 401 Unauthorized - Invalid or missing authentication token
112    #[error("Unauthorized (401): {message}")]
113    Unauthorized { message: String, status_code: u16 },
114
115    /// 403 Forbidden - Valid credentials but insufficient permissions
116    #[error("Forbidden (403): {message}")]
117    Forbidden { message: String, status_code: u16 },
118
119    /// 404 Not Found - Resource or endpoint not found
120    #[error("Not Found (404): {message}")]
121    NotFound { message: String, status_code: u16 },
122
123    /// 429 Too Many Requests - Rate limit exceeded
124    #[error("Rate Limited (429): {message}")]
125    RateLimited { message: String, status_code: u16 },
126
127    /// 5xx Server Error - Server-side error
128    #[error("Server Error ({status_code}): {message}")]
129    ServerError { message: String, status_code: u16 },
130
131    /// Generic OData server error for other status codes
132    #[error("OData error ({status_code}): {message}")]
133    ODataError { message: String, status_code: u16 },
134
135    /// Parsing error
136    #[error("Parse error: {0}")]
137    Parse(String),
138
139    /// Invalid query
140    #[error("Invalid query: {0}")]
141    InvalidQuery(String),
142}
143
144impl ResoError {
145    /// Parse error response body and extract OData error message if present
146    ///
147    /// OData servers may return structured errors in a specific JSON format.
148    /// This method attempts to parse that format and extract meaningful error details.
149    pub(crate) fn parse_error_body(body: &str) -> String {
150        // Try to parse as OData error format: {"error": {"code": "...", "message": "..."}}
151        if let Ok(odata_error) = serde_json::from_str::<ODataErrorResponse>(body) {
152            if !odata_error.error.code.is_empty() {
153                return format!(
154                    "{} (code: {})",
155                    odata_error.error.message, odata_error.error.code
156                );
157            }
158            return odata_error.error.message;
159        }
160
161        // If not OData format or parsing failed, return the body as-is
162        // Truncate if too long to avoid overwhelming error messages
163        if body.len() > 500 {
164            format!("{}... (truncated)", &body[..500])
165        } else {
166            body.to_string()
167        }
168    }
169
170    /// Create an appropriate error from HTTP status code and response body
171    pub(crate) fn from_status(status_code: u16, body: &str) -> Self {
172        let message = Self::parse_error_body(body);
173
174        match status_code {
175            401 => ResoError::Unauthorized {
176                message,
177                status_code,
178            },
179            403 => ResoError::Forbidden {
180                message,
181                status_code,
182            },
183            404 => ResoError::NotFound {
184                message,
185                status_code,
186            },
187            429 => ResoError::RateLimited {
188                message,
189                status_code,
190            },
191            500..=599 => ResoError::ServerError {
192                message,
193                status_code,
194            },
195            _ => ResoError::ODataError {
196                message,
197                status_code,
198            },
199        }
200    }
201}