bing_webmaster_api/
error.rs

1use serde::{Deserialize, Serialize};
2use std::fmt;
3use thiserror::Error;
4
5/// Bing API error codes
6///
7/// This enum represents all possible error codes that can be returned by the Bing Webmaster API.
8/// Each variant corresponds to a specific error condition documented in the API specification.
9///
10/// Reference: Microsoft.Bing.Webmaster.Api.Interfaces.ApiErrorCode
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
12#[repr(i32)]
13pub enum BingErrorCode {
14    /// No error
15    None = 0,
16    /// Internal server error
17    InternalError = 1,
18    /// Unknown error occurred
19    UnknownError = 2,
20    /// Invalid or missing API key
21    InvalidApiKey = 3,
22    /// User request rate limit exceeded
23    ThrottleUser = 4,
24    /// Host request rate limit exceeded
25    ThrottleHost = 5,
26    /// User has been blocked
27    UserBlocked = 6,
28    /// URL format is invalid
29    InvalidUrl = 7,
30    /// Request parameter is invalid
31    InvalidParameter = 8,
32    /// Too many sites associated with this account
33    TooManySites = 9,
34    /// User not found
35    UserNotFound = 10,
36    /// Requested resource not found
37    NotFound = 11,
38    /// Resource already exists
39    AlreadyExists = 12,
40    /// Operation not allowed
41    NotAllowed = 13,
42    /// User not authorized for this operation
43    NotAuthorized = 14,
44    /// Resource in unexpected state
45    UnexpectedState = 15,
46    /// API method is deprecated
47    Deprecated = 16,
48}
49
50impl BingErrorCode {
51    /// Create from integer value
52    pub fn from_i32(value: i32) -> Option<Self> {
53        match value {
54            0 => Some(Self::None),
55            1 => Some(Self::InternalError),
56            2 => Some(Self::UnknownError),
57            3 => Some(Self::InvalidApiKey),
58            4 => Some(Self::ThrottleUser),
59            5 => Some(Self::ThrottleHost),
60            6 => Some(Self::UserBlocked),
61            7 => Some(Self::InvalidUrl),
62            8 => Some(Self::InvalidParameter),
63            9 => Some(Self::TooManySites),
64            10 => Some(Self::UserNotFound),
65            11 => Some(Self::NotFound),
66            12 => Some(Self::AlreadyExists),
67            13 => Some(Self::NotAllowed),
68            14 => Some(Self::NotAuthorized),
69            15 => Some(Self::UnexpectedState),
70            16 => Some(Self::Deprecated),
71            _ => None,
72        }
73    }
74
75    /// Get integer value
76    pub fn to_i32(self) -> i32 {
77        self as i32
78    }
79}
80
81impl fmt::Display for BingErrorCode {
82    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
83        let name = match self {
84            Self::None => "None",
85            Self::InternalError => "InternalError",
86            Self::UnknownError => "UnknownError",
87            Self::InvalidApiKey => "InvalidApiKey",
88            Self::ThrottleUser => "ThrottleUser",
89            Self::ThrottleHost => "ThrottleHost",
90            Self::UserBlocked => "UserBlocked",
91            Self::InvalidUrl => "InvalidUrl",
92            Self::InvalidParameter => "InvalidParameter",
93            Self::TooManySites => "TooManySites",
94            Self::UserNotFound => "UserNotFound",
95            Self::NotFound => "NotFound",
96            Self::AlreadyExists => "AlreadyExists",
97            Self::NotAllowed => "NotAllowed",
98            Self::NotAuthorized => "NotAuthorized",
99            Self::UnexpectedState => "UnexpectedState",
100            Self::Deprecated => "Deprecated",
101        };
102        write!(f, "{}", name)
103    }
104}
105
106/// Internal wrapper for error code deserialization
107#[derive(Debug, Clone, Deserialize)]
108#[serde(untagged)]
109enum ErrorCodeWrapper {
110    /// Structured error code
111    Structured { value: i32 },
112    /// Raw integer value
113    Raw(i32),
114}
115
116impl ErrorCodeWrapper {
117    fn value(&self) -> i32 {
118        match self {
119            Self::Structured { value } => *value,
120            Self::Raw(value) => *value,
121        }
122    }
123}
124
125/// Internal structure for deserializing Bing API error responses
126///
127/// Reference: Microsoft.Bing.Webmaster.Api.Interfaces.ApiFault
128#[derive(Debug, Clone, Deserialize)]
129struct ApiErrorResponse {
130    #[serde(rename = "ErrorCode")]
131    error_code: ErrorCodeWrapper,
132    #[serde(rename = "Message")]
133    message: String,
134}
135
136/// Errors that can occur when interacting with the Bing Webmaster API
137#[derive(Debug, Error)]
138pub enum WebmasterApiError {
139    /// HTTP request failed
140    #[error("HTTP request failed: {0}")]
141    HttpError(#[from] reqwest::Error),
142
143    /// Middleware request failed
144    #[error("Middleware request failed: {0}")]
145    MiddlewareHttpError(#[from] reqwest_middleware::Error),
146
147    /// Failed to parse response
148    #[error("Failed to parse response: {0}")]
149    ParseError(#[from] serde_json::Error),
150
151    /// API returned a structured error
152    #[error("API error ({error_code_raw}): {message}")]
153    ApiError {
154        /// HTTP status code (if available)
155        status: Option<u16>,
156        /// Error code enum (if recognized)
157        error_code: Option<BingErrorCode>,
158        /// Raw error code value
159        error_code_raw: i32,
160        /// Error message from the API
161        message: String,
162    },
163
164    /// HTTP status error without structured API response
165    #[error("HTTP {status}: {message}")]
166    HttpStatusError {
167        /// HTTP status code
168        status: u16,
169        /// Error message
170        message: String,
171        /// Optional response body
172        response_body: Option<String>,
173    },
174
175    /// Invalid API response format
176    #[error("Invalid API response: {0}")]
177    InvalidResponse(String),
178
179    /// Authentication failed
180    #[error("Authentication failed: missing or invalid API key")]
181    AuthenticationError,
182
183    /// Other errors (for anyhow integration)
184    #[error(transparent)]
185    Other(#[from] anyhow::Error),
186}
187
188impl WebmasterApiError {
189    /// Create an API error from error code and message
190    pub fn api_error(error_code_raw: i32, message: String, status: Option<u16>) -> Self {
191        let error_code = BingErrorCode::from_i32(error_code_raw);
192        Self::ApiError {
193            status,
194            error_code,
195            error_code_raw,
196            message,
197        }
198    }
199
200    /// Create an HTTP status error
201    pub fn http_status(status: u16, message: impl Into<String>) -> Self {
202        Self::HttpStatusError {
203            status,
204            message: message.into(),
205            response_body: None,
206        }
207    }
208
209    /// Create an HTTP status error with response body
210    pub fn http_status_with_body(
211        status: u16,
212        message: impl Into<String>,
213        response_body: impl Into<String>,
214    ) -> Self {
215        Self::HttpStatusError {
216            status,
217            message: message.into(),
218            response_body: Some(response_body.into()),
219        }
220    }
221
222    /// Create an invalid response error
223    pub fn invalid_response(message: impl Into<String>) -> Self {
224        Self::InvalidResponse(message.into())
225    }
226
227    /// Check if this error is retryable
228    pub fn is_retryable(&self) -> bool {
229        match self {
230            // Network errors might be retryable
231            Self::HttpError(_) | Self::MiddlewareHttpError(_) => true,
232            // Some HTTP status codes are retryable
233            Self::HttpStatusError { status, .. } => {
234                matches!(status, 429 | 500 | 502 | 503 | 504)
235            }
236            // Some API errors are retryable
237            Self::ApiError { error_code, .. } => {
238                matches!(
239                    error_code,
240                    Some(BingErrorCode::ThrottleUser) | Some(BingErrorCode::ThrottleHost)
241                )
242            }
243            _ => false,
244        }
245    }
246
247    /// Check if this error is related to authentication
248    pub fn is_authentication_error(&self) -> bool {
249        match self {
250            Self::AuthenticationError => true,
251            Self::HttpStatusError { status, .. } => *status == 401 || *status == 403,
252            Self::ApiError { error_code, .. } => {
253                matches!(
254                    error_code,
255                    Some(BingErrorCode::InvalidApiKey) | Some(BingErrorCode::NotAuthorized)
256                )
257            }
258            _ => false,
259        }
260    }
261
262    /// Check if this error is related to rate limiting
263    pub fn is_rate_limit_error(&self) -> bool {
264        match self {
265            Self::HttpStatusError { status, .. } => *status == 429,
266            Self::ApiError { error_code, .. } => {
267                matches!(
268                    error_code,
269                    Some(BingErrorCode::ThrottleUser) | Some(BingErrorCode::ThrottleHost)
270                )
271            }
272            _ => false,
273        }
274    }
275
276    /// Get the HTTP status code if available
277    pub fn status_code(&self) -> Option<u16> {
278        match self {
279            Self::HttpStatusError { status, .. } => Some(*status),
280            Self::ApiError { status, .. } => *status,
281            Self::HttpError(err) => err.status().map(|s| s.as_u16()),
282            _ => None,
283        }
284    }
285
286    /// Get the response body if available
287    pub fn response_body(&self) -> Option<&str> {
288        match self {
289            Self::HttpStatusError { response_body, .. } => response_body.as_deref(),
290            _ => None,
291        }
292    }
293
294    /// Get the error code if this is an API error
295    ///
296    /// Returns the BingErrorCode if recognized, otherwise returns the raw error code value
297    pub fn error_code(&self) -> Option<std::result::Result<BingErrorCode, i32>> {
298        match self {
299            Self::ApiError {
300                error_code,
301                error_code_raw,
302                ..
303            } => Some(error_code.ok_or(*error_code_raw)),
304            _ => None,
305        }
306    }
307
308    /// Get the error message if this is an API error
309    pub fn api_message(&self) -> Option<&str> {
310        match self {
311            Self::ApiError { message, .. } => Some(message.as_str()),
312            _ => None,
313        }
314    }
315}
316
317/// Result type alias for Bing Webmaster API operations
318pub type Result<T> = std::result::Result<T, WebmasterApiError>;
319
320/// Helper function to map HTTP status codes to appropriate errors
321pub fn map_status_error(status: reqwest::StatusCode, response_text: String) -> WebmasterApiError {
322    let status_code = status.as_u16();
323    let message = match status_code {
324        400 => "Bad Request - The request is invalid or malformed",
325        401 => "Unauthorized - Invalid or missing API key",
326        403 => "Forbidden - Access denied to the requested resource",
327        404 => "Not Found - The requested resource was not found",
328        429 => "Too Many Requests - Rate limit exceeded",
329        500 => "Internal Server Error - Server encountered an error",
330        502 => "Bad Gateway - Invalid response from upstream server",
331        503 => "Service Unavailable - Service temporarily unavailable",
332        504 => "Gateway Timeout - Request timeout",
333        _ => "HTTP request failed",
334    };
335
336    match status_code {
337        401 | 403 => WebmasterApiError::AuthenticationError,
338        404 => WebmasterApiError::InvalidResponse(format!("{}: {}", message, response_text)),
339        _ => WebmasterApiError::http_status_with_body(status_code, message, response_text),
340    }
341}
342
343/// Helper function to parse API error responses from response text
344///
345/// Returns a tuple of (error_code, message) if parsing succeeds
346pub fn try_parse_api_error(response_text: &str) -> Option<(i32, String)> {
347    // Try to parse as JSON response wrapper first
348    if let Ok(wrapper) =
349        serde_json::from_str::<crate::dto::ResponseWrapper<ApiErrorResponse>>(response_text)
350    {
351        return Some((wrapper.d.error_code.value(), wrapper.d.message));
352    }
353
354    // Try to parse as direct ApiErrorResponse
355    serde_json::from_str::<ApiErrorResponse>(response_text)
356        .ok()
357        .map(|r| (r.error_code.value(), r.message))
358}