From 160460494f8c56fd42ff9ab96ace74cc4457f479 Mon Sep 17 00:00:00 2001 From: 魏曹先生 <1992414357@qq.com> Date: Tue, 28 Jul 2026 11:42:27 +0800 Subject: feat(picker): add filesystem-aware path value types Add `FilePath`, `NoFilePath`, `DirPath`, `NoDirPath`, `SymlinkPath`, `NoSymlinkPath`, `NoPath`, and `RecursiveFiles` wrapper types with filesystem validation at parse time. Each type implements `SinglePickable` and provides standard conversions via `From`, `AsRef`, `Deref`, and `DerefMut`. --- CHANGELOG.md | 35 +++++ arg_picker/src/builtin.rs | 1 + arg_picker/src/builtin/pick_paths.rs | 167 ++++++++++++++++++++ arg_picker/src/value.rs | 3 + arg_picker/src/value/paths.rs | 294 +++++++++++++++++++++++++++++++++++ 5 files changed, 500 insertions(+) create mode 100644 arg_picker/src/builtin/pick_paths.rs create mode 100644 arg_picker/src/value/paths.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index e7ae58d..cd2c0a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -50,6 +50,41 @@ None --- +## Contents + +### ?.?.? (Unreleased) + +#### Fixes: + +None + +#### Optimizations: + +None + +#### Features: + +1. **[`picker:value:paths`]** Added new path wrapper types to `arg_picker::value` for filesystem-aware argument parsing: + + - **`FilePath`** — Wraps `PathBuf`, validated at parse time to exist and be a file. + - **`NoFilePath`** — Wraps `PathBuf`, validated at parse time to _not_ exist as a file. + - **`DirPath`** — Wraps `PathBuf`, validated at parse time to exist and be a directory. + - **`NoDirPath`** — Wraps `PathBuf`, validated at parse time to _not_ exist as a directory. + - **`SymlinkPath`** — Wraps `PathBuf`, validated at parse time to exist and be a symlink. + - **`NoSymlinkPath`** — Wraps `PathBuf`, validated at parse time to _not_ exist as a symlink. + - **`NoPath`** — Wraps `PathBuf`, validated at parse time to have no filesystem entry at all. + - **`RecursiveFiles`** — Wraps `Vec`. If given a file path, returns a single-element list; if given a directory path, recursively collects all files (and symlinks) under it. + + All single-path types implement `From`, `From<&PathBuf>`, `AsRef`, `Deref`, `DerefMut`, and `Into`. `RecursiveFiles` additionally provides `len()`, `is_empty()`, `iter()`, `From>` for merging multiple collections, and the `IntoRecursiveFiles` trait for ergonomic combination from `Vec`, `&[T]`, and `[T; N]`. + + Each type implements `SinglePickable`, performing filesystem validation at parse time and returning `NotFound` when the precondition is not met. + +#### **BREAKING CHANGES** (API CHANGES): + +None + +--- + ### Release 0.3.0 (2026-07-27) > In detail, the changes in Mingling 0.3.0 are as follows: diff --git a/arg_picker/src/builtin.rs b/arg_picker/src/builtin.rs index b51590d..76c0645 100644 --- a/arg_picker/src/builtin.rs +++ b/arg_picker/src/builtin.rs @@ -3,6 +3,7 @@ mod pick_flag; mod pick_ip_attr; mod pick_numbers; mod pick_pathbuf; +mod pick_paths; mod pick_picker_args; mod pick_socket_attr; mod pick_string; diff --git a/arg_picker/src/builtin/pick_paths.rs b/arg_picker/src/builtin/pick_paths.rs new file mode 100644 index 0000000..8e6c0fd --- /dev/null +++ b/arg_picker/src/builtin/pick_paths.rs @@ -0,0 +1,167 @@ +use std::{ + fs, + path::{Path, PathBuf}, +}; + +use crate::{ + PickerArgResult::{self, NotFound, Parsed, Unparsed}, + SinglePickable, + value::{ + DirPath, FilePath, NoDirPath, NoFilePath, NoPath, NoSymlinkPath, RecursiveFiles, + SymlinkPath, + }, +}; + +impl SinglePickable for FilePath { + fn pick_single(str: Option<&str>) -> PickerArgResult { + match ::pick_single(str) { + Parsed(path) => { + if path.exists() && path.is_file() { + Parsed(FilePath::from(path)) + } else { + NotFound + } + } + Unparsed => Unparsed, + NotFound => NotFound, + } + } +} + +impl SinglePickable for NoFilePath { + fn pick_single(str: Option<&str>) -> PickerArgResult { + match ::pick_single(str) { + Parsed(path) => { + if !path.exists() || !path.is_file() { + Parsed(NoFilePath::from(path)) + } else { + NotFound + } + } + Unparsed => Unparsed, + NotFound => NotFound, + } + } +} + +impl SinglePickable for DirPath { + fn pick_single(str: Option<&str>) -> PickerArgResult { + match ::pick_single(str) { + Parsed(path) => { + if path.exists() && path.is_dir() { + Parsed(DirPath::from(path)) + } else { + NotFound + } + } + Unparsed => Unparsed, + NotFound => NotFound, + } + } +} + +impl SinglePickable for NoDirPath { + fn pick_single(str: Option<&str>) -> PickerArgResult { + match ::pick_single(str) { + Parsed(path) => { + if !path.exists() || !path.is_dir() { + Parsed(NoDirPath::from(path)) + } else { + NotFound + } + } + Unparsed => Unparsed, + NotFound => NotFound, + } + } +} + +impl SinglePickable for SymlinkPath { + fn pick_single(str: Option<&str>) -> PickerArgResult { + match ::pick_single(str) { + Parsed(path) => { + if path.exists() && path.is_symlink() { + Parsed(SymlinkPath::from(path)) + } else { + NotFound + } + } + Unparsed => Unparsed, + NotFound => NotFound, + } + } +} + +impl SinglePickable for NoSymlinkPath { + fn pick_single(str: Option<&str>) -> PickerArgResult { + match ::pick_single(str) { + Parsed(path) => { + if !path.exists() || !path.is_symlink() { + Parsed(NoSymlinkPath::from(path)) + } else { + NotFound + } + } + Unparsed => Unparsed, + NotFound => NotFound, + } + } +} + +impl SinglePickable for NoPath { + fn pick_single(str: Option<&str>) -> PickerArgResult { + match ::pick_single(str) { + Parsed(path) => { + if !path.exists() { + Parsed(NoPath::from(path)) + } else { + NotFound + } + } + Unparsed => Unparsed, + NotFound => NotFound, + } + } +} + +impl SinglePickable for RecursiveFiles { + fn pick_single(str: Option<&str>) -> PickerArgResult { + match ::pick_single(str) { + Parsed(path) => { + if !path.exists() { + return NotFound; + } + if path.is_file() || path.is_symlink() { + return Parsed(RecursiveFiles::from(vec![path])); + } + let mut entries = Vec::new(); + if let Ok(dir_entries) = fs::read_dir(&path) { + for entry in dir_entries.flatten() { + let entry_path = entry.path(); + if entry_path.is_file() || entry_path.is_symlink() { + entries.push(entry_path); + } else if entry_path.is_dir() { + collect_files(&entry_path, &mut entries); + } + } + } + Parsed(RecursiveFiles::from(entries)) + } + Unparsed => Unparsed, + NotFound => NotFound, + } + } +} + +fn collect_files(dir: &Path, entries: &mut Vec) { + if let Ok(dir_entries) = fs::read_dir(dir) { + for entry in dir_entries.flatten() { + let entry_path = entry.path(); + if entry_path.is_file() || entry_path.is_symlink() { + entries.push(entry_path); + } else if entry_path.is_dir() { + collect_files(&entry_path, entries); + } + } + } +} diff --git a/arg_picker/src/value.rs b/arg_picker/src/value.rs index 995aa00..04d3e01 100644 --- a/arg_picker/src/value.rs +++ b/arg_picker/src/value.rs @@ -1,5 +1,8 @@ mod flag; pub use flag::*; +mod paths; +pub use paths::*; + mod vec_until; pub use vec_until::*; diff --git a/arg_picker/src/value/paths.rs b/arg_picker/src/value/paths.rs new file mode 100644 index 0000000..403d6cc --- /dev/null +++ b/arg_picker/src/value/paths.rs @@ -0,0 +1,294 @@ +use std::{ + ops::{Deref, DerefMut}, + path::{Path, PathBuf}, +}; + +/// A file path. +/// +/// `FilePath` is a wrapper type around `PathBuf` representing an arbitrary file path. +/// +/// It implements `Pickable` and can be parsed by a `Picker`. +/// +/// # Parsing Behavior +/// +/// At the moment of parsing, checks whether the path exists and is a file. +/// If not a file, returns `NotFound`. +#[non_exhaustive] +pub struct FilePath { + path: PathBuf, +} + +/// A file path that should not exist. +/// +/// `NoFilePath` is a wrapper type around `PathBuf` representing an arbitrary path +/// that must not currently point to an existing file. +/// +/// It implements `Pickable` and can be parsed by a `Picker`. +/// +/// # Parsing Behavior +/// +/// At the moment of parsing, checks whether no file exists at the given path. +/// If a file already exists (regardless of whether it is a regular file, directory, +/// symlink, or other type), returns `NotFound`. +#[non_exhaustive] +pub struct NoFilePath { + path: PathBuf, +} + +/// A directory path. +/// +/// `DirPath` is a wrapper type around `PathBuf` representing an arbitrary existing +/// directory path. +/// +/// It implements `Pickable` and can be parsed by a `Picker`. +/// +/// # Parsing Behavior +/// +/// At the moment of parsing, checks whether a directory exists at the given path. +/// If no directory exists, returns `NotFound`. +#[non_exhaustive] +pub struct DirPath { + path: PathBuf, +} + +/// A directory path that should not exist. +/// +/// `NoDirPath` is a wrapper type around `PathBuf` representing an arbitrary path +/// that must not currently point to an existing directory. +/// +/// It implements `Pickable` and can be parsed by a `Picker`. +/// +/// # Parsing Behavior +/// +/// At the moment of parsing, checks whether no directory exists at the given path. +/// If a directory already exists (regardless of whether it is a regular file, file, +/// symlink, or other type), returns `NotFound`. +#[non_exhaustive] +pub struct NoDirPath { + path: PathBuf, +} + +/// A symbolic link path. +/// +/// `SymlinkPath` is a wrapper type around `PathBuf` representing an existing symbolic link. +/// +/// It implements `Pickable` and can be parsed by a `Picker`. +/// +/// # Parsing Behavior +/// +/// At the moment of parsing, checks whether the path exists and is a symlink. +/// If not a symlink, returns `NotFound`. +#[non_exhaustive] +pub struct SymlinkPath { + path: PathBuf, +} + +/// A symbolic link path that should not exist. +/// +/// `NoSymlinkPath` is a wrapper type around `PathBuf` representing an arbitrary path +/// that must not currently point to an existing symbolic link. +/// +/// It implements `Pickable` and can be parsed by a `Picker`. +/// +/// # Parsing Behavior +/// +/// At the moment of parsing, checks whether no symlink exists at the given path. +/// If a symlink already exists (regardless of whether it points to a file, directory, +/// or other type), returns `NotFound`. +#[non_exhaustive] +pub struct NoSymlinkPath { + path: PathBuf, +} + +/// A path that should not exist at all. +/// +/// `NoPath` is a wrapper type around `PathBuf` representing an arbitrary path +/// that must not currently exist on the filesystem (as a file, directory, symlink, +/// or any other type). +/// +/// It implements `Pickable` and can be parsed by a `Picker`. +/// +/// # Parsing Behavior +/// +/// At the moment of parsing, checks whether any filesystem entry exists at the given path. +/// If anything exists at that path, returns `NotFound`. +#[non_exhaustive] +pub struct NoPath { + path: PathBuf, +} + +/// Implements common trait impls (From, AsRef, Deref, DerefMut) for a path wrapper type. +macro_rules! impl_path_traits { + ($type:ident) => { + impl From for $type { + fn from(value: PathBuf) -> Self { + $type { path: value } + } + } + + impl From<&PathBuf> for $type { + fn from(value: &PathBuf) -> Self { + $type { + path: value.clone(), + } + } + } + + impl AsRef for $type { + fn as_ref(&self) -> &Path { + &self.path + } + } + + impl DerefMut for $type { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.path + } + } + + impl Deref for $type { + type Target = PathBuf; + + fn deref(&self) -> &Self::Target { + &self.path + } + } + + impl From<$type> for PathBuf { + fn from(value: $type) -> Self { + value.path + } + } + + impl From<&$type> for PathBuf { + fn from(value: &$type) -> Self { + value.path.clone() + } + } + }; +} + +impl_path_traits!(FilePath); +impl_path_traits!(NoFilePath); +impl_path_traits!(DirPath); +impl_path_traits!(NoDirPath); +impl_path_traits!(SymlinkPath); +impl_path_traits!(NoSymlinkPath); +impl_path_traits!(NoPath); + +/// Recursive file paths. +/// +/// `RecursiveFiles` is a wrapper type around `Vec` representing a list of +/// existing files. +/// +/// It implements `Pickable` and can be parsed by a `Picker`. +/// +/// # Parsing Behavior +/// +/// - If a file path is given, returns a list of length 1 containing that file. +/// - If a directory path is given, recursively collects all files under that directory +/// and returns them as a list. +#[non_exhaustive] +pub struct RecursiveFiles { + paths: Vec, +} + +impl From> for RecursiveFiles { + fn from(paths: Vec) -> Self { + Self { paths } + } +} + +impl From for Vec { + fn from(value: RecursiveFiles) -> Self { + value.paths + } +} + +impl AsRef<[PathBuf]> for RecursiveFiles { + fn as_ref(&self) -> &[PathBuf] { + &self.paths + } +} + +impl Deref for RecursiveFiles { + type Target = Vec; + + fn deref(&self) -> &Self::Target { + &self.paths + } +} + +impl DerefMut for RecursiveFiles { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.paths + } +} + +impl RecursiveFiles { + /// Returns the number of file paths. + pub fn len(&self) -> usize { + self.paths.len() + } + + /// Returns `true` if there are no file paths. + pub fn is_empty(&self) -> bool { + self.paths.is_empty() + } + + /// Returns an iterator over the file paths. + pub fn iter(&self) -> std::slice::Iter<'_, PathBuf> { + self.paths.iter() + } +} + +impl From> for RecursiveFiles { + fn from(value: Vec) -> Self { + Self { + paths: value.into_iter().flat_map(|r| r.paths).collect(), + } + } +} + +/// Trait for types that can be combined into a single `RecursiveFiles`. +pub trait IntoRecursiveFiles { + /// Combines multiple sources of file paths into a single `RecursiveFiles`. + fn combine(self) -> RecursiveFiles; +} + +impl IntoRecursiveFiles for Vec +where + T: Into, +{ + fn combine(self) -> RecursiveFiles { + self.into_iter() + .map(Into::into) + .collect::>() + .into() + } +} + +impl IntoRecursiveFiles for &[T] +where + T: Into + Clone, +{ + fn combine(self) -> RecursiveFiles { + self.iter() + .cloned() + .map(Into::into) + .collect::>() + .into() + } +} + +impl IntoRecursiveFiles for [T; N] +where + T: Into, +{ + fn combine(self) -> RecursiveFiles { + self.into_iter() + .map(Into::into) + .collect::>() + .into() + } +} -- cgit