1#![doc(html_logo_url = "https://images.squarespace-cdn.com/content/v1/60416644b68a5453a868e856/1628206791960-M61IU1QP850NRLI9CE07/Stanhope+Etching.jpg?format=2500w")]
2mod read_csv;
14mod read_ebml;
15mod write_ebml;
16mod write_html;
17mod write_script;
18mod write_webmenu;
19
20use std::{
22 process::Command,
23 fs::{self},
26 io::{self, Error, ErrorKind, Result},
27 path::Path,
28};
29use chrono::prelude::*;
30use read_ebml::Process;
31
32use clap::Parser; use read_csv::read_csv; use write_ebml::get_process_folder_name;
35use write_ebml::write_ebml;
36use read_ebml::read_ebml;
37use write_html::generate_complete_html;
38use write_webmenu::generate_complete_webmenu;
39use glob::glob;
40
41
42#[derive(Parser, Debug)]
44#[command(version, about =
45"\n\n\x1b[1;30;47mStanope\x1b[0m\x1b[30;47m, the Easy Button process generation engine.\x1b[0m
46
47Arguments passed into the options should be surrounded by single or double quotes, e.g.
48% ./stanhope -p \"EB-WI-0010\" \x1b[36m<< double quotes are accepted\x1b[0m
49% ./stanhope -a 'EB-WI-*' \x1b[36m<< single quotes are accepted\x1b[0m",
50author = "Strativus Group <contact@strativusgroup.com>",
51long_about = None,
52after_help =
53"
54")]
55struct StanhopeArgs {
56 #[arg(short, long, value_name = "CSV-FILE", default_value_t = String::from(""), verbatim_doc_comment)]
70 listgen: String,
71
72 #[arg(short, long, default_value_t = false, verbatim_doc_comment)]
76 webmenu: bool,
77
78 #[arg(short, long, value_name = "DOCUMENT-NUMBER", default_value_t = String::from(""), verbatim_doc_comment)]
84 process_ebml: String,
85
86 #[arg(short, long, use_value_delimiter = true, value_delimiter = ' ', num_args = 3, value_names = ["DOCUMENT-NUMBER","SCRIPT-FORMAT","PAUSE-AFTER-EACH"], verbatim_doc_comment)]
96 scriptify_process: Vec<String>,
97
98 #[arg(short, long, value_name = "DOCUMENT-NUMBER", default_value_t = String::from(""), verbatim_doc_comment)]
106 archive_process: String,
107
108 #[arg(short, long, value_name = "DOCUMENT-NUMBER", default_value_t = String::from(""), verbatim_doc_comment)]
114 inspect_process: String,
115
116 #[arg(short, long, value_name = "DOCUMENT-NUMBER", default_value_t = String::from(""), verbatim_doc_comment)]
122 cleanup_previous: String,
123
124 #[arg(short, long, default_value_t = false,)]
126 verbose: bool,
127
128 #[arg(short, long, default_value_t = false,)]
130 ebml_help: bool,
131}
132
133fn copy_dir_all(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> io::Result<()> {
135 fs::create_dir_all(&dst)?;
136 for entry in fs::read_dir(src)? {
137 let entry = entry?;
138 let ty = entry.file_type()?;
139 if ty.is_dir() {
140 copy_dir_all(entry.path(), dst.as_ref().join(entry.file_name()))?;
141 } else {
142 fs::copy(entry.path(), dst.as_ref().join(entry.file_name()))?;
143 }
144 }
145 Ok(())
146}
147
148fn copy_files_all(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> io::Result<()> {
150 fs::create_dir_all(&dst)?;
151 for entry in fs::read_dir(src)? {
152 let entry = entry?;
153 let ty = entry.file_type()?;
154 if ty.is_dir() {
155 ();
156 } else {
157 fs::copy(entry.path(), dst.as_ref().join(entry.file_name()))?;
158 }
159 }
160 Ok(())
161}
162
163fn archive_single_process(new_proc: Process, verbose: &bool) {
165 let previous_folder = get_process_folder_name(&new_proc).to_owned() + "/previous";
168 match Path::new(&previous_folder).exists() {
169 true => (),
170 false => { let _ = fs::create_dir(&previous_folder); },
171 };
172 let root_assets_folder = "./assets";
173 let previous_assets_folder = previous_folder.clone() + "/assets";
174 match Path::new(&previous_assets_folder).exists() {
175 true => (),
176 false => { let _ = fs::create_dir(&previous_assets_folder); },
177 };
178
179 let rev_string = new_proc.get_revision();
181 let right_now_string = Utc::now().format("UTC-%Y-%m-%d-T%H-%M-%S").to_string();
182 let timestamp_folder = previous_folder.clone() + "/Rev_" + new_proc.get_revision() + "_" + &right_now_string;
183 if *verbose {
184 println!("Revision: {}",&rev_string);
185 println!("SystemTime::now() ...ish: {}",right_now_string);
186 println!("Trying to create this folder: {:?}",timestamp_folder);
187 }
188 let _ = fs::create_dir(×tamp_folder);
189 let _ = copy_dir_all(root_assets_folder,previous_assets_folder);
191 let _ = copy_files_all(get_process_folder_name(&new_proc),timestamp_folder.clone());
193}
194
195fn list_directories_to_cleanup(prev:String) -> Vec<String> {
206
207 let mut all_revs:Vec<String> = vec![];
208 let mut directories_to_cleanup:Vec<String> = vec![];
209 let all_archives = glob(&(prev.clone() + "/Rev_*_UTC-*-*-*-T*-*-*")).expect("Failed to read glob pattern");
210
211 for entry in all_archives {
212 match entry {
213 Ok(path) => {
214 let archive = path.file_name().expect("huh?").to_str().expect("huh?");
215 let pos = archive.find("_UTC-").expect("rust is hard");
216 let rev = String::from(&archive[4..pos]);
217 all_revs.push(rev);
218 },
219 Err(e) => println!("{:?}",e),
220 }
221 }
222
223 all_revs.dedup();
224 println!("For this process, these are all the revisions: {:?}",&all_revs);
225
226 for rev in all_revs {
227 println!("\nHere are all of the archive folders for Rev {}:",rev);
228 let these = glob(&(prev.clone() + "/Rev_" + &rev + "_UTC-*-*-*-T*-*-*")).expect("Failed to read glob pattern");
229 let mut vec:Vec<String> = vec![];
230 for this in these {
231 match this {
232 Ok(path) => vec.push(path.file_name().expect("huh?").to_str().expect("huh?").to_string()),
233 Err(e) => println!("{:?}",e),
234 }
235 }
236 vec.sort_by(|a, b| a.to_lowercase().cmp(&b.to_lowercase()));
237 let keeper = vec.pop().expect("pop?");
238 for d in &vec {
239 println!(" {} \x1b[93mDELETE\x1b[0m",&d);
240 }
241 println!(" 💾 {} 💾 << only one to be saved...",&keeper);
242 directories_to_cleanup.append(&mut vec);
243 }
244 directories_to_cleanup
245}
246
247fn globify_document_number(input:&String) -> Vec<String> {
249 let all_results = match input.chars().last().unwrap() {
250 '*' => glob(&(input.to_owned())).expect("Failed to read glob pattern"),
251 _ => glob(&(input.to_owned()+"*")).expect("Failed to read glob pattern"),
252 };
253 let mut matching_process_folders:Vec<String> = vec![];
254
255 for entry in all_results {
256 match entry {
257 Ok(path) => {
258 let process_folder = path.file_name().expect("huh?").to_str().expect("huh?");
259 matching_process_folders.push(String::from(process_folder));
260 },
261 Err(e) => println!("{:?}",e),
262 }
263 }
264 matching_process_folders
265}
266
267fn attempt_fallbacks(fallbacks: Vec<Box<dyn Fn() -> Result<i32>>>) -> Result<i32> {
269 let mut last_error = None;
270
271 for fallback in fallbacks {
272 match fallback() {
273 Ok(value) => return Ok(value),
274 Err(e) => {
275 println!("Call failed, trying next... (Error: {})", e);
276 last_error = Some(e);
277 }
278 }
279 }
280
281 Err(last_error.unwrap_or_else(|| Error::new(ErrorKind::Other, "No fallbacks provided")))
282}
283
284fn main() {
296
297 let args = StanhopeArgs::parse();
298
299 if !(&args.listgen=="") {
302
303 let needed_processes_list = read_csv(&args.listgen,args.verbose.clone());
305 if args.verbose { println!("Stanhope found {} EBML files to create in the CSV file.",needed_processes_list.len()); }
306 for new_proc_to_create in needed_processes_list {
308 if args.verbose { new_proc_to_create.display_process_to_stdout() };
309 write_ebml(&new_proc_to_create,args.verbose.clone());
310 }
311
312 }
313
314 if !(args.scriptify_process.len()==0) {
317 let matching_processes = globify_document_number(&args.scriptify_process[0]);
319 if args.verbose { println!("{:?}",&matching_processes) };
320
321 if matching_processes.len() > 0 {
322
323 for process in matching_processes {
324
325
326 let file_to_read = "./".to_owned() + &process + "/" + &process + ".ebml";
327
328 if args.verbose {
329 println!("");
330 println!("=== Stanhope Scriptification =========================================================");
331 println!("===");
332 println!("=== >> Process: {}",&process);
333 println!("=== >> Script Format: {}",&args.scriptify_process[1]);
334 println!("=== >> Pause after each? {}",&args.scriptify_process[2]);
335 println!("===");
336 println!("======================================================================================");
337 println!("");
338 }
339
340 if args.verbose { print!("[A] Attempting to read EBML -> {} ...",&file_to_read); }
341 let new_proc = read_ebml(&file_to_read);
342 if args.verbose { print!("success!\n"); }
343
344 let all_commands = new_proc.get_all_command_lines();
345 if args.verbose { print!("[B] Stanhope found {} lines of type \"Command\" in the file.\n",all_commands.len()); }
346
347 if args.verbose { print!("[C] Processing script format \"{}\"\n",&args.scriptify_process[1]); }
348
349
350 let wait:bool = match args.scriptify_process[2].to_uppercase().as_str() {
351 "F" | "FALSE" | "N" | "NO" => false,
352 _ => true,
353 };
354 write_script::script_file_from_process(new_proc,write_script::learn(&args.scriptify_process[1]),wait)
355
356
357
358 } }; } if !(&args.process_ebml=="") {
399 let matching_processes = globify_document_number(&args.process_ebml);
401 if args.verbose { println!("{:?}",&matching_processes) };
402
403 if matching_processes.len() > 0 {
404
405 for process in matching_processes {
406
407 let file_to_read = "./".to_owned() + &process + "/" + &process + ".ebml";
408 if args.verbose { println!("Attempting to process this file: {}",&file_to_read) };
409
410 let new_proc = read_ebml(&file_to_read);
411 if args.verbose { new_proc.display_process_to_stdout() };
412
413 if args.verbose { println!("Attempting to delete old HTML file: {:?}",fs::remove_file(&file_to_read.replace(".ebml",".html"))); } else { let _ = fs::remove_file(&file_to_read.replace(".ebml",".html")); }
415
416 generate_complete_html(&String::from(&file_to_read.replace(".ebml",".html")),&new_proc);
417
418 let win_chrome_path = "& 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe' --headless --no-pdf-header-footer --print-to-pdf=\"";
420 let win_process_library = "$PWD\\";
421 let win_chrome_string_to_throw = &(win_chrome_path.to_string() + win_process_library + &file_to_read.replace(".ebml",".pdf") + "\" \"" + win_process_library + &file_to_read.clone().replace(".ebml",".html\""));
422 let posix_chrome = "chrome --headless --print-to-pdf=\"".to_string() + &file_to_read.clone().replace(".ebml",".pdf") + "\" \"" + &file_to_read.clone().replace(".ebml",".html") + "\" --no-pdf-header-footer";
424 let posix_chromium = "chromium --headless --print-to-pdf=\"".to_string() + &file_to_read.clone().replace(".ebml",".pdf") + "\" \"" + &file_to_read.clone().replace(".ebml",".html") + "\" --no-pdf-header-footer";
426
427 let _output = if cfg!(target_os = "windows") {
428
429 let mut fallbacks: Vec<Box<dyn Fn() -> Result<i32>>> = Vec::new();
430 fallbacks.push(Box::new(move || {
431
432 if args.verbose { println!("Here's my command string to chrome:\n\n{}\n\n",win_chrome_string_to_throw); }
433 Command::new("powershell.exe")
434 .args(["-Command", win_chrome_string_to_throw])
435 .output()
436 .expect("failed to execute process");
437
438 Err(Error::new(ErrorKind::Other, "Chrome call to OS attempted."))
439 }));
440
441 } else {
442
443 let mut fallbacks: Vec<Box<dyn Fn() -> Result<i32>>> = Vec::new();
445 fallbacks.push(Box::new(move || {
446
447 Command::new("sh")
448 .arg("-c")
449 .arg(&(posix_chrome))
450 .output()
451 .expect("failed to execute process");
452
453 Err(Error::new(ErrorKind::Other, "Chrome call to OS attempted."))
454 }));
455 fallbacks.push(Box::new(move || {
456
457 Command::new("sh")
458 .arg("-c")
459 .arg(&(posix_chromium))
460 .output()
461 .expect("failed to execute process");
462
463 Err(Error::new(ErrorKind::Other, "Chromium call to OS attempted."))
464 }));
465 let _result = attempt_fallbacks(fallbacks);
466
467 };
468
469 archive_single_process(new_proc,&args.verbose);
470
471 } } } if !(&args.archive_process=="") {
478 let matching_processes = globify_document_number(&args.archive_process);
480 if args.verbose { println!("{:?}",&matching_processes) };
481
482 if matching_processes.len() > 0 {
483
484 for process in matching_processes {
485
486 let file_to_read = "./".to_owned() + &process + "/" + &process + ".ebml";
487
488 let new_proc = read_ebml(&file_to_read);
489 archive_single_process(new_proc,&args.verbose);
490 }
491 }
492 }
493
494 if !(&args.cleanup_previous=="") {
497 let matching_processes = globify_document_number(&args.cleanup_previous);
499 if args.verbose { println!("{:?}",&matching_processes) };
500
501 if matching_processes.len() > 0 {
502
503 for process in matching_processes {
504
505 let prevs = "./".to_owned() + &process + "/previous";
506
507 let delete_these = list_directories_to_cleanup(prevs);
508
509 for dir in delete_these {
510 println!("Attempting to delete {} ... ",&dir);
511 let delete_this = "./".to_owned() + &process + "/previous/" + &dir;
512 let _ = fs::remove_dir_all(delete_this);
513 }
514 }
515 }
516 }
517
518 if !(&args.inspect_process=="") {
521 let matching_processes = globify_document_number(&args.inspect_process);
523 if args.verbose { println!("{:?}",&matching_processes) };
524
525 if matching_processes.len() > 0 {
526
527 for process in matching_processes {
528
529 let file_to_read = "./".to_owned() + &process + "/" + &process + ".ebml";
530
531 let new_proc = read_ebml(&file_to_read);
532 new_proc.display_process_to_stdout();
533 }
534 }
535 }
536
537 if args.webmenu {
540 generate_complete_webmenu(&args.verbose);
541 }
542
543 if args.ebml_help {
546 println!("
547
548Note: Process files passed into --process-ebml are specifically-formatted EBML files (Easy Button Markup Language).
549
550==================================================
551== Easy Button Markup Language Cheat Sheet ===
552==================================================
553
554// <COMMENT>
555Template | <CSS FILENAME FOUND IN ASSETS FOLDER>
556
557Title | <PROCESS TITLE>
558Number | <PROCESS NUMBER>
559Author | <PROCESS AUTHOR>
560Reviewer | <PROCESS REVIEWER>
561Process Type | <CATEGORY>, e.g. Test Procedure
562
563Subject | <THING TO WHICH THE PROCESS APPLIES>
564Subject Image | <FILENAME OF IMAGE OF SUBJECT>
565Product | <THING PRODUCED BY THIS PROCESS> - optional
566Product Image | <FILENAME OF IMAGE OF PRODUCT> - optional
567
568Revision | <LATEST REV NUM> | <DESCRIPTION OF CHANGE>
569Revision | <INTERMEDIATE REV NUM> | <DESCRIPTION OF CHANGE>
570Revision | <FIRST REV NUM> | <INITIAL DESCTIPTION>
571
572Section | <TITLE OF SECTION>
573 Step | <TITLE OF STEP>
574
575 // These all must occur within a STEP
576
577 Context | <PLAIN TEXT STATEMENTS FOR CONTEXT>
578 Command | <VERBATIM COMPUTER CODE>
579 Warning | <BIG BOLD TEXT TO CAPTURE ATTENTION>
580 Image | <FILENAME> | <CAPTION TEXT>
581 Resource | <THING YOU NEED TO COMPLETE THIS STEP> | <CALIBRATED?>
582 Objective | <ACHIEVED PURPOSE OF THIS PROCESS>
583 Out of Scope | <NOTE ABOUT WHAT NOT TO DO HERE>
584
585 // Verification methods can be single letter: A, I, D, T, S
586 // They can also be full words: Analysis, Inspection, Demonstration, Test, Sampling
587 Verification | <REQUIREMENT ID> | <FULL TEXT OF REQUIREMENT> | <VERIFICATION METHOD>
588
589 // Action lines can be grouped together to form consecutive table rows
590 // To optionally include Two-Party Verification, <TPV> can be \"TPV\"
591 // To NOT make an action Two-Party Verified, stop at the <EXPECTED RESULT>
592 Action | <THING TO DO> | <EXPECTED RESULT> | <TPV?> - optional
593 Action | <THING TO DO> | <EXPECTED RESULT> | <TPV?> - optional
594
595For rendered examples, see https://stanhope.strativusgroup.com/latest/stylesheetviewer.html");
596 }
597
598}