ggen-core 26.6.11

Core graph-aware code generation engine
Documentation
//! Parallel template generation for bulk operations
//!
//! **QUICK WIN 2: PARALLEL TEMPLATE GENERATION**
//! Provides 2-4x speedup for bulk template processing using Rayon.

use crate::utils::error::{Error, Result};
use rayon::prelude::*;
use std::path::{Path, PathBuf};
use tera::Context;
use walkdir::WalkDir;

use crate::pipeline::Pipeline;
use crate::streaming_generator::GenerationResult;
use crate::template_types::Template;

/// Parallel template generator using Rayon for bulk operations
///
/// This generator processes multiple templates concurrently, providing
/// significant speedup for projects with many templates.
///
/// # Performance
///
/// - **2-4x faster** than sequential processing for bulk operations
/// - Scales with CPU core count
/// - Best for projects with 10+ templates
///
/// # Examples
///
/// ```rust,no_run
/// use crate::parallel_generator::ParallelGenerator;
/// use tera::Context;
/// use std::path::PathBuf;
///
/// # fn main() -> crate::utils::error::Result<()> {
/// let template_dir = PathBuf::from("templates");
/// let output_dir = PathBuf::from("output");
/// let vars = Context::new();
///
/// let result = ParallelGenerator::generate_all(
///     &template_dir,
///     &output_dir,
///     &vars
/// )?;
///
/// println!("Generated {} files in {:.2}s",
///     result.success_count,
///     result.duration.as_secs_f64()
/// );
/// # Ok(())
/// # }
/// ```
pub struct ParallelGenerator;

impl ParallelGenerator {
    /// Generate all templates in parallel
    ///
    /// **QUICK WIN 2:** Uses Rayon to process templates concurrently,
    /// providing 2-4x speedup for bulk operations.
    pub fn generate_all(
        template_dir: &Path, output_dir: &Path, vars: &Context,
    ) -> Result<GenerationResult> {
        let start = std::time::Instant::now();

        // Collect all template paths
        let template_paths: Vec<PathBuf> = WalkDir::new(template_dir)
            .into_iter()
            .filter_map(|entry| entry.ok())
            .filter(|entry| entry.file_type().is_file())
            .filter(|entry| entry.path().extension() == Some(std::ffi::OsStr::new("tmpl")))
            .map(|entry| entry.path().to_path_buf())
            .collect();

        // QUICK WIN 2: Process templates in parallel using Rayon
        let results: Vec<_> = template_paths
            .par_iter()
            .map(|template_path| Self::process_template_isolated(template_path, output_dir, vars))
            .collect();

        // Aggregate results
        let mut generation_result = GenerationResult::default();
        for result in results {
            match result {
                Ok(output_path) => {
                    generation_result.success_count += 1;
                    generation_result.generated_files.push(output_path);
                }
                Err(e) => {
                    generation_result.error_count += 1;
                    generation_result.errors.push(e.to_string());
                }
            }
        }

        generation_result.duration = start.elapsed();
        Ok(generation_result)
    }

    /// Process a single template in isolation (thread-safe)
    fn process_template_isolated(
        template_path: &Path, output_dir: &Path, vars: &Context,
    ) -> Result<PathBuf> {
        // Create isolated pipeline for this template (thread-safe)
        let mut pipeline = Pipeline::new()?;

        // Parse template
        let content = std::fs::read_to_string(template_path).map_err(|e| {
            Error::with_source(
                &format!("Failed to read template {}", template_path.display()),
                Box::new(e),
            )
        })?;
        let mut template = Template::parse(&content)?;

        // Render frontmatter
        template.render_frontmatter(&mut pipeline.tera, vars)?;

        // QUICK WIN 1: Early check for RDF usage
        // Process RDF graph only if template actually uses RDF/SPARQL
        if !template.front.rdf_inline.is_empty()
            || !template.front.rdf.is_empty()
            || !template.front.sparql.is_empty()
        {
            template.process_graph(&mut pipeline.graph, &mut pipeline.tera, vars, template_path)?;
        }

        // Render template
        let rendered = template.render(&mut pipeline.tera, vars, std::path::Path::new(""))?;

        // Determine output path
        let output_path = if let Some(to_path) = &template.front.to {
            let rendered_to = pipeline.tera.render_str(to_path, vars)?;
            output_dir.join(rendered_to)
        } else {
            let filename = template_path
                .file_stem()
                .ok_or_else(|| Error::new("Template has no filename"))?
                .to_string_lossy();
            output_dir.join(format!("{}.out", filename))
        };

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

        // Write output file
        std::fs::write(&output_path, rendered).map_err(|e| {
            Error::new(&format!(
                "Failed to write output file {}: {}",
                output_path.display(),
                e
            ))
        })?;

        Ok(output_path)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use tempfile::TempDir;

    fn create_test_template(dir: &Path, name: &str, content: &str) -> Result<PathBuf> {
        let path = dir.join(format!("{}.tmpl", name));
        std::fs::write(&path, content)
            .map_err(|e| Error::with_source("Failed to write test template", Box::new(e)))?;
        Ok(path)
    }

    #[test]
    fn test_parallel_generation() -> Result<()> {
        let temp_dir = TempDir::new()?;
        let output_dir = TempDir::new()?;

        // Create multiple templates
        for i in 0..10 {
            create_test_template(
                temp_dir.path(),
                &format!("test_{}", i),
                &format!(
                    r#"---
to: "output_{i}.txt"
---
File {i} content"#
                ),
            )?;
        }

        let vars = Context::new();
        let result = ParallelGenerator::generate_all(temp_dir.path(), output_dir.path(), &vars)?;

        assert_eq!(result.success_count, 10);
        assert_eq!(result.error_count, 0);
        assert!(result.throughput() > 0.0);

        Ok(())
    }

    #[test]
    fn test_parallel_with_rdf_skip() -> Result<()> {
        let temp_dir = TempDir::new()?;
        let output_dir = TempDir::new()?;

        // Create templates WITHOUT RDF (should use Quick Win 1)
        for i in 0..5 {
            create_test_template(
                temp_dir.path(),
                &format!("no_rdf_{}", i),
                r#"---
to: "simple.txt"
---
Simple template without RDF"#,
            )?;
        }

        let vars = Context::new();
        let result = ParallelGenerator::generate_all(temp_dir.path(), output_dir.path(), &vars)?;

        assert_eq!(result.success_count, 5);
        assert_eq!(result.error_count, 0);

        Ok(())
    }
}