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, #[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 { // 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 { Single(T), Vec(Vec), } impl OptVec { 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 From> for Vec { fn from(val: OptVec) -> 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), Nested(PickRules), } #[derive(Debug, Clone, Deserialize)] #[serde(transparent)] pub struct PickRules(OptVec>); 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, } pub struct PickRuleIterator<'a> { stack: Vec>, } impl Iterator for PickRuleIterator<'_> { type Item = FlatPickRule; fn next(&mut self) -> Option { 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 = 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 = 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 = 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 = 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 = 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::(toml_str); assert!(result.is_err()); } }