1use crate::constants::{APPLICATION, ORGANIZATION, QUALIFIER};
5use bat::PrettyPrinter;
6use bon::Builder;
7use comfy_table::modifiers::UTF8_ROUND_CORNERS;
8use comfy_table::presets::UTF8_FULL;
9use comfy_table::*;
10use console::Emoji;
11use data_encoding::HEXUPPER;
12use derive_more::Display;
13use directories::ProjectDirs;
14use duct::cmd;
15use fancy_regex::Regex;
16use glob::glob;
17use is_executable::IsExecutable;
18use nanoid::nanoid;
19use owo_colors::{OwoColorize, Style, Styled};
20use ring::digest::{Context, SHA256};
21use rust_embed::Embed;
22use serde::{Deserialize, Serialize};
23use similar::{
24 ChangeTag::{self, Delete, Equal, Insert},
25 TextDiff,
26};
27use std::fs::create_dir_all;
28use std::fs::File;
29use std::io::{copy, BufReader, Cursor, Read, Write};
30use std::path::{Path, PathBuf};
31use tracing::{debug, error, warn};
32use which::which;
33
34#[cfg(feature = "cli")]
35pub mod cli;
36
37#[derive(Clone, Debug, Display, PartialEq)]
43pub enum MimeType {
44 #[display("text/csv")]
46 Csv,
47 #[display("application/ld+json")]
51 LdJson,
52 #[display("image/jpeg")]
54 Jpeg,
55 #[display("application/json")]
59 Json,
60 #[display("font/otf")]
62 Otf,
63 #[display("image/png")]
65 Png,
66 #[display("image/svg+xml")]
68 Svg,
69 #[display("text/plain")]
73 Text,
74}
75#[derive(Clone, Copy, Debug, Display)]
79pub enum ProgrammingLanguage {
80 #[display("html")]
82 Html,
83 #[display("markdown")]
87 Markdown,
88 #[display("json")]
92 Json,
93 #[display("yaml")]
97 Yaml,
98}
99#[derive(Embed)]
103#[folder = "assets/constants/"]
104pub struct Constant;
105pub struct Label {}
117#[derive(Builder, Clone, Copy, Debug, Deserialize, Display, Serialize)]
121#[builder(start_fn = init)]
122#[display("{}.{}.{}", major, minor, patch)]
123pub struct SemanticVersion {
124 #[builder(default = 0)]
126 pub major: u32,
127 #[builder(default = 0)]
129 pub minor: u32,
130 #[builder(default = 0)]
132 pub patch: u32,
133}
134impl Label {
135 pub const CAUTION: Emoji<'_, '_> = Emoji("⚠️ ", "!!! ");
137 pub const CHECKMARK: Emoji<'_, '_> = Emoji("✅ ", "☑ ");
139 pub const PROGRESS_BAR_TEMPLATE: &str = " {spinner:.green}{pos:>5} of{len:^5}[{bar:40.green}] {msg}";
143 pub fn dry_run() -> Styled<&'static &'static str> {
145 let style = Style::new().black().on_yellow();
146 " DRY_RUN ■ ".style(style)
147 }
148 pub fn invalid() -> String {
150 Label::fmt_invalid("INVALID")
151 }
152 pub fn fmt_invalid(value: &str) -> String {
154 let style = Style::new().red().on_default_color();
155 value.style(style).to_string()
156 }
157 pub fn fail() -> String {
159 Label::fmt_fail("FAIL")
160 }
161 pub fn fmt_fail(value: &str) -> String {
163 let style = Style::new().white().on_red();
164 format!(" ✗ {value} ").style(style).to_string()
165 }
166 pub fn found() -> Styled<&'static &'static str> {
168 let style = Style::new().green().on_default_color();
169 "FOUND".style(style)
170 }
171 pub fn not_found() -> String {
173 Label::fmt_not_found("NOT_FOUND")
174 }
175 pub fn fmt_not_found(value: &str) -> String {
177 let style = Style::new().red().on_default_color();
178 value.style(style).to_string()
179 }
180 pub fn output() -> String {
182 Label::fmt_output("OUTPUT")
183 }
184 pub fn fmt_output(value: &str) -> String {
186 let style = Style::new().cyan().dimmed().on_default_color();
187 value.style(style).to_string()
188 }
189 pub fn pass() -> String {
191 Label::fmt_pass("SUCCESS")
192 }
193 pub fn fmt_pass(value: &str) -> String {
195 let style = Style::new().green().bold().on_default_color();
196 format!("{}{}", Label::CHECKMARK, value).style(style).to_string()
197 }
198 pub fn read() -> Styled<&'static &'static str> {
200 let style = Style::new().green().on_default_color();
201 "READ".style(style)
202 }
203 pub fn skip() -> String {
205 Label::fmt_skip("SKIP")
206 }
207 pub fn fmt_skip(value: &str) -> String {
209 let style = Style::new().yellow().on_default_color();
210 format!("{}{} ", Label::CAUTION, value).style(style).to_string()
211 }
212 pub fn using() -> String {
214 Label::fmt_using("USING")
215 }
216 pub fn fmt_using(value: &str) -> String {
218 let style = Style::new().cyan();
219 value.style(style).to_string()
220 }
221}
222impl Constant {
223 pub fn from_asset(file_name: &str) -> String {
229 match Constant::get(file_name) {
230 | Some(value) => String::from_utf8_lossy(value.data.as_ref()).into(),
231 | None => {
232 error!(file_name, "=> {} Import Constant asset", Label::fail());
233 panic!("Unable to import {file_name}")
234 }
235 }
236 }
237 pub fn last_values(file_name: &str) -> impl Iterator<Item = String> {
241 Constant::csv(file_name)
242 .into_iter()
243 .map(|x| match x.last() {
244 | Some(value) => value.to_string(),
245 | None => "".to_string(),
246 })
247 .filter(|x| !x.is_empty())
248 }
249 pub fn read_lines(file_name: &str) -> Vec<String> {
255 let data = Constant::from_asset(file_name);
256 data.lines().map(String::from).collect()
257 }
258 pub fn csv(file_name: &str) -> Vec<Vec<String>> {
269 Constant::read_lines(format!("{file_name}.csv").as_str())
270 .into_iter()
271 .map(|x| x.split(",").map(String::from).collect())
272 .collect()
273 }
274}
275impl MimeType {
276 pub fn from_path<S>(file_name: S) -> MimeType
294 where
295 S: Into<String>,
296 {
297 let name = &file_name.into().to_lowercase();
298 match extension(Path::new(name)).as_str() {
299 | "csv" => MimeType::Csv,
300 | "jpg" | "jpeg" => MimeType::Jpeg,
301 | "json" => MimeType::Json,
302 | "jsonld" => MimeType::LdJson,
303 | "otf" => MimeType::Otf,
304 | "png" => MimeType::Png,
305 | "svg" => MimeType::Svg,
306 | "txt" => MimeType::Text,
307 | _ => unimplemented!("Unsupported MIME type"),
308 }
309 }
310}
311impl SemanticVersion {
312 pub fn from_command<S>(name: S) -> Option<SemanticVersion>
317 where
318 S: Into<String> + duct::IntoExecutablePath + std::marker::Copy,
319 {
320 if command_exists(name.into()) {
321 let result = cmd(name, vec!["--version"]).read();
322 match result {
323 | Ok(value) => {
324 let first_line = value.lines().collect::<Vec<_>>().first().cloned();
325 match first_line {
326 | Some(line) => Some(SemanticVersion::from_string(line)),
327 | None => None,
328 }
329 }
330 | Err(_) => None,
331 }
332 } else {
333 None
334 }
335 }
336 pub fn from_string<S>(value: S) -> SemanticVersion
338 where
339 S: Into<String>,
340 {
341 let value = match Regex::new(r"\d*[.]\d*[.]\d*") {
342 | Ok(re) => match re.find(&value.into()) {
343 | Ok(value) => match value {
344 | Some(value) => value.as_str().to_string(),
345 | None => unreachable!(),
346 },
347 | Err(_) => unreachable!(),
348 },
349 | Err(_) => unreachable!(),
350 };
351 let mut parts = value.split('.');
352 let major = parts.next().unwrap().parse::<u32>().unwrap();
353 let minor = parts.next().unwrap().parse::<u32>().unwrap();
354 let patch = parts.next().unwrap().parse::<u32>().unwrap();
355 SemanticVersion { major, minor, patch }
356 }
357}
358impl Default for SemanticVersion {
359 fn default() -> Self {
360 SemanticVersion::init().build()
361 }
362}
363pub fn checksum<P>(path: P) -> String
367where
368 P: Into<PathBuf>,
369{
370 let value = path.into();
371 match File::open(value.clone()) {
372 | Ok(file) => {
373 let mut buffer = [0; 1024];
374 let mut context = Context::new(&SHA256);
375 let mut reader = BufReader::new(file);
376 loop {
377 let count = reader.read(&mut buffer).unwrap();
378 if count == 0 {
379 break;
380 }
381 context.update(&buffer[..count]);
382 }
383 let digest = context.finish();
384 let result = HEXUPPER.encode(digest.as_ref());
385 result.to_lowercase()
386 }
387 | Err(err) => {
388 error!(error = err.to_string(), path = path_to_string(value), "=> {} Read file", Label::fail());
389 "".to_string()
390 }
391 }
392}
393pub fn command_exists<S>(name: S) -> bool
403where
404 S: Into<String> + AsRef<std::ffi::OsStr> + tracing::Value,
405{
406 match which(&name) {
407 | Ok(value) => {
408 let path = path_to_string(value.clone());
409 match value.try_exists() {
410 | Ok(true) => {
411 debug!(path, "=> {} Command", Label::found());
412 true
413 }
414 | _ => {
415 debug!(path, "=> {} Command", Label::not_found());
416 false
417 }
418 }
419 }
420 | Err(_) => {
421 warn!(name, "=> {} Command", Label::not_found());
422 false
423 }
424 }
425}
426pub fn download_binary<S, P>(url: S, destination: P) -> Result<PathBuf, String>
437where
438 S: Into<String> + Clone + std::marker::Copy,
439 P: Into<PathBuf> + Clone,
440{
441 async fn download<P>(url: String, destination: P) -> Result<(), String>
442 where
443 P: Into<PathBuf>,
444 {
445 let client = reqwest::Client::new();
446 let response = client.get(url.clone()).send();
447 let filename = PathBuf::from(url.clone()).file_name().unwrap().to_str().unwrap().to_string();
448 match response.await {
449 | Ok(data) => match data.bytes().await {
450 | Ok(content) => {
451 let mut output = File::create(destination.into().join(filename.clone())).unwrap();
452 let _ = copy(&mut Cursor::new(content.clone()), &mut output);
453 debug!(filename = filename, "=> {} Downloaded", Label::output());
454 Ok(())
455 }
456 | Err(_) => Err(format!("No content downloaded from {url}")),
457 },
458 | Err(_) => Err(format!("Failed to download {url}")),
459 }
460 }
461 let runtime = tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap();
462 let _ = runtime.block_on(download(url.into(), destination.clone()));
463 let filename = PathBuf::from(url.into()).file_name().unwrap().to_str().unwrap().to_string();
464 Ok(destination.into().join(filename))
465}
466pub fn extension(path: &Path) -> String {
479 path.extension().unwrap_or_default().to_str().unwrap_or_default().to_string()
480}
481pub fn files_all(path: PathBuf, extension: Option<&str>, ignore: Option<String>) -> Vec<PathBuf> {
493 fn paths_to_vec(paths: glob::Paths) -> Vec<PathBuf> {
494 paths.collect::<Vec<_>>().into_iter().filter_map(|x| x.ok()).collect::<Vec<_>>()
495 }
496 if path.is_dir() {
497 let pattern = match extension {
498 | Some(value) => {
499 let ext = &value.to_lowercase();
500 format!("{}/**/*.{}", path_to_string(path), ext)
501 }
502 | None => {
503 format!("{}/**/*", path_to_string(path))
504 }
505 };
506 debug!(pattern, "=> {}", Label::using());
507 let paths = glob(&pattern);
508 match ignore {
509 | Some(ignore_pattern) => match Regex::new(&ignore_pattern) {
510 | Ok(re) => match paths {
511 | Ok(values) => paths_to_vec(values)
512 .into_iter()
513 .filter(|path| !re.is_match(&path_to_string(path.to_path_buf())).unwrap())
514 .collect(),
515 | Err(why) => {
516 error!("=> {} Get all files - {why}", Label::fail());
517 vec![]
518 }
519 },
520 | Err(why) => {
521 error!("=> {} Get all files (Regex) - {why}", Label::fail());
522 vec![]
523 }
524 },
525 | None => match paths {
526 | Ok(values) => paths_to_vec(values),
527 | Err(why) => {
528 error!("=> {} Get all files - {why}", Label::fail());
529 vec![]
530 }
531 },
532 }
533 } else {
534 if extension.is_some() {
535 warn!(
536 path = path_to_string(path.clone()),
537 "=> {} Extension passed with single file to files_all() - please make sure this is desired",
538 Label::using()
539 );
540 }
541 vec![path]
542 }
543}
544pub fn files_from_git_branch(value: &str, extension: Option<&str>) -> Vec<PathBuf> {
551 if command_exists("git".to_owned()) {
552 let default_branch = match git_default_branch_name() {
553 | Some(value) => value,
554 | None => "main".to_string(),
555 };
556 let args = vec!["diff", "--name-only", &default_branch, "--merge-base", value];
557 let result = cmd("git", args).read();
558 filter_git_command_result(result, extension)
559 } else {
560 vec![]
561 }
562}
563pub fn files_from_git_commit(value: &str, extension: Option<&str>) -> Vec<PathBuf> {
570 if command_exists("git".to_owned()) {
571 let args = vec!["diff-tree", "--no-commit-id", "--name-only", "-r", value];
572 let result = cmd("git", args).read();
573 filter_git_command_result(result, extension)
574 } else {
575 vec![]
576 }
577}
578fn filter_git_command_result(result: Result<String, std::io::Error>, file_extension: Option<&str>) -> Vec<PathBuf> {
579 match result {
580 | Ok(value) => match file_extension {
581 | Some(ext) => value
582 .to_lowercase()
583 .split("\n")
584 .map(PathBuf::from)
585 .filter(|x| extension(Path::new(x)) == ext)
586 .collect::<Vec<_>>(),
587 | None => value.to_lowercase().split("\n").map(PathBuf::from).collect::<Vec<_>>(),
588 },
589 | Err(_) => vec![],
590 }
591}
592pub fn generate_guid() -> String {
602 let alphabet = [
603 '-', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'T', 'U', 'V', 'W', 'X', 'Y', 'a', 'b', 'c', 'd',
604 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'm', 'n', 'p', 'q', 'r', 't', 'w', 'x', 'y', 'z', '3', '4', '6', '7', '8', '9',
605 ];
606 let id = nanoid!(10, &alphabet);
607 debug!(id, "=> {}", Label::using());
608 id
609}
610pub fn git_branch_name() -> Option<String> {
616 if command_exists("git".to_owned()) {
617 let args = vec!["symbolic-ref", "--short", "HEAD"];
618 let result = cmd("git", args).read();
619 match result {
620 | Ok(ref value) => {
621 let name = match value.clone().split("/").last() {
622 | Some(x) => Some(x.to_string()),
623 | None => None,
624 };
625 name
626 }
627 | Err(_) => None,
628 }
629 } else {
630 None
631 }
632}
633pub fn git_default_branch_name() -> Option<String> {
639 if command_exists("git".to_owned()) {
640 let args = vec!["symbolic-ref", "refs/remotes/origin/HEAD", "--short"];
641 let result = cmd("git", args).read();
642 match result {
643 | Ok(ref value) => {
644 let name = match value.clone().split("/").last() {
645 | Some(x) => Some(x.to_string()),
646 | None => None,
647 };
648 name
649 }
650 | Err(_) => None,
651 }
652 } else {
653 None
654 }
655}
656pub fn image_paths<P>(root: P) -> Vec<PathBuf>
668where
669 P: Into<PathBuf> + Clone,
670{
671 let extensions = ["jpg", "jpeg", "png", "svg"];
672 let mut files = extensions
673 .iter()
674 .flat_map(|ext| glob(&format!("{}/**/*.{}", root.clone().into().display(), ext)))
675 .flat_map(|paths| paths.collect::<Vec<_>>())
676 .flatten()
677 .collect::<Vec<PathBuf>>();
678 files.sort();
679 files
680}
681#[cfg(any(unix, target_os = "wasi", target_os = "redox"))]
699pub fn make_executable<P>(path: P) -> bool
700where
701 P: Into<PathBuf> + Clone,
702{
703 use std::os::unix::fs::PermissionsExt;
704 std::fs::set_permissions(path.clone().into(), std::fs::Permissions::from_mode(0o755)).unwrap();
705 path.into().is_executable()
706}
707#[cfg(windows)]
723pub fn make_executable<P>(path: P) -> bool
724where
725 P: Into<PathBuf> + Clone,
726{
727 path.into().is_executable()
729}
730pub fn parent<P>(path: P) -> String
732where
733 P: Into<PathBuf>,
734{
735 PathBuf::from(path.into().parent().unwrap()).display().to_string()
736}
737pub fn path_to_string(path: PathBuf) -> String {
750 let result = match std::fs::canonicalize(path.as_path()) {
752 | Ok(value) => value,
753 | Err(_) => path,
754 };
755 result.to_str().unwrap().to_string()
756}
757pub fn pretty_print<I: IntoIterator<Item = usize>>(text: &str, syntax: ProgrammingLanguage, highlight: I) {
761 let input = format!("{text}\n");
762 let language = syntax.to_string();
763 let mut printer = PrettyPrinter::new();
764 printer
765 .input_from_bytes(input.as_bytes())
766 .theme("zenburn")
767 .language(&language)
768 .line_numbers(true);
769 for line in highlight {
770 printer.highlight(line);
771 }
772 printer.print().unwrap();
773}
774pub fn print_changes(old: &str, new: &str) {
781 let changes = text_diff_changes(old, new);
782 let has_no_changes = changes.clone().into_iter().all(|(tag, _)| tag == Equal);
783 if has_no_changes {
784 debug!("=> {}No format changes", Label::skip());
785 } else {
786 for change in changes {
787 print!("{}", change.1);
788 }
789 }
790}
791pub fn print_values_as_table(title: &str, headers: Vec<&str>, rows: Vec<Vec<String>>) {
800 let mut table = Table::new();
801 table
802 .load_preset(UTF8_FULL)
803 .apply_modifier(UTF8_ROUND_CORNERS)
804 .set_content_arrangement(ContentArrangement::Dynamic)
805 .set_header(headers);
806 rows.into_iter().for_each(|row| {
807 table.add_row(row);
808 });
809 println!("=> {} \n{table}", title.green().bold());
810}
811pub fn read_file<P>(path: P) -> Result<String, std::io::Error>
822where
823 P: Into<PathBuf> + Clone,
824{
825 let mut content = String::new();
826 let _ = match File::open(path.clone().into()) {
827 | Ok(mut file) => {
828 debug!(path = path_to_string(path.into()), "=> {}", Label::read());
829 file.read_to_string(&mut content)
830 }
831 | Err(why) => {
832 error!(path = path_to_string(path.into()), "=> {} Cannot read file", Label::fail());
833 Err(why)
834 }
835 };
836 Ok(content)
837}
838pub fn standard_project_folder(namespace: &str, default: Option<PathBuf>) -> PathBuf {
854 let root = match default {
855 | Some(value) => value,
856 | None => match ProjectDirs::from(QUALIFIER, ORGANIZATION, APPLICATION) {
857 | Some(dirs) => dirs.cache_dir().join(namespace).to_path_buf(),
858 | None => PathBuf::from(format!("./{namespace}")),
859 },
860 };
861 match create_dir_all(root.clone()) {
862 | Ok(_) => {}
863 | Err(why) => error!(directory = path_to_string(root.clone()), "=> {} Create - {}", Label::fail(), why),
864 };
865 root.join(generate_guid())
866}
867pub fn suffix(value: usize) -> String {
869 (if value == 1 { "" } else { "s" }).to_string()
870}
871pub fn text_diff_changes(old: &str, new: &str) -> Vec<(ChangeTag, String)> {
888 TextDiff::from_lines(old, new)
889 .iter_all_changes()
890 .map(|line| {
891 let tag = line.tag();
892 let text = match tag {
893 | Delete => format!("- {line}").red().to_string(),
894 | Insert => format!("+ {line}").green().to_string(),
895 | Equal => format!(" {line}").dimmed().to_string(),
896 };
897 (tag, text)
898 })
899 .collect::<Vec<_>>()
900}
901pub fn tokio_runtime() -> tokio::runtime::Runtime {
903 debug!("=> {} Tokio runtime", Label::using());
904 tokio::runtime::Builder::new_multi_thread().enable_all().build().unwrap()
905}
906pub fn to_string(values: Vec<&str>) -> Vec<String> {
908 values.iter().map(|s| s.to_string()).collect()
909}
910pub fn write_file<P>(path: P, content: String) -> Result<(), std::io::Error>
922where
923 P: Into<PathBuf>,
924{
925 match File::create(path.into().clone()) {
926 | Ok(mut file) => {
927 file.write_all(content.as_bytes()).unwrap();
928 file.flush()
929 }
930 | Err(why) => {
931 error!("=> {} Cannot create file - {why}", Label::fail());
932 Err(why)
933 }
934 }
935}
936
937#[cfg(test)]
938mod tests;