egg_mode/common/
response.rs

1//! Infrastructure types related to packaging rate-limit information alongside responses from
2//! Twitter.
3
4use crate::error::Error::{self, *};
5use crate::error::{Result, TwitterErrors};
6
7use hyper::client::{HttpConnector, ResponseFuture};
8use hyper::{self, Body, Request};
9use serde::{de::DeserializeOwned, Deserialize};
10
11use std::convert::TryFrom;
12
13use super::Headers;
14
15const X_RATE_LIMIT_LIMIT: &str = "X-Rate-Limit-Limit";
16const X_RATE_LIMIT_REMAINING: &str = "X-Rate-Limit-Remaining";
17const X_RATE_LIMIT_RESET: &str = "X-Rate-Limit-Reset";
18
19fn rate_limit(headers: &Headers, header: &'static str) -> Result<Option<i32>> {
20    let val = headers.get(header);
21
22    if let Some(val) = val {
23        let val = val.to_str()?.parse::<i32>()?;
24        Ok(Some(val))
25    } else {
26        Ok(None)
27    }
28}
29
30fn rate_limit_limit(headers: &Headers) -> Result<Option<i32>> {
31    rate_limit(headers, X_RATE_LIMIT_LIMIT)
32}
33
34fn rate_limit_remaining(headers: &Headers) -> Result<Option<i32>> {
35    rate_limit(headers, X_RATE_LIMIT_REMAINING)
36}
37
38fn rate_limit_reset(headers: &Headers) -> Result<Option<i32>> {
39    rate_limit(headers, X_RATE_LIMIT_RESET)
40}
41
42// n.b. this type is re-exported at the crate root - these docs are public!
43///A helper struct to wrap response data with accompanying rate limit information.
44///
45///This is returned by any function that calls a rate-limited method on Twitter, to allow for
46///inline checking of the rate-limit information without an extra call to
47///`service::rate_limit_info`.
48///
49///As this implements `Deref` and `DerefMut`, you can transparently use the contained `response`'s
50///methods as if they were methods on this struct.
51#[derive(
52    Debug, Deserialize, derive_more::Constructor, derive_more::Deref, derive_more::DerefMut,
53)]
54pub struct Response<T> {
55    /// The latest rate-limit information returned with the request.
56    #[serde(flatten)]
57    pub rate_limit_status: RateLimit,
58    /// The decoded response from the request.
59    #[deref]
60    #[deref_mut]
61    #[serde(default)]
62    pub response: T,
63}
64
65impl<T> Response<T> {
66    ///Convert a `Response<T>` to a `Response<U>` by running its contained response through the
67    ///given function. This preserves its rate-limit information.
68    ///
69    ///Note that this is not a member function, so as to not conflict with potential methods on the
70    ///contained `T`.
71    pub fn map<F, U>(src: Response<T>, fun: F) -> Response<U>
72    where
73        F: FnOnce(T) -> U,
74    {
75        Response {
76            rate_limit_status: src.rate_limit_status,
77            response: fun(src.response),
78        }
79    }
80
81    ///Attempt to convert a `Response<T>` into a `Response<U>` by running its contained response
82    ///through the given function, preserving its rate-limit information. If the conversion
83    ///function fails, an error is returned instead.
84    ///
85    ///Note that this is not a member function, so as to not conflict with potential methods on the
86    ///contained `T`.
87    pub fn try_map<F, U, E>(src: Response<T>, fun: F) -> std::result::Result<Response<U>, E>
88    where
89        F: FnOnce(T) -> std::result::Result<U, E>,
90    {
91        Ok(Response {
92            rate_limit_status: src.rate_limit_status,
93            response: fun(src.response)?,
94        })
95    }
96
97    /// Converts a `Response<T>` into a `Response<U>` using the `Into` trait.
98    ///
99    /// This is implemented as a type function instead of the `From`/`Into` trait due to
100    /// implementation conflicts with the `From<T> for T` implementation in the standard library.
101    /// It is also implemented as a function directly on the `Response` type instead of as a member
102    /// function to not clash with the `into()` function that would be available on the contained
103    /// `T`.
104    pub fn into<U>(src: Self) -> Response<U>
105    where
106        T: Into<U>,
107    {
108        Response {
109            rate_limit_status: src.rate_limit_status,
110            response: src.response.into(),
111        }
112    }
113}
114
115impl<T: IntoIterator> IntoIterator for Response<T> {
116    type IntoIter = ResponseIter<T::IntoIter>;
117    type Item = Response<T::Item>;
118
119    fn into_iter(self) -> Self::IntoIter {
120        ResponseIter {
121            it: Response::map(self, |it| it.into_iter()),
122        }
123    }
124}
125
126/// Iterator wrapper around a `Response`.
127///
128/// This type is returned by `Response`'s `IntoIterator` implementation. It uses the `IntoIterator`
129/// implementation of the contained `T`, and copies the rate-limit information to yield individual
130/// `Response<T::Item>` instances.
131pub struct ResponseIter<T> {
132    it: Response<T>,
133}
134
135impl<T: Iterator> Iterator for ResponseIter<T> {
136    type Item = Response<T::Item>;
137
138    fn next(&mut self) -> Option<Self::Item> {
139        Some(Response {
140            rate_limit_status: self.it.rate_limit_status,
141            response: self.it.response.next()?,
142        })
143    }
144}
145
146#[cfg(not(any(feature = "native_tls", feature = "rustls", feature = "rustls_webpki")))]
147compile_error!(
148    "Crate `egg_mode` must be compiled with exactly one of the three \
149feature flags `native_tls`, `rustls` or `rustls_webpki` enabled, you attempted to \
150compile `egg_mode` with none of them enabled"
151);
152
153#[cfg(any(
154    all(
155        feature = "native_tls",
156        any(feature = "rustls", feature = "rustls_webpki")
157    ),
158    all(
159        feature = "rustls",
160        any(feature = "native_tls", feature = "rustls_webpki")
161    ),
162    all(
163        feature = "rustls_webpki",
164        any(feature = "native_tls", feature = "rustls")
165    ),
166))]
167compile_error!(
168    "features `egg_mode/native_tls`, `egg_mode/rustls` and \
169`egg_mode/rustls_webpki` are mutually exclusive, you attempted to compile `egg_mode` \
170with more than one of these feature flags enabled at the same time"
171);
172
173#[cfg(feature = "native_tls")]
174fn new_https_connector() -> hyper_tls::HttpsConnector<HttpConnector> {
175    hyper_tls::HttpsConnector::new()
176}
177
178#[cfg(feature = "rustls")]
179fn new_https_connector() -> hyper_rustls::HttpsConnector<HttpConnector> {
180    hyper_rustls::HttpsConnector::with_native_roots()
181}
182
183#[cfg(feature = "rustls_webpki")]
184fn new_https_connector() -> hyper_rustls::HttpsConnector<HttpConnector> {
185    hyper_rustls::HttpsConnector::with_webpki_roots()
186}
187
188// n.b. this function is re-exported in the `raw` module - these docs are public!
189/// Converts the given request into a raw `ResponseFuture` from hyper.
190pub fn get_response(request: Request<Body>) -> ResponseFuture {
191    let connector = new_https_connector();
192    let client = hyper::Client::builder().build(connector);
193    client.request(request)
194}
195
196// n.b. this function is re-exported in the `raw` module - these docs are public!
197/// Loads the given request, parses the headers and response for potential errors given by Twitter,
198/// and returns the headers and raw bytes returned from the response.
199pub async fn raw_request(request: Request<Body>) -> Result<(Headers, Vec<u8>)> {
200    let connector = new_https_connector();
201    let client = hyper::Client::builder().build(connector);
202    let resp = client.request(request).await?;
203    let (parts, body) = resp.into_parts();
204    let body: Vec<_> = hyper::body::to_bytes(body).await?.to_vec();
205    if let Ok(errors) = serde_json::from_slice::<TwitterErrors>(&body) {
206        if errors.errors.iter().any(|e| e.code == 88)
207            && parts.headers.contains_key(X_RATE_LIMIT_RESET)
208        {
209            return Err(RateLimit(rate_limit_reset(&parts.headers)?.unwrap()));
210        } else {
211            return Err(TwitterError(parts.headers, errors));
212        }
213    }
214    if !parts.status.is_success() {
215        return Err(BadStatus(parts.status));
216    }
217    Ok((parts.headers, body))
218}
219
220// n.b. this function is re-exported in the `raw` module - these docs are public!
221/// Loads the given request and discards the response body after parsing it for rate-limit and
222/// error information, returning the rate-limit information from the headers.
223pub async fn request_with_empty_response(request: Request<Body>) -> Result<Response<()>> {
224    let (headers, _) = raw_request(request).await?;
225    let rate_limit_status = RateLimit::try_from(&headers)?;
226    Ok(Response {
227        rate_limit_status,
228        response: (),
229    })
230}
231
232// n.b. this function is re-exported in the `raw` module - these docs are public!
233/// Loads the given request and parses the response as JSON into the given type, including
234/// rate-limit headers.
235pub async fn request_with_json_response<T: DeserializeOwned>(
236    request: Request<Body>,
237) -> Result<Response<T>> {
238    let (headers, body) = raw_request(request).await?;
239    let response = serde_json::from_slice(&body)?;
240    let rate_limit_status = RateLimit::try_from(&headers)?;
241    Ok(Response {
242        rate_limit_status,
243        response,
244    })
245}
246
247// n.b. this type is exported at the crate root - these docs are public!
248/// Rate limit information returned with a `Response`.
249///
250/// With every API call, Twitter returns information about how many times you're allowed to call
251/// that endpoint, and at what point your limit refreshes and allows you to call it more. These are
252/// normally passed through the response headers, and egg-mode reads for these headers when a
253/// function returns a `Response<T>`. If the headers are absent for a given request, the field will
254/// be `-1`.
255///
256/// Rate limits are tracked separately based on the kind of `Token` you're using. For Bearer tokens
257/// using Application-only authentication, the rate limit is based on your application as a whole,
258/// regardless of how many instances are using that token. For Access tokens, the rate limit is
259/// broken down by-user, so more-active users will not use up the rate limit for less-active ones.
260///
261/// For more information about rate-limiting, see [Twitter's documentation about rate
262/// limits][rate-limit].
263///
264/// [rate-limit]: https://developer.twitter.com/en/docs/basics/rate-limiting
265#[derive(Copy, Clone, Debug, Deserialize)]
266pub struct RateLimit {
267    /// The rate limit ceiling for the given request.
268    pub limit: i32,
269    /// The number of requests left for the 15-minute window.
270    pub remaining: i32,
271    /// The UTC Unix timestamp at which the rate window resets.
272    pub reset: i32,
273}
274
275impl TryFrom<&Headers> for RateLimit {
276    type Error = Error;
277    fn try_from(headers: &Headers) -> Result<Self> {
278        Ok(Self {
279            limit: rate_limit_limit(headers)?.unwrap_or(-1),
280            remaining: rate_limit_remaining(headers)?.unwrap_or(-1),
281            reset: rate_limit_reset(headers)?.unwrap_or(-1),
282        })
283    }
284}