1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
#![doc(html_root_url = "https://docs.rs/twitter-client/0.0.1")]

pub mod auth;
pub mod error;
pub mod request;
pub mod response;
pub mod traits;

mod util;

pub use crate::error::{Error, ErrorCode};
pub use crate::request::Request;
pub use crate::response::Response;

#[derive(Clone, Copy, Debug)]
pub struct RateLimit {
    pub limit: u64,
    pub remaining: u64,
    pub reset: u64,
}

impl RateLimit {
    fn from_headers(headers: &http::HeaderMap) -> Option<Self> {
        pub const RATE_LIMIT_LIMIT: &str = "x-rate-limit-limit";
        pub const RATE_LIMIT_REMAINING: &str = "x-rate-limit-remaining";
        pub const RATE_LIMIT_RESET: &str = "x-rate-limit-reset";

        fn header(headers: &http::HeaderMap, name: &str) -> Option<u64> {
            headers
                .get(name)
                .and_then(|value| atoi::atoi(value.as_bytes()))
        }

        Some(RateLimit {
            limit: header(headers, RATE_LIMIT_LIMIT)?,
            remaining: header(headers, RATE_LIMIT_REMAINING)?,
            reset: header(headers, RATE_LIMIT_RESET)?,
        })
    }
}

#[cfg(test)]
mod tests {
    use std::convert::Infallible;

    use hyper::Body;
    use oauth_credentials::Token;
    use tower::ServiceExt;

    use crate::request::RawRequest;

    use super::*;

    #[tokio::test]
    async fn parse_errors() {
        #[derive(oauth::Request)]
        struct Foo {
            param: u32,
        }

        impl RawRequest for Foo {
            fn method(&self) -> &http::Method {
                &http::Method::GET
            }

            fn uri(&self) -> &'static str {
                "https://api.twitter.com/1.1/test/foo.json"
            }
        }

        let token = Token::from_parts("", "", "", "");

        let (http, mut handle) = tower_test::mock::pair::<http::Request<Vec<u8>>, _>();
        let mut http = http.map_err(|e| -> Infallible { panic!("{:?}", e) });

        let res_fut =
            tokio::spawn(Foo { param: 42 }.send_raw(&token, http.ready_and().await.unwrap()));

        let (_req, tx) = handle.next_request().await.unwrap();
        let payload = br#"{"errors":[{"code":104,"message":"You aren't allowed to add members to this list."}]}"#;
        let res = http::Response::builder()
            .status(http::StatusCode::FORBIDDEN)
            .body(Body::from(&payload[..]))
            .unwrap();
        tx.send_response(res);

        match res_fut.await.unwrap().unwrap_err() {
            Error::Twitter(crate::error::TwitterErrors {
                status: http::StatusCode::FORBIDDEN,
                errors,
                rate_limit: None,
            }) => {
                assert_eq!(errors.len(), 1);
                assert_eq!(errors[0].code, 104);
                assert_eq!(
                    errors[0].message,
                    "You aren't allowed to add members to this list.",
                );
            }
            e => panic!("{:?}", e),
        };
    }
}