use crate::constants::{DATABASE_FILE_NAME, METIS_DIR_NAME};
use crate::{Application, Database};
use anyhow::Result;
use std::path::{Path, PathBuf};
pub struct WorkspaceDetectionService;
impl WorkspaceDetectionService {
pub fn new() -> Self {
Self
}
pub fn find_workspace(&self) -> Result<Option<PathBuf>> {
let mut current_dir = std::env::current_dir()?;
loop {
let metis_dir = current_dir.join(METIS_DIR_NAME);
if let Some(validated_dir) = self.validate_workspace(&metis_dir)? {
return Ok(Some(validated_dir));
}
match current_dir.parent() {
Some(parent) => current_dir = parent.to_path_buf(),
None => break, }
}
Ok(None)
}
pub fn find_workspace_from(&self, start_path: &Path) -> Result<Option<PathBuf>> {
let mut current_dir = start_path.to_path_buf();
loop {
let metis_dir = current_dir.join(METIS_DIR_NAME);
if let Some(validated_dir) = self.validate_workspace(&metis_dir)? {
return Ok(Some(validated_dir));
}
match current_dir.parent() {
Some(parent) => current_dir = parent.to_path_buf(),
None => break, }
}
Ok(None)
}
pub fn validate_workspace(&self, metis_dir: &Path) -> Result<Option<PathBuf>> {
if !metis_dir.exists() || !metis_dir.is_dir() {
return Ok(None);
}
Ok(Some(metis_dir.to_path_buf()))
}
pub fn is_in_workspace(&self, path: &Path) -> Result<bool> {
Ok(self.find_workspace_from(path)?.is_some())
}
pub fn get_workspace_root(&self, path: &Path) -> Result<Option<PathBuf>> {
if let Some(metis_dir) = self.find_workspace_from(path)? {
if let Some(parent) = metis_dir.parent() {
return Ok(Some(parent.to_path_buf()));
}
}
Ok(None)
}
pub fn resolve_metis_dir(&self, path: &Path) -> PathBuf {
if path.file_name().map(|f| f == METIS_DIR_NAME).unwrap_or(false) {
return path.to_path_buf();
}
let metis_subdir = path.join(METIS_DIR_NAME);
if metis_subdir.is_dir() && metis_subdir.join("config.toml").exists() {
tracing::info!(
"Auto-corrected project_path from '{}' to '{}'",
path.display(),
metis_subdir.display()
);
return metis_subdir;
}
path.to_path_buf()
}
pub async fn prepare_workspace(&self, metis_dir: &Path) -> Result<Database> {
let metis_dir = self.resolve_metis_dir(metis_dir);
let metis_dir = metis_dir.as_path();
if self.validate_workspace(metis_dir)?.is_none() {
anyhow::bail!("Not a valid Metis workspace: {}", metis_dir.display());
}
let db_path = metis_dir.join(DATABASE_FILE_NAME);
let database = Database::new(db_path.to_str().unwrap())
.map_err(|e| anyhow::anyhow!("Failed to initialize database: {}", e))?;
let app = Application::new(database);
app.sync_directory(metis_dir).await?;
let database = Database::new(db_path.to_str().unwrap())
.map_err(|e| anyhow::anyhow!("Failed to reconnect to database: {}", e))?;
Ok(database)
}
pub async fn find_and_prepare_workspace(&self) -> Result<Option<(PathBuf, Database)>> {
if let Some(metis_dir) = self.find_workspace()? {
let db = self.prepare_workspace(&metis_dir).await?;
Ok(Some((metis_dir, db)))
} else {
Ok(None)
}
}
}
impl Default for WorkspaceDetectionService {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
#[test]
fn test_validate_workspace_missing_directory() {
let service = WorkspaceDetectionService::new();
let temp_dir = TempDir::new().unwrap();
let metis_dir = temp_dir.path().join(METIS_DIR_NAME);
let result = service.validate_workspace(&metis_dir).unwrap();
assert!(result.is_none());
}
#[test]
fn test_validate_workspace_with_metis_directory() {
let service = WorkspaceDetectionService::new();
let temp_dir = TempDir::new().unwrap();
let metis_dir = temp_dir.path().join(METIS_DIR_NAME);
fs::create_dir_all(&metis_dir).unwrap();
let result = service.validate_workspace(&metis_dir);
assert!(result.is_ok());
assert!(result.unwrap().is_some());
}
#[test]
fn test_find_workspace_traversal() {
let service = WorkspaceDetectionService::new();
let temp_dir = TempDir::new().unwrap();
let project_root = temp_dir.path();
let metis_dir = project_root.join(".metis");
let nested_dir = project_root.join("src").join("deep").join("nested");
fs::create_dir_all(&metis_dir).unwrap();
fs::create_dir_all(&nested_dir).unwrap();
let result = service.find_workspace_from(&nested_dir).unwrap();
assert!(result.is_some());
assert_eq!(result.unwrap(), metis_dir);
}
#[test]
fn test_resolve_metis_dir_already_metis() {
let service = WorkspaceDetectionService::new();
let temp_dir = TempDir::new().unwrap();
let metis_dir = temp_dir.path().join(".metis");
fs::create_dir_all(&metis_dir).unwrap();
let resolved = service.resolve_metis_dir(&metis_dir);
assert_eq!(resolved, metis_dir);
}
#[test]
fn test_resolve_metis_dir_from_project_root() {
let service = WorkspaceDetectionService::new();
let temp_dir = TempDir::new().unwrap();
let project_root = temp_dir.path();
let metis_dir = project_root.join(".metis");
fs::create_dir_all(&metis_dir).unwrap();
fs::write(metis_dir.join("config.toml"), "[project]\nprefix = \"TEST\"").unwrap();
let resolved = service.resolve_metis_dir(project_root);
assert_eq!(resolved, metis_dir);
}
#[test]
fn test_resolve_metis_dir_no_config_toml() {
let service = WorkspaceDetectionService::new();
let temp_dir = TempDir::new().unwrap();
let project_root = temp_dir.path();
let metis_dir = project_root.join(".metis");
fs::create_dir_all(&metis_dir).unwrap();
let resolved = service.resolve_metis_dir(project_root);
assert_eq!(resolved, project_root.to_path_buf());
}
#[test]
fn test_resolve_metis_dir_no_metis_subdir() {
let service = WorkspaceDetectionService::new();
let temp_dir = TempDir::new().unwrap();
let some_path = temp_dir.path().join("random");
fs::create_dir_all(&some_path).unwrap();
let resolved = service.resolve_metis_dir(&some_path);
assert_eq!(resolved, some_path);
}
#[test]
fn test_get_workspace_root() {
let service = WorkspaceDetectionService::new();
let temp_dir = TempDir::new().unwrap();
let project_root = temp_dir.path();
let metis_dir = project_root.join(".metis");
let nested_dir = project_root.join("src").join("deep");
fs::create_dir_all(&metis_dir).unwrap();
fs::create_dir_all(&nested_dir).unwrap();
let result = service.get_workspace_root(&nested_dir).unwrap();
assert!(result.is_some());
assert_eq!(result.unwrap(), project_root);
}
}