use std::{
fs,
path::{Path, PathBuf},
time::SystemTime,
};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum MergeOrder {
Alphabetical,
Reverse,
SizeAscending,
SizeDescending,
ModificationTimeAscending,
ModificationTimeDescending,
Custom(Vec<String>),
Hierarchical,
AsDiscovered,
}
impl Default for MergeOrder {
fn default() -> Self {
Self::Alphabetical
}
}
impl MergeOrder {
pub fn sort_files(&self, mut files: Vec<PathBuf>) -> Vec<PathBuf> {
match self {
MergeOrder::Alphabetical => {
files.sort_by(|a, b| {
a.file_name()
.and_then(|n| n.to_str())
.unwrap_or("")
.cmp(b.file_name().and_then(|n| n.to_str()).unwrap_or(""))
});
files
}
MergeOrder::Reverse => {
files.sort_by(|a, b| {
b.file_name()
.and_then(|n| n.to_str())
.unwrap_or("")
.cmp(a.file_name().and_then(|n| n.to_str()).unwrap_or(""))
});
files
}
MergeOrder::SizeAscending => {
files.sort_by(|a, b| {
let size_a = file_size(a).unwrap_or(0);
let size_b = file_size(b).unwrap_or(0);
size_a.cmp(&size_b)
});
files
}
MergeOrder::SizeDescending => {
files.sort_by(|a, b| {
let size_a = file_size(a).unwrap_or(0);
let size_b = file_size(b).unwrap_or(0);
size_b.cmp(&size_a)
});
files
}
MergeOrder::ModificationTimeAscending => {
files.sort_by(|a, b| {
let time_a = modification_time(a).unwrap_or(SystemTime::UNIX_EPOCH);
let time_b = modification_time(b).unwrap_or(SystemTime::UNIX_EPOCH);
time_a.cmp(&time_b)
});
files
}
MergeOrder::ModificationTimeDescending => {
files.sort_by(|a, b| {
let time_a = modification_time(a).unwrap_or(SystemTime::UNIX_EPOCH);
let time_b = modification_time(b).unwrap_or(SystemTime::UNIX_EPOCH);
time_b.cmp(&time_a)
});
files
}
MergeOrder::Custom(patterns) => {
files.sort_by(|a, b| {
let priority_a = pattern_priority(a, patterns);
let priority_b = pattern_priority(b, patterns);
match priority_a.cmp(&priority_b) {
std::cmp::Ordering::Equal => {
a.file_name()
.and_then(|n| n.to_str())
.unwrap_or("")
.cmp(b.file_name().and_then(|n| n.to_str()).unwrap_or(""))
}
other => other,
}
});
files
}
MergeOrder::Hierarchical => {
files.sort_by(|a, b| {
let priority_a = hierarchical_priority(a);
let priority_b = hierarchical_priority(b);
match priority_a.cmp(&priority_b) {
std::cmp::Ordering::Equal => {
a.file_name()
.and_then(|n| n.to_str())
.unwrap_or("")
.cmp(b.file_name().and_then(|n| n.to_str()).unwrap_or(""))
}
other => other,
}
});
files
}
MergeOrder::AsDiscovered => {
files
}
}
}
}
fn file_size(path: &Path) -> Option<u64> {
fs::metadata(path).ok().map(|meta| meta.len())
}
fn modification_time(path: &Path) -> Option<SystemTime> {
fs::metadata(path)
.ok()
.and_then(|meta| meta.modified().ok())
}
fn hierarchical_priority(path: &Path) -> usize {
let path_str = path.to_string_lossy();
if path_str.contains("/.config/") {
0 } else if path_str.starts_with("./")
&& path_str.contains("/.")
&& !path_str.contains("/.config/")
{
1 } else if path_str.starts_with("./") || path_str.contains("**/") {
2 } else {
3 }
}
fn pattern_priority(path: &Path, patterns: &[String]) -> usize {
let filename = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
for (index, pattern) in patterns.iter().enumerate() {
if pattern_matches(filename, pattern) {
return index;
}
}
usize::MAX }
fn pattern_matches(filename: &str, pattern: &str) -> bool {
if pattern.contains('*') {
let parts: Vec<&str> = pattern.split('*').collect();
if parts.len() == 2 {
let prefix = parts[0];
let suffix = parts[1];
filename.starts_with(prefix) && filename.ends_with(suffix)
} else if let Some(stripped) = pattern.strip_prefix('*') {
filename.ends_with(stripped)
} else if let Some(stripped) = pattern.strip_suffix('*') {
filename.starts_with(stripped)
} else {
filename == pattern
}
} else {
filename == pattern
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
#[test]
fn test_alphabetical_sorting() {
let files = vec![
PathBuf::from("config-z.toml"),
PathBuf::from("config-a.toml"),
PathBuf::from("config-m.toml"),
];
let sorted = MergeOrder::Alphabetical.sort_files(files);
assert_eq!(sorted[0].file_name().unwrap(), "config-a.toml");
assert_eq!(sorted[1].file_name().unwrap(), "config-m.toml");
assert_eq!(sorted[2].file_name().unwrap(), "config-z.toml");
}
#[test]
fn test_reverse_sorting() {
let files = vec![
PathBuf::from("config-a.toml"),
PathBuf::from("config-z.toml"),
PathBuf::from("config-m.toml"),
];
let sorted = MergeOrder::Reverse.sort_files(files);
assert_eq!(sorted[0].file_name().unwrap(), "config-z.toml");
assert_eq!(sorted[1].file_name().unwrap(), "config-m.toml");
assert_eq!(sorted[2].file_name().unwrap(), "config-a.toml");
}
#[test]
fn test_pattern_matching() {
assert!(pattern_matches("config.toml", "config.*"));
assert!(pattern_matches("base.yaml", "base.*"));
assert!(pattern_matches("env-prod.toml", "env-*.toml"));
assert!(pattern_matches("local", "local"));
assert!(!pattern_matches("other.json", "config.*"));
}
#[test]
fn test_custom_sorting() {
let files = vec![
PathBuf::from("local.toml"),
PathBuf::from("base.yaml"),
PathBuf::from("env-prod.toml"),
PathBuf::from("other.json"),
];
let patterns = vec![
"base.*".to_string(),
"env-*.toml".to_string(),
"local.*".to_string(),
];
let sorted = MergeOrder::Custom(patterns).sort_files(files);
assert_eq!(sorted[0].file_name().unwrap(), "base.yaml"); assert_eq!(sorted[1].file_name().unwrap(), "env-prod.toml"); assert_eq!(sorted[2].file_name().unwrap(), "local.toml"); assert_eq!(sorted[3].file_name().unwrap(), "other.json"); }
}