twitch_api_rs/
requests.rs

1//! Common traits used to construct requestable types
2//!
3//! - TODO: Make `make_request` function based on feature and requests constructable without
4//!     async or sending in all cases
5
6use async_trait::async_trait;
7use reqwest::Client;
8use reqwest::RequestBuilder;
9use serde::de::DeserializeOwned;
10use thiserror::Error;
11
12/// Used in place of [`Headers`], [`Parameters`] or [`Body`] to inidicate for the
13/// respective type that there is none
14type None = ();
15
16impl Headers for None {
17    fn write_headers(&self, req: RequestBuilder) -> RequestBuilder {
18        req
19    }
20}
21
22impl Parameters for None {
23    fn write_parameters(&self, req: RequestBuilder) -> RequestBuilder {
24        req
25    }
26}
27
28impl Body for None {
29    fn write_body(&self, req: RequestBuilder) -> RequestBuilder {
30        req
31    }
32}
33
34use serde::Deserialize;
35
36#[derive(Debug, Deserialize)]
37/// Represents a sucessful request that was denied by the twitch api for some reason.
38/// Use request's associated [`ErrorCodes`] to get matchable value.
39pub struct FailureStatus<S>
40where
41    S: DeserializeOwned + std::fmt::Display + std::fmt::Debug + 'static,
42{
43    /// Error message
44    pub error: Option<String>,
45
46    #[serde(bound(deserialize = "S: DeserializeOwned"))]
47    /// The status code of the Failure
48    ///
49    /// If S is ErrorCodes then this is a known error for this request, if u16 then it is not known
50    pub status: S,
51
52    /// The message twitch sent with the error
53    pub message: String,
54}
55
56impl<S> std::fmt::Display for FailureStatus<S>
57where
58    S: DeserializeOwned + std::fmt::Display + std::fmt::Debug + 'static,
59{
60    fn fmt(&self, w: &mut std::fmt::Formatter) -> std::fmt::Result {
61        if let Some(error) = &self.error {
62            write!(
63                w,
64                "Encountered error with code {}, error {}, and message {}",
65                self.status, error, self.message
66            )
67        } else {
68            write!(
69                w,
70                "Encountered error with code {}, and message {}",
71                self.status, self.message
72            )
73        }
74    }
75}
76
77impl<S> std::error::Error for FailureStatus<S> where
78    S: DeserializeOwned + std::fmt::Display + std::fmt::Debug + 'static
79{
80}
81
82impl<E: ErrorCodes> From<FailureStatus<u16>> for RequestError<E> {
83    fn from(failure: FailureStatus<u16>) -> Self {
84        match E::from_status(failure) {
85            Ok(known) => RequestError::KnownErrorStatus(known),
86            Err(unkn) => RequestError::UnkownErrorStatus(unkn),
87        }
88    }
89}
90
91#[derive(Debug, Deserialize)]
92#[serde(untagged)]
93/// Represents a possible response from the twitch api, deserialized from a sucessful
94/// request. May not contain the requested content but instead a [`FailureStatus`]
95pub enum PossibleResponse<R>
96where
97    R: Response + 'static,
98{
99    #[serde(bound(deserialize = "R: DeserializeOwned"))]
100    /// Sucessful response
101    Response(R),
102
103    /// Response that was denied by the twitch service
104    Failure(FailureStatus<u16>),
105}
106
107impl<R> PossibleResponse<R>
108where
109    R: Response + 'static,
110{
111    fn into_result(self) -> Result<R, FailureStatus<u16>> {
112        match self {
113            Self::Response(r) => Ok(r),
114            Self::Failure(f) => Err(f),
115        }
116    }
117}
118
119#[derive(Debug, Error)]
120/// Returned from a request when it could not be completed
121pub enum RequestError<C: ErrorCodes + 'static> {
122    #[error("You must provide valid authorization to this endpoint")]
123    /// Returned when this endpoint was not given a valid authorization key
124    MissingAuth,
125
126    #[error("Request Malformed with message: {0}")]
127    /// Could not try to make request because it was malformed in some way
128    MalformedRequest(String),
129
130    #[error("Did not have user scopes required {0:?}")]
131    /// Did not have the correct user scopes available to make request.
132    ScopesError(Vec<String>),
133
134    #[error("Known Error enountered: {0}")]
135    /// Encountered a known error status, match on `0.status` for all `C::*`
136    KnownErrorStatus(FailureStatus<C>),
137
138    #[error("Unknown Error enountered: {0}")]
139    /// Encountered an unknown error status from twitch
140    UnkownErrorStatus(FailureStatus<u16>),
141
142    #[error("Reqwest encountered an error: {0}")]
143    /// Reqwest could not complete the request for some reason
144    ReqwestError(#[from] reqwest::Error),
145
146    #[error("Unknown Error encountered {0:?}")]
147    /// Unknown error
148    UnknownError(#[from] Box<dyn std::error::Error>),
149}
150
151/// Represents A Known set of error status codes that an endpoint may return.o
152///
153/// See src for [`CommonResponseCodes`] for example of implementation using thiserror
154pub trait ErrorCodes: std::error::Error + Sized + DeserializeOwned + Copy {
155    /// Possibly mark the status as a known status of this kind, used by [`RequestError`]
156    fn from_status(codes: FailureStatus<u16>) -> Result<FailureStatus<Self>, FailureStatus<u16>>;
157}
158
159#[derive(Debug, Clone, Copy, Error, Deserialize)]
160/// Error codes used by twitch that are the same across most endpoints.
161pub enum CommonResponseCodes {
162    #[error("400: Malformed Request")]
163    /// The request did not conform to what the endpoint was expecting
164    BadRequestCode,
165
166    #[error("401: Authorization Error")]
167    /// The authorization provided was not valid or was out of date
168    AuthErrorCode,
169
170    #[error("500: Server Error")]
171    /// Twitch may be experiencing internal errors, if encountered the request should
172    /// be retried once. If that fails then assume twitch is temporarily down.
173    ServerErrorCode,
174}
175
176#[macro_export]
177/// Generate a [`ErrorCodes`] impl block for a given Enum by mapping known status codes
178/// to specific variants. Variants must not be struct variants
179macro_rules! response_codes {
180    ($for:ty : [$($val:expr => $item:path),+]) => {
181        impl ErrorCodes for $for {
182            fn from_status(codes: FailureStatus<u16>) -> Result<FailureStatus<Self>, FailureStatus<u16>> {
183                match codes.status {
184                $(
185                    $val => Ok(FailureStatus::<Self> {
186                        error: codes.error,
187                        status: $item,
188                        message: codes.message
189                    }),
190                )*
191                    _ => Err(codes),
192                }
193            }
194        }
195    }
196}
197
198response_codes!(
199    CommonResponseCodes: [
200        400 => CommonResponseCodes::BadRequestCode,
201        401 => CommonResponseCodes::AuthErrorCode,
202        500 => CommonResponseCodes::ServerErrorCode
203]);
204
205/// Headers for a request
206pub trait Headers {
207    /// Write headers to request builder and return request builder
208    fn write_headers(&self, req: RequestBuilder) -> RequestBuilder;
209}
210
211/// Marker trait for auto implementation of headers
212///
213/// Must be able to borrow as a map of header names to values
214pub trait HeadersExt {
215    /// Borrow the object as map from header names to values
216    fn as_ref<'a>(&'a self) -> &'a [(&'a str, &'a str)];
217}
218
219impl<T: HeadersExt> Headers for T {
220    fn write_headers<'a>(&'a self, mut req: RequestBuilder) -> RequestBuilder {
221        for (a, b) in self.as_ref() {
222            req = req.header(*a, *b);
223        }
224        req
225    }
226}
227
228/// Parameters for a request
229pub trait Parameters {
230    /// Write parameters to request builder and return request builder
231    fn write_parameters(&self, req: RequestBuilder) -> RequestBuilder;
232}
233
234/// Marker trait for auto implementation of Parameters for types that implement
235/// [`serde::Serialize`]
236pub trait ParametersExt: serde::Serialize {}
237
238impl<T: ParametersExt> Parameters for T {
239    fn write_parameters(&self, req: RequestBuilder) -> RequestBuilder {
240        req.query(self)
241    }
242}
243
244/// Body for a request
245pub trait Body {
246    /// Write body to request builder and return request builder
247    fn write_body(&self, req: RequestBuilder) -> RequestBuilder;
248}
249
250/// Marker trait for auto implementation of Body for types that implement
251/// [`serde::Serialize`]
252pub trait BodyExt: serde::Serialize {}
253
254impl<T: BodyExt> Body for T {
255    fn write_body(&self, req: RequestBuilder) -> RequestBuilder {
256        req.json(self)
257    }
258}
259
260/// Represents a request that can be made to the twitch api
261#[async_trait]
262#[cfg_attr(feature = "nightly", doc(spotlight))]
263pub trait Request {
264    /// Endpoint where the request is made
265    const ENDPOINT: &'static str;
266
267    /// The type that represents the headers sent with this request
268    type Headers: Headers;
269
270    /// The type that represents the query parameters sent with this request
271    type Parameters: Parameters;
272
273    /// The type that represents the body of this request
274    type Body: Body;
275
276    /// The type returned by a sucessful request, must be [`DeserializeOwned`]
277    /// and have at least a static lifetime (owned).
278    type Response: Response + 'static;
279
280    /// The type that encapsulates the error codes that this endpoint can return,
281    /// must have at least a static lifetime (owned).
282    type ErrorCodes: ErrorCodes + 'static;
283
284    /// The method that this request will use
285    const METHOD: reqwest::Method;
286
287    /// Get a builder for this method
288    fn builder() -> Self;
289
290    /// Get the Headers struct for this Request
291    ///
292    /// Will only be called when [`Self::ready`] returns `Ok(())` and may not fail
293    /// in that case
294    fn headers(&self) -> &Self::Headers;
295
296    /// Get the Parameters struct for this Request
297    ///
298    /// Will only be called when [`Self::ready`] returns `Ok(())` and may not fail
299    /// in that case
300    fn parameters(&self) -> &Self::Parameters;
301
302    /// Get the Body struct for this Request
303    ///
304    /// Will only be called when [`Self::ready`] returns `Ok(())` and may not fail
305    /// in that case
306    fn body(&self) -> &Self::Body;
307
308    /// Must return `Ok(())` if and only if this request is ready to have
309    /// [`Self::make_request`] called on it.
310    ///
311    /// Should return [`RequestError::MalformedRequest`] with a message in the case
312    /// that the request is not ready to be sent.
313    ///
314    /// Called by [`Self::make_request`], error is propogated.
315    fn ready(&self) -> Result<(), RequestError<Self::ErrorCodes>>;
316
317    /// Make the request represented by this object. Only makes request if [`Self::ready`] returns
318    /// `Ok(())`.
319    async fn make_request<C>(
320        &self,
321        client: C,
322    ) -> Result<Self::Response, RequestError<Self::ErrorCodes>>
323    where
324        C: std::borrow::Borrow<Client> + Send,
325    {
326        // Make sure request thinks that it is ready to be sent
327        self.ready()?;
328
329        // Build request with method and endpoint
330        let mut req = client.borrow().request(Self::METHOD, Self::ENDPOINT);
331
332        // add headers, body, and params
333        req = self.headers().write_headers(req);
334        req = self.parameters().write_parameters(req);
335        req = self.body().write_body(req);
336
337        log::info!("Making request {:#?}", req);
338
339        // send
340        let resp = req.send().await?;
341
342        log::info!("Got response {:#?}", resp);
343
344        resp.json::<PossibleResponse<Self::Response>>()
345            .await?
346            .into_result()
347            .map_err(FailureStatus::into)
348    }
349}
350
351/// Type that is returned by a sucessful request
352pub trait Response: DeserializeOwned + Sized {}
353
354// Auto impl for types that are already [`DeserializeOwned`]
355impl<T: DeserializeOwned> Response for T {}