use globset::{Glob, GlobSetBuilder};
use std::path::PathBuf;
use super::discovery::SearchStrategy;
pub fn parse_pattern(pattern: &str) -> Result<(SearchStrategy, Vec<String>), figment::Error> {
if pattern.is_empty() {
return Err(figment::Error::from("Pattern cannot be empty"));
}
if let Some((directories, file_pattern)) = parse_brace_expansion(pattern) {
return Ok((SearchStrategy::Directories(directories), vec![file_pattern]));
}
if pattern.contains("**/") {
let (dirs, file_pattern) = parse_recursive_pattern(pattern);
return Ok((
SearchStrategy::Recursive {
roots: dirs,
max_depth: None,
},
vec![file_pattern],
));
}
if pattern.contains('/') {
let (dir, file_pattern) = parse_path_pattern(pattern);
return Ok((SearchStrategy::Directories(vec![dir]), vec![file_pattern]));
}
Ok((SearchStrategy::Current, vec![pattern.to_string()]))
}
fn parse_brace_expansion(pattern: &str) -> Option<(Vec<PathBuf>, String)> {
if !pattern.starts_with('{') {
return None;
}
let close_brace = pattern.find('}')?;
let dirs_part = &pattern[1..close_brace]; let rest = &pattern[close_brace + 1..];
let directories: Vec<PathBuf> = dirs_part
.split(',')
.map(|d| PathBuf::from(d.trim()))
.collect();
let file_pattern = if let Some(stripped) = rest.strip_prefix('/') {
stripped.to_string()
} else {
rest.to_string()
};
Some((directories, file_pattern))
}
fn parse_recursive_pattern(pattern: &str) -> (Vec<PathBuf>, String) {
if let Some(double_star_pos) = pattern.find("**/") {
let prefix = &pattern[..double_star_pos];
let suffix = &pattern[double_star_pos + 3..];
let root_dir = if prefix.is_empty() || prefix == "./" {
PathBuf::from(".")
} else {
PathBuf::from(prefix.trim_end_matches('/'))
};
(vec![root_dir], suffix.to_string())
} else {
let prefix = pattern.trim_end_matches("/**");
let root_dir = if prefix.is_empty() {
PathBuf::from(".")
} else {
PathBuf::from(prefix)
};
(vec![root_dir], "*".to_string())
}
}
fn parse_path_pattern(pattern: &str) -> (PathBuf, String) {
if let Some(last_slash) = pattern.rfind('/') {
let dir_part = &pattern[..last_slash];
let file_part = &pattern[last_slash + 1..];
let directory = if dir_part.is_empty() {
PathBuf::from("/") } else {
PathBuf::from(dir_part)
};
(directory, file_part.to_string())
} else {
(PathBuf::from("."), pattern.to_string())
}
}
pub fn build_globset(patterns: &[impl AsRef<str>]) -> Result<globset::GlobSet, figment::Error> {
let mut builder = GlobSetBuilder::new();
for pattern in patterns {
let glob = Glob::new(pattern.as_ref()).map_err(|e| {
figment::Error::from(format!("Invalid pattern '{}': {}", pattern.as_ref(), e))
})?;
builder.add(glob);
}
builder
.build()
.map_err(|e| figment::Error::from(format!("Failed to build globset: {e}")))
}
pub fn parse_multiple_patterns(
patterns: &[impl AsRef<str>],
) -> Result<(SearchStrategy, Vec<String>), figment::Error> {
if patterns.is_empty() {
return Err(figment::Error::from("At least one pattern is required"));
}
if patterns.len() == 1 {
return parse_pattern(patterns[0].as_ref());
}
let mut all_strategies = Vec::new();
let mut all_file_patterns = Vec::new();
for pattern in patterns {
let (strategy, file_patterns) = parse_pattern(pattern.as_ref())?;
all_strategies.push(strategy);
all_file_patterns.extend(file_patterns);
}
let combined_strategy = resolve_combined_strategy(all_strategies);
Ok((combined_strategy, all_file_patterns))
}
fn resolve_combined_strategy(strategies: Vec<SearchStrategy>) -> SearchStrategy {
let mut directories = Vec::new();
let mut recursive_roots = Vec::new();
let mut has_recursive = false;
let mut has_custom = false;
let mut current_count = 0;
let strategies_len = strategies.len();
for strategy in strategies {
match strategy {
SearchStrategy::Directories(dirs) => {
directories.extend(dirs);
}
SearchStrategy::Recursive { roots, .. } => {
has_recursive = true;
recursive_roots.extend(roots);
}
SearchStrategy::Current => {
current_count += 1;
directories.push(PathBuf::from("."));
}
SearchStrategy::Custom(_) => {
has_custom = true;
recursive_roots.push(PathBuf::from("."));
has_recursive = true;
}
}
}
if has_recursive || has_custom {
recursive_roots.sort();
recursive_roots.dedup();
SearchStrategy::Recursive {
roots: recursive_roots,
max_depth: None,
}
} else if current_count == strategies_len {
SearchStrategy::Current
} else {
directories.sort();
directories.dedup();
SearchStrategy::Directories(directories)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_simple_pattern() {
let (strategy, patterns) = parse_pattern("*.toml").unwrap();
match strategy {
SearchStrategy::Current => {}
_ => panic!("Expected Current strategy"),
}
assert_eq!(patterns, vec!["*.toml"]);
}
#[test]
fn test_parse_path_pattern() {
let (strategy, patterns) = parse_pattern("./config/*.yaml").unwrap();
match strategy {
SearchStrategy::Directories(dirs) => {
assert_eq!(dirs.len(), 1);
assert_eq!(dirs[0], PathBuf::from("./config"));
}
_ => panic!("Expected Directories strategy"),
}
assert_eq!(patterns, vec!["*.yaml"]);
}
#[test]
fn test_parse_recursive_pattern() {
let (strategy, patterns) = parse_pattern("**/*.json").unwrap();
match strategy {
SearchStrategy::Recursive { roots, max_depth } => {
assert_eq!(roots.len(), 1);
assert_eq!(roots[0], PathBuf::from("."));
assert_eq!(max_depth, None);
}
_ => panic!("Expected Recursive strategy"),
}
assert_eq!(patterns, vec!["*.json"]);
}
#[test]
fn test_parse_brace_expansion() {
let (strategy, patterns) = parse_pattern("{./config,~/.config}/*.toml").unwrap();
match strategy {
SearchStrategy::Directories(dirs) => {
assert_eq!(dirs.len(), 2);
assert!(dirs.contains(&PathBuf::from("./config")));
assert!(dirs.contains(&PathBuf::from("~/.config")));
}
_ => panic!("Expected Directories strategy"),
}
assert_eq!(patterns, vec!["*.toml"]);
}
#[test]
fn test_parse_multiple_patterns_same_type() {
let patterns = vec!["*.toml".to_string(), "*.yaml".to_string()];
let (strategy, combined_patterns) = parse_multiple_patterns(&patterns).unwrap();
match strategy {
SearchStrategy::Current => {}
_ => panic!("Expected Current strategy for simple patterns"),
}
assert_eq!(combined_patterns.len(), 2);
assert!(combined_patterns.contains(&"*.toml".to_string()));
assert!(combined_patterns.contains(&"*.yaml".to_string()));
}
#[test]
fn test_parse_multiple_patterns_mixed_types() {
let patterns = vec!["./config/*.toml".to_string(), "**/*.yaml".to_string()];
let (strategy, _) = parse_multiple_patterns(&patterns).unwrap();
match strategy {
SearchStrategy::Recursive { .. } => {}
_ => panic!("Expected Recursive strategy for mixed patterns"),
}
}
#[test]
fn test_build_globset() {
let patterns = vec!["*.toml".to_string(), "*.yaml".to_string()];
let globset = build_globset(&patterns).unwrap();
assert!(globset.is_match("config.toml"));
assert!(globset.is_match("app.yaml"));
assert!(!globset.is_match("readme.txt"));
}
#[test]
fn test_parse_path_pattern_helper() {
let (dir, file) = parse_path_pattern("./config/app.toml");
assert_eq!(dir, PathBuf::from("./config"));
assert_eq!(file, "app.toml");
}
#[test]
fn test_parse_recursive_pattern_helper() {
let (roots, pattern) = parse_recursive_pattern("./config/**/*.yaml");
assert_eq!(roots, vec![PathBuf::from("./config")]);
assert_eq!(pattern, "*.yaml");
}
#[test]
fn test_parse_brace_expansion_helper() {
let result = parse_brace_expansion("{dir1,dir2}/config.toml");
assert!(result.is_some());
let (dirs, pattern) = result.unwrap();
assert_eq!(dirs.len(), 2);
assert!(dirs.contains(&PathBuf::from("dir1")));
assert!(dirs.contains(&PathBuf::from("dir2")));
assert_eq!(pattern, "config.toml");
}
}