1use std::{any::type_name, marker::PhantomData};
2
3use reqwest::header::CONTENT_TYPE;
4
5use crate::{All, HttpMethod, InRequestGroup, Request, SerializeBody};
6
7pub struct Client<RequestGroup = All> {
12 base_url: String,
13 inner: reqwest::Client,
14 _p: PhantomData<RequestGroup>,
15}
16
17impl<RequestGroup> std::fmt::Debug for Client<RequestGroup> {
19 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
20 f.debug_struct(type_name::<Self>())
21 .field("base_url", &self.base_url)
22 .field("inner", &self.inner)
23 .finish()
24 }
25}
26
27impl<RequestGroup> Default for Client<RequestGroup> {
29 fn default() -> Self {
30 Self {
31 base_url: Default::default(),
32 inner: Default::default(),
33 _p: PhantomData,
34 }
35 }
36}
37
38impl<RequestGroup> Clone for Client<RequestGroup> {
40 fn clone(&self) -> Self {
41 Self {
42 base_url: self.base_url.clone(),
43 inner: self.inner.clone(),
44 _p: PhantomData,
45 }
46 }
47}
48
49impl<RequestGroup> Client<RequestGroup> {
50 pub fn new(base_url: String) -> Self {
51 Self {
52 base_url,
53 inner: reqwest::Client::new(),
54 _p: PhantomData,
55 }
56 }
57
58 pub async fn send<Req>(
64 &self,
65 request: Req,
66 ) -> Result<Req::Response, Error<<Req::Serializer as SerializeBody<Req>>::Error>>
67 where
68 Req: Request + InRequestGroup<RequestGroup>,
69 Req::Response: for<'a> serde::Deserialize<'a>,
70 {
71 send_custom_with_client(
72 &self.inner,
73 &format!("{}{}", self.base_url, request.path()),
74 request.method(),
75 request,
76 )
77 .await
78 }
79
80 pub async fn send_to<Req>(
90 &self,
91 url_infix: &str,
92 request: Req,
93 ) -> Result<Req::Response, Error<<Req::Serializer as SerializeBody<Req>>::Error>>
94 where
95 Req: Request + InRequestGroup<RequestGroup>,
96 Req::Response: for<'a> serde::Deserialize<'a>,
97 {
98 send_custom_with_client(
99 &self.inner,
100 &format!("{}{url_infix}{}", self.base_url, request.path()),
101 request.method(),
102 request,
103 )
104 .await
105 }
106
107 pub async fn send_custom<Req, Res>(
115 &self,
116 path: &str,
117 method: HttpMethod,
118 request: Req,
119 ) -> Result<Res, Error<Req::Error>>
120 where
121 Req: SimpleBody,
122 Res: for<'a> serde::Deserialize<'a>,
123 {
124 send_custom_with_client(
125 &self.inner,
126 &format!("{}{path}", self.base_url),
127 method,
128 request,
129 )
130 .await
131 }
132}
133
134pub async fn send<Req>(
148 base_url: &str,
149 request: Req,
150) -> Result<Req::Response, Error<<Req::Serializer as SerializeBody<Req>>::Error>>
151where
152 Req: Request,
153 Req::Response: for<'a> serde::Deserialize<'a>,
154{
155 let url = format!("{base_url}{}", request.path());
156 send_custom_with_client(&reqwest::Client::new(), &url, request.method(), request).await
157}
158
159pub async fn send_custom<Req, Res>(
170 url: &str,
171 method: HttpMethod,
172 request: Req,
173) -> Result<Res, Error<Req::Error>>
174where
175 Req: SimpleBody,
176 Res: for<'a> serde::Deserialize<'a>,
177{
178 send_custom_with_client(&reqwest::Client::new(), url, method, request).await
179}
180
181async fn send_custom_with_client<Req, Res>(
182 client: &reqwest::Client,
183 url: &str,
184 method: HttpMethod,
185 request: Req,
186) -> Result<Res, Error<Req::Error>>
187where
188 Req: SimpleBody,
189 Res: for<'a> serde::Deserialize<'a>,
190{
191 let response = client
192 .request(method.into(), url)
193 .body(request.simple_body().map_err(Error::SerializationError)?)
194 .header(CONTENT_TYPE, "application/json")
195 .send()
196 .await?;
197 let status = response.status();
198 if status.is_success() {
199 let body = response.bytes().await?;
200 serde_json::from_slice(&body).map_err(|error| Error::DeserializationError {
201 error,
202 response_body: body_bytes_to_str(&body),
203 })
204 } else {
205 let message = match response.bytes().await {
206 Ok(bytes) => body_bytes_to_str(&bytes),
207 Err(e) => format!("failed to get body: {e:?}"),
208 };
209 Err(Error::InvalidStatusCode(status.into(), message))
210 }
211}
212
213pub trait SimpleBody {
218 type Error;
219 fn simple_body(&self) -> Result<Vec<u8>, Self::Error>;
220}
221
222impl<T: Request> SimpleBody for T {
223 type Error = <<Self as Request>::Serializer as SerializeBody<Self>>::Error;
224
225 fn simple_body(&self) -> Result<Vec<u8>, Self::Error> {
226 <Self as Request>::Serializer::serialize_body(self)
227 }
228}
229
230fn body_bytes_to_str(bytes: &[u8]) -> String {
231 match std::str::from_utf8(bytes) {
232 Ok(message) => message.to_owned(),
233 Err(e) => format!("could not read message body as a string: {e:?}"),
234 }
235}
236
237#[derive(thiserror::Error, Debug)]
238pub enum Error<Ser = serde_json::error::Error> {
239 #[error("reqwest error: {0}")]
240 ClientError(#[from] reqwest::Error),
241 #[error("request body serialization error: {0}")]
242 SerializationError(Ser),
243 #[error("serde deserialization error `{error}` while parsing response body: {response_body}")]
244 DeserializationError {
245 error: serde_json::error::Error,
246 response_body: String,
247 },
248 #[error("invalid status code {0} with response body: `{1}`")]
249 InvalidStatusCode(u16, String),
250}
251
252impl From<HttpMethod> for reqwest::Method {
253 fn from(value: HttpMethod) -> Self {
254 match value {
255 HttpMethod::Options => reqwest::Method::OPTIONS,
256 HttpMethod::Get => reqwest::Method::GET,
257 HttpMethod::Post => reqwest::Method::POST,
258 HttpMethod::Put => reqwest::Method::PUT,
259 HttpMethod::Delete => reqwest::Method::DELETE,
260 HttpMethod::Head => reqwest::Method::HEAD,
261 HttpMethod::Trace => reqwest::Method::TRACE,
262 HttpMethod::Connect => reqwest::Method::CONNECT,
263 HttpMethod::Patch => reqwest::Method::PATCH,
264 }
265 }
266}