1use std::collections::HashMap;
10use std::path::Path;
11use std::sync::Arc;
12
13use tokio::sync::RwLock;
14
15use super::{Index, PathMatched};
16
17pub struct IndexRegistry<I: Index> {
40 indices: HashMap<String, I>,
41 content_cache: Arc<RwLock<HashMap<String, String>>>,
42}
43
44impl<I: Index> IndexRegistry<I> {
45 pub fn new() -> Self {
47 Self {
48 indices: HashMap::new(),
49 content_cache: Arc::new(RwLock::new(HashMap::new())),
50 }
51 }
52
53 pub fn register(&mut self, index: I) {
58 let name = index.name().to_string();
59
60 if let Some(existing) = self.indices.get(&name) {
61 if index.priority() >= existing.priority() {
62 self.indices.insert(name, index);
63 }
64 } else {
65 self.indices.insert(name, index);
66 }
67 }
68
69 pub fn register_all(&mut self, indices: impl IntoIterator<Item = I>) {
71 for index in indices {
72 self.register(index);
73 }
74 }
75
76 pub fn get(&self, name: &str) -> Option<&I> {
78 self.indices.get(name)
79 }
80
81 pub fn list(&self) -> Vec<&str> {
83 self.indices.keys().map(String::as_str).collect()
84 }
85
86 pub fn iter(&self) -> impl Iterator<Item = &I> {
88 self.indices.values()
89 }
90
91 pub fn len(&self) -> usize {
93 self.indices.len()
94 }
95
96 pub fn is_empty(&self) -> bool {
98 self.indices.is_empty()
99 }
100
101 pub fn contains(&self, name: &str) -> bool {
103 self.indices.contains_key(name)
104 }
105
106 pub async fn remove(&mut self, name: &str) -> Option<I> {
110 self.content_cache.write().await.remove(name);
112 self.indices.remove(name)
113 }
114
115 pub async fn clear(&mut self) {
119 self.indices.clear();
120 self.content_cache.write().await.clear();
121 }
122
123 pub async fn load_content(&self, name: &str) -> crate::Result<String> {
127 {
129 let cache = self.content_cache.read().await;
130 if let Some(content) = cache.get(name) {
131 return Ok(content.clone());
132 }
133 }
134
135 let index = self
137 .indices
138 .get(name)
139 .ok_or_else(|| crate::Error::Config(format!("Index entry '{}' not found", name)))?;
140
141 let content = index.load_content().await?;
142
143 {
145 let mut cache = self.content_cache.write().await;
146 cache.insert(name.to_string(), content.clone());
147 }
148
149 Ok(content)
150 }
151
152 pub async fn invalidate_cache(&self, name: &str) {
154 let mut cache = self.content_cache.write().await;
155 cache.remove(name);
156 }
157
158 pub async fn clear_cache(&self) {
160 let mut cache = self.content_cache.write().await;
161 cache.clear();
162 }
163
164 pub fn build_summary(&self) -> String {
168 let mut lines: Vec<_> = self
169 .indices
170 .values()
171 .map(|idx| idx.to_summary_line())
172 .collect();
173 lines.sort();
174 lines.join("\n")
175 }
176
177 pub fn build_summary_with<F>(&self, formatter: F) -> String
179 where
180 F: Fn(&I) -> String,
181 {
182 let mut lines: Vec<_> = self.indices.values().map(formatter).collect();
183 lines.sort();
184 lines.join("\n")
185 }
186
187 pub fn sorted_by_priority(&self) -> Vec<&I> {
189 let mut items: Vec<_> = self.indices.values().collect();
190 items.sort_by_key(|i| std::cmp::Reverse(i.priority()));
191 items
192 }
193
194 pub fn filter<F>(&self, predicate: F) -> Vec<&I>
196 where
197 F: Fn(&I) -> bool,
198 {
199 self.indices.values().filter(|i| predicate(i)).collect()
200 }
201}
202
203#[derive(Clone, Debug)]
209pub struct LoadedEntry<I: Index> {
210 pub index: I,
212 pub content: String,
214}
215
216impl<I: Index + PathMatched> IndexRegistry<I> {
217 pub fn find_matching(&self, path: &Path) -> Vec<&I> {
230 let mut matches: Vec<_> = self
231 .indices
232 .values()
233 .filter(|i| i.matches_path(path))
234 .collect();
235 matches.sort_by_key(|i| std::cmp::Reverse(i.priority()));
237 matches
238 }
239
240 pub async fn load_matching(&self, path: &Path) -> Vec<LoadedEntry<I>> {
245 let matching = self.find_matching(path);
246 let mut results = Vec::with_capacity(matching.len());
247
248 for index in matching {
249 let name = index.name();
250 match self.load_content(name).await {
251 Ok(content) => {
252 results.push(LoadedEntry {
253 index: index.clone(),
254 content,
255 });
256 }
257 Err(e) => {
258 tracing::warn!("Failed to load content for '{}': {}", name, e);
259 }
260 }
261 }
262
263 results
264 }
265
266 pub fn has_matching(&self, path: &Path) -> bool {
268 self.indices.values().any(|i| i.matches_path(path))
269 }
270
271 pub fn build_matching_summary(&self, path: &Path) -> String {
273 let matching = self.find_matching(path);
274 if matching.is_empty() {
275 return String::new();
276 }
277
278 matching
279 .into_iter()
280 .map(|i| i.to_summary_line())
281 .collect::<Vec<_>>()
282 .join("\n")
283 }
284}
285
286impl<I: Index> Default for IndexRegistry<I> {
287 fn default() -> Self {
288 Self::new()
289 }
290}
291
292impl<I: Index> Clone for IndexRegistry<I> {
293 fn clone(&self) -> Self {
294 Self {
295 indices: self.indices.clone(),
296 content_cache: Arc::new(RwLock::new(HashMap::new())),
297 }
298 }
299}
300
301impl<I: Index> FromIterator<I> for IndexRegistry<I> {
302 fn from_iter<T: IntoIterator<Item = I>>(iter: T) -> Self {
303 let mut registry = Self::new();
304 registry.register_all(iter);
305 registry
306 }
307}
308
309#[cfg(test)]
310mod tests {
311 use async_trait::async_trait;
312
313 use super::*;
314 use crate::common::{ContentSource, Named, SourceType};
315
316 #[derive(Clone, Debug)]
317 struct TestIndex {
318 name: String,
319 desc: String,
320 source: ContentSource,
321 source_type: SourceType,
322 }
323
324 impl TestIndex {
325 fn new(name: &str, desc: &str, source_type: SourceType) -> Self {
326 Self {
327 name: name.into(),
328 desc: desc.into(),
329 source: ContentSource::in_memory(format!("Content for {}", name)),
330 source_type,
331 }
332 }
333 }
334
335 impl Named for TestIndex {
336 fn name(&self) -> &str {
337 &self.name
338 }
339 }
340
341 #[async_trait]
342 impl Index for TestIndex {
343 fn source(&self) -> &ContentSource {
344 &self.source
345 }
346
347 fn source_type(&self) -> SourceType {
348 self.source_type
349 }
350
351 fn to_summary_line(&self) -> String {
352 format!("- {}: {}", self.name, self.desc)
353 }
354 }
355
356 #[test]
357 fn test_basic_operations() {
358 let mut registry = IndexRegistry::new();
359
360 registry.register(TestIndex::new("a", "Desc A", SourceType::User));
361 registry.register(TestIndex::new("b", "Desc B", SourceType::User));
362
363 assert_eq!(registry.len(), 2);
364 assert!(registry.contains("a"));
365 assert!(registry.contains("b"));
366 assert!(!registry.contains("c"));
367 }
368
369 #[test]
370 fn test_priority_override() {
371 let mut registry = IndexRegistry::new();
372
373 registry.register(TestIndex::new("test", "Builtin", SourceType::Builtin));
375 assert_eq!(registry.get("test").unwrap().desc, "Builtin");
376
377 registry.register(TestIndex::new("test", "User", SourceType::User));
379 assert_eq!(registry.get("test").unwrap().desc, "User");
380
381 registry.register(TestIndex::new("test", "Project", SourceType::Project));
383 assert_eq!(registry.get("test").unwrap().desc, "Project");
384
385 registry.register(TestIndex::new("test", "Builtin2", SourceType::Builtin));
387 assert_eq!(registry.get("test").unwrap().desc, "Project");
388 }
389
390 #[tokio::test]
391 async fn test_content_loading() {
392 let mut registry = IndexRegistry::new();
393 registry.register(TestIndex::new("test", "Desc", SourceType::User));
394
395 let content = registry.load_content("test").await.unwrap();
396 assert_eq!(content, "Content for test");
397 }
398
399 #[tokio::test]
400 async fn test_content_caching() {
401 let mut registry = IndexRegistry::new();
402 registry.register(TestIndex::new("test", "Desc", SourceType::User));
403
404 let content1 = registry.load_content("test").await.unwrap();
406
407 let content2 = registry.load_content("test").await.unwrap();
409
410 assert_eq!(content1, content2);
411 }
412
413 #[test]
414 fn test_build_summary() {
415 let mut registry = IndexRegistry::new();
416 registry.register(TestIndex::new("commit", "Create commits", SourceType::User));
417 registry.register(TestIndex::new("review", "Review code", SourceType::User));
418
419 let summary = registry.build_summary();
420 assert!(summary.contains("- commit: Create commits"));
421 assert!(summary.contains("- review: Review code"));
422 }
423
424 #[test]
425 fn test_from_iterator() {
426 let indices = vec![
427 TestIndex::new("a", "A", SourceType::User),
428 TestIndex::new("b", "B", SourceType::User),
429 ];
430
431 let registry: IndexRegistry<TestIndex> = indices.into_iter().collect();
432 assert_eq!(registry.len(), 2);
433 }
434
435 #[test]
436 fn test_filter() {
437 let mut registry = IndexRegistry::new();
438 registry.register(TestIndex::new("builtin1", "B1", SourceType::Builtin));
439 registry.register(TestIndex::new("user1", "U1", SourceType::User));
440 registry.register(TestIndex::new("project1", "P1", SourceType::Project));
441
442 let users = registry.filter(|i| i.source_type() == SourceType::User);
443 assert_eq!(users.len(), 1);
444 assert_eq!(users[0].name(), "user1");
445 }
446
447 #[test]
448 fn test_sorted_by_priority() {
449 let mut registry = IndexRegistry::new();
450 registry.register(TestIndex::new("builtin", "B", SourceType::Builtin));
451 registry.register(TestIndex::new("user", "U", SourceType::User));
452 registry.register(TestIndex::new("project", "P", SourceType::Project));
453
454 let sorted = registry.sorted_by_priority();
455 assert_eq!(sorted[0].name(), "project");
456 assert_eq!(sorted[1].name(), "user");
457 assert_eq!(sorted[2].name(), "builtin");
458 }
459
460 #[derive(Clone, Debug)]
465 struct PathMatchedIndex {
466 name: String,
467 desc: String,
468 patterns: Option<Vec<String>>,
469 source: ContentSource,
470 source_type: SourceType,
471 }
472
473 impl PathMatchedIndex {
474 fn new(name: &str, patterns: Option<Vec<&str>>, source_type: SourceType) -> Self {
475 Self {
476 name: name.into(),
477 desc: format!("Desc for {}", name),
478 patterns: patterns.map(|p| p.into_iter().map(String::from).collect()),
479 source: ContentSource::in_memory(format!("Content for {}", name)),
480 source_type,
481 }
482 }
483 }
484
485 impl Named for PathMatchedIndex {
486 fn name(&self) -> &str {
487 &self.name
488 }
489 }
490
491 #[async_trait]
492 impl Index for PathMatchedIndex {
493 fn source(&self) -> &ContentSource {
494 &self.source
495 }
496
497 fn source_type(&self) -> SourceType {
498 self.source_type
499 }
500
501 fn to_summary_line(&self) -> String {
502 format!("- {}: {}", self.name, self.desc)
503 }
504 }
505
506 impl PathMatched for PathMatchedIndex {
507 fn path_patterns(&self) -> Option<&[String]> {
508 self.patterns.as_deref()
509 }
510
511 fn matches_path(&self, path: &Path) -> bool {
512 match &self.patterns {
513 None => true, Some(patterns) if patterns.is_empty() => false,
515 Some(patterns) => {
516 let path_str = path.to_string_lossy();
517 patterns.iter().any(|p| {
518 glob::Pattern::new(p)
519 .map(|pat| pat.matches(&path_str))
520 .unwrap_or(false)
521 })
522 }
523 }
524 }
525 }
526
527 #[test]
528 fn test_find_matching_global() {
529 let mut registry = IndexRegistry::new();
530 registry.register(PathMatchedIndex::new("global", None, SourceType::User));
531
532 let matches = registry.find_matching(Path::new("any/file.rs"));
533 assert_eq!(matches.len(), 1);
534 assert_eq!(matches[0].name(), "global");
535 }
536
537 #[test]
538 fn test_find_matching_with_patterns() {
539 let mut registry = IndexRegistry::new();
540 registry.register(PathMatchedIndex::new(
541 "rust",
542 Some(vec!["**/*.rs"]),
543 SourceType::User,
544 ));
545 registry.register(PathMatchedIndex::new(
546 "typescript",
547 Some(vec!["**/*.ts", "**/*.tsx"]),
548 SourceType::User,
549 ));
550
551 let rust_matches = registry.find_matching(Path::new("src/lib.rs"));
552 assert_eq!(rust_matches.len(), 1);
553 assert_eq!(rust_matches[0].name(), "rust");
554
555 let ts_matches = registry.find_matching(Path::new("src/app.tsx"));
556 assert_eq!(ts_matches.len(), 1);
557 assert_eq!(ts_matches[0].name(), "typescript");
558 }
559
560 #[test]
561 fn test_find_matching_sorted_by_priority() {
562 let mut registry = IndexRegistry::new();
563 registry.register(PathMatchedIndex::new("builtin", None, SourceType::Builtin));
564 registry.register(PathMatchedIndex::new("user", None, SourceType::User));
565 registry.register(PathMatchedIndex::new("project", None, SourceType::Project));
566
567 let matches = registry.find_matching(Path::new("any/file.rs"));
568 assert_eq!(matches.len(), 3);
569 assert_eq!(matches[0].name(), "project");
571 assert_eq!(matches[1].name(), "user");
572 assert_eq!(matches[2].name(), "builtin");
573 }
574
575 #[test]
576 fn test_has_matching() {
577 let mut registry = IndexRegistry::new();
578 registry.register(PathMatchedIndex::new(
579 "rust",
580 Some(vec!["**/*.rs"]),
581 SourceType::User,
582 ));
583
584 assert!(registry.has_matching(Path::new("src/lib.rs")));
585 assert!(!registry.has_matching(Path::new("src/lib.ts")));
586 }
587
588 #[tokio::test]
589 async fn test_load_matching() {
590 let mut registry = IndexRegistry::new();
591 registry.register(PathMatchedIndex::new(
592 "rust",
593 Some(vec!["**/*.rs"]),
594 SourceType::User,
595 ));
596 registry.register(PathMatchedIndex::new("global", None, SourceType::User));
597
598 let loaded = registry.load_matching(Path::new("src/lib.rs")).await;
599 assert_eq!(loaded.len(), 2);
600
601 for entry in &loaded {
603 assert!(entry.content.starts_with("Content for"));
604 }
605 }
606}