github_star_counter/
request.rs

1use super::Error;
2use log::{error, info};
3use serde::{de::DeserializeOwned, Deserialize};
4use std::ops::AddAssign;
5use std::sync::atomic::{AtomicU64, Ordering};
6use std::sync::Mutex;
7use std::time::Duration;
8
9lazy_static! {
10    pub static ref TOTAL_DURATION: Mutex<Duration> = Mutex::new(Duration::default());
11    pub static ref TOTAL_BYTES_RECEIVED_IN_BODY: AtomicU64 = AtomicU64::default();
12}
13
14#[derive(Clone)]
15pub struct BasicAuth {
16    pub username: String,
17    pub password: Option<String>,
18}
19
20impl ToString for BasicAuth {
21    fn to_string(&self) -> String {
22        format!(
23            "Basic {}",
24            base64::encode(&match &self.password {
25                Some(password) => format!("{}:{}", self.username, password),
26                None => self.username.clone(),
27            })
28        )
29    }
30}
31
32pub async fn json_log_failure<D>(url: String, auth: Option<BasicAuth>) -> Result<D, Error>
33// TODO want Result<impl DeserializeOwned, ...> but that does not compile
34where
35    D: DeserializeOwned,
36{
37    match json(url, auth).await {
38        Ok(v) => Ok(v),
39        Err(e) => {
40            error!("{}", e);
41            Err(e)
42        }
43    }
44}
45
46// TODO: Can the url string also NOT be owned?
47pub async fn json<D>(url: String, auth: Option<BasicAuth>) -> Result<D, Error>
48// TODO want Result<impl DeserializeOwned, ...> but that does not compile
49where
50    D: DeserializeOwned,
51{
52    let url = format!("https://api.github.com/{}", url);
53    let mut req = surf::get(&url);
54    req = req.set_header("User-Agent", "GitHub StarCounter.rs");
55    if let Some(auth) = auth {
56        req = req.set_header("Authorization", auth.to_string());
57    }
58    info!("{} - requested", url);
59    let started = std::time::Instant::now();
60    let mut res = req.await.map_err(|e| e.to_string())?;
61    let status = res.status();
62    let bytes = res.body_bytes().await?;
63    let elapsed = started.elapsed();
64    info!(
65        "{} - received in {:?} ({})",
66        url,
67        elapsed,
68        bytesize::ByteSize(bytes.len() as u64)
69    );
70    TOTAL_DURATION.lock().unwrap().add_assign(elapsed);
71    TOTAL_BYTES_RECEIVED_IN_BODY.fetch_add(bytes.len() as u64, Ordering::Relaxed);
72
73    if status.is_success() {
74        Ok(serde_json::from_slice(&bytes)?)
75    } else {
76        #[derive(Deserialize)]
77        struct Error {
78            message: String,
79        }
80        let err: Error = serde_json::from_slice(&bytes).or_else(|e| {
81            Ok::<_, serde_json::Error>(Error {
82                message: format!(
83                    "Unexpected error message format returned by Github: '{:#?}'",
84                    e
85                ),
86            })
87        })?;
88        Err(err.message.into())
89    }
90}