1use crate::constants::{APPLICATION, ORGANIZATION, QUALIFIER};
2use bat::PrettyPrinter;
3use bon::Builder;
4use comfy_table::modifiers::UTF8_ROUND_CORNERS;
5use comfy_table::presets::UTF8_FULL;
6use comfy_table::*;
7use console::Emoji;
8use data_encoding::HEXUPPER;
9use derive_more::Display;
10use directories::ProjectDirs;
11use duct::cmd;
12use fancy_regex::Regex;
13use glob::glob;
14use is_executable::IsExecutable;
15use nanoid::nanoid;
16use owo_colors::{OwoColorize, Style, Styled};
17use ring::digest::{Context, SHA256};
18use rust_embed::Embed;
19use serde::{Deserialize, Serialize};
20use similar::{
21 ChangeTag::{self, Delete, Equal, Insert},
22 TextDiff,
23};
24use std::error::Error;
25use std::fs::create_dir_all;
26use std::fs::File;
27use std::io::copy;
28use std::io::Cursor;
29use std::io::{BufReader, Read};
30use std::path::{Path, PathBuf};
31use tracing::{debug, error, warn};
32use which::which;
33
34#[derive(Clone, Debug, Display)]
36pub enum MimeType {
37 #[display("text/csv")]
38 Csv,
39 #[display("application/ld+json")]
40 LdJson,
41 #[display("image/jpeg")]
42 Jpeg,
43 #[display("application/json")]
44 Json,
45 #[display("font/otf")]
46 Otf,
47 #[display("image/png")]
48 Png,
49 #[display("image/svg+xml")]
50 Svg,
51 #[display("text/plain")]
52 Text,
53}
54#[derive(Clone, Copy, Debug, Display)]
55pub enum ProgrammingLanguage {
56 #[display("html")]
57 Html,
58 #[display("markdown")]
59 Markdown,
60 #[display("json")]
61 Json,
62 #[display("yaml")]
63 Yaml,
64}
65#[derive(Embed)]
66#[folder = "assets/constants/"]
67pub struct Constant;
68pub struct Label {}
70#[derive(Builder, Clone, Copy, Debug, Deserialize, Display, Serialize)]
74#[builder(start_fn = init)]
75#[display("{}.{}.{}", major, minor, patch)]
76pub struct SemanticVersion {
77 #[builder(default = 0)]
79 pub major: u32,
80 #[builder(default = 0)]
82 pub minor: u32,
83 #[builder(default = 0)]
85 pub patch: u32,
86}
87impl Label {
88 pub const CAUTION: Emoji<'_, '_> = Emoji("⚠️ ", "!!! ");
89 pub const CHECKMARK: Emoji<'_, '_> = Emoji("✅ ", "☑ ");
90 pub const PROGRESS_BAR_TEMPLATE: &str = " {spinner:.green}{pos:>5} of{len:^5}[{bar:40.green}] {msg}";
91 pub fn dry_run() -> Styled<&'static &'static str> {
92 let style = Style::new().black().on_yellow();
93 " DRY_RUN ■ ".style(style)
94 }
95 pub fn invalid() -> String {
96 Label::fmt_invalid("INVALID")
97 }
98 pub fn fmt_invalid(value: &str) -> String {
99 let style = Style::new().red().on_default_color();
100 value.style(style).to_string()
101 }
102 pub fn fail() -> String {
103 Label::fmt_fail("FAIL")
104 }
105 pub fn fmt_fail(value: &str) -> String {
106 let style = Style::new().white().on_red();
107 format!(" ✗ {} ", value).style(style).to_string()
108 }
109 pub fn found() -> Styled<&'static &'static str> {
110 let style = Style::new().green().on_default_color();
111 "FOUND".style(style)
112 }
113 pub fn not_found() -> String {
114 Label::fmt_not_found("NOT_FOUND")
115 }
116 pub fn fmt_not_found(value: &str) -> String {
117 let style = Style::new().red().on_default_color();
118 value.style(style).to_string()
119 }
120 pub fn output() -> String {
121 Label::fmt_output("OUTPUT")
122 }
123 pub fn fmt_output(value: &str) -> String {
124 let style = Style::new().cyan().dimmed().on_default_color();
125 value.style(style).to_string()
126 }
127 pub fn pass() -> String {
128 Label::fmt_pass("SUCCESS")
129 }
130 pub fn fmt_pass(value: &str) -> String {
131 let style = Style::new().green().bold().on_default_color();
132 format!("{}{}", Label::CHECKMARK, value).style(style).to_string()
133 }
134 pub fn read() -> Styled<&'static &'static str> {
135 let style = Style::new().green().on_default_color();
136 "READ".style(style)
137 }
138 pub fn skip() -> String {
139 Label::fmt_skip("SKIP")
140 }
141 pub fn fmt_skip(value: &str) -> String {
142 let style = Style::new().yellow().on_default_color();
143 format!("{}{} ", Label::CAUTION, value).style(style).to_string()
144 }
145 pub fn using() -> String {
146 Label::fmt_using("USING")
147 }
148 pub fn fmt_using(value: &str) -> String {
149 let style = Style::new().cyan();
150 value.style(style).to_string()
151 }
152}
153impl Constant {
154 pub fn from_asset(file_name: &str) -> String {
155 match Constant::get(file_name) {
156 | Some(value) => String::from_utf8_lossy(value.data.as_ref()).into(),
157 | None => {
158 error!(file_name, "=> {} Import Constant asset", Label::fail());
159 panic!("Unable to import {}", file_name)
160 }
161 }
162 }
163 pub fn get_last_values(file_name: &str) -> impl Iterator<Item = String> {
164 Constant::csv(file_name)
165 .into_iter()
166 .map(|x| match x.last() {
167 | Some(value) => value.to_string(),
168 | None => "".to_string(),
169 })
170 .filter(|x| !x.is_empty())
171 }
172 pub fn read_lines(file_name: &str) -> Vec<String> {
173 let data = Constant::from_asset(file_name);
174 data.lines().map(String::from).collect()
175 }
176 pub fn csv(file_name: &str) -> Vec<Vec<String>> {
177 Constant::read_lines(format!("{}.csv", file_name).as_str())
178 .into_iter()
179 .map(|x| x.split(",").map(String::from).collect())
180 .collect()
181 }
182}
183impl MimeType {
184 pub fn from_path(file_name: &str) -> MimeType {
185 let name = &file_name.to_lowercase();
186 match get_extension(Path::new(name)).as_str() {
187 | "csv" => MimeType::Csv,
188 | "jpg" | "jpeg" => MimeType::Jpeg,
189 | "json" => MimeType::Json,
190 | "jsonld" => MimeType::LdJson,
191 | "otf" => MimeType::Otf,
192 | "png" => MimeType::Png,
193 | "svg" => MimeType::Svg,
194 | "txt" => MimeType::Text,
195 | _ => unimplemented!("Unsupported MIME type"),
196 }
197 }
198}
199impl SemanticVersion {
200 pub fn from_command(name: &str) -> Option<SemanticVersion> {
201 if test_command(name.to_owned()) {
202 let result = cmd(name, vec!["--version"]).read();
203 match result {
204 | Ok(value) => {
205 let first_line = value.lines().collect::<Vec<_>>().first().cloned();
206 match first_line {
207 | Some(line) => Some(SemanticVersion::from_string(line)),
208 | None => None,
209 }
210 }
211 | Err(_) => None,
212 }
213 } else {
214 None
215 }
216 }
217 pub fn from_string(value: &str) -> SemanticVersion {
218 let value = match Regex::new(r"\d*[.]\d*[.]\d*") {
219 | Ok(re) => match re.find(value) {
220 | Ok(value) => match value {
221 | Some(value) => value.as_str().to_string(),
222 | None => unreachable!(),
223 },
224 | Err(_) => unreachable!(),
225 },
226 | Err(_) => unreachable!(),
227 };
228 let mut parts = value.split('.');
229 let major = parts.next().unwrap().parse::<u32>().unwrap();
230 let minor = parts.next().unwrap().parse::<u32>().unwrap();
231 let patch = parts.next().unwrap().parse::<u32>().unwrap();
232 SemanticVersion { major, minor, patch }
233 }
234}
235impl Default for SemanticVersion {
236 fn default() -> Self {
237 SemanticVersion::init().build()
238 }
239}
240pub fn download_binary(url: String, destination: PathBuf) -> Result<PathBuf, String> {
241 async fn download(url: String, destination: PathBuf) -> Result<(), String> {
242 let client = reqwest::Client::new();
243 let response = client.get(url.clone()).send();
244 let filename = PathBuf::from(url.clone()).file_name().unwrap().to_str().unwrap().to_string();
245 match response.await {
246 | Ok(data) => match data.bytes().await {
247 | Ok(content) => {
248 let mut output = File::create(destination.join(filename.clone())).unwrap();
249 let _ = copy(&mut Cursor::new(content.clone()), &mut output);
250 debug!(filename = filename, "=> {} Downloaded", Label::output());
251 Ok(())
252 }
253 | Err(_) => Err(format!("No content downloaded from {url}")),
254 },
255 | Err(_) => Err(format!("Failed to download {url}")),
256 }
257 }
258 let runtime = tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap();
259 let _ = runtime.block_on(download(url.clone(), destination.clone()));
260 let filename = PathBuf::from(url.clone()).file_name().unwrap().to_str().unwrap().to_string();
261 Ok(destination.join(filename))
262}
263pub fn generate_guid() -> String {
264 let alphabet = [
265 '-', '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',
266 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'm', 'n', 'p', 'q', 'r', 't', 'w', 'x', 'y', 'z', '3', '4', '6', '7', '8', '9',
267 ];
268 let id = nanoid!(10, &alphabet);
269 debug!(id, "=> {}", Label::using());
270 id
271}
272pub fn get_all_files(path: PathBuf, extension: Option<&str>, ignore: Option<String>) -> Vec<PathBuf> {
273 fn paths_to_vec(paths: glob::Paths) -> Vec<PathBuf> {
274 paths.collect::<Vec<_>>().into_iter().filter_map(|x| x.ok()).collect::<Vec<_>>()
275 }
276 if path.is_dir() {
277 let pattern = match extension {
278 | Some(value) => {
279 let ext = &value.to_lowercase();
280 format!("{}/**/*.{}", path_to_string(path), ext)
281 }
282 | None => {
283 format!("{}/**/*", path_to_string(path))
284 }
285 };
286 debug!(pattern, "=> {}", Label::using());
287 let paths = glob(&pattern);
288 match ignore {
289 | Some(ignore_pattern) => match Regex::new(&ignore_pattern) {
290 | Ok(re) => match paths {
291 | Ok(values) => paths_to_vec(values)
292 .into_iter()
293 .filter(|path| !re.is_match(&path_to_string(path.to_path_buf())).unwrap())
294 .collect(),
295 | Err(why) => {
296 error!("=> {} Get all files - {why}", Label::fail());
297 vec![]
298 }
299 },
300 | Err(why) => {
301 error!("=> {} get_all_files Regex - {why}", Label::fail());
302 vec![]
303 }
304 },
305 | None => match paths {
306 | Ok(values) => paths_to_vec(values),
307 | Err(why) => {
308 error!("=> {} Get all files - {why}", Label::fail());
309 vec![]
310 }
311 },
312 }
313 } else {
314 if extension.is_some() {
315 warn!(
316 path = path_to_string(path.clone()),
317 "=> {} Extension passed with single file to get_all_files() - please make sure this is desired",
318 Label::using()
319 );
320 }
321 vec![path]
322 }
323}
324pub fn get_changes(old: &str, new: &str) -> Vec<(ChangeTag, String)> {
325 TextDiff::from_lines(old, new)
326 .iter_all_changes()
327 .map(|line| {
328 let tag = line.tag();
329 let text = match tag {
330 | Delete => format!("- {}", line).red().to_string(),
331 | Insert => format!("+ {}", line).green().to_string(),
332 | Equal => format!(" {}", line).dimmed().to_string(),
333 };
334 (tag, text)
335 })
336 .collect::<Vec<_>>()
337}
338pub fn get_checksum(path: PathBuf) -> String {
341 match File::open(path.clone()) {
342 | Ok(file) => {
343 let mut buffer = [0; 1024];
344 let mut context = Context::new(&SHA256);
345 let mut reader = BufReader::new(file);
346 loop {
347 let count = reader.read(&mut buffer).unwrap();
348 if count == 0 {
349 break;
350 }
351 context.update(&buffer[..count]);
352 }
353 let digest = context.finish();
354 let result = HEXUPPER.encode(digest.as_ref());
355 result.to_lowercase()
356 }
357 | Err(err) => {
358 error!(error = err.to_string(), path = path_to_string(path), "=> {} Read file", Label::fail());
359 "".to_string()
360 }
361 }
362}
363pub fn get_command_folder(task: &str, default: Option<PathBuf>) -> PathBuf {
364 let root = match default {
365 | Some(value) => value,
366 | None => match ProjectDirs::from(QUALIFIER, ORGANIZATION, APPLICATION) {
367 | Some(dirs) => dirs.cache_dir().join(task).to_path_buf(),
368 | None => PathBuf::from(format!("./{}", task)),
369 },
370 };
371 match create_dir_all(root.clone()) {
372 | Ok(_) => {}
373 | Err(why) => error!(directory = path_to_string(root.clone()), "=> {} Create - {}", Label::fail(), why),
374 };
375 root.join(generate_guid())
376}
377pub fn get_extension(path: &Path) -> String {
390 path.extension().unwrap_or_default().to_str().unwrap_or_default().to_string()
391}
392fn filter_git_command_result(result: Result<String, std::io::Error>, extension: Option<&str>) -> Vec<PathBuf> {
393 match result {
394 | Ok(value) => match extension {
395 | Some(ext) => value
396 .to_lowercase()
397 .split("\n")
398 .map(PathBuf::from)
399 .filter(|x| get_extension(Path::new(x)) == ext)
400 .collect::<Vec<_>>(),
401 | None => value.to_lowercase().split("\n").map(PathBuf::from).collect::<Vec<_>>(),
402 },
403 | Err(_) => vec![],
404 }
405}
406pub fn get_files_from_git_branch(value: &str, extension: Option<&str>) -> Vec<PathBuf> {
407 if test_command("git".to_owned()) {
408 let default_branch = match get_git_default_branch_name() {
409 | Some(value) => value,
410 | None => "main".to_string(),
411 };
412 let args = vec!["diff", "--name-only", &default_branch, "--merge-base", value];
413 let result = cmd("git", args).read();
414 filter_git_command_result(result, extension)
415 } else {
416 vec![]
417 }
418}
419pub fn get_files_from_git_commit(value: &str, extension: Option<&str>) -> Vec<PathBuf> {
420 if test_command("git".to_owned()) {
421 let args = vec!["diff-tree", "--no-commit-id", "--name-only", "-r", value];
422 let result = cmd("git", args).read();
423 filter_git_command_result(result, extension)
424 } else {
425 vec![]
426 }
427}
428pub fn get_git_branch_name() -> Option<String> {
429 if test_command("git".to_owned()) {
430 let args = vec!["symbolic-ref", "--short", "HEAD"];
431 let result = cmd("git", args).read();
432 match result {
433 | Ok(ref value) => {
434 let name = match value.clone().split("/").last() {
435 | Some(x) => Some(x.to_string()),
436 | None => None,
437 };
438 name
439 }
440 | Err(_) => None,
441 }
442 } else {
443 None
444 }
445}
446pub fn get_git_default_branch_name() -> Option<String> {
447 if test_command("git".to_owned()) {
448 let args = vec!["symbolic-ref", "refs/remotes/origin/HEAD", "--short"];
449 let result = cmd("git", args).read();
450 match result {
451 | Ok(ref value) => {
452 let name = match value.clone().split("/").last() {
453 | Some(x) => Some(x.to_string()),
454 | None => None,
455 };
456 name
457 }
458 | Err(_) => None,
459 }
460 } else {
461 None
462 }
463}
464pub fn get_image_paths(root: PathBuf) -> Vec<PathBuf> {
465 let extensions = ["jpg", "jpeg", "png", "svg"];
466 let mut files = extensions
467 .iter()
468 .flat_map(|ext| glob(&format!("{}/**/*.{}", root.display(), ext)))
469 .flat_map(|paths| paths.collect::<Vec<_>>())
470 .flatten()
471 .collect::<Vec<PathBuf>>();
472 files.sort();
473 files
474}
475pub fn get_parent(path: String) -> String {
476 PathBuf::from(PathBuf::from(path).parent().unwrap()).display().to_string()
477}
478#[cfg(any(unix, target_os = "wasi", target_os = "redox"))]
479pub fn make_executable(path: &PathBuf) -> bool {
480 use std::os::unix::fs::PermissionsExt;
481 std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o755)).unwrap();
482 path.is_executable()
483}
484#[cfg(windows)]
485pub fn make_executable(path: &PathBuf) -> bool {
486 path.is_executable()
488}
489pub fn path_to_string(path: PathBuf) -> String {
490 let result = match std::fs::canonicalize(path.as_path()) {
492 | Ok(value) => value,
493 | Err(_) => path,
494 };
495 result.to_str().unwrap().to_string()
496}
497pub fn pretty_print(text: &str, syntax: ProgrammingLanguage, highlight: Vec<usize>) {
498 let input = format!("{}\n", text);
499 let language = syntax.to_string();
500 let mut printer = PrettyPrinter::new();
501 printer
502 .input_from_bytes(input.as_bytes())
503 .theme("zenburn")
504 .language(&language)
505 .line_numbers(true);
506 for line in highlight {
507 printer.highlight(line);
508 }
509 printer.print().unwrap();
510}
511pub fn print_changes(old: &str, new: &str) {
512 let changes = get_changes(old, new);
513 let has_no_changes = changes.clone().into_iter().all(|(tag, _)| tag == Equal);
514 if has_no_changes {
515 debug!("=> {}No format changes", Label::skip());
516 } else {
517 for change in changes {
518 print!("{}", change.1);
519 }
520 }
521}
522pub fn print_values_as_table(title: &str, headers: Vec<&str>, rows: Vec<Vec<String>>) {
523 let mut table = Table::new();
524 table
525 .load_preset(UTF8_FULL)
526 .apply_modifier(UTF8_ROUND_CORNERS)
527 .set_content_arrangement(ContentArrangement::Dynamic)
528 .set_header(headers);
529 rows.into_iter().for_each(|row| {
530 table.add_row(row);
531 });
532 println!("=> {} \n{table}", title.green().bold());
533}
534pub fn read_file(path: PathBuf) -> Result<String, Box<dyn Error>> {
535 let mut content = String::new();
536 let _ = match File::open(path.clone()) {
537 | Ok(mut file) => {
538 debug!(path = path.to_str().unwrap(), "=> {}", Label::read());
539 file.read_to_string(&mut content)
540 }
541 | Err(err) => {
542 error!(path = path.to_str().unwrap(), "=> {} Cannot read file", Label::fail());
543 Err(err)
544 }
545 };
546 Ok(content)
547}
548pub fn test_command(name: String) -> bool {
549 match which(&name) {
550 | Ok(value) => {
551 let path = path_to_string(value.clone());
552 match value.try_exists() {
553 | Ok(true) => {
554 debug!(path, "=> {} Command", Label::found());
555 true
556 }
557 | _ => {
558 debug!(path, "=> {} Command", Label::not_found());
559 false
560 }
561 }
562 }
563 | Err(_) => {
564 warn!(name, "=> {} Command", Label::not_found());
565 false
566 }
567 }
568}
569pub fn tokio_runtime() -> tokio::runtime::Runtime {
570 debug!("=> {} Tokio runtime", Label::using());
571 tokio::runtime::Builder::new_multi_thread().enable_all().build().unwrap()
572}
573pub fn to_string(values: Vec<&str>) -> Vec<String> {
574 values.iter().map(|s| s.to_string()).collect()
575}
576
577#[cfg(test)]
578mod tests;