use portalis_core::{Error, Result};
use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
pub struct WorkspaceGenerator {
root_path: PathBuf,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CrateInfo {
pub name: String,
pub path: String,
pub rust_code: String,
pub dependencies: Vec<String>,
pub external_deps: Vec<ExternalDependency>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExternalDependency {
pub name: String,
pub version: String,
pub features: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WorkspaceConfig {
pub name: String,
pub crates: Vec<CrateInfo>,
}
impl WorkspaceGenerator {
pub fn new(root_path: PathBuf) -> Self {
Self { root_path }
}
pub fn generate(&self, config: &WorkspaceConfig) -> Result<()> {
fs::create_dir_all(&self.root_path)
.map_err(|e| Error::Pipeline(format!("Failed to create workspace root: {}", e)))?;
self.generate_workspace_toml(config)?;
for crate_info in &config.crates {
self.generate_crate(crate_info)?;
}
self.generate_readme(config)?;
Ok(())
}
fn generate_workspace_toml(&self, config: &WorkspaceConfig) -> Result<()> {
let mut content = String::new();
content.push_str("[workspace]\n");
content.push_str("resolver = \"2\"\n");
content.push_str("members = [\n");
for crate_info in &config.crates {
content.push_str(&format!(" \"{}\",\n", crate_info.path));
}
content.push_str("]\n\n");
content.push_str("[workspace.dependencies]\n");
let mut all_deps: HashMap<String, &ExternalDependency> = HashMap::new();
for crate_info in &config.crates {
for dep in &crate_info.external_deps {
all_deps.entry(dep.name.clone()).or_insert(dep);
}
}
for (name, dep) in &all_deps {
if dep.features.is_empty() {
content.push_str(&format!("{} = \"{}\"\n", name, dep.version));
} else {
content.push_str(&format!(
"{} = {{ version = \"{}\", features = {:?} }}\n",
name, dep.version, dep.features
));
}
}
let toml_path = self.root_path.join("Cargo.toml");
fs::write(&toml_path, content)
.map_err(|e| Error::Pipeline(format!("Failed to write workspace Cargo.toml: {}", e)))?;
Ok(())
}
fn generate_crate(&self, crate_info: &CrateInfo) -> Result<()> {
let crate_path = self.root_path.join(&crate_info.path);
let src_path = crate_path.join("src");
fs::create_dir_all(&src_path)
.map_err(|e| Error::Pipeline(format!("Failed to create crate directory: {}", e)))?;
self.generate_crate_toml(crate_info)?;
let lib_path = src_path.join("lib.rs");
fs::write(&lib_path, &crate_info.rust_code)
.map_err(|e| Error::Pipeline(format!("Failed to write lib.rs: {}", e)))?;
Ok(())
}
fn generate_crate_toml(&self, crate_info: &CrateInfo) -> Result<()> {
let mut content = String::new();
content.push_str("[package]\n");
content.push_str(&format!("name = \"{}\"\n", crate_info.name));
content.push_str("version = \"0.1.0\"\n");
content.push_str("edition = \"2021\"\n\n");
if !crate_info.dependencies.is_empty() || !crate_info.external_deps.is_empty() {
content.push_str("[dependencies]\n");
for dep in &crate_info.dependencies {
content.push_str(&format!("{} = {{ path = \"../{}\" }}\n", dep, dep));
}
for dep in &crate_info.external_deps {
content.push_str(&format!("{} = {{ workspace = true }}\n", dep.name));
}
}
let toml_path = self.root_path.join(&crate_info.path).join("Cargo.toml");
fs::write(&toml_path, content)
.map_err(|e| Error::Pipeline(format!("Failed to write crate Cargo.toml: {}", e)))?;
Ok(())
}
fn generate_readme(&self, config: &WorkspaceConfig) -> Result<()> {
let mut content = String::new();
content.push_str(&format!("# {}\n\n", config.name));
content.push_str("Generated by Portalis - Python to Rust transpiler\n\n");
content.push_str("## Project Structure\n\n");
content.push_str("```\n");
content.push_str(&format!("{}/\n", config.name));
content.push_str("├── Cargo.toml\n");
for crate_info in &config.crates {
content.push_str(&format!("├── {}/\n", crate_info.path));
content.push_str(&format!("│ ├── Cargo.toml\n"));
content.push_str(&format!("│ └── src/lib.rs\n"));
}
content.push_str("```\n\n");
content.push_str("## Building\n\n");
content.push_str("```bash\n");
content.push_str("cargo build --workspace\n");
content.push_str("```\n\n");
content.push_str("## Testing\n\n");
content.push_str("```bash\n");
content.push_str("cargo test --workspace\n");
content.push_str("```\n\n");
content.push_str("## Crates\n\n");
for crate_info in &config.crates {
content.push_str(&format!("- **{}**: {}\n", crate_info.name, crate_info.path));
}
let readme_path = self.root_path.join("README.md");
fs::write(&readme_path, content)
.map_err(|e| Error::Pipeline(format!("Failed to write README: {}", e)))?;
Ok(())
}
pub fn clean(&self) -> Result<()> {
if self.root_path.exists() {
fs::remove_dir_all(&self.root_path)
.map_err(|e| Error::Pipeline(format!("Failed to clean workspace: {}", e)))?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn test_generate_simple_workspace() {
let temp_dir = TempDir::new().unwrap();
let workspace_path = temp_dir.path().join("test_workspace");
let generator = WorkspaceGenerator::new(workspace_path.clone());
let config = WorkspaceConfig {
name: "my_project".to_string(),
crates: vec![
CrateInfo {
name: "core".to_string(),
path: "core".to_string(),
rust_code: "pub fn hello() -> &'static str { \"Hello\" }".to_string(),
dependencies: vec![],
external_deps: vec![],
},
],
};
generator.generate(&config).unwrap();
assert!(workspace_path.exists());
assert!(workspace_path.join("Cargo.toml").exists());
assert!(workspace_path.join("core").exists());
assert!(workspace_path.join("core/Cargo.toml").exists());
assert!(workspace_path.join("core/src/lib.rs").exists());
assert!(workspace_path.join("README.md").exists());
}
#[test]
fn test_generate_multi_crate_workspace() {
let temp_dir = TempDir::new().unwrap();
let workspace_path = temp_dir.path().join("multi_crate");
let generator = WorkspaceGenerator::new(workspace_path.clone());
let config = WorkspaceConfig {
name: "multi_crate_project".to_string(),
crates: vec![
CrateInfo {
name: "core".to_string(),
path: "core".to_string(),
rust_code: "pub struct Data {}".to_string(),
dependencies: vec![],
external_deps: vec![],
},
CrateInfo {
name: "utils".to_string(),
path: "utils".to_string(),
rust_code: "pub fn util() {}".to_string(),
dependencies: vec!["core".to_string()],
external_deps: vec![],
},
],
};
generator.generate(&config).unwrap();
assert!(workspace_path.join("core").exists());
assert!(workspace_path.join("utils").exists());
let utils_toml = fs::read_to_string(workspace_path.join("utils/Cargo.toml")).unwrap();
assert!(utils_toml.contains("core"));
}
#[test]
fn test_workspace_with_external_deps() {
let temp_dir = TempDir::new().unwrap();
let workspace_path = temp_dir.path().join("with_deps");
let generator = WorkspaceGenerator::new(workspace_path.clone());
let config = WorkspaceConfig {
name: "project_with_deps".to_string(),
crates: vec![
CrateInfo {
name: "app".to_string(),
path: "app".to_string(),
rust_code: "use serde_json::Value;".to_string(),
dependencies: vec![],
external_deps: vec![
ExternalDependency {
name: "serde_json".to_string(),
version: "1.0".to_string(),
features: vec![],
},
],
},
],
};
generator.generate(&config).unwrap();
let workspace_toml = fs::read_to_string(workspace_path.join("Cargo.toml")).unwrap();
assert!(workspace_toml.contains("serde_json"));
assert!(workspace_toml.contains("1.0"));
let app_toml = fs::read_to_string(workspace_path.join("app/Cargo.toml")).unwrap();
assert!(app_toml.contains("workspace = true"));
}
#[test]
fn test_clean_workspace() {
let temp_dir = TempDir::new().unwrap();
let workspace_path = temp_dir.path().join("to_clean");
let generator = WorkspaceGenerator::new(workspace_path.clone());
let config = WorkspaceConfig {
name: "temp_project".to_string(),
crates: vec![
CrateInfo {
name: "temp".to_string(),
path: "temp".to_string(),
rust_code: "".to_string(),
dependencies: vec![],
external_deps: vec![],
},
],
};
generator.generate(&config).unwrap();
assert!(workspace_path.exists());
generator.clean().unwrap();
assert!(!workspace_path.exists());
}
}