alpaca_data/
error.rs

1use std::fmt::{self, Display, Formatter};
2
3use crate::transport::meta::ResponseMeta;
4
5const MAX_ERROR_BODY_CHARS: usize = 256;
6
7#[derive(Clone, Debug, Eq, PartialEq)]
8pub enum Error {
9    InvalidConfiguration(String),
10    MissingCredentials,
11    Transport(String),
12    Timeout(String),
13    RateLimited {
14        endpoint: &'static str,
15        retry_after: Option<u64>,
16        request_id: Option<String>,
17        attempt_count: u32,
18        body: Option<String>,
19    },
20    HttpStatus {
21        endpoint: &'static str,
22        status: u16,
23        request_id: Option<String>,
24        attempt_count: u32,
25        body: Option<String>,
26    },
27    Deserialize(String),
28    InvalidRequest(String),
29    Pagination(String),
30    NotImplemented {
31        operation: &'static str,
32    },
33}
34
35impl Display for Error {
36    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
37        match self {
38            Self::InvalidConfiguration(message) => {
39                write!(f, "invalid configuration: {message}")
40            }
41            Self::MissingCredentials => write!(f, "missing credentials"),
42            Self::Transport(message) => write!(f, "transport error: {message}"),
43            Self::Timeout(message) => write!(f, "timeout error: {message}"),
44            Self::RateLimited {
45                endpoint,
46                retry_after,
47                request_id,
48                attempt_count,
49                body,
50            } => write_transport_error(
51                f,
52                "rate limited",
53                *endpoint,
54                Some(("retry_after", retry_after.map(|value| value.to_string()))),
55                request_id.as_deref(),
56                *attempt_count,
57                body.as_deref(),
58            ),
59            Self::HttpStatus {
60                endpoint,
61                status,
62                request_id,
63                attempt_count,
64                body,
65            } => write_transport_error(
66                f,
67                "http status error",
68                *endpoint,
69                Some(("status", Some(status.to_string()))),
70                request_id.as_deref(),
71                *attempt_count,
72                body.as_deref(),
73            ),
74            Self::Deserialize(message) => write!(f, "deserialize error: {message}"),
75            Self::InvalidRequest(message) => write!(f, "invalid request: {message}"),
76            Self::Pagination(message) => write!(f, "pagination error: {message}"),
77            Self::NotImplemented { operation } => {
78                write!(f, "operation not implemented: {operation}")
79            }
80        }
81    }
82}
83
84impl std::error::Error for Error {}
85
86impl Error {
87    pub(crate) fn from_rate_limited(meta: ResponseMeta, body: String) -> Self {
88        Self::RateLimited {
89            endpoint: meta.endpoint_name,
90            retry_after: meta.retry_after.map(|value| value.as_secs()),
91            request_id: meta.request_id,
92            attempt_count: meta.attempt_count,
93            body: snippet_body(body),
94        }
95    }
96
97    pub(crate) fn from_http_status(meta: ResponseMeta, body: String) -> Self {
98        Self::HttpStatus {
99            endpoint: meta.endpoint_name,
100            status: meta.status,
101            request_id: meta.request_id,
102            attempt_count: meta.attempt_count,
103            body: snippet_body(body),
104        }
105    }
106
107    pub(crate) fn from_reqwest(error: reqwest::Error) -> Self {
108        if error.is_timeout() {
109            Self::Timeout(error.to_string())
110        } else {
111            Self::Transport(error.to_string())
112        }
113    }
114
115    pub fn endpoint(&self) -> Option<&str> {
116        match self {
117            Self::RateLimited { endpoint, .. } | Self::HttpStatus { endpoint, .. } => {
118                Some(endpoint)
119            }
120            _ => None,
121        }
122    }
123
124    pub fn request_id(&self) -> Option<&str> {
125        match self {
126            Self::RateLimited { request_id, .. } | Self::HttpStatus { request_id, .. } => {
127                request_id.as_deref()
128            }
129            _ => None,
130        }
131    }
132}
133
134fn write_transport_error(
135    f: &mut Formatter<'_>,
136    label: &str,
137    endpoint: &'static str,
138    primary_field: Option<(&str, Option<String>)>,
139    request_id: Option<&str>,
140    attempt_count: u32,
141    body: Option<&str>,
142) -> fmt::Result {
143    write!(f, "{label}: endpoint={endpoint}")?;
144
145    if let Some((field_name, Some(field_value))) = primary_field {
146        write!(f, ", {field_name}={field_value}")?;
147    }
148
149    if let Some(request_id) = request_id {
150        write!(f, ", request_id={request_id}")?;
151    }
152
153    write!(f, ", attempt_count={attempt_count}")?;
154
155    if let Some(body) = body {
156        write!(f, ", body={body}")?;
157    }
158
159    Ok(())
160}
161
162fn snippet_body(body: String) -> Option<String> {
163    if body.is_empty() {
164        return None;
165    }
166
167    let mut snippet: String = body.chars().take(MAX_ERROR_BODY_CHARS).collect();
168    if snippet.len() < body.len() {
169        snippet.push_str("...");
170    }
171
172    Some(snippet)
173}