mecha10-cli 0.1.47

Mecha10 CLI tool
Documentation
//! Rust project template generation
//!
//! Handles generation of Rust-specific files:
//! - Cargo.toml (dev and production variants)
//! - src/main.rs
//! - build.rs
//! - rustfmt.toml
//! - .cargo/config.toml

use crate::paths;
use anyhow::{Context, Result};
use std::path::Path;

/// Template files embedded at compile time
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");

/// Rust template generator
pub struct RustTemplates;

impl RustTemplates {
    /// Create a new RustTemplates instance
    pub fn new() -> Self {
        Self
    }

    /// Create Cargo.toml for the project
    ///
    /// # Arguments
    ///
    /// * `path` - Project root path
    /// * `project_name` - Name of the project
    /// * `dev` - Whether this is for framework development
    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(())
    }

    /// Create src/main.rs for the project
    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(())
    }

    /// Create build.rs for the project
    pub async fn create_build_rs(&self, path: &Path) -> Result<()> {
        tokio::fs::write(path.join(paths::rust::BUILD_RS), BUILD_RS).await?;
        Ok(())
    }

    /// Create rustfmt.toml for the project
    pub async fn create_rustfmt_toml(&self, path: &Path) -> Result<()> {
        tokio::fs::write(path.join(paths::rust::RUSTFMT_TOML), RUSTFMT_TOML).await?;
        Ok(())
    }

    /// Create .cargo/config.toml for framework development
    ///
    /// # Arguments
    ///
    /// * `path` - Project root path
    /// * `framework_path` - Path to the mecha10-monorepo root
    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()
    }
}

/// Generate cargo patch entries by parsing workspace Cargo.toml
///
/// This automatically discovers all workspace members from the monorepo's
/// Cargo.toml, eliminating the need to maintain a hardcoded list.
fn generate_cargo_patches(framework_path: &str) -> String {
    // Try automated parsing first, fall back to hardcoded list if it fails
    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)
        }
    }
}

/// Automatically generate patches by parsing workspace Cargo.toml
fn generate_cargo_patches_from_workspace(framework_path: &str) -> Result<String> {
    // Read workspace Cargo.toml
    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")?;

    // Extract workspace members
    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")?;

    // Categorize packages for better readability
    let categories = categorize_packages(framework_path, members)?;

    // Generate patch strings
    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 alias for categorized packages: (category_name, vec_of_(package_name, package_path))
type CategorizedPackages = Vec<(&'static str, Vec<(String, String)>)>;

/// Categorize packages by type for organized patch output
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() {
            // Get actual package name from Cargo.toml
            let name = read_package_name(framework_path, path)?;

            // Categorize by path
            let category_idx = if path.starts_with("packages/nodes/") {
                1 // Node packages
            } else if path.starts_with("packages/drivers/") {
                2 // Driver packages
            } else if path.starts_with("packages/services/") {
                3 // Service packages
            } else if matches!(
                path,
                "packages/core"
                    | "packages/messaging"
                    | "packages/config"
                    | "packages/runtime"
                    | "packages/mecha10-macros"
            ) {
                0 // Core packages
            } else {
                4 // Other packages
            };

            categories[category_idx].1.push((name, path.to_string()));
        }
    }

    Ok(categories)
}

/// Read actual package name from a Cargo.toml file
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())
}

/// Fallback hardcoded package list (used if workspace parsing fails)
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
}