stanhope/
read_csv.rs

1//! *Interpret a well-formatted CSV file into a set of initialized Process structs, one for each row*
2//! 
3//! When identifying the need for new processes, a well-formatted list can be constructed, with a specified first set of columns, then all remaining columns assumed to be additional flags for templates:
4//! 
5//! | "Document Number" | "Title" | "Subject" | "Product" | "Author" | "Reviewer" | *TFlag* | *TFlag* | ... |
6//! | --- | --- | --- | --- | --- | --- | :---: | :---: | :---: |
7//! | ... | ... | ... | ... | ... | ... | x |   | ... |
8//! | ... | ... | ... | ... | ... | ... |   | x | ... |
9//! | ... | ... | ... | ... | ... | ... |   |   | ... |
10//! | ... | ... | ... | ... | ... | ... | x | x | ... |
11//! 
12//! The ***TFlag*** columns are user-defined. For example, if a ***TFlag*** column is "WarpSpeed" then this module assumes there is a Cascading Stylesheet file called "WarpSpeed.css" available to these processes, and any process with an "x" in the "WarpSpeed" column will have this Template (CSS file) applied.
13//! 
14//! All other columns are strictly enforced as shown above. Empty values in the data rows are OK, but in the first row of the CSV file, the first 6 columns *must* be those six, in that order.
15use crate::read_ebml::Process;
16
17use std::{
18	fs::{OpenOptions, File},
19	io::{self, BufRead, BufReader, Write},
20	path::Path,
21};
22
23/// Create CSS files for every TFlag found that doesn't already have a CSS file in the "./assets" directory
24fn new_css_file_if_none_exists(tflag:&str) {
25	let assets_folder = "./assets";
26	let new_css_file = assets_folder.to_owned()+"/"+tflag;
27	// println!("New CSS file: {}",new_css_file);
28	if Path::new(&assets_folder).exists() {
29		match Path::new(&new_css_file).exists() {
30	        true => (),
31	        false => { let mut f = OpenOptions::new()
32			            .read(true)
33			            .write(true)
34			            .create(true)
35			            .open(&new_css_file)
36			            .expect("Could not open the file!");
37			            let mut new_line =  "/* *******************************".to_string();
38			            new_line.push_str(&("\n   ".to_owned()+tflag));
39			            new_line.push_str("\n   ******************************* */");
40	    				f.write(new_line.as_bytes()).expect("Could not write template line into new file!");
41			        },
42	    };
43	}
44}
45
46/// Pull header and content data from a specifically-formatted CSV file, and return a list of requested processes (one per CSV data row).
47pub fn read_csv(file_name:&String,verbose:bool) -> Vec<Process> {
48
49	// TODO: better handling with blank call (no CSV file?) perhaps with an error thrown
50	let read_file = if file_name==&String::from("") { "./DocumentList.csv" } else { file_name };
51
52	// Get all lines from the input file into a vector of Strings
53	fn lines_from_file(filename: impl AsRef<Path>) -> io::Result<Vec<String>> {
54		BufReader::new(File::open(filename)?).lines().collect()
55	}
56
57	// Call to the function above to get all lines from the input file
58	let lines = lines_from_file(read_file).expect("Could not load lines");
59
60	// Initialize the output vector as empty, as well as the header row information
61	let mut process_rows:Vec<Process> = vec![];
62	let mut header_row:Vec<&str> = vec![];
63
64	// Loop through every line in the file
65	for (ii,line) in lines.iter().enumerate() {
66
67		// For a single line, a new vector holds each element in the CSV row
68		let all_parts: Vec<&str> = line.split(',').collect();
69		
70		// For the first row:
71		if ii==0 {
72
73			// Confirm the first six columns in the header are the right six columns
74			// Not case-sensitive in theory, so only compare strings biased in one direction (upper) to reduce false positive rejections
75			assert!(all_parts[0].to_string().to_uppercase()=="Document Number".to_string().to_uppercase());
76			assert!(all_parts[1].to_string().to_uppercase()=="Title".to_string().to_uppercase());
77			assert!(all_parts[2].to_string().to_uppercase()=="Subject".to_string().to_uppercase());
78			assert!(all_parts[3].to_string().to_uppercase()=="Product".to_string().to_uppercase());
79			assert!(all_parts[4].to_string().to_uppercase()=="Author".to_string().to_uppercase());
80			assert!(all_parts[5].to_string().to_uppercase()=="Reviewer".to_string().to_uppercase());
81
82			// Store ALL columns into header_row, not just the first six that were confirmed above
83			header_row = all_parts;
84
85		// For every row, so long as it's not a row that's just an empty string
86		} else if !(all_parts[0]=="") {
87
88			// Initialize a single process for this single row
89			let mut needed_proc = Process::new();
90
91			// Use functions of Process to fill the fields with the info in the first six columns
92			needed_proc.set_number(all_parts[0]);
93			needed_proc.set_title(all_parts[1]);
94			needed_proc.set_subject(all_parts[2]);
95			needed_proc.set_product(all_parts[3]);
96			needed_proc.set_author(all_parts[4]);
97			needed_proc.set_reviewer(all_parts[5]);
98
99			// If there are MORE than the minimum required number of columns, then they are Template columns (TFlag)
100			if all_parts.len() > 6 {
101
102				// Loop through all remaining columns
103				for jj in 6..all_parts.len() {
104
105					// Look for the case-insensitive value of "x"
106					match all_parts[jj] {
107						"x"|"X" => {
108							// If the column header is "Template" then the CSS file is "Template.css"
109							let template_name = header_row[jj].to_string() + ".css";
110							if verbose { println!("Template identified: {}",&template_name); }
111							// Use function of Process to add another template to the list
112							needed_proc.add_template(&template_name);
113							new_css_file_if_none_exists(&template_name);
114						},
115						// don't do anything for a column that doesn't include "x" or "X"
116						_ => (),
117					}
118					
119				}
120			}
121			if verbose { needed_proc.display_process_to_stdout(); };
122
123			// Add this process to the list of processes going into the output
124			process_rows.push(needed_proc);
125		}
126
127	}
128
129	// Return the list of every process found in the file
130	process_rows
131
132}
133
134//  ▄▄▄▄▄▄▄▄▄▄▄  ▄▄▄▄▄▄▄▄▄▄▄  ▄▄▄▄▄▄▄▄▄▄▄  ▄▄▄▄▄▄▄▄▄▄▄ 
135// ▐░░░░░░░░░░░▌▐░░░░░░░░░░░▌▐░░░░░░░░░░░▌▐░░░░░░░░░░░▌
136//  ▀▀▀▀█░█▀▀▀▀ ▐░█▀▀▀▀▀▀▀▀▀ ▐░█▀▀▀▀▀▀▀▀▀  ▀▀▀▀█░█▀▀▀▀ 
137//      ▐░▌     ▐░▌          ▐░▌               ▐░▌     
138//      ▐░▌     ▐░█▄▄▄▄▄▄▄▄▄ ▐░█▄▄▄▄▄▄▄▄▄      ▐░▌     
139//      ▐░▌     ▐░░░░░░░░░░░▌▐░░░░░░░░░░░▌     ▐░▌     
140//      ▐░▌     ▐░█▀▀▀▀▀▀▀▀▀  ▀▀▀▀▀▀▀▀▀█░▌     ▐░▌     
141//      ▐░▌     ▐░▌                    ▐░▌     ▐░▌     
142//      ▐░▌     ▐░█▄▄▄▄▄▄▄▄▄  ▄▄▄▄▄▄▄▄▄█░▌     ▐░▌     
143//      ▐░▌     ▐░░░░░░░░░░░▌▐░░░░░░░░░░░▌     ▐░▌     
144//       ▀       ▀▀▀▀▀▀▀▀▀▀▀  ▀▀▀▀▀▀▀▀▀▀▀       ▀  
145
146#[cfg(test)]
147mod tests {
148    // Note this useful idiom: importing names from outer (for mod tests) scope.
149	use super::*;
150	use std::fs;
151	use std::fs::OpenOptions;
152	use std::io::Write;
153
154	fn create_test_file(filename:&str, lines:Vec<String>) {
155		let mut new_file = OpenOptions::new()
156		            .read(true)
157		            .write(true)
158		            .create(true)
159		            .open(filename)
160		            .expect("Could not open the file!");
161        for line in lines {
162        	new_file.write({line+"\n"}.as_bytes()).expect("Could not write line to test-only file!");
163        }
164	}
165
166	fn destroy_test_file(filename:&str) {
167		let _ = fs::remove_file(filename);
168	}
169
170	fn generate_minimum_csv_file() -> Vec<String> {
171		vec!["Document Number,Title,Subject,Product,Author,Reviewer".to_string()]
172	}
173
174	fn generate_csv_with_1000_templates() -> Vec<String> {
175		let mut new_vec_of_strings = generate_minimum_csv_file();
176		new_vec_of_strings.push(new_vec_of_strings[0].clone());
177		//println!("{:?}",new_vec_of_strings);
178		for ii in 0..1000 {
179			new_vec_of_strings[0].push_str(",Template");
180			new_vec_of_strings[0].push_str(&ii.to_string());
181			new_vec_of_strings[1].push_str(",x");
182		}
183		//println!("{:?}",new_vec_of_strings);
184		new_vec_of_strings
185	}
186
187	fn generate_csv_with_1000_processes() -> Vec<String> {
188		let mut new_vec_of_strings = generate_minimum_csv_file();
189		//println!("{:?}",new_vec_of_strings);
190		for _ii in 0..1000 {
191			new_vec_of_strings.push(new_vec_of_strings[0].clone());
192		}
193		//println!("{:?}",new_vec_of_strings);
194		new_vec_of_strings
195	}
196
197	fn generate_csv_with_25_templates_250_processes() -> Vec<String> {
198		let mut new_vec_of_strings = generate_minimum_csv_file();
199		new_vec_of_strings.push(new_vec_of_strings[0].clone());
200		//println!("{:?}",new_vec_of_strings);
201		for ii in 0..25 {
202			new_vec_of_strings[0].push_str(",Template");
203			new_vec_of_strings[0].push_str(&ii.to_string());
204			new_vec_of_strings[1].push_str(",x");
205		}
206		for _ii in 1..250 {
207			new_vec_of_strings.push(new_vec_of_strings[1].clone());
208		}
209		//println!("{:?}",new_vec_of_strings);
210		new_vec_of_strings
211	}
212
213	fn generate_csv_with_allcaps_headers() -> Vec<String> {
214		let mut new_vec_of_strings = generate_minimum_csv_file();
215		new_vec_of_strings[0] = new_vec_of_strings[0].to_uppercase();
216		new_vec_of_strings
217	}
218
219	fn generate_csv_with_lowercase_headers() -> Vec<String> {
220		let mut new_vec_of_strings = generate_minimum_csv_file();
221		new_vec_of_strings[0] = new_vec_of_strings[0].to_lowercase();
222		new_vec_of_strings
223	}
224
225	fn generate_csv_with_1000_templates_uppercase_x() -> Vec<String> {
226		let mut new_vec_of_strings = generate_minimum_csv_file();
227		new_vec_of_strings.push(new_vec_of_strings[0].clone());
228		//println!("{:?}",new_vec_of_strings);
229		for ii in 0..1000 {
230			new_vec_of_strings[0].push_str(",Template");
231			new_vec_of_strings[0].push_str(&ii.to_string());
232			new_vec_of_strings[1].push_str(",X");
233		}
234		//println!("{:?}",new_vec_of_strings);
235		new_vec_of_strings
236	}
237
238	// Test different kinds of CSV files
239
240	#[test]
241	fn test_minimum() {
242		let file_name = "test_minimum.csv".to_string();
243		create_test_file(&file_name,generate_minimum_csv_file());
244
245		let list_of_processes = read_csv(&file_name,true);
246		assert_eq!(list_of_processes.len(),0);
247
248		destroy_test_file(&file_name);
249	}
250
251	#[test]
252	fn test_1000_templates_quiet() {
253		let file_name = "test_1000_templates.csv".to_string();
254		create_test_file(&file_name,generate_csv_with_1000_templates());
255
256		let list_of_processes = read_csv(&file_name,false);
257		assert_eq!(list_of_processes.len(),1);
258		assert_eq!(list_of_processes[0].get_all_templates().len(),1000);
259
260		destroy_test_file(&file_name);
261	}
262
263	#[test]
264	fn test_1000_processes_quiet() {
265		let file_name = "test_1000_processes.csv".to_string();
266		create_test_file(&file_name,generate_csv_with_1000_processes());
267
268		let list_of_processes = read_csv(&file_name,false);
269		assert_eq!(list_of_processes.len(),1000);
270		assert_eq!(list_of_processes[0].get_all_templates().len(),0);
271
272		destroy_test_file(&file_name);
273	}
274
275	#[test]
276	fn test_25_templates_250_processes_quiet() {
277		let file_name = "test_25_templates_250_processes.csv".to_string();
278		create_test_file(&file_name,generate_csv_with_25_templates_250_processes());
279
280		let list_of_processes = read_csv(&file_name,false);
281		assert_eq!(list_of_processes.len(),250);
282		for ii in 0..250 {
283			assert_eq!(list_of_processes[ii].get_all_templates().len(),25);
284		}
285		destroy_test_file(&file_name);
286	}
287
288	#[test]
289	fn test_allcaps_csv() {
290		let file_name = "test_allcaps.csv".to_string();
291		create_test_file(&file_name,generate_csv_with_allcaps_headers());
292
293		let list_of_processes = read_csv(&file_name,true);
294		assert_eq!(list_of_processes.len(),0);
295
296		destroy_test_file(&file_name);
297	}
298
299	#[test]
300	fn test_lowercase_csv() {
301		let file_name = "test_lowercase.csv".to_string();
302		create_test_file(&file_name,generate_csv_with_lowercase_headers());
303
304		let list_of_processes = read_csv(&file_name,true);
305		assert_eq!(list_of_processes.len(),0);
306
307		destroy_test_file(&file_name);
308	}
309
310	#[test]
311	fn test_uppercase_x_quiet() {
312		let file_name = "test_1000_templates_uppercase_x.csv".to_string();
313		create_test_file(&file_name,generate_csv_with_1000_templates_uppercase_x());
314
315		let list_of_processes = read_csv(&file_name,false);
316		assert_eq!(list_of_processes.len(),1);
317		assert_eq!(list_of_processes[0].get_all_templates().len(),1000);
318
319		destroy_test_file(&file_name);
320	}
321
322
323}