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}