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);
111 self.indices.remove(name)
112 }
113
114 pub async fn clear(&mut self) {
118 self.indices.clear();
119 self.content_cache.write().await.clear();
120 }
121
122 pub async fn load_content(&self, name: &str) -> crate::Result<String> {
126 {
127 let cache = self.content_cache.read().await;
128 if let Some(content) = cache.get(name) {
129 return Ok(content.clone());
130 }
131 }
132
133 let index = self
134 .indices
135 .get(name)
136 .ok_or_else(|| crate::Error::Config(format!("Index entry '{}' not found", name)))?;
137
138 let content = index.load_content().await?;
139
140 {
141 let mut cache = self.content_cache.write().await;
142 cache.insert(name.to_string(), content.clone());
143 }
144
145 Ok(content)
146 }
147
148 pub async fn invalidate_cache(&self, name: &str) {
150 let mut cache = self.content_cache.write().await;
151 cache.remove(name);
152 }
153
154 pub async fn clear_cache(&self) {
156 let mut cache = self.content_cache.write().await;
157 cache.clear();
158 }
159
160 pub fn build_summary(&self) -> String {
164 let mut lines: Vec<_> = self
165 .indices
166 .values()
167 .map(|idx| idx.to_summary_line())
168 .collect();
169 lines.sort();
170 lines.join("\n")
171 }
172
173 pub fn build_priority_summary(&self) -> String {
178 self.sorted_by_priority()
179 .iter()
180 .map(|idx| idx.to_summary_line())
181 .collect::<Vec<_>>()
182 .join("\n")
183 }
184
185 pub fn build_summary_with<F>(&self, formatter: F) -> String
187 where
188 F: Fn(&I) -> String,
189 {
190 let mut lines: Vec<_> = self.indices.values().map(formatter).collect();
191 lines.sort();
192 lines.join("\n")
193 }
194
195 pub fn sorted_by_priority(&self) -> Vec<&I> {
197 let mut items: Vec<_> = self.indices.values().collect();
198 items.sort_by_key(|i| std::cmp::Reverse(i.priority()));
199 items
200 }
201
202 pub fn filter<F>(&self, predicate: F) -> Vec<&I>
204 where
205 F: Fn(&I) -> bool,
206 {
207 self.indices.values().filter(|i| predicate(i)).collect()
208 }
209}
210
211#[derive(Clone, Debug)]
217pub struct LoadedEntry<I: Index> {
218 pub index: I,
220 pub content: String,
222}
223
224impl<I: Index + PathMatched> IndexRegistry<I> {
225 pub fn find_matching(&self, path: &Path) -> Vec<&I> {
238 let mut matches: Vec<_> = self
239 .indices
240 .values()
241 .filter(|i| i.matches_path(path))
242 .collect();
243 matches.sort_by_key(|i| std::cmp::Reverse(i.priority()));
245 matches
246 }
247
248 pub async fn load_matching(&self, path: &Path) -> Vec<LoadedEntry<I>> {
253 let matching = self.find_matching(path);
254 let mut results = Vec::with_capacity(matching.len());
255
256 for index in matching {
257 let name = index.name();
258 match self.load_content(name).await {
259 Ok(content) => {
260 results.push(LoadedEntry {
261 index: index.clone(),
262 content,
263 });
264 }
265 Err(e) => {
266 tracing::warn!("Failed to load content for '{}': {}", name, e);
267 }
268 }
269 }
270
271 results
272 }
273
274 pub fn has_matching(&self, path: &Path) -> bool {
276 self.indices.values().any(|i| i.matches_path(path))
277 }
278
279 pub fn build_matching_summary(&self, path: &Path) -> String {
281 let matching = self.find_matching(path);
282 if matching.is_empty() {
283 return String::new();
284 }
285
286 matching
287 .into_iter()
288 .map(|i| i.to_summary_line())
289 .collect::<Vec<_>>()
290 .join("\n")
291 }
292}
293
294impl<I: Index> Default for IndexRegistry<I> {
295 fn default() -> Self {
296 Self::new()
297 }
298}
299
300impl<I: Index> Clone for IndexRegistry<I> {
301 fn clone(&self) -> Self {
302 Self {
303 indices: self.indices.clone(),
304 content_cache: Arc::new(RwLock::new(HashMap::new())),
305 }
306 }
307}
308
309impl<I: Index> FromIterator<I> for IndexRegistry<I> {
310 fn from_iter<T: IntoIterator<Item = I>>(iter: T) -> Self {
311 let mut registry = Self::new();
312 registry.register_all(iter);
313 registry
314 }
315}
316
317#[cfg(test)]
318mod tests {
319 use async_trait::async_trait;
320
321 use super::*;
322 use crate::common::{ContentSource, Named, SourceType};
323
324 #[derive(Clone, Debug)]
325 struct TestIndex {
326 name: String,
327 desc: String,
328 source: ContentSource,
329 source_type: SourceType,
330 }
331
332 impl TestIndex {
333 fn new(name: &str, desc: &str, source_type: SourceType) -> Self {
334 Self {
335 name: name.into(),
336 desc: desc.into(),
337 source: ContentSource::in_memory(format!("Content for {}", name)),
338 source_type,
339 }
340 }
341 }
342
343 impl Named for TestIndex {
344 fn name(&self) -> &str {
345 &self.name
346 }
347 }
348
349 #[async_trait]
350 impl Index for TestIndex {
351 fn source(&self) -> &ContentSource {
352 &self.source
353 }
354
355 fn source_type(&self) -> SourceType {
356 self.source_type
357 }
358
359 fn to_summary_line(&self) -> String {
360 format!("- {}: {}", self.name, self.desc)
361 }
362 }
363
364 #[test]
365 fn test_basic_operations() {
366 let mut registry = IndexRegistry::new();
367
368 registry.register(TestIndex::new("a", "Desc A", SourceType::User));
369 registry.register(TestIndex::new("b", "Desc B", SourceType::User));
370
371 assert_eq!(registry.len(), 2);
372 assert!(registry.contains("a"));
373 assert!(registry.contains("b"));
374 assert!(!registry.contains("c"));
375 }
376
377 #[test]
378 fn test_priority_override() {
379 let mut registry = IndexRegistry::new();
380
381 registry.register(TestIndex::new("test", "Builtin", SourceType::Builtin));
383 assert_eq!(registry.get("test").unwrap().desc, "Builtin");
384
385 registry.register(TestIndex::new("test", "User", SourceType::User));
387 assert_eq!(registry.get("test").unwrap().desc, "User");
388
389 registry.register(TestIndex::new("test", "Project", SourceType::Project));
391 assert_eq!(registry.get("test").unwrap().desc, "Project");
392
393 registry.register(TestIndex::new("test", "Builtin2", SourceType::Builtin));
395 assert_eq!(registry.get("test").unwrap().desc, "Project");
396 }
397
398 #[tokio::test]
399 async fn test_content_loading() {
400 let mut registry = IndexRegistry::new();
401 registry.register(TestIndex::new("test", "Desc", SourceType::User));
402
403 let content = registry.load_content("test").await.unwrap();
404 assert_eq!(content, "Content for test");
405 }
406
407 #[tokio::test]
408 async fn test_content_caching() {
409 let mut registry = IndexRegistry::new();
410 registry.register(TestIndex::new("test", "Desc", SourceType::User));
411
412 let content1 = registry.load_content("test").await.unwrap();
414
415 let content2 = registry.load_content("test").await.unwrap();
417
418 assert_eq!(content1, content2);
419 }
420
421 #[test]
422 fn test_build_summary() {
423 let mut registry = IndexRegistry::new();
424 registry.register(TestIndex::new("commit", "Create commits", SourceType::User));
425 registry.register(TestIndex::new("review", "Review code", SourceType::User));
426
427 let summary = registry.build_summary();
428 assert!(summary.contains("- commit: Create commits"));
429 assert!(summary.contains("- review: Review code"));
430 }
431
432 #[test]
433 fn test_from_iterator() {
434 let indices = vec![
435 TestIndex::new("a", "A", SourceType::User),
436 TestIndex::new("b", "B", SourceType::User),
437 ];
438
439 let registry: IndexRegistry<TestIndex> = indices.into_iter().collect();
440 assert_eq!(registry.len(), 2);
441 }
442
443 #[test]
444 fn test_filter() {
445 let mut registry = IndexRegistry::new();
446 registry.register(TestIndex::new("builtin1", "B1", SourceType::Builtin));
447 registry.register(TestIndex::new("user1", "U1", SourceType::User));
448 registry.register(TestIndex::new("project1", "P1", SourceType::Project));
449
450 let users = registry.filter(|i| i.source_type() == SourceType::User);
451 assert_eq!(users.len(), 1);
452 assert_eq!(users[0].name(), "user1");
453 }
454
455 #[test]
456 fn test_sorted_by_priority() {
457 let mut registry = IndexRegistry::new();
458 registry.register(TestIndex::new("builtin", "B", SourceType::Builtin));
459 registry.register(TestIndex::new("user", "U", SourceType::User));
460 registry.register(TestIndex::new("project", "P", SourceType::Project));
461
462 let sorted = registry.sorted_by_priority();
463 assert_eq!(sorted[0].name(), "project");
464 assert_eq!(sorted[1].name(), "user");
465 assert_eq!(sorted[2].name(), "builtin");
466 }
467
468 #[derive(Clone, Debug)]
473 struct PathMatchedIndex {
474 name: String,
475 desc: String,
476 patterns: Option<Vec<String>>,
477 source: ContentSource,
478 source_type: SourceType,
479 }
480
481 impl PathMatchedIndex {
482 fn new(name: &str, patterns: Option<Vec<&str>>, source_type: SourceType) -> Self {
483 Self {
484 name: name.into(),
485 desc: format!("Desc for {}", name),
486 patterns: patterns.map(|p| p.into_iter().map(String::from).collect()),
487 source: ContentSource::in_memory(format!("Content for {}", name)),
488 source_type,
489 }
490 }
491 }
492
493 impl Named for PathMatchedIndex {
494 fn name(&self) -> &str {
495 &self.name
496 }
497 }
498
499 #[async_trait]
500 impl Index for PathMatchedIndex {
501 fn source(&self) -> &ContentSource {
502 &self.source
503 }
504
505 fn source_type(&self) -> SourceType {
506 self.source_type
507 }
508
509 fn to_summary_line(&self) -> String {
510 format!("- {}: {}", self.name, self.desc)
511 }
512 }
513
514 impl PathMatched for PathMatchedIndex {
515 fn path_patterns(&self) -> Option<&[String]> {
516 self.patterns.as_deref()
517 }
518
519 fn matches_path(&self, path: &Path) -> bool {
520 match &self.patterns {
521 None => true, Some(patterns) if patterns.is_empty() => false,
523 Some(patterns) => {
524 let path_str = path.to_string_lossy();
525 patterns.iter().any(|p| {
526 glob::Pattern::new(p)
527 .map(|pat| pat.matches(&path_str))
528 .unwrap_or(false)
529 })
530 }
531 }
532 }
533 }
534
535 #[test]
536 fn test_find_matching_global() {
537 let mut registry = IndexRegistry::new();
538 registry.register(PathMatchedIndex::new("global", None, SourceType::User));
539
540 let matches = registry.find_matching(Path::new("any/file.rs"));
541 assert_eq!(matches.len(), 1);
542 assert_eq!(matches[0].name(), "global");
543 }
544
545 #[test]
546 fn test_find_matching_with_patterns() {
547 let mut registry = IndexRegistry::new();
548 registry.register(PathMatchedIndex::new(
549 "rust",
550 Some(vec!["**/*.rs"]),
551 SourceType::User,
552 ));
553 registry.register(PathMatchedIndex::new(
554 "typescript",
555 Some(vec!["**/*.ts", "**/*.tsx"]),
556 SourceType::User,
557 ));
558
559 let rust_matches = registry.find_matching(Path::new("src/lib.rs"));
560 assert_eq!(rust_matches.len(), 1);
561 assert_eq!(rust_matches[0].name(), "rust");
562
563 let ts_matches = registry.find_matching(Path::new("src/app.tsx"));
564 assert_eq!(ts_matches.len(), 1);
565 assert_eq!(ts_matches[0].name(), "typescript");
566 }
567
568 #[test]
569 fn test_find_matching_sorted_by_priority() {
570 let mut registry = IndexRegistry::new();
571 registry.register(PathMatchedIndex::new("builtin", None, SourceType::Builtin));
572 registry.register(PathMatchedIndex::new("user", None, SourceType::User));
573 registry.register(PathMatchedIndex::new("project", None, SourceType::Project));
574
575 let matches = registry.find_matching(Path::new("any/file.rs"));
576 assert_eq!(matches.len(), 3);
577 assert_eq!(matches[0].name(), "project");
579 assert_eq!(matches[1].name(), "user");
580 assert_eq!(matches[2].name(), "builtin");
581 }
582
583 #[test]
584 fn test_has_matching() {
585 let mut registry = IndexRegistry::new();
586 registry.register(PathMatchedIndex::new(
587 "rust",
588 Some(vec!["**/*.rs"]),
589 SourceType::User,
590 ));
591
592 assert!(registry.has_matching(Path::new("src/lib.rs")));
593 assert!(!registry.has_matching(Path::new("src/lib.ts")));
594 }
595
596 #[tokio::test]
597 async fn test_load_matching() {
598 let mut registry = IndexRegistry::new();
599 registry.register(PathMatchedIndex::new(
600 "rust",
601 Some(vec!["**/*.rs"]),
602 SourceType::User,
603 ));
604 registry.register(PathMatchedIndex::new("global", None, SourceType::User));
605
606 let loaded = registry.load_matching(Path::new("src/lib.rs")).await;
607 assert_eq!(loaded.len(), 2);
608
609 for entry in &loaded {
611 assert!(entry.content.starts_with("Content for"));
612 }
613 }
614}