Skip to content

Commit d35d6b5

Browse files
unhappychoiceclaude
andcommitted
refactor: extract OSS Insight API client to infrastructure/http
- Add OSS Insight API client to infrastructure/http module - Update trending.rs to use new API client - Update trending_unified_view.rs to use new API structure 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 27486c0 commit d35d6b5

File tree

5 files changed

+140
-113
lines changed

5 files changed

+140
-113
lines changed

src/infrastructure/http/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
pub mod oss_insight_client;
2+
3+
pub use oss_insight_client::OssInsightClient;
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
use reqwest::Client;
2+
use serde::Deserialize;
3+
use std::time::Duration;
4+
5+
use crate::domain::error::{GitTypeError, Result};
6+
use crate::infrastructure::cache::TrendingRepository;
7+
8+
#[derive(Debug, Clone)]
9+
pub struct OssInsightClient {
10+
client: Client,
11+
}
12+
13+
impl OssInsightClient {
14+
pub fn new() -> Self {
15+
Self {
16+
client: Client::new(),
17+
}
18+
}
19+
20+
pub async fn fetch_trending_repositories(
21+
&self,
22+
language: Option<&str>,
23+
period: &str,
24+
) -> Result<Vec<TrendingRepository>> {
25+
let api_period = match period {
26+
"daily" => "past_24_hours",
27+
"weekly" => "past_week",
28+
"monthly" => "past_month",
29+
_ => "past_24_hours",
30+
};
31+
32+
let mut url = format!(
33+
"https://api.ossinsight.io/v1/trends/repos/?period={}",
34+
api_period
35+
);
36+
37+
if let Some(lang) = language {
38+
let api_lang = self.map_language_name(lang);
39+
url = format!("{}&language={}", url, urlencoding::encode(&api_lang));
40+
}
41+
42+
let response = self
43+
.client
44+
.get(&url)
45+
.header("User-Agent", "gittype")
46+
.header("Accept", "application/json")
47+
.timeout(Duration::from_secs(10))
48+
.send()
49+
.await?;
50+
51+
if !response.status().is_success() {
52+
return Err(GitTypeError::ApiError(format!(
53+
"OSS Insight API request failed: {}",
54+
response.status()
55+
)));
56+
}
57+
58+
let api_response: ApiResponse = response.json().await?;
59+
let repositories = self.convert_api_response(api_response);
60+
61+
Ok(repositories)
62+
}
63+
64+
fn map_language_name(&self, lang: &str) -> String {
65+
match lang.to_lowercase().as_str() {
66+
"javascript" => "JavaScript".to_string(),
67+
"typescript" => "TypeScript".to_string(),
68+
"c++" => "C++".to_string(),
69+
"c#" => "C#".to_string(),
70+
"php" => "PHP".to_string(),
71+
_ => {
72+
let mut chars = lang.chars();
73+
match chars.next() {
74+
None => lang.to_string(),
75+
Some(first) => {
76+
first.to_uppercase().collect::<String>() + &chars.as_str().to_lowercase()
77+
}
78+
}
79+
}
80+
}
81+
}
82+
83+
fn convert_api_response(&self, api_response: ApiResponse) -> Vec<TrendingRepository> {
84+
api_response
85+
.data
86+
.rows
87+
.into_iter()
88+
.map(|row| TrendingRepository {
89+
repo_name: row.repo_name,
90+
primary_language: row.primary_language,
91+
description: row.description,
92+
stars: row.stars.unwrap_or_else(|| "0".to_string()),
93+
forks: row.forks.unwrap_or_else(|| "0".to_string()),
94+
total_score: row.total_score.unwrap_or_else(|| "0".to_string()),
95+
})
96+
.collect()
97+
}
98+
}
99+
100+
impl Default for OssInsightClient {
101+
fn default() -> Self {
102+
Self::new()
103+
}
104+
}
105+
106+
#[derive(Deserialize)]
107+
struct ApiResponse {
108+
data: ApiData,
109+
}
110+
111+
#[derive(Deserialize)]
112+
struct ApiData {
113+
rows: Vec<RowData>,
114+
}
115+
116+
#[derive(Deserialize)]
117+
struct RowData {
118+
repo_name: String,
119+
primary_language: Option<String>,
120+
description: Option<String>,
121+
stars: Option<String>,
122+
forks: Option<String>,
123+
total_score: Option<String>,
124+
}

