Skip to main content

alpaca_http/
meta.rs

1use std::time::Duration;
2
3use reqwest::{
4    StatusCode,
5    header::{HeaderMap, HeaderName, RETRY_AFTER},
6};
7
8const MAX_BODY_SNIPPET_CHARS: usize = 256;
9
10#[derive(Clone, Debug, PartialEq, Eq)]
11pub struct ResponseMeta {
12    operation: Option<String>,
13    url: String,
14    status: u16,
15    request_id: Option<String>,
16    attempt_count: u32,
17    elapsed: Duration,
18    retry_after: Option<Duration>,
19}
20
21impl ResponseMeta {
22    #[must_use]
23    pub fn from_response_parts(
24        operation: Option<String>,
25        url: String,
26        status: StatusCode,
27        headers: &HeaderMap,
28        request_id_header: &HeaderName,
29        attempt_count: u32,
30        elapsed: Duration,
31    ) -> Self {
32        Self {
33            operation,
34            url,
35            status: status.as_u16(),
36            request_id: parse_header_string(headers, request_id_header),
37            attempt_count,
38            elapsed,
39            retry_after: parse_retry_after(headers),
40        }
41    }
42
43    #[must_use]
44    pub fn operation(&self) -> Option<&str> {
45        self.operation.as_deref()
46    }
47
48    #[must_use]
49    pub fn url(&self) -> &str {
50        &self.url
51    }
52
53    #[must_use]
54    pub fn status(&self) -> u16 {
55        self.status
56    }
57
58    #[must_use]
59    pub fn status_code(&self) -> StatusCode {
60        StatusCode::from_u16(self.status).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR)
61    }
62
63    #[must_use]
64    pub fn request_id(&self) -> Option<&str> {
65        self.request_id.as_deref()
66    }
67
68    #[must_use]
69    pub fn attempt_count(&self) -> u32 {
70        self.attempt_count
71    }
72
73    #[must_use]
74    pub fn elapsed(&self) -> Duration {
75        self.elapsed
76    }
77
78    #[must_use]
79    pub fn retry_after(&self) -> Option<Duration> {
80        self.retry_after
81    }
82}
83
84#[derive(Clone, Debug, PartialEq, Eq)]
85pub struct ErrorMeta {
86    operation: Option<String>,
87    url: String,
88    status: u16,
89    request_id: Option<String>,
90    attempt_count: u32,
91    elapsed: Duration,
92    retry_after: Option<Duration>,
93    body_snippet: Option<String>,
94}
95
96impl ErrorMeta {
97    #[must_use]
98    pub fn from_response_meta(meta: ResponseMeta, body: impl Into<String>) -> Self {
99        Self {
100            operation: meta.operation,
101            url: meta.url,
102            status: meta.status,
103            request_id: meta.request_id,
104            attempt_count: meta.attempt_count,
105            elapsed: meta.elapsed,
106            retry_after: meta.retry_after,
107            body_snippet: snippet_body(body.into()),
108        }
109    }
110
111    #[must_use]
112    pub fn operation(&self) -> Option<&str> {
113        self.operation.as_deref()
114    }
115
116    #[must_use]
117    pub fn url(&self) -> &str {
118        &self.url
119    }
120
121    #[must_use]
122    pub fn status(&self) -> u16 {
123        self.status
124    }
125
126    #[must_use]
127    pub fn request_id(&self) -> Option<&str> {
128        self.request_id.as_deref()
129    }
130
131    #[must_use]
132    pub fn attempt_count(&self) -> u32 {
133        self.attempt_count
134    }
135
136    #[must_use]
137    pub fn elapsed(&self) -> Duration {
138        self.elapsed
139    }
140
141    #[must_use]
142    pub fn retry_after(&self) -> Option<Duration> {
143        self.retry_after
144    }
145
146    #[must_use]
147    pub fn body_snippet(&self) -> Option<&str> {
148        self.body_snippet.as_deref()
149    }
150}
151
152#[derive(Clone, Debug, PartialEq, Eq)]
153pub struct HttpResponse<T> {
154    body: T,
155    meta: ResponseMeta,
156}
157
158impl<T> HttpResponse<T> {
159    #[must_use]
160    pub fn new(body: T, meta: ResponseMeta) -> Self {
161        Self { body, meta }
162    }
163
164    #[must_use]
165    pub fn body(&self) -> &T {
166        &self.body
167    }
168
169    #[must_use]
170    pub fn meta(&self) -> &ResponseMeta {
171        &self.meta
172    }
173
174    #[must_use]
175    pub fn into_body(self) -> T {
176        self.body
177    }
178
179    #[must_use]
180    pub fn into_parts(self) -> (T, ResponseMeta) {
181        (self.body, self.meta)
182    }
183}
184
185fn parse_header_string(headers: &HeaderMap, name: &HeaderName) -> Option<String> {
186    headers
187        .get(name)
188        .and_then(|value| value.to_str().ok())
189        .map(ToOwned::to_owned)
190}
191
192fn parse_retry_after(headers: &HeaderMap) -> Option<Duration> {
193    headers
194        .get(RETRY_AFTER)
195        .and_then(|value| value.to_str().ok())
196        .and_then(|value| value.parse::<u64>().ok())
197        .map(Duration::from_secs)
198}
199
200fn snippet_body(body: String) -> Option<String> {
201    if body.is_empty() {
202        return None;
203    }
204
205    let mut snippet: String = body.chars().take(MAX_BODY_SNIPPET_CHARS).collect();
206    if snippet.len() < body.len() {
207        snippet.push_str("...");
208    }
209
210    Some(snippet)
211}