github_rust/github/service.rs
1use crate::error::*;
2use crate::github::client::GitHubClient;
3use crate::github::graphql::{self, Repository};
4use crate::github::{rest, search};
5
6/// High-level service for GitHub API operations.
7///
8/// `GitHubService` is the main entry point for interacting with the GitHub API.
9/// It provides a simple, ergonomic interface with automatic fallback from GraphQL
10/// to REST API when needed.
11///
12/// # Authentication
13///
14/// The service automatically detects the `GITHUB_TOKEN` environment variable.
15/// - **With token**: 5,000 requests/hour, access to private repos
16/// - **Without token**: 60 requests/hour, public repos only
17///
18/// # Example
19///
20/// ```no_run
21/// use github_rust::GitHubService;
22///
23/// # async fn example() -> github_rust::Result<()> {
24/// let service = GitHubService::new()?;
25///
26/// // Check authentication status
27/// if service.has_token() {
28/// println!("Authenticated with higher rate limits");
29/// }
30///
31/// // Get repository information
32/// let repo = service.get_repository_info("rust-lang", "rust").await?;
33/// println!("{} has {} stars", repo.name_with_owner, repo.stargazer_count);
34/// # Ok(())
35/// # }
36/// ```
37pub struct GitHubService {
38 /// The underlying HTTP client with connection pooling.
39 pub client: GitHubClient,
40}
41
42impl GitHubService {
43 /// Creates a new GitHub service with default configuration.
44 ///
45 /// Automatically detects `GITHUB_TOKEN` from environment variables.
46 ///
47 /// # Errors
48 ///
49 /// Returns an error if the HTTP client cannot be initialized (rare, typically
50 /// indicates system-level TLS or network configuration issues).
51 ///
52 /// # Example
53 ///
54 /// ```no_run
55 /// use github_rust::GitHubService;
56 ///
57 /// let service = GitHubService::new()?;
58 /// # Ok::<(), github_rust::GitHubError>(())
59 /// ```
60 #[must_use = "Creating a service without using it is wasteful"]
61 pub fn new() -> Result<Self> {
62 let client = GitHubClient::new()?;
63 Ok(Self { client })
64 }
65
66 /// Creates a GitHub service with a custom client.
67 ///
68 /// Useful for testing or when you need custom HTTP configuration.
69 #[must_use]
70 pub fn with_client(client: GitHubClient) -> Self {
71 Self { client }
72 }
73
74 /// Fetches detailed information about a GitHub repository.
75 ///
76 /// Uses GraphQL API by default for efficiency, automatically falls back to
77 /// REST API if GraphQL fails (e.g., when no token is provided for certain queries).
78 ///
79 /// # Arguments
80 ///
81 /// * `owner` - Repository owner (username or organization)
82 /// * `name` - Repository name
83 ///
84 /// # Returns
85 ///
86 /// Full repository details including stars, forks, language, topics, license, etc.
87 ///
88 /// # Errors
89 ///
90 /// * [`GitHubError::NotFoundError`] - Repository doesn't exist or is private without auth
91 /// * [`GitHubError::RateLimitError`] - API rate limit exceeded
92 /// * [`GitHubError::AccessBlockedError`] - Repository access blocked by GitHub
93 /// * [`GitHubError::DmcaBlockedError`] - Repository blocked for legal reasons
94 ///
95 /// # Example
96 ///
97 /// ```no_run
98 /// # use github_rust::GitHubService;
99 /// # async fn example() -> github_rust::Result<()> {
100 /// let service = GitHubService::new()?;
101 /// let repo = service.get_repository_info("microsoft", "vscode").await?;
102 ///
103 /// println!("Name: {}", repo.name_with_owner);
104 /// println!("Stars: {}", repo.stargazer_count);
105 /// println!("Forks: {}", repo.fork_count);
106 /// if let Some(lang) = &repo.primary_language {
107 /// println!("Language: {}", lang.name);
108 /// }
109 /// # Ok(())
110 /// # }
111 /// ```
112 pub async fn get_repository_info(&self, owner: &str, name: &str) -> Result<Repository> {
113 match graphql::get_repository_info(&self.client, owner, name).await {
114 Ok(repo) => Ok(repo),
115 Err(GitHubError::AuthenticationError(_)) if !self.client.has_token() => {
116 tracing::debug!("GraphQL auth failed without token, falling back to REST");
117 rest::get_repository_info(&self.client, owner, name).await
118 }
119 Err(e) => {
120 tracing::debug!("GraphQL failed ({}), falling back to REST", e);
121 rest::get_repository_info(&self.client, owner, name).await
122 }
123 }
124 }
125
126 /// Searches for recently created repositories with filtering options.
127 ///
128 /// Finds repositories created within the specified time period, filtered by
129 /// language and minimum star count. Results are sorted by stars (descending).
130 ///
131 /// # Arguments
132 ///
133 /// * `days_back` - Search for repos created in the last N days
134 /// * `limit` - Maximum number of results (capped at 1000)
135 /// * `language` - Optional programming language filter (e.g., "rust", "python", "C++")
136 /// * `min_stars` - Minimum number of stars required
137 ///
138 /// # Errors
139 ///
140 /// * [`GitHubError::InvalidInput`] - Invalid language parameter
141 /// * [`GitHubError::RateLimitError`] - API rate limit exceeded
142 /// * [`GitHubError::AuthenticationError`] - Token required for this operation
143 ///
144 /// # Example
145 ///
146 /// ```no_run
147 /// # use github_rust::GitHubService;
148 /// # async fn example() -> github_rust::Result<()> {
149 /// let service = GitHubService::new()?;
150 ///
151 /// // Find Rust repos created in last 30 days with 50+ stars
152 /// let repos = service.search_repositories(
153 /// 30, // days back
154 /// 100, // limit
155 /// Some("rust"), // language
156 /// 50, // min stars
157 /// ).await?;
158 ///
159 /// for repo in repos {
160 /// println!("{}: {} stars", repo.name_with_owner, repo.stargazer_count);
161 /// }
162 /// # Ok(())
163 /// # }
164 /// ```
165 pub async fn search_repositories(
166 &self,
167 days_back: u32,
168 limit: usize,
169 language: Option<&str>,
170 min_stars: u32,
171 ) -> Result<Vec<search::SearchRepository>> {
172 search::search_repositories(&self.client, days_back, limit, language, min_stars).await
173 }
174
175 /// Checks the current GitHub API rate limit status.
176 ///
177 /// Useful for monitoring API usage and implementing backoff strategies.
178 ///
179 /// # Returns
180 ///
181 /// Rate limit information including limit, remaining requests, and reset time.
182 ///
183 /// # Example
184 ///
185 /// ```no_run
186 /// # use github_rust::GitHubService;
187 /// # async fn example() -> github_rust::Result<()> {
188 /// let service = GitHubService::new()?;
189 /// let limits = service.check_rate_limit().await?;
190 ///
191 /// println!("Remaining: {}/{}", limits.remaining, limits.limit);
192 /// println!("Resets at: {}", limits.reset_datetime());
193 ///
194 /// if limits.is_exceeded() {
195 /// println!("Rate limited! Wait {:?}", limits.time_until_reset());
196 /// }
197 /// # Ok(())
198 /// # }
199 /// ```
200 pub async fn check_rate_limit(&self) -> Result<crate::github::client::RateLimit> {
201 self.client.check_rate_limit().await
202 }
203
204 /// Returns whether a GitHub token is configured.
205 ///
206 /// Useful for conditional logic based on authentication status.
207 ///
208 /// # Example
209 ///
210 /// ```no_run
211 /// # use github_rust::GitHubService;
212 /// let service = GitHubService::new()?;
213 ///
214 /// if service.has_token() {
215 /// println!("Authenticated: 5000 requests/hour");
216 /// } else {
217 /// println!("Anonymous: 60 requests/hour");
218 /// }
219 /// # Ok::<(), github_rust::GitHubError>(())
220 /// ```
221 #[must_use]
222 pub fn has_token(&self) -> bool {
223 self.client.has_token()
224 }
225
226 /// Gets all repositories starred by the authenticated user.
227 ///
228 /// Requires authentication via `GITHUB_TOKEN`.
229 ///
230 /// # Returns
231 ///
232 /// List of repository full names in "owner/repo" format.
233 /// Limited to 10,000 repositories maximum.
234 ///
235 /// # Errors
236 ///
237 /// * [`GitHubError::AuthenticationError`] - No token or invalid token
238 /// * [`GitHubError::RateLimitError`] - API rate limit exceeded
239 ///
240 /// # Example
241 ///
242 /// ```no_run
243 /// # use github_rust::GitHubService;
244 /// # async fn example() -> github_rust::Result<()> {
245 /// let service = GitHubService::new()?;
246 /// let starred = service.get_user_starred_repositories().await?;
247 ///
248 /// println!("You have starred {} repositories", starred.len());
249 /// for repo in starred.iter().take(5) {
250 /// println!(" - {}", repo);
251 /// }
252 /// # Ok(())
253 /// # }
254 /// ```
255 pub async fn get_user_starred_repositories(&self) -> Result<Vec<String>> {
256 rest::get_user_starred_repositories(&self.client).await
257 }
258
259 /// Gets the profile of the authenticated user.
260 ///
261 /// Requires authentication via `GITHUB_TOKEN`.
262 ///
263 /// # Errors
264 ///
265 /// * [`GitHubError::AuthenticationError`] - No token or invalid token
266 ///
267 /// # Example
268 ///
269 /// ```no_run
270 /// # use github_rust::GitHubService;
271 /// # async fn example() -> github_rust::Result<()> {
272 /// let service = GitHubService::new()?;
273 /// let profile = service.get_user_profile().await?;
274 ///
275 /// println!("Logged in as: {}", profile.login);
276 /// if let Some(name) = profile.name {
277 /// println!("Name: {}", name);
278 /// }
279 /// # Ok(())
280 /// # }
281 /// ```
282 pub async fn get_user_profile(&self) -> Result<rest::UserProfile> {
283 rest::get_user_profile(&self.client).await
284 }
285
286 /// Gets users who starred a repository with timestamps.
287 ///
288 /// Returns stargazers with the date they starred the repository.
289 /// Supports pagination for repositories with many stars.
290 ///
291 /// # Arguments
292 ///
293 /// * `owner` - Repository owner
294 /// * `name` - Repository name
295 /// * `per_page` - Results per page (max 100, default 30)
296 /// * `page` - Page number (default 1)
297 ///
298 /// # Errors
299 ///
300 /// * [`GitHubError::NotFoundError`] - Repository not found
301 /// * [`GitHubError::RateLimitError`] - API rate limit exceeded
302 ///
303 /// # Example
304 ///
305 /// ```no_run
306 /// # use github_rust::GitHubService;
307 /// # async fn example() -> github_rust::Result<()> {
308 /// let service = GitHubService::new()?;
309 ///
310 /// // Get first 100 stargazers
311 /// let stargazers = service.get_repository_stargazers(
312 /// "rust-lang", "rust",
313 /// Some(100), // per_page
314 /// Some(1), // page
315 /// ).await?;
316 ///
317 /// for sg in stargazers {
318 /// println!("{} starred at {}", sg.user.login, sg.starred_at);
319 /// }
320 /// # Ok(())
321 /// # }
322 /// ```
323 pub async fn get_repository_stargazers(
324 &self,
325 owner: &str,
326 name: &str,
327 per_page: Option<u32>,
328 page: Option<u32>,
329 ) -> Result<Vec<crate::github::types::StargazerWithDate>> {
330 rest::get_repository_stargazers(&self.client, owner, name, per_page, page).await
331 }
332}
333
334#[cfg(test)]
335mod tests {
336 use super::*;
337
338 #[test]
339 fn test_github_service_creation() {
340 let service = GitHubService::new();
341 assert!(service.is_ok());
342 }
343
344 #[test]
345 fn test_github_service_has_token_detection() {
346 let service = GitHubService::new().unwrap();
347 // Token detection should work without panicking
348 let _has_token = service.has_token();
349 }
350}