From 4665fc94ab9508d115298dd988e2381354f46c01 Mon Sep 17 00:00:00 2001 From: 魏曹先生 <1992414357@qq.com> Date: Tue, 14 Jul 2026 01:36:27 +0800 Subject: feat(picker): add core trait, types, and builder macro for argument parsing Implement the foundational `mingling_picker` library along with its companion `mingling_picker_macros` crate. The picker provides: - `Pickable` trait for parsing types from raw strings - `PickerResult` enum modeling parse outcomes - `Picker` struct for storing and indexing command-line arguments - `PickerRequirement` struct for declarative parameter definitions - `PickerPattern` family (1..32) of typed pattern structs via the `internal_repeat` proc macro - `req!` proc macro as a succinct builder for `PickerRequirement` Re-export `mingling_picker::macros::*` from the `mingling` crate when the `picker` feature is enabled, replacing the previous wildcard re-export of `mingling_macros`. --- mingling_picker/src/lib.rs | 20 ++- mingling_picker/src/pickable.rs | 39 +++++ mingling_picker/src/pickable/implements.rs | 43 ++++++ mingling_picker/src/picker.rs | 232 +++++++++++++++++++++++++++++ mingling_picker/src/picker/patterns.rs | 12 ++ mingling_picker/src/requirement.rs | 150 +++++++++++++++++++ mingling_picker/src/result.rs | 59 ++++++++ 7 files changed, 554 insertions(+), 1 deletion(-) create mode 100644 mingling_picker/src/pickable.rs create mode 100644 mingling_picker/src/pickable/implements.rs create mode 100644 mingling_picker/src/picker.rs create mode 100644 mingling_picker/src/picker/patterns.rs create mode 100644 mingling_picker/src/requirement.rs create mode 100644 mingling_picker/src/result.rs (limited to 'mingling_picker/src') diff --git a/mingling_picker/src/lib.rs b/mingling_picker/src/lib.rs index 3c3251d..97020a4 100644 --- a/mingling_picker/src/lib.rs +++ b/mingling_picker/src/lib.rs @@ -1 +1,19 @@ -pub mod prelude {} +mod picker; +pub use picker::*; + +mod pickable; +pub use pickable::*; + +mod requirement; +pub use requirement::*; + +mod result; +pub use result::*; + +pub mod prelude { + pub use crate::IntoPicker; +} + +pub mod macros { + pub use mingling_picker_macros::*; +} diff --git a/mingling_picker/src/pickable.rs b/mingling_picker/src/pickable.rs new file mode 100644 index 0000000..9fe2c9d --- /dev/null +++ b/mingling_picker/src/pickable.rs @@ -0,0 +1,39 @@ +use crate::PickerResult; + +mod implements; + +/// A trait for types that can be constructed from a raw string representation. +/// +/// Implementing this trait allows a type to be "picked" or parsed from a string, +/// enabling deserialization or configuration loading from textual input. +/// +/// # Requirements +/// +/// - The implementing type must be [`Sized`] and implement [`Default`]. +/// - The [`pick`] method performs the actual parsing and may fail. +/// +/// # Errors +/// +/// Returns a [`PickerResult`] which encapsulates either a successful parse +/// or an error indicating why the input could not be parsed. +/// +/// # Examples +/// +/// ``` +/// # use mingling_picker::{Pickable, PickerResult}; +/// #[derive(Default)] +/// struct MyType(String); +/// +/// impl Pickable for MyType { +/// fn pick(raw_str: &str) -> PickerResult { +/// PickerResult::Parsed(MyType(raw_str.to_string())) +/// } +/// } +/// ``` +pub trait Pickable +where + Self: Sized + Default, +{ + /// Parses a `Self` value from the given raw string input. + fn pick(raw_str: &str) -> PickerResult; +} diff --git a/mingling_picker/src/pickable/implements.rs b/mingling_picker/src/pickable/implements.rs new file mode 100644 index 0000000..34f9727 --- /dev/null +++ b/mingling_picker/src/pickable/implements.rs @@ -0,0 +1,43 @@ +use crate::{Pickable, PickerResult}; +use std::path::PathBuf; + +macro_rules! impl_pickable { + ($($t:ty),*) => { + $( + impl Pickable for $t { + fn pick(raw_str: &str) -> PickerResult { + raw_str.parse::<$t>().into() + } + } + )* + }; +} + +impl_pickable!( + i8, i16, i32, i64, i128, u8, u16, u32, u64, u128, usize, f32, f64, isize +); + +impl Pickable for String { + fn pick(raw_str: &str) -> PickerResult { + PickerResult::Parsed(raw_str.to_string()) + } +} + +impl Pickable for PathBuf { + fn pick(raw_str: &str) -> PickerResult { + PickerResult::Parsed(PathBuf::from(raw_str)) + } +} + +impl Pickable for Option { + fn pick(raw_str: &str) -> PickerResult { + if raw_str.is_empty() { + PickerResult::Parsed(None) + } else { + match T::pick(raw_str) { + PickerResult::Parsed(v) => PickerResult::Parsed(Some(v)), + _ => PickerResult::Parsed(None), + } + } + } +} diff --git a/mingling_picker/src/picker.rs b/mingling_picker/src/picker.rs new file mode 100644 index 0000000..f8d3cb1 --- /dev/null +++ b/mingling_picker/src/picker.rs @@ -0,0 +1,232 @@ +use std::ops::Index; + +mod patterns; +pub use patterns::*; + +/// Picker, used to record all states of a parameter parsing +/// +/// Includes the following: +/// +/// - Basic arguments +/// - Parsing states +/// - Parsing results +pub struct Picker<'a> { + /// Internal arguments of Picker + args: PickerArguments<'a>, +} + +/// Internal arguments of Picker +/// +/// - `Slice` - borrowed slice of string slices +/// - `Vec` - owned vector of borrowed string slices +/// - `Owned` - owned vector of owned strings +pub enum PickerArguments<'a> { + /// Borrowed slice of string slices + Slice(&'a [&'a str]), + /// Owned vector of borrowed string slices + Vec(Vec<&'a str>), + /// Owned vector of owned strings + Owned(Vec), +} + +impl<'a> From<&'a [&'a str]> for Picker<'a> { + fn from(value: &'a [&'a str]) -> Self { + Picker { + args: PickerArguments::Slice(value), + } + } +} + +impl<'a> From> for Picker<'a> { + fn from(value: Vec<&'a str>) -> Self { + Picker { + args: PickerArguments::Vec(value), + } + } +} + +impl<'a> From> for Picker<'a> { + fn from(value: Vec) -> Self { + Picker { + args: PickerArguments::Owned(value), + } + } +} + +impl<'a> Picker<'a> { + /// Returns the number of arguments in the picker. + /// + /// # Examples + /// + /// ``` + /// use mingling_picker::Picker; + /// + /// let args = Picker::from(&["hello", "world"][..]); + /// assert_eq!(args.len(), 2); + /// + /// let args = Picker::from(vec!["foo", "bar"]); + /// assert_eq!(args.len(), 2); + /// + /// let args = Picker::from(vec!["a".to_string(), "b".to_string()]); + /// assert_eq!(args.len(), 2); + /// + /// let empty: Picker = Picker::from(&[][..]); + /// assert_eq!(empty.len(), 0); + /// ``` + pub fn len(&self) -> usize { + match &self.args { + PickerArguments::Slice(items) => items.len(), + PickerArguments::Vec(items) => items.len(), + PickerArguments::Owned(items) => items.len(), + } + } + + /// Returns an iterator over the arguments, yielding owned `String` values. + /// + /// # Examples + /// + /// ``` + /// use mingling_picker::Picker; + /// + /// let args = Picker::from(&["hello", "world"][..]); + /// let collected: Vec = args.iter().collect(); + /// assert_eq!(collected, vec!["hello", "world"]); + /// + /// let args = Picker::from(vec!["foo", "bar"]); + /// let collected: Vec = args.iter().collect(); + /// assert_eq!(collected, vec!["foo", "bar"]); + /// + /// let args = Picker::from(vec!["a".to_string(), "b".to_string()]); + /// let collected: Vec = args.iter().collect(); + /// assert_eq!(collected, vec!["a", "b"]); + /// ``` + pub fn iter(&'a self) -> PickerIter<'a> { + match &self.args { + PickerArguments::Slice(items) => PickerIter::Slice(items.iter()), + PickerArguments::Vec(items) => PickerIter::Vec(items.iter()), + PickerArguments::Owned(items) => PickerIter::Owned(items.iter()), + } + } +} + +impl<'a> Index for Picker<'a> { + type Output = str; + + fn index(&self, index: usize) -> &Self::Output { + match &self.args { + PickerArguments::Slice(items) => items[index], + PickerArguments::Vec(items) => items[index], + PickerArguments::Owned(items) => &items[index], + } + } +} + +impl<'a> Index for &Picker<'a> { + type Output = str; + + fn index(&self, index: usize) -> &Self::Output { + match &self.args { + PickerArguments::Slice(items) => items[index], + PickerArguments::Vec(items) => items[index], + PickerArguments::Owned(items) => &items[index], + } + } +} + +impl<'a> IntoIterator for &'a Picker<'a> { + type Item = String; + type IntoIter = PickerIter<'a>; + + fn into_iter(self) -> Self::IntoIter { + match &self.args { + PickerArguments::Slice(items) => PickerIter::Slice(items.iter()), + PickerArguments::Vec(items) => PickerIter::Vec(items.iter()), + PickerArguments::Owned(items) => PickerIter::Owned(items.iter()), + } + } +} + +/// Iterator for `Picker`, yielding owned `String` values. +/// +/// This enum wraps the underlying slice iterators for each variant of +/// `PickerArguments`, allowing uniform iteration over borrowed or owned data. +pub enum PickerIter<'a> { + /// Iterates over a borrowed slice (`&[&str]`) + Slice(std::slice::Iter<'a, &'a str>), + /// Iterates over an owned vector of borrowed string slices (`Vec<&str>`) + Vec(std::slice::Iter<'a, &'a str>), + /// Iterates over an owned vector of owned strings (`Vec`) + Owned(std::slice::Iter<'a, String>), +} + +impl<'a> Iterator for PickerIter<'a> { + type Item = String; + + fn next(&mut self) -> Option { + match self { + PickerIter::Slice(iter) => iter.next().map(|s| s.to_string()), + PickerIter::Vec(iter) => iter.next().map(|s| s.to_string()), + PickerIter::Owned(iter) => iter.next().cloned(), + } + } + + fn size_hint(&self) -> (usize, Option) { + match self { + PickerIter::Slice(iter) => iter.size_hint(), + PickerIter::Vec(iter) => iter.size_hint(), + PickerIter::Owned(iter) => iter.size_hint(), + } + } +} + +impl<'a> ExactSizeIterator for PickerIter<'a> {} + +/// Trait for converting types into a `Picker` +/// +/// Implemented for: +/// - `&[&str]` (borrowed slice) +/// - `Vec<&str>` (owned vector of borrowed strings) +/// - `Vec` (owned vector of owned strings) +pub trait IntoPicker<'a> { + /// Converts the value into a `Picker` + /// + /// # Examples + /// + /// ``` + /// use mingling_picker::{IntoPicker, Picker}; + /// + /// let args: Picker = (&["hello", "world"][..]).to_picker(); + /// assert_eq!(args.len(), 2); + /// + /// let args: Picker = vec!["foo", "bar"].to_picker(); + /// assert_eq!(args.len(), 2); + /// + /// let args: Picker = vec!["a".to_string(), "b".to_string()].to_picker(); + /// assert_eq!(args.len(), 2); + /// ``` + fn to_picker(self) -> Picker<'a>; +} + +impl<'a> IntoPicker<'a> for &'a [&'a str] { + fn to_picker(self) -> Picker<'a> { + Picker { + args: PickerArguments::Slice(self), + } + } +} + +impl<'a> IntoPicker<'a> for Vec<&'a str> { + fn to_picker(self) -> Picker<'a> { + Picker { + args: PickerArguments::Vec(self), + } + } +} + +impl<'a> IntoPicker<'a> for Vec { + fn to_picker(self) -> Picker<'a> { + Picker { + args: PickerArguments::Owned(self), + } + } +} diff --git a/mingling_picker/src/picker/patterns.rs b/mingling_picker/src/picker/patterns.rs new file mode 100644 index 0000000..74a8cd8 --- /dev/null +++ b/mingling_picker/src/picker/patterns.rs @@ -0,0 +1,12 @@ +use crate::{Pickable, PickerArguments, PickerRequirement}; + +mingling_picker_macros::internal_repeat! (1..=32 => { + pub struct PickerPattern$<'a, (Type$,)+> + where (Type$: Pickable + Default,)+ + { + pub args: PickerArguments<'a>, + ( + pub require_$: PickerRequirement<'a, Type$>, + )+ + } +}); diff --git a/mingling_picker/src/requirement.rs b/mingling_picker/src/requirement.rs new file mode 100644 index 0000000..32fefff --- /dev/null +++ b/mingling_picker/src/requirement.rs @@ -0,0 +1,150 @@ +use crate::{Pickable, PickerResult}; + +/// Represents a constraint definition for a parameter selection. +/// +/// This structure describes the constraints that a command-line parameter (Picker parameter item) +/// should satisfy, including its full name list (with aliases), short name form, and whether it is +/// positional. +/// +/// # Field Descriptions +/// +/// - `full`: Full name or alias list. For example, `["config", "cfg"]` means the parameter can be +/// matched with either `--config` or `--cfg`. Must contain at least one non-empty string. +/// +/// - `short`: Short name (single character). For example, `Some('c')` means it can be passed using +/// the `-c` form. If set to `None`, the short name form is not supported. +/// +/// - `positional`: Whether the parameter is positional (i.e., an argument without a flag). +/// - `true`: The parameter is positional; it is matched by its position in the command line rather +/// than by a `--name` or `-n` flag. +/// - `false`: The parameter is a named (flag-based) parameter. +/// +/// - `result`: The parsed result of this parameter requirement. Initially set to +/// `PickerResult::Unparsed`. After parsing, contains either the successfully parsed value or an +/// error. +#[derive(Default)] +pub struct PickerRequirement<'a, Type> +where + Type: Default + Pickable, +{ + /// Full name, may include variant names (aliases), e.g., `["config", "cfg"]`. + full: &'a [&'a str], + + /// Short name, e.g., `'c'`. + short: Option, + + /// Whether the parameter is positional (no flag, matched by position). + positional: bool, + + /// The parsed result of this parameter requirement. + result: PickerResult, +} + +impl<'a, Type> PickerRequirement<'a, Type> +where + Type: Default + Pickable, +{ + /// Creates a new `PickerRequirement` with the provided parameters. + pub fn new(full: &'a [&'a str], short: Option, positional: bool) -> Self { + Self { + full, + short, + positional, + result: PickerResult::Unparsed, + } + } + + /// Returns the full name list (including aliases). + pub fn full(&self) -> &'a [&'a str] { + self.full + } + + /// Returns the short name, if any. + pub fn short(&self) -> Option { + self.short + } + + /// Returns whether the parameter is positional. + /// + /// If `full` is empty or `short` is `None`, the parameter is considered positional + /// regardless of the stored value. + pub fn is_positional(&self) -> bool { + if self.full.is_empty() && self.short.is_none() { + true + } else { + self.positional + } + } + + /// Sets the full name list. + pub fn set_full(&mut self, full: &'a [&'a str]) { + self.full = full; + } + + /// Sets the short name. + pub fn set_short(&mut self, short: Option) { + self.short = short; + } + + /// Sets whether the parameter is positional. + pub fn set_positional(&mut self, positional: bool) { + self.positional = positional; + } + + /// Sets the full name list and returns self. + pub fn with_full(mut self, full: &'a [&'a str]) -> Self { + self.full = full; + self + } + + /// Clears the full name list (sets it to an empty slice) and returns self. + pub fn without_full(mut self) -> Self { + self.full = &[]; + self + } + + /// Sets the short name to the given character and returns self. + pub fn with_short(mut self, short: char) -> Self { + self.short = Some(short); + self + } + + /// Clears the short name (sets it to None) and returns self. + pub fn without_short(mut self) -> Self { + self.short = None; + self + } + + /// Sets whether the parameter is positional and returns self. + pub fn with_positional(mut self, positional: bool) -> Self { + self.positional = positional; + self + } + + /// Returns a reference to the current parse result. + pub fn result(&self) -> &PickerResult { + &self.result + } + + /// Returns a mutable reference to the current parse result. + pub fn result_mut(&mut self) -> &mut PickerResult { + &mut self.result + } + + /// Sets the parse result. + pub fn set_result(&mut self, result: PickerResult) { + self.result = result; + } + + /// Replaces the parse result with `PickerResult::Unparsed` and returns self. + pub fn reset_result(mut self) -> Self { + self.result = PickerResult::Unparsed; + self + } + + /// Sets the parse result and returns self. + pub fn with_result(mut self, result: PickerResult) -> Self { + self.result = result; + self + } +} diff --git a/mingling_picker/src/result.rs b/mingling_picker/src/result.rs new file mode 100644 index 0000000..1fad4f5 --- /dev/null +++ b/mingling_picker/src/result.rs @@ -0,0 +1,59 @@ +use crate::Pickable; + +/// Represents the result of parsing or looking up a value. +/// +/// This enum is generic over the type being parsed. It models four possible outcomes: +/// - [`Unparsed`](PickerResult::Unparsed): The value has not yet been parsed (default). +/// - [`Parsed`](PickerResult::Parsed): The value was successfully parsed into `Type`. +/// - [`NotFound`](PickerResult::NotFound): The requested value could not be found. +/// - [`FormatError`](PickerResult::FormatError): The input could not be parsed due to a format error. +#[derive(Default)] +pub enum PickerResult +where + Type: Default + Pickable, +{ + /// The value has not yet been parsed (default). + #[default] + Unparsed, + + /// The value was successfully parsed into `Type`. + Parsed(Type), + + /// The requested value could not be found. + NotFound, + + /// The input could not be parsed due to a format error. + FormatError, +} + +impl From> for PickerResult +where + Type: Default + Pickable, +{ + /// Converts a `Result` into a `PickerResult`. + /// + /// - `Ok(value)` maps to [`Parsed(value)`](PickerResult::Parsed). + /// - `Err(_)` maps to [`FormatError`](PickerResult::FormatError). + fn from(result: Result) -> Self { + match result { + Ok(value) => PickerResult::Parsed(value), + Err(_) => PickerResult::FormatError, + } + } +} + +impl From> for PickerResult +where + Type: Default + Pickable, +{ + /// Converts an `Option` into a `PickerResult`. + /// + /// - `Some(value)` maps to [`Parsed(value)`](PickerResult::Parsed). + /// - `None` maps to [`NotFound`](PickerResult::NotFound). + fn from(option: Option) -> Self { + match option { + Some(value) => PickerResult::Parsed(value), + None => PickerResult::NotFound, + } + } +} -- cgit