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}