1use std::marker::PhantomData;
2use std::sync::Arc;
3
4use reqwest::header::{HeaderValue, CONTENT_TYPE};
5use reqwest::{Method, Response};
6use serde::de::DeserializeOwned;
7use serde::Serialize;
8
9use crate::client::HttpClient;
10use crate::hooks::{Cost, DynAuthenticator, RequestParts};
11use crate::ApiError;
12
13pub trait QueryBuilder: Sized {
15 fn add_query(&mut self, key: String, value: String);
17
18 fn query(mut self, key: impl Into<String>, value: impl ToString) -> Self {
20 self.add_query(key.into(), value.to_string());
21 self
22 }
23
24 fn query_opt(mut self, key: impl Into<String>, value: Option<impl ToString>) -> Self {
26 if let Some(v) = value {
27 self.add_query(key.into(), v.to_string());
28 }
29 self
30 }
31
32 fn query_many<I, V>(self, key: impl Into<String>, values: I) -> Self
34 where
35 I: IntoIterator<Item = V>,
36 V: ToString,
37 {
38 let key = key.into();
39 let mut result = self;
40 for value in values {
41 result.add_query(key.clone(), value.to_string());
42 }
43 result
44 }
45
46 fn query_many_opt<I, V>(self, key: impl Into<String>, values: Option<I>) -> Self
48 where
49 I: IntoIterator<Item = V>,
50 V: ToString,
51 {
52 if let Some(values) = values {
53 self.query_many(key, values)
54 } else {
55 self
56 }
57 }
58}
59
60pub trait RequestError: From<ApiError> + std::fmt::Debug {}
68
69impl<E: From<ApiError> + std::fmt::Debug> RequestError for E {}
70
71pub struct Request<T, E> {
78 pub(crate) http_client: HttpClient,
79 pub(crate) method: Method,
80 pub(crate) path: String,
81 pub(crate) query: Vec<(String, String)>,
82 pub(crate) body: Option<String>,
83 pub(crate) authenticator: Option<Arc<DynAuthenticator<'static>>>,
84 pub(crate) costs: Vec<Cost>,
85 pub(crate) _marker: PhantomData<(T, E)>,
86}
87
88impl<T, E> Request<T, E> {
89 pub fn new(http_client: HttpClient, path: impl Into<String>) -> Self {
91 Self {
92 http_client,
93 method: Method::GET,
94 path: path.into(),
95 query: Vec::new(),
96 body: None,
97 authenticator: None,
98 costs: Vec::new(),
99 _marker: PhantomData,
100 }
101 }
102
103 pub fn method(mut self, method: Method) -> Self {
105 self.method = method;
106 self
107 }
108
109 pub fn authenticator(mut self, auth: Arc<DynAuthenticator<'static>>) -> Self {
111 self.authenticator = Some(auth);
112 self
113 }
114
115 pub fn with_cost(mut self, cost: Cost) -> Self {
118 self.costs.push(cost);
119 self
120 }
121}
122
123impl<T, E: From<ApiError>> Request<T, E> {
124 pub fn body<B: Serialize + ?Sized>(mut self, body: &B) -> Result<Self, E> {
135 let value = serde_json::to_value(body).map_err(ApiError::from)?;
136 self.body = Some(value.to_string());
137 Ok(self)
138 }
139}
140
141impl<T, E> Clone for Request<T, E> {
147 fn clone(&self) -> Self {
148 Self {
149 http_client: self.http_client.clone(),
150 method: self.method.clone(),
151 path: self.path.clone(),
152 query: self.query.clone(),
153 body: self.body.clone(),
154 authenticator: self.authenticator.clone(),
155 costs: self.costs.clone(),
156 _marker: PhantomData,
157 }
158 }
159}
160
161impl<T, E> QueryBuilder for Request<T, E> {
162 fn add_query(&mut self, key: String, value: String) {
163 self.query.push((key, value));
164 }
165}
166
167impl<T: DeserializeOwned, E: RequestError> Request<T, E> {
168 pub async fn send(self) -> Result<T, E> {
170 let path = self.path.clone();
171 let response = self.send_raw().await?;
172
173 let text = response
174 .text()
175 .await
176 .map_err(|e| E::from(ApiError::from(e)))?;
177
178 crate::decode_json(&path, &text).map_err(|e| E::from(ApiError::from(e)))
179 }
180
181 pub async fn send_raw(self) -> Result<Response, E> {
187 let mut parts = RequestParts::new(self.method, self.path);
188 parts.query = self.query;
189 if let Some(body) = self.body {
190 parts
191 .headers
192 .insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
193 parts.body = Some(body);
194 }
195 let failure = match self
196 .http_client
197 .send(parts, &self.costs, self.authenticator.as_deref())
198 .await
199 {
200 Ok(response) if response.status().is_success() => {
201 tracing::debug!("Response status: {}", response.status());
202 return Ok(response);
203 }
204 Ok(response) => ApiError::from_response(response).await,
207 Err(err) => err,
208 };
209 let answered = matches!(failure, ApiError::Response(_));
210 let error = E::from(failure);
211 if answered {
212 tracing::error!("Request failed: {:?}", error);
213 }
214 Err(error)
215 }
216}
217
218pub struct TypedRequest<T> {
220 pub(crate) _marker: PhantomData<T>,
221}
222
223impl<T> TypedRequest<T> {
224 pub fn new() -> Self {
226 Self {
227 _marker: PhantomData,
228 }
229 }
230}
231
232impl<T> Default for TypedRequest<T> {
233 fn default() -> Self {
234 Self::new()
235 }
236}
237
238#[cfg(test)]
239mod tests {
240 use super::*;
241 use crate::HttpClientBuilder;
242
243 fn make_request() -> Request<(), ApiError> {
247 let http = HttpClientBuilder::new("https://example.com")
248 .build()
249 .unwrap();
250 Request::new(http, "/test")
251 }
252
253 #[test]
254 fn test_query_adds_key_value() {
255 let req = make_request().query("limit", 10);
256 assert_eq!(req.query, vec![("limit".into(), "10".into())]);
257 }
258
259 #[test]
260 fn test_query_chaining_preserves_order() {
261 let req = make_request()
262 .query("limit", 10)
263 .query("offset", "abc")
264 .query("active", true);
265 assert_eq!(
266 req.query,
267 vec![
268 ("limit".into(), "10".into()),
269 ("offset".into(), "abc".into()),
270 ("active".into(), "true".into()),
271 ]
272 );
273 }
274
275 #[test]
276 fn test_query_opt_some_adds_parameter() {
277 let req = make_request().query_opt("tag", Some("politics"));
278 assert_eq!(req.query, vec![("tag".into(), "politics".into())]);
279 }
280
281 #[test]
282 fn test_query_opt_none_skips_parameter() {
283 let req = make_request().query_opt("tag", None::<&str>);
284 assert!(req.query.is_empty());
285 }
286
287 #[test]
288 fn test_query_opt_interleaved_with_query() {
289 let req = make_request()
290 .query("limit", 25)
291 .query_opt("cursor", None::<String>)
292 .query("active", true)
293 .query_opt("slug", Some("will-x-happen"));
294
295 assert_eq!(
296 req.query,
297 vec![
298 ("limit".into(), "25".into()),
299 ("active".into(), "true".into()),
300 ("slug".into(), "will-x-happen".into()),
301 ]
302 );
303 }
304
305 #[test]
306 fn test_query_many_adds_repeated_key() {
307 let req = make_request().query_many("id", vec!["abc", "def", "ghi"]);
308 assert_eq!(
309 req.query,
310 vec![
311 ("id".into(), "abc".into()),
312 ("id".into(), "def".into()),
313 ("id".into(), "ghi".into()),
314 ]
315 );
316 }
317
318 #[test]
319 fn test_query_many_empty_iterator() {
320 let req = make_request().query_many("id", Vec::<String>::new());
321 assert!(req.query.is_empty());
322 }
323
324 #[test]
325 fn test_query_many_opt_some_adds_values() {
326 let ids = vec![1u64, 2, 3];
327 let req = make_request().query_many_opt("id", Some(ids));
328 assert_eq!(
329 req.query,
330 vec![
331 ("id".into(), "1".into()),
332 ("id".into(), "2".into()),
333 ("id".into(), "3".into()),
334 ]
335 );
336 }
337
338 #[test]
339 fn test_query_many_opt_none_skips() {
340 let req = make_request().query_many_opt("id", None::<Vec<String>>);
341 assert!(req.query.is_empty());
342 }
343
344 #[test]
345 fn test_query_duplicate_keys_allowed() {
346 let req = make_request()
347 .query("sort", "price")
348 .query("sort", "volume");
349 assert_eq!(
350 req.query,
351 vec![
352 ("sort".into(), "price".into()),
353 ("sort".into(), "volume".into()),
354 ]
355 );
356 }
357
358 #[test]
361 fn test_request_new_stores_path() {
362 let req = make_request();
363 assert_eq!(req.path, "/test");
364 assert!(req.query.is_empty());
365 }
366
367 #[test]
368 fn test_request_new_with_string_path() {
369 let http = HttpClientBuilder::new("https://example.com")
370 .build()
371 .unwrap();
372 let req: Request<(), ApiError> = Request::new(http, String::from("/events"));
373 assert_eq!(req.path, "/events");
374 }
375
376 #[test]
379 fn test_request_clone_does_not_require_clone_markers() {
380 struct NotClone;
382 struct NotCloneError;
383
384 let http = HttpClientBuilder::new("https://example.com")
385 .build()
386 .unwrap();
387 let original: Request<NotClone, NotCloneError> =
388 Request::new(http, "/feed").query("user", "0xabc");
389 let extended = original.clone().query("cursor", "c1");
390
391 assert_eq!(original.path, "/feed");
392 assert_eq!(original.query, vec![("user".into(), "0xabc".into())]);
393 assert_eq!(extended.path, "/feed");
394 assert_eq!(
395 extended.query,
396 vec![
397 ("user".into(), "0xabc".into()),
398 ("cursor".into(), "c1".into()),
399 ]
400 );
401 }
402
403 #[test]
406 fn test_typed_request_new_and_default() {
407 let _t1: TypedRequest<String> = TypedRequest::new();
408 let _t2: TypedRequest<String> = TypedRequest::default();
409 }
411}