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