github_rust/github/
client.rs1use crate::{config::*, error::*};
2use reqwest::{Client, header::HeaderMap};
3use secrecy::{ExposeSecret, SecretString};
4use std::env;
5
6#[derive(Clone)]
11pub struct GitHubClient {
12 client: Client,
13 token: Option<SecretString>,
15}
16
17impl GitHubClient {
18 #[must_use = "Creating a client without using it is wasteful"]
24 pub fn new() -> Result<Self> {
25 let token: Option<SecretString> = env::var("GITHUB_TOKEN").ok().map(SecretString::from);
26 let mut headers = HeaderMap::new();
27
28 headers.insert(
29 "User-Agent",
30 USER_AGENT
31 .parse()
32 .map_err(|_| GitHubError::ConfigError("Invalid User-Agent header".to_string()))?,
33 );
34 headers.insert(
35 "Accept",
36 "application/vnd.github+json"
37 .parse()
38 .map_err(|_| GitHubError::ConfigError("Invalid Accept header".to_string()))?,
39 );
40 headers.insert(
41 "X-GitHub-Api-Version",
42 "2022-11-28"
43 .parse()
44 .map_err(|_| GitHubError::ConfigError("Invalid API version header".to_string()))?,
45 );
46
47 if let Some(ref token) = token {
48 let auth_value = format!("Bearer {}", token.expose_secret());
49 headers.insert(
50 "Authorization",
51 auth_value.parse().map_err(|_| {
52 GitHubError::ConfigError("Invalid Authorization header".to_string())
53 })?,
54 );
55 }
56
57 let client = Client::builder()
58 .timeout(DEFAULT_TIMEOUT)
59 .default_headers(headers)
60 .build()
61 .map_err(|e| GitHubError::NetworkError(e.to_string()))?;
62
63 Ok(Self { client, token })
64 }
65
66 #[must_use]
67 pub fn has_token(&self) -> bool {
68 self.token.is_some()
69 }
70
71 #[must_use]
72 pub fn client(&self) -> &Client {
73 &self.client
74 }
75
76 pub async fn check_rate_limit(&self) -> Result<RateLimit> {
77 let response = self
78 .client
79 .get(format!("{}/rate_limit", GITHUB_API_URL))
80 .send()
81 .await?;
82
83 if response.status() == 403 {
84 let error_text = response.text().await.unwrap_or_default();
87 let error_lower = error_text.to_lowercase();
88
89 if error_lower.contains("rate limit") || error_lower.is_empty() {
90 return Err(GitHubError::RateLimitError(
92 "API rate limit exceeded".to_string(),
93 ));
94 } else if error_lower.contains("repository access blocked")
95 || error_lower.contains("access blocked")
96 {
97 return Err(GitHubError::AccessBlockedError(
98 "Rate limit check blocked".to_string(),
99 ));
100 } else {
101 return Err(GitHubError::AuthenticationError(format!(
102 "Access denied for rate limit check: {}",
103 error_text
104 )));
105 }
106 }
107
108 let rate_limit_response: serde_json::Value = response.json().await?;
109 let rate = &rate_limit_response["rate"];
110
111 Ok(RateLimit {
112 limit: rate["limit"].as_u64().unwrap_or(0),
113 remaining: rate["remaining"].as_u64().unwrap_or(0),
114 reset: rate["reset"].as_u64().unwrap_or(0),
115 })
116 }
117}
118
119#[derive(Debug, Clone)]
124pub struct RateLimit {
125 pub limit: u64,
127 pub remaining: u64,
129 pub reset: u64,
131}
132
133impl RateLimit {
134 #[must_use]
136 pub fn reset_datetime(&self) -> chrono::DateTime<chrono::Utc> {
137 chrono::DateTime::from_timestamp(self.reset as i64, 0).unwrap_or_else(chrono::Utc::now)
138 }
139
140 #[must_use]
142 pub fn time_until_reset(&self) -> std::time::Duration {
143 let now = chrono::Utc::now().timestamp() as u64;
144 if self.reset > now {
145 std::time::Duration::from_secs(self.reset - now)
146 } else {
147 std::time::Duration::ZERO
148 }
149 }
150
151 #[must_use]
153 pub fn is_exceeded(&self) -> bool {
154 self.remaining == 0
155 }
156
157 #[must_use]
159 pub fn used(&self) -> u64 {
160 self.limit.saturating_sub(self.remaining)
161 }
162}