use crate::dal::database::configuration_repository::ConfigurationRepository;
use crate::domain::configuration::ConfigFile;
use crate::{Database, MetisError, Phase, Result, Tag, Vision};
use diesel::{sqlite::SqliteConnection, Connection};
use std::path::{Path, PathBuf};
pub struct WorkspaceInitializationService;
pub struct WorkspaceInitializationResult {
pub metis_dir: PathBuf,
pub database_path: PathBuf,
pub vision_path: PathBuf,
}
impl WorkspaceInitializationService {
pub async fn initialize_workspace<P: AsRef<Path>>(
base_path: P,
project_name: &str,
) -> Result<WorkspaceInitializationResult> {
Self::initialize_workspace_with_prefix(base_path, project_name, None).await
}
pub async fn initialize_workspace_with_prefix<P: AsRef<Path>>(
base_path: P,
project_name: &str,
prefix: Option<&str>,
) -> Result<WorkspaceInitializationResult> {
let base_path = base_path.as_ref();
let metis_dir = base_path.join(".metis");
std::fs::create_dir_all(&metis_dir)?;
let db_path = metis_dir.join("metis.db");
let db_exists = db_path.exists();
let db_result = Database::new(db_path.to_str().unwrap());
match db_result {
Ok(_db) => {
let mut config_repo = ConfigurationRepository::new(
SqliteConnection::establish(db_path.to_str().unwrap()).map_err(|e| {
MetisError::ConfigurationError(
crate::domain::configuration::ConfigurationError::InvalidValue(
e.to_string(),
),
)
})?,
);
let project_prefix = if config_repo.get_project_prefix()?.is_none() {
let default_prefix = {
let p = prefix.unwrap_or("PROJ").to_uppercase();
if p.len() > 6 {
p.chars().take(6).collect()
} else {
p
}
};
config_repo.set_project_prefix(&default_prefix)?;
default_prefix
} else {
config_repo.get_project_prefix()?.unwrap()
};
let config_file_path = metis_dir.join("config.toml");
if !config_file_path.exists() {
let flight_levels = config_repo.get_flight_level_config()?;
let config_file = ConfigFile::new(project_prefix, flight_levels)
.map_err(MetisError::ConfigurationError)?;
config_file.save(&config_file_path)
.map_err(MetisError::ConfigurationError)?;
tracing::info!("Created configuration file at {}", config_file_path.display());
}
}
Err(e) => {
if db_exists {
return Err(MetisError::FileSystem(format!(
"Invalid existing database at {}: {}",
db_path.display(),
e
)));
} else {
return Err(MetisError::FileSystem(format!(
"Failed to initialize database: {}",
e
)));
}
}
}
let strategies_dir = metis_dir.join("strategies");
std::fs::create_dir_all(&strategies_dir)?;
let vision_path = metis_dir.join("vision.md");
if !vision_path.exists() {
let vision_path = Self::create_default_vision(&metis_dir, project_name).await?;
Ok(WorkspaceInitializationResult {
metis_dir,
database_path: db_path,
vision_path,
})
} else {
Ok(WorkspaceInitializationResult {
metis_dir,
database_path: db_path,
vision_path,
})
}
}
async fn create_default_vision(workspace_dir: &Path, title: &str) -> Result<PathBuf> {
let db_path = workspace_dir.join("metis.db");
let mut config_repo = ConfigurationRepository::new(
SqliteConnection::establish(&db_path.to_string_lossy()).map_err(|e| {
MetisError::ConfigurationError(
crate::domain::configuration::ConfigurationError::InvalidValue(e.to_string()),
)
})?,
);
let short_code = config_repo.generate_short_code("vision")?;
let tags = vec![Tag::Label("vision".to_string()), Tag::Phase(Phase::Draft)];
let vision = Vision::new(
title.to_string(),
tags,
false, short_code,
)?;
let vision_path = workspace_dir.join("vision.md");
vision.to_file(&vision_path).await?;
Ok(vision_path)
}
pub fn is_workspace(path: &Path) -> bool {
let metis_dir = path.join(".metis");
metis_dir.exists() && metis_dir.is_dir()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::tempdir;
#[tokio::test]
async fn test_initialize_workspace() {
let temp_dir = tempdir().unwrap();
let base_path = temp_dir.path();
let result =
WorkspaceInitializationService::initialize_workspace(base_path, "Test Project").await;
assert!(result.is_ok());
let result = result.unwrap();
let metis_dir = base_path.join(".metis");
assert!(metis_dir.exists());
assert!(metis_dir.is_dir());
assert_eq!(result.metis_dir, metis_dir);
let db_path = metis_dir.join("metis.db");
assert!(db_path.exists());
assert!(db_path.is_file());
assert_eq!(result.database_path, db_path);
let strategies_dir = metis_dir.join("strategies");
assert!(strategies_dir.exists());
assert!(strategies_dir.is_dir());
let vision_path = metis_dir.join("vision.md");
assert!(vision_path.exists());
assert!(vision_path.is_file());
assert_eq!(result.vision_path, vision_path);
let vision_content = fs::read_to_string(&vision_path).unwrap();
assert!(vision_content.contains("Test Project"));
assert!(vision_content.contains("#vision"));
assert!(vision_content.contains("#phase/draft"));
assert!(vision_content.contains("archived: false"));
let config_path = metis_dir.join("config.toml");
assert!(config_path.exists(), "config.toml should be created during initialization");
assert!(config_path.is_file());
let config_content = fs::read_to_string(&config_path).unwrap();
assert!(config_content.contains("[project]"));
assert!(config_content.contains("prefix = \"PROJ\""));
}
#[tokio::test]
async fn test_initialize_workspace_already_exists() {
let temp_dir = tempdir().unwrap();
let base_path = temp_dir.path();
let result1 =
WorkspaceInitializationService::initialize_workspace(base_path, "Test Project").await;
assert!(result1.is_ok());
let db_path = base_path.join(".metis").join("metis.db");
let original_size = fs::metadata(&db_path).unwrap().len();
let result2 =
WorkspaceInitializationService::initialize_workspace(base_path, "Test Project").await;
assert!(result2.is_ok());
let new_size = fs::metadata(&db_path).unwrap().len();
assert!(new_size > 0);
assert!(new_size >= original_size / 2 && new_size <= original_size * 2);
}
#[test]
fn test_is_workspace() {
let temp_dir = tempdir().unwrap();
let base_path = temp_dir.path();
assert!(!WorkspaceInitializationService::is_workspace(base_path));
let metis_dir = base_path.join(".metis");
fs::create_dir_all(&metis_dir).unwrap();
assert!(WorkspaceInitializationService::is_workspace(base_path));
}
}