tatuin_providers/github/
client.rs1use tatuin_core::filter::FilterState;
4
5use super::structs::Issue;
6use itertools::Itertools;
7use reqwest::{Method, RequestBuilder, header::HeaderMap};
8use std::error::Error;
9
10pub struct Client {
11 base_url: String,
12 default_header: HeaderMap,
13 client: reqwest::Client,
14}
15
16impl Client {
17 pub fn new(api_key: &str) -> Self {
18 let mut headers = HeaderMap::new();
19 headers.insert("Authorization", format!("Bearer {api_key}").parse().unwrap());
20 headers.insert("X-GitHub-Api-Version", "2022-11-28".parse().unwrap());
21 headers.insert("Accept", "application/vnd.github+json".parse().unwrap());
22 headers.insert("User-Agent", "Tatuin".parse().unwrap());
23 Self {
24 base_url: "https://api.github.com".to_string(),
25 default_header: headers,
26 client: reqwest::Client::new(),
27 }
28 }
29
30 fn request(&self, method: Method, url: &str) -> RequestBuilder {
31 self.client.request(method, url).headers(self.default_header.clone())
32 }
33
34 pub async fn issues(&self, repo: &str, states: &[FilterState]) -> Result<Vec<Issue>, Box<dyn Error>> {
35 let mut result = Vec::new();
36
37 const PER_PAGE: i8 = 100;
38 let mut page = 1;
39
40 let state_query = if states.is_empty() {
41 "state=all".to_string()
42 } else {
43 states
44 .iter()
45 .map(|s| match s {
46 FilterState::Completed => "state=closed".to_string(),
47 FilterState::Todo => "state=open".to_string(),
48 _ => String::new(),
49 })
50 .filter(|s| !s.is_empty())
51 .join("&")
52 };
53
54 loop {
55 let url = format!(
56 "{}/repos/{repo}/issues?page={page}&per_page={PER_PAGE}&{state_query}",
57 self.base_url
58 );
59 match self.request(Method::GET, &url).send().await?.json::<Vec<Issue>>().await {
60 Ok(mut r) => {
61 if r.is_empty() {
62 break;
63 }
64
65 result.append(&mut r);
66 page += 1;
67 }
68 Err(e) => {
69 tracing::error!(target:"github_client", url=url, error=?e);
70 return Err(e.into());
71 }
72 }
73 }
74
75 Ok(result)
76 }
77}