Skip to main content

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}