yandex_webmaster_api/
error.rs

1use serde::{Deserialize, Serialize};
2use std::fmt;
3use thiserror::Error;
4
5/// Yandex API error codes
6///
7/// This enum represents all possible error codes that can be returned by the Yandex Webmaster API.
8/// Each variant corresponds to a specific error condition documented in the API specification.
9#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
10#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
11pub enum YandexErrorCode {
12    // 400 Bad Request
13    EmptyDates,
14    EmptyPaths,
15    EntityValidationError,
16    FieldValidationError,
17    InvalidUrl,
18    NoChanges,
19    SomeDatesAreUnavailable,
20    UrlsAreCorrupted,
21    WrongRegion,
22
23    // 403 Forbidden
24    AccessForbidden,
25    InvalidOauthToken,
26    InvalidUserId,
27    HostsLimitExceeded,
28    FeedsLimitExceeded,
29    BatchLimitExceeded,
30    FeedsCategoryBan,
31    LimitsExceeded,
32
33    // 404 Not Found
34    ResourceNotFound,
35    HostNotIndexed,
36    HostNotLoaded,
37    HostNotVerified,
38    HostNotFound,
39    SitemapNotFound,
40    SitemapNotAdded,
41    TaskNotFound,
42    QueryIdNotFound,
43    BadHttpCode,
44    BadMimeType,
45    RequestNotFound,
46    TimedOut,
47    FeedAlreadyAdded,
48    OnlyHttps,
49    ManyUrlsForRemove,
50    IncorrectUrl,
51    NotExist,
52
53    // 405 Method Not Allowed
54    MethodNotAllowed,
55
56    // 406 Not Acceptable
57    ContentTypeUnsupported,
58
59    // 409 Conflict
60    UrlAlreadyAdded,
61    HostAlreadyAdded,
62    VerificationAlreadyInProgress,
63    TextAlreadyAdded,
64    SitemapAlreadyAdded,
65
66    // 410 Gone
67    UploadAddressExpired,
68
69    // 413 Request Entity Too Large
70    RequestEntityTooLarge,
71    PayloadTooLarge,
72
73    // 415 Unsupported Media Type
74    ContentEncodingUnsupported,
75
76    // 422 Unprocessable Entity
77    TextLengthConstraintsViolation,
78    NoVerificationRecord,
79
80    // 429 Too Many Requests
81    QuotaExceeded,
82    TooManyRequestsError,
83
84    /// Unknown error code not listed in the API documentation
85    #[serde(untagged)]
86    Unknown(String),
87}
88
89impl fmt::Display for YandexErrorCode {
90    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
91        match self {
92            YandexErrorCode::Unknown(s) => write!(f, "{}", s),
93            _ => {
94                let json = serde_json::to_string(self).unwrap_or_else(|_| "UNKNOWN".to_string());
95                write!(f, "{}", json.trim_matches('"'))
96            }
97        }
98    }
99}
100
101/// Response structure for Yandex API errors
102///
103/// This struct represents the error response format returned by the Yandex Webmaster API.
104/// It includes the error code and a human-readable error message.
105#[derive(Debug, Clone, Serialize, Deserialize)]
106pub struct YandexApiErrorResponse {
107    /// Error code identifying the specific error condition
108    pub error_code: YandexErrorCode,
109
110    /// Human-readable error message providing additional context
111    pub error_message: String,
112
113    /// Optional list of acceptable content types (for 406 errors)
114    #[serde(skip_serializing_if = "Option::is_none")]
115    pub acceptable_types: Option<Vec<String>>,
116
117    /// Optional expiration date (for 410 errors)
118    #[serde(skip_serializing_if = "Option::is_none")]
119    pub valid_until: Option<String>,
120}
121
122/// Errors that can occur when interacting with the Yandex Webmaster API
123#[derive(Debug, Error)]
124pub enum YandexWebmasterError {
125    /// HTTP request failed
126    #[error("HTTP request failed: {0}")]
127    HttpError(#[from] reqwest::Error),
128
129    /// Middleware request failed
130    #[error("Middleware request failed: {0}")]
131    MiddlewareHttpError(#[from] reqwest_middleware::Error),
132
133    /// Failed to parse response
134    #[error("Failed to parse response: {0}")]
135    ParseError(#[from] serde_json::Error),
136
137    /// Failed to serialize url
138    #[error("Failed serialize url: {0}")]
139    SerdeQsError(#[from] serde_qs::Error),
140
141    /// Middleware error
142    #[error("Middleware error: {0}")]
143    MiddlewareError(String),
144
145    /// Authentication failed
146    #[error("Authentication failed: missing or invalid OAuth token")]
147    AuthenticationError,
148
149    /// API returned a structured error (RFC 7807 compliant)
150    #[error("API error ({error_code}): {error_message}", error_code = .response.error_code, error_message = .response.error_message)]
151    ApiError {
152        /// HTTP status code
153        status: u16,
154        /// Yandex API error response
155        response: YandexApiErrorResponse,
156    },
157
158    /// API returned an unstructured error
159    #[error("API error: {0}")]
160    GenericApiError(String),
161}
162
163/// Result type alias for Yandex Webmaster API operations
164pub type Result<T> = std::result::Result<T, YandexWebmasterError>;
165
166#[cfg(test)]
167mod tests {
168    use super::*;
169
170    #[test]
171    fn test_parse_entity_validation_error() {
172        let json = r#"{
173            "error_code": "ENTITY_VALIDATION_ERROR",
174            "error_message": "some string"
175        }"#;
176
177        let result: YandexApiErrorResponse = serde_json::from_str(json).unwrap();
178        assert_eq!(result.error_code, YandexErrorCode::EntityValidationError);
179        assert_eq!(result.error_message, "some string");
180        assert!(result.acceptable_types.is_none());
181        assert!(result.valid_until.is_none());
182    }
183
184    #[test]
185    fn test_parse_invalid_url() {
186        let json = r#"{
187            "error_code": "INVALID_URL",
188            "error_message": "some string"
189        }"#;
190
191        let result: YandexApiErrorResponse = serde_json::from_str(json).unwrap();
192        assert_eq!(result.error_code, YandexErrorCode::InvalidUrl);
193        assert_eq!(result.error_message, "some string");
194    }
195
196    #[test]
197    fn test_parse_no_changes() {
198        let json = r#"{
199            "error_code": "NO_CHANGES",
200            "error_message": "some string"
201        }"#;
202
203        let result: YandexApiErrorResponse = serde_json::from_str(json).unwrap();
204        assert_eq!(result.error_code, YandexErrorCode::NoChanges);
205        assert_eq!(result.error_message, "some string");
206    }
207
208    #[test]
209    fn test_parse_wrong_region() {
210        let json = r#"{
211            "error_code": "WRONG_REGION",
212            "error_message": "some string"
213        }"#;
214
215        let result: YandexApiErrorResponse = serde_json::from_str(json).unwrap();
216        assert_eq!(result.error_code, YandexErrorCode::WrongRegion);
217        assert_eq!(result.error_message, "some string");
218    }
219
220    #[test]
221    fn test_parse_access_forbidden() {
222        let json = r#"{
223            "error_code": "ACCESS_FORBIDDEN",
224            "error_message": "explicit error message"
225        }"#;
226
227        let result: YandexApiErrorResponse = serde_json::from_str(json).unwrap();
228        assert_eq!(result.error_code, YandexErrorCode::AccessForbidden);
229        assert_eq!(result.error_message, "explicit error message");
230    }
231
232    #[test]
233    fn test_parse_invalid_oauth_token() {
234        let json = r#"{
235            "error_code": "INVALID_OAUTH_TOKEN",
236            "error_message": "explicit error message"
237        }"#;
238
239        let result: YandexApiErrorResponse = serde_json::from_str(json).unwrap();
240        assert_eq!(result.error_code, YandexErrorCode::InvalidOauthToken);
241        assert_eq!(result.error_message, "explicit error message");
242    }
243
244    #[test]
245    fn test_parse_hosts_limit_exceeded() {
246        let json = r#"{
247            "error_code": "HOSTS_LIMIT_EXCEEDED",
248            "limit": 1,
249            "error_message": "explicit error message"
250        }"#;
251
252        let result: YandexApiErrorResponse = serde_json::from_str(json).unwrap();
253        assert_eq!(result.error_code, YandexErrorCode::HostsLimitExceeded);
254        assert_eq!(result.error_message, "explicit error message");
255    }
256
257    #[test]
258    fn test_parse_feeds_limit_exceeded() {
259        let json = r#"{
260            "error_code": "FEEDS_LIMIT_EXCEEDED",
261            "limit": 1,
262            "error_message": "explicit error message"
263        }"#;
264
265        let result: YandexApiErrorResponse = serde_json::from_str(json).unwrap();
266        assert_eq!(result.error_code, YandexErrorCode::FeedsLimitExceeded);
267        assert_eq!(result.error_message, "explicit error message");
268    }
269
270    #[test]
271    fn test_parse_resource_not_found() {
272        let json = r#"{
273            "error_code": "RESOURCE_NOT_FOUND",
274            "error_message": "some string"
275        }"#;
276
277        let result: YandexApiErrorResponse = serde_json::from_str(json).unwrap();
278        assert_eq!(result.error_code, YandexErrorCode::ResourceNotFound);
279        assert_eq!(result.error_message, "some string");
280    }
281
282    #[test]
283    fn test_parse_host_not_indexed() {
284        let json = r#"{
285            "error_code": "HOST_NOT_INDEXED",
286            "host_id": "http:ya.ru:80",
287            "error_message": "some string"
288        }"#;
289
290        let result: YandexApiErrorResponse = serde_json::from_str(json).unwrap();
291        assert_eq!(result.error_code, YandexErrorCode::HostNotIndexed);
292        assert_eq!(result.error_message, "some string");
293    }
294
295    #[test]
296    fn test_parse_host_not_found() {
297        let json = r#"{
298            "error_code": "HOST_NOT_FOUND",
299            "host_id": "http:ya.ru:80",
300            "error_message": "explicit error message"
301        }"#;
302
303        let result: YandexApiErrorResponse = serde_json::from_str(json).unwrap();
304        assert_eq!(result.error_code, YandexErrorCode::HostNotFound);
305        assert_eq!(result.error_message, "explicit error message");
306    }
307
308    #[test]
309    fn test_parse_sitemap_not_found() {
310        let json = r#"{
311            "error_code": "SITEMAP_NOT_FOUND",
312            "host_id": "http:ya.ru:80",
313            "sitemap_id": "c7-fe:80-c0",
314            "error_message": "some string"
315        }"#;
316
317        let result: YandexApiErrorResponse = serde_json::from_str(json).unwrap();
318        assert_eq!(result.error_code, YandexErrorCode::SitemapNotFound);
319        assert_eq!(result.error_message, "some string");
320    }
321
322    #[test]
323    fn test_parse_task_not_found() {
324        let json = r#"{
325            "error_code": "TASK_NOT_FOUND",
326            "error_message": "some string"
327        }"#;
328
329        let result: YandexApiErrorResponse = serde_json::from_str(json).unwrap();
330        assert_eq!(result.error_code, YandexErrorCode::TaskNotFound);
331        assert_eq!(result.error_message, "some string");
332    }
333
334    #[test]
335    fn test_parse_query_id_not_found() {
336        let json = r#"{
337            "error_code": "QUERY_ID_NOT_FOUND",
338            "error_message": "some string"
339        }"#;
340
341        let result: YandexApiErrorResponse = serde_json::from_str(json).unwrap();
342        assert_eq!(result.error_code, YandexErrorCode::QueryIdNotFound);
343        assert_eq!(result.error_message, "some string");
344    }
345
346    #[test]
347    fn test_parse_method_not_allowed() {
348        let json = r#"{
349            "error_code": "METHOD_NOT_ALLOWED",
350            "error_message": "explicit error message"
351        }"#;
352
353        let result: YandexApiErrorResponse = serde_json::from_str(json).unwrap();
354        assert_eq!(result.error_code, YandexErrorCode::MethodNotAllowed);
355        assert_eq!(result.error_message, "explicit error message");
356    }
357
358    #[test]
359    fn test_parse_content_type_unsupported_with_acceptable_types() {
360        let json = r#"{
361            "error_code": "CONTENT_TYPE_UNSUPPORTED",
362            "acceptable_types": [
363                "application/json",
364                "application/xml"
365            ],
366            "error_message": "explicit error message"
367        }"#;
368
369        let result: YandexApiErrorResponse = serde_json::from_str(json).unwrap();
370        assert_eq!(result.error_code, YandexErrorCode::ContentTypeUnsupported);
371        assert_eq!(result.error_message, "explicit error message");
372        assert_eq!(
373            result.acceptable_types,
374            Some(vec![
375                "application/json".to_string(),
376                "application/xml".to_string()
377            ])
378        );
379    }
380
381    #[test]
382    fn test_parse_url_already_added() {
383        let json = r#"{
384            "error_code": "URL_ALREADY_ADDED",
385            "error_message": "some string"
386        }"#;
387
388        let result: YandexApiErrorResponse = serde_json::from_str(json).unwrap();
389        assert_eq!(result.error_code, YandexErrorCode::UrlAlreadyAdded);
390        assert_eq!(result.error_message, "some string");
391    }
392
393    #[test]
394    fn test_parse_host_already_added() {
395        let json = r#"{
396            "error_code": "HOST_ALREADY_ADDED",
397            "host_id": "http:ya.ru:80",
398            "verified": false,
399            "error_message": "some string"
400        }"#;
401
402        let result: YandexApiErrorResponse = serde_json::from_str(json).unwrap();
403        assert_eq!(result.error_code, YandexErrorCode::HostAlreadyAdded);
404        assert_eq!(result.error_message, "some string");
405    }
406
407    #[test]
408    fn test_parse_verification_already_in_progress() {
409        let json = r#"{
410            "error_code": "VERIFICATION_ALREADY_IN_PROGRESS",
411            "verification_type": "META_TAG",
412            "error_message": "some string"
413        }"#;
414
415        let result: YandexApiErrorResponse = serde_json::from_str(json).unwrap();
416        assert_eq!(
417            result.error_code,
418            YandexErrorCode::VerificationAlreadyInProgress
419        );
420        assert_eq!(result.error_message, "some string");
421    }
422
423    #[test]
424    fn test_parse_sitemap_already_added() {
425        let json = r#"{
426            "error_code": "SITEMAP_ALREADY_ADDED",
427            "sitemap_id": "c7-fe:80-c0",
428            "error_message": "some string"
429        }"#;
430
431        let result: YandexApiErrorResponse = serde_json::from_str(json).unwrap();
432        assert_eq!(result.error_code, YandexErrorCode::SitemapAlreadyAdded);
433        assert_eq!(result.error_message, "some string");
434    }
435
436    #[test]
437    fn test_parse_upload_address_expired_with_valid_until() {
438        let json = r#"{
439            "error_code": "UPLOAD_ADDRESS_EXPIRED",
440            "valid_until": "2016-01-01T00:00:00,000+0300",
441            "error_message": "some string"
442        }"#;
443
444        let result: YandexApiErrorResponse = serde_json::from_str(json).unwrap();
445        assert_eq!(result.error_code, YandexErrorCode::UploadAddressExpired);
446        assert_eq!(result.error_message, "some string");
447        assert_eq!(
448            result.valid_until,
449            Some("2016-01-01T00:00:00,000+0300".to_string())
450        );
451    }
452
453    #[test]
454    fn test_parse_request_entity_too_large() {
455        let json = r#"{
456            "error_code": "REQUEST_ENTITY_TOO_LARGE",
457            "error_message": "some string"
458        }"#;
459
460        let result: YandexApiErrorResponse = serde_json::from_str(json).unwrap();
461        assert_eq!(result.error_code, YandexErrorCode::RequestEntityTooLarge);
462        assert_eq!(result.error_message, "some string");
463    }
464
465    #[test]
466    fn test_parse_content_encoding_unsupported() {
467        let json = r#"{
468            "error_code": "CONTENT_ENCODING_UNSUPPORTED",
469            "error_message": "some string"
470        }"#;
471
472        let result: YandexApiErrorResponse = serde_json::from_str(json).unwrap();
473        assert_eq!(
474            result.error_code,
475            YandexErrorCode::ContentEncodingUnsupported
476        );
477        assert_eq!(result.error_message, "some string");
478    }
479
480    #[test]
481    fn test_parse_quota_exceeded() {
482        let json = r#"{
483            "error_code": "QUOTA_EXCEEDED",
484            "error_message": "some string"
485        }"#;
486
487        let result: YandexApiErrorResponse = serde_json::from_str(json).unwrap();
488        assert_eq!(result.error_code, YandexErrorCode::QuotaExceeded);
489        assert_eq!(result.error_message, "some string");
490    }
491
492    #[test]
493    fn test_parse_too_many_requests() {
494        let json = r#"{
495            "error_code": "TOO_MANY_REQUESTS_ERROR",
496            "error_message": "some string"
497        }"#;
498
499        let result: YandexApiErrorResponse = serde_json::from_str(json).unwrap();
500        assert_eq!(result.error_code, YandexErrorCode::TooManyRequestsError);
501        assert_eq!(result.error_message, "some string");
502    }
503
504    #[test]
505    fn test_parse_unknown_error_code() {
506        let json = r#"{
507            "error_code": "SOME_UNKNOWN_ERROR",
508            "error_message": "unknown error occurred"
509        }"#;
510
511        let result: YandexApiErrorResponse = serde_json::from_str(json).unwrap();
512        assert_eq!(
513            result.error_code,
514            YandexErrorCode::Unknown("SOME_UNKNOWN_ERROR".to_string())
515        );
516        assert_eq!(result.error_message, "unknown error occurred");
517    }
518
519    #[test]
520    fn test_error_code_display() {
521        assert_eq!(YandexErrorCode::InvalidUrl.to_string(), "INVALID_URL");
522        assert_eq!(YandexErrorCode::HostNotFound.to_string(), "HOST_NOT_FOUND");
523        assert_eq!(
524            YandexErrorCode::Unknown("CUSTOM_ERROR".to_string()).to_string(),
525            "CUSTOM_ERROR"
526        );
527    }
528
529    #[test]
530    fn test_error_display() {
531        let error = YandexWebmasterError::ApiError {
532            status: 404,
533            response: YandexApiErrorResponse {
534                error_code: YandexErrorCode::HostNotFound,
535                error_message: "Host not found in user's list".to_string(),
536                acceptable_types: None,
537                valid_until: None,
538            },
539        };
540
541        let error_string = error.to_string();
542        assert!(error_string.contains("HOST_NOT_FOUND"));
543        assert!(error_string.contains("Host not found in user's list"));
544    }
545
546    #[test]
547    fn test_parse_with_extra_fields_ignored() {
548        // Test that extra fields in the JSON are ignored
549        let json = r#"{
550            "error_code": "HOST_NOT_FOUND",
551            "host_id": "http:example.com:80",
552            "extra_field": "should be ignored",
553            "error_message": "Host not found"
554        }"#;
555
556        let result: YandexApiErrorResponse = serde_json::from_str(json).unwrap();
557        assert_eq!(result.error_code, YandexErrorCode::HostNotFound);
558        assert_eq!(result.error_message, "Host not found");
559    }
560}