use crate::codegen::TargetLanguage;
use anyhow::{Context, Result, bail};
use std::path::PathBuf;
use thiserror::Error;
use super::scaffolder::ScaffoldedFile;
#[derive(Debug, Error)]
pub enum InitError {
#[error("Invalid project name '{name}': {reason}")]
InvalidProjectName { name: String, reason: String },
#[error("Directory '{path}' already exists; initialize in a new directory")]
DirectoryAlreadyExists { path: PathBuf },
#[error("Schema file not found: {path}")]
SchemaPathNotFound { path: PathBuf },
#[error("Scaffolding failed: {reason}")]
ScaffoldingFailed { reason: String },
}
#[derive(Debug, Clone)]
pub struct InitRequest {
pub project_name: String,
pub language: TargetLanguage,
pub project_dir: PathBuf,
pub schema_path: Option<PathBuf>,
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct InitResponse {
pub files_created: Vec<PathBuf>,
pub next_steps: Vec<String>,
}
pub struct InitEngine;
impl InitEngine {
pub fn execute(request: InitRequest) -> Result<InitResponse> {
Self::validate_request(&request).context("Project initialization request validation failed")?;
let scaffolder = Self::get_scaffolder(request.language);
let files = scaffolder
.scaffold(&request.project_dir, &request.project_name)
.context("Failed to scaffold project files")?;
std::fs::create_dir_all(&request.project_dir).context(format!(
"Failed to create project directory: {}",
request.project_dir.display()
))?;
let mut files_created = Vec::new();
for file in files {
let full_path = request.project_dir.join(&file.path);
if let Some(parent) = full_path.parent() {
std::fs::create_dir_all(parent).context(format!("Failed to create directory: {}", parent.display()))?;
}
std::fs::write(&full_path, &file.content)
.context(format!("Failed to write file: {}", full_path.display()))?;
files_created.push(full_path);
}
let next_steps = scaffolder.next_steps(&request.project_name);
Ok(InitResponse {
files_created,
next_steps,
})
}
fn get_scaffolder(language: TargetLanguage) -> Box<dyn super::scaffolder::ProjectScaffolder> {
match language {
TargetLanguage::Python => Box::new(super::python::PythonScaffolder),
TargetLanguage::TypeScript => Box::new(super::typescript::TypeScriptScaffolder),
TargetLanguage::Rust => Box::new(super::rust_lang::RustScaffolder),
TargetLanguage::Ruby => Box::new(super::ruby::RubyScaffolder),
TargetLanguage::Php => Box::new(super::php::PhpScaffolder),
TargetLanguage::Elixir => Box::new(super::elixir::ElixirScaffolder),
}
}
fn validate_request(request: &InitRequest) -> Result<()> {
Self::validate_project_name(&request.project_name, request.language)
.context("Project name validation failed")?;
if request.project_dir.exists() {
bail!(InitError::DirectoryAlreadyExists {
path: request.project_dir.clone(),
});
}
if let Some(schema_path) = &request.schema_path
&& !schema_path.exists()
{
bail!(InitError::SchemaPathNotFound {
path: schema_path.clone(),
});
}
Ok(())
}
pub fn validate_project_name(project_name: &str, language: TargetLanguage) -> Result<()> {
if project_name.is_empty() {
bail!(InitError::InvalidProjectName {
name: project_name.to_string(),
reason: "Project name cannot be empty".to_string(),
});
}
match language {
TargetLanguage::Python => {
if !project_name
.chars()
.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_')
{
bail!(InitError::InvalidProjectName {
name: project_name.to_string(),
reason: "Python project names must contain only lowercase letters, digits, and underscores"
.to_string(),
});
}
if project_name.starts_with(|c: char| c.is_ascii_digit()) {
bail!(InitError::InvalidProjectName {
name: project_name.to_string(),
reason: "Python project names cannot start with a digit".to_string(),
});
}
}
TargetLanguage::TypeScript => {
if !project_name
.chars()
.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-')
{
bail!(InitError::InvalidProjectName {
name: project_name.to_string(),
reason: "TypeScript project names must contain only lowercase letters, digits, and hyphens"
.to_string(),
});
}
}
TargetLanguage::Rust => {
if !project_name
.chars()
.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_')
{
bail!(InitError::InvalidProjectName {
name: project_name.to_string(),
reason: "Rust project names must contain only lowercase letters, digits, and underscores"
.to_string(),
});
}
if project_name.starts_with(|c: char| c.is_ascii_digit()) {
bail!(InitError::InvalidProjectName {
name: project_name.to_string(),
reason: "Rust project names cannot start with a digit".to_string(),
});
}
}
TargetLanguage::Ruby => {
if !project_name
.chars()
.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_')
{
bail!(InitError::InvalidProjectName {
name: project_name.to_string(),
reason: "Ruby project names must contain only lowercase letters, digits, and underscores"
.to_string(),
});
}
if project_name.starts_with(|c: char| c.is_ascii_digit()) {
bail!(InitError::InvalidProjectName {
name: project_name.to_string(),
reason: "Ruby project names cannot start with a digit".to_string(),
});
}
}
TargetLanguage::Php => {
if !project_name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') {
bail!(InitError::InvalidProjectName {
name: project_name.to_string(),
reason: "PHP project names must contain only alphanumeric characters and underscores"
.to_string(),
});
}
if project_name.starts_with(|c: char| c.is_ascii_digit()) {
bail!(InitError::InvalidProjectName {
name: project_name.to_string(),
reason: "PHP project names cannot start with a digit".to_string(),
});
}
}
TargetLanguage::Elixir => {
if !project_name
.chars()
.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_')
{
bail!(InitError::InvalidProjectName {
name: project_name.to_string(),
reason: "Elixir project names must contain only lowercase letters, digits, and underscores"
.to_string(),
});
}
if project_name.starts_with(|c: char| c.is_ascii_digit()) {
bail!(InitError::InvalidProjectName {
name: project_name.to_string(),
reason: "Elixir project names cannot start with a digit".to_string(),
});
}
}
}
Ok(())
}
#[allow(dead_code)]
fn write_files(project_dir: &std::path::Path, files: Vec<ScaffoldedFile>) -> Result<Vec<PathBuf>> {
std::fs::create_dir_all(project_dir).context("Failed to create project directory")?;
let mut created_files = Vec::new();
for file in files {
let full_path = project_dir.join(&file.path);
if let Some(parent) = full_path.parent()
&& !parent.exists()
{
std::fs::create_dir_all(parent).context(format!("Failed to create directory: {}", parent.display()))?;
}
std::fs::write(&full_path, &file.content)
.context(format!("Failed to write file: {}", full_path.display()))?;
created_files.push(full_path);
}
Ok(created_files)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_validate_python_project_name_valid() {
assert!(InitEngine::validate_project_name("my_api", TargetLanguage::Python).is_ok());
assert!(InitEngine::validate_project_name("api_v2", TargetLanguage::Python).is_ok());
assert!(InitEngine::validate_project_name("a", TargetLanguage::Python).is_ok());
}
#[test]
fn test_validate_python_project_name_invalid() {
assert!(InitEngine::validate_project_name("MyApi", TargetLanguage::Python).is_err());
assert!(InitEngine::validate_project_name("2api", TargetLanguage::Python).is_err());
assert!(InitEngine::validate_project_name("my-api", TargetLanguage::Python).is_err());
assert!(InitEngine::validate_project_name("", TargetLanguage::Python).is_err());
}
#[test]
fn test_validate_typescript_project_name_valid() {
assert!(InitEngine::validate_project_name("my-api", TargetLanguage::TypeScript).is_ok());
assert!(InitEngine::validate_project_name("api", TargetLanguage::TypeScript).is_ok());
}
#[test]
fn test_validate_typescript_project_name_invalid() {
assert!(InitEngine::validate_project_name("MyApi", TargetLanguage::TypeScript).is_err());
assert!(InitEngine::validate_project_name("my_api", TargetLanguage::TypeScript).is_err());
}
#[test]
fn test_validate_rust_project_name_valid() {
assert!(InitEngine::validate_project_name("my_api", TargetLanguage::Rust).is_ok());
assert!(InitEngine::validate_project_name("api", TargetLanguage::Rust).is_ok());
}
#[test]
fn test_validate_rust_project_name_invalid() {
assert!(InitEngine::validate_project_name("MyApi", TargetLanguage::Rust).is_err());
assert!(InitEngine::validate_project_name("2api", TargetLanguage::Rust).is_err());
assert!(InitEngine::validate_project_name("my-api", TargetLanguage::Rust).is_err());
}
#[test]
fn test_validate_ruby_project_name_valid() {
assert!(InitEngine::validate_project_name("my_api", TargetLanguage::Ruby).is_ok());
}
#[test]
fn test_validate_ruby_project_name_invalid() {
assert!(InitEngine::validate_project_name("2api", TargetLanguage::Ruby).is_err());
}
#[test]
fn test_validate_php_project_name_valid() {
assert!(InitEngine::validate_project_name("my_api", TargetLanguage::Php).is_ok());
assert!(InitEngine::validate_project_name("MyApi", TargetLanguage::Php).is_ok());
}
#[test]
fn test_validate_php_project_name_invalid() {
assert!(InitEngine::validate_project_name("2api", TargetLanguage::Php).is_err());
}
#[test]
fn test_validate_elixir_project_name_valid() {
assert!(InitEngine::validate_project_name("my_api", TargetLanguage::Elixir).is_ok());
}
#[test]
fn test_validate_elixir_project_name_invalid() {
assert!(InitEngine::validate_project_name("MyApi", TargetLanguage::Elixir).is_err());
assert!(InitEngine::validate_project_name("2api", TargetLanguage::Elixir).is_err());
}
}