blindfold 1.0.7

⚙️ gitignore file generator written in rust
Documentation
use std::path::{Path, PathBuf};
use strsim::normalized_levenshtein;
use std::fs::File;
use std::io::*;
use std::collections::HashMap;
use colored::*;
use reqwest;
use serde::{Serialize, Deserialize};
use prettytable::{Table, Row, Cell};

#[derive(Serialize, Deserialize, Debug)]
pub struct FileRes {
    name: String,
    download_url: Option<String>,
}

// performs a http GET request using the reqwest crate
pub fn http_get(url: &str) -> String {
    let response = reqwest::get(url)
        .expect("Error: Url Not Found")
        .text()
        .expect("Error: Text unextractable from url");

    return response;
}

// builds a mapping of template names to urls to download them
pub fn build_file_map(res: &str) -> HashMap<String, String> {
    // parse json response to extract name and download link into FileRes struct
    let all_files: Vec<FileRes> = serde_json::from_str(res).unwrap();

    // filter out non-gitignore files
    let gitignore_files: Vec<&FileRes> = all_files
        .iter()
        .filter(|file| file.name.contains("gitignore"))
        .collect();

    // destructure vec of structs to vec of tuples in form (name, url)
    let destructured: Vec<(String, String)> = gitignore_files
        .iter()
        .map(|file| destructure_to_tup(file))
        .collect();

    // collect vector of tuples into a hashmap
    let file_map: HashMap<String, String> = destructured
        .into_iter()
        .collect();

    return file_map;
}

// destructure FileRes struct to a tuple of its fields
pub fn destructure_to_tup(file_struct: &FileRes) -> (String, String) {
    // format name to be language name lowercased
    let name:String = file_struct.name
        .clone()
        .replace(".gitignore", "")
        .to_lowercase();

    let mut url:String = String::from("");

    if let Some(download_url) = &file_struct.download_url  {
        url.push_str(download_url);
    }

    return (name, url);
}

// make http get request for the specified template and return the raw text of the gitignore as a string
pub fn get_raw_ignore_file(file_map: &HashMap<String, String>, lang: &str) -> String {
    let mut response: String = String::from("");
    let file_url: Option<&String> = file_map.get(lang);

    if let Some(file) = file_url {
        response.push_str(&http_get(&file));
    }

    return response;
}

// Add title for each raw gitignore
fn format_gitignore(body : &String, language: &str) -> String {
    let ignore_template: String = format!("# {} gitignore generated by Blindfold\n\n{}\n\n",
                                          language.to_uppercase(),
                                          body);

    println!("Generated .gitignore for {} 🔧", language.magenta().bold());
    return ignore_template;
}

// returns formatted gitignore string for each language provided
pub fn generate_gitignore_file(languages: Vec<&str>, file_map: &HashMap<String, String>) -> String {
    // string to store all the gitignores
    let mut gitignore: String = String::from("");

    // generate gitignore for each language and append to output string
    for language in languages.iter() {
        // make sure a language is added
        if language == &"" {
            continue;
        }
        if file_map.contains_key(&language.to_string()) {
            let ignore_body: String = get_raw_ignore_file(&file_map, language);
            gitignore.push_str(&format_gitignore(&ignore_body, language));
        }
        else {
            let stdio = stdin();
            let input = stdio.lock();
            let output = stdout();

            let most_similar: Option<String> = suggest_most_similar(input,
                                                                    output,
                                                                    language.clone(),
                                                                    file_map.clone());

            if let Some(language) = most_similar {
                let ignore_body: String = get_raw_ignore_file(&file_map, &language);
                gitignore.push_str(&format_gitignore(&ignore_body, &language));
            }
        }
    }

    return gitignore;
}

// given a mis-typed language this function returns the most similar language available
pub fn suggest_most_similar<R, W>(mut reader: R,
                                  mut writer: W,
                                  typo: &str,
                                  file_map: HashMap<String, String>) -> Option<String>
where
    R: BufRead,
    W: Write,
{
    // find language most similar to what was requested
    let mut max: f64 = 0.0;
    let mut most_similar: String = String::new();

    for candidate in file_map.keys() {
        let similarity: f64 = normalized_levenshtein(typo, candidate);
        if similarity > max {
            most_similar = candidate.to_string();
            max = similarity;
        }
    }

    // take input to accept/deny suggestion
    write!(&mut writer, "Couldn't generate template for {}, did you mean {}? [y/N]: ",
           typo.yellow().bold(),
           most_similar.bright_green().bold()).expect("Unable to write");
    // flush input buffer so that it prints immediately
    stdout().flush().ok();

    let mut choice: String = String::new();
    reader.read_line(&mut choice)
        .ok()
        .expect("Couldn't read line");

    if choice.to_lowercase().trim() == String::from("y") {
        return Some(most_similar);
    }

    return None;
}

// writes gitignore string to file
pub fn write_to_file(dest: &str, gitignore: String) -> std::io::Result<()> {
    let filepath: PathBuf = Path::new(dest).join(".gitignore");
    println!("Writing file to {}... ✏️", format!("{}.gitignore", dest)
            .bright_blue()
            .bold());
    let mut file = File::create(filepath)?;
    file.write_all(gitignore.as_bytes())?;
    println!("{}", "Done!".green().bold());

    Ok(())
}

// add gitignore to existing gitignore file
pub fn append_to_file(destination: &str, gitignore: String) -> std::io::Result<()>  {
    let filepath: PathBuf = Path::new(destination).join(".gitignore");

    // open existing gitignore and concatenate with new template
    let mut file = File::open(filepath)?;
    let mut existing: String= String::new();
    file.read_to_string(&mut existing)?;
    let combined: String = format!("{}{}", existing, gitignore);

    if !combined.is_empty() {
        println!("Loaded existing gitignore file from {} 💾", format!("{}.gitignore", destination)
            .bright_blue()
            .bold());

        // write it to file
        write_to_file(destination, combined).expect("Could'nt write to file ⚠️ ");
    }

    return Ok(());
}

// print a table containing all available templates for generation
pub fn list_templates(file_map: HashMap<String, String>) {
    let mut table = Table::new();

    let mut keys: Vec<String> = file_map
        .keys()
        .map(|key| key.clone())
        .collect();

    keys.sort();

    let mut chunks = keys.chunks(4);

    // while another row can be constructed, construct one and add to table
    while let Some(chunk) = chunks.next() {
        // map chunk items to cell
        let cells = chunk
            .iter()
            .map(|item| Cell::new(item))
            .collect();

        let row = Row::new(cells);
        table.add_row(row);
    }

    // print table
    table.printstd();
}