auric 0.1.0

CLI for the `auric` MVC SPA framework
use clap::Parser;
use handlebars::Handlebars;
use std::collections::HashMap;
use std::fs::{self, File};
use std::io::Write;
use std::path::Path;
use syn::{Item, ItemMod};

const ROUTE_RS_HBS: &str = include_str!("../../../assets/src/route.rs.hbs");

#[derive(Debug, Parser)]
pub struct Opts {
    /// Route Name
    #[arg(value_name = "name")]
    pub name: String,
}

pub async fn run(name: String) -> anyhow::Result<()> {
    // Generate route module
    let mut handlebars = Handlebars::default();
    const NAME: &str = "default";
    handlebars.register_template_string(NAME, ROUTE_RS_HBS)?;
    let data = HashMap::from([("name", struct_name(&name))]);
    let contents = handlebars.render(NAME, &data)?;

    // Deploy the route module file
    let root_dir = Path::new(".");
    let routes_dir = root_dir.join("src").join("routes");
    fs::create_dir_all(&routes_dir)?;
    let modulename = change_case::snake_case(&name);
    let filename = format!("{modulename}.rs");
    let path = routes_dir.join(filename);
    let mut file = File::create(path)?;
    file.write_all(contents.as_bytes())?;

    // Reference it from the parent module
    let path = routes_dir.join("mod.rs");
    let code = fs::read_to_string(&path)?;
    let new_code = {
        let syntax = syn::parse_file(&code)?;
        let mods = find_mods(&syntax);
        if !mods.iter().any(|item_mod| item_mod.ident == modulename) {
            let mut new_line = format!("pub mod {modulename};\n");
            if code.starts_with("use ") {
                new_line = format!("{new_line}\n");
            }
            format!("{new_line}{code}")
        } else {
            code.clone()
        }
    };
    if code != new_code {
        fs::write(&path, new_code)?;
    }

    // Generate template
    let templates_dir = root_dir.join("src").join("templates");
    let mut template_dir = templates_dir.clone();
    let segments: Vec<_> = name.split('.').collect();
    let short = segments.last().cloned().unwrap_or_default();
    for segment in segments.into_iter().rev().skip(1).rev() {
        template_dir = template_dir.join(segment);
    }
    fs::create_dir_all(&template_dir)?;
    let mut file = File::create(template_dir.join(format!("{short}.hbs")))?;
    file.write_all("{{outlet}}\n".as_bytes())?;

    Ok(())
}

fn struct_name(name: &str) -> String {
    format!("{}Route", change_case::pascal_case(&name.replace(".", "_")))
}

fn find_mods(syntax: &syn::File) -> Vec<ItemMod> {
    syntax
        .items
        .iter()
        .filter_map(|item| match item {
            Item::Mod(impl_) => Some(impl_.clone()),
            _ => None,
        })
        .collect()
}

#[cfg(test)]
mod tests {
    use crate::cmd::generate::route::struct_name;

    #[test]
    fn can_generate_struct_name() {
        assert_eq!(struct_name("index"), "IndexRoute");
        assert_eq!(struct_name("foo"), "FooRoute");
        assert_eq!(struct_name("foo.bar"), "FooBarRoute");
        assert_eq!(struct_name("foo.bar.baz.index"), "FooBarBazIndexRoute");
    }
}