twitch_api2/
client.rs

1//! Different clients you can use with this crate to call endpoints.
2//!
3//! This enables you to use your own http client/implementation.
4//! For example, say you have a http client that has a "client" named `foo::Client`.
5//!
6//! That client has a function `call` which looks something like this
7//! ```rust,no_run
8//! # struct Client;type ClientError = std::io::Error; impl Client {
9//! fn call(&self, req: http::Request<Vec<u8>>) -> futures::future::BoxFuture<'static, Result<http::Response<Vec<u8>>, ClientError>> {
10//! # stringify!(
11//!     ...
12//! # ); todo!()
13//! }
14//! # }
15//! ```
16//! To use that for requests we do the following.
17//!
18//! ```no_run
19//! use twitch_api2::client::{BoxedFuture, Request, RequestExt as _, Response};
20//! mod foo {
21//!     use twitch_api2::client::{BoxedFuture, Response};
22//!     pub struct Client;
23//!     impl Client {
24//!         pub fn call(
25//!             &self,
26//!             req: http::Request<Vec<u8>>,
27//!         ) -> futures::future::BoxFuture<'static, Result<http::Response<Vec<u8>>, ClientError>>
28//!         {
29//!             unimplemented!()
30//!         }
31//!     }
32//!     pub type ClientError = std::io::Error;
33//! }
34//! impl<'a> twitch_api2::HttpClient<'a> for foo::Client {
35//!     type Error = foo::ClientError;
36//!
37//!     fn req(&'a self, request: Request) -> BoxedFuture<'a, Result<Response, Self::Error>> {
38//!         Box::pin(async move {
39//!             Ok(self
40//!                 .call(
41//!                     request
42//!                         // The `RequestExt` trait provides a convenience function to convert
43//!                         // a `Request<impl Into<hyper::body::Body>>` into a `Request<Vec<u8>>`
44//!                         .into_request_vec()
45//!                         .await
46//!                         .expect("a request given to the client should always be valid"),
47//!                 )
48//!                 .await?
49//!                 .map(|body| body.into()))
50//!         })
51//!     }
52//! }
53//! // And for full usage
54//! use twitch_api2::TwitchClient;
55//! pub struct MyStruct {
56//!     twitch: TwitchClient<'static, foo::Client>,
57//!     token: twitch_oauth2::AppAccessToken,
58//! }
59//! ```
60//! If your client is from a remote crate, you can use [the newtype pattern](https://github.com/rust-unofficial/patterns/blob/607fcb00c4ecb9c6317e4e101e16dc15717758bd/patterns/newtype.md)
61//!
62//! Of course, sometimes the clients use different types for their responses and requests. but simply translate them into [`http`] types and it will work.
63//!
64//! See the source of this module for the implementation of [`Client`] for [surf](https://crates.io/crates/surf) and [reqwest](https://crates.io/crates/reqwest) if you need inspiration.
65
66use std::error::Error;
67use std::future::Future;
68
69pub use hyper::body::Bytes;
70pub use hyper::Body;
71
72#[cfg(feature = "ureq")]
73mod ureq_impl;
74#[cfg(feature = "ureq")]
75pub use ureq_impl::UreqError;
76
77#[cfg(feature = "surf")]
78mod surf_impl;
79#[cfg(feature = "surf")]
80pub use surf_impl::SurfError;
81
82#[cfg(feature = "reqwest")]
83mod reqwest_impl;
84#[cfg(feature = "reqwest")]
85pub use reqwest_impl::ReqwestClientDefaultError;
86
87/// The User-Agent `product` of this crate.
88pub static TWITCH_API2_USER_AGENT: &str =
89    concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"),);
90
91/// A boxed future, mimics `futures::future::BoxFuture`
92pub type BoxedFuture<'a, T> = std::pin::Pin<Box<dyn Future<Output = T> + Send + 'a>>;
93
94/// The request type we're expecting with body.
95pub type Request = http::Request<Bytes>;
96/// The response type we're expecting with body
97pub type Response = http::Response<Body>;
98
99/// Extension trait for [`Response`]
100pub trait ResponseExt {
101    /// Error returned
102    type Error;
103    /// Return the body as a vector of bytes
104    fn into_response_vec<'a>(self)
105        -> BoxedFuture<'a, Result<http::Response<Vec<u8>>, Self::Error>>;
106    /// Return the body as a [`Bytes`]
107    fn into_response_bytes<'a>(
108        self,
109    ) -> BoxedFuture<'a, Result<http::Response<hyper::body::Bytes>, Self::Error>>;
110}
111
112impl<Buffer> ResponseExt for http::Response<Buffer>
113where Buffer: Into<hyper::body::Body>
114{
115    type Error = hyper::Error;
116
117    fn into_response_vec<'a>(
118        self,
119    ) -> BoxedFuture<'a, Result<http::Response<Vec<u8>>, Self::Error>> {
120        let (parts, body) = self.into_parts();
121        let body: Body = body.into();
122        Box::pin(async move {
123            let body = hyper::body::to_bytes(body).await?.to_vec();
124            Ok(http::Response::from_parts(parts, body))
125        })
126    }
127
128    fn into_response_bytes<'a>(
129        self,
130    ) -> BoxedFuture<'a, Result<http::Response<hyper::body::Bytes>, Self::Error>> {
131        let (parts, body) = self.into_parts();
132        let body: Body = body.into();
133        Box::pin(async move {
134            let body = hyper::body::to_bytes(body).await?;
135            Ok(http::Response::from_parts(parts, body))
136        })
137    }
138}
139
140/// Extension trait for [`Request`]
141pub trait RequestExt {
142    /// Error returned
143    type Error;
144    /// Return the body as a vector of bytes
145    fn into_request_vec<'a>(self) -> BoxedFuture<'a, Result<http::Request<Vec<u8>>, Self::Error>>;
146}
147
148impl<Buffer> RequestExt for http::Request<Buffer>
149where Buffer: Into<hyper::body::Body>
150{
151    type Error = hyper::Error;
152
153    // TODO: Specialize for Buffer = AsRef<[u8]> or Vec<u8>
154    fn into_request_vec<'a>(self) -> BoxedFuture<'a, Result<http::Request<Vec<u8>>, Self::Error>> {
155        let (parts, body) = self.into_parts();
156        let body = body.into();
157        Box::pin(async move {
158            let body = hyper::body::to_bytes(body).await?.to_vec();
159            Ok(http::Request::from_parts(parts, body))
160        })
161    }
162}
163
164/// A client that can do requests
165pub trait Client<'a>: Send + Sync + 'a {
166    /// Error returned by the client
167    type Error: Error + Send + Sync + 'static;
168    /// Send a request
169    fn req(
170        &'a self,
171        request: Request,
172    ) -> BoxedFuture<'a, Result<Response, <Self as Client>::Error>>;
173}
174
175/// A specific client default for setting some sane defaults for API calls and oauth2 usage
176pub trait ClientDefault<'a>: Clone + Sized {
177    /// Errors that can happen when assembling the client
178    type Error: std::error::Error + Send + Sync + 'static;
179    /// Construct [`Self`] with sane defaults for API calls and oauth2.
180    fn default_client() -> Self {
181        Self::default_client_with_name(None)
182            .expect("a new twitch_api2 client without an extra product should never fail")
183    }
184
185    /// Constructs [`Self`] with sane defaults for API calls and oauth2 and setting user-agent to include another product
186    ///
187    /// Specifically, one should
188    ///
189    /// * Set User-Agent to `{product} twitch_api2/{version_of_twitch_api2}` (According to RFC7231)
190    ///   See [`TWITCH_API2_USER_AGENT`] for the product of this crate
191    /// * Disallow redirects
192    ///
193    /// # Notes
194    ///
195    /// When the product name is none, this function should never fail. This should be ensured with tests.
196    fn default_client_with_name(product: Option<http::HeaderValue>) -> Result<Self, Self::Error>;
197}
198
199// This makes errors very muddy, preferably we'd actually use rustc_on_unimplemented, but that is highly not recommended (and doesn't work 100% for me at least)
200// impl<'a, F, R, E> Client<'a> for F
201// where
202//     F: Fn(Req) -> R + Send + Sync + 'a,
203//     R: Future<Output = Result<Response, E>> + Send + Sync + 'a,
204//     E: Error + Send + Sync + 'static,
205// {
206//     type Error = E;
207//
208//     fn req(&'a self, request: Req) -> BoxedFuture<'a, Result<Response, Self::Error>> {
209//         Box::pin((self)(request))
210//     }
211// }
212
213#[derive(Debug, Default, thiserror::Error, Clone)]
214/// A client that will never work, used to trick documentation tests
215#[error("this client does not do anything, only used for documentation test that only checks")]
216pub struct DummyHttpClient;
217
218impl<'a> Client<'a> for DummyHttpClient {
219    type Error = DummyHttpClient;
220
221    fn req(&'a self, _: Request) -> BoxedFuture<'a, Result<Response, Self::Error>> {
222        Box::pin(async { Err(DummyHttpClient) })
223    }
224}
225
226impl<'a> Client<'a> for twitch_oauth2::client::DummyClient {
227    type Error = twitch_oauth2::client::DummyClient;
228
229    fn req(&'a self, _: Request) -> BoxedFuture<'a, Result<Response, Self::Error>> {
230        Box::pin(async { Err(twitch_oauth2::client::DummyClient) })
231    }
232}
233
234impl<'a, C> Client<'a> for std::sync::Arc<C>
235where C: Client<'a>
236{
237    type Error = <C as Client<'a>>::Error;
238
239    fn req(&'a self, req: Request) -> BoxedFuture<'a, Result<Response, Self::Error>> {
240        self.as_ref().req(req)
241    }
242}
243
244#[cfg(feature = "surf")]
245impl ClientDefault<'static> for DummyHttpClient
246where Self: Default
247{
248    type Error = DummyHttpClient;
249
250    fn default_client_with_name(_: Option<http::HeaderValue>) -> Result<Self, Self::Error> {
251        Ok(Self)
252    }
253}
254
255/// A compability shim for ensuring an error can represent [`hyper::Error`]
256#[derive(Debug, thiserror::Error)]
257pub enum CompatError<E> {
258    /// An error occurrec when assembling the body
259    #[error("could not get the body of the response")]
260    BodyError(#[source] hyper::Error),
261    /// An error occured
262    #[error(transparent)]
263    Other(#[from] E),
264}
265
266#[cfg(feature = "helix")]
267impl<'a, C: Client<'a> + Sync> twitch_oauth2::client::Client<'a> for crate::HelixClient<'a, C> {
268    type Error = CompatError<<C as Client<'a>>::Error>;
269
270    fn req(
271        &'a self,
272        request: http::Request<Vec<u8>>,
273    ) -> BoxedFuture<
274        'a,
275        Result<http::Response<Vec<u8>>, <Self as twitch_oauth2::client::Client>::Error>,
276    > {
277        let client = self.get_client();
278        {
279            let request = request.map(Bytes::from);
280            let resp = client.req(request);
281            Box::pin(async {
282                let resp = resp.await?;
283                let (parts, mut body) = resp.into_parts();
284                Ok(http::Response::from_parts(
285                    parts,
286                    hyper::body::to_bytes(&mut body)
287                        .await
288                        .map_err(CompatError::BodyError)?
289                        .to_vec(),
290                ))
291            })
292        }
293    }
294}
295
296#[cfg(feature = "tmi")]
297impl<'a, C: Client<'a> + Sync> twitch_oauth2::client::Client<'a> for crate::TmiClient<'a, C> {
298    type Error = CompatError<<C as Client<'a>>::Error>;
299
300    fn req(
301        &'a self,
302        request: http::Request<Vec<u8>>,
303    ) -> BoxedFuture<
304        'a,
305        Result<http::Response<Vec<u8>>, <Self as twitch_oauth2::client::Client>::Error>,
306    > {
307        let client = self.get_client();
308        {
309            let request = request.map(|b| Bytes::from(b));
310            let resp = client.req(request);
311            Box::pin(async {
312                let resp = resp.await?;
313                let (parts, mut body) = resp.into_parts();
314                Ok(http::Response::from_parts(
315                    parts,
316                    hyper::body::to_bytes(&mut body)
317                        .await
318                        .map_err(CompatError::BodyError)?
319                        .to_vec(),
320                ))
321            })
322        }
323    }
324}
325
326#[cfg(any(feature = "tmi", feature = "helix"))]
327impl<'a, C: Client<'a> + Sync> twitch_oauth2::client::Client<'a> for crate::TwitchClient<'a, C> {
328    type Error = CompatError<<C as Client<'a>>::Error>;
329
330    fn req(
331        &'a self,
332        request: http::Request<Vec<u8>>,
333    ) -> BoxedFuture<
334        'a,
335        Result<http::Response<Vec<u8>>, <Self as twitch_oauth2::client::Client>::Error>,
336    > {
337        let client = self.get_client();
338        {
339            let request = request.map(|b| Bytes::from(b));
340            let resp = client.req(request);
341            Box::pin(async {
342                let resp = resp.await?;
343                let (parts, mut body) = resp.into_parts();
344                Ok(http::Response::from_parts(
345                    parts,
346                    hyper::body::to_bytes(&mut body)
347                        .await
348                        .map_err(CompatError::BodyError)?
349                        .to_vec(),
350                ))
351            })
352        }
353    }
354}
355
356/// Gives the User-Agent header value for a client annotated with an added `twitch_api2` product
357pub fn user_agent(
358    product: Option<http::HeaderValue>,
359) -> Result<http::HeaderValue, http::header::InvalidHeaderValue> {
360    use std::convert::TryInto;
361
362    if let Some(product) = product {
363        let mut user_agent = product.as_bytes().to_owned();
364        user_agent.push(b' ');
365        user_agent.extend(TWITCH_API2_USER_AGENT.as_bytes());
366        user_agent.as_slice().try_into()
367    } else {
368        http::HeaderValue::from_str(TWITCH_API2_USER_AGENT)
369    }
370}