use std::collections::HashMap;
use std::path::Path;
use std::sync::Arc;
use tokio::sync::RwLock;
use super::{Index, PathMatched};
pub struct IndexRegistry<I: Index> {
indices: HashMap<String, I>,
content_cache: Arc<RwLock<HashMap<String, String>>>,
}
impl<I: Index> IndexRegistry<I> {
pub fn new() -> Self {
Self {
indices: HashMap::new(),
content_cache: Arc::new(RwLock::new(HashMap::new())),
}
}
pub fn register(&mut self, index: I) {
let name = index.name().to_string();
if let Some(existing) = self.indices.get(&name) {
if index.priority() >= existing.priority() {
self.indices.insert(name, index);
}
} else {
self.indices.insert(name, index);
}
}
pub fn register_all(&mut self, indices: impl IntoIterator<Item = I>) {
for index in indices {
self.register(index);
}
}
pub fn get(&self, name: &str) -> Option<&I> {
self.indices.get(name)
}
pub fn list(&self) -> Vec<&str> {
self.indices.keys().map(String::as_str).collect()
}
pub fn iter(&self) -> impl Iterator<Item = &I> {
self.indices.values()
}
pub fn len(&self) -> usize {
self.indices.len()
}
pub fn is_empty(&self) -> bool {
self.indices.is_empty()
}
pub fn contains(&self, name: &str) -> bool {
self.indices.contains_key(name)
}
pub async fn remove(&mut self, name: &str) -> Option<I> {
self.content_cache.write().await.remove(name);
self.indices.remove(name)
}
pub async fn clear(&mut self) {
self.indices.clear();
self.content_cache.write().await.clear();
}
pub async fn load_content(&self, name: &str) -> crate::Result<String> {
{
let cache = self.content_cache.read().await;
if let Some(content) = cache.get(name) {
return Ok(content.clone());
}
}
let index = self
.indices
.get(name)
.ok_or_else(|| crate::Error::Config(format!("Index entry '{}' not found", name)))?;
let content = index.load_content().await?;
{
let mut cache = self.content_cache.write().await;
cache.insert(name.to_string(), content.clone());
}
Ok(content)
}
pub async fn invalidate_cache(&self, name: &str) {
let mut cache = self.content_cache.write().await;
cache.remove(name);
}
pub async fn clear_cache(&self) {
let mut cache = self.content_cache.write().await;
cache.clear();
}
pub fn build_summary(&self) -> String {
let mut lines: Vec<_> = self
.indices
.values()
.map(|idx| idx.to_summary_line())
.collect();
lines.sort();
lines.join("\n")
}
pub fn build_priority_summary(&self) -> String {
self.sorted_by_priority()
.iter()
.map(|idx| idx.to_summary_line())
.collect::<Vec<_>>()
.join("\n")
}
pub fn build_summary_with<F>(&self, formatter: F) -> String
where
F: Fn(&I) -> String,
{
let mut lines: Vec<_> = self.indices.values().map(formatter).collect();
lines.sort();
lines.join("\n")
}
pub fn sorted_by_priority(&self) -> Vec<&I> {
let mut items: Vec<_> = self.indices.values().collect();
items.sort_by_key(|i| std::cmp::Reverse(i.priority()));
items
}
pub fn filter<F>(&self, predicate: F) -> Vec<&I>
where
F: Fn(&I) -> bool,
{
self.indices.values().filter(|i| predicate(i)).collect()
}
}
#[derive(Clone, Debug)]
pub struct LoadedEntry<I: Index> {
pub index: I,
pub content: String,
}
impl<I: Index + PathMatched> IndexRegistry<I> {
pub fn find_matching(&self, path: &Path) -> Vec<&I> {
let mut matches: Vec<_> = self
.indices
.values()
.filter(|i| i.matches_path(path))
.collect();
matches.sort_by_key(|i| std::cmp::Reverse(i.priority()));
matches
}
pub async fn load_matching(&self, path: &Path) -> Vec<LoadedEntry<I>> {
let matching = self.find_matching(path);
let mut results = Vec::with_capacity(matching.len());
for index in matching {
let name = index.name();
match self.load_content(name).await {
Ok(content) => {
results.push(LoadedEntry {
index: index.clone(),
content,
});
}
Err(e) => {
tracing::warn!("Failed to load content for '{}': {}", name, e);
}
}
}
results
}
pub fn has_matching(&self, path: &Path) -> bool {
self.indices.values().any(|i| i.matches_path(path))
}
pub fn build_matching_summary(&self, path: &Path) -> String {
let matching = self.find_matching(path);
if matching.is_empty() {
return String::new();
}
matching
.into_iter()
.map(|i| i.to_summary_line())
.collect::<Vec<_>>()
.join("\n")
}
}
impl<I: Index> Default for IndexRegistry<I> {
fn default() -> Self {
Self::new()
}
}
impl<I: Index> Clone for IndexRegistry<I> {
fn clone(&self) -> Self {
Self {
indices: self.indices.clone(),
content_cache: Arc::new(RwLock::new(HashMap::new())),
}
}
}
impl<I: Index> FromIterator<I> for IndexRegistry<I> {
fn from_iter<T: IntoIterator<Item = I>>(iter: T) -> Self {
let mut registry = Self::new();
registry.register_all(iter);
registry
}
}
#[cfg(test)]
mod tests {
use async_trait::async_trait;
use super::*;
use crate::common::{ContentSource, Named, SourceType};
#[derive(Clone, Debug)]
struct TestIndex {
name: String,
desc: String,
source: ContentSource,
source_type: SourceType,
}
impl TestIndex {
fn new(name: &str, desc: &str, source_type: SourceType) -> Self {
Self {
name: name.into(),
desc: desc.into(),
source: ContentSource::in_memory(format!("Content for {}", name)),
source_type,
}
}
}
impl Named for TestIndex {
fn name(&self) -> &str {
&self.name
}
}
#[async_trait]
impl Index for TestIndex {
fn source(&self) -> &ContentSource {
&self.source
}
fn source_type(&self) -> SourceType {
self.source_type
}
fn to_summary_line(&self) -> String {
format!("- {}: {}", self.name, self.desc)
}
}
#[test]
fn test_basic_operations() {
let mut registry = IndexRegistry::new();
registry.register(TestIndex::new("a", "Desc A", SourceType::User));
registry.register(TestIndex::new("b", "Desc B", SourceType::User));
assert_eq!(registry.len(), 2);
assert!(registry.contains("a"));
assert!(registry.contains("b"));
assert!(!registry.contains("c"));
}
#[test]
fn test_priority_override() {
let mut registry = IndexRegistry::new();
registry.register(TestIndex::new("test", "Builtin", SourceType::Builtin));
assert_eq!(registry.get("test").unwrap().desc, "Builtin");
registry.register(TestIndex::new("test", "User", SourceType::User));
assert_eq!(registry.get("test").unwrap().desc, "User");
registry.register(TestIndex::new("test", "Project", SourceType::Project));
assert_eq!(registry.get("test").unwrap().desc, "Project");
registry.register(TestIndex::new("test", "Builtin2", SourceType::Builtin));
assert_eq!(registry.get("test").unwrap().desc, "Project");
}
#[tokio::test]
async fn test_content_loading() {
let mut registry = IndexRegistry::new();
registry.register(TestIndex::new("test", "Desc", SourceType::User));
let content = registry.load_content("test").await.unwrap();
assert_eq!(content, "Content for test");
}
#[tokio::test]
async fn test_content_caching() {
let mut registry = IndexRegistry::new();
registry.register(TestIndex::new("test", "Desc", SourceType::User));
let content1 = registry.load_content("test").await.unwrap();
let content2 = registry.load_content("test").await.unwrap();
assert_eq!(content1, content2);
}
#[test]
fn test_build_summary() {
let mut registry = IndexRegistry::new();
registry.register(TestIndex::new("commit", "Create commits", SourceType::User));
registry.register(TestIndex::new("review", "Review code", SourceType::User));
let summary = registry.build_summary();
assert!(summary.contains("- commit: Create commits"));
assert!(summary.contains("- review: Review code"));
}
#[test]
fn test_from_iterator() {
let indices = vec![
TestIndex::new("a", "A", SourceType::User),
TestIndex::new("b", "B", SourceType::User),
];
let registry: IndexRegistry<TestIndex> = indices.into_iter().collect();
assert_eq!(registry.len(), 2);
}
#[test]
fn test_filter() {
let mut registry = IndexRegistry::new();
registry.register(TestIndex::new("builtin1", "B1", SourceType::Builtin));
registry.register(TestIndex::new("user1", "U1", SourceType::User));
registry.register(TestIndex::new("project1", "P1", SourceType::Project));
let users = registry.filter(|i| i.source_type() == SourceType::User);
assert_eq!(users.len(), 1);
assert_eq!(users[0].name(), "user1");
}
#[test]
fn test_sorted_by_priority() {
let mut registry = IndexRegistry::new();
registry.register(TestIndex::new("builtin", "B", SourceType::Builtin));
registry.register(TestIndex::new("user", "U", SourceType::User));
registry.register(TestIndex::new("project", "P", SourceType::Project));
let sorted = registry.sorted_by_priority();
assert_eq!(sorted[0].name(), "project");
assert_eq!(sorted[1].name(), "user");
assert_eq!(sorted[2].name(), "builtin");
}
#[derive(Clone, Debug)]
struct PathMatchedIndex {
name: String,
desc: String,
patterns: Option<Vec<String>>,
source: ContentSource,
source_type: SourceType,
}
impl PathMatchedIndex {
fn new(name: &str, patterns: Option<Vec<&str>>, source_type: SourceType) -> Self {
Self {
name: name.into(),
desc: format!("Desc for {}", name),
patterns: patterns.map(|p| p.into_iter().map(String::from).collect()),
source: ContentSource::in_memory(format!("Content for {}", name)),
source_type,
}
}
}
impl Named for PathMatchedIndex {
fn name(&self) -> &str {
&self.name
}
}
#[async_trait]
impl Index for PathMatchedIndex {
fn source(&self) -> &ContentSource {
&self.source
}
fn source_type(&self) -> SourceType {
self.source_type
}
fn to_summary_line(&self) -> String {
format!("- {}: {}", self.name, self.desc)
}
}
impl PathMatched for PathMatchedIndex {
fn path_patterns(&self) -> Option<&[String]> {
self.patterns.as_deref()
}
fn matches_path(&self, path: &Path) -> bool {
match &self.patterns {
None => true, Some(patterns) if patterns.is_empty() => false,
Some(patterns) => {
let path_str = path.to_string_lossy();
patterns.iter().any(|p| {
glob::Pattern::new(p)
.map(|pat| pat.matches(&path_str))
.unwrap_or(false)
})
}
}
}
}
#[test]
fn test_find_matching_global() {
let mut registry = IndexRegistry::new();
registry.register(PathMatchedIndex::new("global", None, SourceType::User));
let matches = registry.find_matching(Path::new("any/file.rs"));
assert_eq!(matches.len(), 1);
assert_eq!(matches[0].name(), "global");
}
#[test]
fn test_find_matching_with_patterns() {
let mut registry = IndexRegistry::new();
registry.register(PathMatchedIndex::new(
"rust",
Some(vec!["**/*.rs"]),
SourceType::User,
));
registry.register(PathMatchedIndex::new(
"typescript",
Some(vec!["**/*.ts", "**/*.tsx"]),
SourceType::User,
));
let rust_matches = registry.find_matching(Path::new("src/lib.rs"));
assert_eq!(rust_matches.len(), 1);
assert_eq!(rust_matches[0].name(), "rust");
let ts_matches = registry.find_matching(Path::new("src/app.tsx"));
assert_eq!(ts_matches.len(), 1);
assert_eq!(ts_matches[0].name(), "typescript");
}
#[test]
fn test_find_matching_sorted_by_priority() {
let mut registry = IndexRegistry::new();
registry.register(PathMatchedIndex::new("builtin", None, SourceType::Builtin));
registry.register(PathMatchedIndex::new("user", None, SourceType::User));
registry.register(PathMatchedIndex::new("project", None, SourceType::Project));
let matches = registry.find_matching(Path::new("any/file.rs"));
assert_eq!(matches.len(), 3);
assert_eq!(matches[0].name(), "project");
assert_eq!(matches[1].name(), "user");
assert_eq!(matches[2].name(), "builtin");
}
#[test]
fn test_has_matching() {
let mut registry = IndexRegistry::new();
registry.register(PathMatchedIndex::new(
"rust",
Some(vec!["**/*.rs"]),
SourceType::User,
));
assert!(registry.has_matching(Path::new("src/lib.rs")));
assert!(!registry.has_matching(Path::new("src/lib.ts")));
}
#[tokio::test]
async fn test_load_matching() {
let mut registry = IndexRegistry::new();
registry.register(PathMatchedIndex::new(
"rust",
Some(vec!["**/*.rs"]),
SourceType::User,
));
registry.register(PathMatchedIndex::new("global", None, SourceType::User));
let loaded = registry.load_matching(Path::new("src/lib.rs")).await;
assert_eq!(loaded.len(), 2);
for entry in &loaded {
assert!(entry.content.starts_with("Content for"));
}
}
}