github_rust/github/
graphql.rs1use crate::github::client::GitHubClient;
2use crate::github::types::*;
3use crate::{config::*, error::*};
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6
7#[derive(Deserialize, Serialize)]
12pub struct Repository {
13 pub id: String,
15 pub name: String,
17 #[serde(rename = "nameWithOwner")]
19 pub name_with_owner: String,
20 pub description: Option<String>,
22 pub url: String,
24 #[serde(rename = "homepageUrl")]
26 pub homepage_url: Option<String>,
27 #[serde(rename = "createdAt")]
29 pub created_at: String,
30 #[serde(rename = "updatedAt")]
32 pub updated_at: String,
33 #[serde(rename = "pushedAt")]
35 pub pushed_at: Option<String>,
36 #[serde(rename = "isPrivate")]
38 pub is_private: bool,
39 #[serde(rename = "isFork")]
41 pub is_fork: bool,
42 #[serde(rename = "isArchived")]
44 pub is_archived: bool,
45 #[serde(rename = "stargazerCount")]
47 pub stargazer_count: u32,
48 #[serde(rename = "forkCount")]
50 pub fork_count: u32,
51 pub watchers: TotalCount,
53 pub issues: TotalCount,
55 #[serde(rename = "pullRequests")]
57 pub pull_requests: TotalCount,
58 pub releases: TotalCount,
60 #[serde(rename = "primaryLanguage")]
62 pub primary_language: Option<Language>,
63 pub languages: LanguageConnection,
65 #[serde(rename = "licenseInfo")]
67 pub license_info: Option<License>,
68 #[serde(rename = "defaultBranchRef")]
70 pub default_branch_ref: Option<Branch>,
71 #[serde(rename = "repositoryTopics")]
73 pub repository_topics: TopicConnection,
74}
75
76impl Repository {
77 #[must_use]
94 pub fn language(&self) -> Option<&str> {
95 self.primary_language.as_ref().map(|l| l.name.as_str())
96 }
97
98 #[must_use]
100 pub fn license(&self) -> Option<&str> {
101 self.license_info.as_ref().map(|l| l.name.as_str())
102 }
103
104 #[must_use]
106 pub fn license_spdx(&self) -> Option<&str> {
107 self.license_info
108 .as_ref()
109 .and_then(|l| l.spdx_id.as_deref())
110 }
111
112 #[must_use]
114 pub fn default_branch(&self) -> Option<&str> {
115 self.default_branch_ref.as_ref().map(|b| b.name.as_str())
116 }
117
118 #[must_use]
135 pub fn topics(&self) -> Vec<&str> {
136 self.repository_topics
137 .edges
138 .iter()
139 .map(|e| e.node.topic.name.as_str())
140 .collect()
141 }
142
143 #[must_use]
145 pub fn owner(&self) -> &str {
146 self.name_with_owner
147 .split('/')
148 .next()
149 .unwrap_or(&self.name_with_owner)
150 }
151
152 #[must_use]
154 pub fn open_issues(&self) -> u32 {
155 self.issues.total_count
156 }
157
158 #[must_use]
160 pub fn watcher_count(&self) -> u32 {
161 self.watchers.total_count
162 }
163}
164
165#[derive(Deserialize)]
166struct RepositoryResponse {
167 repository: Option<Repository>,
168}
169
170pub async fn get_repository_info(
171 client: &GitHubClient,
172 owner: &str,
173 name: &str,
174) -> Result<Repository> {
175 let mut variables = HashMap::new();
176 variables.insert("owner".to_string(), owner.to_string());
177 variables.insert("name".to_string(), name.to_string());
178
179 let query: GraphQLQuery<HashMap<String, String>> = GraphQLQuery {
180 query: GRAPHQL_REPOSITORY_QUERY.to_string(),
181 variables,
182 };
183
184 let response = client
185 .client()
186 .post(GITHUB_GRAPHQL_URL)
187 .json(&query)
188 .send()
189 .await?;
190
191 let status = response.status();
192 if !status.is_success() {
193 let error_text = response.text().await.unwrap_or_default();
194 return match status.as_u16() {
195 401 => Err(GitHubError::AuthenticationError(
196 "Invalid or missing GitHub token".to_string(),
197 )),
198 403 => {
199 let error_lower = error_text.to_lowercase();
201 if error_lower.contains("rate limit")
202 || error_lower.contains("api rate limit exceeded")
203 {
204 Err(GitHubError::RateLimitError(
205 "GraphQL API rate limit exceeded".to_string(),
206 ))
207 } else if error_lower.contains("repository access blocked")
208 || error_lower.contains("access blocked")
209 || error_lower.contains("blocked")
210 {
211 Err(GitHubError::AccessBlockedError(format!(
212 "{}/{}",
213 owner, name
214 )))
215 } else {
216 Err(GitHubError::AuthenticationError(format!(
218 "Access denied to {}/{}: {}",
219 owner, name, error_text
220 )))
221 }
222 }
223 404 => Err(GitHubError::NotFoundError(format!("{}/{}", owner, name))),
224 451 => Err(GitHubError::DmcaBlockedError(format!("{}/{}", owner, name))),
225 _ => Err(GitHubError::ApiError {
226 status: status.as_u16(),
227 message: error_text,
228 }),
229 };
230 }
231
232 let graphql_response: GraphQLResponse<RepositoryResponse> = response.json().await?;
233
234 if let Some(errors) = graphql_response.errors {
235 let error_message = errors
236 .into_iter()
237 .map(|e| e.message)
238 .collect::<Vec<_>>()
239 .join(", ");
240 return Err(GitHubError::ApiError {
241 status: 200,
242 message: error_message,
243 });
244 }
245
246 match graphql_response.data {
247 Some(data) => match data.repository {
248 Some(repo) => Ok(repo),
249 None => Err(GitHubError::NotFoundError(format!("{}/{}", owner, name))),
250 },
251 None => Err(GitHubError::ParseError(
252 "No data in GraphQL response".to_string(),
253 )),
254 }
255}