pub mod custom;
pub mod discovery;
use chrono::Duration;
use serde::{Deserialize, Serialize};
use tracing::{debug, warn};
use crate::cache::FileCache;
use crate::config::load_config;
const EMBEDDED_REPOS: &str = include_str!("../../data/curated-repos.json");
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CuratedRepo {
pub owner: String,
pub name: String,
pub language: String,
pub description: String,
}
impl CuratedRepo {
#[must_use]
pub fn full_name(&self) -> String {
format!("{}/{}", self.owner, self.name)
}
}
fn embedded_defaults() -> Vec<CuratedRepo> {
serde_json::from_str(EMBEDDED_REPOS).expect("embedded repos JSON is valid")
}
async fn fetch_from_remote(url: &str) -> crate::Result<Vec<CuratedRepo>> {
debug!("Fetching curated repositories from {}", url);
let response = reqwest::Client::new().get(url).send().await?;
if let Ok(repos) = response.json::<Vec<CuratedRepo>>().await {
Ok(repos)
} else {
warn!("Failed to parse remote curated repositories, using embedded defaults");
Ok(embedded_defaults())
}
}
pub async fn fetch() -> crate::Result<Vec<CuratedRepo>> {
let config = load_config()?;
let url = &config.cache.curated_repos_url;
let ttl = Duration::hours(config.cache.repo_ttl_hours);
let cache: crate::cache::FileCacheImpl<Vec<CuratedRepo>> =
crate::cache::FileCacheImpl::new("repos", ttl);
if let Ok(Some(repos)) = cache.get("curated_repos") {
debug!("Using cached curated repositories");
return Ok(repos);
}
let repos = fetch_from_remote(url).await?;
let _ = cache.set("curated_repos", &repos);
debug!("Fetched and cached {} curated repositories", repos.len());
Ok(repos)
}
#[derive(Debug, Clone, Copy)]
pub enum RepoFilter {
All,
Curated,
Custom,
}
fn add_filtered_repos(
repos: &mut Vec<CuratedRepo>,
seen: &mut std::collections::HashSet<String>,
new_repos: Vec<CuratedRepo>,
) {
for repo in new_repos {
if seen.insert(repo.full_name()) {
repos.push(repo);
}
}
}
pub async fn fetch_all(filter: RepoFilter) -> crate::Result<Vec<CuratedRepo>> {
let config = load_config()?;
let mut repos = Vec::new();
let mut seen = std::collections::HashSet::new();
match filter {
RepoFilter::All | RepoFilter::Curated => {
if config.repos.curated {
let curated = fetch().await?;
add_filtered_repos(&mut repos, &mut seen, curated);
}
}
RepoFilter::Custom => {}
}
match filter {
RepoFilter::All | RepoFilter::Custom => {
let custom = custom::read_custom_repos()?;
add_filtered_repos(&mut repos, &mut seen, custom);
}
RepoFilter::Curated => {}
}
debug!(
"Fetched {} repositories with filter {:?}",
repos.len(),
filter
);
Ok(repos)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn full_name_format() {
let repo = CuratedRepo {
owner: "owner".to_string(),
name: "repo".to_string(),
language: "Rust".to_string(),
description: "Test repository".to_string(),
};
assert_eq!(repo.full_name(), "owner/repo");
}
#[test]
fn embedded_defaults_returns_non_empty() {
let repos = embedded_defaults();
assert!(
!repos.is_empty(),
"embedded defaults should contain repositories"
);
}
}