use std::any::TypeId;
use std::future::Future;
use std::path::{Path, PathBuf};
use std::pin::Pin;
use std::sync::Arc;
use crate::kit::{AsyncAutoBuilder, AsyncKit, ModuleMeta};
use super::capability::Indexer;
use super::error::IndexError;
use super::pipeline::{IndexFacade, IndexResult};
use crate::storage::StorageError;
#[derive(Debug, Clone)]
pub struct IndexConfig {
pub db_path: PathBuf,
}
impl IndexConfig {
#[must_use]
pub fn in_memory() -> Self {
Self {
db_path: PathBuf::from(":memory:"),
}
}
}
pub struct IndexerModule;
impl ModuleMeta for IndexerModule {
const NAME: &'static str = "indexer";
fn dependencies() -> &'static [(&'static str, TypeId)] {
&[]
}
}
impl AsyncAutoBuilder for IndexerModule {
type Capability = Arc<dyn Indexer>;
type Error = IndexError;
fn build<'a>(
kit: &'a AsyncKit,
) -> Pin<Box<dyn Future<Output = Result<Self::Capability, Self::Error>> + Send + 'a>> {
Box::pin(async move {
let config = kit
.config::<IndexConfig>()
.map_err(|e| IndexError::Storage(StorageError::InvalidData(e.to_string())))?;
Self::build_cap(&config)
})
}
}
impl IndexerModule {
pub(crate) fn build_cap(config: &IndexConfig) -> Result<Arc<dyn Indexer>, IndexError> {
let facade = IndexFacade::new(&config.db_path)?;
Ok(Arc::new(IndexerCapability { facade }))
}
}
struct IndexerCapability {
facade: IndexFacade,
}
impl Indexer for IndexerCapability {
fn index(
&self,
path: &Path,
project_name: &str,
force: bool,
) -> Result<IndexResult, IndexError> {
self.facade.index(path, project_name, force)
}
fn index_incremental(
&self,
path: &Path,
project_name: &str,
force: bool,
) -> Result<IndexResult, IndexError> {
self.facade.index_incremental(path, project_name, force)
}
fn index_ram_first(
&self,
path: &Path,
project_name: &str,
force: bool,
) -> Result<IndexResult, IndexError> {
self.facade.index_ram_first(path, project_name, force)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::kit::{AsyncKit, IndexerModule};
use std::fs;
use tempfile::TempDir;
fn write_file(dir: &Path, rel: &str, content: &str) {
let path = dir.join(rel);
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).unwrap();
}
fs::write(path, content).unwrap();
}
#[test]
fn build_returns_send_sync_capability() {
let cap = IndexerModule::build_cap(&IndexConfig::in_memory()).expect("build_cap");
fn _assert_send_sync<T: Send + Sync>(_: &T) {}
_assert_send_sync(&cap);
}
#[test]
fn capability_index_empty_dir_returns_zero_files() {
let cap = IndexerModule::build_cap(&IndexConfig::in_memory()).expect("build_cap");
let tmp = TempDir::new().unwrap();
let result = cap.index(tmp.path(), "empty", false).expect("index");
assert_eq!(result.files_indexed, 0, "empty dir → 0 files indexed");
assert_eq!(result.files_skipped, 0);
assert!(result.duration_ms < u64::MAX, "duration should be recorded");
}
#[cfg(feature = "lang-rust")]
#[test]
fn capability_index_real_files_creates_nodes() {
let cap = IndexerModule::build_cap(&IndexConfig::in_memory()).expect("build_cap");
let tmp = TempDir::new().unwrap();
write_file(
tmp.path(),
"main.rs",
"fn main() { helper(); }\nfn helper() {}\n",
);
let result = cap.index(tmp.path(), "demo", false).expect("index");
assert!(result.files_indexed > 0, "should index files: {result:?}");
assert!(result.nodes_created > 0, "should create nodes: {result:?}");
assert!(!result.project_id.is_empty(), "project_id should be set");
}
#[test]
fn capability_path_not_found_returns_error() {
let cap = IndexerModule::build_cap(&IndexConfig::in_memory()).expect("build_cap");
let result = cap.index(Path::new("/nonexistent/path/xyz"), "demo", false);
assert!(result.is_err(), "path not found should error");
let err = result.unwrap_err();
assert!(
matches!(err, IndexError::PathNotFound(_)),
"expected PathNotFound, got {err:?}"
);
assert_eq!(err.exit_code(), 1, "PRD §4.1.6: path not found → exit 1");
}
#[tokio::test]
async fn kit_registration_flow() {
let mut kit = AsyncKit::new();
kit.set_config(IndexConfig::in_memory());
kit.register::<IndexerModule>()
.expect("register::<IndexerModule>");
let kit = kit.build().await.expect("build");
assert!(kit.contains::<IndexerModule>());
let _required = kit
.require::<IndexerModule>()
.expect("require::<IndexerModule>");
}
#[test]
fn capability_index_incremental_empty_dir_returns_zero_files() {
let cap = IndexerModule::build_cap(&IndexConfig::in_memory()).expect("build_cap");
let tmp = TempDir::new().unwrap();
let result = cap
.index_incremental(tmp.path(), "empty", false)
.expect("index_incremental on empty dir");
assert_eq!(result.files_indexed, 0, "empty dir → 0 files indexed");
assert_eq!(result.files_skipped, 0);
}
}