meritocrab_api/
repo_config_loader.rs1use crate::error::{ApiError, ApiResult};
2use meritocrab_core::RepoConfig;
3use meritocrab_github::GithubApiClient;
4use std::collections::HashMap;
5use std::sync::Arc;
6use std::time::{Duration, Instant};
7use tokio::sync::RwLock;
8use tracing::{info, warn};
9
10#[derive(Debug, Clone)]
12struct CachedConfig {
13 config: RepoConfig,
14 fetched_at: Instant,
15}
16
17pub struct RepoConfigLoader {
23 github_client: Arc<GithubApiClient>,
24 cache: Arc<RwLock<HashMap<String, CachedConfig>>>,
25 cache_ttl: Duration,
26 default_config: RepoConfig,
27}
28
29impl RepoConfigLoader {
30 pub fn new(github_client: Arc<GithubApiClient>, cache_ttl_seconds: u64) -> Self {
36 Self {
37 github_client,
38 cache: Arc::new(RwLock::new(HashMap::new())),
39 cache_ttl: Duration::from_secs(cache_ttl_seconds),
40 default_config: RepoConfig::default(),
41 }
42 }
43
44 pub async fn get_config(&self, repo_owner: &str, repo_name: &str) -> RepoConfig {
49 let cache_key = format!("{}/{}", repo_owner, repo_name);
50
51 {
53 let cache_guard = self.cache.read().await;
54 if let Some(cached) = cache_guard.get(&cache_key) {
55 if cached.fetched_at.elapsed() < self.cache_ttl {
56 info!("Using cached config for {}/{}", repo_owner, repo_name);
57 return cached.config.clone();
58 }
59 }
60 }
61
62 info!("Fetching .meritocrab.toml for {}/{}", repo_owner, repo_name);
64
65 match self.fetch_config_from_github(repo_owner, repo_name).await {
66 Ok(config) => {
67 let mut cache_guard = self.cache.write().await;
69 cache_guard.insert(
70 cache_key.clone(),
71 CachedConfig {
72 config: config.clone(),
73 fetched_at: Instant::now(),
74 },
75 );
76 info!("Cached config for {}/{}", repo_owner, repo_name);
77 config
78 }
79 Err(e) => {
80 warn!(
81 "Failed to fetch config for {}/{}: {}. Using defaults.",
82 repo_owner, repo_name, e
83 );
84 self.default_config.clone()
85 }
86 }
87 }
88
89 async fn fetch_config_from_github(&self, repo_owner: &str, repo_name: &str) -> ApiResult<RepoConfig> {
91 let file_content = self.github_client
93 .get_file_content(repo_owner, repo_name, ".meritocrab.toml")
94 .await?;
95
96 let config: RepoConfig = toml::from_str(&file_content).map_err(|e| {
98 warn!(
99 "Invalid .meritocrab.toml syntax for {}/{}: {}",
100 repo_owner, repo_name, e
101 );
102 ApiError::Internal(format!("Invalid TOML syntax: {}", e))
103 })?;
104
105 info!(
106 "Successfully loaded config for {}/{}: starting_credit={}, pr_threshold={}, blacklist_threshold={}",
107 repo_owner, repo_name, config.starting_credit, config.pr_threshold, config.blacklist_threshold
108 );
109
110 Ok(config)
111 }
112
113 #[allow(dead_code)]
115 pub async fn invalidate_cache(&self, repo_owner: &str, repo_name: &str) {
116 let cache_key = format!("{}/{}", repo_owner, repo_name);
117 let mut cache_guard = self.cache.write().await;
118 cache_guard.remove(&cache_key);
119 info!("Invalidated cache for {}/{}", repo_owner, repo_name);
120 }
121
122 #[allow(dead_code)]
124 pub async fn clear_cache(&self) {
125 let mut cache_guard = self.cache.write().await;
126 cache_guard.clear();
127 info!("Cleared all config cache");
128 }
129
130 #[allow(dead_code)]
132 pub async fn cache_size(&self) -> usize {
133 let cache_guard = self.cache.read().await;
134 cache_guard.len()
135 }
136}
137
138#[cfg(test)]
139mod tests {
140 use super::*;
141 use meritocrab_github::GithubApiClient;
142
143 #[tokio::test]
144 async fn test_loader_returns_default_on_error() {
145 let _ = rustls::crypto::aws_lc_rs::default_provider().install_default();
147
148 let github_client = Arc::new(
150 GithubApiClient::new("test-token".to_string()).expect("Failed to create client")
151 );
152
153 let loader = RepoConfigLoader::new(github_client, 300);
154 let config = loader.get_config("owner", "repo").await;
155
156 assert_eq!(config.starting_credit, 100);
158 assert_eq!(config.pr_threshold, 50);
159 assert_eq!(config.blacklist_threshold, 0);
160 }
161
162 #[tokio::test]
163 async fn test_cache_ttl() {
164 let _ = rustls::crypto::aws_lc_rs::default_provider().install_default();
166
167 let github_client = Arc::new(
168 GithubApiClient::new("test-token".to_string()).expect("Failed to create client")
169 );
170
171 let loader = RepoConfigLoader::new(github_client, 1);
173
174 let config1 = loader.get_config("owner", "repo").await;
176 assert_eq!(loader.cache_size().await, 0);
178
179 let config2 = loader.get_config("owner", "repo").await;
181 assert_eq!(config1.starting_credit, config2.starting_credit);
182
183 assert_eq!(loader.cache_size().await, 0);
185 }
186
187 #[tokio::test]
188 async fn test_invalidate_cache() {
189 let _ = rustls::crypto::aws_lc_rs::default_provider().install_default();
191
192 let github_client = Arc::new(
193 GithubApiClient::new("test-token".to_string()).expect("Failed to create client")
194 );
195
196 let loader = RepoConfigLoader::new(github_client, 300);
197
198 let _config = loader.get_config("owner", "repo").await;
200 assert_eq!(loader.cache_size().await, 0);
201
202 loader.invalidate_cache("owner", "repo").await;
204 assert_eq!(loader.cache_size().await, 0);
205 }
206
207 #[tokio::test]
208 async fn test_clear_cache() {
209 let _ = rustls::crypto::aws_lc_rs::default_provider().install_default();
211
212 let github_client = Arc::new(
213 GithubApiClient::new("test-token".to_string()).expect("Failed to create client")
214 );
215
216 let loader = RepoConfigLoader::new(github_client, 300);
217
218 let _config1 = loader.get_config("owner1", "repo1").await;
220 let _config2 = loader.get_config("owner2", "repo2").await;
221 assert_eq!(loader.cache_size().await, 0);
222
223 loader.clear_cache().await;
225 assert_eq!(loader.cache_size().await, 0);
226 }
227}