diff options
| author | 魏曹先生 <1992414357@qq.com> | 2026-05-21 19:22:27 +0800 |
|---|---|---|
| committer | 魏曹先生 <1992414357@qq.com> | 2026-05-21 19:22:27 +0800 |
| commit | 2213d332782764ab19e20c867bafa75a8aab0b65 (patch) | |
| tree | d696cb0256addadf93f543f711e878ed70644027 /mingling | |
| parent | c2edd02745b5bdfcb8a6bb1da64e411e77855cac (diff) | |
Add path picker and PathChecker for CLI argument validation
Diffstat (limited to 'mingling')
| -rw-r--r-- | mingling/src/parser.rs | 1 | ||||
| -rw-r--r-- | mingling/src/parser/picker.rs | 3 | ||||
| -rw-r--r-- | mingling/src/parser/picker/path.rs | 145 | ||||
| -rw-r--r-- | mingling/src/parser/picker/path/rule.rs | 218 |
4 files changed, 367 insertions, 0 deletions
diff --git a/mingling/src/parser.rs b/mingling/src/parser.rs index e25322e..b3d9059 100644 --- a/mingling/src/parser.rs +++ b/mingling/src/parser.rs @@ -5,6 +5,7 @@ mod picker; pub use crate::parser::picker::*; pub use crate::parser::picker::bools::*; +pub use crate::parser::picker::path::*; #[cfg(test)] mod test; diff --git a/mingling/src/parser/picker.rs b/mingling/src/parser/picker.rs index 91c7c83..725d4e6 100644 --- a/mingling/src/parser/picker.rs +++ b/mingling/src/parser/picker.rs @@ -7,6 +7,9 @@ pub mod builtin; #[doc(hidden)] pub mod bools; +#[doc(hidden)] +pub mod path; + /// A builder for extracting values from command-line arguments. /// /// The `Picker` struct holds parsed arguments and provides a fluent interface diff --git a/mingling/src/parser/picker/path.rs b/mingling/src/parser/picker/path.rs new file mode 100644 index 0000000..589531f --- /dev/null +++ b/mingling/src/parser/picker/path.rs @@ -0,0 +1,145 @@ +use std::path::PathBuf; + +use crate::parser::Pickable; + +mod rule; +pub use rule::*; + +impl Pickable for Vec<PathBuf> { + type Output = Vec<PathBuf>; + + fn pick(args: &mut crate::parser::Argument, flag: mingling_core::Flag) -> Option<Self::Output> { + let raw: Vec<String> = args.pick_arguments(flag); + let paths: Vec<PathBuf> = raw.into_iter().map(|s| PathBuf::from(s)).collect(); + Some(paths) + } +} + +impl Pickable for PathBuf { + type Output = PathBuf; + + fn pick(args: &mut crate::parser::Argument, flag: mingling_core::Flag) -> Option<Self::Output> { + let raw: String = args.pick_argument(flag)?; + let path: PathBuf = PathBuf::from(raw); + Some(path) + } +} + +/// Provides path checking methods for [`Vec<PathBuf>`] +/// +/// This trait automatically provides implementations for `Into<Vec<PathBuf>>` +pub trait PathsChecker { + /// Check if all paths in the list satisfy the rule + fn is_all_passed(&self, rule: &PathCheckRule) -> bool + where + Self: Into<Vec<PathBuf>> + Clone, + { + check_paths(self.clone(), rule).is_ok() + } + + /// Classify paths into (Passed, Stripped) + /// + /// Passed means paths that satisfy the rule, Stripped means paths that do not. + fn classify(self, rule: &PathCheckRule) -> (Vec<PathBuf>, Vec<PathBuf>) + where + Self: Into<Vec<PathBuf>>, + { + let paths = self.into(); + let mut passed = Vec::new(); + let mut stripped = Vec::new(); + for path in paths { + if check_path(&path, rule).is_ok() { + passed.push(path); + } else { + stripped.push(path); + } + } + (passed, stripped) + } + + /// Return paths that satisfy the rule + fn passed(self, rule: &PathCheckRule) -> Vec<PathBuf> + where + Self: Into<Vec<PathBuf>>, + { + self.classify(rule).0 + } + + /// Return paths that do not satisfy the rule + fn stripped(self, rule: &PathCheckRule) -> Vec<PathBuf> + where + Self: Into<Vec<PathBuf>>, + { + self.classify(rule).1 + } +} + +/// Provides path checking methods for [`PathBuf`] +/// +/// This trait automatically provides implementations for `Into<PathBuf>` +pub trait PathChecker { + fn is_passed(&self, rule: &PathCheckRule) -> bool + where + Self: Into<PathBuf> + Clone, + { + check_path(self.clone(), rule).is_ok() + } +} + +impl<T: Into<Vec<PathBuf>>> PathsChecker for T where T: Into<Vec<PathBuf>> {} +impl<T: Into<PathBuf>> PathChecker for T where T: Into<PathBuf> {} + +fn check_paths(path: impl Into<Vec<PathBuf>>, rule: &PathCheckRule) -> Result<(), ()> { + let paths = path.into(); + for p in paths.iter() { + check_exist(p, rule)?; + check_type(p, rule)?; + } + + Ok(()) +} + +fn check_path(path: impl Into<PathBuf>, rule: &PathCheckRule) -> Result<(), ()> { + let p = path.into(); + check_exist(&p, rule)?; + check_type(&p, rule)?; + + Ok(()) +} + +fn check_exist(path: &PathBuf, rule: &PathCheckRule) -> Result<(), ()> { + let Some(exist_check) = &rule.exist_check else { + return Ok(()); + }; + + match exist_check { + PathExistCheck::Exists => bool_to_result(path.exists()), + PathExistCheck::NotExists => bool_to_result(!path.exists()), + } +} + +fn check_type(path: &PathBuf, rule: &PathCheckRule) -> Result<(), ()> { + let Some(type_check) = &rule.type_check else { + return Ok(()); + }; + + let is_dir = path.is_dir(); + let is_file = path.is_file(); + let is_symlink = path.is_symlink(); + + if type_check.allow_dir && is_dir { + return Ok(()); + } + if type_check.allow_file && is_file { + return Ok(()); + } + if type_check.allow_symlink && is_symlink { + return Ok(()); + } + + Err(()) +} + +fn bool_to_result(b: bool) -> Result<(), ()> { + if b { Ok(()) } else { Err(()) } +} diff --git a/mingling/src/parser/picker/path/rule.rs b/mingling/src/parser/picker/path/rule.rs new file mode 100644 index 0000000..07df705 --- /dev/null +++ b/mingling/src/parser/picker/path/rule.rs @@ -0,0 +1,218 @@ +/// Path check rule +#[derive(Default)] +pub struct PathCheckRule { + pub exist_check: Option<PathExistCheck>, + pub type_check: Option<PathTypeCheck>, +} + +/// Path existence check +pub enum PathExistCheck { + Exists, + NotExists, +} + +/// Path type check +pub struct PathTypeCheck { + /// Whether the path is allowed to be a file + pub allow_file: bool, + + /// Whether the path is allowed to be a directory + pub allow_dir: bool, + + /// Whether the path is allowed to be a symlink + pub allow_symlink: bool, +} + +impl PathCheckRule { + /// Creates a new `PathCheckRule` with default values + pub fn new() -> Self { + Self { + exist_check: None, + type_check: None, + } + } + + /// Allows the path to be a file + pub fn allow_file(self) -> Self { + match self.type_check { + Some(type_check) => Self { + type_check: Some(PathTypeCheck { + allow_file: true, + allow_dir: type_check.allow_dir, + allow_symlink: type_check.allow_symlink, + }), + ..self + }, + None => Self { + type_check: Some(PathTypeCheck { + allow_file: true, + allow_dir: false, + allow_symlink: false, + }), + ..self + }, + } + } + + /// Allows the path to be a directory + pub fn allow_dir(self) -> Self { + match self.type_check { + Some(type_check) => Self { + type_check: Some(PathTypeCheck { + allow_file: type_check.allow_file, + allow_dir: true, + allow_symlink: type_check.allow_symlink, + }), + ..self + }, + None => Self { + type_check: Some(PathTypeCheck { + allow_file: false, + allow_dir: true, + allow_symlink: false, + }), + ..self + }, + } + } + + /// Allows the path to be a symlink + pub fn allow_symlink(self) -> Self { + match self.type_check { + Some(type_check) => Self { + type_check: Some(PathTypeCheck { + allow_file: type_check.allow_file, + allow_dir: type_check.allow_dir, + allow_symlink: true, + }), + ..self + }, + None => Self { + type_check: Some(PathTypeCheck { + allow_file: false, + allow_dir: false, + allow_symlink: true, + }), + ..self + }, + } + } + + /// Denies the path from being a file + pub fn deny_file(self) -> Self { + match self.type_check { + Some(type_check) => Self { + type_check: Some(PathTypeCheck { + allow_file: false, + allow_dir: type_check.allow_dir, + allow_symlink: type_check.allow_symlink, + }), + ..self + }, + None => Self { + type_check: Some(PathTypeCheck { + allow_file: false, + allow_dir: true, + allow_symlink: true, + }), + ..self + }, + } + } + + /// Denies the path from being a directory + pub fn deny_dir(self) -> Self { + match self.type_check { + Some(type_check) => Self { + type_check: Some(PathTypeCheck { + allow_file: type_check.allow_file, + allow_dir: false, + allow_symlink: type_check.allow_symlink, + }), + ..self + }, + None => Self { + type_check: Some(PathTypeCheck { + allow_file: true, + allow_dir: false, + allow_symlink: true, + }), + ..self + }, + } + } + + /// Denies the path from being a symlink + pub fn deny_symlink(self) -> Self { + match self.type_check { + Some(type_check) => Self { + type_check: Some(PathTypeCheck { + allow_file: type_check.allow_file, + allow_dir: type_check.allow_dir, + allow_symlink: false, + }), + ..self + }, + None => Self { + type_check: Some(PathTypeCheck { + allow_file: true, + allow_dir: true, + allow_symlink: false, + }), + ..self + }, + } + } + + /// Requires the path to be a file (overrides type checks) + pub fn must_file(self) -> Self { + Self { + type_check: Some(PathTypeCheck { + allow_file: true, + allow_dir: false, + allow_symlink: false, + }), + ..self + } + } + + /// Requires the path to be a directory (overrides type checks) + pub fn must_dir(self) -> Self { + Self { + type_check: Some(PathTypeCheck { + allow_file: false, + allow_dir: true, + allow_symlink: false, + }), + ..self + } + } + + /// Requires the path to be a symlink (overrides type checks) + pub fn must_symlink(self) -> Self { + Self { + type_check: Some(PathTypeCheck { + allow_file: false, + allow_dir: false, + allow_symlink: true, + }), + ..self + } + } + + /// Requires the path to exist + pub fn must_exist(self) -> Self { + Self { + exist_check: Some(PathExistCheck::Exists), + ..self + } + } + + /// Requires the path to not exist + pub fn must_not_exist(self) -> Self { + Self { + exist_check: Some(PathExistCheck::NotExists), + ..self + } + } +} |
