1use clap::{Arg, ArgAction, Command};
2use colored::Colorize;
3use std::{
4 env,
5 error::Error,
6 fs::File,
7 io::{self, BufRead, BufReader},
8};
9
10#[derive(Debug)]
11pub struct Config {
12 acronym: String,
13 file: String,
14 acro_column: usize,
15 definition_column: usize,
16 color: bool,
17 header: bool,
18 delimiter: char,
19}
20
21pub fn get_args() -> Result<Config, Box<dyn Error>> {
22 let matches = Command::new("Acro")
23 .author("Nil Ventosa")
24 .version("0.2.0")
25 .about("Helps query csv files of acronyms")
26 .arg(
27 Arg::new("acronym")
28 .help("the acronym to query")
29 .required(true),
30 )
31 .arg(
32 Arg::new("file")
33 .short('f')
34 .long("file")
35 .help("the csv file with the acronyms and definitions, can be set with env variable ACRO_FILE"),
36 )
37 .arg(
38 Arg::new("acro_column")
39 .short('a')
40 .long("acro")
41 .help("the column with the acronyms, can be set with env variable ACRO_COLUMN, defaults to 1")
42 .value_parser(clap::value_parser!(usize)),
43 )
44 .arg(
45 Arg::new("definition_column")
46 .short('d')
47 .long("definition")
48 .help("the column with the definitions, can be set with env variable DEFINITION_COLUMN, defaults to 2")
49 .value_parser(clap::value_parser!(usize)),
50 )
51 .arg(
52 Arg::new("delimiter")
53 .short('D')
54 .long("delimiter")
55 .help("delimiter character between columns. Defaults to ','")
56 .value_parser(clap::value_parser!(char)),
57 )
58 .arg(
59 Arg::new("header")
60 .short('H')
61 .long("header")
62 .action(ArgAction::SetTrue)
63 .help("flag if there is a header line"),
64 )
65 .arg(
66 Arg::new("color")
67 .short('c')
68 .long("color")
69 .action(ArgAction::SetTrue)
70 .help("disables color output"),
71 )
72 .get_matches();
73
74 let mut file: String = "".to_string();
75 if let Some(f) = matches.get_one::<String>("file") {
76 file = f.to_string();
77 } else if let Ok(f) = env::var("ACRO_FILE") {
78 file = f;
79 } else {
80 eprintln!("File should be specified in argument -f or in env variable ACRO_FILE");
81 }
82
83 let mut acro_column: usize = 0;
84 if let Some(a) = matches.get_one::<usize>("acro_column") {
85 acro_column = a.to_owned() - 1;
86 } else if let Ok(a) = env::var("ACRO_COLUMN") {
87 if let Ok(a) = a.parse::<usize>() {
88 acro_column = a - 1;
89 }
90 }
91
92 let mut definition_column: usize = 1;
93 if let Some(d) = matches.get_one::<usize>("definition_column") {
94 definition_column = d.to_owned() - 1;
95 } else if let Ok(d) = env::var("DEFINITION_COLUMN") {
96 if let Ok(d) = d.parse::<usize>() {
97 definition_column = d - 1;
98 }
99 }
100
101 let mut delimiter: char = ',';
102 if let Some(d) = matches.get_one::<char>("delimiter") {
103 delimiter = d.to_owned();
104 } else if let Ok(d) = env::var("ACRO_DELIMITER") {
105 if let Ok(d) = d.parse::<char>() {
106 delimiter = d;
107 }
108 }
109
110 let header: bool = if matches.get_flag("header") {
111 true
112 } else {
113 env::var("ACRO_HEADER").is_ok()
114 };
115
116 let color: bool = if matches.get_flag("color") {
117 false
118 } else {
119 !env::var("ACRO_COLOR").is_ok()
120 };
121
122 Ok(Config {
123 acronym: matches.get_one::<String>("acronym").unwrap().to_string(),
124 file,
125 acro_column,
126 definition_column,
127 color,
128 header,
129 delimiter,
130 })
131}
132
133pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
134 let all_entries = get_entries_from_file(&config);
135 let matching_entries = find_matching_entries(&all_entries, &config.acronym);
136
137 for entry in matching_entries {
138 entry.print(&config.color);
139 }
140
141 Ok(())
142}
143
144#[derive(Debug)]
145struct Entry {
146 acronym: String,
147 definition: String,
148}
149
150impl Entry {
151 fn print(&self, color: &bool) {
152 if *color {
153 println!(" {}: {}", self.acronym.bold().blue(), self.definition);
154 } else {
155 println!(" {}: {}", self.acronym, self.definition);
156 }
157 }
158}
159
160fn find_matching_entries(entries: &Vec<Entry>, acro: &str) -> Vec<Entry> {
161 let matching = find_exact_match(entries, acro);
162
163 if matching.is_empty() {
164 return find_partial_match(entries, acro);
165 }
166 matching
167}
168
169fn find_exact_match(entries: &Vec<Entry>, acro: &str) -> Vec<Entry> {
170 let mut matching = Vec::new();
171
172 for entry in entries {
173 if entry.acronym.to_uppercase() == acro.to_uppercase() {
174 matching.push(Entry {
175 acronym: entry.acronym.clone(),
176 definition: entry.definition.clone(),
177 });
178 }
179 }
180
181 matching
182}
183
184fn find_partial_match(entries: &Vec<Entry>, acro: &str) -> Vec<Entry> {
185 let mut matching = Vec::new();
186
187 for entry in entries {
188 if entry.acronym.to_uppercase().contains(&acro.to_uppercase()) {
189 matching.push(Entry {
190 acronym: entry.acronym.clone(),
191 definition: entry.definition.clone(),
192 });
193 }
194 }
195 matching
196}
197
198fn get_entries_from_file(config: &Config) -> Vec<Entry> {
199 let mut entries = Vec::new();
200
201 match open(&config.file) {
202 Err(err) => eprintln!("Failed to open {}: {}", config.file, err),
203 Ok(file) => {
204 let mut reader = csv::ReaderBuilder::new()
205 .has_headers(config.header)
206 .delimiter(config.delimiter as u8)
207 .from_reader(file);
208
209 for record in reader.records().flatten() {
210 if let (Some(acronym), Some(definition)) = (
211 record.get(config.acro_column),
212 record.get(config.definition_column),
213 ) {
214 entries.push(Entry {
215 acronym: acronym.to_string(),
216 definition: definition.to_string(),
217 });
218 }
219 }
220 }
221 }
222 entries
223}
224
225fn open(filename: &str) -> Result<Box<dyn BufRead>, Box<dyn Error>> {
226 match filename {
227 "-" => Ok(Box::new(BufReader::new(io::stdin()))),
228 _ => Ok(Box::new(BufReader::new(File::open(filename)?))),
229 }
230}
231
232#[cfg(test)]
233mod tests {
234 use super::*;
235
236 fn get_test_entry_vec() -> Vec<Entry> {
237 Vec::from([
238 Entry {
239 acronym: String::from("NATO"),
240 definition: String::from("N A T O"),
241 },
242 Entry {
243 acronym: String::from("USA"),
244 definition: String::from("U S A"),
245 },
246 ])
247 }
248
249 #[test]
250 fn test_find_exact_match_found() {
251 let result = find_exact_match(&get_test_entry_vec(), "NATO");
252 assert_eq!(result.len(), 1);
253 }
254
255 #[test]
256 fn test_find_exact_match_nothing() {
257 let result = find_exact_match(&get_test_entry_vec(), "NAT");
258 assert_eq!(result.len(), 0);
259 }
260
261 #[test]
262 fn test_find_partial_match_nothing() {
263 let result = find_partial_match(&get_test_entry_vec(), "NATA");
264 assert_eq!(result.len(), 0);
265 }
266
267 #[test]
268 fn test_find_partial_match_two_results() {
269 let result = find_partial_match(&get_test_entry_vec(), "A");
270 assert_eq!(result.len(), 2);
271 }
272
273 #[test]
274 fn test_find_matching_entries_two_results() {
275 let result = find_matching_entries(&get_test_entry_vec(), "A");
276 assert_eq!(result.len(), 2);
277 }
278
279 #[test]
280 fn test_find_matching_entries_one_result() {
281 let result = find_matching_entries(&get_test_entry_vec(), "NATO");
282 assert_eq!(result.len(), 1);
283 }
284
285 #[test]
286 fn test_find_matching_entries_no_results() {
287 let result = find_matching_entries(&get_test_entry_vec(), "NATA");
288 assert_eq!(result.len(), 0);
289 }
290}