auric 0.1.6

CLI for the Ember-inspired Auric 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, PathBuf};
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", name.clone()),
        ("path", path(&name)),
        ("structName", struct_name(&name)),
    ]);
    let contents = handlebars.render(NAME, &data)?;

    // Deploy the route module file
    let routes_dir = PathBuf::new().join("src").join("routes");
    let (modulename, filename, dir) = {
        let s = change_case::snake_case(&name).replace("_", "/");
        let filename = format!("{s}.rs");
        let modulename = match s.rfind("/") {
            None => s,
            Some(n) => s[n + 1..].to_owned(),
        };
        let dir = routes_dir
            .join(&filename)
            .parent()
            .expect("Expected to get parent dir")
            .to_owned();
        (modulename, filename, dir)
    };
    fs::create_dir_all(&dir)?;
    let relative_path = routes_dir.join(&filename);
    let root_dir = Path::new(".");
    let path = root_dir.join(&relative_path);
    let mut file = File::create(path)?;
    file.write_all(contents.as_bytes())?;
    eprintln!("created {}", relative_path.display());

    // Ensure module linkage
    let mut current_dir = dir.clone();
    let mut current_modulename = modulename.clone();
    for _n in 1..=name.split(".").count() {
        // Ensure mod.rs references current module
        let relative_path = current_dir.join("mod.rs");
        let path = root_dir.join(&relative_path);
        let code = match path.exists() {
            false => String::new(),
            true => 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 == current_modulename) {
                let mut new_line = format!("pub mod {current_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)?;
            eprintln!("updated {}", relative_path.display());
        }

        // Advance loop to parent dir
        current_modulename = match current_dir.file_name() {
            None => break,
            Some(filename) => filename.to_string_lossy().into_owned(),
        };
        current_dir = match current_dir.parent() {
            None => break,
            Some(parent) => parent.into(),
        };
    }

    // Generate template
    super::template::run(name).await?;

    Ok(())
}

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

fn path(name: &str) -> String {
    let s = name.replace(".", "/");
    let s = change_case::snake_case(&s).replace("_", "/");
    format!("/{s}")
}

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 super::*;

    #[test]
    fn can_generate_struct_name() {
        assert_eq!(struct_name("application"), "ApplicationRoute");
        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");
    }

    #[test]
    fn can_derive_path() {
        assert_eq!(path("application"), "/application");
        assert_eq!(path("index"), "/index");
        assert_eq!(path("foo"), "/foo");
        assert_eq!(path("foo.bar"), "/foo/bar");
        assert_eq!(path("foo.bar.baz.index"), "/foo/bar/baz/index");
    }
}