src/infrastructure/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
pub mod cache;
22
pub mod config;
33
pub mod external;
4+
pub mod git;
5+
pub mod http;
46
pub mod logging;
57
pub mod repository_manager;
68
pub mod storage;

src/presentation/cli/commands/trending.rs

Lines changed: 10 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
use crate::infrastructure::cache::{TrendingRepository, TRENDING_CACHE};
2+
use crate::infrastructure::http::OssInsightClient;
23
use crate::presentation::cli::commands::run_game_session;
34
use crate::presentation::cli::views::{trending_repository_selection_view, trending_unified_view};
45
use crate::presentation::cli::Cli;
56
use crate::{GitTypeError, Result};
6-
use reqwest::Client;
7-
use std::time::Duration;
87

98
const SUPPORTED_LANGUAGES: &[(&str, &str)] = &[
109
("C", "C"),
@@ -52,9 +51,8 @@ pub async fn run_trending(
5251
}
5352
if let Some(name) = repo_name {
5453
// Direct repository selection by name
55-
let client = Client::new();
5654
let repos =
57-
fetch_trending_repositories_cached(&client, language.as_deref(), &period).await?;
55+
fetch_trending_repositories_cached(&(), language.as_deref(), &period).await?;
5856

5957
if let Some(repo) = select_repository_by_name(&repos, &name) {
6058
let repo_url = format!("https://github.com/{}", repo.repo_name);
@@ -72,9 +70,8 @@ pub async fn run_trending(
7270
}
7371
} else if language.is_some() {
7472
// Language provided - show repositories directly
75-
let client = Client::new();
7673
let repos =
77-
fetch_trending_repositories_cached(&client, language.as_deref(), &period).await?;
74+
fetch_trending_repositories_cached(&(), language.as_deref(), &period).await?;
7875

7976
if repos.is_empty() {
8077
return Ok(());
@@ -116,132 +113,34 @@ pub async fn run_trending(
116113
Ok(())
117114
}
118115

119-
// Make this function public so it can be used from the UI
120116
pub async fn fetch_trending_repositories_cached(
121-
client: &Client,
117+
_client: &(),
122118
language: Option<&str>,
123119
period: &str,
124120
) -> Result<Vec<TrendingRepository>> {
125-
// Create cache key from parameters
126121
let cache_key = format!("{}:{}", language.unwrap_or("all"), period);
127122

128-
// Check cache first
129123
if let Some(cached_repos) = TRENDING_CACHE.get(&cache_key) {
130124
log::debug!("Using cached trending repositories for key: {}", cache_key);
131125
return Ok(cached_repos);
132126
}
133127

134-
// Clean up expired cache entries before making API call
135128
TRENDING_CACHE.cleanup_expired();
136129

137-
// Rate limiting: wait a bit between API calls to be respectful
138-
let rate_limit_ms = 100; // 100ms
139-
tokio::time::sleep(Duration::from_millis(rate_limit_ms)).await;
130+
let rate_limit_ms = 100;
131+
tokio::time::sleep(std::time::Duration::from_millis(rate_limit_ms)).await;
140132

141-
// Fetch from API
142-
let repos = fetch_trending_repositories(client, language, period).await?;
133+
let api_client = OssInsightClient::new();
134+
let repos = api_client
135+
.fetch_trending_repositories(language, period)
136+
.await?;
143137

144-
// Cache the result
145138
TRENDING_CACHE.set(&cache_key, repos.clone());
146139
log::debug!("Cached trending repositories for key: {}", cache_key);
147140

148141
Ok(repos)
149142
}
150143

151-
async fn fetch_trending_repositories(
152-
client: &Client,
153-
language: Option<&str>,
154-
period: &str,
155-
) -> Result<Vec<TrendingRepository>> {
156-
let api_period = match period {
157-
"daily" => "past_24_hours",
158-
"weekly" => "past_week",
159-
"monthly" => "past_month",
160-
_ => "past_24_hours",
161-
};
162-
163-
let mut url = format!(
164-
"https://api.ossinsight.io/v1/trends/repos/?period={}",
165-
api_period
166-
);
167-
168-
if let Some(lang) = language {
169-
// Map to correct language name format expected by API
170-
let api_lang = match lang.to_lowercase().as_str() {
171-
"javascript" => "JavaScript".to_string(),
172-
"typescript" => "TypeScript".to_string(),
173-
"c++" => "C++".to_string(),
174-
"c#" => "C#".to_string(),
175-
"php" => "PHP".to_string(),
176-
_ => {
177-
// Capitalize first letter for other languages
178-
let mut chars = lang.chars();
179-
match chars.next() {
180-
None => lang.to_string(),
181-
Some(first) => {
182-
first.to_uppercase().collect::<String>() + &chars.as_str().to_lowercase()
183-
}
184-
}
185-
}
186-
};
187-
url = format!("{}&language={}", url, urlencoding::encode(&api_lang));
188-
}
189-
190-
let response = client
191-
.get(&url)
192-
.header("User-Agent", "gittype")
193-
.header("Accept", "application/json")
194-
.timeout(Duration::from_secs(10))
195-
.send()
196-
.await?;
197-
198-
if !response.status().is_success() {
199-
return Err(GitTypeError::ApiError(format!(
200-
"OSS Insight API request failed: {}",
201-
response.status()
202-
)));
203-
}
204-
205-
#[derive(serde::Deserialize)]
206-
struct ApiResponse {
207-
data: ApiData,
208-
}
209-
210-
#[derive(serde::Deserialize)]
211-
struct ApiData {
212-
rows: Vec<RowData>,
213-
}
214-
215-
#[derive(serde::Deserialize)]
216-
struct RowData {
217-
repo_name: String,
218-
primary_language: Option<String>,
219-
description: Option<String>,
220-
stars: Option<String>,
221-
forks: Option<String>,
222-
total_score: Option<String>,
223-
}
224-
225-
let api_response: ApiResponse = response.json().await?;
226-
227-
// Convert API response to TrendingRepository objects
228-
let repositories: Vec<TrendingRepository> = api_response
229-
.data
230-
.rows
231-
.into_iter()
232-
.map(|row| TrendingRepository {
233-
repo_name: row.repo_name,
234-
primary_language: row.primary_language,
235-
description: row.description,
236-
stars: row.stars.unwrap_or_else(|| "0".to_string()),
237-
forks: row.forks.unwrap_or_else(|| "0".to_string()),
238-
total_score: row.total_score.unwrap_or_else(|| "0".to_string()),
239-
})
240-
.collect();
241-
242-
Ok(repositories)
243-
}
244-
245144
fn select_repository_by_name<'a>(
246145
repos: &'a [TrendingRepository],
247146
name: &str,

src/presentation/cli/views/trending_unified_view.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -380,11 +380,10 @@ pub async fn render_trending_selection_ui() -> Result<Option<String>> {
380380
})?;
381381

382382
// Now fetch repositories in background
383-
let client = reqwest::Client::new();
384383
let effective_period = "daily";
385384

386385
repositories = fetch_trending_repositories_cached(
387-
&client,
386+
&(),
388387
Some(lang_code),
389388
effective_period,
390389
)

0 commit comments

Comments
 (0)