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> QueryBuilder for Request<T, E> {
83 fn add_query(&mut self, key: String, value: String) {
84 self.query.push((key, value));
85 }
86}
87
88impl<T: DeserializeOwned, E: RequestError> Request<T, E> {
89 pub async fn send(self) -> Result<T, E> {
91 let response = self.send_raw().await?;
92
93 let text = response
95 .text()
96 .await
97 .map_err(|e| E::from(ApiError::from(e)))?;
98
99 serde_json::from_str(&text).map_err(|e| {
101 tracing::error!("Deserialization failed: {}", e);
102 tracing::error!("Failed to deserialize: {}", crate::truncate_for_log(&text));
103 E::from(ApiError::from(e))
104 })
105 }
106
107 pub async fn send_raw(self) -> Result<Response, E> {
109 let url = self
110 .http_client
111 .base_url
112 .join(&self.path)
113 .map_err(|e| E::from(ApiError::from(e)))?;
114
115 let http_client = self.http_client;
116 let query = self.query;
117 let path = self.path;
118 let mut attempt = 0u32;
119
120 loop {
121 let _permit = http_client.acquire_concurrency().await;
122 http_client.acquire_rate_limit(&path, None).await;
123
124 let mut request = http_client.client.get(url.clone());
125
126 if !query.is_empty() {
127 request = request.query(&query);
128 }
129
130 let response = request
131 .send()
132 .await
133 .map_err(|e| E::from(ApiError::from(e)))?;
134 let status = response.status();
135 let retry_after = retry_after_header(&response);
136
137 if let Some(backoff) = http_client.should_retry(status, attempt, retry_after.as_deref())
138 {
139 attempt += 1;
140 tracing::warn!(
141 "Rate limited (429) on {}, retry {} after {}ms",
142 path,
143 attempt,
144 backoff.as_millis()
145 );
146 drop(_permit);
147 tokio::time::sleep(backoff).await;
148 continue;
149 }
150
151 tracing::debug!("Response status: {}", status);
152
153 if !status.is_success() {
154 let error = E::from_response(response).await;
155 tracing::error!("Request failed: {:?}", error);
156 return Err(error);
157 }
158
159 return Ok(response);
160 }
161 }
162}
163
164pub struct TypedRequest<T> {
166 pub(crate) _marker: PhantomData<T>,
167}
168
169impl<T> TypedRequest<T> {
170 pub fn new() -> Self {
172 Self {
173 _marker: PhantomData,
174 }
175 }
176}
177
178impl<T> Default for TypedRequest<T> {
179 fn default() -> Self {
180 Self::new()
181 }
182}
183
184#[cfg(test)]
185mod tests {
186 use super::*;
187 use crate::HttpClientBuilder;
188
189 fn make_request() -> Request<(), ApiError> {
193 let http = HttpClientBuilder::new("https://example.com")
194 .build()
195 .unwrap();
196 Request::new(http, "/test")
197 }
198
199 #[test]
200 fn test_query_adds_key_value() {
201 let req = make_request().query("limit", 10);
202 assert_eq!(req.query, vec![("limit".into(), "10".into())]);
203 }
204
205 #[test]
206 fn test_query_chaining_preserves_order() {
207 let req = make_request()
208 .query("limit", 10)
209 .query("offset", "abc")
210 .query("active", true);
211 assert_eq!(
212 req.query,
213 vec![
214 ("limit".into(), "10".into()),
215 ("offset".into(), "abc".into()),
216 ("active".into(), "true".into()),
217 ]
218 );
219 }
220
221 #[test]
222 fn test_query_opt_some_adds_parameter() {
223 let req = make_request().query_opt("tag", Some("politics"));
224 assert_eq!(req.query, vec![("tag".into(), "politics".into())]);
225 }
226
227 #[test]
228 fn test_query_opt_none_skips_parameter() {
229 let req = make_request().query_opt("tag", None::<&str>);
230 assert!(req.query.is_empty());
231 }
232
233 #[test]
234 fn test_query_opt_interleaved_with_query() {
235 let req = make_request()
236 .query("limit", 25)
237 .query_opt("cursor", None::<String>)
238 .query("active", true)
239 .query_opt("slug", Some("will-x-happen"));
240
241 assert_eq!(
242 req.query,
243 vec![
244 ("limit".into(), "25".into()),
245 ("active".into(), "true".into()),
246 ("slug".into(), "will-x-happen".into()),
247 ]
248 );
249 }
250
251 #[test]
252 fn test_query_many_adds_repeated_key() {
253 let req = make_request().query_many("id", vec!["abc", "def", "ghi"]);
254 assert_eq!(
255 req.query,
256 vec![
257 ("id".into(), "abc".into()),
258 ("id".into(), "def".into()),
259 ("id".into(), "ghi".into()),
260 ]
261 );
262 }
263
264 #[test]
265 fn test_query_many_empty_iterator() {
266 let req = make_request().query_many("id", Vec::<String>::new());
267 assert!(req.query.is_empty());
268 }
269
270 #[test]
271 fn test_query_many_opt_some_adds_values() {
272 let ids = vec![1u64, 2, 3];
273 let req = make_request().query_many_opt("id", Some(ids));
274 assert_eq!(
275 req.query,
276 vec![
277 ("id".into(), "1".into()),
278 ("id".into(), "2".into()),
279 ("id".into(), "3".into()),
280 ]
281 );
282 }
283
284 #[test]
285 fn test_query_many_opt_none_skips() {
286 let req = make_request().query_many_opt("id", None::<Vec<String>>);
287 assert!(req.query.is_empty());
288 }
289
290 #[test]
291 fn test_query_duplicate_keys_allowed() {
292 let req = make_request()
293 .query("sort", "price")
294 .query("sort", "volume");
295 assert_eq!(
296 req.query,
297 vec![
298 ("sort".into(), "price".into()),
299 ("sort".into(), "volume".into()),
300 ]
301 );
302 }
303
304 #[test]
307 fn test_request_new_stores_path() {
308 let req = make_request();
309 assert_eq!(req.path, "/test");
310 assert!(req.query.is_empty());
311 }
312
313 #[test]
314 fn test_request_new_with_string_path() {
315 let http = HttpClientBuilder::new("https://example.com")
316 .build()
317 .unwrap();
318 let req: Request<(), ApiError> = Request::new(http, String::from("/events"));
319 assert_eq!(req.path, "/events");
320 }
321
322 #[test]
325 fn test_typed_request_new_and_default() {
326 let _t1: TypedRequest<String> = TypedRequest::new();
327 let _t2: TypedRequest<String> = TypedRequest::default();
328 }
330}