1use std::marker::PhantomData;
2
3use reqwest::Response;
4use serde::de::DeserializeOwned;
5
6use crate::client::{retry_after_header, HttpClient};
7use crate::ApiError;
8
9pub trait QueryBuilder: Sized {
11 fn add_query(&mut self, key: String, value: String);
13
14 fn query(mut self, key: impl Into<String>, value: impl ToString) -> Self {
16 self.add_query(key.into(), value.to_string());
17 self
18 }
19
20 fn query_opt(mut self, key: impl Into<String>, value: Option<impl ToString>) -> Self {
22 if let Some(v) = value {
23 self.add_query(key.into(), v.to_string());
24 }
25 self
26 }
27
28 fn query_many<I, V>(self, key: impl Into<String>, values: I) -> Self
30 where
31 I: IntoIterator<Item = V>,
32 V: ToString,
33 {
34 let key = key.into();
35 let mut result = self;
36 for value in values {
37 result.add_query(key.clone(), value.to_string());
38 }
39 result
40 }
41
42 fn query_many_opt<I, V>(self, key: impl Into<String>, values: Option<I>) -> Self
44 where
45 I: IntoIterator<Item = V>,
46 V: ToString,
47 {
48 if let Some(values) = values {
49 self.query_many(key, values)
50 } else {
51 self
52 }
53 }
54}
55
56pub trait RequestError: From<ApiError> + std::fmt::Debug {
58 fn from_response(response: Response) -> impl std::future::Future<Output = Self> + Send;
60}
61
62pub struct Request<T, E> {
64 pub(crate) http_client: HttpClient,
65 pub(crate) path: String,
66 pub(crate) query: Vec<(String, String)>,
67 pub(crate) _marker: PhantomData<(T, E)>,
68}
69
70impl<T, E> Request<T, E> {
71 pub fn new(http_client: HttpClient, path: impl Into<String>) -> Self {
73 Self {
74 http_client,
75 path: path.into(),
76 query: Vec::new(),
77 _marker: PhantomData,
78 }
79 }
80}
81
82impl<T, E> Clone for Request<T, E> {
87 fn clone(&self) -> Self {
88 Self {
89 http_client: self.http_client.clone(),
90 path: self.path.clone(),
91 query: self.query.clone(),
92 _marker: PhantomData,
93 }
94 }
95}
96
97impl<T, E> QueryBuilder for Request<T, E> {
98 fn add_query(&mut self, key: String, value: String) {
99 self.query.push((key, value));
100 }
101}
102
103impl<T: DeserializeOwned, E: RequestError> Request<T, E> {
104 pub async fn send(self) -> Result<T, E> {
106 let response = self.send_raw().await?;
107
108 let text = response
110 .text()
111 .await
112 .map_err(|e| E::from(ApiError::from(e)))?;
113
114 serde_json::from_str(&text).map_err(|e| {
116 tracing::error!("Deserialization failed: {}", e);
117 tracing::error!("Failed to deserialize: {}", crate::truncate_for_log(&text));
118 E::from(ApiError::from(e))
119 })
120 }
121
122 pub async fn send_raw(self) -> Result<Response, E> {
124 let url = self
125 .http_client
126 .base_url
127 .join(&self.path)
128 .map_err(|e| E::from(ApiError::from(e)))?;
129
130 let http_client = self.http_client;
131 let query = self.query;
132 let path = self.path;
133 let mut attempt = 0u32;
134
135 loop {
136 let _permit = http_client.acquire_concurrency().await;
137 http_client.acquire_rate_limit(&path, None).await;
138
139 let mut request = http_client.client.get(url.clone());
140
141 if !query.is_empty() {
142 request = request.query(&query);
143 }
144
145 let response = request
146 .send()
147 .await
148 .map_err(|e| E::from(ApiError::from(e)))?;
149 let status = response.status();
150 let retry_after = retry_after_header(&response);
151
152 http_client.note_rate_limited(status, retry_after.as_deref());
156
157 if let Some(backoff) = http_client.should_retry(status, attempt, retry_after.as_deref())
158 {
159 attempt += 1;
160 tracing::warn!(
161 "Retriable status {} on {}, retry {} after {}ms",
162 status,
163 path,
164 attempt,
165 backoff.as_millis()
166 );
167 drop(_permit);
168 tokio::time::sleep(backoff).await;
169 continue;
170 }
171
172 tracing::debug!("Response status: {}", status);
173
174 if !status.is_success() {
175 let error = E::from_response(response).await;
176 tracing::error!("Request failed: {:?}", error);
177 return Err(error);
178 }
179
180 return Ok(response);
181 }
182 }
183}
184
185pub struct TypedRequest<T> {
187 pub(crate) _marker: PhantomData<T>,
188}
189
190impl<T> TypedRequest<T> {
191 pub fn new() -> Self {
193 Self {
194 _marker: PhantomData,
195 }
196 }
197}
198
199impl<T> Default for TypedRequest<T> {
200 fn default() -> Self {
201 Self::new()
202 }
203}
204
205#[cfg(test)]
206mod tests {
207 use super::*;
208 use crate::HttpClientBuilder;
209
210 fn make_request() -> Request<(), ApiError> {
214 let http = HttpClientBuilder::new("https://example.com")
215 .build()
216 .unwrap();
217 Request::new(http, "/test")
218 }
219
220 #[test]
221 fn test_query_adds_key_value() {
222 let req = make_request().query("limit", 10);
223 assert_eq!(req.query, vec![("limit".into(), "10".into())]);
224 }
225
226 #[test]
227 fn test_query_chaining_preserves_order() {
228 let req = make_request()
229 .query("limit", 10)
230 .query("offset", "abc")
231 .query("active", true);
232 assert_eq!(
233 req.query,
234 vec![
235 ("limit".into(), "10".into()),
236 ("offset".into(), "abc".into()),
237 ("active".into(), "true".into()),
238 ]
239 );
240 }
241
242 #[test]
243 fn test_query_opt_some_adds_parameter() {
244 let req = make_request().query_opt("tag", Some("politics"));
245 assert_eq!(req.query, vec![("tag".into(), "politics".into())]);
246 }
247
248 #[test]
249 fn test_query_opt_none_skips_parameter() {
250 let req = make_request().query_opt("tag", None::<&str>);
251 assert!(req.query.is_empty());
252 }
253
254 #[test]
255 fn test_query_opt_interleaved_with_query() {
256 let req = make_request()
257 .query("limit", 25)
258 .query_opt("cursor", None::<String>)
259 .query("active", true)
260 .query_opt("slug", Some("will-x-happen"));
261
262 assert_eq!(
263 req.query,
264 vec![
265 ("limit".into(), "25".into()),
266 ("active".into(), "true".into()),
267 ("slug".into(), "will-x-happen".into()),
268 ]
269 );
270 }
271
272 #[test]
273 fn test_query_many_adds_repeated_key() {
274 let req = make_request().query_many("id", vec!["abc", "def", "ghi"]);
275 assert_eq!(
276 req.query,
277 vec![
278 ("id".into(), "abc".into()),
279 ("id".into(), "def".into()),
280 ("id".into(), "ghi".into()),
281 ]
282 );
283 }
284
285 #[test]
286 fn test_query_many_empty_iterator() {
287 let req = make_request().query_many("id", Vec::<String>::new());
288 assert!(req.query.is_empty());
289 }
290
291 #[test]
292 fn test_query_many_opt_some_adds_values() {
293 let ids = vec![1u64, 2, 3];
294 let req = make_request().query_many_opt("id", Some(ids));
295 assert_eq!(
296 req.query,
297 vec![
298 ("id".into(), "1".into()),
299 ("id".into(), "2".into()),
300 ("id".into(), "3".into()),
301 ]
302 );
303 }
304
305 #[test]
306 fn test_query_many_opt_none_skips() {
307 let req = make_request().query_many_opt("id", None::<Vec<String>>);
308 assert!(req.query.is_empty());
309 }
310
311 #[test]
312 fn test_query_duplicate_keys_allowed() {
313 let req = make_request()
314 .query("sort", "price")
315 .query("sort", "volume");
316 assert_eq!(
317 req.query,
318 vec![
319 ("sort".into(), "price".into()),
320 ("sort".into(), "volume".into()),
321 ]
322 );
323 }
324
325 #[test]
328 fn test_request_new_stores_path() {
329 let req = make_request();
330 assert_eq!(req.path, "/test");
331 assert!(req.query.is_empty());
332 }
333
334 #[test]
335 fn test_request_new_with_string_path() {
336 let http = HttpClientBuilder::new("https://example.com")
337 .build()
338 .unwrap();
339 let req: Request<(), ApiError> = Request::new(http, String::from("/events"));
340 assert_eq!(req.path, "/events");
341 }
342
343 #[test]
346 fn test_request_clone_does_not_require_clone_markers() {
347 struct NotClone;
349 struct NotCloneError;
350
351 let http = HttpClientBuilder::new("https://example.com")
352 .build()
353 .unwrap();
354 let original: Request<NotClone, NotCloneError> =
355 Request::new(http, "/feed").query("user", "0xabc");
356 let extended = original.clone().query("cursor", "c1");
357
358 assert_eq!(original.path, "/feed");
359 assert_eq!(original.query, vec![("user".into(), "0xabc".into())]);
360 assert_eq!(extended.path, "/feed");
361 assert_eq!(
362 extended.query,
363 vec![
364 ("user".into(), "0xabc".into()),
365 ("cursor".into(), "c1".into()),
366 ]
367 );
368 }
369
370 #[test]
373 fn test_typed_request_new_and_default() {
374 let _t1: TypedRequest<String> = TypedRequest::new();
375 let _t2: TypedRequest<String> = TypedRequest::default();
376 }
378}