acorn_lib/util/
mod.rs

1//! # Common utilities
2//!
3//! This module contains common functions and data structures used to build the ACORN command line interface as well as support open science endeavors.
4//!
5//! ## Example Uses
6//! ### Work with semantic versions
7//! ```ignore
8//! use acorn_lib::util::SemanticVersion;
9//!
10//! let version = SemanticVersion::from_string("1.2.3");
11//! assert_eq!(version.minor, 2);
12//!
13//! if let Some(version) = SemanticVersion::from_command("cargo") {
14//!     println!("cargo version: {version}");
15//! }
16//! ```
17//!
18//! ### Perform file read and write operations
19//! ```ignore
20//! use acorn_lib::util::{checksum, read_file, write_file};
21//! use std::path::PathBuf;
22//!
23//! // Verify file integrity
24//! assert_eq!(checksum(PathBuf::from("/path/to/file")), "somesha256hashvaluethatisreallylong");
25//!
26//! // Read file contents
27//! let contents = read_file(PathBuf::from("/path/to/this/file"));
28//!
29//! // Write file contents
30//! write_file(PathBuf::from("/path/to/that/file"), contents);
31//! ```
32//!
33use crate::constants::{APPLICATION, ORGANIZATION, QUALIFIER};
34use bat::PrettyPrinter;
35use bon::Builder;
36use comfy_table::modifiers::UTF8_ROUND_CORNERS;
37use comfy_table::presets::UTF8_FULL;
38use comfy_table::*;
39use console::Emoji;
40use convert_case::{Case, Casing};
41use data_encoding::HEXUPPER;
42use derive_more::Display;
43use directories::ProjectDirs;
44use duct::cmd;
45use fancy_regex::Regex;
46use glob::glob;
47use is_executable::IsExecutable;
48use itertools::Itertools;
49use nanoid::nanoid;
50use owo_colors::{OwoColorize, Style, Styled};
51use reqwest::header::USER_AGENT;
52use ring::digest::{Context, SHA256};
53use rust_embed::Embed;
54use schemars::JsonSchema;
55use serde::{Deserialize, Serialize};
56use similar::{
57    ChangeTag::{self, Delete, Equal, Insert},
58    TextDiff,
59};
60use std::collections::HashMap;
61use std::fs::create_dir_all;
62use std::fs::File;
63use std::io::{copy, BufReader, Cursor, Read, Write};
64use std::path::{Path, PathBuf};
65use tracing::{debug, error, warn};
66use which::which;
67
68pub mod citeas;
69#[cfg(feature = "cli")]
70pub mod cli;
71
72/// Trait for augmenting path value functionality with absolute path string conversion
73pub trait ToAbsoluteString {
74    /// Return a string representation of the absolute path
75    fn to_absolute_string(&self) -> String;
76}
77/// SPDX compliant license identifier
78///
79/// See <https://spdx.org/licenses/>
80#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
81pub enum License {
82    /// MIT License
83    Mit,
84    /// Creative Commons
85    #[serde(alias = "Creative Commons CC-0")]
86    CreativeCommons,
87    /// Unknown license
88    Unknown,
89}
90/// Supports an incomplete list of common <span title="Multipurpose Internet Mail Extension">MIME</span> types
91///
92/// See listing of [common HTTP MIME types](https://developer.mozilla.org/en-US/docs/Web/HTTP/MIME_types/Common_types) and <https://mimetype.io/all-types> for more information
93#[derive(Clone, Debug, Display, PartialEq)]
94pub enum MimeType {
95    /// Citation File Format (CFF)
96    /// ### Note
97    /// > CFF does not have a standard MIME type, but is valid YAML
98    ///
99    /// See <https://citation-file-format.github.io/> for more information
100    #[display("application/yaml")]
101    Cff,
102    /// Comma Separated Values (CSV)
103    #[display("text/csv")]
104    Csv,
105    /// Linked Data [JSON](https://www.json.org/json-en.html)
106    ///
107    /// See <https://json-ld.org/>
108    #[display("application/ld+json")]
109    LdJson,
110    /// Joint Photographic Experts Group (JPEG)
111    #[display("image/jpeg")]
112    Jpeg,
113    /// JavaScript Object Notation (JSON)
114    ///
115    /// See <https://www.json.org/json-en.html>
116    #[display("application/json")]
117    Json,
118    /// Markdown
119    #[display("text/markdown")]
120    Markdown,
121    /// OpenType Font (OTF)
122    #[display("font/otf")]
123    Otf,
124    /// Portable Network Graphic (PNG)
125    #[display("image/png")]
126    Png,
127    /// Rust Source Code (RS)
128    #[display("text/rust")]
129    Rust,
130    /// Scalable Vector Graphic (SVG)
131    #[display("image/svg+xml")]
132    Svg,
133    /// Plain Text
134    ///
135    /// Just plain old text
136    #[display("text/plain")]
137    Text,
138    /// Tom's Obvious Minimal Language (TOML)
139    ///
140    /// See <https://toml.io/>
141    #[display("application/toml")]
142    Toml,
143    /// YAML Ain't Markup Language (YAML)
144    ///
145    /// See <https://yaml.org/>
146    #[display("application/yaml")]
147    Yaml,
148    /// Unknown MIME type
149    #[display("application/unknown")]
150    Unknown,
151}
152/// Provides a small subset of common programming languages available for syntax highlighting
153#[derive(Clone, Copy, Debug, Display)]
154pub enum ProgrammingLanguage {
155    /// HyperText Markup Language (HTML)
156    #[display("html")]
157    Html,
158    /// Markdown
159    ///
160    /// See <https://www.markdownguide.org/>
161    #[display("markdown")]
162    Markdown,
163    /// JavaScript Object Notation (JSON)
164    ///
165    /// See <https://www.json.org/json-en.html>
166    #[display("json")]
167    Json,
168    /// YAM Ain't Markup Language (YAML)
169    ///
170    /// See <https://yaml.org/>
171    #[display("yaml")]
172    Yaml,
173}
174/// Struct for using and sharing constants
175///
176/// See <https://git.sr.ht/~pyrossh/rust-embed>
177#[derive(Embed)]
178#[folder = "assets/constants/"]
179pub struct Constant;
180/// Struct for using and sharing colorized logging labels
181///
182/// ### Labels [^1]
183/// | Name    | Example Output |
184/// |---------|----------------|
185/// | Dry run | "=> DRY_RUN ■ Pretending to do a thing" |
186/// | Skip    | "=> ⚠️  Thing was skipped" |
187/// | Pass    | "=> ✅ Thing passed " |
188/// | Fail    | "=> ✗ Thing failed " |
189///
190/// [^1]: Incomplete list of examples without foreground/background coloring
191pub struct Label {}
192/// Semantic version
193///
194/// see <https://semver.org/>
195///
196/// ```rust
197/// use acorn_lib::util::SemanticVersion;
198///
199/// let version = SemanticVersion::from_string("1.2.3");
200/// assert_eq!(version.major, 1);
201/// assert_eq!(version.to_string(), "1.2.3");
202/// ```
203
204#[derive(Builder, Clone, Copy, Debug, Deserialize, Display, Serialize, JsonSchema)]
205#[builder(start_fn = init)]
206#[display("{}.{}.{}", major, minor, patch)]
207pub struct SemanticVersion {
208    /// Version when you make incompatible API changes
209    #[builder(default = 0)]
210    pub major: u32,
211    /// Version when you add functionality in a backward compatible manner
212    #[builder(default = 0)]
213    pub minor: u32,
214    /// Version when you make backward compatible bug fixes
215    #[builder(default = 0)]
216    pub patch: u32,
217}
218impl Constant {
219    /// Reads a file from the asset folder and returns its contents as a UTF-8 string.
220    ///
221    /// # Panics
222    ///
223    /// Panics if the file does not exist in the asset folder.
224    pub fn from_asset(file_name: &str) -> String {
225        match Constant::get(file_name) {
226            | Some(value) => String::from_utf8_lossy(value.data.as_ref()).into(),
227            | None => {
228                error!(file_name, "=> {} Import Constant asset", Label::fail());
229                panic!("Unable to import {file_name}")
230            }
231        }
232    }
233    /// Returns an iterator over the last values of each row in the given file.
234    ///
235    /// If a row is empty, an empty string is returned.
236    pub fn last_values(file_name: &str) -> impl Iterator<Item = String> {
237        Constant::csv(file_name)
238            .into_iter()
239            .map(|x| match x.last() {
240                | Some(value) => value.to_string(),
241                | None => "".to_string(),
242            })
243            .filter(|x| !x.is_empty())
244    }
245    /// Reads a file from the asset folder and returns its contents as an iterator over individual lines.
246    ///
247    /// # Panics
248    ///
249    /// Panics if the file does not exist in the asset folder.
250    pub fn read_lines(file_name: &str) -> Vec<String> {
251        let data = Constant::from_asset(file_name);
252        data.lines().map(String::from).collect()
253    }
254    /// Reads a CSV file from the asset folder and returns its contents as a `Vec` of `Vec<String>`,
255    /// where each inner vector represents a row and each string within the inner vector represents a cell value.
256    ///
257    /// # Arguments
258    ///
259    /// * `file_name` - A string slice representing the name of the CSV file (without extension).
260    ///
261    /// # Panics
262    ///
263    /// Panics if the file does not exist in the asset folder.
264    pub fn csv(file_name: &str) -> Vec<Vec<String>> {
265        Constant::read_lines(format!("{file_name}.csv").as_str())
266            .into_iter()
267            .map(|x| x.split(",").map(String::from).collect())
268            .collect()
269    }
270}
271impl Label {
272    /// Emoji for use when logging a warning, caution, etc.
273    pub const CAUTION: Emoji<'_, '_> = Emoji("⚠️  ", "!!! ");
274    /// Emoji for use when logging a success, pass, etc.
275    pub const CHECKMARK: Emoji<'_, '_> = Emoji("✅ ", "☑ ");
276    /// Template string to customize the progress bar
277    ///
278    /// See <https://docs.rs/indicatif/latest/indicatif/#templates>
279    pub const PROGRESS_BAR_TEMPLATE: &str = "  {spinner:.green}{pos:>5} of{len:^5}[{bar:40.green}] {msg}";
280    /// "Dry run" label
281    pub fn dry_run() -> Styled<&'static &'static str> {
282        let style = Style::new().black().on_yellow();
283        " DRY_RUN ■ ".style(style)
284    }
285    /// "Invalid" label
286    pub fn invalid() -> String {
287        Label::fmt_invalid(" ✗ INVALID")
288    }
289    /// "Invalid" label formatting
290    pub fn fmt_invalid(value: &str) -> String {
291        let style = Style::new().red().on_default_color();
292        value.style(style).to_string()
293    }
294    /// "Valid" label
295    pub fn valid() -> String {
296        Label::fmt_valid(" ✓ VALID  ")
297    }
298    /// "Invalid" label formatting
299    pub fn fmt_valid(value: &str) -> String {
300        let style = Style::new().green().on_default_color();
301        value.style(style).to_string()
302    }
303    /// "Fail" label
304    pub fn fail() -> String {
305        Label::fmt_fail("FAIL")
306    }
307    /// "Fail" label formatting
308    pub fn fmt_fail(value: &str) -> String {
309        let style = Style::new().white().on_red();
310        format!(" ✗ {value} ").style(style).to_string()
311    }
312    /// "Found" label
313    pub fn found() -> String {
314        Label::fmt_found("FOUND")
315    }
316    /// "Found" label formatting
317    pub fn fmt_found(value: &str) -> String {
318        let style = Style::new().green().on_default_color();
319        value.to_string().style(style).to_string()
320    }
321    /// "Not found" label
322    pub fn not_found() -> String {
323        Label::fmt_not_found("NOT_FOUND")
324    }
325    /// "Not found" label formatting
326    pub fn fmt_not_found(value: &str) -> String {
327        let style = Style::new().red().on_default_color();
328        value.style(style).to_string()
329    }
330    /// "Output" label
331    pub fn output() -> String {
332        Label::fmt_output("OUTPUT")
333    }
334    /// "Output" label formatting
335    pub fn fmt_output(value: &str) -> String {
336        let style = Style::new().cyan().dimmed().on_default_color();
337        value.style(style).to_string()
338    }
339    /// "Pass" label
340    pub fn pass() -> String {
341        Label::fmt_pass("SUCCESS")
342    }
343    /// "Pass" label formatting
344    pub fn fmt_pass(value: &str) -> String {
345        let style = Style::new().green().bold().on_default_color();
346        format!("{}{}", Label::CHECKMARK, value).style(style).to_string()
347    }
348    /// "Read" label
349    pub fn read() -> Styled<&'static &'static str> {
350        let style = Style::new().green().on_default_color();
351        "READ".style(style)
352    }
353    /// "Rejected" label
354    pub fn rejected() -> String {
355        Label::fmt_rejected("REJECTED")
356    }
357    /// "Rejected" label formatting
358    pub fn fmt_rejected(value: &str) -> String {
359        let style = Style::new().red().on_default_color();
360        format!("🛑 {value} ").style(style).to_string()
361    }
362    /// "Run" label
363    pub fn run() -> String {
364        Label::fmt_run("RUN")
365    }
366    /// "Run" label formatting
367    pub fn fmt_run(value: &str) -> String {
368        let style = Style::new().black().on_yellow();
369        format!("{value} ▶ ").style(style).to_string()
370    }
371    /// "Skip" label
372    pub fn skip() -> String {
373        Label::fmt_skip("SKIP")
374    }
375    /// "Skip" label formatting
376    pub fn fmt_skip(value: &str) -> String {
377        let style = Style::new().yellow().on_default_color();
378        format!("{}{} ", Label::CAUTION, value).style(style).to_string()
379    }
380    /// "Using" label
381    pub fn using() -> String {
382        Label::fmt_using("USING")
383    }
384    /// "Using" label formatting
385    pub fn fmt_using(value: &str) -> String {
386        let style = Style::new().cyan();
387        value.style(style).to_string()
388    }
389}
390impl MimeType {
391    /// Returns the file type as a string
392    /// ### Example
393    /// ```rust
394    /// use acorn_lib::util::MimeType;
395    ///
396    /// let mime = MimeType::Cff;
397    /// assert_eq!(mime.file_type(), "cff");
398    /// ```
399    pub fn file_type(self) -> String {
400        match self {
401            | MimeType::Cff => "cff",
402            | MimeType::Csv => "csv",
403            | MimeType::Jpeg => "jpeg",
404            | MimeType::Json => "json",
405            | MimeType::LdJson => "jsonld",
406            | MimeType::Markdown => "md",
407            | MimeType::Otf => "otf",
408            | MimeType::Png => "png",
409            | MimeType::Rust => "rs",
410            | MimeType::Svg => "svg",
411            | MimeType::Text => "txt",
412            | MimeType::Toml => "toml",
413            | MimeType::Yaml => "yaml",
414            | _ => "unknown-file-type",
415        }
416        .to_string()
417    }
418    /// Returns a [`MimeType`] value based on the file extension of the given file name.
419    ///
420    /// Uses [`MimeType::from_string`].
421    ///
422    /// ```rust
423    /// use acorn_lib::util::MimeType;
424    /// use std::path::PathBuf;
425    ///
426    /// let mime = MimeType::from_path(PathBuf::from("test.cff"));
427    /// assert_eq!(mime, MimeType::Yaml);
428    /// ```
429    pub fn from_path<P>(value: P) -> MimeType
430    where
431        P: Into<PathBuf>,
432    {
433        MimeType::from_string(value.into().display().to_string())
434    }
435    /// Returns a `MimeType` value based on the file extension of the given file name.
436    ///
437    /// # Supported MIME types
438    ///
439    /// | File Extension | MIME Type |
440    /// | --- | --- |
441    /// | cff | application/yaml |
442    /// | csv | text/csv |
443    /// | jpg | image/jpeg |
444    /// | jpeg | image/jpeg |
445    /// | json | application/json |
446    /// | jsonld | application/ld+json |
447    /// | md | text/markdown |
448    /// | otf | font/otf |
449    /// | png | image/png |
450    /// | rs | text/rust |
451    /// | svg | image/svg+xml |
452    /// | toml | application/toml |
453    /// | txt | text/plain |
454    /// | yaml | application/yaml |
455    pub fn from_string<S>(value: S) -> MimeType
456    where
457        S: Into<String>,
458    {
459        let name = &value.into().to_lowercase();
460        match extension(Path::new(name)).as_str() {
461            | "csv" => MimeType::Csv,
462            | "jpg" | "jpeg" => MimeType::Jpeg,
463            | "json" => MimeType::Json,
464            | "jsonld" | "json-ld" => MimeType::LdJson,
465            | "md" | "markdown" => MimeType::Markdown,
466            | "otf" => MimeType::Otf,
467            | "png" => MimeType::Png,
468            | "rs" => MimeType::Rust,
469            | "svg" => MimeType::Svg,
470            | "toml" => MimeType::Toml,
471            | "txt" => MimeType::Text,
472            | "yml" | "yaml" | "cff" => MimeType::Yaml,
473            | _ => MimeType::Unknown,
474        }
475    }
476}
477impl SemanticVersion {
478    /// Returns a `SemanticVersion` value based on the output of the `--version` command-line flag
479    /// of the given executable name. Tested with [cargo](https://rustup.rs/), [git](https://git-scm.com/book/en/v2/Getting-Started-The-Command-Line), and [pandoc](https://pandoc.org/).
480    ///
481    /// <div class="warning">this function only supports commands that provide a `--version` flag</div>
482    ///
483    /// ### Example
484    /// ```ignore
485    /// use acorn_lib::schema::validate::SemanticVersion;
486    ///
487    /// let version = SemanticVersion::from_command("cargo").to_string();
488    /// assert_eq!(version, "1.90.0");
489    /// ```
490    pub fn from_command<S>(name: S) -> Option<SemanticVersion>
491    where
492        S: Into<String> + duct::IntoExecutablePath + std::marker::Copy,
493    {
494        if command_exists(name.into()) {
495            let result = cmd(name, vec!["--version"]).read();
496            match result {
497                | Ok(value) => {
498                    let first_line = value.lines().collect::<Vec<_>>().first().cloned();
499                    match first_line {
500                        | Some(line) => Some(SemanticVersion::from_string(line)),
501                        | None => None,
502                    }
503                }
504                | Err(_) => None,
505            }
506        } else {
507            None
508        }
509    }
510    /// Parses a string into a `SemanticVersion` value
511    ///
512    /// ### Example
513    /// ```rust
514    /// use acorn_lib::util::SemanticVersion;
515    ///
516    /// let version = SemanticVersion::from_string("1.2.3");
517    /// assert_eq!(version.minor, 2);
518    /// ```
519    pub fn from_string<S>(value: S) -> SemanticVersion
520    where
521        S: Into<String>,
522    {
523        let value = match Regex::new(r"\d*[.]\d*[.]\d*") {
524            | Ok(re) => match re.find(&value.into()) {
525                | Ok(value) => match value {
526                    | Some(value) => value.as_str().to_string(),
527                    | None => unreachable!(),
528                },
529                | Err(_) => unreachable!(),
530            },
531            | Err(_) => unreachable!(),
532        };
533        let mut parts = value.split('.');
534        let major = parts.next().unwrap().parse::<u32>().unwrap();
535        let minor = parts.next().unwrap().parse::<u32>().unwrap();
536        let patch = parts.next().unwrap().parse::<u32>().unwrap();
537        SemanticVersion { major, minor, patch }
538    }
539}
540impl Default for SemanticVersion {
541    fn default() -> Self {
542        SemanticVersion::init().build()
543    }
544}
545impl ToAbsoluteString for PathBuf {
546    fn to_absolute_string(&self) -> String {
547        to_absolute_string(self.clone())
548    }
549}
550/// Get SHA256 hash of a file
551///
552/// See <https://rust-lang-nursery.github.io/rust-cookbook/cryptography/hashing.html>
553///
554/// ### Example
555/// ```ignore
556/// use acorn_lib::util::checksum;
557///
558/// let checksum = checksum("path/to/file");
559/// assert!(checksum.is_some());
560/// ```
561pub fn checksum<P>(path: P) -> Option<String>
562where
563    P: Into<PathBuf>,
564{
565    let value = path.into();
566    match File::open(value.clone()) {
567        | Ok(file) => {
568            let mut buffer = [0; 1024];
569            let mut context = Context::new(&SHA256);
570            let mut reader = BufReader::new(file);
571            loop {
572                let count = reader.read(&mut buffer).unwrap();
573                if count == 0 {
574                    break;
575                }
576                context.update(&buffer[..count]);
577            }
578            let digest = context.finish();
579            let result = HEXUPPER.encode(digest.as_ref());
580            Some(result.to_lowercase())
581        }
582        | Err(err) => {
583            error!(
584                error = err.to_string(),
585                path = to_absolute_string(value),
586                "=> {} Read file",
587                Label::fail()
588            );
589            None
590        }
591    }
592}
593/// Checks if a given command exists in current terminal context.
594///
595/// # Arguments
596///
597/// * `name` - A string slice or `String` containing the name of the command to be checked.
598///
599/// # Return
600///
601/// A boolean indicating whether the command exists or not.
602pub fn command_exists<S>(name: S) -> bool
603where
604    S: Into<String> + AsRef<std::ffi::OsStr> + tracing::Value,
605{
606    match which(&name) {
607        | Ok(value) => {
608            let path = to_absolute_string(value.clone());
609            match value.try_exists() {
610                | Ok(true) => {
611                    debug!(path, "=> {} Command", Label::found());
612                    true
613                }
614                | _ => {
615                    debug!(path, "=> {} Command", Label::not_found());
616                    false
617                }
618            }
619        }
620        | Err(_) => {
621            warn!(name, "=> {} Command", Label::not_found());
622            false
623        }
624    }
625}
626/// Downloads a binary file from the given URL to the destination path.
627///
628/// # Arguments
629///
630/// * `url` - A string slice representing the URL of the binary to download.
631/// * `destination` - A path to the root directory where the file should be saved.
632///
633/// # Returns
634///
635/// A `Result` containing a `PathBuf` to the downloaded file on success, or a string error message on failure.
636///
637/// # Notes
638/// - Uses [`tokio_runtime`] for asynchronous operations.
639pub fn download_binary<S, P>(url: S, destination: P) -> Result<PathBuf, String>
640where
641    S: Into<String> + Clone + std::marker::Copy,
642    P: Into<PathBuf> + Clone,
643{
644    async fn download<P>(url: String, destination: P) -> Result<(), String>
645    where
646        P: Into<PathBuf>,
647    {
648        let client = reqwest::Client::new();
649        let response = client.get(url.clone()).header(USER_AGENT, "rust-web-api-client").send();
650        let filename = PathBuf::from(url.clone()).file_name().unwrap().to_str().unwrap().to_string();
651        match response.await {
652            | Ok(data) => match data.bytes().await {
653                | Ok(content) => {
654                    let mut output = File::create(destination.into().join(filename.clone())).unwrap();
655                    let _ = copy(&mut Cursor::new(content.clone()), &mut output);
656                    debug!(filename = filename, "=> {} Downloaded", Label::output());
657                    Ok(())
658                }
659                | Err(_) => Err(format!("No content downloaded from {url}")),
660            },
661            | Err(_) => Err(format!("Failed to download {url}")),
662        }
663    }
664    let runtime = tokio_runtime();
665    let _ = runtime.block_on(download(url.into(), destination.clone()));
666    let filename = PathBuf::from(url.into()).file_name().unwrap().to_str().unwrap().to_string();
667    Ok(destination.into().join(filename))
668}
669/// Get file extension
670///
671/// # Examples
672/// ```
673/// use std::path::Path;
674/// use acorn_lib::util::extension;
675///
676/// assert_eq!("txt", extension(Path::new("hello.txt")));
677/// assert_eq!("md", extension(Path::new("README.md")));
678/// assert_eq!("", extension(Path::new(".dotfile")));
679/// assert_eq!("", extension(Path::new("/path/to/folder")));
680/// ```
681pub fn extension(path: &Path) -> String {
682    path.extension().unwrap_or_default().to_str().unwrap_or_default().to_string()
683}
684/// Returns a vector of `PathBuf` containing all files in a directory that match at least one of the given extensions.
685///
686/// # Arguments
687///
688/// * `path` - A `PathBuf` to the directory to search.
689/// * `extensions` - An `Option` containing a list of string slice(s) representing the file extension(s) to search for.
690///
691/// # Returns
692///
693/// A `Vec` containing `PathBuf` values of all files in the given directory that match at least one of the given extensions.
694// TODO: Add support for URI path
695pub fn files_all(path: PathBuf, extensions: Option<Vec<&str>>) -> Vec<PathBuf> {
696    fn paths_to_vec(paths: glob::Paths) -> Vec<PathBuf> {
697        paths.collect::<Vec<_>>().into_iter().filter_map(|x| x.ok()).collect::<Vec<_>>()
698    }
699    fn pattern(path: PathBuf, extension: &str) -> String {
700        let ext = &extension.to_lowercase();
701        let result = format!("{}/**/*.{}", to_absolute_string(path), ext);
702        debug!("=> {} {result}", Label::using());
703        result
704    }
705    if path.is_dir() {
706        match extensions {
707            | Some(values) => values
708                .into_iter()
709                .map(|extension| {
710                    let glob_pattern = pattern(path.clone(), extension);
711                    glob(&glob_pattern)
712                })
713                .filter(|x| x.is_ok())
714                .flat_map(|x| paths_to_vec(x.unwrap()))
715                .unique()
716                .collect::<Vec<PathBuf>>(),
717            | None => match glob(&format!("{}/**/*", to_absolute_string(path))) {
718                | Ok(paths) => paths_to_vec(paths),
719                | Err(why) => {
720                    error!("=> {} Get all files (Glob) - {why}", Label::fail());
721                    vec![]
722                }
723            },
724        }
725    } else {
726        if extensions.is_some() {
727            warn!(
728                path = to_absolute_string(path.clone()),
729                "=> {} Extension passed with single file to files_all() - please make sure this is desired",
730                Label::using()
731            );
732        }
733        vec![path]
734    }
735}
736/// Returns a vector of `PathBuf` containing all files changed in the given Git branch relative to the default branch.
737///
738/// # Arguments
739///
740/// * `value` - A string slice representing the name of the Git branch to check.
741/// * `extension` - An `Option` containing a string slice representing the file extension to filter results by.
742pub fn files_from_git_branch(value: &str, extensions: Option<Vec<&str>>) -> Vec<PathBuf> {
743    if command_exists("git".to_owned()) {
744        let default_branch = match git_default_branch_name() {
745            | Some(value) => value,
746            | None => "main".to_string(),
747        };
748        let args = vec!["diff", "--name-only", &default_branch, "--merge-base", value];
749        let result = cmd("git", args).read();
750        filter_git_command_result(result, extensions)
751    } else {
752        vec![]
753    }
754}
755/// Returns a vector of `PathBuf` containing all files changed in the given Git commit.
756///
757/// # Arguments
758///
759/// * `value` - A string slice representing the Git commit hash to check.
760/// * `extension` - An `Option` containing a string slice representing the file extension to filter results by.
761pub fn files_from_git_commit(value: &str, extensions: Option<Vec<&str>>) -> Vec<PathBuf> {
762    if command_exists("git".to_owned()) {
763        let args = vec!["diff-tree", "--no-commit-id", "--name-only", "-r", value];
764        let result = cmd("git", args).read();
765        debug!("=> {} Git command response - {result:?}", Label::using());
766        let files = filter_git_command_result(result, extensions);
767        debug!(
768            "=> {} Found {} file{} from Git commit - {files:?}",
769            Label::using(),
770            files.len(),
771            suffix(files.len())
772        );
773        files
774    } else {
775        vec![]
776    }
777}
778fn filter_git_command_result(result: Result<String, std::io::Error>, extensions: Option<Vec<&str>>) -> Vec<PathBuf> {
779    match result {
780        | Ok(value) => match extensions {
781            | Some(values) => value
782                .to_lowercase()
783                .split("\n")
784                .map(PathBuf::from)
785                .filter(|path| values.iter().any(|ext| MimeType::from_path(path).file_type() == *ext.to_lowercase()))
786                .collect::<Vec<_>>(),
787            | None => value.to_lowercase().split("\n").map(PathBuf::from).collect::<Vec<_>>(),
788        },
789        | Err(_) => vec![],
790    }
791}
792/// Return file paths in a vector that don't match the ignore pattern
793/// ### Example
794/// ```rust
795/// use acorn_lib::util::filter_ignored;
796/// use std::path::PathBuf;
797///
798/// let paths = vec![PathBuf::from("/path/to/foo.txt"), PathBuf::from("/path/to/bar.txt")];
799/// let ignore = Some("*.txt".to_string());
800/// let result = filter_ignored(paths, ignore);
801/// assert!(result.is_empty());
802/// ```
803pub fn filter_ignored(paths: Vec<PathBuf>, ignore: Option<String>) -> Vec<PathBuf> {
804    match ignore {
805        | Some(ignore_pattern) => match Regex::new(&ignore_pattern) {
806            | Ok(re) => paths
807                .into_iter()
808                .map(to_absolute_string)
809                .filter(|x| !re.is_match(x).unwrap())
810                .map(PathBuf::from)
811                .collect(),
812            | Err(why) => {
813                error!("=> {} Filter ignored - {why}", Label::fail());
814                vec![]
815            }
816        },
817        | None => paths,
818    }
819}
820/// Return fisrt key/value pair with key that matches pattern
821/// ### Example
822/// ```rust
823/// use acorn_lib::util::find_first;
824///
825/// let values = vec![("foo".to_string(), "bar".to_string()), ("baz".to_string(), "qux".to_string())];
826/// let pattern = "ba";
827/// let result = find_first(values, pattern);
828/// assert_eq!(result, Some(("baz".to_string(), "qux".to_string())));
829/// ```
830pub fn find_first(values: Vec<(String, String)>, pattern: &str) -> Option<(String, String)> {
831    let results = values
832        .clone()
833        .into_iter()
834        .filter(|x| !x.1.is_empty())
835        .find(|(key, _)| key.starts_with(pattern));
836    match results {
837        | Some(value) => Some(value),
838        | None => None,
839    }
840}
841/// Generates a random GUID using a custom alphabet.
842///
843/// The generated GUID is a 10-character string composed of a mix of uppercase
844/// letters, lowercase letters, digits, and a hyphen. The function uses the
845/// [nanoid](https://github.com/ai/nanoid) library to ensure randomness and uniqueness of the GUID.
846///
847/// # Returns
848///
849/// A `String` representing a randomly generated GUID.
850pub fn generate_guid() -> String {
851    let alphabet = [
852        '-', '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',
853        'e', 'f', 'g', 'h', 'i', 'j', 'k', 'm', 'n', 'p', 'q', 'r', 't', 'w', 'x', 'y', 'z', '3', '4', '6', '7', '8', '9',
854    ];
855    let id = nanoid!(10, &alphabet);
856    debug!(id, "=> {}", Label::using());
857    id
858}
859/// Returns the current Git branch name if the `git` command is available and executed successfully.
860///
861/// This function executes the `git symbolic-ref --short HEAD` command to retrieve the name of
862/// the current Git branch. If the command is successful, the branch name is extracted and returned
863/// as a `String`. If the command fails or if `git` is not available, the function returns `None`.
864pub fn git_branch_name() -> Option<String> {
865    if command_exists("git".to_owned()) {
866        let args = vec!["symbolic-ref", "--short", "HEAD"];
867        let result = cmd("git", args).read();
868        match result {
869            | Ok(ref value) => {
870                let name = match value.clone().split("/").last() {
871                    | Some(x) => Some(x.to_string()),
872                    | None => None,
873                };
874                name
875            }
876            | Err(_) => None,
877        }
878    } else {
879        None
880    }
881}
882/// Returns the default Git branch name if the `git` command is available and executed successfully.
883///
884/// This function executes the `git symbolic-ref refs/remotes/origin/HEAD --short` command to retrieve
885/// the default Git branch name. If the command is successful, the branch name is extracted and returned
886/// as a `String`. If the command fails or if `git` is not available, the function returns `None`.
887pub fn git_default_branch_name() -> Option<String> {
888    if command_exists("git".to_owned()) {
889        let args = vec!["symbolic-ref", "refs/remotes/origin/HEAD", "--short"];
890        let result = cmd("git", args).read();
891        match result {
892            | Ok(ref value) => {
893                let name = match value.clone().split("/").last() {
894                    | Some(x) => Some(x.to_string()),
895                    | None => None,
896                };
897                name
898            }
899            | Err(_) => None,
900        }
901    } else {
902        None
903    }
904}
905/// Returns a vector of `PathBuf` representing paths to all images found in the given
906/// directory and all of its subdirectories.
907///
908/// # Arguments
909///
910/// * `root` - A value that can be converted into a `PathBuf` and implements the `Clone` trait. This is the directory in which the search for images is performed.
911///
912/// # Returns
913///
914/// A vector of `PathBuf` representing paths to all images found in the given directory and
915/// all of its subdirectories. The paths are sorted alphabetically.
916///
917/// # Notes
918/// - Supported image formats are "JPEG", "PNG", "SVG", and "GIF"
919pub fn image_paths<P>(root: P) -> Vec<PathBuf>
920where
921    P: Into<PathBuf> + Clone,
922{
923    let extensions = ["jpg", "jpeg", "png", "svg", "gif"];
924    let mut files = extensions
925        .iter()
926        .flat_map(|ext| glob(&format!("{}/**/*.{}", root.clone().into().display(), ext)))
927        .flat_map(|paths| paths.collect::<Vec<_>>())
928        .flatten()
929        .collect::<Vec<PathBuf>>();
930    files.sort();
931    files
932}
933/// Makes the given file executable.
934///
935/// # Parameters
936///
937/// * `path` - A `PathBuf` containing the path to the file to be made executable.
938///
939/// # Return
940///
941/// A boolean indicating whether the file is executable after calling this function.
942#[cfg(any(unix, target_os = "wasi", target_os = "redox"))]
943pub fn make_executable<P>(path: P) -> bool
944where
945    P: Into<PathBuf> + Clone,
946{
947    use std::os::unix::fs::PermissionsExt;
948    std::fs::set_permissions(path.clone().into(), std::fs::Permissions::from_mode(0o755)).unwrap();
949    path.into().is_executable()
950}
951/// Makes the given file executable.
952///
953/// # Parameters
954///
955/// * `path` - A `PathBuf` containing the path to the file to be made executable.
956///
957/// # Return
958///
959/// A boolean indicating whether the file is executable after calling this function.
960#[cfg(windows)]
961pub fn make_executable<P>(path: P) -> bool
962where
963    P: Into<PathBuf> + Clone,
964{
965    // TODO: Add windows support...pass through?
966    path.into().is_executable()
967}
968/// Returns the absolute path of the parent directory for the given path.
969pub fn parent<P>(path: P) -> PathBuf
970where
971    P: Into<PathBuf> + Clone,
972{
973    let default = PathBuf::from(".");
974    match path.clone().into().canonicalize() {
975        | Ok(value) => match value.parent() {
976            | Some(value) => value.to_path_buf(),
977            | None => {
978                warn!("=> {} Resolve parent path", Label::fail());
979                default
980            }
981        },
982        | Err(why) => {
983            debug!("=> {} Resolve absolute path - {why}", Label::fail());
984            match path.into().parent() {
985                | Some(value) if !to_absolute_string(value.to_path_buf()).is_empty() => value.to_path_buf(),
986                | Some(_) | None => {
987                    warn!("=> {} Parent path was empty or could not be resolved", Label::fail());
988                    default
989                }
990            }
991        }
992    }
993}
994/// Converts a `PathBuf` into a `String` representation of the **absolute** path.
995/// <div class="warning">Uses <code>fs::canonicalize</code>, which might cause problems on Windows</div>
996///
997/// This function attempts to canonicalize the provided path, which resolves any symbolic links
998/// and returns an absolute path. If canonicalization fails, the original path is returned as a string.
999///
1000/// # Arguments
1001///
1002/// * `path` - A `PathBuf` representing the file system path to be converted.
1003///
1004/// # Returns
1005///
1006/// A `String` containing the absolute path if canonicalization succeeds, or the original path as a string otherwise.
1007pub fn to_absolute_string<P>(path: P) -> String
1008where
1009    P: Into<PathBuf> + Clone,
1010{
1011    let result = match std::fs::canonicalize(path.clone().into().as_path()) {
1012        | Ok(value) => value,
1013        | Err(_) => path.into(),
1014    };
1015    result.display().to_string()
1016}
1017/// Prints `text` to stdout using syntax highlighting for the specified `syntax`.
1018///
1019/// `highlight` is an iterator of line numbers to highlight in the output.
1020pub fn pretty_print<I: IntoIterator<Item = usize>>(text: &str, syntax: ProgrammingLanguage, highlight: I) {
1021    let input = format!("{text}\n");
1022    let language = syntax.to_string();
1023    let mut printer = PrettyPrinter::new();
1024    printer
1025        .input_from_bytes(input.as_bytes())
1026        .theme("zenburn")
1027        .language(&language)
1028        .line_numbers(true);
1029    for line in highlight {
1030        printer.highlight(line);
1031    }
1032    printer.print().unwrap();
1033}
1034/// Prints a diff of changes between two strings.
1035///
1036/// If there are no changes between `old` and `new`, prints a debug message indicating so.
1037/// Otherwise, prints a unified diff of the changes, with `+` indicating lines that are
1038/// present in `new` but not `old`, `-` indicating lines that are present in `old` but
1039/// not `new`, and lines that are the same in both are prefixed with a space.
1040pub fn print_changes(old: &str, new: &str) {
1041    let changes = text_diff_changes(old, new);
1042    let has_no_changes = changes.clone().into_iter().all(|(tag, _)| tag == Equal);
1043    if has_no_changes {
1044        debug!("=> {}No format changes", Label::skip());
1045    } else {
1046        for change in changes {
1047            print!("{}", change.1);
1048        }
1049    }
1050}
1051// TODO: Improve flexibility (see https://rust-lang.github.io/api-guidelines/flexibility.html#c-generic)
1052/// Prints the given values as a table.
1053///
1054/// # Arguments
1055///
1056/// * `title` - The title of the table.
1057/// * `headers` - The headers of the table.
1058/// * `rows` - The rows of the table as a vector of vectors of strings.
1059pub fn print_values_as_table(title: &str, headers: Vec<&str>, rows: Vec<Vec<String>>) {
1060    let mut table = Table::new();
1061    table
1062        .load_preset(UTF8_FULL)
1063        .apply_modifier(UTF8_ROUND_CORNERS)
1064        .set_content_arrangement(ContentArrangement::Dynamic)
1065        .set_header(headers);
1066    rows.into_iter().for_each(|row| {
1067        table.add_row(row);
1068    });
1069    println!("=> {} \n{table}", title.green().bold());
1070}
1071/// Reads the given file and returns its contents as a string.
1072///
1073/// # Parameters
1074///
1075/// * `path` - A `PathBuf` or string slice containing the path to the file to be read.
1076///
1077/// # Return
1078///
1079/// A `Result` containing the contents of the file as a string if the file is readable, or an
1080/// `std::io::Error` otherwise.
1081pub fn read_file<P>(path: P) -> Result<String, std::io::Error>
1082where
1083    P: Into<PathBuf> + Clone,
1084{
1085    let mut content = String::new();
1086    let _ = match File::open(path.clone().into()) {
1087        | Ok(mut file) => {
1088            debug!(path = to_absolute_string(path.into()), "=> {}", Label::read());
1089            file.read_to_string(&mut content)
1090        }
1091        | Err(why) => {
1092            error!(path = to_absolute_string(path.into()), "=> {} Read file", Label::fail());
1093            Err(why)
1094        }
1095    };
1096    Ok(content)
1097}
1098/// Helper function to create a lookup dictionary for regex captures
1099/// ### Example
1100/// ```rust
1101/// use acorn_lib::util::regex_capture_lookup;
1102/// let lookup = regex_capture_lookup(
1103///     r"(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})",
1104///     "2023-06-30",
1105///     vec!["year", "month", "day"]
1106/// );
1107/// assert_eq!(lookup["year"], "2023");
1108/// assert_eq!(lookup["month"], "06");
1109/// assert_eq!(lookup["day"], "30");
1110/// ```
1111pub fn regex_capture_lookup<S>(pattern: S, value: S, names: Vec<S>) -> HashMap<S, String>
1112where
1113    S: Into<String> + AsRef<str> + Clone + std::cmp::Eq + std::hash::Hash,
1114{
1115    let re = Regex::new(pattern.as_ref()).unwrap();
1116    let mut lookup: HashMap<S, String> = HashMap::new();
1117    if let Some(capture_matches) = re.captures_iter(value.as_ref()).last() {
1118        match capture_matches {
1119            | Ok(captures) => {
1120                captures.iter().skip(1).enumerate().for_each(|(index, data)| {
1121                    if let Some(results) = data {
1122                        let key = names[index].clone();
1123                        let value = results.as_str().to_string();
1124                        lookup.insert(key, value);
1125                    }
1126                });
1127            }
1128            | Err(_) => (),
1129        }
1130    };
1131    lookup
1132}
1133/// Converts the given string to snake case.
1134/// ### Example
1135/// ```rust
1136/// use acorn_lib::util::snake_case;
1137///
1138/// let snake = snake_case("CamelCase");
1139/// assert_eq!(snake, "camel_case");
1140/// ```
1141pub fn snake_case<S>(value: S) -> String
1142where
1143    S: Into<String>,
1144{
1145    value.into().to_case(Case::Snake)
1146}
1147/// Returns path to a folder in the operating system's cache directory that is unique to the given
1148/// `namespace` with a random UUID as the name of the final folder.
1149///
1150/// The folder is ***not*** created.
1151///
1152/// Used primarily by ACORN CLI where `namespace` is of a subcommand task. e.g. "check", "extract", etc.
1153///
1154/// # Arguments
1155///
1156/// * `namespace` - A string slice representing the name of the namespace.
1157/// * `default` - An optional `PathBuf` to use as the root directory instead of the cache directory.
1158///
1159/// # Returns
1160///
1161/// A `PathBuf` to the folder.
1162pub fn standard_project_folder(namespace: &str, default: Option<PathBuf>) -> PathBuf {
1163    let root = match default {
1164        | Some(value) => value,
1165        | None => match ProjectDirs::from(QUALIFIER, ORGANIZATION, APPLICATION) {
1166            | Some(dirs) => dirs.cache_dir().join(namespace).to_path_buf(),
1167            | None => PathBuf::from(format!("./{namespace}")),
1168        },
1169    };
1170    match create_dir_all(root.clone()) {
1171        | Ok(_) => {}
1172        | Err(why) => error!(directory = to_absolute_string(root.clone()), "=> {} Create - {}", Label::fail(), why),
1173    };
1174    root.join(generate_guid())
1175}
1176/// Returns "s" if the given value is not 1, otherwise returns an empty string.
1177/// ### Example
1178/// ```
1179/// use acorn_lib::util::suffix;
1180///
1181/// assert_eq!(suffix(1), "");
1182/// assert_eq!(suffix(2), "s");
1183/// ```
1184pub fn suffix(value: usize) -> String {
1185    (if value == 1 { "" } else { "s" }).to_string()
1186}
1187/// Computes the differences between two strings line by line and returns a vector of changes.
1188///
1189/// Each change is represented as a tuple containing a `ChangeTag` indicating the type of change
1190/// (deletion, insertion, or equality) and a `String` with the formatted line prefixed with a
1191/// symbol indicating the type of change (`-` for deletions, `+` for insertions, and a space for equal lines).
1192///
1193/// The formatted string is also colored: red for deletions, green for insertions, and dimmed for equal lines.
1194///
1195/// # Arguments
1196///
1197/// * `old` - A string slice representing the original text.
1198/// * `new` - A string slice representing the modified text.
1199///
1200/// # Returns
1201///
1202/// A vector of tuples, each containing a `ChangeTag` and a formatted `String` representing the changes.
1203pub fn text_diff_changes(old: &str, new: &str) -> Vec<(ChangeTag, String)> {
1204    TextDiff::from_lines(old, new)
1205        .iter_all_changes()
1206        .map(|line| {
1207            let tag = line.tag();
1208            let text = match tag {
1209                | Delete => format!("- {line}").red().to_string(),
1210                | Insert => format!("+ {line}").green().to_string(),
1211                | Equal => format!("  {line}").dimmed().to_string(),
1212            };
1213            (tag, text)
1214        })
1215        .collect::<Vec<_>>()
1216}
1217/// Create a new [Tokio](https://tokio.rs/) runtime
1218/// ### Example
1219/// ```ignore
1220/// tokio_runtime().block_on(async {
1221///     // ...async stuff
1222/// });
1223/// ```
1224pub fn tokio_runtime() -> tokio::runtime::Runtime {
1225    debug!("=> {} Tokio runtime", Label::using());
1226    tokio::runtime::Builder::new_multi_thread().enable_all().build().unwrap()
1227}
1228/// Convert a vector of string slices to a vector of strings
1229pub fn to_string(values: Vec<&str>) -> Vec<String> {
1230    values.iter().map(|s| s.to_string()).collect()
1231}
1232/// Writes the given content to a file at the given path.
1233///
1234/// # Arguments
1235///
1236/// * `path` - A `PathBuf` or string slice containing the path to the file to be written.
1237/// * `content` - A `String` containing the content to be written to the file.
1238///
1239/// # Return
1240///
1241/// A `Result` containing a unit value if the file is written successfully, or an
1242/// `std::io::Error` otherwise.
1243pub fn write_file<P>(path: P, content: String) -> Result<(), std::io::Error>
1244where
1245    P: Into<PathBuf>,
1246{
1247    match File::create(path.into().clone()) {
1248        | Ok(mut file) => {
1249            file.write_all(content.as_bytes()).unwrap();
1250            file.flush()
1251        }
1252        | Err(why) => {
1253            error!("=> {} Cannot create file - {why}", Label::fail());
1254            Err(why)
1255        }
1256    }
1257}
1258
1259#[cfg(test)]
1260mod tests;