use crate::paths;
use anyhow::{Context, Result};
use std::path::Path;
const CARGO_TOML_DEV: &str = include_str!("../../templates/Cargo.toml.dev.template");
const CARGO_TOML_PROD: &str = include_str!("../../templates/Cargo.toml.prod.template");
const MAIN_RS: &str = include_str!("../../templates/main.rs.template");
const BUILD_RS: &str = include_str!("../../templates/build.rs.template");
const RUSTFMT_TOML: &str = include_str!("../../templates/rustfmt.toml.template");
const CARGO_CONFIG: &str = include_str!("../../templates/.cargo-config.toml.template");
pub struct RustTemplates;
impl RustTemplates {
pub fn new() -> Self {
Self
}
pub async fn create_cargo_toml(&self, path: &Path, project_name: &str, dev: bool) -> Result<()> {
let template = if dev { CARGO_TOML_DEV } else { CARGO_TOML_PROD };
let content = template.replace("{{project_name}}", project_name);
tokio::fs::write(path.join(paths::rust::CARGO_TOML), content).await?;
Ok(())
}
pub async fn create_main_rs(&self, path: &Path, project_name: &str) -> Result<()> {
let content = MAIN_RS.replace("{{project_name}}", project_name);
let src_dir = path.join(paths::project::SRC_DIR);
tokio::fs::create_dir_all(&src_dir).await?;
tokio::fs::write(path.join(paths::rust::MAIN_RS), content).await?;
Ok(())
}
pub async fn create_build_rs(&self, path: &Path) -> Result<()> {
tokio::fs::write(path.join(paths::rust::BUILD_RS), BUILD_RS).await?;
Ok(())
}
pub async fn create_rustfmt_toml(&self, path: &Path) -> Result<()> {
tokio::fs::write(path.join(paths::rust::RUSTFMT_TOML), RUSTFMT_TOML).await?;
Ok(())
}
pub async fn create_cargo_config(&self, path: &Path, framework_path: &str) -> Result<()> {
let patches = generate_cargo_patches(framework_path);
let content = CARGO_CONFIG.replace("{{cargo_patches}}", &patches);
let cargo_dir = path.join(paths::rust::CARGO_CONFIG_DIR);
tokio::fs::create_dir_all(&cargo_dir).await?;
tokio::fs::write(path.join(paths::rust::CARGO_CONFIG), content).await?;
Ok(())
}
}
impl Default for RustTemplates {
fn default() -> Self {
Self::new()
}
}
fn generate_cargo_patches(framework_path: &str) -> String {
match generate_cargo_patches_from_workspace(framework_path) {
Ok(patches) => patches,
Err(e) => {
eprintln!("Warning: Failed to parse workspace Cargo.toml: {}", e);
eprintln!("Falling back to hardcoded package list");
generate_cargo_patches_fallback(framework_path)
}
}
}
fn generate_cargo_patches_from_workspace(framework_path: &str) -> Result<String> {
let workspace_toml_path = Path::new(framework_path).join(paths::rust::CARGO_TOML);
let content = std::fs::read_to_string(&workspace_toml_path).context("Failed to read workspace Cargo.toml")?;
let toml: toml::Value = toml::from_str(&content).context("Failed to parse workspace Cargo.toml")?;
let members = toml
.get("workspace")
.and_then(|w| w.get("members"))
.and_then(|m| m.as_array())
.context("No workspace.members found in Cargo.toml")?;
let categories = categorize_packages(framework_path, members)?;
let mut patches = String::new();
patches.push_str("# Auto-generated from workspace Cargo.toml\n\n");
for (category, pkgs) in categories {
if pkgs.is_empty() {
continue;
}
patches.push_str(&format!("# {}\n", category));
for (name, pkg_path) in pkgs {
patches.push_str(&format!(
"{} = {{ path = \"{}/{}\" }}\n",
name, framework_path, pkg_path
));
}
patches.push('\n');
}
Ok(patches)
}
type CategorizedPackages = Vec<(&'static str, Vec<(String, String)>)>;
fn categorize_packages(framework_path: &str, members: &[toml::Value]) -> Result<CategorizedPackages> {
let mut categories: CategorizedPackages = vec![
("Core packages", vec![]),
("Node packages", vec![]),
("Driver packages", vec![]),
("Service packages", vec![]),
("Other packages", vec![]),
];
for member in members {
if let Some(path) = member.as_str() {
let name = read_package_name(framework_path, path)?;
let category_idx = if path.starts_with("packages/nodes/") {
1 } else if path.starts_with("packages/drivers/") {
2 } else if path.starts_with("packages/services/") {
3 } else if matches!(
path,
"packages/core"
| "packages/messaging"
| "packages/config"
| "packages/runtime"
| "packages/mecha10-macros"
) {
0 } else {
4 };
categories[category_idx].1.push((name, path.to_string()));
}
}
Ok(categories)
}
fn read_package_name(framework_path: &str, package_path: &str) -> Result<String> {
let cargo_toml_path = Path::new(framework_path)
.join(package_path)
.join(paths::rust::CARGO_TOML);
let content = std::fs::read_to_string(&cargo_toml_path)
.with_context(|| format!("Failed to read Cargo.toml for package at {}", package_path))?;
let toml: toml::Value = toml::from_str(&content).context("Failed to parse Cargo.toml")?;
let name = toml
.get("package")
.and_then(|p| p.get("name"))
.and_then(|n| n.as_str())
.context("No package.name found in Cargo.toml")?;
Ok(name.to_string())
}
fn generate_cargo_patches_fallback(framework_path: &str) -> String {
let packages = [
(
"Core packages",
vec![
("mecha10-core", "packages/core"),
("mecha10-macros", "packages/mecha10-macros"),
("mecha10-messaging", "packages/messaging"),
("mecha10-runtime", "packages/runtime"),
("mecha10-config", "packages/config"),
],
),
(
"Node packages",
vec![
("mecha10-nodes-speaker", "packages/nodes/speaker"),
("mecha10-nodes-listener", "packages/nodes/listener"),
("mecha10-nodes-motor", "packages/nodes/motor"),
("mecha10-nodes-imu", "packages/nodes/imu"),
("mecha10-nodes-teleop", "packages/nodes/teleop"),
("mecha10-nodes-simulation-bridge", "packages/nodes/simulation-bridge"),
],
),
(
"Driver packages",
vec![
("mecha10-driver-motor-fake", "packages/drivers/motor_fake"),
("mecha10-driver-imu-fake", "packages/drivers/imu_fake"),
],
),
];
let mut patches = String::new();
patches.push_str("# Fallback hardcoded package list\n\n");
for (category, pkgs) in packages {
patches.push_str(&format!("# {}\n", category));
for (name, pkg_path) in pkgs {
patches.push_str(&format!(
"{} = {{ path = \"{}/{}\" }}\n",
name, framework_path, pkg_path
));
}
patches.push('\n');
}
patches
}