use std::path::Path;
use crate::Result;
use chrono::{DateTime, Datelike as _, Local, Timelike as _};
use once_cell::sync::Lazy;
use regex::Regex;
pub trait MyParseFormat {
fn parse_all(&self) -> Result<String>;
fn parse_format(&self) -> Result<String>;
fn parse_replace<F>(&self, start: char, end: char, match_value: F) -> Result<String>
where
F: Fn(String) -> String;
fn parse_replace_zh<F>(&self, start: char, end: char, match_value: F) -> Result<String>
where
F: Fn(String) -> String;
fn parse_env(&self) -> Result<String>;
fn parse_path(&self) -> String;
}
pub static REGEX_FORMAT_PATTERN: Lazy<Regex> = Lazy::new(|| Regex::new(r"\{([a-zA-Z0-9_]+)\}").expect("Invalid regex pattern for REGEX_FORMAT_PATTERN"));
#[cfg(target_os = "windows")]
pub static REGEX_ENV_PATTERN: Lazy<Regex> =
Lazy::new(|| Regex::new(r"\%([a-zA-Z0-9_]+)\%").expect("Invalid regex pattern for REGEX_ENV_PATTERN on Windows"));
#[cfg(any(target_os = "linux", target_os = "macos"))]
pub static REGEX_ENV_PATTERN: Lazy<Regex> =
Lazy::new(|| Regex::new(r"\$\{([a-zA-Z0-9_]+)\}").expect("Invalid regex pattern for REGEX_ENV_PATTERN on Unix-like systems"));
#[cfg(not(any(target_os = "linux", target_os = "windows", target_os = "macos")))]
pub static REGEX_ENV_PATTERN: Lazy<Regex> =
Lazy::new(|| Regex::new(r"\$([a-zA-Z0-9_]+)").expect("Invalid regex pattern for REGEX_ENV_PATTERN on other systems"));
impl<S: AsRef<str>> MyParseFormat for S {
fn parse_all(&self) -> Result<String> {
self.as_ref().parse_format()?.parse_path().parse_env()
}
fn parse_format(&self) -> Result<String> {
let local: DateTime<Local> = Local::now();
Ok(
REGEX_FORMAT_PATTERN
.replace_all(self.as_ref(), |caps: ®ex::Captures<'_>| {
let key: String = caps.get(1).map_or("", |m| m.as_str()).to_lowercase();
match &*key {
"date" => format!("{:04}-{:02}-{:02}", local.year(), local.month(), local.day()),
"time" => format!("{:02}:{:02}:{:02}", local.hour(), local.minute(), local.second()),
"timestamp-millis" => local.timestamp_millis().to_string(),
"day" => format!("{:02}", local.day()),
"month" => format!("{:02}", local.month()),
"year" => format!("{:02}", local.year()),
"hour" => format!("{:02}", local.hour()),
"minute" => format!("{:02}", local.minute()),
"second" => format!("{:02}", local.second()),
"cwd" => std::env::current_dir().and_then(|x| Ok(x.display().to_string())).unwrap_or_default(),
"nanoid" => {
#[cfg(feature = "algorithm")]
return crate::algorithm!(nanoid 12);
#[cfg(not(feature = "algorithm"))]
return String::new();
}
_ => String::new(),
}
})
.to_string(),
)
}
fn parse_env(&self) -> Result<String> {
Ok(
REGEX_ENV_PATTERN
.replace_all(self.as_ref(), |caps: ®ex::Captures<'_>| {
let key = caps.get(1).map_or("", |m| m.as_str());
let var = std::env::var(key).unwrap_or_default();
var
})
.to_string(),
)
}
fn parse_path(&self) -> String {
self.as_ref().replace("\\\\", "/").replace("\\", "/")
}
fn parse_replace<F>(&self, start: char, end: char, match_callback: F) -> Result<String>
where
F: Fn(String) -> String,
{
let re = regex::Regex::new(&format!(r"\{}([a-zA-Z0-9_]+)\{}", start, end))?;
Ok(
re.replace_all(self.as_ref(), |caps: ®ex::Captures<'_>| {
let key: String = caps.get(1).map_or("", |m| m.as_str()).to_lowercase();
match_callback(key)
})
.to_string(),
)
}
fn parse_replace_zh<F>(&self, start: char, end: char, match_callback: F) -> Result<String>
where
F: Fn(String) -> String,
{
let re = regex::Regex::new(&format!(r"\{}([\p{{Han}}a-zA-Z0-9_@$]+)\{}", start, end))?;
Ok(
re.replace_all(self.as_ref(), |caps: ®ex::Captures<'_>| {
let key: String = caps.get(1).map_or("", |m| m.as_str()).to_string();
match_callback(key)
})
.to_string(),
)
}
}
impl MyParseFormat for Path {
fn parse_format(&self) -> Result<String> {
self.to_string_lossy().parse_format()
}
fn parse_env(&self) -> Result<String> {
self.to_string_lossy().parse_env()
}
fn parse_all(&self) -> Result<String> {
self.to_string_lossy().parse_all()
}
fn parse_path(&self) -> String {
self.to_string_lossy().parse_path()
}
fn parse_replace<F>(&self, start: char, end: char, match_value: F) -> Result<String>
where
F: Fn(String) -> String,
{
self.to_string_lossy().parse_replace(start, end, match_value)
}
fn parse_replace_zh<F>(&self, start: char, end: char, match_value: F) -> Result<String>
where
F: Fn(String) -> String,
{
self.to_string_lossy().parse_replace_zh(start, end, match_value)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::env;
#[test]
fn test_parse_all() {
env::set_var("TEST_ENV", "test_value");
let input = "Date: {date}, Time: {time}, Env: %TEST_ENV%";
let result = input.parse_all().unwrap();
assert!(result.contains("Date: "));
assert!(result.contains("Time: "));
assert!(result.contains("Env: test_value"));
}
#[test]
fn test_parse_format() {
let input = "{date} {time} {day} {month} {year} {hour} {minute} {second} {cwd}";
let result = input.parse_format().unwrap();
println!("{result}");
assert!(!result.contains("{"));
assert!(!result.contains("}"));
}
#[test]
fn test_parse_replace() {
let input = "Hello [name] [age]";
let result = input
.parse_replace('[', ']', |key| match key.as_str() {
"name" => "Alice".to_string(),
"age" => "30".to_string(),
_ => key,
})
.unwrap();
assert_eq!(result, "Hello Alice 30");
}
#[test]
fn test_parse_env() {
env::set_var("TEST_VAR", "test_value");
#[cfg(target_os = "windows")]
let input = "Path: %TEST_VAR%";
#[cfg(any(target_os = "linux", target_os = "macos"))]
let input = "Path: ${TEST_VAR}";
#[cfg(not(any(target_os = "linux", target_os = "windows", target_os = "macos")))]
let input = "Path: $TEST_VAR";
let result = input.parse_env().unwrap();
assert_eq!(result, "Path: test_value");
}
#[test]
fn test_parse_path() {
let input = r"C:\Users\Alice\Documents";
let result = input.parse_path();
assert_eq!(result, "C:/Users/Alice/Documents");
}
#[test]
fn test_path_parse_format() {
let path = Path::new("/home/user/{date}");
let result = path.parse_format().unwrap();
assert!(!result.contains("{date}"));
}
#[test]
fn test_path_parse_env() {
env::set_var("HOME", "/home/user");
#[cfg(target_os = "windows")]
let path = Path::new("%HOME%\\documents");
#[cfg(any(target_os = "linux", target_os = "macos"))]
let path = Path::new("${HOME}/documents");
#[cfg(not(any(target_os = "linux", target_os = "windows", target_os = "macos")))]
let path = Path::new("$HOME/documents");
let result = path.parse_env().unwrap();
assert!(result.contains("/home/user"));
}
#[test]
fn test_path_parse_path() {
let path = Path::new("C:\\Users\\Alice\\Documents");
let result = path.parse_path();
assert_eq!(result, "C:/Users/Alice/Documents");
}
#[test]
fn test_path_parse_replace() {
let path = Path::new("/home/[user]/[folder]");
let result = path
.parse_replace('[', ']', |key| match key.as_str() {
"user" => "alice".to_string(),
"folder" => "documents".to_string(),
_ => key,
})
.unwrap();
assert_eq!(result, "/home/alice/documents");
}
}