gitwrap 0.11.0

GitWrap is a simple wrapper around `git` command
Documentation
mod options;
mod template;

use crate::options::{normalize, CmdOptionKind};
use crate::template::{
    command_templates, TEMPLATE_GIT_COMMAND_FILE, TEMPLATE_GIT_COMMAND_MACRO, TEMPLATE_MOD_RS,
    TEMPLATE_OPTION_DOC_COMMENTS, TEMPLATE_OPTION_EQUAL_NO_OPTIONAL,
    TEMPLATE_OPTION_EQUAL_OPTIONAL, TEMPLATE_OPTION_NAME_CONSTANT, TEMPLATE_OPTION_SIMPLE,
    TEMPLATE_OPTION_VALUE_PARAMETER, TEMPLATE_OPTION_WITH_OPTIONAL_PARAMETER,
    TEMPLATE_OPTION_WITH_PARAMETER,
};
use serde_json::{from_str, Value};
use std::fs::OpenOptions;
use std::io::Write;
use std::string::String;
use std::time::{SystemTime, UNIX_EPOCH};
use std::{env, fs};
use upon::Engine;

const PHG_VERSION: &str = env!("CARGO_PKG_VERSION");
const PKG_NAME: &str = env!("CARGO_PKG_NAME");
const DESCRIPTION_FILENAME: &str = "description.json";
const MOD_RS_FILENAME: &str = "mod.rs";
const GIT_COMMAND_FILENAME: &str = "git_command.rs";

pub fn main() {
    println!("{PKG_NAME} v{PHG_VERSION}");
    let args: Vec<String> = env::args().collect();
    let desc_file = if args.len() <= 1 {
        DESCRIPTION_FILENAME
    } else {
        &args[1]
    };
    generate(desc_file);
}

fn generate(desc_file: &str) {
    println!("reading git commands description file: {desc_file}");
    let output_dir = create_output_dir();
    let git_command_file = format!("{output_dir}/{GIT_COMMAND_FILENAME}");
    let engine = command_templates();
    git_command_file_create(&engine, &git_command_file);
    let json = read_descriptions(desc_file);
    for section in json.as_array().unwrap() {
        section_generator(output_dir.as_str(), &engine, section, git_command_file.as_str())
    }
}

fn section_generator(output_dir: &str, engine: &Engine, section: &Value, git_command_file: &str) {
    let enabled = section.get("enabled").unwrap().as_bool().unwrap();
    if enabled {
        let command_name = section.get("section").unwrap().as_str().unwrap();
        let is_command = section.get("is_command").unwrap().as_bool().unwrap();
        let options = section.get("options").unwrap().as_array().unwrap();
        let description = section.get("description").unwrap().as_str().unwrap();
        let doc_url = section.get("doc-url").unwrap().as_str().unwrap();
        command_generator(
            output_dir,
            engine,
            command_name,
            options,
            description,
            doc_url,
            is_command
        );
        if is_command {
            git_command_file_append_command(engine, command_name, git_command_file);
        }
    }
}

fn command_generator(
    output_dir: &str,
    engine: &Engine,
    command_name: &str,
    options: &Vec<Value>,
    description: &str,
    doc_url: &str,
    is_command: bool
) {
    let normalized_command_name = normalize(command_name);
    let command_path = format!("{output_dir}/{normalized_command_name}");
    fs::create_dir_all(command_path.as_str()).expect("could not create dir");
    command_mod_file_generator(
        engine,
        command_name,
        format!("{command_path}/{MOD_RS_FILENAME}").as_str(),
        description,
        doc_url,
        is_command
    );
    command_options_file_generator(
        engine,
        options,
        format!("{command_path}/options.rs").as_str(),
    );
    println!("command {command_name} generated");
}

fn command_mod_file_generator(
    engine: &Engine,
    cmd: &str,
    mod_file_path: &str,
    description: &str,
    doc_url: &str,
    is_command: bool
) {
    let tpl = engine.template(TEMPLATE_MOD_RS);
    let descriptions: Vec<&str> = description.lines().collect();
    let mod_rs_content = tpl
        .render(upon::value!{is_command: is_command, command_name: normalize(cmd), git_command: cmd, descriptions: descriptions, doc_url: doc_url})
        .to_string()
        .expect("could not render template mod_rs");

    fs::write(mod_file_path, mod_rs_content.as_str()).expect("Unable to write file module file");
}

fn git_command_file_create(engine: &Engine, file_path: &str) {
    let tpl = engine.template(TEMPLATE_GIT_COMMAND_FILE);
    let git_command_content = tpl
        .render(upon::value! {})
        .to_string()
        .expect("could not render template git_command_file");

    fs::write(file_path, &git_command_content).expect("Unable to write git command file");
}

fn git_command_file_append_command(engine: &Engine, cmd: &str, file_path: &str) {
    let tpl = engine.template(TEMPLATE_GIT_COMMAND_MACRO);
    let git_macro_content = tpl
        .render(upon::value! {command_name: normalize(cmd), git_command: cmd})
        .to_string()
        .expect("could not render template git_command_macro");

    // Open a file with append option
    let mut data_file = OpenOptions::new()
        .append(true)
        .open(file_path)
        .expect("cannot open git_command file");

    data_file
        .write_fmt(format_args!("\n{git_macro_content}\n"))
        .unwrap();
}

