asana_cli/api/error.rs
1//! Error types for the Asana API client.
2
3use reqwest::StatusCode;
4use serde_json::Value;
5use std::time::Duration;
6use thiserror::Error;
7
8/// Structured information about Asana rate-limit headers.
9#[derive(Debug, Clone)]
10pub struct RateLimitInfo {
11 /// Total allowed requests in the current window.
12 pub limit: Option<u32>,
13 /// Remaining requests available before throttling.
14 pub remaining: Option<u32>,
15 /// Epoch seconds when the quota resets, if supplied by the API.
16 pub reset: Option<u64>,
17 /// Suggested delay before retrying (for 429 responses).
18 pub retry_after: Option<Duration>,
19}
20
21/// Errors that can occur while interacting with the Asana API.
22#[derive(Debug, Error)]
23pub enum ApiError {
24 /// General networking failure.
25 #[error("network error: {0}")]
26 Network(#[from] reqwest::Error),
27 /// Response payload could not be deserialised.
28 #[error("failed to parse response: {0}")]
29 Deserialize(#[from] serde_json::Error),
30 /// HTTP status code returned an error.
31 #[error("HTTP {status}: {message}")]
32 Http {
33 /// HTTP status returned by Asana.
34 status: StatusCode,
35 /// Message extracted from the response body or canonical reason.
36 message: String,
37 /// Optional structured payload returned alongside the error.
38 details: Option<Value>,
39 },
40 /// Authentication failed (401/403).
41 #[error("authentication failed: {0}")]
42 Authentication(String),
43 /// Rate limit was hit and retries exhausted.
44 #[error("rate limited after {retry_after:?}: {body}")]
45 RateLimited {
46 /// Recommended wait duration before retrying.
47 retry_after: Duration,
48 /// Raw response body returned with the 429.
49 body: String,
50 },
51 /// Cache layer failure.
52 #[error("cache error: {0}")]
53 Cache(#[from] std::io::Error),
54 /// Offline mode requested data that was not cached.
55 #[error("offline mode enabled and no cached response available for {resource}")]
56 Offline {
57 /// Resource identifier, typically the request path.
58 resource: String,
59 },
60 /// Request could not be cloned for retry attempts.
61 #[error("request could not be cloned for retry")]
62 UnclonableRequest,
63 /// Catch-all error message.
64 #[error("{0}")]
65 Other(String),
66}
67
68impl ApiError {
69 /// Convenience constructor for HTTP errors with an optional JSON payload.
70 #[must_use]
71 pub const fn http(status: StatusCode, message: String, details: Option<Value>) -> Self {
72 Self::Http {
73 status,
74 message,
75 details,
76 }
77 }
78}