crusty-hooks 0.1.0

git hooks integration for Rust projects
Documentation
use std::collections::HashMap;

const CONFIG_FILE_TEMPLATE: &str = "[hooks]
pre-commit = [
  [\"cargo test\"]
]

[logging]
verbose = true
";

const DEFAULT_CONFIG_FILE_NAME: &str = ".crusty-hooks.toml";
const CONFIG_FILE_NAMES: [&str; 2] = [DEFAULT_CONFIG_FILE_NAME, "crusty-hooks.toml"];
pub const NO_CONFIG_FILE_FOUND: &str = "No config file found";

pub(crate) const FATAL_ERROR_DURING_CONFIG_LOOKUP: &str =
    "Fatal error encountered while looking for existing config";

fn find_config_file<F>(root_directory_path: &str, file_exists: F) -> Result<String, String>
where
    F: Fn(&str) -> Result<bool, ()>,
{
    for &config_file_name in CONFIG_FILE_NAMES.iter() {
        let path = format!("{root_directory_path}/{config_file_name}");
        match file_exists(&path) {
            Err(_) => {
                return Err(String::from(FATAL_ERROR_DURING_CONFIG_LOOKUP));
            }
            Ok(found) => {
                if found {
                    return Ok(path);
                }
            }
        };
    }

    Ok(String::from(NO_CONFIG_FILE_FOUND))
}

pub(super) fn create_default_config_file<F, G>(
    write_file: F,
    file_exists: G,
    root_directory_path: &str,
) -> Result<(), String>
where
    F: Fn(&str, &str, bool) -> Result<(), String>,
    G: Fn(&str) -> Result<bool, ()>,
{
    create_config_file(
        &write_file,
        &file_exists,
        root_directory_path,
        DEFAULT_CONFIG_FILE_NAME,
    )
}

pub(super) fn create_config_file<F, G>(
    write_file: F,
    file_exists: G,
    root_directory_path: &str,
    desired_config_file_name: &str,
) -> Result<(), String>
where
    F: Fn(&str, &str, bool) -> Result<(), String>,
    G: Fn(&str) -> Result<bool, ()>,
{
    match find_config_file(root_directory_path, &file_exists) {
        Err(_) => {
            return Err(String::from(FATAL_ERROR_DURING_CONFIG_LOOKUP));
        }
        Ok(path) => {
            if path != NO_CONFIG_FILE_FOUND {
                return Ok(());
            }
        }
    };

    let config_file = if CONFIG_FILE_NAMES
        .iter()
        .any(|n| n == &desired_config_file_name)
    {
        desired_config_file_name
    } else {
        DEFAULT_CONFIG_FILE_NAME
    };

    if write_file(
        &format!("{}/{}", root_directory_path, config_file),
        CONFIG_FILE_TEMPLATE,
        false,
    )
    .is_err()
    {
        return Err(String::from("Failed to create config file"));
    };
    Ok(())
}

#[derive(Debug, serde::Serialize, serde::Deserialize)]
pub struct ConfigFile {
    pub hooks: HashMap<String, Vec<Vec<String>>>,
}

impl ConfigFile {
    pub fn try_from_str(x: &str) -> Result<Self, toml::de::Error> {
        toml::from_str::<Self>(x)
    }
}

pub(super) fn get_config_file_contents<F, G>(
    read_file: F,
    file_exists: G,
    root_directory_path: &str,
) -> Result<String, String>
where
    F: Fn(&str) -> Result<String, ()>,
    G: Fn(&str) -> Result<bool, ()>,
{
    let path = match find_config_file(root_directory_path, &file_exists) {
        Ok(path) => {
            if path == NO_CONFIG_FILE_FOUND {
                return Err(String::from(NO_CONFIG_FILE_FOUND));
            } else {
                path
            }
        }
        Err(_) => return Err(String::from(NO_CONFIG_FILE_FOUND)),
    };

    match read_file(&path) {
        Ok(contents) => Ok(contents),
        Err(_) => Err(String::from("Failure reading file")),
    }
}