342 lines
7.0 KiB
Rust
342 lines
7.0 KiB
Rust
use anyhow::Result;
|
|
use indexmap::IndexMap;
|
|
use serde::Deserialize;
|
|
use std::path::{Path, PathBuf};
|
|
|
|
use crate::tool::ToolConfig;
|
|
|
|
use super::rule::FlatPickRule;
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub struct Manifest {
|
|
pub config: PickConfig,
|
|
pub tool: ToolConfig,
|
|
pub rules: PickRules,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Deserialize)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub struct PickConfig {
|
|
#[serde(default)]
|
|
pub work_dir: Option<PathBuf>,
|
|
|
|
#[serde(default = "default_false")]
|
|
pub follow_links: bool,
|
|
|
|
#[serde(default = "default_true")]
|
|
pub process_files: bool,
|
|
|
|
#[serde(default = "default_false")]
|
|
pub process_dirs: bool,
|
|
|
|
#[serde(default = "default_false")]
|
|
pub process_links: bool,
|
|
}
|
|
|
|
impl PickConfig {
|
|
pub fn work_dir(&self, manifest_path: &Path) -> Result<PathBuf> {
|
|
// Parent directory should always exist since manifest is a file.
|
|
#[expect(clippy::unwrap_used)]
|
|
let p = manifest_path.parent().unwrap().to_path_buf();
|
|
|
|
match &self.work_dir {
|
|
None => Ok(p),
|
|
Some(path) => {
|
|
if path.is_absolute() {
|
|
Ok(path.to_owned())
|
|
} else {
|
|
Ok(std::path::absolute(p.join(path))?)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn default_true() -> bool {
|
|
true
|
|
}
|
|
|
|
fn default_false() -> bool {
|
|
false
|
|
}
|
|
|
|
//
|
|
// MARK: rules
|
|
//
|
|
|
|
#[derive(Debug, Clone, Deserialize)]
|
|
#[serde(untagged)]
|
|
pub enum OptVec<T: Clone> {
|
|
Single(T),
|
|
Vec(Vec<T>),
|
|
}
|
|
|
|
impl<T: Clone> OptVec<T> {
|
|
pub fn len(&self) -> usize {
|
|
match self {
|
|
Self::Single(_) => 1,
|
|
Self::Vec(v) => v.len(),
|
|
}
|
|
}
|
|
|
|
pub fn is_empty(&self) -> bool {
|
|
match self {
|
|
Self::Single(_) => false,
|
|
Self::Vec(v) => v.is_empty(),
|
|
}
|
|
}
|
|
|
|
pub fn get(&self, idx: usize) -> Option<&T> {
|
|
match self {
|
|
Self::Single(t) => (idx == 0).then_some(t),
|
|
Self::Vec(v) => v.get(idx),
|
|
}
|
|
}
|
|
}
|
|
impl<T: Clone> From<OptVec<T>> for Vec<T> {
|
|
fn from(val: OptVec<T>) -> Self {
|
|
match val {
|
|
OptVec::Single(t) => vec![t],
|
|
OptVec::Vec(v) => v,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Deserialize)]
|
|
#[serde(untagged)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub enum PickRule {
|
|
Plain(OptVec<String>),
|
|
Nested(PickRules),
|
|
}
|
|
|
|
#[derive(Debug, Clone, Deserialize)]
|
|
#[serde(transparent)]
|
|
pub struct PickRules(OptVec<IndexMap<String, PickRule>>);
|
|
|
|
impl PickRules {
|
|
pub fn iter(&self) -> PickRuleIterator<'_> {
|
|
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
|
|
//
|
|
|
|
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;
|
|
}
|
|
|
|
#[expect(clippy::unwrap_used)]
|
|
let current = self.stack.last_mut().unwrap();
|
|
|
|
if current.map_index >= current.rules.0.len() {
|
|
self.stack.pop();
|
|
return self.next();
|
|
}
|
|
|
|
#[expect(clippy::unwrap_used)]
|
|
let current_map = ¤t.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();
|
|
}
|
|
|
|
#[expect(clippy::unwrap_used)]
|
|
let (key, value) = current_map.get_index(current.entry_index).unwrap();
|
|
|
|
current.entry_index += 1;
|
|
|
|
match value {
|
|
PickRule::Plain(task) => {
|
|
let mut patterns = current.prefix.clone();
|
|
patterns.push(key.to_string());
|
|
|
|
Some(FlatPickRule {
|
|
patterns,
|
|
tasks: task.clone().into(),
|
|
})
|
|
}
|
|
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)]
|
|
#[expect(clippy::unwrap_used)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[derive(Debug, Clone, Deserialize)]
|
|
struct TestManifest {
|
|
rules: PickRules,
|
|
}
|
|
|
|
#[test]
|
|
fn rule_ordering_preserved() {
|
|
let toml_str = r#"
|
|
[[rules]]
|
|
"third" = "c"
|
|
"first" = "a"
|
|
"second" = "b"
|
|
"#;
|
|
|
|
let test_manifest: TestManifest = toml::from_str(toml_str).unwrap();
|
|
let rules: Vec<FlatPickRule> = test_manifest.rules.iter().collect();
|
|
|
|
assert_eq!(rules.len(), 3);
|
|
assert_eq!(rules[0].patterns, vec!["third"]);
|
|
assert_eq!(rules[0].tasks, vec!["c"]);
|
|
assert_eq!(rules[1].patterns, vec!["first"]);
|
|
assert_eq!(rules[1].tasks, vec!["a"]);
|
|
assert_eq!(rules[2].patterns, vec!["second"]);
|
|
assert_eq!(rules[2].tasks, vec!["b"]);
|
|
}
|
|
|
|
#[test]
|
|
fn nested_rules_order() {
|
|
let toml_str = r#"
|
|
[[rules]]
|
|
"a" = "task_a"
|
|
"b" = "task_b"
|
|
|
|
[[rules."nested"]]
|
|
"c" = "task_c"
|
|
"d" = "task_d"
|
|
|
|
[[rules]]
|
|
"e" = "task_e"
|
|
"#;
|
|
|
|
let test_manifest: TestManifest = toml::from_str(toml_str).unwrap();
|
|
let rules: Vec<FlatPickRule> = test_manifest.rules.iter().collect();
|
|
|
|
assert_eq!(rules.len(), 5);
|
|
assert_eq!(rules[0].patterns, vec!["a"]);
|
|
assert_eq!(rules[0].tasks, vec!["task_a"]);
|
|
assert_eq!(rules[1].patterns, vec!["b"]);
|
|
assert_eq!(rules[1].tasks, vec!["task_b"]);
|
|
assert_eq!(rules[2].patterns, vec!["nested", "c"]);
|
|
assert_eq!(rules[2].tasks, vec!["task_c"]);
|
|
assert_eq!(rules[3].patterns, vec!["nested", "d"]);
|
|
assert_eq!(rules[3].tasks, vec!["task_d"]);
|
|
assert_eq!(rules[4].patterns, vec!["e"]);
|
|
assert_eq!(rules[4].tasks, vec!["task_e"]);
|
|
}
|
|
|
|
#[test]
|
|
fn deeply_nested_rules() {
|
|
let toml_str = r#"
|
|
[[rules."a"."b"."c"]]
|
|
"d" = "task_d"
|
|
"#;
|
|
|
|
let test_manifest: TestManifest = toml::from_str(toml_str).unwrap();
|
|
let rules: Vec<FlatPickRule> = test_manifest.rules.iter().collect();
|
|
|
|
assert_eq!(rules.len(), 1);
|
|
assert_eq!(rules[0].patterns, vec!["a", "b", "c", "d"]);
|
|
assert_eq!(rules[0].tasks, vec!["task_d"]);
|
|
}
|
|
|
|
#[test]
|
|
fn multiple_maps_same_level() {
|
|
let toml_str = r#"
|
|
[[rules]]
|
|
"a1" = "copy"
|
|
"a2" = "ignore"
|
|
|
|
[[rules]]
|
|
"b1" = "copy"
|
|
"b2" = "ignore"
|
|
"#;
|
|
|
|
let test_manifest: TestManifest = toml::from_str(toml_str).unwrap();
|
|
let rules: Vec<FlatPickRule> = test_manifest.rules.iter().collect();
|
|
|
|
assert_eq!(rules.len(), 4);
|
|
assert_eq!(rules[0].patterns, vec!["a1"]);
|
|
assert_eq!(rules[0].tasks, vec!["copy"]);
|
|
assert_eq!(rules[1].patterns, vec!["a2"]);
|
|
assert_eq!(rules[1].tasks, vec!["ignore"]);
|
|
assert_eq!(rules[2].patterns, vec!["b1"]);
|
|
assert_eq!(rules[2].tasks, vec!["copy"]);
|
|
assert_eq!(rules[3].patterns, vec!["b2"]);
|
|
assert_eq!(rules[3].tasks, vec!["ignore"]);
|
|
}
|
|
|
|
#[test]
|
|
fn empty_rules_list() {
|
|
let toml_str = "
|
|
[[rules]]
|
|
";
|
|
|
|
let test_manifest: TestManifest = toml::from_str(toml_str).unwrap();
|
|
let rules: Vec<FlatPickRule> = test_manifest.rules.iter().collect();
|
|
|
|
assert_eq!(rules.len(), 0);
|
|
}
|
|
|
|
#[test]
|
|
fn mixed_rule_types() {
|
|
let toml_str = r#"
|
|
[[rules]]
|
|
"plain" = "copy"
|
|
"nested" = { invalid_as_string = true }
|
|
"#;
|
|
|
|
let result = toml::from_str::<TestManifest>(toml_str);
|
|
assert!(result.is_err());
|
|
}
|
|
}
|