use crate::service::error::CodeNexusError;
use crate::storage::capability::Storage;
use crate::storage::is_table_missing_error;
use crate::FALLBACK_PROJECT_NAME;
use std::path::Path;
#[must_use]
pub fn sanitize_project_name(name: &str) -> String {
let cleaned: String = name
.trim()
.chars()
.map(|c| {
if c.is_whitespace() || c == '/' || c == '\\' {
'_'
} else {
c
}
})
.filter(|c| c.is_alphanumeric() || *c == '_' || *c == '-' || *c == '.')
.collect();
if cleaned.is_empty() {
FALLBACK_PROJECT_NAME.to_string()
} else {
cleaned
}
}
#[must_use]
pub fn derive_project_name(path: &Path) -> String {
let base = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
sanitize_project_name(base)
}
pub fn resolve_project_id(storage: &dyn Storage, project: &str) -> Result<String, CodeNexusError> {
let projects = match storage.list_projects() {
Ok(projects) => projects,
Err(err) if is_table_missing_error(&err) => {
return Err(CodeNexusError::ProjectNotFound(project.to_string()));
}
Err(err) => return Err(err.into()),
};
let project_id = projects
.iter()
.find(|p| p.name == project)
.map(|p| p.id.clone())
.or_else(|| {
if projects.iter().any(|p| p.id == project) {
Some(project.to_string())
} else {
None
}
});
project_id.ok_or_else(|| CodeNexusError::ProjectNotFound(project.to_string()))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::kit::{build_kit, KitBootstrapConfig, StorageModule};
use tempfile::TempDir;
fn fresh_db_path() -> (TempDir, std::path::PathBuf) {
let dir = TempDir::new().unwrap();
let path = dir.path().join("svc_project_testdb");
(dir, path)
}
fn build_kit_for_db(db: &std::path::Path) -> crate::kit::AsyncKit<crate::kit::AsyncReady> {
let config = KitBootstrapConfig::new(db.to_path_buf());
tokio::runtime::Runtime::new()
.unwrap()
.block_on(build_kit(&config))
.expect("build_kit")
}
fn seed_project(storage: &dyn Storage, id: &str, name: &str) {
storage
.execute(&format!(
"CREATE (:Project {{id: '{id}', name: '{name}', rootPath: '/demo', language: 'rust', fileCount: 1, indexedAt: 1000, lastCommit: 'abc'}});"
))
.expect("create project");
}
#[test]
fn resolve_by_name_returns_id() {
let (_dir, db) = fresh_db_path();
let kit = build_kit_for_db(&db);
let storage = kit.require::<StorageModule>().expect("storage");
seed_project(&*storage, "proj_abc", "my_project");
let id = resolve_project_id(&*storage, "my_project").expect("should resolve");
assert_eq!(id, "proj_abc");
}
#[test]
fn resolve_by_id_returns_id() {
let (_dir, db) = fresh_db_path();
let kit = build_kit_for_db(&db);
let storage = kit.require::<StorageModule>().expect("storage");
seed_project(&*storage, "proj_abc", "my_project");
let id = resolve_project_id(&*storage, "proj_abc").expect("should resolve");
assert_eq!(id, "proj_abc");
}
#[test]
fn resolve_unknown_returns_project_not_found() {
let (_dir, db) = fresh_db_path();
let kit = build_kit_for_db(&db);
let storage = kit.require::<StorageModule>().expect("storage");
seed_project(&*storage, "proj_abc", "my_project");
let err = resolve_project_id(&*storage, "nonexistent").expect_err("should error");
assert!(matches!(err, CodeNexusError::ProjectNotFound(_)));
}
#[test]
fn resolve_name_takes_precedence_over_id() {
let (_dir, db) = fresh_db_path();
let kit = build_kit_for_db(&db);
let storage = kit.require::<StorageModule>().expect("storage");
seed_project(&*storage, "p1", "p2");
seed_project(&*storage, "p2", "other");
let id = resolve_project_id(&*storage, "p2").expect("should resolve");
assert_eq!(id, "p1");
}
#[test]
fn resolve_empty_db_returns_project_not_found() {
let (_dir, db) = fresh_db_path();
let kit = build_kit_for_db(&db);
let storage = kit.require::<StorageModule>().expect("storage");
let err = resolve_project_id(&*storage, "anything").expect_err("should error");
assert!(matches!(err, CodeNexusError::ProjectNotFound(_)));
}
#[test]
fn resolve_when_project_table_dropped_returns_project_not_found() {
let (_dir, db) = fresh_db_path();
let kit = build_kit_for_db(&db);
let storage = kit.require::<StorageModule>().expect("storage");
storage
.execute("DROP TABLE Project;")
.expect("drop project table");
let err = resolve_project_id(&*storage, "anything").expect_err("should error");
assert!(
matches!(err, CodeNexusError::ProjectNotFound(_)),
"expected ProjectNotFound, got: {err:?}"
);
}
}