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}