gfas_api/
lib.rs

1//! This crate exports some GitHub API bindings through [`GitHub`].
2
3use std::collections::HashSet;
4use std::ops::{Deref, DerefMut};
5
6use octorust::auth::Credentials;
7use octorust::Client;
8use tracing::{info, instrument, Level};
9
10type Result<T> = std::result::Result<T, octorust::ClientError>;
11
12/// Asynchronous GitHub API bindings that wraps [`octorust::Client`] internally.
13///
14/// # Examples
15///
16/// ```rust
17/// use gfas_api::GitHub;
18///
19/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
20/// let github = GitHub::new(String::from("<TOKEN>"))?;
21/// # Ok(())
22/// # }
23/// ```
24#[derive(Clone)]
25#[repr(transparent)]
26pub struct GitHub(Client);
27
28impl Deref for GitHub {
29    type Target = Client;
30
31    fn deref(&self) -> &Self::Target {
32        &self.0
33    }
34}
35
36impl DerefMut for GitHub {
37    fn deref_mut(&mut self) -> &mut Self::Target {
38        &mut self.0
39    }
40}
41
42impl GitHub {
43    const USER_AGENT: &'static str = "gfas";
44
45    /// Create a new GitHub API client.
46    pub fn new(token: String) -> Result<Self> {
47        Ok(Self(Client::new(Self::USER_AGENT, Credentials::Token(token))?))
48    }
49
50    /// Paginates through the given user profile link and returns
51    /// discovered followings/followers collected in [`HashSet`].
52    ///
53    /// # Errors
54    ///
55    /// Fails if an error occurs during sending requests.
56    #[instrument(skip(self), ret(level = Level::TRACE), err)]
57    pub async fn explore(&self, user: &str, following: bool) -> Result<HashSet<String>> {
58        let mut res = HashSet::new();
59
60        const PER_PAGE: i64 = 100;
61
62        let users = self.users();
63
64        for page in 1.. {
65            let response = if following {
66                users.list_following_for_user(user, PER_PAGE, page).await
67            } else {
68                users.list_followers_for_user(user, PER_PAGE, page).await
69            }?;
70
71            let explored = response.body.into_iter().map(|u| u.login);
72
73            let len = explored.len() as i64;
74
75            res.extend(explored);
76
77            info!("{}(+{len})", res.len());
78
79            if len < PER_PAGE {
80                break;
81            }
82        }
83
84        Ok(res)
85    }
86}