1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
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();
}