github_stats/
search.rs

1use std::fmt;
2
3use serde::Deserialize;
4use serde_json::Value;
5
6use crate::Result;
7
8pub use query::Query;
9
10mod query;
11
12/// Uses [Github]'s search API.
13///
14/// # Example
15/// ## Get merged PRs
16///
17/// ```no_run
18/// # async fn run() {
19/// use github_stats::{Query, Search};
20///
21/// let query = Query::new()
22///     .repo("rust-lang", "rust")
23///     .is("pr")
24///     .is("merged");
25///
26/// let results = Search::issues(&query)
27///     .per_page(10)
28///     .page(1)
29///     .search("<my user agent>")
30///     .await;
31///
32/// match results {
33///     Ok(results) => { /* do stuff */ }
34///     Err(e) => eprintln!(":("),
35/// }
36/// # }
37/// ```
38///
39/// [Github]: https://github.com/
40pub struct Search {
41    search_area: SearchArea,
42    query: String,
43    per_page: usize,
44    page: usize,
45    authorization: Option<String>,
46}
47
48enum SearchArea {
49    Issues,
50    Users,
51}
52
53impl fmt::Display for SearchArea {
54    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
55        use SearchArea::*;
56        write!(f, "{}", match self {
57            Issues => "issues",
58            Users => "users",
59        })
60    }
61}
62
63#[derive(Debug, Deserialize)]
64pub struct SearchResults {
65    total_count: u64,
66    items: Vec<Value>,
67}
68
69impl Search {
70    fn new(search_area: SearchArea, query: &Query) -> Self {
71        Search {
72            search_area,
73            query: query.to_string(),
74            per_page: 10,
75            page: 1,
76            authorization: None,
77        }
78    }
79
80    pub fn issues(query: &Query) -> Self {
81        Search::new(SearchArea::Issues, query)
82    }
83
84    pub fn users(query: &Query) -> Self {
85        Search::new(SearchArea::Users, query)
86    }
87
88    /// Sets an authorization token for querying the API
89    pub fn authorization(mut self, token: &str) -> Self {
90        self.authorization = Some(String::from(token));
91        self
92    }
93
94    /// Gets the query that will be used for the search.
95    pub fn get_query(&self) -> &str {
96        &self.query
97    }
98
99    /// Defaults to 10.
100    pub fn per_page(mut self, per_page: usize) -> Self {
101        self.per_page = per_page;
102        self
103    }
104
105    /// Defaults to 1.
106    pub fn page(mut self, page: usize) -> Self {
107        self.page = page;
108        self
109    }
110
111    /// Moves one page forward.
112    pub fn next_page(&mut self) {
113        if self.page < std::usize::MAX {
114            self.page += 1;
115        }
116    }
117
118    /// Moves one page backward.
119    pub fn prev_page(&mut self) {
120        if self.page > std::usize::MIN {
121            self.page -= 1;
122        }
123    }
124
125    /// Runs the search.
126    pub async fn search(&self, user_agent: &str) -> Result<SearchResults> {
127        let request = reqwest::Client::builder()
128             .user_agent(user_agent)
129             .build()?
130             .get(&self.to_string());
131        let request = match &self.authorization {
132            Some(t) => request.header("Authorization", format!("Bearer {token}", token=t)),
133            None => request,
134        };
135        let results: SearchResults = request
136             .send()
137             .await?
138             .json()
139             .await?;
140        Ok(results)
141    }
142}
143
144impl SearchResults {
145    /// Gets total count of values matching query.
146    ///
147    /// This ignores `per_page`. If you only want the total count, it is
148    /// recommended that you set `per_page` to `1` to shrink results size.
149    pub fn total_count(&self) -> u64 {
150        self.total_count
151    }
152
153    /// Items matching the query.
154    pub fn items(&self) -> &Vec<Value> {
155        &self.items
156    }
157}
158
159impl fmt::Display for Search {
160    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
161        let search_area = match self.search_area {
162            SearchArea::Issues => "issues",
163            SearchArea::Users => "users",
164        };
165        write!(
166            f,
167            "https://api.github.com/search/{0}?per_page={1}&page={2}&q={3}",
168            search_area, self.per_page, self.page, self.query,
169        )
170    }
171}
172
173#[cfg(test)]
174mod tests {
175    use super::*;
176
177    #[test]
178    fn built_search() {
179        const EXPECTED: &str = "https://api.github.com/search/issues?per_page=1&page=1&q=repo:rust-lang/rust+is:pr+is:merged";
180        let search = Search::issues(
181            &Query::new().repo("rust-lang", "rust").is("pr").is("merged"),
182        )
183        .per_page(1);
184
185        assert_eq!(EXPECTED, search.to_string());
186    }
187}