gitfetch_rs/fetcher/
gitlab.rs1use super::Fetcher;
2use anyhow::Result;
3use async_trait::async_trait;
4use serde_json::Value;
5
6pub struct GitLabFetcher {
7 client: reqwest::Client,
8 base_url: String,
9 token: Option<String>,
10}
11
12impl GitLabFetcher {
13 pub fn new(base_url: &str, token: Option<&str>) -> Result<Self> {
14 Ok(Self {
15 client: reqwest::Client::new(),
16 base_url: base_url.trim_end_matches('/').to_string(),
17 token: token.map(String::from),
18 })
19 }
20
21 fn api_request(&self, endpoint: &str) -> Result<Value> {
22 let url = format!("{}/api/v4{}", self.base_url, endpoint);
23
24 let mut req = self.client.get(&url);
25
26 if let Some(token) = &self.token {
27 req = req.header("PRIVATE-TOKEN", token);
28 }
29
30 let rt = tokio::runtime::Runtime::new()?;
31 let response =
32 rt.block_on(async { req.timeout(std::time::Duration::from_secs(30)).send().await })?;
33
34 if !response.status().is_success() {
35 return Err(anyhow::anyhow!(
36 "GitLab API request failed: {}",
37 response.status()
38 ));
39 }
40
41 let rt = tokio::runtime::Runtime::new()?;
42 let data = rt.block_on(async { response.json::<Value>().await })?;
43
44 Ok(data)
45 }
46}
47
48#[async_trait]
49impl Fetcher for GitLabFetcher {
50 async fn get_authenticated_user(&self) -> Result<String> {
51 if self.token.is_none() {
52 return Err(anyhow::anyhow!("Token required for GitLab authentication"));
53 }
54
55 let data = self.api_request("/user")?;
56 data["username"]
57 .as_str()
58 .map(String::from)
59 .ok_or_else(|| anyhow::anyhow!("Could not get authenticated user"))
60 }
61
62 async fn fetch_user_data(&self, username: &str) -> Result<Value> {
63 let users = self.api_request(&format!("/users?username={}", username))?;
65
66 if let Some(user_array) = users.as_array() {
67 if let Some(user) = user_array.first() {
68 return Ok(user.clone());
69 }
70 }
71
72 Err(anyhow::anyhow!("User not found: {}", username))
73 }
74
75 async fn fetch_user_stats(&self, username: &str, user_data: Option<&Value>) -> Result<Value> {
76 let user = if let Some(data) = user_data {
77 data.clone()
78 } else {
79 self.fetch_user_data(username).await?
80 };
81
82 let user_id = user["id"]
83 .as_u64()
84 .ok_or_else(|| anyhow::anyhow!("Invalid user ID"))?;
85
86 let mut repos = Vec::new();
88 let mut page = 1;
89 let per_page = 100;
90
91 loop {
92 let endpoint = format!(
93 "/users/{}/projects?page={}&per_page={}",
94 user_id, page, per_page
95 );
96 let data = self.api_request(&endpoint)?;
97
98 let data_array = match data.as_array() {
99 Some(arr) if !arr.is_empty() => arr,
100 _ => break,
101 };
102
103 repos.extend(data_array.clone());
104 page += 1;
105
106 if data_array.len() < per_page {
107 break;
108 }
109 }
110
111 let total_stars: i64 = repos.iter().filter_map(|r| r["star_count"].as_i64()).sum();
113
114 let total_forks: i64 = repos.iter().filter_map(|r| r["forks_count"].as_i64()).sum();
115
116 let languages = self.calculate_language_stats(&repos);
118
119 Ok(serde_json::json!({
122 "total_stars": total_stars,
123 "total_forks": total_forks,
124 "total_repos": repos.len(),
125 "languages": languages,
126 "contribution_graph": [],
127 "current_streak": 0,
128 "longest_streak": 0,
129 "total_contributions": 0,
130 "pull_requests": {
131 "open": 0,
132 "awaiting_review": 0,
133 "mentions": 0
134 },
135 "issues": {
136 "assigned": 0,
137 "created": 0,
138 "mentions": 0
139 },
140 }))
141 }
142}
143
144impl GitLabFetcher {
145 fn calculate_language_stats(&self, repos: &[Value]) -> Value {
146 use std::collections::HashMap;
147
148 let mut language_counts: HashMap<String, i32> = HashMap::new();
149
150 for repo in repos {
151 if let Some(language) = repo["language"].as_str() {
152 if !language.is_empty() {
153 let normalized = language.to_lowercase();
154 *language_counts.entry(normalized).or_insert(0) += 1;
155 }
156 }
157 }
158
159 let total: i32 = language_counts.values().sum();
160 if total == 0 {
161 return serde_json::json!({});
162 }
163
164 let mut language_percentages: HashMap<String, f64> = HashMap::new();
165 for (lang, count) in language_counts {
166 let percentage = (count as f64 / total as f64) * 100.0;
167 let display_name = lang
168 .chars()
169 .enumerate()
170 .map(|(i, c)| {
171 if i == 0 {
172 c.to_uppercase().to_string()
173 } else {
174 c.to_string()
175 }
176 })
177 .collect::<String>();
178 language_percentages.insert(display_name, percentage);
179 }
180
181 serde_json::to_value(language_percentages).unwrap_or_else(|_| serde_json::json!({}))
182 }
183}