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
24pub fn 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
122			// Add this process to the list of processes going into the output
123			process_rows.push(needed_proc);
124		}
125
126	}
127
128	// Return the list of every process found in the file
129	process_rows
130
131}
132
133//  ▄▄▄▄▄▄▄▄▄▄▄  ▄▄▄▄▄▄▄▄▄▄▄  ▄▄▄▄▄▄▄▄▄▄▄  ▄▄▄▄▄▄▄▄▄▄▄ 
134// ▐░░░░░░░░░░░▌▐░░░░░░░░░░░▌▐░░░░░░░░░░░▌▐░░░░░░░░░░░▌
135//  ▀▀▀▀█░█▀▀▀▀ ▐░█▀▀▀▀▀▀▀▀▀ ▐░█▀▀▀▀▀▀▀▀▀  ▀▀▀▀█░█▀▀▀▀ 
136//      ▐░▌     ▐░▌          ▐░▌               ▐░▌     
137//      ▐░▌     ▐░█▄▄▄▄▄▄▄▄▄ ▐░█▄▄▄▄▄▄▄▄▄      ▐░▌     
138//      ▐░▌     ▐░░░░░░░░░░░▌▐░░░░░░░░░░░▌     ▐░▌     
139//      ▐░▌     ▐░█▀▀▀▀▀▀▀▀▀  ▀▀▀▀▀▀▀▀▀█░▌     ▐░▌     
140//      ▐░▌     ▐░▌                    ▐░▌     ▐░▌     
141//      ▐░▌     ▐░█▄▄▄▄▄▄▄▄▄  ▄▄▄▄▄▄▄▄▄█░▌     ▐░▌     
142//      ▐░▌     ▐░░░░░░░░░░░▌▐░░░░░░░░░░░▌     ▐░▌     
143//       ▀       ▀▀▀▀▀▀▀▀▀▀▀  ▀▀▀▀▀▀▀▀▀▀▀       ▀  
144
145#[cfg(test)]
146mod tests {
147    // Note this useful idiom: importing names from outer (for mod tests) scope.
148	use super::*;
149	use std::fs;
150	use std::fs::OpenOptions;
151	use std::io::Write;
152
153	fn create_test_file(filename:&str, lines:Vec<String>) {
154		let mut new_file = OpenOptions::new()
155		            .read(true)
156		            .write(true)
157		            .create(true)
158		            .open(filename)
159		            .expect("Could not open the file!");
160        for line in lines {
161        	new_file.write({line+"\n"}.as_bytes()).expect("Could not write line to test-only file!");
162        }
163	}
164
165	fn destroy_test_file(filename:&str) {
166		let _ = fs::remove_file(filename);
167	}
168
169	fn generate_minimum_csv_file() -> Vec<String> {
170		vec!["Document Number,Title,Subject,Product,Author,Reviewer".to_string()]
171	}
172
173	fn generate_csv_with_1000_templates() -> Vec<String> {
174		let mut new_vec_of_strings = generate_minimum_csv_file();
175		new_vec_of_strings.push(new_vec_of_strings[0].clone());
176		//println!("{:?}",new_vec_of_strings);
177		for ii in 0..1000 {
178			new_vec_of_strings[0].push_str(",Template");
179			new_vec_of_strings[0].push_str(&ii.to_string());
180			new_vec_of_strings[1].push_str(",x");
181		}
182		//println!("{:?}",new_vec_of_strings);
183		new_vec_of_strings
184	}
185
186	fn generate_csv_with_1000_processes() -> Vec<String> {
187		let mut new_vec_of_strings = generate_minimum_csv_file();
188		//println!("{:?}",new_vec_of_strings);
189		for _ii in 0..1000 {
190			new_vec_of_strings.push(new_vec_of_strings[0].clone());
191		}
192		//println!("{:?}",new_vec_of_strings);
193		new_vec_of_strings
194	}
195
196	fn generate_csv_with_25_templates_250_processes() -> Vec<String> {
197		let mut new_vec_of_strings = generate_minimum_csv_file();
198		new_vec_of_strings.push(new_vec_of_strings[0].clone());
199		//println!("{:?}",new_vec_of_strings);
200		for ii in 0..25 {
201			new_vec_of_strings[0].push_str(",Template");
202			new_vec_of_strings[0].push_str(&ii.to_string());
203			new_vec_of_strings[1].push_str(",x");
204		}
205		for _ii in 1..250 {
206			new_vec_of_strings.push(new_vec_of_strings[1].clone());
207		}
208		//println!("{:?}",new_vec_of_strings);
209		new_vec_of_strings
210	}
211
212	fn generate_csv_with_allcaps_headers() -> Vec<String> {
213		let mut new_vec_of_strings = generate_minimum_csv_file();
214		new_vec_of_strings[0] = new_vec_of_strings[0].to_uppercase();
215		new_vec_of_strings
216	}
217
218	fn generate_csv_with_lowercase_headers() -> Vec<String> {
219		let mut new_vec_of_strings = generate_minimum_csv_file();
220		new_vec_of_strings[0] = new_vec_of_strings[0].to_lowercase();
221		new_vec_of_strings
222	}
223
224	fn generate_csv_with_1000_templates_uppercase_x() -> Vec<String> {
225		let mut new_vec_of_strings = generate_minimum_csv_file();
226		new_vec_of_strings.push(new_vec_of_strings[0].clone());
227		//println!("{:?}",new_vec_of_strings);
228		for ii in 0..1000 {
229			new_vec_of_strings[0].push_str(",Template");
230			new_vec_of_strings[0].push_str(&ii.to_string());
231			new_vec_of_strings[1].push_str(",X");
232		}
233		//println!("{:?}",new_vec_of_strings);
234		new_vec_of_strings
235	}
236
237	// Test different kinds of CSV files
238
239	#[test]
240	fn test_minimum() {
241		let file_name = "test_minimum.csv".to_string();
242		create_test_file(&file_name,generate_minimum_csv_file());
243
244		let list_of_processes = read_csv(&file_name,true);
245		assert_eq!(list_of_processes.len(),0);
246
247		destroy_test_file(&file_name);
248	}
249
250	#[test]
251	fn test_1000_templates_quiet() {
252		let file_name = "test_1000_templates.csv".to_string();
253		create_test_file(&file_name,generate_csv_with_1000_templates());
254
255		let list_of_processes = read_csv(&file_name,false);
256		assert_eq!(list_of_processes.len(),1);
257		assert_eq!(list_of_processes[0].get_all_templates().len(),1000);
258
259		destroy_test_file(&file_name);
260	}
261
262	#[test]
263	fn test_1000_processes_quiet() {
264		let file_name = "test_1000_processes.csv".to_string();
265		create_test_file(&file_name,generate_csv_with_1000_processes());
266
267		let list_of_processes = read_csv(&file_name,false);
268		assert_eq!(list_of_processes.len(),1000);
269		assert_eq!(list_of_processes[0].get_all_templates().len(),0);
270
271		destroy_test_file(&file_name);
272	}
273
274	#[test]
275	fn test_25_templates_250_processes_quiet() {
276		let file_name = "test_25_templates_250_processes.csv".to_string();
277		create_test_file(&file_name,generate_csv_with_25_templates_250_processes());
278
279		let list_of_processes = read_csv(&file_name,false);
280		assert_eq!(list_of_processes.len(),250);
281		for ii in 0..250 {
282			assert_eq!(list_of_processes[ii].get_all_templates().len(),25);
283		}
284		destroy_test_file(&file_name);
285	}
286
287	#[test]
288	fn test_allcaps_csv() {
289		let file_name = "test_allcaps.csv".to_string();
290		create_test_file(&file_name,generate_csv_with_allcaps_headers());
291
292		let list_of_processes = read_csv(&file_name,true);
293		assert_eq!(list_of_processes.len(),0);
294
295		destroy_test_file(&file_name);
296	}
297
298	#[test]
299	fn test_lowercase_csv() {
300		let file_name = "test_lowercase.csv".to_string();
301		create_test_file(&file_name,generate_csv_with_lowercase_headers());
302
303		let list_of_processes = read_csv(&file_name,true);
304		assert_eq!(list_of_processes.len(),0);
305
306		destroy_test_file(&file_name);
307	}
308
309	#[test]
310	fn test_uppercase_x_quiet() {
311		let file_name = "test_1000_templates_uppercase_x.csv".to_string();
312		create_test_file(&file_name,generate_csv_with_1000_templates_uppercase_x());
313
314		let list_of_processes = read_csv(&file_name,false);
315		assert_eq!(list_of_processes.len(),1);
316		assert_eq!(list_of_processes[0].get_all_templates().len(),1000);
317
318		destroy_test_file(&file_name);
319	}
320
321
322}