aboutsummaryrefslogtreecommitdiff
path: root/mingling/src
diff options
context:
space:
mode:
author魏曹先生 <1992414357@qq.com>2026-05-21 19:22:27 +0800
committer魏曹先生 <1992414357@qq.com>2026-05-21 19:22:27 +0800
commit2213d332782764ab19e20c867bafa75a8aab0b65 (patch)
treed696cb0256addadf93f543f711e878ed70644027 /mingling/src
parentc2edd02745b5bdfcb8a6bb1da64e411e77855cac (diff)
Add path picker and PathChecker for CLI argument validation
Diffstat (limited to 'mingling/src')
-rw-r--r--mingling/src/parser.rs1
-rw-r--r--mingling/src/parser/picker.rs3
-rw-r--r--mingling/src/parser/picker/path.rs145
-rw-r--r--mingling/src/parser/picker/path/rule.rs218
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
+ }
+ }
+}