Initial path matching

This commit is contained in:
2025-05-03 02:48:28 -07:00
commit 20bf33fa05
7 changed files with 796 additions and 0 deletions

33
src/main.rs Normal file
View File

@ -0,0 +1,33 @@
use walkdir::WalkDir;
pub mod manifest;
fn main() {
let file = std::fs::read_to_string("./test.toml").unwrap();
let x: manifest::Manifest = toml::from_str(&file).unwrap();
let rules = x
.rules
.iter()
.map(|rule| (rule.regex(), rule.action))
.collect::<Vec<_>>();
let walker = WalkDir::new("./target").into_iter();
for entry in walker {
let e = entry.unwrap();
let p = e.path();
let s = p.to_str().unwrap();
if !p.is_file() {
continue;
}
let m = rules
.iter()
.find(|(r, _)| r.is_match(s))
.map(|x| x.1.clone());
println!(" {m:?} {s}")
}
}

389
src/manifest.rs Normal file
View File

@ -0,0 +1,389 @@
use indexmap::IndexMap;
use regex::Regex;
use serde::Deserialize;
use std::path::PathBuf;
#[derive(Debug, Clone, Deserialize)]
pub struct Manifest {
pub config: PickConfig,
pub rules: PickRules,
}
#[derive(Debug, Clone, Deserialize)]
pub struct PickConfig {
pub source: PathBuf,
pub target: PathBuf,
}
//
// MARK: rules
//
#[derive(Debug, Clone, Deserialize)]
#[serde(untagged)]
pub enum OptVec<T> {
Single(T),
Vec(Vec<T>),
}
impl<T> OptVec<T> {
pub fn len(&self) -> usize {
match self {
Self::Single(_) => 1,
Self::Vec(v) => v.len(),
}
}
pub fn get(&self, idx: usize) -> Option<&T> {
match self {
Self::Single(t) => (idx == 0).then_some(t),
Self::Vec(v) => v.get(idx),
}
}
}
#[derive(Debug, Clone, Deserialize)]
#[serde(untagged)]
pub enum PickRule {
Plain(String),
Nested(PickRules),
}
#[derive(Debug, Clone, Deserialize)]
#[serde(transparent)]
pub struct PickRules(OptVec<IndexMap<String, PickRule>>);
impl PickRules {
pub fn iter<'a>(&'a self) -> PickRuleIterator<'a> {
PickRuleIterator {
stack: vec![PickRuleIterState {
rules: self,
map_index: 0,
entry_index: 0,
prefix: Vec::new(),
}],
}
}
}
impl<'a> IntoIterator for &'a PickRules {
type Item = FlatPickRule;
type IntoIter = PickRuleIterator<'a>;
fn into_iter(self) -> Self::IntoIter {
self.iter()
}
}
//
// MARK: rule iterator
//
#[derive(Debug, Clone)]
pub struct FlatPickRule {
pub patterns: Vec<String>,
pub action: String,
}
impl FlatPickRule {
pub fn regex(&self) -> Regex {
Regex::new(
&self
.patterns
.join("/")
.split("/")
.map(|x| match x {
"**" => "((:?[^/]+)*)".to_owned(),
"*" => "([^/]+)".to_owned(),
x => regex::escape(x),
})
.collect::<Vec<_>>()
.join("/"),
)
.unwrap()
}
}
struct PickRuleIterState<'a> {
rules: &'a PickRules,
map_index: usize,
entry_index: usize,
prefix: Vec<String>,
}
pub struct PickRuleIterator<'a> {
stack: Vec<PickRuleIterState<'a>>,
}
impl Iterator for PickRuleIterator<'_> {
type Item = FlatPickRule;
fn next(&mut self) -> Option<Self::Item> {
if self.stack.is_empty() {
return None;
}
let current = self.stack.last_mut().unwrap();
if current.map_index >= current.rules.0.len() {
self.stack.pop();
return self.next();
}
let current_map = &current.rules.0.get(current.map_index).unwrap();
if current.entry_index >= current_map.len() {
current.map_index += 1;
current.entry_index = 0;
return self.next();
}
let (key, value) = current_map.get_index(current.entry_index).unwrap();
current.entry_index += 1;
match value {
PickRule::Plain(action) => {
let mut patterns = current.prefix.clone();
patterns.push(key.to_string());
Some(FlatPickRule {
patterns,
action: action.clone(),
})
}
PickRule::Nested(nested_rules) => {
let mut prefix = current.prefix.clone();
prefix.push(key.to_string());
self.stack.push(PickRuleIterState {
rules: nested_rules,
map_index: 0,
entry_index: 0,
prefix,
});
self.next()
}
}
}
}
//
// MARK: tests
//
#[cfg(test)]
mod tests {
use super::*;
use std::path::Path;
#[test]
fn parse_simple_manifest() {
let toml_str = r#"
[config]
source = "./src"
target = "./tgt"
[[rules]]
"*.rs" = "copy"
"*.md" = "ignore"
"#;
let manifest: Manifest = toml::from_str(toml_str).unwrap();
assert_eq!(manifest.config.source, Path::new("./src"));
assert_eq!(manifest.config.target, Path::new("./tgt"));
let rules: Vec<FlatPickRule> = manifest.rules.iter().collect();
assert_eq!(rules.len(), 2);
assert_eq!(rules[0].patterns, vec!["*.rs"]);
assert_eq!(rules[0].action, "copy");
assert_eq!(rules[1].patterns, vec!["*.md"]);
assert_eq!(rules[1].action, "ignore");
}
#[test]
fn rule_ordering_preserved() {
let toml_str = r#"
[config]
source = "./src"
target = "./tgt"
[[rules]]
"third" = "c"
"first" = "a"
"second" = "b"
"#;
let manifest: Manifest = toml::from_str(toml_str).unwrap();
let rules: Vec<FlatPickRule> = manifest.rules.iter().collect();
assert_eq!(rules.len(), 3);
assert_eq!(rules[0].patterns, vec!["third"]);
assert_eq!(rules[0].action, "c");
assert_eq!(rules[1].patterns, vec!["first"]);
assert_eq!(rules[1].action, "a");
assert_eq!(rules[2].patterns, vec!["second"]);
assert_eq!(rules[2].action, "b");
}
#[test]
fn nested_rules_order() {
let toml_str = r#"
[config]
source = "./src"
target = "./tgt"
[[rules]]
"a" = "action_a"
"b" = "action_b"
[[rules."nested"]]
"c" = "action_c"
"d" = "action_d"
[[rules]]
"e" = "action_e"
"#;
let manifest: Manifest = toml::from_str(toml_str).unwrap();
let rules: Vec<FlatPickRule> = manifest.rules.iter().collect();
assert_eq!(rules.len(), 5);
assert_eq!(rules[0].patterns, vec!["a"]);
assert_eq!(rules[0].action, "action_a");
assert_eq!(rules[1].patterns, vec!["b"]);
assert_eq!(rules[1].action, "action_b");
assert_eq!(rules[2].patterns, vec!["nested", "c"]);
assert_eq!(rules[2].action, "action_c");
assert_eq!(rules[3].patterns, vec!["nested", "d"]);
assert_eq!(rules[3].action, "action_d");
assert_eq!(rules[4].patterns, vec!["e"]);
assert_eq!(rules[4].action, "action_e");
}
#[test]
fn deeply_nested_rules() {
let toml_str = r#"
[config]
source = "./src"
target = "./tgt"
[[rules."a"."b"."c"]]
"d" = "action_d"
"#;
let manifest: Manifest = toml::from_str(toml_str).unwrap();
let rules: Vec<FlatPickRule> = manifest.rules.iter().collect();
assert_eq!(rules.len(), 1);
assert_eq!(rules[0].patterns, vec!["a", "b", "c", "d"]);
assert_eq!(rules[0].action, "action_d");
}
#[test]
fn multiple_maps_same_level() {
let toml_str = r#"
[config]
source = "./src"
target = "./tgt"
[[rules]]
"a1" = "copy"
"a2" = "ignore"
[[rules]]
"b1" = "copy"
"b2" = "ignore"
"#;
let manifest: Manifest = toml::from_str(toml_str).unwrap();
let rules: Vec<FlatPickRule> = manifest.rules.iter().collect();
// Test that all rules exist and are in the correct order
assert_eq!(rules.len(), 4);
assert_eq!(rules[0].patterns, vec!["a1"]);
assert_eq!(rules[0].action, "copy");
assert_eq!(rules[1].patterns, vec!["a2"]);
assert_eq!(rules[1].action, "ignore");
assert_eq!(rules[2].patterns, vec!["b1"]);
assert_eq!(rules[2].action, "copy");
assert_eq!(rules[3].patterns, vec!["b2"]);
assert_eq!(rules[3].action, "ignore");
}
#[test]
fn empty_rules_list() {
let toml_str = r#"
[config]
source = "./src"
target = "./tgt"
[[rules]]
"#;
let manifest: Manifest = toml::from_str(toml_str).unwrap();
let rules: Vec<FlatPickRule> = manifest.rules.iter().collect();
assert_eq!(rules.len(), 0);
}
#[test]
#[should_panic(expected = "missing field `config`")]
fn missing_config() {
let toml_str = r#"
[[rules]]
"a" = "copy"
"#;
let _: Manifest = toml::from_str(toml_str).unwrap();
}
#[test]
#[should_panic(expected = "missing field `source`")]
fn incomplete_config() {
let toml_str = r#"
[config]
target = "./tgt"
[[rules]]
"a" = "copy"
"#;
let _: Manifest = toml::from_str(toml_str).unwrap();
}
#[test]
#[should_panic]
fn invalid_toml_syntax() {
let toml_str = r#"
[config]
source = "./src"
target = "./tgt"
[[rules]]
"invalid" = { this is not valid TOML }
"#;
let _: Manifest = toml::from_str(toml_str).unwrap();
}
#[test]
fn mixed_rule_types() {
let toml_str = r#"
[config]
source = "./src"
target = "./tgt"
[[rules]]
"plain" = "copy"
"nested" = { invalid_as_string = true }
"#;
// This should fail because a table is not a valid PickRule
let result = toml::from_str::<Manifest>(toml_str);
assert!(result.is_err());
}
}