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},
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)]
69 listgen: String,
70
71 #[arg(short, long, default_value_t = false, verbatim_doc_comment)]
74 webmenu: bool,
75
76 #[arg(short, long, value_name = "DOCUMENT-NUMBER", default_value_t = String::from(""), verbatim_doc_comment)]
81 process_ebml: String,
82
83 #[arg(short, long, use_value_delimiter = true, value_delimiter = ' ', num_args = 3, value_names = ["DOCUMENT-NUMBER","SCRIPT-FORMAT","PAUSE-AFTER-EACH"], verbatim_doc_comment)]
92 scriptify_process: Vec<String>,
93
94 #[arg(short, long, value_name = "DOCUMENT-NUMBER", default_value_t = String::from(""), verbatim_doc_comment)]
101 archive_process: String,
102
103 #[arg(short, long, value_name = "DOCUMENT-NUMBER", default_value_t = String::from(""), verbatim_doc_comment)]
108 inspect_process: String,
109
110 #[arg(short, long, value_name = "DOCUMENT-NUMBER", default_value_t = String::from(""), verbatim_doc_comment)]
115 cleanup_previous: String,
116
117 #[arg(short, long, default_value_t = false,)]
119 verbose: bool,
120
121 #[arg(short, long, default_value_t = false,)]
123 ebml_help: bool,
124}
125
126fn copy_dir_all(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> io::Result<()> {
128 fs::create_dir_all(&dst)?;
129 for entry in fs::read_dir(src)? {
130 let entry = entry?;
131 let ty = entry.file_type()?;
132 if ty.is_dir() {
133 copy_dir_all(entry.path(), dst.as_ref().join(entry.file_name()))?;
134 } else {
135 fs::copy(entry.path(), dst.as_ref().join(entry.file_name()))?;
136 }
137 }
138 Ok(())
139}
140
141fn copy_files_all(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> io::Result<()> {
143 fs::create_dir_all(&dst)?;
144 for entry in fs::read_dir(src)? {
145 let entry = entry?;
146 let ty = entry.file_type()?;
147 if ty.is_dir() {
148 ();
149 } else {
150 fs::copy(entry.path(), dst.as_ref().join(entry.file_name()))?;
151 }
152 }
153 Ok(())
154}
155
156fn archive_single_process(new_proc: Process, verbose: &bool) {
157 let previous_folder = get_process_folder_name(&new_proc).to_owned() + "/previous";
160 match Path::new(&previous_folder).exists() {
161 true => (),
162 false => { let _ = fs::create_dir(&previous_folder); },
163 };
164 let root_assets_folder = "./assets";
165 let previous_assets_folder = previous_folder.clone() + "/assets";
166 match Path::new(&previous_assets_folder).exists() {
167 true => (),
168 false => { let _ = fs::create_dir(&previous_assets_folder); },
169 };
170
171 let rev_string = new_proc.get_revision();
173 let right_now_string = Utc::now().format("UTC-%Y-%m-%d-T%H-%M-%S").to_string();
174 let timestamp_folder = previous_folder.clone() + "/Rev_" + new_proc.get_revision() + "_" + &right_now_string;
175 if *verbose {
176 println!("Revision: {}",&rev_string);
177 println!("SystemTime::now() ...ish: {}",right_now_string);
178 println!("Trying to create this folder: {:?}",timestamp_folder);
179 }
180 let _ = fs::create_dir(×tamp_folder);
181 let _ = copy_dir_all(root_assets_folder,previous_assets_folder);
183 let _ = copy_files_all(get_process_folder_name(&new_proc),timestamp_folder.clone());
185}
186
187fn list_directories_to_cleanup(prev:String) -> Vec<String> {
198
199 let mut all_revs:Vec<String> = vec![];
200 let mut directories_to_cleanup:Vec<String> = vec![];
201 let all_archives = glob(&(prev.clone() + "/Rev_*_UTC-*-*-*-T*-*-*")).expect("Failed to read glob pattern");
202
203 for entry in all_archives {
204 match entry {
205 Ok(path) => {
206 let archive = path.file_name().expect("huh?").to_str().expect("huh?");
207 let pos = archive.find("_UTC-").expect("rust is hard");
208 let rev = String::from(&archive[4..pos]);
209 all_revs.push(rev);
210 },
211 Err(e) => println!("{:?}",e),
212 }
213 }
214
215 all_revs.dedup();
216 println!("For this process, these are all the revisions: {:?}",&all_revs);
217
218 for rev in all_revs {
219 println!("\nHere are all of the archive folders for Rev {}:",rev);
220 let these = glob(&(prev.clone() + "/Rev_" + &rev + "_UTC-*-*-*-T*-*-*")).expect("Failed to read glob pattern");
221 let mut vec:Vec<String> = vec![];
222 for this in these {
223 match this {
224 Ok(path) => vec.push(path.file_name().expect("huh?").to_str().expect("huh?").to_string()),
225 Err(e) => println!("{:?}",e),
226 }
227 }
228 vec.sort_by(|a, b| a.to_lowercase().cmp(&b.to_lowercase()));
229 let keeper = vec.pop().expect("pop?");
230 for d in &vec {
231 println!(" {} \x1b[93mDELETE\x1b[0m",&d);
232 }
233 println!(" 💾 {} 💾 << only one to be saved...",&keeper);
234 directories_to_cleanup.append(&mut vec);
235 }
236 directories_to_cleanup
237}
238
239fn globify_document_number(input:&String) -> Vec<String> {
240 let all_results = match input.chars().last().unwrap() {
241 '*' => glob(&(input.to_owned())).expect("Failed to read glob pattern"),
242 _ => glob(&(input.to_owned()+"*")).expect("Failed to read glob pattern"),
243 };
244 let mut matching_process_folders:Vec<String> = vec![];
245
246 for entry in all_results {
247 match entry {
248 Ok(path) => {
249 let process_folder = path.file_name().expect("huh?").to_str().expect("huh?");
250 matching_process_folders.push(String::from(process_folder));
251 },
252 Err(e) => println!("{:?}",e),
253 }
254 }
255 matching_process_folders
256}
257
258fn main() {
270
271 let args = StanhopeArgs::parse();
272
273 if !(&args.listgen=="") {
276
277 let needed_processes_list = read_csv(&args.listgen,args.verbose.clone());
279 if args.verbose { println!("Stanhope found {} EBML files to create in the CSV file.",needed_processes_list.len()); }
280 for new_proc_to_create in needed_processes_list {
282 if args.verbose { new_proc_to_create.display_process_to_stdout() };
283 write_ebml(&new_proc_to_create,args.verbose.clone());
284 }
285
286 }
287
288 if !(args.scriptify_process.len()==0) {
291 let matching_processes = globify_document_number(&args.scriptify_process[0]);
293 if args.verbose { println!("{:?}",&matching_processes) };
294
295 if matching_processes.len() > 0 {
296
297 for process in matching_processes {
298
299
300 let file_to_read = "./".to_owned() + &process + "/" + &process + ".ebml";
301
302 if args.verbose {
303 println!("");
304 println!("=== Stanhope Scriptification =========================================================");
305 println!("===");
306 println!("=== >> Process: {}",&process);
307 println!("=== >> Script Format: {}",&args.scriptify_process[1]);
308 println!("=== >> Pause after each? {}",&args.scriptify_process[2]);
309 println!("===");
310 println!("======================================================================================");
311 println!("");
312 }
313 if args.verbose { print!("[A] Attempting to read EBML -> {} ...",&file_to_read); }
314 let new_proc = read_ebml(&file_to_read);
315 if args.verbose { print!("success!\n"); }
316 let all_commands = new_proc.get_all_command_lines();
317 if args.verbose { print!("[B] Stanhope found {} lines of type \"Command\" in the file.\n",all_commands.len()); }
318
319 if args.verbose { print!("[C] Processing script format \"{}\"\n",&args.scriptify_process[1]); }
320 match args.scriptify_process[1].as_str() {
321 "bash" => {
322 let suffix = ".sh".to_string();
323 let all_script_lines = write_script::bash_file_from_commands(all_commands,&process,&args.scriptify_process[2],&suffix);
324 write_script::write_script(all_script_lines,&process,&suffix);
325 },
326 "powershell" => {
327 let suffix = ".ps1".to_string();
328 let all_script_lines = write_script::powershell_file_from_commands(all_commands,&process,&args.scriptify_process[2],&suffix);
329 write_script::write_script(all_script_lines,&process,&suffix);
330 },
331 "python" => {
332 let suffix = ".py".to_string();
333 let all_script_lines = write_script::python_file_from_commands(all_commands,&process,&args.scriptify_process[2],&suffix);
334 write_script::write_script(all_script_lines,&process,&suffix);
335 }
336 "applescript" => {
337 let suffix = ".command".to_string();
338 let all_script_lines = write_script::bash_file_from_commands(all_commands,&process,&args.scriptify_process[2],&suffix);
339 write_script::write_script(all_script_lines,&process,&suffix);
340 }
341 _ => {
342 println!("That type of <SCRIPT FORMAT> is not yet implemented in stanhope.");
343 println!("Case-sensitive options are: \x1b[93mbash\x1b[0m, \x1b[93mpowershell\x1b[0m, \x1b[93mpython\x1b[0m, \x1b[93mapplescript\x1b[0m");
344 }
345 } } }; } if !(&args.process_ebml=="") {
353 let matching_processes = globify_document_number(&args.process_ebml);
355 if args.verbose { println!("{:?}",&matching_processes) };
356
357 if matching_processes.len() > 0 {
358
359 for process in matching_processes {
360
361 let file_to_read = "./".to_owned() + &process + "/" + &process + ".ebml";
362
363 if args.verbose { println!("Attempting to process this file: {}",&file_to_read) };
364
365 let new_proc = read_ebml(&file_to_read);
366 if args.verbose { new_proc.display_process_to_stdout() };
367
368 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")); }
370
371 generate_complete_html(&String::from(&file_to_read.replace(".ebml",".html")),&new_proc);
372
373 let _output = if cfg!(target_os = "windows") {
374 let chrome_path = "& 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe' --headless --no-pdf-header-footer --print-to-pdf=\"";
376 let process_library = "$PWD\\";
377 let string_to_throw = &(chrome_path.to_string() + process_library + &file_to_read.replace(".ebml",".pdf") + "\" \"" + process_library + &file_to_read.replace(".ebml",".html\""));
379 if args.verbose { println!("Here's my command string to chrome:\n\n{}\n\n",string_to_throw); }
380 Command::new("powershell.exe")
381 .args(["-Command", string_to_throw])
382 .output()
383 .expect("failed to execute process")
384 } else {
385 Command::new("sh")
386 .arg("-c")
387 .arg(&("chromium --headless --print-to-pdf=\"".to_string() + &file_to_read.replace(".ebml",".pdf") + "\" \"" + &file_to_read.replace(".ebml",".html") + "\" --no-pdf-header-footer"))
388 .output()
389 .expect("failed to execute process")
390 };
391
392 archive_single_process(new_proc,&args.verbose);
393
394 } } } if !(&args.archive_process=="") {
401 let matching_processes = globify_document_number(&args.archive_process);
403 if args.verbose { println!("{:?}",&matching_processes) };
404
405 if matching_processes.len() > 0 {
406
407 for process in matching_processes {
408
409 let file_to_read = "./".to_owned() + &process + "/" + &process + ".ebml";
410
411 let new_proc = read_ebml(&file_to_read);
412 archive_single_process(new_proc,&args.verbose);
413 }
414 }
415 }
416
417 if !(&args.cleanup_previous=="") {
420 let matching_processes = globify_document_number(&args.cleanup_previous);
422 if args.verbose { println!("{:?}",&matching_processes) };
423
424 if matching_processes.len() > 0 {
425
426 for process in matching_processes {
427
428 let prevs = "./".to_owned() + &process + "/previous";
429
430 let delete_these = list_directories_to_cleanup(prevs);
431
432 for dir in delete_these {
433 println!("Attempting to delete {} ... ",&dir);
434 let delete_this = "./".to_owned() + &process + "/previous/" + &dir;
435 let _ = fs::remove_dir_all(delete_this);
436 }
437 }
438 }
439 }
440
441 if !(&args.inspect_process=="") {
444 let matching_processes = globify_document_number(&args.inspect_process);
446 if args.verbose { println!("{:?}",&matching_processes) };
447
448 if matching_processes.len() > 0 {
449
450 for process in matching_processes {
451
452 let file_to_read = "./".to_owned() + &process + "/" + &process + ".ebml";
453
454 let new_proc = read_ebml(&file_to_read);
455 new_proc.display_process_to_stdout();
456 }
457 }
458 }
459
460 if args.webmenu {
463 generate_complete_webmenu(&args.verbose);
464 }
465
466 if args.ebml_help {
469 println!("
470
471Note: Process files passed into --process-ebml are specifically-formatted EBML files (Easy Button Markup Language).
472
473==================================================
474== Easy Button Markup Language Cheat Sheet ===
475==================================================
476
477// <COMMENT>
478Template | <CSS FILENAME FOUND IN ASSETS FOLDER>
479
480Title | <PROCESS TITLE>
481Number | <PROCESS NUMBER>
482Author | <PROCESS AUTHOR>
483Reviewer | <PROCESS REVIEWER>
484Process Type | <CATEGORY>, e.g. Test Procedure
485
486Subject | <THING TO WHICH THE PROCESS APPLIES>
487Subject Image | <FILENAME OF IMAGE OF SUBJECT>
488Product | <THING PRODUCED BY THIS PROCESS> - optional
489Product Image | <FILENAME OF IMAGE OF PRODUCT> - optional
490
491Revision | <LATEST REV NUM> | <DESCRIPTION OF CHANGE>
492Revision | <INTERMEDIATE REV NUM> | <DESCRIPTION OF CHANGE>
493Revision | <FIRST REV NUM> | <INITIAL DESCTIPTION>
494
495Section | <TITLE OF SECTION>
496 Step | <TITLE OF STEP>
497
498 // These all must occur within a STEP
499
500 Context | <PLAIN TEXT STATEMENTS FOR CONTEXT>
501 Command | <VERBATIM COMPUTER CODE>
502 Warning | <BIG BOLD TEXT TO CAPTURE ATTENTION>
503 Image | <FILENAME> | <CAPTION TEXT>
504 Resource | <THING YOU NEED TO COMPLETE THIS STEP>
505 Objective | <ACHIEVED PURPOSE OF THIS PROCESS>
506 Out of Scope | <NOTE ABOUT WHAT NOT TO DO HERE>
507
508 // Verification methods can be single letter: A, I, D, T, S
509 // They can also be full words: Analysis, Inspection, Demonstration, Test, Sampling
510 Verification | <REQUIREMENT ID> | <FULL TEXT OF REQUIREMENT> | <VERIFICATION METHOD>
511
512 // Action lines can be grouped together to form consecutive table rows
513 // To optionally include Two-Party Verification, <TPV> can be \"TPV\"
514 // To NOT make an action Two-Party Verified, stop at the <EXPECTED RESULT>
515 Action | <THING TO DO> | <EXPECTED RESULT> | <TPV?> - optional
516 Action | <THING TO DO> | <EXPECTED RESULT> | <TPV?> - optional
517
518For rendered examples, see https://stanhope.strativusgroup.com/latest/stylesheetviewer.html");
519 }
520
521}