ggen-core 26.7.3

Core graph-aware code generation engine
Documentation
//! Workspace generator for 2026 CLI projects
//!
//! This module generates the workspace structure with separate CLI and domain crates,
//! following Rust workspace best practices for 2026. It creates the root workspace
//! Cargo.toml and directory structure needed for a multi-crate project.
//!
//! ## Features
//!
//! - **Workspace Root**: Generates workspace Cargo.toml with member crates
//! - **Directory Structure**: Creates crates directory for CLI and domain crates
//! - **Template-Based**: Uses Tera templates for flexible workspace generation
//! - **Project Metadata**: Incorporates project name, version, edition, license, and authors
//!
//! ## Architecture
//!
//! The workspace generator creates a structure like:
//!
//! ```text
//! project-root/
//! ├── Cargo.toml          # Workspace manifest
//! └── crates/
//!     ├── cli/            # CLI crate (generated by CliLayerGenerator)
//!     └── domain/          # Domain crate (generated by DomainLayerGenerator)
//! ```
//!
//! ## Examples
//!
//! ### Generating a Workspace
//!
//! ```ignore
//! use crate::cli_generator::{WorkspaceGenerator, types::CliProject};
//! use std::path::Path;
//!
//! # fn main() -> crate::utils::error::Result<()> {
//! let generator = WorkspaceGenerator::new(Path::new("templates"))?;
//!
//! let project = CliProject {
//!     name: "my-cli".to_string(),
//!     version: "1.0.0".to_string(),
//!     edition: "2021".to_string(),
//!     license: "MIT".to_string(),
//!     authors: vec!["Alice".to_string()],
//!     resolver: "2".to_string(),
//!     cli_crate: Some("cli".to_string()),
//!     domain_crate: Some("domain".to_string()),
//!     // ... other fields
//! };
//!
//! generator.generate(&project, Path::new("output"))?;
//! # Ok(())
//! # }
//! ```

use crate::cli_generator::types::CliProject;
use crate::utils::error::{Error, Result};
use std::path::Path;
use tera::{Context, Tera};

/// Template path for workspace Cargo.toml
const WORKSPACE_CARGO_TEMPLATE: &str = "cli/workspace/Cargo.toml.tmpl";

/// Workspace generator for creating workspace structure
///
/// This generator creates Rust workspace structures with separate CLI and domain crates.
/// It generates the root workspace Cargo.toml and directory structure needed for
/// multi-crate projects following 2026 best practices.
///
/// # Examples
///
/// ```rust,no_run
/// use crate::cli_generator::workspace::WorkspaceGenerator;
/// use std::path::Path;
///
/// # fn main() -> crate::utils::error::Result<()> {
/// let generator = WorkspaceGenerator::new(Path::new("templates"))?;
/// # Ok(())
/// # }
/// ```
pub struct WorkspaceGenerator {
    tera: Tera,
}

impl WorkspaceGenerator {
    /// Create a new workspace generator
    ///
    /// Loads Tera templates from the specified directory. Templates should be
    /// organized in subdirectories matching the template paths used in `generate()`.
    ///
    /// # Arguments
    ///
    /// * `template_dir` - Directory containing Tera template files (`.tmpl` extension)
    ///
    /// # Errors
    ///
    /// Returns an error if the template directory cannot be read or if templates
    /// fail to load.
    ///
    /// # Examples
    ///
    /// ```rust,no_run
    /// use crate::cli_generator::workspace::WorkspaceGenerator;
    /// use std::path::Path;
    ///
    /// # fn main() -> crate::utils::error::Result<()> {
    /// let generator = WorkspaceGenerator::new(Path::new("./templates"))?;
    /// # Ok(())
    /// # }
    /// ```
    pub fn new(template_dir: &Path) -> Result<Self> {
        let pattern = format!("{}/**/*.tmpl", template_dir.display());
        let tera = Tera::new(&pattern).map_err(|e| {
            Error::with_context(
                "Failed to load templates",
                &format!("{}: {}", template_dir.display(), e),
            )
        })?;

        Ok(Self { tera })
    }

    /// Generate workspace structure
    ///
    /// Creates a complete Rust workspace structure including:
    /// - Workspace root `Cargo.toml` with member crates
    /// - `crates/` directory for CLI and domain crates
    ///
    /// # Arguments
    ///
    /// * `project` - CLI project configuration with metadata and crate names
    /// * `output_dir` - Root directory where workspace will be created
    ///
    /// # Errors
    ///
    /// Returns an error if:
    /// - `cli_crate` or `domain_crate` are missing from project
    /// - Template rendering fails
    /// - File system operations fail
    ///
    /// # Examples
    ///
    /// ```ignore
    /// use crate::cli_generator::{workspace::WorkspaceGenerator, types::CliProject};
    /// use std::path::Path;
    ///
    /// # fn main() -> crate::utils::error::Result<()> {
    /// let generator = WorkspaceGenerator::new(Path::new("templates"))?;
    ///
    /// let project = CliProject {
    ///     name: "my-cli".to_string(),
    ///     version: "1.0.0".to_string(),
    ///     edition: "2021".to_string(),
    ///     license: "MIT".to_string(),
    ///     authors: vec!["Alice".to_string()],
    ///     resolver: "2".to_string(),
    ///     cli_crate: Some("cli".to_string()),
    ///     domain_crate: Some("domain".to_string()),
    ///     // ... other required fields
    /// };
    ///
    /// generator.generate(&project, Path::new("output"))?;
    /// // Creates output/Cargo.toml and output/crates/ directory
    /// # Ok(())
    /// # }
    /// ```
    pub fn generate(&self, project: &CliProject, output_dir: &Path) -> Result<()> {
        let mut context = Context::new();
        context.insert("project_name", &project.name);
        let cli_crate = project
            .cli_crate
            .as_ref()
            .ok_or_else(|| Error::new("cli_crate is required for workspace generation"))?;
        let core_crate = project
            .domain_crate
            .as_ref()
            .ok_or_else(|| Error::new("domain_crate is required for workspace generation"))?;
        context.insert("cli_crate", cli_crate);
        context.insert("core_crate", core_crate);
        context.insert("version", &project.version);
        context.insert("edition", &project.edition);
        context.insert("license", &project.license);
        context.insert("authors", &project.authors);
        context.insert("resolver", &project.resolver);
        context.insert("project", project);

        // Generate workspace root Cargo.toml
        let workspace_cargo = output_dir.join("Cargo.toml");
        self.render_template(WORKSPACE_CARGO_TEMPLATE, &context, &workspace_cargo)
            .map_err(|e| {
                Error::with_context("Failed to generate workspace Cargo.toml", &e.to_string())
            })?;

        // Create crates directory
        let crates_dir = output_dir.join("crates");
        std::fs::create_dir_all(&crates_dir).map_err(|e| {
            Error::with_context("Failed to create crates directory", &e.to_string())
        })?;

        Ok(())
    }

    fn render_template(&self, template: &str, context: &Context, output: &Path) -> Result<()> {
        let content = self.tera.render(template, context).map_err(|e| {
            Error::with_context("Failed to render template", &format!("{}: {}", template, e))
        })?;

        // Create parent directory if needed
        if let Some(parent) = output.parent() {
            std::fs::create_dir_all(parent).map_err(|e| {
                Error::with_context(
                    "Failed to create directory",
                    &format!("{}: {}", parent.display(), e),
                )
            })?;
        }

        std::fs::write(output, content).map_err(|e| {
            Error::with_context(
                "Failed to write file",
                &format!("{}: {}", output.display(), e),
            )
        })?;

        Ok(())
    }
}