use std::{
collections::HashMap,
path::{Path, PathBuf},
};
pub const BACKUP_EXT: &str = "dotrbak";
pub const SYMLINK_FOLDER: &str = "deployed";
pub fn resolve_path(path: &str, cwd: &Path) -> PathBuf {
if path.starts_with('/') {
PathBuf::from(path)
} else if path.starts_with("~") {
let home_dir = std::env::home_dir().expect("Failed to get home directory");
let p = path.splitn(2, '/').collect::<Vec<&str>>();
home_dir.join(p[1..].join("/"))
} else {
let p = cwd.join(path);
std::path::absolute(&p).expect("Failed to get absolute path")
}
}
pub fn normalize_home_path(path: &str) -> String {
if path.starts_with('~') {
return path.to_string();
}
if let Some(home_dir) = std::env::home_dir() {
let home_str = home_dir.to_string_lossy();
if path == home_str.as_ref() {
return "~".to_string();
}
let home_with_slash = format!("{}/", home_str);
if path.starts_with(&home_with_slash) {
let relative = &path[home_str.len()..];
return format!("~{}", relative);
}
}
path.to_string()
}
pub const COLOR_WARNING: &str = "\x1b[33m"; pub const COLOR_ERROR: &str = "\x1b[31m"; pub const COLOR_INFO: &str = "\x1b[34m"; pub const COLOR_FATAL: &str = "\x1b[35m"; pub const RESET_COLOR: &str = "\x1b[0m";
pub enum LogLevel {
WARNING,
ERROR,
INFO,
FATAL,
}
impl LogLevel {
pub fn as_str(&self) -> &str {
match self {
LogLevel::WARNING => "WARNING",
LogLevel::ERROR => "ERROR",
LogLevel::INFO => "INFO",
LogLevel::FATAL => "FATAL",
}
}
pub fn to_colorful_str(&self) -> String {
match self {
LogLevel::WARNING => format!("{}[{}]{}", COLOR_WARNING, self.as_str(), RESET_COLOR),
LogLevel::ERROR => format!("{}[{}]{}", COLOR_ERROR, self.as_str(), RESET_COLOR),
LogLevel::INFO => format!("{}[{}]{}", COLOR_INFO, self.as_str(), RESET_COLOR),
LogLevel::FATAL => format!("{}[{}]{}", COLOR_FATAL, self.as_str(), RESET_COLOR),
}
}
}
pub fn cprintln(message: &str, level: &LogLevel) {
match level {
LogLevel::ERROR | LogLevel::FATAL => {
eprintln!("{} {}", level.to_colorful_str(), message);
}
LogLevel::WARNING | LogLevel::INFO => {
println!("{} {}", level.to_colorful_str(), message);
}
}
}
pub fn get_string_from_value(v: Option<&toml::Value>) -> anyhow::Result<String> {
Ok(v.ok_or_else(|| anyhow::anyhow!("Package src is required"))?
.as_str()
.ok_or_else(|| anyhow::anyhow!("Package src must be a string"))?
.to_string())
}
pub fn get_string_hashmap_from_value(
v: Option<&toml::Value>,
) -> anyhow::Result<HashMap<String, String>> {
match v {
Some(value) => value
.as_table()
.ok_or_else(|| anyhow::anyhow!("The 'prompts' field must be a table"))?
.iter()
.map(|(key, value)| {
let prompt_str = value
.as_str()
.ok_or_else(|| anyhow::anyhow!("Prompt message must be a string"))?;
Ok((key.clone(), prompt_str.to_string()))
})
.collect::<Result<HashMap<_, _>, _>>(),
None => Ok(HashMap::new()),
}
}
pub fn vec_string_to_toml_array(vec: &[String]) -> toml::Value {
toml::Value::Array(vec.iter().map(|a| toml::Value::String(a.clone())).collect())
}
pub fn string_hashmap_to_toml_table(map: &HashMap<String, String>) -> toml::Value {
toml::Value::Table(
map.iter()
.map(|(k, v)| (k.clone(), toml::Value::String(v.clone())))
.collect(),
)
}
pub fn get_vec_string_from_value(v: Option<&toml::Value>) -> anyhow::Result<Vec<String>> {
match v {
Some(block) => block
.as_array()
.ok_or_else(|| anyhow::anyhow!("The 'pre_actions' field must be an array"))?
.iter()
.map(|v| {
v.as_str()
.ok_or_else(|| anyhow::anyhow!("Pre-action must be a string"))
.map(|s| s.to_string())
})
.collect::<Result<Vec<_>, _>>(),
None => Ok(Vec::new()),
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
#[test]
fn test_resolve_path_absolute() {
let cwd = PathBuf::from("/some/cwd");
let path = "/absolute/path";
let resolved = resolve_path(path, &cwd);
assert_eq!(resolved, PathBuf::from("/absolute/path"));
}
#[test]
fn test_resolve_path_with_tilde() {
let cwd = PathBuf::from("/some/cwd");
let home = std::env::home_dir().expect("Failed to get home directory");
let path = "~/Documents";
let resolved = resolve_path(path, &cwd);
assert_eq!(resolved, home.join("Documents"));
let path = "~";
let resolved = resolve_path(path, &cwd);
assert_eq!(resolved, home);
}
#[test]
fn test_resolve_path_relative() {
let cwd = PathBuf::from("/some/cwd");
let path = "relative/path";
let resolved = resolve_path(path, &cwd);
assert!(resolved.is_absolute());
assert!(resolved.ends_with("relative/path"));
}
#[test]
fn test_resolve_path_dot_relative() {
let cwd = PathBuf::from("/some/cwd");
let path = "./file.txt";
let resolved = resolve_path(path, &cwd);
assert!(resolved.is_absolute());
assert!(resolved.ends_with("file.txt"));
}
#[test]
fn test_resolve_path_parent_relative() {
let cwd = PathBuf::from("/some/cwd/subdir");
let path = "../file.txt";
let resolved = resolve_path(path, &cwd);
assert!(resolved.is_absolute());
}
#[test]
fn test_normalize_home_path_already_normalized() {
let path = "~/.config/nvim";
let normalized = normalize_home_path(path);
assert_eq!(normalized, "~/.config/nvim");
}
#[test]
fn test_normalize_home_path_in_home_directory() {
let home = std::env::home_dir().expect("Failed to get home directory");
let home_str = home.to_string_lossy();
let path = format!("{}/.config/nvim", home_str);
let normalized = normalize_home_path(&path);
assert_eq!(normalized, "~/.config/nvim");
}
#[test]
fn test_normalize_home_path_home_root() {
let home = std::env::home_dir().expect("Failed to get home directory");
let home_str = home.to_string_lossy().to_string();
let normalized = normalize_home_path(&home_str);
assert_eq!(normalized, "~");
}
#[test]
fn test_normalize_home_path_outside_home() {
let path = "/etc/config";
let normalized = normalize_home_path(path);
assert_eq!(normalized, "/etc/config");
let path = "/tmp/test";
let normalized = normalize_home_path(path);
assert_eq!(normalized, "/tmp/test");
}
#[test]
fn test_normalize_home_path_with_trailing_slash() {
let home = std::env::home_dir().expect("Failed to get home directory");
let home_str = home.to_string_lossy();
let path = format!("{}/.config/", home_str);
let normalized = normalize_home_path(&path);
assert_eq!(normalized, "~/.config/");
}
#[test]
fn test_normalize_home_path_deep_nested() {
let home = std::env::home_dir().expect("Failed to get home directory");
let home_str = home.to_string_lossy();
let path = format!("{}/a/b/c/d/e/f", home_str);
let normalized = normalize_home_path(&path);
assert_eq!(normalized, "~/a/b/c/d/e/f");
}
#[test]
fn test_backup_ext_constant() {
assert_eq!(BACKUP_EXT, "dotrbak");
}
#[test]
fn test_resolve_path_empty_relative() {
let cwd = PathBuf::from("/some/cwd");
let path = "";
let resolved = resolve_path(path, &cwd);
assert!(resolved.is_absolute());
}
#[test]
fn test_normalize_home_path_similar_prefix() {
let home = std::env::home_dir().expect("Failed to get home directory");
let home_str = home.to_string_lossy();
let fake_path = format!("{}_fake/config", home_str);
let normalized = normalize_home_path(&fake_path);
assert_eq!(normalized, fake_path);
}
#[test]
fn test_resolve_and_normalize_round_trip() {
let cwd = PathBuf::from("/some/cwd");
let home = std::env::home_dir().expect("Failed to get home directory");
let original = "~/.bashrc";
let resolved = resolve_path(original, &cwd);
assert_eq!(resolved, home.join(".bashrc"));
let normalized = normalize_home_path(resolved.to_string_lossy().as_ref());
assert_eq!(normalized, original);
}
#[test]
fn test_normalize_home_path_with_spaces() {
let home = std::env::home_dir().expect("Failed to get home directory");
let home_str = home.to_string_lossy();
let path = format!("{}/My Documents/file.txt", home_str);
let normalized = normalize_home_path(&path);
assert_eq!(normalized, "~/My Documents/file.txt");
}
#[test]
fn test_normalize_home_path_with_dots() {
let home = std::env::home_dir().expect("Failed to get home directory");
let home_str = home.to_string_lossy();
let path = format!("{}/.config/.hidden/..dotfile", home_str);
let normalized = normalize_home_path(&path);
assert_eq!(normalized, "~/.config/.hidden/..dotfile");
}
}