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