http_typed/
client.rs

1use std::{any::type_name, marker::PhantomData};
2
3use reqwest::header::CONTENT_TYPE;
4
5use crate::{All, HttpMethod, InRequestGroup, Request, SerializeBody};
6
7/// A client to delegate to the send function that provides the ability to
8/// optionally specify:
9/// - a base url to be used for all requests
10/// - a request group to constrain the request types accepted by this type
11pub struct Client<RequestGroup = All> {
12    base_url: String,
13    inner: reqwest::Client,
14    _p: PhantomData<RequestGroup>,
15}
16
17/// Explicitly implemented to avoid requirement RequestGroup: Debug
18impl<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
27/// Explicitly implemented to avoid requirement RequestGroup: Default
28impl<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
38/// Explicitly implemented to avoid requirement RequestGroup: Clone
39impl<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    /// Send the provided request to the host at this client's base_url, using
59    /// the Request implementation to determine the remaining url path and
60    /// request data.
61    ///
62    /// The url used for the request is {self.base_url}{request.path()}
63    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    /// Send the provided request to the host at this client's base_url plus
81    /// url_infix, using the Request implementation to determine the remaining
82    /// url path and request data.
83    ///
84    /// The url used for the request is
85    /// {self.base_url}{url_infix}{request.path()}
86    ///
87    /// If you'd like to specify the entire base url for each request using this
88    /// method, instantiate this struct with base_url = "" (the default)
89    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    /// Send the provided request to the specified path using the specified method,
108    /// and deserialize the response into the specified response type.
109    ///
110    /// The url used for this request is {self.base_url}{path}
111    ///
112    /// If you'd like to specify the entire base url for each request using this
113    /// method, instantiate this struct with base_url = "" (the default)
114    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
134/// Convenience function to create a client and send a request using minimal
135/// boilerplate. Creating a client is expensive, so you should not use this
136/// function if you plan on sending multiple requests.
137///
138/// Equivalent to:
139/// - `Client::new(base_url).send(request)`
140/// - `Client::default().send_to(base_url, request)`
141///
142/// Send the provided request to the host at the specified base url, using the
143/// request metadata specified by the Request implementation to create the http
144/// request and determine the response type.
145///
146/// The url used for the request is {base_url}{request.path()}
147pub 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
159/// Convenience function to create a client and send a request using minimal
160/// boilerplate. Creating a client is expensive, so you should not use this
161/// function if you plan on sending multiple requests.
162///
163/// Equivalent to:
164/// - `Client::default().send_custom(url, method, request)`
165/// - `Client::new(url).send_custom("", method, request)`
166///
167/// Send the provided request to the specified url using the specified method,
168/// and deserialize the response into the specified response type.
169pub 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
213/// This allows the send_custom methods to accept objects that do not implement
214/// Request. SimpleBody is a more minimal requirement that you get automatically
215/// if you implement request, but you can also implement this by itself without
216/// implementing Request, to keep things simple with usages of send_custom.
217pub 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}