fn command_options_file_generator(engine: &Engine, options: &Vec<Value>, options_file_path: &str) {
    let mut options_content: Vec<String> = vec![
        String::from("// Warning!! Code generated automatically: this file must not be edited by hand"),
        String::from("use crate::option_arg;"),
        String::from("use crate::wrap_command::FnOptionArg;"),
        String::from("")
    ];

    let mut functions: Vec<String> = Vec::new();
    let mut constants: Vec<String> = Vec::new();

    for opt in options {
        let (function_desc, constant, function) = option_render_match(engine, opt);
        if let Some(f_def) = function {
            if let Some(f_desc) = function_desc {
                functions.push(format!("{}\n{}\n", f_desc, f_def));
            }
            if let Some(c_def) = constant {
                constants.push(c_def);
            }
        }
    }

    options_content.append(&mut constants);
    options_content.push(String::from(""));
    options_content.append(&mut functions);

    fs::write(options_file_path, options_content.join("\n"))
        .expect("Unable to write command options file");
}

fn option_render_match(engine: &Engine, opt: &Value) -> (Option<String>, Option<String>, Option<String>) {
    let argument = opt.get("argument").unwrap().as_str().unwrap();
    let arguments = opt.get("arguments").unwrap().as_str().unwrap();
    let method_name = opt.get("method_name");
    let descriptions: Vec<&str> = opt
        .get("description")
        .unwrap()
        .as_str()
        .unwrap()
        .lines()
        .collect();

    let function_desc = render(engine, TEMPLATE_OPTION_DOC_COMMENTS, upon::value! {descriptions: descriptions, arguments: arguments});

    let (constant_value, template, option_value) = match options::option_kind(argument) {
        CmdOptionKind::Simple(git_option, option_name) =>
            build_option(method_name, Some(TEMPLATE_OPTION_SIMPLE), option_name.as_str(), git_option.as_str(), None),

        CmdOptionKind::EqualNoOptional(git_option, option_name, argument) |
        CmdOptionKind::EqualOptionalWithName(git_option, option_name, argument) =>
            build_option(method_name, Some(TEMPLATE_OPTION_EQUAL_NO_OPTIONAL), option_name.as_str(), git_option.as_str(), Some(argument.as_str())),

        CmdOptionKind::EqualOptionalWithoutName(git_option, option_name) |
        CmdOptionKind::EqualWithoutName(git_option, option_name) =>
            build_option(method_name, Some(TEMPLATE_OPTION_EQUAL_OPTIONAL), option_name.as_str(), git_option.as_str(), Some("value")),

        CmdOptionKind::WithParameter(git_option, option_name, argument) =>
            build_option(method_name, Some(TEMPLATE_OPTION_WITH_PARAMETER), option_name.as_str(), git_option.as_str(), Some(argument.as_str())),

        CmdOptionKind::WithOptionalParameter(git_option, option_name, argument) =>
            build_option(method_name, Some(TEMPLATE_OPTION_WITH_OPTIONAL_PARAMETER), option_name.as_str(), git_option.as_str(), Some(argument.as_str())),

        CmdOptionKind::ValueParameter(value_parameter) =>
            build_value_parameter(method_name, TEMPLATE_OPTION_VALUE_PARAMETER, value_parameter.as_str()),

        CmdOptionKind::None =>
            (None, None, None)
    };

    if let Some(tpl) = template {
        option_render(engine, Some(function_desc), constant_value, tpl.as_str(), option_value.unwrap())
    } else {
        (None, None, None)
    }
}

fn build_option(method_name: Option<&Value>, template: Option<&str>, option_name: &str, git_option: &str, option_argument: Option<&str>) -> (Option<upon::Value>, Option<String>, Option<upon::Value>) {
    let name = option_method_name(method_name, option_name);
    let constant_name = name.to_uppercase();
    let constant_value = upon::value! {constant_name: String::from(constant_name.as_str()), git_option: String::from(git_option)};
    if let Some(tpl) = template {
        if let Some(argument) = option_argument {
            (Some(constant_value), Some(String::from(tpl)), Some(upon::value! {method_name: name, constant_name: String::from(constant_name.as_str()), option_argument: String::from(argument)}))
        } else {
            (Some(constant_value), Some(String::from(tpl)), Some(upon::value! {method_name: name, constant_name: String::from(constant_name.as_str())}))
        }
    } else {
        (Some(constant_value), None, None)
    }
}

fn build_value_parameter(method_name: Option<&Value>, template: &str, value_parameter: &str) -> (Option<upon::Value>, Option<String>, Option<upon::Value>) {
    let name = option_method_name(method_name, value_parameter);
    (None, Some(String::from(template)), Some(upon::value! {method_name: name, value_parameter: value_parameter}))
}

fn option_render(engine: &Engine, function_desc: Option<String>, constant_value: Option<upon::Value>, template: &str, option_value: upon::Value) -> (Option<String>, Option<String>, Option<String>) {
    if let Some(cv) = constant_value {
        (
            function_desc,
            Some(render(engine, TEMPLATE_OPTION_NAME_CONSTANT, cv)),
            Some(render(engine, template, option_value)),
        )
    } else {
        (
            function_desc,
            None,
            Some(render(engine, template, option_value)),
        )
    }
}

fn option_method_name(method_name: Option<&Value>, option_name: &str) -> String {
    normalize(match method_name {
        None => option_name,
        Some(v) => v.as_str().unwrap(),
    })
}

fn render(engine: &Engine, template_name: &str, template_values: upon::Value) -> String {
    engine
        .template(template_name)
        .render(template_values)
        .to_string()
        .expect("could not render template")
}

fn read_descriptions(file_name: &str) -> Value {
    let contents = fs::read_to_string(file_name).expect("Should have been able to read the file");
    from_str(contents.as_str()).expect("file should be proper JSON")
}

fn create_output_dir() -> String {
    let now = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .expect("Time went backwards");
    let output_dir = format!("output_{:?}", now.as_millis());
    fs::create_dir_all(&output_dir).expect("could not create output dir");
    output_dir
}