1use crate::constants::{APPLICATION, ORGANIZATION, QUALIFIER};
6use bat::PrettyPrinter;
7use bon::Builder;
8use comfy_table::modifiers::UTF8_ROUND_CORNERS;
9use comfy_table::presets::UTF8_FULL;
10use comfy_table::*;
11use console::Emoji;
12use data_encoding::HEXUPPER;
13use derive_more::Display;
14use directories::ProjectDirs;
15use duct::cmd;
16use fancy_regex::Regex;
17use glob::glob;
18use is_executable::IsExecutable;
19use itertools::Itertools;
20use lychee_lib::{CacheStatus, Response, Status};
21use nanoid::nanoid;
22use owo_colors::{OwoColorize, Style, Styled};
23use ring::digest::{Context, SHA256};
24use rust_embed::Embed;
25use schemars::JsonSchema;
26use serde::{Deserialize, Serialize};
27use similar::{
28 ChangeTag::{self, Delete, Equal, Insert},
29 TextDiff,
30};
31use std::fs::create_dir_all;
32use std::fs::File;
33use std::io::{copy, BufReader, Cursor, Read, Write};
34use std::path::{Path, PathBuf};
35use titlecase::titlecase;
36use tracing::{debug, error, info, warn};
37use validator::ValidationErrorsKind;
38use which::which;
39
40pub mod citeas;
41#[cfg(feature = "cli")]
42pub mod cli;
43
44#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
48pub enum License {
49 Mit,
51 CreativeCommons,
53 Unknown,
55}
56#[derive(Clone, Debug, Display, PartialEq)]
62pub enum MimeType {
63 #[display("application/yaml")]
69 Cff,
70 #[display("text/csv")]
72 Csv,
73 #[display("application/ld+json")]
77 LdJson,
78 #[display("image/jpeg")]
80 Jpeg,
81 #[display("application/json")]
85 Json,
86 #[display("text/markdown")]
88 Markdown,
89 #[display("font/otf")]
91 Otf,
92 #[display("image/png")]
94 Png,
95 #[display("text/rust")]
97 Rust,
98 #[display("image/svg+xml")]
100 Svg,
101 #[display("text/plain")]
105 Text,
106 #[display("application/toml")]
110 Toml,
111 #[display("application/yaml")]
115 Yaml,
116 #[display("application/unknown")]
118 Unknown,
119}
120#[derive(Clone, Copy, Debug, Display)]
124pub enum ProgrammingLanguage {
125 #[display("html")]
127 Html,
128 #[display("markdown")]
132 Markdown,
133 #[display("json")]
137 Json,
138 #[display("yaml")]
142 Yaml,
143}
144#[derive(Embed)]
148#[folder = "assets/constants/"]
149pub struct Constant;
150pub struct Label {}
162#[derive(Builder, Clone, Debug, Display)]
164#[builder(start_fn = init)]
165#[display("{message}")]
166pub struct LinkCheck {
167 #[builder(default = false)]
169 pub success: bool,
170 pub code: Option<String>,
172 pub url: Option<String>,
175 pub message: String,
177}
178#[derive(Builder, Clone, Debug)]
180#[builder(start_fn = init)]
181pub struct SchemaCheck {
182 #[builder(default = false)]
184 pub success: bool,
185 pub errors: Option<ValidationErrorsKind>,
187 pub path: Option<PathBuf>,
190 pub message: String,
192}
193#[derive(Builder, Clone, Copy, Debug, Deserialize, Display, Serialize, JsonSchema)]
197#[builder(start_fn = init)]
198#[display("{}.{}.{}", major, minor, patch)]
199pub struct SemanticVersion {
200 #[builder(default = 0)]
202 pub major: u32,
203 #[builder(default = 0)]
205 pub minor: u32,
206 #[builder(default = 0)]
208 pub patch: u32,
209}
210impl Constant {
211 pub fn from_asset(file_name: &str) -> String {
217 match Constant::get(file_name) {
218 | Some(value) => String::from_utf8_lossy(value.data.as_ref()).into(),
219 | None => {
220 error!(file_name, "=> {} Import Constant asset", Label::fail());
221 panic!("Unable to import {file_name}")
222 }
223 }
224 }
225 pub fn last_values(file_name: &str) -> impl Iterator<Item = String> {
229 Constant::csv(file_name)
230 .into_iter()
231 .map(|x| match x.last() {
232 | Some(value) => value.to_string(),
233 | None => "".to_string(),
234 })
235 .filter(|x| !x.is_empty())
236 }
237 pub fn read_lines(file_name: &str) -> Vec<String> {
243 let data = Constant::from_asset(file_name);
244 data.lines().map(String::from).collect()
245 }
246 pub fn csv(file_name: &str) -> Vec<Vec<String>> {
257 Constant::read_lines(format!("{file_name}.csv").as_str())
258 .into_iter()
259 .map(|x| x.split(",").map(String::from).collect())
260 .collect()
261 }
262}
263impl Label {
264 pub const CAUTION: Emoji<'_, '_> = Emoji("⚠️ ", "!!! ");
266 pub const CHECKMARK: Emoji<'_, '_> = Emoji("✅ ", "☑ ");
268 pub const PROGRESS_BAR_TEMPLATE: &str = " {spinner:.green}{pos:>5} of{len:^5}[{bar:40.green}] {msg}";
272 pub fn dry_run() -> Styled<&'static &'static str> {
274 let style = Style::new().black().on_yellow();
275 " DRY_RUN ■ ".style(style)
276 }
277 pub fn invalid() -> String {
279 Label::fmt_invalid(" ✗ INVALID")
280 }
281 pub fn fmt_invalid(value: &str) -> String {
283 let style = Style::new().red().on_default_color();
284 value.style(style).to_string()
285 }
286 pub fn valid() -> String {
288 Label::fmt_valid(" ✓ VALID ")
289 }
290 pub fn fmt_valid(value: &str) -> String {
292 let style = Style::new().green().on_default_color();
293 value.style(style).to_string()
294 }
295 pub fn fail() -> String {
297 Label::fmt_fail("FAIL")
298 }
299 pub fn fmt_fail(value: &str) -> String {
301 let style = Style::new().white().on_red();
302 format!(" ✗ {value} ").style(style).to_string()
303 }
304 pub fn found() -> Styled<&'static &'static str> {
306 let style = Style::new().green().on_default_color();
307 "FOUND".style(style)
308 }
309 pub fn not_found() -> String {
311 Label::fmt_not_found("NOT_FOUND")
312 }
313 pub fn fmt_not_found(value: &str) -> String {
315 let style = Style::new().red().on_default_color();
316 value.style(style).to_string()
317 }
318 pub fn output() -> String {
320 Label::fmt_output("OUTPUT")
321 }
322 pub fn fmt_output(value: &str) -> String {
324 let style = Style::new().cyan().dimmed().on_default_color();
325 value.style(style).to_string()
326 }
327 pub fn pass() -> String {
329 Label::fmt_pass("SUCCESS")
330 }
331 pub fn fmt_pass(value: &str) -> String {
333 let style = Style::new().green().bold().on_default_color();
334 format!("{}{}", Label::CHECKMARK, value).style(style).to_string()
335 }
336 pub fn read() -> Styled<&'static &'static str> {
338 let style = Style::new().green().on_default_color();
339 "READ".style(style)
340 }
341 pub fn skip() -> String {
343 Label::fmt_skip("SKIP")
344 }
345 pub fn fmt_skip(value: &str) -> String {
347 let style = Style::new().yellow().on_default_color();
348 format!("{}{} ", Label::CAUTION, value).style(style).to_string()
349 }
350 pub fn using() -> String {
352 Label::fmt_using("USING")
353 }
354 pub fn fmt_using(value: &str) -> String {
356 let style = Style::new().cyan();
357 value.style(style).to_string()
358 }
359}
360impl LinkCheck {
361 pub fn from_lychee(response: Response) -> Self {
363 match response.status() {
364 | Status::Ok(code) | Status::Redirected(code) => LinkCheck::init()
365 .success(true)
366 .code(code.to_string())
367 .message("has no HTTP errors".to_string())
368 .build(),
369 | Status::Cached(status) => match status {
370 | CacheStatus::Ok(code) => LinkCheck::init()
371 .success(true)
372 .code(code.to_string())
373 .message("has no HTTP errors".to_string())
374 .build(),
375 | CacheStatus::Error(Some(code)) => LinkCheck::init()
376 .success(false)
377 .code(code.to_string())
378 .message("has cached HTTP errors".to_string())
379 .build(),
380 | CacheStatus::Unsupported => LinkCheck::init()
381 .success(false)
382 .message("unsupported cached response".to_string())
383 .build(),
384 | _ => LinkCheck::init()
385 .success(true)
386 .message("ignored or otherwise successful (cached response)".to_string())
387 .build(),
388 },
389 | Status::Error(code) => LinkCheck::init()
390 .success(false)
391 .code(code.to_string())
392 .message("has HTTP errors".to_string())
393 .build(),
394 | Status::Unsupported(why) => LinkCheck::init()
395 .success(false)
396 .message(format!("unsupported HTTP response - {why}"))
397 .build(),
398 | Status::UnknownStatusCode(code) => LinkCheck::init()
399 .success(false)
400 .code(code.to_string())
401 .message("unknown HTTP response".to_string())
402 .build(),
403 | Status::Timeout(_) => LinkCheck::init().success(false).message("HTTP timeout".to_string()).build(),
404 | _ => LinkCheck::init()
405 .success(true)
406 .message("ignored or otherwise successful".to_string())
407 .build(),
408 }
409 }
410 pub async fn run(url: Option<String>) -> LinkCheck {
412 match url {
413 | Some(url) => {
414 let response = lychee_lib::check(url.as_str()).await;
415 match response {
416 | Ok(response) => LinkCheck::from_lychee(response).with_url(url),
417 | Err(_) => LinkCheck::init().success(false).url(url).message("unreachable".to_string()).build(),
418 }
419 }
420 | None => LinkCheck::init().success(false).message("missing URL".to_string()).build(),
421 }
422 }
423 pub fn print(self) {
425 let code = match self.code {
426 | Some(code) => format!(" ({code})").dimmed().to_string(),
427 | None => "".to_string(),
428 };
429 let url = match self.url {
430 | Some(url) => url.underline().italic().to_string(),
431 | None => "Missing".italic().to_string(),
432 };
433 if self.success {
434 let message = titlecase(&self.message).green().bold().to_string();
435 info!("=> {} \"{url}\" {message}{code}", Label::valid());
436 } else {
437 let message = titlecase(&self.message).red().bold().to_string();
438 error!("=> {} \"{url}\" {message}{code}", Label::invalid());
439 }
440 }
441 pub fn with_url(self, value: String) -> Self {
443 LinkCheck::init()
444 .success(self.success)
445 .url(value)
446 .maybe_code(self.code)
447 .message(self.message)
448 .build()
449 }
450}
451impl MimeType {
452 pub fn file_type(self) -> String {
454 match self {
455 | MimeType::Cff => "cff",
456 | MimeType::Csv => "csv",
457 | MimeType::Jpeg => "jpeg",
458 | MimeType::Json => "json",
459 | MimeType::LdJson => "jsonld",
460 | MimeType::Markdown => "md",
461 | MimeType::Otf => "otf",
462 | MimeType::Png => "png",
463 | MimeType::Rust => "rs",
464 | MimeType::Svg => "svg",
465 | MimeType::Text => "txt",
466 | MimeType::Toml => "toml",
467 | MimeType::Yaml => "yaml",
468 | _ => "unknown-file-type",
469 }
470 .to_string()
471 }
472 pub fn from_path<P>(value: P) -> MimeType
476 where
477 P: Into<PathBuf>,
478 {
479 MimeType::from_string(path_to_string(value.into()))
480 }
481 pub fn from_string<S>(value: S) -> MimeType
502 where
503 S: Into<String>,
504 {
505 let name = &value.into().to_lowercase();
506 match extension(Path::new(name)).as_str() {
507 | "csv" => MimeType::Csv,
508 | "jpg" | "jpeg" => MimeType::Jpeg,
509 | "json" => MimeType::Json,
510 | "jsonld" | "json-ld" => MimeType::LdJson,
511 | "md" | "markdown" => MimeType::Markdown,
512 | "otf" => MimeType::Otf,
513 | "png" => MimeType::Png,
514 | "rs" => MimeType::Rust,
515 | "svg" => MimeType::Svg,
516 | "toml" => MimeType::Toml,
517 | "txt" => MimeType::Text,
518 | "yml" | "yaml" | "cff" => MimeType::Yaml,
519 | _ => MimeType::Unknown,
520 }
521 }
522}
523impl SchemaCheck {
524 pub fn issue_count(&self) -> usize {
526 if let Some(errors) = &self.errors {
527 match errors.clone() {
528 | ValidationErrorsKind::Field(_) => 1,
529 | ValidationErrorsKind::Struct(errors) => errors.into_errors().len(),
530 | ValidationErrorsKind::List(_) => 0,
531 }
532 } else {
533 0
534 }
535 }
536 pub fn print(self) {
538 let path = self.clone().path.unwrap().display().to_string();
539 if self.success {
540 info!("=> {} {} has {}", Label::pass(), path, "no schema validation issues".green().bold());
541 } else {
542 let count = self.issue_count();
543 error!(
544 "=> {} Found {} schema validation issue{} in {}: \n{:#?}",
545 Label::fail(),
546 count.red(),
547 suffix(count),
548 path.italic().underline(),
549 self.errors.unwrap()
550 );
551 }
552 }
553 pub fn with_path(self, value: PathBuf) -> Self {
555 SchemaCheck::init()
556 .success(self.success)
557 .maybe_errors(self.errors)
558 .path(value)
559 .message(self.message)
560 .build()
561 }
562}
563impl SemanticVersion {
564 pub fn from_command<S>(name: S) -> Option<SemanticVersion>
569 where
570 S: Into<String> + duct::IntoExecutablePath + std::marker::Copy,
571 {
572 if command_exists(name.into()) {
573 let result = cmd(name, vec!["--version"]).read();
574 match result {
575 | Ok(value) => {
576 let first_line = value.lines().collect::<Vec<_>>().first().cloned();
577 match first_line {
578 | Some(line) => Some(SemanticVersion::from_string(line)),
579 | None => None,
580 }
581 }
582 | Err(_) => None,
583 }
584 } else {
585 None
586 }
587 }
588 pub fn from_string<S>(value: S) -> SemanticVersion
590 where
591 S: Into<String>,
592 {
593 let value = match Regex::new(r"\d*[.]\d*[.]\d*") {
594 | Ok(re) => match re.find(&value.into()) {
595 | Ok(value) => match value {
596 | Some(value) => value.as_str().to_string(),
597 | None => unreachable!(),
598 },
599 | Err(_) => unreachable!(),
600 },
601 | Err(_) => unreachable!(),
602 };
603 let mut parts = value.split('.');
604 let major = parts.next().unwrap().parse::<u32>().unwrap();
605 let minor = parts.next().unwrap().parse::<u32>().unwrap();
606 let patch = parts.next().unwrap().parse::<u32>().unwrap();
607 SemanticVersion { major, minor, patch }
608 }
609}
610impl Default for SemanticVersion {
611 fn default() -> Self {
612 SemanticVersion::init().build()
613 }
614}
615pub fn checksum<P>(path: P) -> String
619where
620 P: Into<PathBuf>,
621{
622 let value = path.into();
623 match File::open(value.clone()) {
624 | Ok(file) => {
625 let mut buffer = [0; 1024];
626 let mut context = Context::new(&SHA256);
627 let mut reader = BufReader::new(file);
628 loop {
629 let count = reader.read(&mut buffer).unwrap();
630 if count == 0 {
631 break;
632 }
633 context.update(&buffer[..count]);
634 }
635 let digest = context.finish();
636 let result = HEXUPPER.encode(digest.as_ref());
637 result.to_lowercase()
638 }
639 | Err(err) => {
640 error!(error = err.to_string(), path = path_to_string(value), "=> {} Read file", Label::fail());
641 "".to_string()
642 }
643 }
644}
645pub fn command_exists<S>(name: S) -> bool
655where
656 S: Into<String> + AsRef<std::ffi::OsStr> + tracing::Value,
657{
658 match which(&name) {
659 | Ok(value) => {
660 let path = path_to_string(value.clone());
661 match value.try_exists() {
662 | Ok(true) => {
663 debug!(path, "=> {} Command", Label::found());
664 true
665 }
666 | _ => {
667 debug!(path, "=> {} Command", Label::not_found());
668 false
669 }
670 }
671 }
672 | Err(_) => {
673 warn!(name, "=> {} Command", Label::not_found());
674 false
675 }
676 }
677}
678pub fn download_binary<S, P>(url: S, destination: P) -> Result<PathBuf, String>
689where
690 S: Into<String> + Clone + std::marker::Copy,
691 P: Into<PathBuf> + Clone,
692{
693 async fn download<P>(url: String, destination: P) -> Result<(), String>
694 where
695 P: Into<PathBuf>,
696 {
697 let client = reqwest::Client::new();
698 let response = client.get(url.clone()).send();
699 let filename = PathBuf::from(url.clone()).file_name().unwrap().to_str().unwrap().to_string();
700 match response.await {
701 | Ok(data) => match data.bytes().await {
702 | Ok(content) => {
703 let mut output = File::create(destination.into().join(filename.clone())).unwrap();
704 let _ = copy(&mut Cursor::new(content.clone()), &mut output);
705 debug!(filename = filename, "=> {} Downloaded", Label::output());
706 Ok(())
707 }
708 | Err(_) => Err(format!("No content downloaded from {url}")),
709 },
710 | Err(_) => Err(format!("Failed to download {url}")),
711 }
712 }
713 let runtime = tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap();
714 let _ = runtime.block_on(download(url.into(), destination.clone()));
715 let filename = PathBuf::from(url.into()).file_name().unwrap().to_str().unwrap().to_string();
716 Ok(destination.into().join(filename))
717}
718pub fn extension(path: &Path) -> String {
731 path.extension().unwrap_or_default().to_str().unwrap_or_default().to_string()
732}
733pub fn files_all(path: PathBuf, extensions: Option<Vec<&str>>) -> Vec<PathBuf> {
744 fn paths_to_vec(paths: glob::Paths) -> Vec<PathBuf> {
745 paths.collect::<Vec<_>>().into_iter().filter_map(|x| x.ok()).collect::<Vec<_>>()
746 }
747 fn pattern(path: PathBuf, extension: &str) -> String {
748 let ext = &extension.to_lowercase();
749 let result = format!("{}/**/*.{}", path_to_string(path), ext);
750 debug!("=> {} {result}", Label::using());
751 result
752 }
753 if path.is_dir() {
754 match extensions {
755 | Some(values) => values
756 .into_iter()
757 .map(|extension| {
758 let glob_pattern = pattern(path.clone(), extension);
759 glob(&glob_pattern)
760 })
761 .filter(|x| x.is_ok())
762 .flat_map(|x| paths_to_vec(x.unwrap()))
763 .unique()
764 .collect::<Vec<PathBuf>>(),
765 | None => match glob(&format!("{}/**/*", path_to_string(path))) {
766 | Ok(paths) => paths_to_vec(paths),
767 | Err(why) => {
768 error!("=> {} Get all files (Glob) - {why}", Label::fail());
769 vec![]
770 }
771 },
772 }
773 } else {
774 if extensions.is_some() {
775 warn!(
776 path = path_to_string(path.clone()),
777 "=> {} Extension passed with single file to files_all() - please make sure this is desired",
778 Label::using()
779 );
780 }
781 vec![path]
782 }
783}
784pub fn files_from_git_branch(value: &str, extensions: Option<Vec<&str>>) -> Vec<PathBuf> {
791 if command_exists("git".to_owned()) {
792 let default_branch = match git_default_branch_name() {
793 | Some(value) => value,
794 | None => "main".to_string(),
795 };
796 let args = vec!["diff", "--name-only", &default_branch, "--merge-base", value];
797 let result = cmd("git", args).read();
798 filter_git_command_result(result, extensions)
799 } else {
800 vec![]
801 }
802}
803pub fn files_from_git_commit(value: &str, extensions: Option<Vec<&str>>) -> Vec<PathBuf> {
810 if command_exists("git".to_owned()) {
811 let args = vec!["diff-tree", "--no-commit-id", "--name-only", "-r", value];
812 let result = cmd("git", args).read();
813 debug!("=> {} Git command response - {result:?}", Label::using());
814 let files = filter_git_command_result(result, extensions);
815 debug!(
816 "=> {} Found {} file{} from Git commit - {files:?}",
817 Label::using(),
818 files.len(),
819 suffix(files.len())
820 );
821 files
822 } else {
823 vec![]
824 }
825}
826fn filter_git_command_result(result: Result<String, std::io::Error>, extensions: Option<Vec<&str>>) -> Vec<PathBuf> {
827 match result {
828 | Ok(value) => match extensions {
829 | Some(values) => value
830 .to_lowercase()
831 .split("\n")
832 .map(PathBuf::from)
833 .filter(|path| values.iter().any(|ext| MimeType::from_path(path).file_type() == *ext.to_lowercase()))
834 .collect::<Vec<_>>(),
835 | None => value.to_lowercase().split("\n").map(PathBuf::from).collect::<Vec<_>>(),
836 },
837 | Err(_) => vec![],
838 }
839}
840pub fn filter_ignored(paths: Vec<PathBuf>, ignore: Option<String>) -> Vec<PathBuf> {
842 match ignore {
843 | Some(ignore_pattern) => match Regex::new(&ignore_pattern) {
844 | Ok(re) => paths
845 .into_iter()
846 .map(path_to_string)
847 .filter(|x| !re.is_match(x).unwrap())
848 .map(PathBuf::from)
849 .collect(),
850 | Err(why) => {
851 error!("=> {} Filter ignored - {why}", Label::fail());
852 vec![]
853 }
854 },
855 | None => paths,
856 }
857}
858pub fn find_first(values: Vec<(String, String)>, pattern: &str) -> Option<(String, String)> {
860 let results = values
861 .clone()
862 .into_iter()
863 .filter(|x| !x.1.is_empty())
864 .find(|(key, _)| key.starts_with(pattern));
865 match results {
866 | Some(value) => Some(value),
867 | None => None,
868 }
869}
870pub fn generate_guid() -> String {
880 let alphabet = [
881 '-', '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',
882 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'm', 'n', 'p', 'q', 'r', 't', 'w', 'x', 'y', 'z', '3', '4', '6', '7', '8', '9',
883 ];
884 let id = nanoid!(10, &alphabet);
885 debug!(id, "=> {}", Label::using());
886 id
887}
888pub fn git_branch_name() -> Option<String> {
894 if command_exists("git".to_owned()) {
895 let args = vec!["symbolic-ref", "--short", "HEAD"];
896 let result = cmd("git", args).read();
897 match result {
898 | Ok(ref value) => {
899 let name = match value.clone().split("/").last() {
900 | Some(x) => Some(x.to_string()),
901 | None => None,
902 };
903 name
904 }
905 | Err(_) => None,
906 }
907 } else {
908 None
909 }
910}
911pub fn git_default_branch_name() -> Option<String> {
917 if command_exists("git".to_owned()) {
918 let args = vec!["symbolic-ref", "refs/remotes/origin/HEAD", "--short"];
919 let result = cmd("git", args).read();
920 match result {
921 | Ok(ref value) => {
922 let name = match value.clone().split("/").last() {
923 | Some(x) => Some(x.to_string()),
924 | None => None,
925 };
926 name
927 }
928 | Err(_) => None,
929 }
930 } else {
931 None
932 }
933}
934pub fn image_paths<P>(root: P) -> Vec<PathBuf>
946where
947 P: Into<PathBuf> + Clone,
948{
949 let extensions = ["jpg", "jpeg", "png", "svg", "gif"];
950 let mut files = extensions
951 .iter()
952 .flat_map(|ext| glob(&format!("{}/**/*.{}", root.clone().into().display(), ext)))
953 .flat_map(|paths| paths.collect::<Vec<_>>())
954 .flatten()
955 .collect::<Vec<PathBuf>>();
956 files.sort();
957 files
958}
959#[cfg(any(unix, target_os = "wasi", target_os = "redox"))]
977pub fn make_executable<P>(path: P) -> bool
978where
979 P: Into<PathBuf> + Clone,
980{
981 use std::os::unix::fs::PermissionsExt;
982 std::fs::set_permissions(path.clone().into(), std::fs::Permissions::from_mode(0o755)).unwrap();
983 path.into().is_executable()
984}
985#[cfg(windows)]
1001pub fn make_executable<P>(path: P) -> bool
1002where
1003 P: Into<PathBuf> + Clone,
1004{
1005 path.into().is_executable()
1007}
1008pub fn parent<P>(path: P) -> PathBuf
1010where
1011 P: Into<PathBuf> + Clone,
1012{
1013 let default = PathBuf::from(".");
1014 match path.clone().into().canonicalize() {
1015 | Ok(value) => match value.parent() {
1016 | Some(value) => value.to_path_buf(),
1017 | None => {
1018 warn!("=> {} Resolve parent path", Label::fail());
1019 default
1020 }
1021 },
1022 | Err(why) => {
1023 debug!("=> {} Resolve absolute path - {why}", Label::fail());
1024 match path.into().parent() {
1025 | Some(value) if !path_to_string(value.to_path_buf()).is_empty() => value.to_path_buf(),
1026 | Some(_) | None => {
1027 warn!("=> {} Parent path was empty or could not be resolved", Label::fail());
1028 default
1029 }
1030 }
1031 }
1032 }
1033}
1034pub fn path_to_string(path: PathBuf) -> String {
1047 let result = match std::fs::canonicalize(path.as_path()) {
1049 | Ok(value) => value,
1050 | Err(_) => path,
1051 };
1052 result.to_str().unwrap().to_string()
1053}
1054pub fn pretty_print<I: IntoIterator<Item = usize>>(text: &str, syntax: ProgrammingLanguage, highlight: I) {
1058 let input = format!("{text}\n");
1059 let language = syntax.to_string();
1060 let mut printer = PrettyPrinter::new();
1061 printer
1062 .input_from_bytes(input.as_bytes())
1063 .theme("zenburn")
1064 .language(&language)
1065 .line_numbers(true);
1066 for line in highlight {
1067 printer.highlight(line);
1068 }
1069 printer.print().unwrap();
1070}
1071pub fn print_changes(old: &str, new: &str) {
1078 let changes = text_diff_changes(old, new);
1079 let has_no_changes = changes.clone().into_iter().all(|(tag, _)| tag == Equal);
1080 if has_no_changes {
1081 debug!("=> {}No format changes", Label::skip());
1082 } else {
1083 for change in changes {
1084 print!("{}", change.1);
1085 }
1086 }
1087}
1088pub fn print_values_as_table(title: &str, headers: Vec<&str>, rows: Vec<Vec<String>>) {
1097 let mut table = Table::new();
1098 table
1099 .load_preset(UTF8_FULL)
1100 .apply_modifier(UTF8_ROUND_CORNERS)
1101 .set_content_arrangement(ContentArrangement::Dynamic)
1102 .set_header(headers);
1103 rows.into_iter().for_each(|row| {
1104 table.add_row(row);
1105 });
1106 println!("=> {} \n{table}", title.green().bold());
1107}
1108pub fn read_file<P>(path: P) -> Result<String, std::io::Error>
1119where
1120 P: Into<PathBuf> + Clone,
1121{
1122 let mut content = String::new();
1123 let _ = match File::open(path.clone().into()) {
1124 | Ok(mut file) => {
1125 debug!(path = path_to_string(path.into()), "=> {}", Label::read());
1126 file.read_to_string(&mut content)
1127 }
1128 | Err(why) => {
1129 error!(path = path_to_string(path.into()), "=> {} Read file", Label::fail());
1130 Err(why)
1131 }
1132 };
1133 Ok(content)
1134}
1135pub fn standard_project_folder(namespace: &str, default: Option<PathBuf>) -> PathBuf {
1151 let root = match default {
1152 | Some(value) => value,
1153 | None => match ProjectDirs::from(QUALIFIER, ORGANIZATION, APPLICATION) {
1154 | Some(dirs) => dirs.cache_dir().join(namespace).to_path_buf(),
1155 | None => PathBuf::from(format!("./{namespace}")),
1156 },
1157 };
1158 match create_dir_all(root.clone()) {
1159 | Ok(_) => {}
1160 | Err(why) => error!(directory = path_to_string(root.clone()), "=> {} Create - {}", Label::fail(), why),
1161 };
1162 root.join(generate_guid())
1163}
1164pub fn suffix(value: usize) -> String {
1166 (if value == 1 { "" } else { "s" }).to_string()
1167}
1168pub fn text_diff_changes(old: &str, new: &str) -> Vec<(ChangeTag, String)> {
1185 TextDiff::from_lines(old, new)
1186 .iter_all_changes()
1187 .map(|line| {
1188 let tag = line.tag();
1189 let text = match tag {
1190 | Delete => format!("- {line}").red().to_string(),
1191 | Insert => format!("+ {line}").green().to_string(),
1192 | Equal => format!(" {line}").dimmed().to_string(),
1193 };
1194 (tag, text)
1195 })
1196 .collect::<Vec<_>>()
1197}
1198pub fn tokio_runtime() -> tokio::runtime::Runtime {
1206 debug!("=> {} Tokio runtime", Label::using());
1207 tokio::runtime::Builder::new_multi_thread().enable_all().build().unwrap()
1208}
1209pub fn to_string(values: Vec<&str>) -> Vec<String> {
1211 values.iter().map(|s| s.to_string()).collect()
1212}
1213pub fn write_file<P>(path: P, content: String) -> Result<(), std::io::Error>
1225where
1226 P: Into<PathBuf>,
1227{
1228 match File::create(path.into().clone()) {
1229 | Ok(mut file) => {
1230 file.write_all(content.as_bytes()).unwrap();
1231 file.flush()
1232 }
1233 | Err(why) => {
1234 error!("=> {} Cannot create file - {why}", Label::fail());
1235 Err(why)
1236 }
1237 }
1238}
1239
1240#[cfg(test)]
1241mod tests;