twitter_client/
lib.rs

1#![doc(html_root_url = "https://docs.rs/twitter-client/0.0.1")]
2
3pub mod auth;
4pub mod error;
5pub mod request;
6pub mod response;
7pub mod traits;
8
9mod util;
10
11pub use crate::error::{Error, ErrorCode};
12pub use crate::request::Request;
13pub use crate::response::Response;
14
15#[derive(Clone, Copy, Debug)]
16pub struct RateLimit {
17    pub limit: u64,
18    pub remaining: u64,
19    pub reset: u64,
20}
21
22impl RateLimit {
23    fn from_headers(headers: &http::HeaderMap) -> Option<Self> {
24        pub const RATE_LIMIT_LIMIT: &str = "x-rate-limit-limit";
25        pub const RATE_LIMIT_REMAINING: &str = "x-rate-limit-remaining";
26        pub const RATE_LIMIT_RESET: &str = "x-rate-limit-reset";
27
28        fn header(headers: &http::HeaderMap, name: &str) -> Option<u64> {
29            headers
30                .get(name)
31                .and_then(|value| atoi::atoi(value.as_bytes()))
32        }
33
34        Some(RateLimit {
35            limit: header(headers, RATE_LIMIT_LIMIT)?,
36            remaining: header(headers, RATE_LIMIT_REMAINING)?,
37            reset: header(headers, RATE_LIMIT_RESET)?,
38        })
39    }
40}
41
42#[cfg(test)]
43mod tests {
44    use std::convert::Infallible;
45
46    use hyper::Body;
47    use oauth_credentials::Token;
48    use tower::ServiceExt;
49
50    use crate::request::RawRequest;
51
52    use super::*;
53
54    #[tokio::test]
55    async fn parse_errors() {
56        #[derive(oauth::Request)]
57        struct Foo {
58            param: u32,
59        }
60
61        impl RawRequest for Foo {
62            fn method(&self) -> &http::Method {
63                &http::Method::GET
64            }
65
66            fn uri(&self) -> &'static str {
67                "https://api.twitter.com/1.1/test/foo.json"
68            }
69        }
70
71        let token = Token::from_parts("", "", "", "");
72
73        let (http, mut handle) = tower_test::mock::pair::<http::Request<Vec<u8>>, _>();
74        let mut http = http.map_err(|e| -> Infallible { panic!("{:?}", e) });
75
76        let res_fut =
77            tokio::spawn(Foo { param: 42 }.send_raw(&token, http.ready_and().await.unwrap()));
78
79        let (_req, tx) = handle.next_request().await.unwrap();
80        let payload = br#"{"errors":[{"code":104,"message":"You aren't allowed to add members to this list."}]}"#;
81        let res = http::Response::builder()
82            .status(http::StatusCode::FORBIDDEN)
83            .body(Body::from(&payload[..]))
84            .unwrap();
85        tx.send_response(res);
86
87        match res_fut.await.unwrap().unwrap_err() {
88            Error::Twitter(crate::error::TwitterErrors {
89                status: http::StatusCode::FORBIDDEN,
90                errors,
91                rate_limit: None,
92            }) => {
93                assert_eq!(errors.len(), 1);
94                assert_eq!(errors[0].code, 104);
95                assert_eq!(
96                    errors[0].message,
97                    "You aren't allowed to add members to this list.",
98                );
99            }
100            e => panic!("{:?}", e),
101        };
102    }
103}