racal/
reqwest.rs

1//! An optional API client feature using `reqwest`
2//!
3//! Besides using this, you could instead easily implement your own client using
4//! a different HTTP library with the [`racal::Queryable`](crate::Queryable)
5//! trait.
6
7use reqwest::{Client, RequestBuilder, Response};
8use serde::de::DeserializeOwned;
9use thiserror::Error;
10
11use crate::{FromApiState, Queryable, RequestMethod};
12
13/// An error that may happen with an API query
14#[derive(Debug, Error)]
15pub enum ApiError {
16	/// An error happened with serialization
17	#[error("An error happened with serialization: {0}")]
18	Serde(serde_json::Error),
19
20	/// An error happened with the request itself
21	#[error("An error happened with the request itself: {0}")]
22	Reqwest(reqwest::Error),
23}
24
25impl From<serde_json::Error> for ApiError {
26	fn from(err: serde_json::Error) -> Self { Self::Serde(err) }
27}
28
29impl From<reqwest::Error> for ApiError {
30	fn from(err: reqwest::Error) -> Self { Self::Reqwest(err) }
31}
32
33/// An API client that can be used to create queries
34#[async_trait::async_trait]
35pub trait ApiClient<State> {
36	/// Gets the API state
37	fn state(&self) -> &State;
38
39	/// Gets the actual reqwest client
40	fn client(&self) -> &Client;
41
42	/// A way to modify the request right before sending it
43	///
44	/// Can also for example be used to implement rate limits thanks to the async
45	/// nature
46	async fn before_request(
47		&self, req: RequestBuilder,
48	) -> Result<RequestBuilder, ApiError> {
49		Ok(req)
50	}
51
52	/// A way to modify the request after it's been received
53	///
54	/// By default errors on bad status messages and just deserializes the value,
55	/// using the queryable.
56	///
57	/// Can also for example be used to implement rate limits thanks to the async
58	/// nature.
59	async fn handle_response<ReturnType, FromState, QueryableType>(
60		&self, queryable: QueryableType, response: Response,
61	) -> Result<ReturnType, ApiError>
62	where
63		ReturnType: DeserializeOwned,
64		FromState: FromApiState<State>,
65		QueryableType: Queryable<FromState, ReturnType> + Send + Sync,
66	{
67		let response = response.error_for_status()?;
68		let val = response.bytes().await?;
69		Ok(queryable.deserialize(&val)?)
70	}
71
72	/// Creates a query
73	async fn query<ReturnType, FromState, QueryableType>(
74		&self, queryable: QueryableType,
75	) -> Result<ReturnType, ApiError>
76	where
77		ReturnType: DeserializeOwned,
78		FromState: FromApiState<State>,
79		QueryableType: Queryable<FromState, ReturnType> + Send + Sync,
80	{
81		let request = Self::build_request(
82			self.client(),
83			FromState::from_state(self.state()),
84			&queryable,
85		)?;
86		let request = self.before_request(request).await?;
87		let response = request.send().await?;
88
89		self.handle_response(queryable, response).await
90	}
91
92	/// Builds the base request
93	///
94	/// # Errors
95	///
96	/// If body cannot be set for the request
97	fn build_request<ReturnType, FromState, QueryableType>(
98		http: &Client, api_state: &FromState, queryable: &QueryableType,
99	) -> Result<RequestBuilder, ApiError>
100	where
101		ReturnType: DeserializeOwned,
102		FromState: FromApiState<State>,
103		QueryableType: Queryable<FromState, ReturnType> + Send + Sync,
104	{
105		let mut request = http.request(
106			match queryable.method(api_state) {
107				RequestMethod::Get => reqwest::Method::GET,
108				RequestMethod::Head => reqwest::Method::HEAD,
109				RequestMethod::Patch => reqwest::Method::PATCH,
110				RequestMethod::Post => reqwest::Method::POST,
111				RequestMethod::Put => reqwest::Method::PUT,
112				RequestMethod::Delete => reqwest::Method::DELETE,
113			},
114			queryable.url(api_state),
115		);
116		if let Some(body) = queryable.body(api_state) {
117			request = request.body(body?).header("Content-Type", "application/json");
118		}
119
120		Ok(request)
121	}
122}