progenitor-cli 0.3.0

A CLI tool for generating custom code templates
use crate::generator::schema::{GeneratorSchema, StructureItem};
use clap::Args;
use colored::Colorize;
use std::collections::HashMap;
use std::fs;
use std::io::Read;
use std::path::{Path, PathBuf};

#[derive(Args, Debug)]
pub struct Add {
    #[arg(short, long)]
    pub path: String,
}

impl Add {
    pub fn execute(&self) {
        let template_path = PathBuf::from(&self.path);

        if !template_path.exists() {
            eprintln!("{}: The specified path does not exist.", "Error".red());
            return;
        }

        if !template_path.is_dir() {
            eprintln!("{}: The specified path is not a directory.", "Error".red());
            return;
        }

        let generator_yml_path = template_path.join("generator.yml");
        if !generator_yml_path.exists() {
            eprintln!(
                "{}: `generator.yml` not found in the template directory.",
                "Error".red()
            );
            return;
        }

        let (template_name, generator_schema) = match self.parse_generator_yml(&generator_yml_path)
        {
            Ok(data) => data,
            Err(e) => {
                eprintln!("{}: Failed to parse `generator.yml`: {}", "Error".red(), e);
                return;
            }
        };

        if let Err(e) = self.validate_gen_files(&template_path, &generator_schema) {
            eprintln!("{}: Template validation failed: {}", "Error".red(), e);
            return;
        }

        match self.copy_template(&template_path, &template_name, &generator_schema) {
            Ok(_) => println!(
                "{}: Template '{}' added successfully!",
                "Success".green(),
                template_name
            ),
            Err(e) => eprintln!("{}: Failed to add template: {}", "Error".red(), e),
        }
    }

    fn parse_generator_yml(
        &self,
        path: &PathBuf,
    ) -> Result<(String, GeneratorSchema), Box<dyn std::error::Error>> {
        let mut file = fs::File::open(path)?;
        let mut contents = String::new();
        file.read_to_string(&mut contents)?;
        let schema_map: HashMap<String, GeneratorSchema> = serde_yaml::from_str(&contents)?;

        let (template_name, schema) = schema_map
            .into_iter()
            .next()
            .ok_or("Empty generator.yml or missing top-level template name")?;
        Ok((template_name, schema))
    }

    fn validate_gen_files(
        &self,
        template_root: &Path,
        schema: &GeneratorSchema,
    ) -> Result<(), String> {
        self.check_structure_items(template_root, &schema.structure, template_root)
    }

    fn check_structure_items(
        &self,
        current_path_in_template: &Path,
        items: &[StructureItem],
        template_root_source: &Path,
    ) -> Result<(), String> {
        for item in items {
            match item {
                StructureItem::Directory { name, content } => {
                    let dir_path = current_path_in_template.join(name);
                    self.check_structure_items(&dir_path, content, template_root_source)?;
                }
                StructureItem::File { name: _, source } => {
                    let gen_file_path = template_root_source.join(source);
                    if !gen_file_path.exists() {
                        return Err(format!(
                            "Referenced .gen file not found: {}",
                            gen_file_path.display()
                        ));
                    }
                    if !gen_file_path.is_file() {
                        return Err(format!(
                            "Referenced .gen path is not a file: {}",
                            gen_file_path.display()
                        ));
                    }
                }
            }
        }
        Ok(())
    }

    fn copy_template(
        &self,
        source_path: &Path,
        template_name: &str,
        schema: &GeneratorSchema,
    ) -> Result<(), Box<dyn std::error::Error>> {
        let home_dir = dirs::home_dir().ok_or("Could not find home directory")?;
        let progenitor_dir = home_dir.join(".progenitor").join("templates");
        let destination_path = progenitor_dir.join(template_name);

        if destination_path.exists() {
            return Err(format!(
                "Template '{}' already exists at {}. Please remove it first.",
                template_name,
                destination_path.display()
            )
            .into());
        }

        fs::create_dir_all(&destination_path)?;

        fs::copy(
            source_path.join("generator.yml"),
            destination_path.join("generator.yml"),
        )?;

        self.copy_structure_items(
            source_path,
            &destination_path,
            &schema.structure,
            source_path,
            &destination_path,
        )?;

        Ok(())
    }

    fn copy_structure_items(
        &self,
        current_source_dir: &Path,
        current_dest_dir: &Path,
        items: &[StructureItem],
        template_root_source: &Path,
        template_root_destination: &Path,
    ) -> Result<(), Box<dyn std::error::Error>> {
        for item in items {
            match item {
                StructureItem::Directory { name, content } => {
                    let new_source_dir = current_source_dir.join(name);
                    let new_dest_dir = current_dest_dir.join(name);
                    fs::create_dir_all(&new_dest_dir)?;
                    self.copy_structure_items(
                        &new_source_dir,
                        &new_dest_dir,
                        content,
                        template_root_source,
                        template_root_destination,
                    )?;
                }
                StructureItem::File { name: _, source } => {
                    let relative_path_from_template_root = PathBuf::from(source);
                    let full_src_path =
                        template_root_source.join(&relative_path_from_template_root);
                    let full_dest_path =
                        template_root_destination.join(&relative_path_from_template_root);

                    if let Some(parent) = full_dest_path.parent() {
                        fs::create_dir_all(parent)?;
                    }
                    fs::copy(&full_src_path, &full_dest_path)?;
                }
            }
        }
        Ok(())
    }
}