aboutsummaryrefslogtreecommitdiff
path: root/mingling_picker/src/picker
diff options
context:
space:
mode:
author魏曹先生 <1992414357@qq.com>2026-07-17 03:24:51 +0800
committer魏曹先生 <1992414357@qq.com>2026-07-17 03:25:24 +0800
commitb9f208deed7b8e012fbcd84202e2fb1d5eb8eeb9 (patch)
tree2e0f00651b0cff09a2f6bafd6ce35046532ffa81 /mingling_picker/src/picker
parent8b2c209c2f111e0e208de5dde9df6c4bd3ea1052 (diff)
feat(picker2): complete Picker2 prototype
Picker2 replaces the original Picker1 with a two-phase (tag → pick) + mask-bitmap architecture, decoupling argument matching from type conversion via a composable matcher pipeline. Architecture - FlagMatcher — boolean flags (--verbose) - ArgMatcher — single flag+value pairs (--name Alice) - MultiArgMatcher — multi-value flag groups (--files a.txt b.txt) - PositionalMatcher — positional arguments, respects `--` - SingleMatcher — composite for Single-type Pickables Types - Flag (Active/Inactive) — semantic bool flag value - String + 14 numeric types via SinglePickable trait - Vec<T> — greedy multi-value (all SinglePickable types) - VecUntil<T> — bounded multi-value via BoundaryCheck trait - VecUntil<T>, pick_string, pick_numbers, pick_bool, pick_flag Style system - UNIX_STYLE (kebab), POWERSHELL_STYLE (Pascal), WINDOWS_STYLE (Pascal) - naming_case auto-conversion via just_fmt integration - Style-aware separator (= for Unix, : for PS/Windows) Infrastructure - internal_repeat! macro generates PickerPattern1..=32 - SinglePickable blanket impl → Pickable - MultiPickableWithBoundary trait with greedy/bounded variants - 151 integration tests - Docs updated for parser feature
Diffstat (limited to 'mingling_picker/src/picker')
-rw-r--r--mingling_picker/src/picker/parse.rs177
-rw-r--r--mingling_picker/src/picker/patterns.rs199
-rw-r--r--mingling_picker/src/picker/result.rs56
3 files changed, 432 insertions, 0 deletions
diff --git a/mingling_picker/src/picker/parse.rs b/mingling_picker/src/picker/parse.rs
new file mode 100644
index 0000000..8f7d514
--- /dev/null
+++ b/mingling_picker/src/picker/parse.rs
@@ -0,0 +1,177 @@
+// --------------------------------------------------------------------------------------------
+// I have to say, the code generated by this `internal_repeat!` macro is really UGLY.
+//
+// But I have to admit, this is a **trade-off**. To achieve the syntax of `pick().pick().pick()`
+// while ensuring type safety, this is the best approach I could think of.
+//
+// P.S. If there's a better way, please let me know. Thanks!
+// --------------------------------------------------------------------------------------------
+//
+// Then, I must disable `clippy::type_complexity` — this guy is way too noisy.
+#![allow(clippy::type_complexity)]
+
+use crate::{Pickable, PickerArgAttr, PickerArgInfo, PickerArgResult, PickerArgs, TagPhaseContext};
+use mingling_picker_macros::internal_repeat;
+
+internal_repeat!(1..=32 => {
+ use crate::PickerPattern$;
+ use crate::PickerResult$;
+});
+
+internal_repeat!(1..=32 => {
+ impl<'a, (T$,+), Route> PickerPattern$<'a, (T$,+), Route>
+ where (T$: Pickable<'a>,+) {
+ /// Unwraps the result, panicking if a route was selected.
+ ///
+ /// # Panics
+ ///
+ /// Panics if a route was selected.
+ pub fn unwrap(self) -> ((T$,+)) {
+ let p = self.parse();
+ ((p.v$.unwrap(),+))
+ }
+
+ /// Returns the individual option values without checking the route.
+ pub fn unpack(self) -> ((Option<T$>,+)) {
+ let p = self.parse();
+ ((p.v$,+))
+ }
+
+ /// Converts to a `Result`, returning `Err(route)` if a route was selected,
+ /// or `Ok(values)` otherwise.
+ pub fn to_result(self) -> Result<((T$,+)), Route> {
+ let p = self.parse();
+ if let Some(r) = p.route {
+ return Err(r);
+ }
+ Ok(p.unwrap())
+ }
+
+ /// Converts to an `Option`, returning `None` if a route was selected,
+ /// or `Some(values)` otherwise.
+ pub fn to_option(self) -> Option<((T$,+))> {
+ let p = self.parse();
+ if p.route.is_some() {
+ return None;
+ }
+ Some(p.unwrap())
+ }
+ }
+});
+
+internal_repeat!(1..=32 => {
+ impl<'a, (T$,+), Route> PickerPattern$<'a, (T$,+), Route>
+ where (T$: Pickable<'a>,+)
+ {
+ pub fn parse(mut self) -> PickerResult$<(T$,+), Route> {
+ // ArgInfos
+ let arg_infos: [PickerArgInfo; $] = [
+ (
+ PickerArgInfo::from(self.arg_$),
+ +)
+ ];
+
+ let mut bundle: [
+ (
+ // Arg Attr
+ PickerArgAttr,
+
+ // Tag Func
+ Box<dyn FnOnce(&PickerArgs<'a>, &[u8]) -> Vec<usize>>,
+
+ // Pick Func
+ Box<dyn FnOnce(&[&str], &mut Option<Route>)>,
+
+ // Index
+ usize
+ )
+ ; $] = [
+ (
+ (
+ // Arg Attr
+ T$::get_attr(self.arg_$),
+
+ // Tag Func
+ Box::new(|args, mask| {
+ let ctx = TagPhaseContext {
+ arg_info: &arg_infos[$-],
+ args,
+ mask
+ };
+ T$::tag(ctx)
+ }),
+
+ // Pick Func
+ Box::new(|args, error_route| {
+ self.result_$ = match T$::pick(args) {
+ PickerArgResult::Parsed(mut value) => {
+ // Postprocess
+ if let Some(post) = self.post_$ {
+ value = post(value);
+ }
+ PickerArgResult::Parsed(value)
+ },
+ other => {
+ if let Some(get_default) = self.default_$ {
+ let mut value = get_default();
+
+ // Postprocess
+ if let Some(post) = self.post_$ {
+ value = post(value);
+ }
+
+ PickerArgResult::Parsed(value)
+ } else {
+ if error_route.is_none() {
+ if let Some(get_route) = self.route_$ {
+ *error_route = Some(get_route());
+ }
+ }
+ other
+ }
+ },
+
+ }
+ }),
+
+ // Index
+ $
+ ),
+ +)
+ ];
+
+ // Sort by Bundle Ord (descending)
+ bundle.sort_by(|a, b| b.0.cmp(&a.0));
+
+ // Mask — size = number of args (not args), so use args length
+ let mut mask: Vec<u8> = vec![0u8; self.args.len()];
+
+ // Parsing
+ for (_, tag_func, pick_func, _idx) in bundle {
+
+ // Tag phase
+ let tagged = tag_func(&self.args, mask.as_slice());
+ let mut args_to_pick: Vec<&str> = vec![];
+
+ for i in tagged {
+ mask[i] = 1;
+
+ // Update args to pick
+ args_to_pick.push(self.args.get(i).unwrap_or_default());
+ }
+
+ // Pick phase
+ pick_func(args_to_pick.as_slice(), &mut self.error_route);
+ }
+
+ // Combine Result
+ let result: PickerResult$<(T$,+), Route> = PickerResult$ {
+ route: self.error_route,
+ (
+ v$: self.result_$.to_option(),
+ +)
+ };
+ result
+ }
+ }
+});
diff --git a/mingling_picker/src/picker/patterns.rs b/mingling_picker/src/picker/patterns.rs
new file mode 100644
index 0000000..3c6e73f
--- /dev/null
+++ b/mingling_picker/src/picker/patterns.rs
@@ -0,0 +1,199 @@
+use mingling_picker_macros::internal_repeat;
+
+use crate::{Pickable, Picker, PickerArg, PickerArgResult, PickerArgs};
+
+internal_repeat!(1..=32 => {
+ #[doc(hidden)]
+ pub struct PickerPattern$<'a, (T$,+), Route>
+ where (T$: Pickable<'a>,+)
+ {
+ pub args: PickerArgs<'a>,
+ pub error_route: Option<Route>,
+ (
+ pub arg_$: &'a PickerArg<'a, T$>,
+ pub result_$: PickerArgResult<T$>,
+ pub default_$: Option<Box<dyn FnOnce() -> T$>>,
+ pub route_$: Option<Box<dyn FnOnce() -> Route>>,
+ pub post_$: Option<Box<dyn FnOnce(T$) -> T$>>,
+ +)
+ }
+});
+
+internal_repeat!(1..=32 => {
+ impl<'a, (T$,+), Route> PickerPattern$<'a, (T$,+), Route>
+ where (T$: Pickable<'a>,+)
+ {
+ /// Sets a default value provider for this arg.
+ ///
+ /// If the arg is not provided by the user at runtime, the given closure will be
+ /// called to produce a default value. The closure is expected to return `T$`.
+ ///
+ /// # Example
+ ///
+ /// ```ignore
+ /// let pattern = picker
+ /// .pick(&my_arg)
+ /// .or(|| 42);
+ /// ```
+ #[allow(clippy::type_complexity)]
+ pub fn or<F>(mut self, func: F) -> Self
+ where
+ F: FnMut() -> T$,
+ F: 'static,
+ {
+ self.default_$ = Some(Box::new(func));
+ self
+ }
+
+ /// Uses the default value for this arg's type if the arg is not provided.
+ ///
+ /// If the arg is not provided by the user at runtime, the default value for `T$`
+ /// (as defined by the `Default` trait) will be used.
+ ///
+ /// # Example
+ ///
+ /// ```ignore
+ /// let pattern = picker
+ /// .pick(&my_arg)
+ /// .or_default();
+ /// ```
+ #[allow(clippy::type_complexity)]
+ pub fn or_default(mut self) -> Self
+ where
+ T$: Default,
+ {
+ self.default_$ = Some(Box::new(|| T$::default()));
+ self
+ }
+
+ /// Sets a route for when the arg is not provided.
+ ///
+ /// If the arg is not provided by the user at runtime, the given closure will be
+ /// called to produce a route value that will be returned early.
+ ///
+ /// # Example
+ ///
+ /// ```ignore
+ /// let pattern = picker
+ /// .pick(&my_arg)
+ /// .or_route(|| Redirect::home());
+ /// ```
+ pub fn or_route<F>(mut self, func: F) -> Self
+ where
+ F: FnMut() -> Route,
+ F: 'static,
+ {
+ self.route_$ = Some(Box::new(func));
+ self
+ }
+
+
+ /// Resets the route for this picker pattern, allowing a different route type.
+ ///
+ /// This method converts the current `PickerPattern` into a new one with a different
+ /// route type `NewRoute`. All existing arg configurations, defaults, and post-
+ /// processing functions are preserved, but the `error_route` and individual
+ /// `route_$` fields are cleared (set to `None`).
+ ///
+ /// This is useful when you want to change the error/redirect route type mid-chain,
+ /// for example when composing patterns from different contexts that use different
+ /// route enums.
+ #[allow(clippy::type_complexity)]
+ pub fn with_route<NewRoute>(self) -> PickerPattern$<'a, (T$,+), NewRoute> {
+ PickerPattern$ {
+ args: self.args,
+ error_route: None,
+ (
+ arg_$: self.arg_$,
+ result_$: self.result_$,
+ default_$: self.default_$,
+ route_$: None,
+ post_$: self.post_$,
+ +)
+ }
+ }
+
+ /// Attaches a post-processing function to this arg.
+ ///
+ /// After the arg's value is parsed (or defaulted), the given closure will be
+ /// invoked with the parsed value and its return value will be used as the final
+ /// result. This allows transforming or validating the parsed value.
+ ///
+ /// # Example
+ ///
+ /// ```ignore
+ /// let pattern = picker
+ /// .pick(&my_arg)
+ /// .post(|val| val * 2);
+ /// ```
+ #[allow(clippy::type_complexity)]
+ pub fn post<F>(mut self, func: F) -> Self
+ where
+ F: FnMut(T$) -> T$,
+ F: 'static,
+ {
+ self.post_$ = Some(Box::new(func));
+ self
+ }
+ }
+});
+
+internal_repeat!(1..32 => {
+ impl<'a, (T$,+), Route> PickerPattern$<'a, (T$,+), Route>
+ where (T$: Pickable<'a>,+)
+ {
+ #[allow(clippy::type_complexity)]
+ /// Adds a new arg to the picking chain, returning a new `PickerPattern` with one more type parameter.
+ ///
+ /// This method extends the current picking pattern by appending an additional arg.
+ /// The previous args and their results are preserved as part of the new pattern.
+ /// The new arg's result is initially `Unparsed`.
+ pub fn pick<N>(self, arg: impl Into<&'a PickerArg<'a, N>>) -> PickerPattern$+<'a, (T$,+), N, Route>
+ where
+ N: Pickable<'a>,
+ {
+ PickerPattern$+ {
+ // Args
+ args: self.args,
+ error_route: self.error_route,
+
+ // Current
+ arg_$+: arg.into(),
+ result_$+: PickerArgResult::Unparsed,
+ default_$+: None,
+ route_$+: None,
+ post_$+: None,
+
+ // Prev
+ (
+ arg_$: self.arg_$,
+ result_$: self.result_$,
+ default_$: self.default_$,
+ route_$: self.route_$,
+ post_$: self.post_$,
+ +)
+ }
+ }
+ }
+});
+
+impl<'a, Route> Picker<'a, Route> {
+ /// Creates a `PickerPattern1` from the given arg to start a picking chain.
+ ///
+ /// This method initiates a parameter picking chain with one arg.
+ /// The result is initially `Unparsed`.
+ pub fn pick<N>(self, arg: impl Into<&'a PickerArg<'a, N>>) -> PickerPattern1<'a, N, Route>
+ where
+ N: Pickable<'a>,
+ {
+ PickerPattern1 {
+ args: self.args,
+ error_route: None::<Route>,
+ arg_1: arg.into(),
+ result_1: PickerArgResult::Unparsed,
+ route_1: None,
+ default_1: None,
+ post_1: None,
+ }
+ }
+}
diff --git a/mingling_picker/src/picker/result.rs b/mingling_picker/src/picker/result.rs
new file mode 100644
index 0000000..9cd78ae
--- /dev/null
+++ b/mingling_picker/src/picker/result.rs
@@ -0,0 +1,56 @@
+#![allow(clippy::type_complexity)] // Aha, Type Gymnastics!
+
+use mingling_picker_macros::internal_repeat;
+
+internal_repeat!(1..=32 => {
+ #[doc(hidden)]
+ pub struct PickerResult$<(T$,+), Route> {
+ /// The route selected by the picker, if any.
+ /// If this is `Some`, the picker chose to follow a route instead of selecting values,
+ /// and all value fields (`v1`, `v2`, ...) will be `None`.
+ ///
+ /// Note: "route" here refers to an alternative path/choice, not a network route.
+ pub route: Option<Route>,
+
+ (
+ #[doc = concat!("The optional value for the ", $, "th type parameter.")]
+ pub v$: Option<T$>,
+ +)
+ }
+});
+
+internal_repeat!(1..=32 => {
+ impl<(T$,+), Route> PickerResult$<(T$,+), Route> {
+ /// Unwraps the result, panicking if a route was selected.
+ ///
+ /// # Panics
+ ///
+ /// Panics if `self.route` is `Some(...)`.
+ pub fn unwrap(self) -> ((T$,+)) {
+ ((self.v$.unwrap(),+))
+ }
+
+ /// Returns the individual option values without checking the route.
+ pub fn unpack(self) -> ((Option<T$>,+)) {
+ ((self.v$,+))
+ }
+
+ /// Converts to a `Result`, returning `Err(route)` if a route was selected,
+ /// or `Ok(values)` otherwise.
+ pub fn to_result(self) -> Result<((T$,+)), Route> {
+ if let Some(r) = self.route {
+ return Err(r);
+ }
+ Ok(self.unwrap())
+ }
+
+ /// Converts to an `Option`, returning `None` if a route was selected,
+ /// or `Some(values)` otherwise.
+ pub fn to_option(self) -> Option<((T$,+))> {
+ if let Some(_) = self.route {
+ return None;
+ }
+ Some(self.unwrap())
+ }
+ }
+});