From 79ec6878877f0fd9246d67d3cd4f8cc2d1200150 Mon Sep 17 00:00:00 2001 From: 魏曹先生 <1992414357@qq.com> Date: Fri, 17 Jul 2026 11:08:07 +0800 Subject: refactor: rename `mingling_picker` to `arg_picker` --- .vscode/settings.json | 18 +- .zed/settings.json | 34 +- CHANGELOG.md | 334 +++++++-------- CONTRIBUTING.md | 54 +-- Cargo.lock | 38 +- Cargo.toml | 9 +- arg_picker/Cargo.toml | 17 + arg_picker/README.md | 47 +++ arg_picker/src/arg.rs | 299 ++++++++++++++ arg_picker/src/builtin.rs | 4 + arg_picker/src/builtin/pick_bool.rs | 22 + arg_picker/src/builtin/pick_flag.rs | 21 + arg_picker/src/builtin/pick_numbers.rs | 80 ++++ arg_picker/src/builtin/pick_string.rs | 11 + arg_picker/src/corebind.rs | 2 + arg_picker/src/corebind/entry_picker.rs | 131 ++++++ arg_picker/src/infos.rs | 446 +++++++++++++++++++++ arg_picker/src/lib.rs | 62 +++ arg_picker/src/parselib.rs | 144 +++++++ arg_picker/src/parselib/arg_matcher.rs | 142 +++++++ arg_picker/src/parselib/flag_matcher.rs | 82 ++++ arg_picker/src/parselib/multi_arg_matcher.rs | 141 +++++++ arg_picker/src/parselib/pos_matcher.rs | 71 ++++ arg_picker/src/parselib/single_matcher.rs | 59 +++ arg_picker/src/parselib/style.rs | 258 ++++++++++++ arg_picker/src/parselib/utils.rs | 276 +++++++++++++ arg_picker/src/pickable.rs | 91 +++++ arg_picker/src/pickable/multi_pickable.rs | 76 ++++ arg_picker/src/pickable/single_pickable.rs | 69 ++++ arg_picker/src/picker.rs | 432 ++++++++++++++++++++ arg_picker/src/picker/parse.rs | 177 ++++++++ arg_picker/src/picker/patterns.rs | 205 ++++++++++ arg_picker/src/picker/result.rs | 56 +++ arg_picker/src/value.rs | 5 + arg_picker/src/value/flag.rs | 145 +++++++ arg_picker/src/value/vec_until.rs | 134 +++++++ arg_picker/test/Cargo.lock | 68 ++++ arg_picker/test/Cargo.toml | 9 + arg_picker/test/src/lib.rs | 23 ++ arg_picker/test/src/test.rs | 10 + arg_picker/test/src/test/arg_matcher_test.rs | 289 +++++++++++++ arg_picker/test/src/test/basic_test.rs | 223 +++++++++++ arg_picker/test/src/test/multi_arg_test.rs | 186 +++++++++ arg_picker/test/src/test/multi_value_test.rs | 66 +++ arg_picker/test/src/test/pos_matcher_test.rs | 141 +++++++ arg_picker/test/src/test/priority_test.rs | 85 ++++ arg_picker/test/src/test/route_test.rs | 200 +++++++++ arg_picker/test/src/test/style_test.rs | 300 ++++++++++++++ arg_picker/test/src/test/value_flag_test.rs | 174 ++++++++ arg_picker/test/src/test/value_string_test.rs | 171 ++++++++ arg_picker_macros/Cargo.toml | 20 + arg_picker_macros/README.md | 40 ++ arg_picker_macros/src/arg.rs | 205 ++++++++++ arg_picker_macros/src/internal_repeat.rs | 315 +++++++++++++++ arg_picker_macros/src/lib.rs | 32 ++ mingling/Cargo.toml | 4 +- mingling/src/lib.rs | 12 +- mingling_picker/Cargo.toml | 17 - mingling_picker/README.md | 47 --- mingling_picker/src/arg.rs | 299 -------------- mingling_picker/src/builtin.rs | 4 - mingling_picker/src/builtin/pick_bool.rs | 22 - mingling_picker/src/builtin/pick_flag.rs | 21 - mingling_picker/src/builtin/pick_numbers.rs | 80 ---- mingling_picker/src/builtin/pick_string.rs | 11 - mingling_picker/src/corebind.rs | 2 - mingling_picker/src/corebind/entry_picker.rs | 131 ------ mingling_picker/src/infos.rs | 446 --------------------- mingling_picker/src/lib.rs | 62 --- mingling_picker/src/parselib.rs | 144 ------- mingling_picker/src/parselib/arg_matcher.rs | 142 ------- mingling_picker/src/parselib/flag_matcher.rs | 82 ---- mingling_picker/src/parselib/multi_arg_matcher.rs | 141 ------- mingling_picker/src/parselib/pos_matcher.rs | 71 ---- mingling_picker/src/parselib/single_matcher.rs | 59 --- mingling_picker/src/parselib/style.rs | 258 ------------ mingling_picker/src/parselib/utils.rs | 276 ------------- mingling_picker/src/pickable.rs | 91 ----- mingling_picker/src/pickable/multi_pickable.rs | 76 ---- mingling_picker/src/pickable/single_pickable.rs | 69 ---- mingling_picker/src/picker.rs | 432 -------------------- mingling_picker/src/picker/parse.rs | 177 -------- mingling_picker/src/picker/patterns.rs | 205 ---------- mingling_picker/src/picker/result.rs | 56 --- mingling_picker/src/value.rs | 5 - mingling_picker/src/value/flag.rs | 145 ------- mingling_picker/src/value/vec_until.rs | 134 ------- mingling_picker/test/Cargo.lock | 68 ---- mingling_picker/test/Cargo.toml | 9 - mingling_picker/test/src/lib.rs | 23 -- mingling_picker/test/src/test.rs | 10 - mingling_picker/test/src/test/arg_matcher_test.rs | 289 ------------- mingling_picker/test/src/test/basic_test.rs | 223 ----------- mingling_picker/test/src/test/multi_arg_test.rs | 181 --------- mingling_picker/test/src/test/multi_value_test.rs | 66 --- mingling_picker/test/src/test/pos_matcher_test.rs | 141 ------- mingling_picker/test/src/test/priority_test.rs | 85 ---- mingling_picker/test/src/test/route_test.rs | 200 --------- mingling_picker/test/src/test/style_test.rs | 300 -------------- mingling_picker/test/src/test/value_flag_test.rs | 174 -------- mingling_picker/test/src/test/value_string_test.rs | 171 -------- mingling_picker_macros/Cargo.toml | 20 - mingling_picker_macros/README.md | 40 -- mingling_picker_macros/src/arg.rs | 205 ---------- mingling_picker_macros/src/internal_repeat.rs | 315 --------------- mingling_picker_macros/src/lib.rs | 32 -- 106 files changed, 6514 insertions(+), 6508 deletions(-) create mode 100644 arg_picker/Cargo.toml create mode 100644 arg_picker/README.md create mode 100644 arg_picker/src/arg.rs create mode 100644 arg_picker/src/builtin.rs create mode 100644 arg_picker/src/builtin/pick_bool.rs create mode 100644 arg_picker/src/builtin/pick_flag.rs create mode 100644 arg_picker/src/builtin/pick_numbers.rs create mode 100644 arg_picker/src/builtin/pick_string.rs create mode 100644 arg_picker/src/corebind.rs create mode 100644 arg_picker/src/corebind/entry_picker.rs create mode 100644 arg_picker/src/infos.rs create mode 100644 arg_picker/src/lib.rs create mode 100644 arg_picker/src/parselib.rs create mode 100644 arg_picker/src/parselib/arg_matcher.rs create mode 100644 arg_picker/src/parselib/flag_matcher.rs create mode 100644 arg_picker/src/parselib/multi_arg_matcher.rs create mode 100644 arg_picker/src/parselib/pos_matcher.rs create mode 100644 arg_picker/src/parselib/single_matcher.rs create mode 100644 arg_picker/src/parselib/style.rs create mode 100644 arg_picker/src/parselib/utils.rs create mode 100644 arg_picker/src/pickable.rs create mode 100644 arg_picker/src/pickable/multi_pickable.rs create mode 100644 arg_picker/src/pickable/single_pickable.rs create mode 100644 arg_picker/src/picker.rs create mode 100644 arg_picker/src/picker/parse.rs create mode 100644 arg_picker/src/picker/patterns.rs create mode 100644 arg_picker/src/picker/result.rs create mode 100644 arg_picker/src/value.rs create mode 100644 arg_picker/src/value/flag.rs create mode 100644 arg_picker/src/value/vec_until.rs create mode 100644 arg_picker/test/Cargo.lock create mode 100644 arg_picker/test/Cargo.toml create mode 100644 arg_picker/test/src/lib.rs create mode 100644 arg_picker/test/src/test.rs create mode 100644 arg_picker/test/src/test/arg_matcher_test.rs create mode 100644 arg_picker/test/src/test/basic_test.rs create mode 100644 arg_picker/test/src/test/multi_arg_test.rs create mode 100644 arg_picker/test/src/test/multi_value_test.rs create mode 100644 arg_picker/test/src/test/pos_matcher_test.rs create mode 100644 arg_picker/test/src/test/priority_test.rs create mode 100644 arg_picker/test/src/test/route_test.rs create mode 100644 arg_picker/test/src/test/style_test.rs create mode 100644 arg_picker/test/src/test/value_flag_test.rs create mode 100644 arg_picker/test/src/test/value_string_test.rs create mode 100644 arg_picker_macros/Cargo.toml create mode 100644 arg_picker_macros/README.md create mode 100644 arg_picker_macros/src/arg.rs create mode 100644 arg_picker_macros/src/internal_repeat.rs create mode 100644 arg_picker_macros/src/lib.rs delete mode 100644 mingling_picker/Cargo.toml delete mode 100644 mingling_picker/README.md delete mode 100644 mingling_picker/src/arg.rs delete mode 100644 mingling_picker/src/builtin.rs delete mode 100644 mingling_picker/src/builtin/pick_bool.rs delete mode 100644 mingling_picker/src/builtin/pick_flag.rs delete mode 100644 mingling_picker/src/builtin/pick_numbers.rs delete mode 100644 mingling_picker/src/builtin/pick_string.rs delete mode 100644 mingling_picker/src/corebind.rs delete mode 100644 mingling_picker/src/corebind/entry_picker.rs delete mode 100644 mingling_picker/src/infos.rs delete mode 100644 mingling_picker/src/lib.rs delete mode 100644 mingling_picker/src/parselib.rs delete mode 100644 mingling_picker/src/parselib/arg_matcher.rs delete mode 100644 mingling_picker/src/parselib/flag_matcher.rs delete mode 100644 mingling_picker/src/parselib/multi_arg_matcher.rs delete mode 100644 mingling_picker/src/parselib/pos_matcher.rs delete mode 100644 mingling_picker/src/parselib/single_matcher.rs delete mode 100644 mingling_picker/src/parselib/style.rs delete mode 100644 mingling_picker/src/parselib/utils.rs delete mode 100644 mingling_picker/src/pickable.rs delete mode 100644 mingling_picker/src/pickable/multi_pickable.rs delete mode 100644 mingling_picker/src/pickable/single_pickable.rs delete mode 100644 mingling_picker/src/picker.rs delete mode 100644 mingling_picker/src/picker/parse.rs delete mode 100644 mingling_picker/src/picker/patterns.rs delete mode 100644 mingling_picker/src/picker/result.rs delete mode 100644 mingling_picker/src/value.rs delete mode 100644 mingling_picker/src/value/flag.rs delete mode 100644 mingling_picker/src/value/vec_until.rs delete mode 100644 mingling_picker/test/Cargo.lock delete mode 100644 mingling_picker/test/Cargo.toml delete mode 100644 mingling_picker/test/src/lib.rs delete mode 100644 mingling_picker/test/src/test.rs delete mode 100644 mingling_picker/test/src/test/arg_matcher_test.rs delete mode 100644 mingling_picker/test/src/test/basic_test.rs delete mode 100644 mingling_picker/test/src/test/multi_arg_test.rs delete mode 100644 mingling_picker/test/src/test/multi_value_test.rs delete mode 100644 mingling_picker/test/src/test/pos_matcher_test.rs delete mode 100644 mingling_picker/test/src/test/priority_test.rs delete mode 100644 mingling_picker/test/src/test/route_test.rs delete mode 100644 mingling_picker/test/src/test/style_test.rs delete mode 100644 mingling_picker/test/src/test/value_flag_test.rs delete mode 100644 mingling_picker/test/src/test/value_string_test.rs delete mode 100644 mingling_picker_macros/Cargo.toml delete mode 100644 mingling_picker_macros/README.md delete mode 100644 mingling_picker_macros/src/arg.rs delete mode 100644 mingling_picker_macros/src/internal_repeat.rs delete mode 100644 mingling_picker_macros/src/lib.rs diff --git a/.vscode/settings.json b/.vscode/settings.json index 7472e1c..389a476 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,11 +1,11 @@ { - "rust-analyzer.check.command": "clippy", - "rust-analyzer.checkOnSave": true, - "rust-analyzer.linkedProjects": [ - ".run/Cargo.toml", - "mingling_picker/Cargo.toml", - "mingling_pathf/test/Cargo.toml", - "mingling_picker/test/Cargo.toml", - ], - "rust-analyzer.cargo.features": ["mingling_support"], + "rust-analyzer.check.command": "clippy", + "rust-analyzer.checkOnSave": true, + "rust-analyzer.linkedProjects": [ + ".run/Cargo.toml", + "mingling_pathf/test/Cargo.toml", + "arg_picker/Cargo.toml", + "arg_picker/test/Cargo.toml" + ], + "rust-analyzer.cargo.features": ["mingling_support"] } diff --git a/.zed/settings.json b/.zed/settings.json index 12c901e..103cc28 100644 --- a/.zed/settings.json +++ b/.zed/settings.json @@ -1,19 +1,19 @@ { - "lsp": { - "rust-analyzer": { - "initialization_options": { - "checkOnSave": true, - "check": { "command": "clippy" }, - "linkedProjects": [ - ".run/Cargo.toml", - "mingling_picker/Cargo.toml", - "mingling_pathf/test/Cargo.toml", - "mingling_picker/test/Cargo.toml", - ], - "cargo": { - "features": ["mingling_support"], - }, - }, - }, - }, + "lsp": { + "rust-analyzer": { + "initialization_options": { + "checkOnSave": true, + "check": { "command": "clippy" }, + "linkedProjects": [ + ".run/Cargo.toml", + "mingling_pathf/test/Cargo.toml", + "arg_picker/Cargo.toml", + "arg_picker/test/Cargo.toml" + ], + "cargo": { + "features": ["mingling_support"] + } + } + } + } } diff --git a/CHANGELOG.md b/CHANGELOG.md index ab4a170..ac00643 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -64,68 +64,68 @@ None 1. **[`core`]** Added `RenderResult::new()` method for creating a new `RenderResult` with default values (empty text and exit code 0). This provides a more explicit and discoverable constructor compared to `RenderResult::default()`, making it clearer when a fresh result is being created for use with `write!`/`writeln!`. - ```rust - let mut result = RenderResult::new(); - writeln!(result, "Hello!").ok(); - result - ``` + ```rust + let mut result = RenderResult::new(); + writeln!(result, "Hello!").ok(); + result + ``` - The method is equivalent to `RenderResult::default()` but serves as a more idiomatic entry point for renderer functions. + The method is equivalent to `RenderResult::default()` but serves as a more idiomatic entry point for renderer functions. 2. **[`core`]** **[`macros`]** Added `core` and `macros` features to the `mingling` crate, both enabled by default. These features allow selective exclusion of `mingling_core` and `mingling_macros` dependencies respectively. - - When `core` is **disabled** (`default-features = false, features = ["macros"]`), the `mingling` crate only re-exports proc-macros and macro-related items, without linking `mingling_core`. - - When `macros` is **disabled** (`default-features = false, features = ["core"]`), the `mingling` crate only provides runtime types and traits without any proc-macro re-exports. - - When **both are disabled** (`default-features = false`), the `mingling` crate provides a minimal re-export surface. + - When `core` is **disabled** (`default-features = false, features = ["macros"]`), the `mingling` crate only re-exports proc-macros and macro-related items, without linking `mingling_core`. + - When `macros` is **disabled** (`default-features = false, features = ["core"]`), the `mingling` crate only provides runtime types and traits without any proc-macro re-exports. + - When **both are disabled** (`default-features = false`), the `mingling` crate provides a minimal re-export surface. - This enables downstream projects to fine-tune dependency weight — e.g., a library crate that only uses `mingling_macros` can disable `core` to avoid pulling in unused runtime types. + This enables downstream projects to fine-tune dependency weight — e.g., a library crate that only uses `mingling_macros` can disable `core` to avoid pulling in unused runtime types. 3. **[`picker`]** Added the new `picker` system — a **non‑breaking companion** to the existing `parser` feature, not a replacement. The `picker` feature provides a chained argument parsing API and a parsing function library (`parselib`) for analyzing command-line argument structure. Key components: - - **Chained Argument Parser with `.pick()`** — A chained-call API for declaring and extracting arguments: + - **Chained Argument Parser with `.pick()`** — A chained-call API for declaring and extracting arguments: - ```rust - use mingling_picker::prelude::*; + ```rust + use arg_picker::prelude::*; - let args: Vec<&str> = vec!["--name", "Bob", "--age", "24"]; + let args: Vec<&str> = vec!["--name", "Bob", "--age", "24"]; - let (name, age) = args - .pick(&arg![name: String]) - .or(|| "Alice".to_string()) - .pick(&arg![age: i32]) - .or(|| 24) - .post(|num| num.clamp(0, 120)) - .unwrap(); - ``` + let (name, age) = args + .pick(&arg![name: String]) + .or(|| "Alice".to_string()) + .pick(&arg![age: i32]) + .or(|| 24) + .post(|num| num.clamp(0, 120)) + .unwrap(); + ``` - - **Pure function library `parselib`** — Provides utility functions for analyzing command-line argument structure: + - **Pure function library `parselib`** — Provides utility functions for analyzing command-line argument structure: - ```rust - use mingling_picker::parselib::*; - ``` + ```rust + use arg_picker::parselib::*; + ``` - The `picker` system is **additive** — the old `parser` feature remains fully functional and is **not deprecated**. Users may choose either or both systems in the same project. The `picker` feature is available both as the `mingling/picker` feature (enabled by default) and as a standalone `mingling_picker` crate. + The `picker` system is **additive** — the old `parser` feature remains fully functional and is **not deprecated**. Users may choose either or both systems in the same project. The `picker` feature is available both as the `mingling/picker` feature (enabled by default) and as a standalone `arg_picker` crate. - _No migration is required for existing `parser` users — the old API continues to work unchanged._ + _No migration is required for existing `parser` users — the old API continues to work unchanged._ #### **BREAKING CHANGES** (API CHANGES): 1. **[`macros:renderer`]** **[`macros:help`]** Removed `r_println!` and `r_print!` macros. The `#[renderer]` and `#[help]` macros no longer implicitly inject an internal `RenderResult` variable or provide `r_println!` / `r_print!` macros. - Renderers and help functions must now explicitly create and return a `RenderResult`: + Renderers and help functions must now explicitly create and return a `RenderResult`: - ```rust - use mingling::prelude::*; + ```rust + use mingling::prelude::*; - #[renderer] - fn render_greeting(greeting: ResultGreeting) -> RenderResult { - let mut result = RenderResult::new(); - writeln!(result, "Hello, {}!", *greeting).ok(); - result - } - ``` + #[renderer] + fn render_greeting(greeting: ResultGreeting) -> RenderResult { + let mut result = RenderResult::new(); + writeln!(result, "Hello, {}!", *greeting).ok(); + result + } + ``` - All examples, docs, and test cases across the repository have been updated to use the new pattern: creating a `RenderResult` with `RenderResult::new()` or `RenderResult::default()`, writing with `write!`/`writeln!` from `std::io::Write`, and returning the result. + All examples, docs, and test cases across the repository have been updated to use the new pattern: creating a `RenderResult` with `RenderResult::new()` or `RenderResult::default()`, writing with `write!`/`writeln!` from `std::io::Write`, and returning the result. --- @@ -160,7 +160,7 @@ None `"=> EntryList"` would incorrectly match as a substring of `"=> EntryListAlias,"`, causing a false duplicate registration detection. Now changed to use `find` + trailing character boundary validation, ensuring the character immediately after the match is not an identifier character (letter/digit/underscore). - Affected scope: Deduplication logic for `#[chain]`, `#[renderer]`, `#[help]`, and `#[completion]` registration. + Affected scope: Deduplication logic for `#[chain]`, `#[renderer]`, `#[help]`, and `#[completion]` registration. #### Optimizations: @@ -189,36 +189,36 @@ None 1. **[`core`]** - **Added complete unit test coverage**, adding `#[cfg(test)]` test modules for 23 modules in `mingling_core` that previously lacked tests, covering: - - **Core types** (`any.rs`): `AnyOutput` creation, downcast, type judgment, route routing, restore deserialization; `ChainProcess` type conversion; `NextProcess` formatting - - **Dispatcher** (`dispatcher.rs`): Conversion of `Dispatchers` from 1~7 tuples, Vec, Box; Deref dereferencing; clone behavior - - **Node** (`node.rs`): Construction, join, kebab-case conversion, equality comparison, sorting - - **Global resource** (`global_resource.rs`): `GlobalResource` new, Deref, AsRef; three default implementations of `ResourceMarker` - - **Lazy resource** (`lazy_resource.rs`): Coverage of all 18+ methods of `LazyRes`, including initialization triggering, get_ref/get_mut/get_clone, into_inner/unwrap, Drop callback, `ResourceMarker` integration - - **Error types** (`chain/error.rs`, `program/error.rs`): All Display, Error source, From conversions - - **Configuration structs** (`config.rs`): Default values for `ProgramStdoutSetting`, `ProgramUserContext`; FromStr parsing and Display output of `StructuralRendererSetting` (feature-gated) - - **Flag system** (`flag.rs`): Added 8 From conversions, Deref, AsRef for `Flag` - - **String wrapper** (`string_vec.rs`): 6 From conversions, Deref, Into\ - - **Render result** (`render_result.rs`): print/println/clear/is_empty, Write trait, Display, Deref, From conversions - - **Render error** (`structural/error.rs`): Construction, From, Deref, Into\ - - **Structural renderer** (`structural.rs`): Rendering in Disable/JSON/YAML/TOML/RON formats (feature-gated) - - **Completion suggestions** (`suggest.rs`): All construction, access, modification, sorting, and conversion methods for `Suggest` and `SuggestItem` - - **Shell context** (`shell_ctx.rs`): Added `filling_argument`, `filling_argument_first`, `typing_argument`, `strip_typed_argument`, `get_typed_arguments` - - **Hook system** (`hook.rs`): `ProgramHook::empty` and all 8 builder methods - - **Singleton management** (`single_instance.rs`): `ProgramCell` set/get_raw/take/double-set-panic - - **Program setup** (`setup.rs`): Verification of `with_setup` invocation - - **Completion detection** (`comp_ctx.rs`): Three scenarios for `is_completing` - - **Build script** (`builds/comp.rs`): `get_tmpl` for four Shells and Other fallback + - **Core types** (`any.rs`): `AnyOutput` creation, downcast, type judgment, route routing, restore deserialization; `ChainProcess` type conversion; `NextProcess` formatting + - **Dispatcher** (`dispatcher.rs`): Conversion of `Dispatchers` from 1~7 tuples, Vec, Box; Deref dereferencing; clone behavior + - **Node** (`node.rs`): Construction, join, kebab-case conversion, equality comparison, sorting + - **Global resource** (`global_resource.rs`): `GlobalResource` new, Deref, AsRef; three default implementations of `ResourceMarker` + - **Lazy resource** (`lazy_resource.rs`): Coverage of all 18+ methods of `LazyRes`, including initialization triggering, get_ref/get_mut/get_clone, into_inner/unwrap, Drop callback, `ResourceMarker` integration + - **Error types** (`chain/error.rs`, `program/error.rs`): All Display, Error source, From conversions + - **Configuration structs** (`config.rs`): Default values for `ProgramStdoutSetting`, `ProgramUserContext`; FromStr parsing and Display output of `StructuralRendererSetting` (feature-gated) + - **Flag system** (`flag.rs`): Added 8 From conversions, Deref, AsRef for `Flag` + - **String wrapper** (`string_vec.rs`): 6 From conversions, Deref, Into\ + - **Render result** (`render_result.rs`): print/println/clear/is_empty, Write trait, Display, Deref, From conversions + - **Render error** (`structural/error.rs`): Construction, From, Deref, Into\ + - **Structural renderer** (`structural.rs`): Rendering in Disable/JSON/YAML/TOML/RON formats (feature-gated) + - **Completion suggestions** (`suggest.rs`): All construction, access, modification, sorting, and conversion methods for `Suggest` and `SuggestItem` + - **Shell context** (`shell_ctx.rs`): Added `filling_argument`, `filling_argument_first`, `typing_argument`, `strip_typed_argument`, `get_typed_arguments` + - **Hook system** (`hook.rs`): `ProgramHook::empty` and all 8 builder methods + - **Singleton management** (`single_instance.rs`): `ProgramCell` set/get_raw/take/double-set-panic + - **Program setup** (`setup.rs`): Verification of `with_setup` invocation + - **Completion detection** (`comp_ctx.rs`): Three scenarios for `is_completing` + - **Build script** (`builds/comp.rs`): `get_tmpl` for four Shells and Other fallback 2. **[`core`]** - **Added 6 integration test crates**, testing public APIs under different feature combinations: - - `test-basic`: Basic type tests with default features (Node, Flag, RenderResult, NextProcess, StringVec) - - `test-comp`: ShellContext, Suggest, SuggestItem, is_completing with `comp + builds` features - - `test-structural-renderer`: StructuralRenderer output in various formats with `structural_renderer_full + parser` features - - `test-repl`: ResREPL and basic types with `repl + extra_macros` features - - `test-dispatch-tree`: Basic types with `dispatch_tree` feature - - `test-all`: Comprehensive testing with all feature combinations (ShellContext, Suggest, ResREPL, StructuralRenderer, Hooks, basic types, etc.) + - `test-basic`: Basic type tests with default features (Node, Flag, RenderResult, NextProcess, StringVec) + - `test-comp`: ShellContext, Suggest, SuggestItem, is_completing with `comp + builds` features + - `test-structural-renderer`: StructuralRenderer output in various formats with `structural_renderer_full + parser` features + - `test-repl`: ResREPL and basic types with `repl + extra_macros` features + - `test-dispatch-tree`: Basic types with `dispatch_tree` feature + - `test-all`: Comprehensive testing with all feature combinations (ShellContext, Suggest, ResREPL, StructuralRenderer, Hooks, basic types, etc.) - These crates are located in `mingling_core/tests/test-*/`, each marked as an independent workspace via `[workspace]`, isolated from the main workspace. + These crates are located in `mingling_core/tests/test-*/`, each marked as an independent workspace via `[workspace]`, isolated from the main workspace. 3. **[`workspace`]** - **Added workspace exclude rules for the 6 test crates in the root `Cargo.toml`**, ensuring that integration test crates are not captured by the workspace's implicit member rules. @@ -229,33 +229,33 @@ None 2. **[`core:comp`]** Fixed `default_completion` jumping to the next subcommand level on partial input (e.g. typing `b` for `bind` would skip `bind` and directly suggest third-level commands `add`/`ls`/`rm`). Now if the last input word is only a partial match (`starts_with` but not equal), the current-level word is suggested instead of skipping ahead 3. **[`core`]** Replaced `OnceLock>>` with a custom `ProgramCell` type backed by `UnsafeCell` and `AtomicBool`. The new `ProgramCell` replaces `OnceLock`'s `get_or_init` / `get` / `as_ref` calls with a direct `set` / `get_raw` / `take` API. This change: - - Eliminates the double indirection (`OnceLock>>` → `UnsafeCell>>`) - - Allows the program instance to be **taken** (moved out) via an `unsafe fn take()` after execution completes, enabling proper cleanup before `std::process::exit()` in `exec_and_exit` - - Is paired with corresponding simplifications in `once_exec.rs` and `repl_exec.rs` that switch from `THIS_PROGRAM.get().unwrap().as_ref()` to `THIS_PROGRAM.get_raw().unwrap()` + - Eliminates the double indirection (`OnceLock>>` → `UnsafeCell>>`) + - Allows the program instance to be **taken** (moved out) via an `unsafe fn take()` after execution completes, enabling proper cleanup before `std::process::exit()` in `exec_and_exit` + - Is paired with corresponding simplifications in `once_exec.rs` and `repl_exec.rs` that switch from `THIS_PROGRAM.get().unwrap().as_ref()` to `THIS_PROGRAM.get_raw().unwrap()` 4. **[`macros:dispatcher_clap`]** Added `dispatch_tree` feature integration for `#[dispatcher_clap]`. When the `dispatch_tree` feature is enabled, `#[dispatcher_clap]` will now automatically register the dispatcher and entry in the dispatch tree via `register_dispatcher!`, matching the behavior already present in the `dispatcher!` macro. When the feature is disabled, no additional code is generated. 5. **[`macros`]** The four macros `#[chain]`, `#[renderer]`, `#[help]`, and `#[completion]` now support using fully qualified type paths with `::` (e.g. `crate::EntryFine`) as type inputs. Previously, these macros required types to be bare single-segment idents (e.g. `EntryFine`), rejecting reasonable paths like `crate::EntryFine`. Specific changes: - - `res_injection::extract_args_info` (shared by `#[chain]` and `#[renderer]`): Removed the single-segment validation for the first parameter type - - `#[renderer]` / `#[help]`: Removed respective `check_single_segment_type` calls - - `#[completion]`: Attribute parameter parsing changed from `Ident` to `TypePath`, supporting `#[completion(crate::EntryFine)]` - - Fixed code generation in `build_chain_arm`, `build_chain_exist_arm`, `build_renderer_entry`, `build_renderer_exist_entry`, `build_general_renderer_entry`, and completion entry: `Self::#variant` match arms now only take the last segment ident of the type path (e.g. `Self::EntryFine`), rather than concatenating the full path directly (which would generate invalid syntax like `Self::crate::EntryFine`), while `downcast::()` and `type Previous = T` still use the full path to ensure correct type resolution + - `res_injection::extract_args_info` (shared by `#[chain]` and `#[renderer]`): Removed the single-segment validation for the first parameter type + - `#[renderer]` / `#[help]`: Removed respective `check_single_segment_type` calls + - `#[completion]`: Attribute parameter parsing changed from `Ident` to `TypePath`, supporting `#[completion(crate::EntryFine)]` + - Fixed code generation in `build_chain_arm`, `build_chain_exist_arm`, `build_renderer_entry`, `build_renderer_exist_entry`, `build_general_renderer_entry`, and completion entry: `Self::#variant` match arms now only take the last segment ident of the type path (e.g. `Self::EntryFine`), rather than concatenating the full path directly (which would generate invalid syntax like `Self::crate::EntryFine`), while `downcast::()` and `type Previous = T` still use the full path to ensure correct type resolution 6. **[`macros:register`]** Added compile-time duplicate variant detection for chain, renderer, help, and completion registrations. When two `#[chain]` (or `#[renderer]`, `#[help]`, `#[completion]`) functions register the same type variant, the compiler now emits a clear error at the registration site (e.g. `fn handle_state_prev1(_p: StatePrev1)`) instead of silently producing an unreachable match arm that only manifests as dead code in the generated `do_chain()`/`render()` dispatch. - Affected registration points: - - `register_chain` — checks `CHAINS` set for existing entries with the same variant - - `register_renderer` — checks `RENDERERS` set - - `help_attr` (via `#[help]`) + `register_help` — checks `HELP_REQUESTS`; `register_help` also serves as a public escape hatch for manual help registration, automatically skipping the duplicate check when the exact same entry was pre-inserted by `#[help]` - - `completion_attr` (via `#[completion]`) — checks `COMPLETIONS` set + Affected registration points: + - `register_chain` — checks `CHAINS` set for existing entries with the same variant + - `register_renderer` — checks `RENDERERS` set + - `help_attr` (via `#[help]`) + `register_help` — checks `HELP_REQUESTS`; `register_help` also serves as a public escape hatch for manual help registration, automatically skipping the duplicate check when the exact same entry was pre-inserted by `#[help]` + - `completion_attr` (via `#[completion]`) — checks `COMPLETIONS` set 7. **[`macros:dispatch_tree`]** Fixed the static name generation for dispatch tree nodes to use `snake_case` conversion instead of simple `.` → `_` replacement, and fixed the `__comp` completion dispatcher static name from `__internal_dispatcher___comp` (triple underscore) to `__internal_dispatcher_comp` (double underscore), resolving a mismatch between the name generated by `register_dispatcher!` and the name used in `program_comp_gen`. 8. **[`core`]** Changed the `exec_without_render` and `exec` methods' behavior: when `stdout_setting.render_output` is `false` or the result is empty, the exit code from the result is now returned instead of hardcoded `0`. This ensures that programs which set a non-zero exit code without producing renderable output (e.g., via `ExitCodeSetup` or `ProgramControlUnit::OverrideExitCode`) will exit with the correct code. Specific changes in `once_exec.rs`: - - `exec()` (async) and `exec_without_render_and_print()` (sync): The `exit_code` is now read from the result before the render check, and returned as the fallback value instead of `0` when output is not printed. - - This means `ExitCode` / `ProgramControls` overrides are now respected regardless of whether any output is rendered. + - `exec()` (async) and `exec_without_render_and_print()` (sync): The `exit_code` is now read from the result before the render check, and returned as the fallback value instead of `0` when output is not printed. + - This means `ExitCode` / `ProgramControls` overrides are now respected regardless of whether any output is rendered. #### Optimizations: @@ -457,20 +457,20 @@ fn main() { The pathfinder system consists of: - **`mingling_pathf` sub-crate** — A standalone crate for build-time source analysis: - - `module_pathf::analyze()` — Scans the crate's source tree and infers module paths from the directory structure - - `pattern_analyzer::init()` — Creates a `PatternAnalyzer` registered with all supported Mingling patterns - - `analyze_and_build_type_mapping()` / `analyze_and_build_type_mapping_for()` — Convenience functions for build scripts - - **Pattern matchers** — Individual pattern implementations for each Mingling macro: - - `PackPattern` — Matches `pack!`, `pack_err!`, `pack_structural!`, `pack_err_structural!` invocations - - `GroupPattern` — Matches `group!` and `group_structural!` invocations - - `GrouppedDerivePattern` — Matches `#[derive(Groupped)]` and `#[derive(GrouppedSerialize)]` - - `ChainPattern` — Matches `#[chain]` functions, extracts `__internal_chain_*` names - - `RendererPattern` — Matches `#[renderer]` functions, extracts `__internal_renderer_*` names - - `HelpPattern` — Matches `#[help]` functions, extracts `__internal_help_*` names - - `CompletionPattern` — Matches `#[completion(T)]` functions, extracts `__internal_completion_*` names - - `DispatcherPattern` — Matches `dispatcher!` invocations, extracts entry type names (supports both explicit and implicit forms) - - `DispatcherClapPattern` — Matches `#[dispatcher_clap]` structs, extracts struct names - - `type_mapping_builder` — Assembles the mapping from all analyzed files and writes `MAPPING` and `type_using.rs` output files + - `module_pathf::analyze()` — Scans the crate's source tree and infers module paths from the directory structure + - `pattern_analyzer::init()` — Creates a `PatternAnalyzer` registered with all supported Mingling patterns + - `analyze_and_build_type_mapping()` / `analyze_and_build_type_mapping_for()` — Convenience functions for build scripts + - **Pattern matchers** — Individual pattern implementations for each Mingling macro: + - `PackPattern` — Matches `pack!`, `pack_err!`, `pack_structural!`, `pack_err_structural!` invocations + - `GroupPattern` — Matches `group!` and `group_structural!` invocations + - `GrouppedDerivePattern` — Matches `#[derive(Groupped)]` and `#[derive(GrouppedSerialize)]` + - `ChainPattern` — Matches `#[chain]` functions, extracts `__internal_chain_*` names + - `RendererPattern` — Matches `#[renderer]` functions, extracts `__internal_renderer_*` names + - `HelpPattern` — Matches `#[help]` functions, extracts `__internal_help_*` names + - `CompletionPattern` — Matches `#[completion(T)]` functions, extracts `__internal_completion_*` names + - `DispatcherPattern` — Matches `dispatcher!` invocations, extracts entry type names (supports both explicit and implicit forms) + - `DispatcherClapPattern` — Matches `#[dispatcher_clap]` structs, extracts struct names + - `type_mapping_builder` — Assembles the mapping from all analyzed files and writes `MAPPING` and `type_using.rs` output files - **Integration with `gen_program!()`** — When the `pathf` feature is enabled, `gen_program!()` includes the generated `type_using.rs` file via `include!()`, making all type paths available in scope for the generated dispatch code. @@ -504,29 +504,29 @@ The following explicit syntaxes are **removed**: 1. **[`core`]** **[`structural_renderer`]** Renamed the `general_renderer` feature to `structural_renderer`. All associated types, structs, and APIs have been renamed accordingly: - - Feature flag: `general_renderer` → `structural_renderer` - - Setup struct: `GeneralRendererSetup` → `StructuralRendererSetup` - - Simple setup struct: `GeneralRendererSimpleSetup` → `StructuralRendererSimpleSetup` - - Renderer type: `GeneralRenderer` → `StructuralRenderer` - - Setting enum: `GeneralRendererSetting` → `StructuralRendererSetting` - - Error type: `GeneralRendererSerializeError` → `StructuralRendererSerializeError` - - Field name: `program.general_renderer_name` → `program.structural_renderer_name` - - Trait method: `ProgramCollect::general_render()` → `ProgramCollect::structural_render()` - - Internal module: `mingling_core::renderer::general` → `mingling::renderer::structural` - - Internal static: `GENERAL_RENDERERS` → `STRUCTURAL_RENDERERS` - - Feature gate attributes: `#[cfg(feature = "general_renderer")]` → `#[cfg(feature = "structural_renderer")]` - - Sub-features: `general_renderer_empty` → `structural_renderer_empty`, `general_renderer_full` → `structural_renderer_full` - - Runtime feature constant: `MINGLING_GENERAL_RENDERER` → `MINGLING_STRUCTURAL_RENDERER` (and similarly for `_EMPTY` and `_FULL`) - - Derive macro feature gate: `#[cfg(feature = "general_renderer")]` on `#[derive(StructuralData)]` and `#[derive(GrouppedSerialize)]` → `#[cfg(feature = "structural_renderer")]` - - Example project: `example-general-renderer` renamed to `example-structural-renderer` - - Test crate: `test-general-renderer` renamed to `test-structural-renderer` + - Feature flag: `general_renderer` → `structural_renderer` + - Setup struct: `GeneralRendererSetup` → `StructuralRendererSetup` + - Simple setup struct: `GeneralRendererSimpleSetup` → `StructuralRendererSimpleSetup` + - Renderer type: `GeneralRenderer` → `StructuralRenderer` + - Setting enum: `GeneralRendererSetting` → `StructuralRendererSetting` + - Error type: `GeneralRendererSerializeError` → `StructuralRendererSerializeError` + - Field name: `program.general_renderer_name` → `program.structural_renderer_name` + - Trait method: `ProgramCollect::general_render()` → `ProgramCollect::structural_render()` + - Internal module: `mingling_core::renderer::general` → `mingling::renderer::structural` + - Internal static: `GENERAL_RENDERERS` → `STRUCTURAL_RENDERERS` + - Feature gate attributes: `#[cfg(feature = "general_renderer")]` → `#[cfg(feature = "structural_renderer")]` + - Sub-features: `general_renderer_empty` → `structural_renderer_empty`, `general_renderer_full` → `structural_renderer_full` + - Runtime feature constant: `MINGLING_GENERAL_RENDERER` → `MINGLING_STRUCTURAL_RENDERER` (and similarly for `_EMPTY` and `_FULL`) + - Derive macro feature gate: `#[cfg(feature = "general_renderer")]` on `#[derive(StructuralData)]` and `#[derive(GrouppedSerialize)]` → `#[cfg(feature = "structural_renderer")]` + - Example project: `example-general-renderer` renamed to `example-structural-renderer` + - Test crate: `test-general-renderer` renamed to `test-structural-renderer` 2. **[`core`]** Changed the signature of `ProgramSetup::setup` from `fn setup(&mut self, program: &mut Program) -> S` to `fn setup(self, program: &mut Program)`, consuming `self` instead of taking a mutable reference. Correspondingly, `Program::with_setup` now accepts `S` by value (`&mut self, setup: S`) instead of by mutable reference (`&mut self, setup: &mut S`). 3. **[`core`]** Consolidated resource naming for `ExitCode` and `REPL`: - - Renamed `ExitCode` to `ResExitCode` and moved `ResREPL` from the `mingling` root to `mingling::res::ResREPL` (the `mingling::ResREPL` re-export is removed). - - This aligns with the naming convention where resources are prefixed with `Res`. - - The corresponding setup `ExitCodeSetup` and resource injection remain unchanged. + - Renamed `ExitCode` to `ResExitCode` and moved `ResREPL` from the `mingling` root to `mingling::res::ResREPL` (the `mingling::ResREPL` re-export is removed). + - This aligns with the naming convention where resources are prefixed with `Res`. + - The corresponding setup `ExitCodeSetup` and resource injection remain unchanged. ```rust // Before @@ -538,25 +538,25 @@ use mingling::{res::ResExitCode, res::ResREPL}; 4. **[`core`]** **[`macros`]** Migrated `to_chain()` and `to_render()` methods from being generated individually per type by `#[derive(Groupped)]` and `pack!` macros, to being provided as default trait methods on the `Groupped` trait itself. - Previously, each packed or derived type had its own inherent `to_chain()` and `to_render()` methods generated by the macros. Now, these methods are defined on the `Groupped` trait with default implementations, making them available to all types that implement the trait without redundant code generation. + Previously, each packed or derived type had its own inherent `to_chain()` and `to_render()` methods generated by the macros. Now, these methods are defined on the `Groupped` trait with default implementations, making them available to all types that implement the trait without redundant code generation. - ```rust - // Before (generated per type by macros): - impl MyType { - pub fn to_chain(self) -> ChainProcess { - AnyOutput::new(self).route_chain() - } - pub fn to_render(self) -> ChainProcess { - AnyOutput::new(self).route_renderer() - } - } + ```rust + // Before (generated per type by macros): + impl MyType { + pub fn to_chain(self) -> ChainProcess { + AnyOutput::new(self).route_chain() + } + pub fn to_render(self) -> ChainProcess { + AnyOutput::new(self).route_renderer() + } + } - // After (provided by Groupped trait default methods): - // just ensure Groupped is implemented — to_chain() and to_render() - // are automatically available - ``` + // After (provided by Groupped trait default methods): + // just ensure Groupped is implemented — to_chain() and to_render() + // are automatically available + ``` - Removed the per-type inherent method generation from both `groupped.rs` and `pack.rs` in `mingling_macros`. + Removed the per-type inherent method generation from both `groupped.rs` and `pack.rs` in `mingling_macros`. 5. **[`macros`]** Changed the `route!()` macro's error branch from `return e` to `return ::mingling::Groupped::to_chain(e)`, so that the error type no longer needs to be pre-converted to `ChainProcess` via `.to_chain()` or `.to_render()`. The macro now accepts any type implementing `Groupped` in the error position and automatically converts it. @@ -570,43 +570,43 @@ let value = route!(prev.pick_or_route((), Error::default()).unpack()); 6. **[`core`]** **[`hook`]** Refactored the hook system to use structured info types and return `ProgramControls` instead of raw values. - The hook system has been redesigned for better type safety, extensibility, and control flow management: + The hook system has been redesigned for better type safety, extensibility, and control flow management: - - **All hook callbacks now receive structured info types** (e.g., `&HookPreDispatchInfo`, `&HookPostChainInfo`) instead of raw tuples or bare values. Each hook event has a dedicated info struct with named fields, making hook signatures self-documenting and easier to evolve. + - **All hook callbacks now receive structured info types** (e.g., `&HookPreDispatchInfo`, `&HookPostChainInfo`) instead of raw tuples or bare values. Each hook event has a dedicated info struct with named fields, making hook signatures self-documenting and easier to evolve. - - Hook signatures changed from `fn(...)` to `Box R>`, with `R: Into>`. Closures that return `()`are automatically converted to`ProgramControls::Empty`via the`From<()>` impl. + - Hook signatures changed from `fn(...)` to `Box R>`, with `R: Into>`. Closures that return `()`are automatically converted to`ProgramControls::Empty`via the`From<()>` impl. - ```rust - // Before - .on_begin(|| println!("Program started")) - .on_pre_dispatch(|args| println!("Dispatching: {args:?}")) - .on_finish(|| 0) // returns i32 as exit code + ```rust + // Before + .on_begin(|| println!("Program started")) + .on_pre_dispatch(|args| println!("Dispatching: {args:?}")) + .on_finish(|| 0) // returns i32 as exit code - // After - .on_begin::<_, ()>(|_| println!("Program started")) - .on_pre_dispatch(|info| println!("Dispatching: {}", info.arguments.join(" "))) - .on_finish(|_| ProgramControlUnit::OverrideExitCode(0)) - ``` + // After + .on_begin::<_, ()>(|_| println!("Program started")) + .on_pre_dispatch(|info| println!("Dispatching: {}", info.arguments.join(" "))) + .on_finish(|_| ProgramControlUnit::OverrideExitCode(0)) + ``` - - **Added `ProgramControls` and `ProgramControlUnit`** — a new control flow system that replaces the previous approach where only the `finish` hook could return a value (exit code). Now any hook can issue control instructions: - - `ProgramControlUnit::OverrideExitCode(i32)` — override the program's exit code - - `ProgramControlUnit::RouteToChain(AnyOutput)` — route to another chain processor - - `ProgramControlUnit::RouteToRender(AnyOutput)` — route directly to the renderer - - `ProgramControlUnit::RouteToHelp(AnyOutput)` — route to help display + - **Added `ProgramControls` and `ProgramControlUnit`** — a new control flow system that replaces the previous approach where only the `finish` hook could return a value (exit code). Now any hook can issue control instructions: + - `ProgramControlUnit::OverrideExitCode(i32)` — override the program's exit code + - `ProgramControlUnit::RouteToChain(AnyOutput)` — route to another chain processor + - `ProgramControlUnit::RouteToRender(AnyOutput)` — route directly to the renderer + - `ProgramControlUnit::RouteToHelp(AnyOutput)` — route to help display - - **Added `handle_program_control` function** in `exec.rs` that processes `ProgramControls` returned by hooks, updating the current execution state (exit code, current `AnyOutput`) or triggering early returns (e.g., routing to render/help). + - **Added `handle_program_control` function** in `exec.rs` that processes `ProgramControls` returned by hooks, updating the current execution state (exit code, current `AnyOutput`) or triggering early returns (e.g., routing to render/help). - - **`ExitCodeSetup` updated** — its `on_finish` hook now returns `ProgramControlUnit::OverrideExitCode(this.exit_code)` instead of just `this.exit_code`. + - **`ExitCodeSetup` updated** — its `on_finish` hook now returns `ProgramControlUnit::OverrideExitCode(this.exit_code)` instead of just `this.exit_code`. - - **`HookPostReadlineInfo` now wraps `line: &mut String`** — the `repl_post_readline` hook receives a structured info object instead of a raw `&mut String`. + - **`HookPostReadlineInfo` now wraps `line: &mut String`** — the `repl_post_readline` hook receives a structured info object instead of a raw `&mut String`. - - **`HookOnReceiveResultInfo` now wraps `result: &RenderResult`** — the `repl_on_receive_result` hook receives the result through an info struct with a `.result` field instead of directly. + - **`HookOnReceiveResultInfo` now wraps `result: &RenderResult`** — the `repl_on_receive_result` hook receives the result through an info struct with a `.result` field instead of directly. - - **`hook` module made public** — moved from `#[doc(hidden)]` to a documented public module (`pub mod hook`), along with all associated info types and control unit types. + - **`hook` module made public** — moved from `#[doc(hidden)]` to a documented public module (`pub mod hook`), along with all associated info types and control unit types. - - **Added `dispatch_args_trie` default method** on `ProgramCollect` (behind `#[cfg(not(feature = "dispatch_tree"))]`) that calls `unreachable!()` by default, avoiding `#[cfg]` gymnastics in `exec.rs`. + - **Added `dispatch_args_trie` default method** on `ProgramCollect` (behind `#[cfg(not(feature = "dispatch_tree"))]`) that calls `unreachable!()` by default, avoiding `#[cfg]` gymnastics in `exec.rs`. - - **Examples and internal callers updated** throughout the codebase to use the new hook API patterns. + - **Examples and internal callers updated** throughout the codebase to use the new hook API patterns. 7. **[`core`]** **[`structural_renderer`]** Added the `pack_err_structural!`, `pack_structural!`, and `group_structural!` macros for creating types that support structured output (JSON/YAML/TOML/RON). These are like `pack_err!`, `pack!`, and `group!` respectively, but also mark the type with the `StructuralData` trait, enabling the `StructuralRenderer` to serialize them. @@ -634,8 +634,8 @@ When the `structural_renderer` feature is enabled, `ResultEmpty` also derives `S #### Fixes: 1. **[`macros:dispatcher_clap`]** Fixed the issue where clap error messages (`DisplayHelp` and parse errors from `try_parse_from`) could not output ANSI - - For error paths, use `e.render().ansi()` instead of `e.to_string()` to prevent ANSI codes from being stripped by `strip_str` in `StyledStr::Display` - - For help info paths, use with `BasicProgramSetup`, output ANSI-colored help content through the mingling framework's `render_help` flow + - For error paths, use `e.render().ansi()` instead of `e.to_string()` to prevent ANSI codes from being stripped by `strip_str` in `StyledStr::Display` + - For help info paths, use with `BasicProgramSetup`, output ANSI-colored help content through the mingling framework's `render_help` flow #### Optimizations: @@ -675,8 +675,8 @@ fn handle_path_pick(prev: PathPick) { ``` 3. **[`macros`]** Extended the `#[renderer]` attribute to support custom return types. Previously, `#[renderer]` functions could only return `()`, and the generated helper function always returned `RenderResult`. Now: - - **`fn foo(x: T)` / `fn foo(x: T) -> ()`** → The generated helper function returns `()`. If the internal `RenderResult` (`dummy_r`) is non-empty, it is automatically printed to stdout. - - **`fn foo(x: T) -> U`** → The generated helper function returns `U`. The internal `RenderResult` is converted via `dummy_r.into()`, and no automatic printing occurs. + - **`fn foo(x: T)` / `fn foo(x: T) -> ()`** → The generated helper function returns `()`. If the internal `RenderResult` (`dummy_r`) is non-empty, it is automatically printed to stdout. + - **`fn foo(x: T) -> U`** → The generated helper function returns `U`. The internal `RenderResult` is converted via `dummy_r.into()`, and no automatic printing occurs. 4. **[`macros`]** Resource injection is now shared between `#[chain]` and `#[renderer]`. Extracted the common resource injection infrastructure (`ResourceInjection`, `extract_args_info`, `generate_immut_resource_bindings`, `wrap_body_with_mut_resources`) from `chain.rs` into a new `res_injection.rs` module. Both `#[chain]` and `#[renderer]` now reuse the same logic. @@ -823,10 +823,10 @@ None 1. **[`core`]** The core library no longer depends on `thiserror` 2. **[`mingling`]** Split the monolithic `general_renderer` feature into separate format-specific features: - - `general_renderer` now only includes core serialization support without any specific format - - `general_renderer_full` bundles all available serialization formats - - Individual format features: `json_serde_fmt`, `yaml_serde_fmt`, `toml_serde_fmt`, `ron_serde_fmt` - - A meta feature `all_serde_fmt` enables all format features at once + - `general_renderer` now only includes core serialization support without any specific format + - `general_renderer_full` bundles all available serialization formats + - Individual format features: `json_serde_fmt`, `yaml_serde_fmt`, `toml_serde_fmt`, `ron_serde_fmt` + - A meta feature `all_serde_fmt` enables all format features at once #### Features: @@ -946,9 +946,9 @@ fn my_chain(prev: Prev) -> Next { 1. **[`core`]** The signature of `exec` has been changed to `exec(self) -> i32` (previously was `exec(self)`) 2. **[`macros`]** All proc macros that accept a program/group name parameter (e.g. `pack!`, `dispatcher!`, `#[chain]`, `#[program_setup]`, `#[dispatcher_clap]`, `#[derive(Groupped)]`) now parse the name as a `syn::Path` instead of a bare `Ident`. This means: - - You can now use paths like `crate::MyProgram` or `my_crate::MyProgram` in addition to plain `MyProgram`. - - The default program name `ThisProgram` is no longer re-exported or required as an import — generated code references `crate::ThisProgram` directly. - - If you previously imported `ThisProgram` from `crate` only for macro use, that import is no longer needed and can be removed. + - You can now use paths like `crate::MyProgram` or `my_crate::MyProgram` in addition to plain `MyProgram`. + - The default program name `ThisProgram` is no longer re-exported or required as an import — generated code references `crate::ThisProgram` directly. + - If you previously imported `ThisProgram` from `crate` only for macro use, that import is no longer needed and can be removed. ```rust use crate::ThisProgram; // Can be removed if not used directly @@ -1125,9 +1125,9 @@ gen_program!(); ``` 6. **[`picker`]** Simplified `Picker` logic: - - `Picker` no longer requires the generic parameter `` by default; it only needs it when using `pick_or_route` or `after_or_route` + - `Picker` no longer requires the generic parameter `` by default; it only needs it when using `pick_or_route` or `after_or_route` - - Additionally, if no `or_route` operations are used, the `unpack_directly` function is no longer available; `unpack` will directly extract the inner value + - Additionally, if no `or_route` operations are used, the `unpack_directly` function is no longer available; `unpack` will directly extract the inner value ```rust // Before diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b10e96d..e729295 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -6,22 +6,22 @@ Before contributing, we recommend reading [README](README.md) to get an overview ## 1. Project Structure 📦 -| Category | Path/Name | Description | -| --------------------------- | ------------------------- | ------------------------------------------------------------------ | -| **Entry crate** | `mingling/` | Project entry point | -| **Core library** | `mingling_core/` | Imported as an external dependency | -| **Macro library** | `mingling_macros/` | Imported as an external dependency | -| **Mingling Pathfinder** | `mingling_pathf/` | Build-time module path resolution for types | -| **Mingling Picker2** | `mingling_picker/` | Mingling Arguments Parser | -| **Mingling Picker2 Macros** | `mingling_picker_macros/` | Mingling Arguments Parser Macros | -| **Scaffolding tool** | `mling/` | Scaffolding tool `mingling-cli` | -| **Examples** | `examples/` | To add expected output tests, modify `examples/test-examples.toml` | -| **Documents** | `docs/` | All documents | -| **Dev Documents** | `docs/dev/` | Internal documents | -| **Resources** | `docs/res/` | All resources | -| **Development tools** | `.run/src/bin` | Contains scripts and Rust tools | -| **CI** | `.run/src/bin/ci.rs` | Can be invoked directly via `cargo ci` | -| **Temporary files** | `.temp/` | Ignored by `.gitignore` | +| Category | Path/Name | Description | +| --------------------------- | -------------------- | ------------------------------------------------------------------ | +| **Entry crate** | `mingling/` | Project entry point | +| **Core library** | `mingling_core/` | Imported as an external dependency | +| **Macro library** | `mingling_macros/` | Imported as an external dependency | +| **Mingling Pathfinder** | `mingling_pathf/` | Build-time module path resolution for types | +| **Mingling Picker2** | `arg_picker/` | Mingling Arguments Parser | +| **Mingling Picker2 Macros** | `arg_picker_macros/` | Mingling Arguments Parser Macros | +| **Scaffolding tool** | `mling/` | Scaffolding tool `mingling-cli` | +| **Examples** | `examples/` | To add expected output tests, modify `examples/test-examples.toml` | +| **Documents** | `docs/` | All documents | +| **Dev Documents** | `docs/dev/` | Internal documents | +| **Resources** | `docs/res/` | All resources | +| **Development tools** | `.run/src/bin` | Contains scripts and Rust tools | +| **CI** | `.run/src/bin/ci.rs` | Can be invoked directly via `cargo ci` | +| **Temporary files** | `.temp/` | Ignored by `.gitignore` | ## 2. How to Contribute @@ -113,28 +113,28 @@ No strict requirements here — just modify the relevant `*.html` files. Preview ## 3. Submission Guide 🖊 1. **Pull Request** - - Submit a GitHub Pull Request and @Reviewer **[Weicao-CatilGrass](https://github.com/Weicao-CatilGrass)** for review - - Or send patches to **catil_grass@qq.com** + - Submit a GitHub Pull Request and @Reviewer **[Weicao-CatilGrass](https://github.com/Weicao-CatilGrass)** for review + - Or send patches to **catil_grass@qq.com** 2. **Commit Messages** - - Clearly and concisely describe the changes, no stringent requirements - - Provide more detail for complex changes, keep it brief for simple changes - - But: if you use [Conventional Commits](https://www.conventionalcommits.org/), it would make me even happier :) + - Clearly and concisely describe the changes, no stringent requirements + - Provide more detail for complex changes, keep it brief for simple changes + - But: if you use [Conventional Commits](https://www.conventionalcommits.org/), it would make me even happier :) 3. **CHANGELOG** - - If the submission includes functional changes or fixes, **the PR must include modifications to CHANGELOG.md** to describe the changes - - For minor changes like typo fixes, **CHANGELOG.md modification is not required**, and we will merge faster + - If the submission includes functional changes or fixes, **the PR must include modifications to CHANGELOG.md** to describe the changes + - For minor changes like typo fixes, **CHANGELOG.md modification is not required**, and we will merge faster 4. **Multi-commit PR** - - A PR can contain multiple commits - - However, at least one commit must modify CHANGELOG.md + - A PR can contain multiple commits + - However, at least one commit must modify CHANGELOG.md 5. **Review** - - After submission, please notify [Weicao-CatilGrass](https://github.com/Weicao-CatilGrass) for review — this is the most efficient way to get feedback + - After submission, please notify [Weicao-CatilGrass](https://github.com/Weicao-CatilGrass) for review — this is the most efficient way to get feedback 6. **Binary Resources** - - For binary resource files (images, etc.), please be cautious about adding them to avoid repository bloat + - For binary resource files (images, etc.), please be cautious about adding them to avoid repository bloat ## 4. Documentation Contribution 📕 diff --git a/Cargo.lock b/Cargo.lock index a283e0f..4c0682e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -70,6 +70,24 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "arg-picker" +version = "0.1.0" +dependencies = [ + "arg-picker-macros", + "just_fmt 0.2.0", + "mingling_core", +] + +[[package]] +name = "arg-picker-macros" +version = "0.1.0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "autocfg" version = "1.5.1" @@ -409,10 +427,10 @@ checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" name = "mingling" version = "0.3.0" dependencies = [ + "arg-picker", "mingling", "mingling_core", "mingling_macros", - "mingling_picker", "serde", "size", "tokio", @@ -467,24 +485,6 @@ dependencies = [ "syn", ] -[[package]] -name = "mingling_picker" -version = "0.3.0" -dependencies = [ - "just_fmt 0.2.0", - "mingling_core", - "mingling_picker_macros", -] - -[[package]] -name = "mingling_picker_macros" -version = "0.3.0" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "mio" version = "1.2.0" diff --git a/Cargo.toml b/Cargo.toml index 90989fc..99523c7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,8 +10,8 @@ members = [ "mingling_pathf", # Picker2 - "mingling_picker", - "mingling_picker_macros", + "arg_picker", + "arg_picker_macros", # Scaffolding tool "mling" @@ -36,8 +36,9 @@ exclude = [ mingling_core = { path = "mingling_core", default-features = false } mingling_macros = { path = "mingling_macros", default-features = false } mingling_pathf = { path = "mingling_pathf", default-features = false } -mingling_picker = { path = "mingling_picker", default-features = false } -mingling_picker_macros = { path = "mingling_picker_macros", default-features = false } + +arg-picker = { path = "arg_picker", default-features = false } +arg-picker-macros = { path = "arg_picker_macros", default-features = false } just_fmt = "0.2.0" just_template = "0.2.0" diff --git a/arg_picker/Cargo.toml b/arg_picker/Cargo.toml new file mode 100644 index 0000000..f7672c1 --- /dev/null +++ b/arg_picker/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "arg-picker" +version = "0.1.0" +edition = "2024" +license = "MIT OR Apache-2.0" +repository = "https://github.com/mingling-rs/mingling/tree/main/arg-picker" +authors = ["Weicao-CatilGrass"] +readme = "README.md" +description = "A lightweight, type-safe CLI argument parser" + +[features] +mingling_support = ["dep:mingling_core", "arg-picker-macros/mingling_support"] + +[dependencies] +mingling_core = { workspace = true, optional = true } +arg-picker-macros.workspace = true +just_fmt.workspace = true diff --git a/arg_picker/README.md b/arg_picker/README.md new file mode 100644 index 0000000..14051f3 --- /dev/null +++ b/arg_picker/README.md @@ -0,0 +1,47 @@ +# Argument Picker + +A command-line argument parser for [Mingling](https://github.com/mingling-rs/mingling), enabled by the `mingling/picker` feature. + +```toml +[dependencies.mingling] +version = "0.3.0" +features = [ + "picker" +] +``` + +Of course, you can also use it as a standalone crate by replacing `mingling::picker` with `arg_picker`: + +```toml +[dependencies] +arg-picker = "0.1.0" +``` + +## Chained Argument Parser + +Provides a clean chained-call API for declaring arguments to parse: + +```rust +use arg_picker::prelude::*; + +let args: Vec<&str> = vec!["--name", "Bob", "--age", "24"]; + +let (name, age) = args + .pick(&arg![name: String]) + .or(|| "Alice".to_string()) + .pick(&arg![age: i32]) + .or(|| 24) + .post(|num| num.clamp(0, 120)) + .unwrap(); + +assert_eq!(name, "Bob".to_string()); +assert_eq!(age, 24); +``` + +## Parsing Function Library + +Provides a pure function library `parselib` for analyzing the structure of command-line arguments. + +```rust +use arg_picker::parselib::*; +``` diff --git a/arg_picker/src/arg.rs b/arg_picker/src/arg.rs new file mode 100644 index 0000000..78ad539 --- /dev/null +++ b/arg_picker/src/arg.rs @@ -0,0 +1,299 @@ +use crate::Pickable; +use std::marker::PhantomData; + +/// 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. +/// +/// - `_type`: PhantomData to hold the type parameter. +#[derive(Default, Clone, Copy)] +pub struct PickerArg<'a, Type> +where + Type: Pickable<'a>, +{ + /// Full name, may include variant names (aliases), e.g., `["config", "cfg"]`. + pub full: &'a [&'a str], + + /// Short name, e.g., `'c'`. + pub short: Option, + + /// Whether the parameter is positional (no flag, matched by position). + pub positional: bool, + + /// PhantomData to hold the type parameter. + pub internal_type: PhantomData, +} + +impl<'a, Type> From<&'a PickerArg<'a, Type>> for PickerArg<'a, Type> +where + Type: Pickable<'a>, +{ + fn from(value: &'a PickerArg<'a, Type>) -> Self { + PickerArg { + full: value.full, + short: value.short, + positional: value.positional, + internal_type: PhantomData, + } + } +} + +impl<'a, Type> PickerArg<'a, Type> +where + Type: Pickable<'a>, +{ + /// Creates a new `PickerArg` with the provided parameters. + pub fn new(full: &'a [&'a str], short: Option, positional: bool) -> Self { + Self { + full, + short, + positional, + internal_type: PhantomData, + } + } + + /// 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 + } +} + +/// Describes the attribute (behavior) of a command-line parameter. +/// +/// The ordering reflects parse priority (higher = parsed first): +/// `PositionalMulti < Positional < Flag < Single < Multi` +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub enum PickerArgAttr { + /// Positional argument that accepts multiple values (e.g., multiple input files). + PositionalMulti, + + /// Positional argument matched by its position (e.g., an input file). + #[default] + Positional, + + /// Boolean flag with no associated value (e.g., `--verbose`). + Flag, + + /// Accepts a single value (e.g., `--name Alice`). + Single, + + /// Accepts multiple values (e.g., `--file a.txt --file b.txt`). + Multi, +} + +impl PickerArgAttr { + /// Determines if the given `PickerArg` represents a positional parameter. + /// + /// If the flag is positional (determined by `flag.is_positional()`), returns + /// `PickerArgAttr::Positional`. Otherwise, invokes the `other` closure to + /// produce and return a `PickerArgAttr`. + /// + /// # Parameters + /// + /// - `flag`: A reference to the [`PickerArg`] to evaluate. + /// - `other`: A closure that returns a [`PickerArgAttr`] when the flag is + /// **not** positional. + #[inline(always)] + pub fn positional_or_else<'a, T>( + flag: &PickerArg<'a, T>, + other: fn() -> PickerArgAttr, + ) -> PickerArgAttr + where + T: Pickable<'a>, + { + if flag.is_positional() { + PickerArgAttr::Positional + } else { + other() + } + } + + /// Determines if the given `PickerArg` represents a positional parameter and returns + /// `PickerArgAttr::Positional` if so. Otherwise, returns the provided `default` attribute. + /// + /// # Parameters + /// + /// - `flag`: A reference to the [`PickerArg`] to evaluate. + /// - `default`: The [`PickerArgAttr`] to return if the flag is not positional. + #[inline(always)] + pub fn positional_or<'a, T>(flag: &PickerArg<'a, T>, default: PickerArgAttr) -> PickerArgAttr + where + T: Pickable<'a>, + { + if flag.is_positional() { + PickerArgAttr::Positional + } else { + default + } + } + + /// Determines if the given `PickerArg` represents a positional parameter and returns + /// `PickerArgAttr::Positional` if so. Otherwise, returns `PickerArgAttr::Single`. + /// + /// # Parameters + /// + /// - `flag`: A reference to the [`PickerArg`] to evaluate. + #[inline(always)] + pub fn positional_or_single<'a, T>(flag: &PickerArg<'a, T>) -> PickerArgAttr + where + T: Pickable<'a>, + { + if flag.is_positional() { + PickerArgAttr::Positional + } else { + PickerArgAttr::Single + } + } + + /// Determines if the given `PickerArg` represents a positional parameter and returns + /// `PickerArgAttr::PositionalMulti` if so. Otherwise, returns `PickerArgAttr::Multi`. + /// + /// # Parameters + /// + /// - `flag`: A reference to the [`PickerArg`] to evaluate. + #[inline(always)] + pub fn positional_or_multi<'a, T>(flag: &PickerArg<'a, T>) -> PickerArgAttr + where + T: Pickable<'a>, + { + if flag.is_positional() { + PickerArgAttr::PositionalMulti + } else { + PickerArgAttr::Multi + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_picker_flag_attr_ordering() { + // Multi > Single > Flag > Positional > PositionalMulti + assert!(PickerArgAttr::Multi > PickerArgAttr::Single); + assert!(PickerArgAttr::Multi > PickerArgAttr::Flag); + assert!(PickerArgAttr::Multi > PickerArgAttr::Positional); + assert!(PickerArgAttr::Multi > PickerArgAttr::PositionalMulti); + + assert!(PickerArgAttr::Single > PickerArgAttr::Flag); + assert!(PickerArgAttr::Single > PickerArgAttr::Positional); + assert!(PickerArgAttr::Single > PickerArgAttr::PositionalMulti); + + assert!(PickerArgAttr::Flag > PickerArgAttr::Positional); + assert!(PickerArgAttr::Flag > PickerArgAttr::PositionalMulti); + + assert!(PickerArgAttr::Positional > PickerArgAttr::PositionalMulti); + + // PartialOrd + assert!(PickerArgAttr::Multi >= PickerArgAttr::Single); + assert!(PickerArgAttr::Single >= PickerArgAttr::Flag); + assert!(PickerArgAttr::Flag >= PickerArgAttr::Positional); + assert!(PickerArgAttr::Positional >= PickerArgAttr::PositionalMulti); + + assert!(PickerArgAttr::PositionalMulti < PickerArgAttr::Positional); + assert!(PickerArgAttr::Positional < PickerArgAttr::Flag); + assert!(PickerArgAttr::Flag < PickerArgAttr::Single); + assert!(PickerArgAttr::Single < PickerArgAttr::Multi); + } + + #[test] + fn test_picker_flag_attr_sorting() { + // Sort + let mut values = vec![ + PickerArgAttr::Flag, + PickerArgAttr::Single, + PickerArgAttr::Positional, + PickerArgAttr::Multi, + PickerArgAttr::PositionalMulti, + ]; + values.sort(); + assert_eq!( + values, + vec![ + PickerArgAttr::PositionalMulti, + PickerArgAttr::Positional, + PickerArgAttr::Flag, + PickerArgAttr::Single, + PickerArgAttr::Multi, + ] + ); + } +} diff --git a/arg_picker/src/builtin.rs b/arg_picker/src/builtin.rs new file mode 100644 index 0000000..e855b08 --- /dev/null +++ b/arg_picker/src/builtin.rs @@ -0,0 +1,4 @@ +mod pick_bool; +mod pick_flag; +mod pick_numbers; +mod pick_string; diff --git a/arg_picker/src/builtin/pick_bool.rs b/arg_picker/src/builtin/pick_bool.rs new file mode 100644 index 0000000..ccc4424 --- /dev/null +++ b/arg_picker/src/builtin/pick_bool.rs @@ -0,0 +1,22 @@ +use crate::parselib::{FlagMatcher, Matcher}; +use crate::pickable_needed::*; + +impl<'a> Pickable<'a> for bool { + fn get_attr(_: &'a PickerArg<'a, Self>) -> PickerArgAttr { + PickerArgAttr::Flag + } + + fn tag(ctx: TagPhaseContext) -> Vec { + FlagMatcher::match_all(ctx.into()) + } + + fn pick(raw_strs: &[&str]) -> PickerArgResult { + if raw_strs.is_empty() { + // No matching flag found — signal NotFound so the fallback chain + // (default → route) gets a chance to run. + PickerArgResult::NotFound + } else { + PickerArgResult::Parsed(true) + } + } +} diff --git a/arg_picker/src/builtin/pick_flag.rs b/arg_picker/src/builtin/pick_flag.rs new file mode 100644 index 0000000..b642a9a --- /dev/null +++ b/arg_picker/src/builtin/pick_flag.rs @@ -0,0 +1,21 @@ +use crate::parselib::{FlagMatcher, Matcher}; +use crate::pickable_needed::*; +use crate::value::Flag; + +impl<'a> Pickable<'a> for Flag { + fn get_attr(_: &'a PickerArg<'a, Self>) -> PickerArgAttr { + PickerArgAttr::Flag + } + + fn tag(ctx: TagPhaseContext) -> Vec { + FlagMatcher::match_all(ctx.into()) + } + + fn pick(raw_strs: &[&str]) -> PickerArgResult { + if raw_strs.is_empty() { + PickerArgResult::Parsed(Flag::Inactive) + } else { + PickerArgResult::Parsed(Flag::Active) + } + } +} diff --git a/arg_picker/src/builtin/pick_numbers.rs b/arg_picker/src/builtin/pick_numbers.rs new file mode 100644 index 0000000..a5ab0a9 --- /dev/null +++ b/arg_picker/src/builtin/pick_numbers.rs @@ -0,0 +1,80 @@ +use crate::{BoundaryCheck, SinglePickable, pickable_needed::*}; + +macro_rules! impl_single_pickable_num { + ($($t:ty),+ $(,)?) => { + $(impl SinglePickable for $t { + fn pick_single(str: Option<&str>) -> PickerArgResult { + match str { + Some(s) => s.parse::<$t>().map(PickerArgResult::Parsed).unwrap_or(PickerArgResult::NotFound), + None => PickerArgResult::NotFound, + } + } + })+ + }; +} + +/// Returns `true` if `raw` looks like an integer (digits, optional `+`/`-` prefix, +/// no decimal point or exponent). +fn is_int_like(raw: &str) -> bool { + let s = raw.trim(); + let bytes = s.as_bytes(); + if bytes.is_empty() { + return false; + } + let mut i = 0; + if bytes[0] == b'-' || bytes[0] == b'+' { + i = 1; + } + if i >= bytes.len() { + return false; + } + for &b in &bytes[i..] { + if !b.is_ascii_digit() { + return false; + } + } + true +} + +/// Returns `true` if `raw` looks like a float (contains `.`, `e`, or `E`). +fn is_float_like(raw: &str) -> bool { + let s = raw.trim(); + s.contains('.') || s.contains('e') || s.contains('E') +} + +impl_single_pickable_num! { + i8, i16, i32, i64, i128, isize, + u8, u16, u32, u64, u128, usize, + f32, f64, +} + +// Integer boundary: only accept strings that look like integers. +// Float-like strings trigger a boundary. +macro_rules! impl_boundary_check_int { + ($($t:ty),+ $(,)?) => { + $(impl BoundaryCheck for $t { + fn check_boundary(raw: &str) -> bool { + !is_int_like(raw) + } + })+ + }; +} + +impl_boundary_check_int! { + i8, i16, i32, i64, i128, isize, + u8, u16, u32, u64, u128, usize, +} + +// Float boundary: only accept strings that look like floats. +// Integer-like strings trigger a boundary. +impl BoundaryCheck for f32 { + fn check_boundary(raw: &str) -> bool { + !is_float_like(raw) || raw.parse::().is_err() + } +} + +impl BoundaryCheck for f64 { + fn check_boundary(raw: &str) -> bool { + !is_float_like(raw) || raw.parse::().is_err() + } +} diff --git a/arg_picker/src/builtin/pick_string.rs b/arg_picker/src/builtin/pick_string.rs new file mode 100644 index 0000000..c96f667 --- /dev/null +++ b/arg_picker/src/builtin/pick_string.rs @@ -0,0 +1,11 @@ +use crate::PickerArgResult::NotFound; +use crate::{SinglePickable, pickable_needed::*}; + +impl SinglePickable for String { + fn pick_single(str: Option<&str>) -> PickerArgResult { + match str { + Some(str) => PickerArgResult::Parsed(str.to_string()), + None => NotFound, + } + } +} diff --git a/arg_picker/src/corebind.rs b/arg_picker/src/corebind.rs new file mode 100644 index 0000000..3581871 --- /dev/null +++ b/arg_picker/src/corebind.rs @@ -0,0 +1,2 @@ +mod entry_picker; +pub use entry_picker::*; diff --git a/arg_picker/src/corebind/entry_picker.rs b/arg_picker/src/corebind/entry_picker.rs new file mode 100644 index 0000000..69bc4d8 --- /dev/null +++ b/arg_picker/src/corebind/entry_picker.rs @@ -0,0 +1,131 @@ +use std::marker::PhantomData; + +use mingling_core::{ChainProcess, Groupped, ProgramCollect}; + +use crate::{Pickable, Picker, PickerArg, PickerArgs, PickerPattern1}; + +/// Trait for converting Mingling entry types (types that implement `Groupped` and `Into>`) +/// into [`Picker`] instances for a given route type `R`. +/// +/// This trait provides a bridge between entry definitions created with the `mingling` framework +/// and the picker argument system used for CLI argument parsing and routing. +/// +/// # Type Parameters +/// +/// * `'a` — The lifetime of the picker and its references to argument definitions. +/// * `This` — The program type used for dispatching runtime routes; must implement [`ProgramCollect`]. +/// * `Route` — The route type used for dispatching; must implement [`Groupped`]. +pub trait EntryPicker<'a, This> { + /// Converts `self` into a [`Picker`] for the given route type `Route`. + fn to_picker(self) -> Picker<'a, ChainProcess>; + + /// Starts building a picker pattern with the first argument. + /// + /// Returns a [`PickerPattern1`] that can be further chained with additional + /// arguments, defaults, routes, and post-processing. + /// + /// # Type Parameters + /// + /// * `Next` — The nominal type of the first argument; must implement [`Pickable`]. + /// + /// # Parameters + /// + /// * `arg` — The argument definition, typically obtained from [`crate::arg`]. + fn pick( + self, + arg: impl Into<&'a PickerArg<'a, Next>>, + ) -> PickerPattern1<'a, Next, ChainProcess> + where + Self: Sized, + Next: Pickable<'a> + Default + Sized, + { + let picker = Self::to_picker(self); + Picker::build_pattern1(picker.args, arg.into(), None) + } + + /// Starts building a picker pattern with the first argument, using a default value provider. + /// + /// This is a shorthand for calling `.pick(arg).or(func)`. + /// + /// # Type Parameters + /// + /// * `Next` — The nominal type of the first argument; must implement [`Pickable`]. + /// + /// # Parameters + /// + /// * `arg` — The argument definition, typically obtained from [`crate::arg`]. + /// * `func` — A closure that provides a default value if the arg is not provided by the user. + fn pick_or( + self, + arg: impl Into<&'a PickerArg<'a, Next>>, + func: F, + ) -> PickerPattern1<'a, Next, ChainProcess> + where + Self: Sized, + Next: Pickable<'a> + Default + Sized, + F: FnMut() -> Next + 'static, + { + self.pick(arg).or(func) + } + + /// Starts building a picker pattern with the first argument, using a default value. + /// + /// This is a shorthand for calling `.pick(arg).or_default()`. + /// + /// # Type Parameters + /// + /// * `Next` — The nominal type of the first argument; must implement [`Pickable`] and [`Default`]. + /// + /// # Parameters + /// + /// * `arg` — The argument definition, typically obtained from [`crate::arg`]. + fn pick_or_default( + self, + arg: impl Into<&'a PickerArg<'a, Next>>, + ) -> PickerPattern1<'a, Next, ChainProcess> + where + Self: Sized, + Next: Pickable<'a> + Default + Sized, + { + self.pick(arg).or_default() + } + + /// Starts building a picker pattern with the first argument, using a route if the arg is not provided. + /// + /// This is a shorthand for calling `.pick(arg).or_route(func)`. + /// + /// # Type Parameters + /// + /// * `Next` — The nominal type of the first argument; must implement [`Pickable`]. + /// + /// # Parameters + /// + /// * `arg` — The argument definition, typically obtained from [`crate::arg`]. + /// * `func` — A closure that produces a route value if the arg is not provided by the user. + fn pick_or_route( + self, + arg: impl Into<&'a PickerArg<'a, Next>>, + func: F, + ) -> PickerPattern1<'a, Next, ChainProcess> + where + Self: Sized, + Next: Pickable<'a> + Default + Sized, + F: FnMut() -> ChainProcess + 'static, + { + self.pick(arg).or_route(func) + } +} + +impl<'a, This, Bind> EntryPicker<'a, This> for Bind +where + This: ProgramCollect, + Bind: Groupped + Into>, +{ + fn to_picker(self) -> Picker<'a, ChainProcess> { + let args = self.into(); + Picker { + route_phantom: PhantomData, + args: PickerArgs::Owned(args), + } + } +} diff --git a/arg_picker/src/infos.rs b/arg_picker/src/infos.rs new file mode 100644 index 0000000..074539a --- /dev/null +++ b/arg_picker/src/infos.rs @@ -0,0 +1,446 @@ +use crate::{Pickable, PickerArg}; + +/// Represents the result of parsing or looking up a value. +/// +/// This enum is generic over the type being parsed. It models three possible outcomes: +/// - [`Unparsed`](PickerArgResult::Unparsed): The value has not yet been parsed (default). +/// - [`Parsed`](PickerArgResult::Parsed): The value was successfully parsed into `Type`. +/// - [`NotFound`](PickerArgResult::NotFound): The requested value could not be found. +#[derive(Default)] +pub enum PickerArgResult { + /// 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, +} + +impl From> for PickerArgResult { + /// Converts a `Result` into a `PickerArgResult`. + /// + /// - `Ok(value)` maps to [`Parsed(value)`](PickerArgResult::Parsed). + /// - `Err(_)` maps to [`NotFound`](PickerArgResult::NotFound). + fn from(result: Result) -> Self { + match result { + Ok(value) => PickerArgResult::Parsed(value), + Err(_) => PickerArgResult::NotFound, + } + } +} + +impl From> for PickerArgResult { + /// Converts an `Option` into a `PickerArgResult`. + /// + /// - `Some(value)` maps to [`Parsed(value)`](PickerArgResult::Parsed). + /// - `None` maps to [`NotFound`](PickerArgResult::NotFound). + fn from(option: Option) -> Self { + match option { + Some(value) => PickerArgResult::Parsed(value), + None => PickerArgResult::NotFound, + } + } +} + +impl PickerArgResult { + /// Returns `true` if the result is [`Parsed`](PickerArgResult::Parsed). + /// + /// # Examples + /// + /// ``` + /// use arg_picker::PickerArgResult; + /// + /// let result: PickerArgResult = PickerArgResult::Parsed(42); + /// assert!(result.is_parsed()); + /// + /// let result: PickerArgResult = PickerArgResult::NotFound; + /// assert!(!result.is_parsed()); + /// ``` + pub fn is_parsed(&self) -> bool { + matches!(self, PickerArgResult::Parsed(_)) + } + + /// Returns `true` if the result is [`Parsed`](PickerArgResult::Parsed) or [`NotFound`](PickerArgResult::NotFound). + /// i.e., the value exists (was either found or not yet parsed). + /// Typically indicates the value was "found" in some sense. + /// + /// # Examples + /// + /// ``` + /// use arg_picker::PickerArgResult; + /// + /// let result: PickerArgResult = PickerArgResult::Parsed(42); + /// assert!(result.is_found()); + /// + /// let result: PickerArgResult = PickerArgResult::NotFound; + /// assert!(result.is_found()); + /// ``` + pub fn is_found(&self) -> bool { + matches!(self, PickerArgResult::Parsed(_) | PickerArgResult::NotFound) + } + + /// Returns `true` if the result is [`Unparsed`](PickerArgResult::Unparsed) or [`NotFound`](PickerArgResult::NotFound). + /// + /// # Examples + /// + /// ``` + /// use arg_picker::PickerArgResult; + /// + /// let result: PickerArgResult = PickerArgResult::Unparsed; + /// assert!(result.is_err()); + /// + /// let result: PickerArgResult = PickerArgResult::Parsed(10); + /// assert!(!result.is_err()); + /// ``` + pub fn is_err(&self) -> bool { + !matches!(self, PickerArgResult::Parsed(_)) + } + + /// Returns `Some(&Type)` if [`Parsed`](PickerArgResult::Parsed), otherwise `None`. + /// + /// # Examples + /// + /// ``` + /// use arg_picker::PickerArgResult; + /// + /// let result: PickerArgResult = PickerArgResult::Parsed(42); + /// assert_eq!(result.parsed(), Some(&42)); + /// + /// let result: PickerArgResult = PickerArgResult::NotFound; + /// assert_eq!(result.parsed(), None); + /// ``` + pub fn parsed(&self) -> Option<&Type> { + if let PickerArgResult::Parsed(value) = self { + Some(value) + } else { + None + } + } + + /// Returns the contained [`Parsed`](PickerArgResult::Parsed) value or panics with a given message. + /// + /// # Panics + /// Panics if the value is not [`Parsed`](PickerArgResult::Parsed), with a message including the provided `msg`. + /// + /// # Examples + /// + /// ```should_panic + /// use arg_picker::PickerArgResult; + /// + /// let result: PickerArgResult = PickerArgResult::NotFound; + /// result.expect("expected a parsed value"); + /// ``` + pub fn expect(self, msg: &str) -> Type { + match self { + PickerArgResult::Parsed(value) => value, + _ => panic!("{}", msg), + } + } + + /// Returns the contained [`Parsed`](PickerArgResult::Parsed) value or panics. + /// + /// # Panics + /// Panics if the value is not [`Parsed`](PickerArgResult::Parsed). + /// + /// # Examples + /// + /// ``` + /// use arg_picker::PickerArgResult; + /// + /// let result: PickerArgResult = PickerArgResult::Parsed(42); + /// assert_eq!(result.unwrap(), 42); + /// ``` + /// + /// ```should_panic + /// use arg_picker::PickerArgResult; + /// + /// let result: PickerArgResult = PickerArgResult::NotFound; + /// result.unwrap(); + /// ``` + pub fn unwrap(self) -> Type { + match self { + PickerArgResult::Parsed(value) => value, + PickerArgResult::Unparsed => { + panic!("called `PickerArgResult::unwrap()` on an `Unparsed` value") + } + PickerArgResult::NotFound => { + panic!("called `PickerArgResult::unwrap()` on a `NotFound` value") + } + } + } + + /// Returns the contained [`Parsed`](PickerArgResult::Parsed) value or a provided `default`. + /// + /// # Examples + /// + /// ``` + /// use arg_picker::PickerArgResult; + /// + /// let result: PickerArgResult = PickerArgResult::Parsed(42); + /// assert_eq!(result.unwrap_or(0), 42); + /// + /// let result: PickerArgResult = PickerArgResult::NotFound; + /// assert_eq!(result.unwrap_or(0), 0); + /// ``` + pub fn unwrap_or(self, default: Type) -> Type { + match self { + PickerArgResult::Parsed(value) => value, + _ => default, + } + } + + /// Returns the contained [`Parsed`](PickerArgResult::Parsed) value or computes it from a closure. + /// + /// # Examples + /// + /// ``` + /// use arg_picker::PickerArgResult; + /// + /// let result: PickerArgResult = PickerArgResult::Parsed(42); + /// assert_eq!(result.unwrap_or_else(|| 0), 42); + /// + /// let result: PickerArgResult = PickerArgResult::NotFound; + /// assert_eq!(result.unwrap_or_else(|| 0), 0); + /// ``` + pub fn unwrap_or_else Type>(self, f: F) -> Type { + match self { + PickerArgResult::Parsed(value) => value, + _ => f(), + } + } + + /// Returns the contained [`Parsed`](PickerArgResult::Parsed) value or the default value of `Type`. + /// + /// # Examples + /// + /// ``` + /// use arg_picker::PickerArgResult; + /// + /// let result: PickerArgResult = PickerArgResult::Parsed(42); + /// assert_eq!(result.unwrap_or_default(), 42); + /// + /// let result: PickerArgResult = PickerArgResult::NotFound; + /// assert_eq!(result.unwrap_or_default(), 0); + /// ``` + pub fn unwrap_or_default(self) -> Type + where + Type: Default, + { + match self { + PickerArgResult::Parsed(value) => value, + _ => Type::default(), + } + } + + /// Converts `PickerArgResult` into `Option`. + /// + /// Returns `Some(Type)` if [`Parsed`](PickerArgResult::Parsed), otherwise `None`. + /// + /// # Examples + /// + /// ``` + /// use arg_picker::PickerArgResult; + /// + /// let result: PickerArgResult = PickerArgResult::Parsed(42); + /// assert_eq!(result.to_option(), Some(42)); + /// + /// let result: PickerArgResult = PickerArgResult::NotFound; + /// assert_eq!(result.to_option(), None); + /// + /// let result: PickerArgResult = PickerArgResult::Unparsed; + /// assert_eq!(result.to_option(), None); + /// ``` + pub fn to_option(self) -> Option { + match self { + PickerArgResult::Parsed(value) => Some(value), + _ => None, + } + } +} + +/// Represents metadata about a command-line argument or flag. +/// +/// This struct stores all relevant information about a tag/argument that can be used +/// for parsing command-line inputs. It includes the short form (e.g., `-n`), long form +/// (e.g., `--name`), aliases, and various flags that control parsing behavior. +pub struct PickerArgInfo<'a> { + /// The short form of the tag, e.g. `'n'` for `-n`. + pub short: Option, + /// The long form of the tag, e.g. `"name"` for `--name`. + pub long: Option<&'a str>, + /// Alternative names for the tag, e.g. `["-N", "--nickname"]`. + pub alias: Option>, + /// Whether this tag is a positional argument (no `-` or `--` prefix). + pub positional: bool, + /// Whether this tag is optional or required. + pub optional: bool, + /// Whether this tag can accept multiple values. + pub multi: bool, + /// Whether this tag participates in parsing after a `--` separator. + pub is_flag: bool, +} + +impl<'a, T> From> for PickerArgInfo<'a> +where + T: Pickable<'a>, +{ + fn from(value: PickerArg<'a, T>) -> Self { + let (long, alias) = match value.full.len() { + 0 => (None, None), + _ => { + let long = Some(value.full[0]); + let alias = if value.full.len() > 1 { + Some(value.full[1..].to_vec()) + } else { + None + }; + (long, alias) + } + }; + + Self { + short: value.short, + long, + alias, + positional: value.positional, + optional: false, + multi: false, + is_flag: false, + } + } +} + +impl<'a, T: Pickable<'a>> From<&'a PickerArg<'a, T>> for PickerArgInfo<'a> { + fn from(value: &'a PickerArg<'a, T>) -> Self { + let (long, alias) = match value.full.len() { + 0 => (None, None), + _ => { + let long = Some(value.full[0]); + let alias = if value.full.len() > 1 { + Some(value.full[1..].to_vec()) + } else { + None + }; + (long, alias) + } + }; + + Self { + short: value.short, + long, + alias, + positional: value.positional, + optional: false, + multi: false, + is_flag: false, + } + } +} + +impl<'a> PickerArgInfo<'a> { + /// Create a new `PickerTag` with default values. + pub fn new() -> Self { + Self { + short: None, + long: None, + alias: None, + positional: false, + optional: false, + multi: false, + is_flag: false, + } + } + + /// Set the short flag (e.g., `'n'` for `-n`). + pub fn with_short(mut self, short: char) -> Self { + self.short = Some(short); + self + } + + /// Set the long flag (e.g., `"name"` for `--name`). + pub fn with_long(mut self, long: &'a str) -> Self { + self.long = Some(long); + self + } + + /// Set aliases for the tag. + pub fn with_alias(mut self, alias: Vec<&'a str>) -> Self { + self.alias = Some(alias); + self + } + + /// Mark the tag as positional. + pub fn with_positional(mut self, positional: bool) -> Self { + self.positional = positional; + self + } + + /// Mark the tag as optional. + pub fn with_optional(mut self, optional: bool) -> Self { + self.optional = optional; + self + } + + /// Mark the tag as multi-value. + pub fn with_multi(mut self, multi: bool) -> Self { + self.multi = multi; + self + } + + /// Mark the tag as a flag that participates in parsing after `--`. + pub fn with_is_flag(mut self, is_flag: bool) -> Self { + self.is_flag = is_flag; + self + } + + /// Set the short flag (e.g., `'n'` for `-n`). + pub fn set_short(&mut self, short: char) -> &mut Self { + self.short = Some(short); + self + } + + /// Set the long flag (e.g., `"name"` for `--name`). + pub fn set_long(&mut self, long: &'a str) -> &mut Self { + self.long = Some(long); + self + } + + /// Set aliases for the tag. + pub fn set_alias(&mut self, alias: Vec<&'a str>) -> &mut Self { + self.alias = Some(alias); + self + } + + /// Set whether this tag is positional. + pub fn set_positional(&mut self, positional: bool) -> &mut Self { + self.positional = positional; + self + } + + /// Set whether this tag is optional. + pub fn set_optional(&mut self, optional: bool) -> &mut Self { + self.optional = optional; + self + } + + /// Set whether this tag accepts multiple values. + pub fn set_multi(&mut self, multi: bool) -> &mut Self { + self.multi = multi; + self + } + + /// Set whether this tag participates in parsing after a `--` separator. + pub fn set_is_flag(&mut self, is_flag: bool) -> &mut Self { + self.is_flag = is_flag; + self + } +} + +impl<'a> Default for PickerArgInfo<'a> { + fn default() -> Self { + Self::new() + } +} diff --git a/arg_picker/src/lib.rs b/arg_picker/src/lib.rs new file mode 100644 index 0000000..21a0d35 --- /dev/null +++ b/arg_picker/src/lib.rs @@ -0,0 +1,62 @@ +#![doc = include_str!("../README.md")] + +mod builtin; + +mod picker; +pub use picker::*; + +mod pickable; +pub use pickable::*; + +mod arg; +pub use arg::*; + +mod infos; +pub use infos::*; + +/// Provides the specific parsing logic for command-line arguments and common utilities, +/// as well as customization of command-line argument styles. +pub mod parselib; + +/// Parser-provided parseable command-line types +pub mod value; + +/// The prelude module, which re-exports the most commonly used traits and types. +/// +/// This module is intended to be imported with a wildcard import: +/// +/// ``` +/// use arg_picker::prelude::*; +/// ``` +pub mod prelude { + pub use crate::macros::arg; + + #[cfg(not(feature = "mingling_support"))] + pub use crate::IntoPicker; + + #[cfg(feature = "mingling_support")] + pub use crate::corebind::EntryPicker; +} + +/// Re-export of the `arg_picker_macros` crate +pub mod macros { + pub use arg_picker_macros::arg; +} + +/// Provides the types necessary for implementing the `Pickable` trait +pub mod pickable_needed { + pub use crate::{Pickable, PickerArg, PickerArgAttr, PickerArgResult, TagPhaseContext}; +} + +/// Provides the types necessary for implementing the `Matcher` trait +pub mod matcher_needed { + pub use crate::PickerArgInfo; + pub use crate::parselib::{MaskedArg, Matcher, ParserStyle}; +} + +#[cfg(feature = "mingling_support")] +mod corebind; + +#[allow(unused_imports)] +#[cfg(feature = "mingling_support")] +pub use corebind::*; diff --git a/arg_picker/src/parselib.rs b/arg_picker/src/parselib.rs new file mode 100644 index 0000000..7fbd606 --- /dev/null +++ b/arg_picker/src/parselib.rs @@ -0,0 +1,144 @@ +mod flag_matcher; +pub use flag_matcher::*; + +mod arg_matcher; +pub use arg_matcher::*; + +mod multi_arg_matcher; +pub use multi_arg_matcher::*; + +mod pos_matcher; +pub use pos_matcher::*; + +mod single_matcher; +pub use single_matcher::*; + +mod style; +pub use style::*; + +mod utils; +pub use utils::*; + +use crate::{PickerArgInfo, PickerArgs}; + +/// Represents a single argument with its original raw string and index. +/// +/// This is used during pattern matching to provide context about +/// which argument is being processed. +#[derive(Clone, Copy, PartialEq, Eq)] +pub struct MaskedArg<'a> { + /// The raw string value of the argument. + pub raw: &'a str, + /// The original index of the argument in the full argument list. + pub raw_idx: usize, +} + +/// Trait for defining matching logic against masked arguments. +/// +/// Implementors can define custom strategies for matching one or all +/// arguments that pass through a mask filter. +pub trait Matcher { + /// Called when only one match is needed. + /// + /// Returns the index of the first matched argument, or `None` if no match. + fn on_match_one( + args: &[MaskedArg], + style: &ParserStyle, + arg_info: &PickerArgInfo, + ) -> Option; + + /// Called when all matches are needed. + /// + /// Returns a vector of indices of all matched arguments. + fn on_match_all( + args: &[MaskedArg], + style: &ParserStyle, + arg_info: &PickerArgInfo, + ) -> Vec; + + /// Convenience method that builds masked arguments from `PickerArgs` and a mask, + /// then calls `on_match_one`. + fn match_one<'a>(ctx: MatcherContext<'a>) -> Option { + let masked_args = build_masked_args(ctx.args, ctx.mask); + Self::on_match_one(masked_args.as_slice(), ctx.style, ctx.arg_info) + } + + /// Convenience method that builds masked arguments from `PickerArgs` and a mask, + /// then calls `on_match_all`. + fn match_all<'a>(ctx: MatcherContext<'a>) -> Vec { + let masked_args = build_masked_args(ctx.args, ctx.mask); + Self::on_match_all(masked_args.as_slice(), ctx.style, ctx.arg_info) + } +} + +/// Context for matcher operations +/// +/// This struct bundles together the key pieces of data needed during matching: +/// - `args`: The full set of parsed arguments. +/// - `mask`: A byte mask indicating which arguments are currently active (non-zero = active). +/// - `style`: The parsing style configuration. +pub struct MatcherContext<'a> { + /// The full set of parsed arguments. + pub args: &'a PickerArgs<'a>, + + /// A byte mask where non-zero values indicate the argument at that position is active/should be matched. + pub mask: &'a [u8], + + /// The parsing style configuration. + pub style: &'a ParserStyle<'a>, + + /// Metadata about the command-line argument/flag being processed. + /// + /// Contains information such as short form (`-n`), long form (`--name`), + /// aliases, and parsing flags (positional, optional, multi, is_flag). + /// Used by matchers to make decisions based on argument characteristics. + pub arg_info: &'a PickerArgInfo<'a>, +} + +impl<'a> From<&'a crate::TagPhaseContext<'a>> for MatcherContext<'a> { + fn from(ctx: &'a crate::TagPhaseContext<'a>) -> Self { + MatcherContext { + args: ctx.args, + mask: ctx.mask, + style: ParserStyle::global_style(), + arg_info: ctx.arg_info, + } + } +} + +impl<'a> From> for MatcherContext<'a> { + fn from(ctx: crate::TagPhaseContext<'a>) -> Self { + MatcherContext { + args: ctx.args, + mask: ctx.mask, + style: ParserStyle::global_style(), + arg_info: ctx.arg_info, + } + } +} + +#[inline(always)] +fn is_masked(mask: &[u8], idx: usize) -> bool { + idx < mask.len() && mask[idx] != 0 +} + +#[inline(always)] +fn build_masked_args<'a>(args: &'a PickerArgs, mask: &'a [u8]) -> Vec> { + let mut cidx = 0; + args.iter() + .filter_map(|r| { + let idx = cidx; + cidx += 1; + // Include args where mask is 0 (available/not yet claimed). + // mask[i] = 0 means available; mask[i] != 0 means already claimed. + if !is_masked(mask, idx) { + Some(MaskedArg { + raw: r, + raw_idx: idx, + }) + } else { + None + } + }) + .collect() +} diff --git a/arg_picker/src/parselib/arg_matcher.rs b/arg_picker/src/parselib/arg_matcher.rs new file mode 100644 index 0000000..38bb9cc --- /dev/null +++ b/arg_picker/src/parselib/arg_matcher.rs @@ -0,0 +1,142 @@ +use crate::{ + matcher_needed::*, + parselib::{build_possible_flags, seek_end_of_options}, +}; + +/// `ArgMatcher` is used for parameters that carry a single value. +/// +/// It handles two scenarios: +/// +/// **Named** — `--name Alice` or `--name=Alice`. +/// Each flag occurrence consumes **one** following argument as its value, +/// regardless of what it is (even if it looks like a flag). +/// This ensures the mask correctly claims the value slot; validation is +/// the `Pickable`'s responsibility. +/// +/// **Positional** — no flag prefix, matched by position. +/// +/// # Examples +/// +/// | Input | `on_match_one` | `on_match_all` | +/// |-------|----------------|----------------| +/// | `--name Alice` | `[0, 1]` (via Pickable tag) | `[0, 1]` | +/// | `--name=Alice` | `[0]` | `[0]` | +/// | `--val a --val b` | `[0, 1]` | `[0, 1, 2, 3]` | +/// +/// Args after `--` are ignored. +pub struct ArgMatcher; + +impl ArgMatcher { + /// Check whether `raw` matches `flag_str`, optionally with an inline value + /// separated by the style's value separator (`=` for Unix, `:` for PowerShell). + #[inline(always)] + fn matches(raw: &str, flag_str: &str, case_sensitive: bool, sep: char) -> bool { + let eq_match = + |r: &str, f: &str| r.len() > f.len() && r.as_bytes().get(f.len()) == Some(&(sep as u8)); + + if case_sensitive { + raw == flag_str || (raw.starts_with(flag_str) && eq_match(raw, flag_str)) + } else { + raw.eq_ignore_ascii_case(flag_str) + || (raw.len() > flag_str.len() + && raw[..flag_str.len()].eq_ignore_ascii_case(flag_str) + && raw.as_bytes()[flag_str.len()] == sep as u8) + } + } + + /// Check whether the argument contains its value inline via the style's + /// value separator (eq mode), so no extra mask slot is needed. + #[inline(always)] + fn is_inline_value(raw: &str, flag_str: &str, sep: char) -> bool { + raw.len() > flag_str.len() && raw.as_bytes().get(flag_str.len()) == Some(&(sep as u8)) + } +} + +impl Matcher for ArgMatcher { + fn on_match_one( + args: &[MaskedArg], + style: &ParserStyle, + arg_info: &PickerArgInfo, + ) -> Option { + if arg_info.positional { + return args.first().map(|a| a.raw_idx); + } + + let possible_flags = build_possible_flags(style, arg_info); + let end = seek_end_of_options(args, style); + let sep = style.value_separator; + + for arg in args { + if end.is_some_and(|e| arg.raw_idx >= e) { + break; + } + + let matched = possible_flags + .iter() + .any(|f| Self::matches(arg.raw, f, style.case_sensitive, sep)); + if matched { + return Some(arg.raw_idx); + } + } + + None + } + + fn on_match_all( + args: &[MaskedArg], + style: &ParserStyle, + arg_info: &PickerArgInfo, + ) -> Vec { + if arg_info.positional { + let end = seek_end_of_options(args, style); + return args + .iter() + .take_while(|a| end.is_none_or(|e| a.raw_idx < e)) + .map(|a| a.raw_idx) + .collect(); + } + + let possible_flags = build_possible_flags(style, arg_info); + let end = seek_end_of_options(args, style); + let sep = style.value_separator; + + let mut result = Vec::new(); + let mut i = 0; + while i < args.len() { + if end.is_some_and(|e| args[i].raw_idx >= e) { + break; + } + + let matched = possible_flags + .iter() + .any(|f| Self::matches(args[i].raw, f, style.case_sensitive, sep)); + + if matched { + let flag_str = possible_flags + .iter() + .find(|f| Self::matches(args[i].raw, f, style.case_sensitive, sep)) + .expect("already matched"); + + result.push(args[i].raw_idx); + + if !Self::is_inline_value(args[i].raw, flag_str, sep) { + if i + 1 < args.len() + // Don't consume `--` (end-of-options marker) as a value. + && end.is_none_or(|e| args[i + 1].raw_idx < e) + { + result.push(args[i + 1].raw_idx); + i += 2; + continue; + } + i += 1; + continue; + } + i += 1; + continue; + } + i += 1; + } + + result + } +} diff --git a/arg_picker/src/parselib/flag_matcher.rs b/arg_picker/src/parselib/flag_matcher.rs new file mode 100644 index 0000000..e93d35a --- /dev/null +++ b/arg_picker/src/parselib/flag_matcher.rs @@ -0,0 +1,82 @@ +use crate::{ + matcher_needed::*, + parselib::{build_possible_flags, get_seeked_first, multi_seek_eq, seek_end_of_options}, +}; + +/// `FlagMatcher` is used to match flags in command-line arguments. +/// +/// Flags typically start with `-` or `--` (e.g., `-h`, `--help`), +/// and do not carry additional values. This matcher is responsible for finding +/// these flags in the argument list, taking into account that flags after `--` +/// (end-of-options marker) should not be matched. +pub struct FlagMatcher; + +impl Matcher for FlagMatcher { + fn on_match_one( + args: &[MaskedArg], + style: &ParserStyle, + arg_info: &PickerArgInfo, + ) -> Option { + let possible_flags = build_possible_flags(style, arg_info); + let flag_refs: Vec<&str> = possible_flags.iter().map(|s| s.as_str()).collect(); + let end_of_options = seek_end_of_options(args, style); + + let result = get_seeked_first(multi_seek_eq(args, &flag_refs, style.case_sensitive)); + + match (end_of_options, result) { + (Some(end), Some(current)) if current > end => None, + _ => result, + } + } + + fn on_match_all( + args: &[MaskedArg], + style: &ParserStyle, + arg_info: &PickerArgInfo, + ) -> Vec { + let possible_flags = build_possible_flags(style, arg_info); + single_pass_match_all(args, style, &possible_flags) + } +} + +/// Single-pass match: finds the `--` marker and matching flags in one iteration. +fn single_pass_match_all( + args: &[MaskedArg], + style: &ParserStyle, + possible_flags: &[String], +) -> Vec { + let flag_refs: Vec<&str> = possible_flags.iter().map(|s| s.as_str()).collect(); + let eoo = style.end_of_options; + let case_sensitive = style.case_sensitive; + + let mut end_pos: Option = None; + let mut matches: Vec = Vec::new(); + + for arg in args { + if end_pos.is_none() { + let is_eoo = if case_sensitive { + arg.raw == eoo + } else { + arg.raw.eq_ignore_ascii_case(eoo) + }; + if is_eoo { + end_pos = Some(arg.raw_idx); + continue; + } + } + + // Only match flags before the end-of-options marker. + if end_pos.is_none() { + let matched = if case_sensitive { + flag_refs.contains(&arg.raw) + } else { + flag_refs.iter().any(|s| arg.raw.eq_ignore_ascii_case(s)) + }; + if matched { + matches.push(arg.raw_idx); + } + } + } + + matches +} diff --git a/arg_picker/src/parselib/multi_arg_matcher.rs b/arg_picker/src/parselib/multi_arg_matcher.rs new file mode 100644 index 0000000..748b1be --- /dev/null +++ b/arg_picker/src/parselib/multi_arg_matcher.rs @@ -0,0 +1,141 @@ +use crate::{ + matcher_needed::*, + parselib::{build_possible_flags, seek_end_of_options}, +}; + +/// `MultiArgMatcher` matches a named flag and **all** consecutive arguments +/// that follow it, stopping at the next flag, the `--` marker, or the end +/// of the argument list. +/// +/// This is the tag implementation for `Multi` and `GreedyMulti` types +/// such as `Vec` (`--files a.txt b.txt`). +/// +/// # Behavior +/// +/// | Input | `on_match_all` | +/// |-------|----------------| +/// | `--val a b --val d e` | `[0, 1, 2, 5, 6]` (two groups) | +/// | `--val=1 2` | `[0, 1]` (eq mode + one extra value) | +/// +/// Args after `--` are ignored. +pub struct MultiArgMatcher; + +impl Matcher for MultiArgMatcher { + fn on_match_one( + args: &[MaskedArg], + style: &ParserStyle, + arg_info: &PickerArgInfo, + ) -> Option { + if arg_info.positional { + return args.first().map(|a| a.raw_idx); + } + + let possible_flags = build_possible_flags(style, arg_info); + let end = seek_end_of_options(args, style); + let sep = style.value_separator; + + for arg in args { + if end.is_some_and(|e| arg.raw_idx >= e) { + break; + } + let matched = possible_flags + .iter() + .any(|f| Self::flag_match(arg.raw, f, style.case_sensitive, sep)); + if matched { + return Some(arg.raw_idx); + } + } + None + } + + fn on_match_all( + args: &[MaskedArg], + style: &ParserStyle, + arg_info: &PickerArgInfo, + ) -> Vec { + if arg_info.positional { + let end = seek_end_of_options(args, style); + return args + .iter() + .take_while(|a| end.is_none_or(|e| a.raw_idx < e)) + .map(|a| a.raw_idx) + .collect(); + } + + let possible_flags = build_possible_flags(style, arg_info); + let end = seek_end_of_options(args, style); + let sep = style.value_separator; + let is_flag = + |raw: &str| raw.starts_with(style.long_prefix) || raw.starts_with(style.short_prefix); + let is_our_flag = |raw: &str| { + possible_flags + .iter() + .any(|f| Self::flag_match(raw, f, style.case_sensitive, sep)) + }; + + let mut result = Vec::new(); + let mut i = 0; + while i < args.len() { + if end.is_some_and(|e| args[i].raw_idx >= e) { + break; + } + + let matched = is_our_flag(args[i].raw); + + if matched { + result.push(args[i].raw_idx); + + if Self::is_eq_match(args[i].raw, &possible_flags, style.case_sensitive, sep) { + i += 1; + while i < args.len() + && end.is_none_or(|e| args[i].raw_idx < e) + && !is_flag(args[i].raw) + { + result.push(args[i].raw_idx); + i += 1; + } + continue; + } + + i += 1; + while i < args.len() + && end.is_none_or(|e| args[i].raw_idx < e) + && !is_flag(args[i].raw) + { + result.push(args[i].raw_idx); + i += 1; + } + continue; + } + i += 1; + } + + result + } +} + +impl MultiArgMatcher { + #[inline(always)] + fn flag_match(raw: &str, flag_str: &str, case_sensitive: bool, sep: char) -> bool { + let eq = + |r: &str, f: &str| r.len() > f.len() && r.as_bytes().get(f.len()) == Some(&(sep as u8)); + + if case_sensitive { + raw == flag_str || (raw.starts_with(flag_str) && eq(raw, flag_str)) + } else { + raw.eq_ignore_ascii_case(flag_str) + || (raw.len() > flag_str.len() + && raw[..flag_str.len()].eq_ignore_ascii_case(flag_str) + && raw.as_bytes()[flag_str.len()] == sep as u8) + } + } + + #[inline(always)] + fn is_eq_match(raw: &str, flags: &[String], case_sensitive: bool, sep: char) -> bool { + flags.iter().any(|f| { + Self::flag_match(raw, f, case_sensitive, sep) + && raw.len() > f.len() + && raw.as_bytes().get(f.len()) == Some(&(sep as u8)) + }) + } +} diff --git a/arg_picker/src/parselib/pos_matcher.rs b/arg_picker/src/parselib/pos_matcher.rs new file mode 100644 index 0000000..279e01e --- /dev/null +++ b/arg_picker/src/parselib/pos_matcher.rs @@ -0,0 +1,71 @@ +use crate::{matcher_needed::*, parselib::seek_end_of_options}; + +/// `PositionalMatcher` matches positional arguments — values not associated +/// with any named flag. +/// +/// # Rules +/// +/// * Before `--`: skips any argument that starts with the style's long or short +/// prefix (those belong to named matchers). +/// * After `--`: takes **everything** — the `--` marker signals that all +/// remaining values are positional, even if they look like flags. +/// * Runs at the lowest priority (see [`PickerArgAttr::Positional`](crate::PickerArgAttr::Positional)). +pub struct PositionalMatcher; + +impl PositionalMatcher { + /// Check whether `raw` looks like a named flag (starts with a prefix). + #[inline(always)] + fn is_flag_like(raw: &str, style: &ParserStyle) -> bool { + raw.starts_with(style.long_prefix) || raw.starts_with(style.short_prefix) + } +} + +impl Matcher for PositionalMatcher { + fn on_match_one( + args: &[MaskedArg], + style: &ParserStyle, + _arg_info: &PickerArgInfo, + ) -> Option { + let end = seek_end_of_options(args, style); + + for arg in args { + if end.is_some_and(|e| arg.raw_idx == e) { + // Hit `--`: everything from here on is positional, + // including the first arg after `--`. + continue; + } + if end.is_some_and(|e| arg.raw_idx > e) { + // After `--`: accept everything. + return Some(arg.raw_idx); + } + // Before `--`: skip flag-like args. + if !Self::is_flag_like(arg.raw, style) { + return Some(arg.raw_idx); + } + } + + None + } + + fn on_match_all( + args: &[MaskedArg], + style: &ParserStyle, + _arg_info: &PickerArgInfo, + ) -> Vec { + let end = seek_end_of_options(args, style); + let mut after_end = false; + let mut result = Vec::new(); + + for arg in args { + if end.is_some_and(|e| arg.raw_idx == e) { + after_end = true; + continue; + } + if after_end || !Self::is_flag_like(arg.raw, style) { + result.push(arg.raw_idx); + } + } + + result + } +} diff --git a/arg_picker/src/parselib/single_matcher.rs b/arg_picker/src/parselib/single_matcher.rs new file mode 100644 index 0000000..25c4741 --- /dev/null +++ b/arg_picker/src/parselib/single_matcher.rs @@ -0,0 +1,59 @@ +use crate::TagPhaseContext; +use crate::parselib::{ArgMatcher, Matcher, ParserStyle, PositionalMatcher}; + +/// `SingleMatcher` is a composite matcher for single-value parameters. +/// +/// It delegates to [`PositionalMatcher`] for positional args and +/// [`ArgMatcher`] for named args, adding a guard: if a named flag +/// captures only itself with no inline value (eq mode), the result +/// is cleared so that [`Pickable::pick`](crate::Pickable::pick) receives `[]` → `NotFound`. +/// +/// This is the standard tag implementation for all `Single`-type +/// `Pickable` implementations (e.g., `String`, `i32`, `u64`). +pub struct SingleMatcher; + +impl SingleMatcher { + /// Match a single positional value or a named flag+value pair. + /// + /// For named args, only complete pairs (flag + value) are kept. + /// Flag occurrences without a following value or inline separator + /// are dropped so they remain available for other matchers. + #[inline(always)] + pub fn tag(ctx: TagPhaseContext) -> Vec { + if ctx.arg_info.positional { + PositionalMatcher::match_one(ctx.into()) + .map(|i| vec![i]) + .unwrap_or_default() + } else { + let args = ctx.args; + let positions = ArgMatcher::match_all(ctx.into()); + let sep = ParserStyle::global_style().value_separator; + + // Walk pairs: [flag, value, flag, value, ...] + // Drop any flag that has no following value and no inline separator. + let mut i = 0; + let mut result = Vec::with_capacity(positions.len()); + while i < positions.len() { + let flag_idx = positions[i]; + if let Some(raw) = args.get(flag_idx) + && raw.contains(sep) + { + // Eq mode: value is inline, keep just the flag. + result.push(flag_idx); + i += 1; + continue; + } + if i + 1 < positions.len() { + // Pair: flag + value. + result.push(flag_idx); + result.push(positions[i + 1]); + i += 2; + } else { + // Flag without value — drop it. + i += 1; + } + } + result + } + } +} diff --git a/arg_picker/src/parselib/style.rs b/arg_picker/src/parselib/style.rs new file mode 100644 index 0000000..36ba8f0 --- /dev/null +++ b/arg_picker/src/parselib/style.rs @@ -0,0 +1,258 @@ +use std::sync::OnceLock; +use std::sync::atomic::{AtomicBool, Ordering}; + +use crate::parselib::ParserStyleNamingCase::{Kebab, Pascal}; + +/// Defines the style of command-line argument parsing (prefixes, separators, etc.). +#[derive(Clone, Copy, PartialEq, Eq)] +pub struct ParserStyle<'a> { + /// End-of-options marker (e.g., `--`) + pub end_of_options: &'a str, + + /// Prefix for long options (e.g., `--` or `/`) + pub long_prefix: &'a str, + + /// Prefix for short options (e.g., `-` or `/`) + pub short_prefix: &'a str, + + /// Prefix for combined short flags (e.g., `-abc`) + pub combine_prefix: &'a str, + + /// Separator between name and value (e.g., `=` or `:`) + pub value_separator: char, + + /// Whether option names are case-sensitive + pub case_sensitive: bool, + + /// Whether combining short flags is allowed (e.g., `-abc` for `-a -b -c`) + pub allow_combine: bool, + + /// Naming case + pub naming_case: ParserStyleNamingCase, +} + +impl<'a> ParserStyle<'a> { + /// Formats a flag (short or long) into a full command-line option string. + /// + /// This method takes any type that can be converted into a `FlagStr` and produces + /// a complete option string by prepending the appropriate prefix. + /// + /// # Examples + /// + /// ``` + /// # use arg_picker::parselib::{ParserStyle, FlagStr, UNIX_STYLE}; + /// let style = &UNIX_STYLE; + /// + /// assert_eq!(style.flag_string('v'), "-v"); + /// assert_eq!(style.flag_string("verbose"), "--verbose"); + /// ``` + /// + /// # Parameters + /// + /// * `flag` - A value that can be converted to `FlagStr`, either a `char` for short flags + /// or a `&str` for long flags. + /// + /// # Returns + /// + /// A `String` with the prefix and the flag name combined. + #[must_use] + #[inline(always)] + pub fn flag_string(&self, flag: F) -> String + where + F: Into>, + { + match flag.into() { + FlagStr::Short(short) => format!("{}{}", self.short_prefix, short), + FlagStr::Long(long) => format!("{}{}", self.long_prefix, long), + } + } +} + +/// Represents a flag name for command-line argument parsing. +/// +/// This enum can hold either a short flag (a single character, e.g., `'v'` for `-v`) +/// or a long flag (a string, e.g., `"verbose"` for `--verbose`). +/// +/// # Examples +/// +/// ``` +/// use arg_picker::parselib::FlagStr; +/// +/// let short: FlagStr = 'v'.into(); +/// let long: FlagStr = "verbose".into(); +/// ``` +pub enum FlagStr<'a> { + /// A short flag represented by a single character. + Short(char), + /// A long flag represented by a string slice. + Long(&'a str), +} + +impl<'a> From for FlagStr<'a> { + /// Converts a single character into a `FlagStr::Short`. + fn from(c: char) -> Self { + FlagStr::Short(c) + } +} + +impl<'a> From<&'a str> for FlagStr<'a> { + /// Converts a string slice into a `FlagStr::Long`. + fn from(s: &'a str) -> Self { + FlagStr::Long(s) + } +} + +impl<'a> From<&'a String> for FlagStr<'a> { + /// Converts a reference to a `String` into a `FlagStr::Long`. + fn from(s: &'a String) -> Self { + FlagStr::Long(s.as_str()) + } +} + +/// Defines the naming convention for command-line option names. +/// +/// Each variant represents a different case format that can be applied +/// to option names (e.g., long option names) during parsing or generation. +/// +/// # Examples +/// +/// ``` +/// # use arg_picker::IntoPicker; +/// use arg_picker::parselib::ParserStyleNamingCase; +/// +/// let case = ParserStyleNamingCase::Kebab; +/// assert_eq!( +/// case.convert("brew_coffee".to_string()), +/// "brew-coffee".to_string() +/// ); +/// ``` +#[repr(u8)] +#[derive(Default, Clone, Copy, PartialEq, Eq)] +pub enum ParserStyleNamingCase { + /// snake_case format: words are separated by underscores, all lowercase. + /// + /// Example: `brew_coffee` + #[default] + Snake, + /// camelCase format: first word is lowercase, subsequent words are capitalized. + /// + /// Example: `brewCoffee` + Camel, + /// PascalCase format: every word starts with an uppercase letter. + /// + /// Example: `BrewCoffee` + Pascal, + /// kebab-case format: words are separated by hyphens, all lowercase. + /// + /// Example: `brew-coffee` + Kebab, + /// dot.case format: words are separated by dots, all lowercase. + /// + /// Example: `brew.coffee` + Dot, + /// Title Case format: words are separated by spaces, each word capitalized. + /// + /// Example: `Brew Coffee` + Title, + /// lower case format: words are separated by spaces, all lowercase. + /// + /// Example: `brew coffee` + Lower, + /// UPPER CASE format: words are separated by spaces, all uppercase. + /// + /// Example: `BREW COFFEE` + Upper, +} + +impl ParserStyleNamingCase { + /// Converts the input string `s` to the naming case represented by this variant. + /// + /// This method takes any type `S` that can be converted into a `String` and + /// produced from a `String`, applies the corresponding case transformation, + /// and returns the result. + /// + /// # Examples + /// + /// ``` + /// use arg_picker::parselib::ParserStyleNamingCase; + /// + /// let camel = ParserStyleNamingCase::Camel; + /// assert_eq!(camel.convert("brew_coffee".to_string()), "brewCoffee"); + /// + /// let kebab = ParserStyleNamingCase::Kebab; + /// assert_eq!(kebab.convert("BrewCoffee".to_string()), "brew-coffee"); + /// ``` + pub fn convert(&self, s: S) -> S + where + S: Into + From, + { + match self { + ParserStyleNamingCase::Camel => just_fmt::camel_case!(s.into()).into(), + ParserStyleNamingCase::Pascal => just_fmt::pascal_case!(s.into()).into(), + ParserStyleNamingCase::Kebab => just_fmt::kebab_case!(s.into()).into(), + ParserStyleNamingCase::Snake => just_fmt::snake_case!(s.into()).into(), + ParserStyleNamingCase::Dot => just_fmt::dot_case!(s.into()).into(), + ParserStyleNamingCase::Title => just_fmt::title_case!(s.into()).into(), + ParserStyleNamingCase::Lower => just_fmt::lower_case!(s.into()).into(), + ParserStyleNamingCase::Upper => just_fmt::upper_case!(s.into()).into(), + } + } +} + +/// Unix-like style (e.g., `--verbose`, `-v`, `--name=value`) +pub const UNIX_STYLE: ParserStyle = ParserStyle { + end_of_options: "--", + long_prefix: "--", + short_prefix: "-", + combine_prefix: "-", + value_separator: '=', + case_sensitive: true, + allow_combine: true, + naming_case: Kebab, +}; + +/// PowerShell style (e.g., `-Verbose`, `-Name:value`) +pub const POWERSHELL_STYLE: ParserStyle = ParserStyle { + end_of_options: "--", + long_prefix: "-", + short_prefix: "-", + combine_prefix: "-", + value_separator: ':', + case_sensitive: false, + allow_combine: false, + naming_case: Pascal, +}; + +/// Windows-style command-line (e.g., `/Verbose`, `/Name:value`) +pub const WINDOWS_STYLE: ParserStyle = ParserStyle { + end_of_options: "--", + long_prefix: "/", + short_prefix: "/", + combine_prefix: "/", + value_separator: ':', + case_sensitive: false, + allow_combine: false, + naming_case: Pascal, +}; + +static GLOBAL_STYLE: OnceLock> = OnceLock::new(); +static GLOBAL_STYLE_SET: AtomicBool = AtomicBool::new(false); + +impl<'a> ParserStyle<'a> { + /// Sets the global parser style. + /// + /// This function can only be called once. Subsequent calls will have no effect. + /// The style is stored as a static reference; the provided style must be a static + /// constant (e.g., `&'static ParserStyle`). Use the built-in constants like + /// `UNIX_STYLE`, `POWERSHELL_STYLE`, or `WINDOWS_STYLE`. + pub fn set_global_style(style: &'static ParserStyle<'static>) { + if !GLOBAL_STYLE_SET.load(Ordering::Acquire) && GLOBAL_STYLE.set(*style).is_ok() { + GLOBAL_STYLE_SET.store(true, Ordering::Release); + } + } + + /// Returns the global parser style, falling back to `UNIX_STYLE` if not set. + pub fn global_style() -> &'static ParserStyle<'static> { + GLOBAL_STYLE.get().unwrap_or(&UNIX_STYLE) + } +} diff --git a/arg_picker/src/parselib/utils.rs b/arg_picker/src/parselib/utils.rs new file mode 100644 index 0000000..47c5b55 --- /dev/null +++ b/arg_picker/src/parselib/utils.rs @@ -0,0 +1,276 @@ +use crate::{ + PickerArgInfo, + parselib::{MaskedArg, ParserStyle}, +}; + +/// Builds a list of possible flag strings for the given argument info +/// +/// This function generates formatted flag strings (e.g., `-h`, `--help`) from the short flag, +/// long flag, and any aliases defined in the argument info. The long flag and alias names +/// are converted according to the style's naming case convention before being formatted. +#[inline(always)] +pub fn build_possible_flags(style: &ParserStyle, arg_info: &PickerArgInfo) -> Vec { + let mut possible_flags = vec![]; + + if let Some(short) = arg_info.short { + possible_flags.push(style.flag_string(short)); + } + + if let Some(long) = arg_info.long { + let converted = style.naming_case.convert(long.to_string()); + possible_flags.push(style.flag_string(&converted)); + } + + if let Some(aliases) = &arg_info.alias { + for alias in aliases { + let converted = style.naming_case.convert(alias.to_string()); + possible_flags.push(style.flag_string(&converted)); + } + } + + possible_flags +} + +/// Extract a single value from the raw strings tagged by [`SingleMatcher`](crate::parselib::SingleMatcher). +/// +/// Returns `None` if no value is available (empty slice), +/// the inline value after the style separator if present (eq mode), +/// or the value directly (positional or flag-following). +/// +/// This is the standard `pick` helper for all `Single`-type +/// [`Pickable`](crate::Pickable) implementations. +#[must_use] +pub fn seek_single<'a>(raw_strs: &'a [&'a str]) -> Option<&'a str> { + match raw_strs.len() { + 0 => None, + 1 => { + let s = raw_strs[0]; + let sep = ParserStyle::global_style().value_separator; + if let Some(pos) = s.rfind(sep) { + Some(&s[pos + 1..]) + } else { + Some(s) + } + } + _ => Some(raw_strs[1]), + } +} + +/// Seeks the index of the end-of-options marker (`--`) in the argument list. +/// +/// This function searches for the standard end-of-options separator (`--`) +/// in the given argument list, respecting the parser's style settings +/// (e.g., case sensitivity). The end-of-options marker indicates that all +/// subsequent arguments should be treated as positional arguments, not flags. +#[must_use] +pub fn seek_end_of_options(args: &[MaskedArg], style: &ParserStyle) -> Option { + args.iter() + .find(|arg| { + if style.case_sensitive { + arg.raw == style.end_of_options + } else { + arg.raw.eq_ignore_ascii_case(style.end_of_options) + } + }) + .map(|arg| arg.raw_idx) +} + +/// Seeks arguments in `args` that are exactly equal to the given `string`. +/// +/// Returns the indices of matching arguments. +#[must_use] +#[inline(always)] +pub fn seek_eq(args: &[MaskedArg], string: &str, case_sensitive: bool) -> Vec { + args.iter() + .filter(|arg| { + if case_sensitive { + arg.raw == string + } else { + arg.raw.eq_ignore_ascii_case(string) + } + }) + .map(|arg| arg.raw_idx) + .collect() +} + +/// Seeks arguments in `args` that contain the given `string` as a substring. +/// +/// Returns the indices of matching arguments. +#[must_use] +#[inline(always)] +pub fn seek_contains(args: &[MaskedArg], string: &str, case_sensitive: bool) -> Vec { + args.iter() + .filter(|arg| { + if case_sensitive { + arg.raw.contains(string) + } else { + arg.raw.to_lowercase().contains(&string.to_lowercase()) + } + }) + .map(|arg| arg.raw_idx) + .collect() +} + +/// Seeks arguments in `args` that start with the given `string`. +/// +/// Returns the indices of matching arguments. +#[must_use] +#[inline(always)] +pub fn seek_start_with(args: &[MaskedArg], string: &str, case_sensitive: bool) -> Vec { + args.iter() + .filter(|arg| { + if case_sensitive { + arg.raw.starts_with(string) + } else { + arg.raw.to_lowercase().starts_with(&string.to_lowercase()) + } + }) + .map(|arg| arg.raw_idx) + .collect() +} + +/// Seeks arguments in `args` that end with the given `string`. +/// +/// Returns the indices of matching arguments. +#[must_use] +#[inline(always)] +pub fn seek_end_with(args: &[MaskedArg], string: &str, case_sensitive: bool) -> Vec { + args.iter() + .filter(|arg| { + if case_sensitive { + arg.raw.ends_with(string) + } else { + arg.raw.to_lowercase().ends_with(&string.to_lowercase()) + } + }) + .map(|arg| arg.raw_idx) + .collect() +} + +/// Seeks arguments in `args` that are exactly equal to any of the given `strings`. +/// +/// Returns the indices of matching arguments. +#[must_use] +#[inline(always)] +pub fn multi_seek_eq(args: &[MaskedArg], strings: &[&str], case_sensitive: bool) -> Vec { + args.iter() + .filter(|arg| { + if case_sensitive { + strings.contains(&arg.raw) + } else { + strings.iter().any(|s| arg.raw.eq_ignore_ascii_case(s)) + } + }) + .map(|arg| arg.raw_idx) + .collect() +} + +/// Seeks arguments in `args` that contain any of the given `strings` as a substring. +/// +/// Returns the indices of matching arguments. +#[must_use] +#[inline(always)] +pub fn multi_seek_contains( + args: &[MaskedArg], + strings: &[&str], + case_sensitive: bool, +) -> Vec { + args.iter() + .filter(|arg| { + if case_sensitive { + strings.iter().any(|s| arg.raw.contains(s)) + } else { + let lower_raw = arg.raw.to_lowercase(); + strings + .iter() + .any(|s| lower_raw.contains(&s.to_lowercase())) + } + }) + .map(|arg| arg.raw_idx) + .collect() +} + +/// Seeks arguments in `args` that start with any of the given `strings`. +/// +/// Returns the indices of matching arguments. +#[must_use] +#[inline(always)] +pub fn multi_seek_start_with( + args: &[MaskedArg], + strings: &[&str], + case_sensitive: bool, +) -> Vec { + args.iter() + .filter(|arg| { + if case_sensitive { + strings.iter().any(|s| arg.raw.starts_with(s)) + } else { + let lower_raw = arg.raw.to_lowercase(); + strings + .iter() + .any(|s| lower_raw.starts_with(&s.to_lowercase())) + } + }) + .map(|arg| arg.raw_idx) + .collect() +} + +/// Seeks arguments in `args` that end with any of the given `strings`. +/// +/// Returns the indices of matching arguments. +#[must_use] +#[inline(always)] +pub fn multi_seek_end_with( + args: &[MaskedArg], + strings: &[&str], + case_sensitive: bool, +) -> Vec { + args.iter() + .filter(|arg| { + if case_sensitive { + strings.iter().any(|s| arg.raw.ends_with(s)) + } else { + let lower_raw = arg.raw.to_lowercase(); + strings + .iter() + .any(|s| lower_raw.ends_with(&s.to_lowercase())) + } + }) + .map(|arg| arg.raw_idx) + .collect() +} + +/// Converts a `&Vec` into a `Vec<&str>` by borrowing each string's slice. +/// +/// This is useful for converting owned `String` vectors into borrowed `&str` slices +/// for functions that take `&[&str]` or similar parameters. +#[must_use] +#[inline(always)] +#[doc(hidden)] +pub fn vec_string_to_vec_str(input: &[String]) -> Vec<&str> { + input.iter().map(|s| s.as_str()).collect() +} + +/// Converts a `&Vec` into a `Vec<&str>` by borrowing each string's slice. +/// +/// This is useful for converting owned `String` vectors into borrowed `&str` slices +/// for functions that take `&[&str]` or similar parameters. +#[macro_export] +#[doc(hidden)] +macro_rules! vec_string_slice { + ($v:expr) => { + $v.iter() + .map(|s| s.as_str()) + .collect::>() + .as_slice() + }; +} + +/// Gets the first element from a vector of seek results, if any. +/// +/// Returns `Some(index)` if the vector is non-empty, otherwise `None`. +#[must_use] +#[inline(always)] +pub fn get_seeked_first(seeked: Vec) -> Option { + seeked.into_iter().next() +} diff --git a/arg_picker/src/pickable.rs b/arg_picker/src/pickable.rs new file mode 100644 index 0000000..758ae9a --- /dev/null +++ b/arg_picker/src/pickable.rs @@ -0,0 +1,91 @@ +use crate::{PickerArg, PickerArgAttr, PickerArgInfo, PickerArgResult, PickerArgs}; + +mod single_pickable; +pub use single_pickable::*; + +mod multi_pickable; +pub use multi_pickable::*; + +/// `Pickable` trait defines how to parse a type instance from command-line arguments. +/// +/// This trait is the core abstraction of the `Picker` argument parsing system, dividing the +/// parsing process into two phases: +/// +/// 1. **Tag phase ([`Pickable::tag`])**: Determines which argument positions the `Pickable` needs to handle. +/// 2. **Pick phase ([`Pickable::pick`])**: Converts the raw strings at the tagged positions into the actual type. +/// +/// Types implementing this trait must also implement [`Default`], so that a default value +/// can be used as a fallback when parsing fails. +/// +/// # Type Parameters +/// +/// * `'a` - Lifetime parameter, used to associate references in [`PickerArg`]. +pub trait Pickable<'a> +where + Self: Sized, +{ + /// Returns the parse-order attribute of this flag. + /// + /// This attribute is used to inform the parser about the parse order + /// between different `Pickable` types. + /// See [`PickerArgAttr`] for specific ordering definitions. + /// + /// # Parameters + /// + /// * `flag` - The current flag instance, which contains a reference to `Self`. + /// + /// # Returns + /// + /// Returns a [`PickerArgAttr`] describing the parse-order attribute of this flag. + fn get_attr(flag: &'a PickerArg<'a, Self>) -> PickerArgAttr; + + /// Tag phase: Determines which argument positions the `Pickable` needs to handle. + /// + /// This function receives a [`TagPhaseContext`] containing argument context information. + /// During this phase, the parser invokes each `Pickable` and collects the position indices + /// they return, in order to determine which arguments to parse later. + /// + /// # Parameters + /// + /// * `ctx` - The tag phase context, containing argument information, all parameters of the + /// current Picker, and an availability mask. + /// + /// # Returns + /// + /// Returns a `Vec` representing the indices of the arguments in the argument list + /// that this `Pickable` needs to handle. + fn tag(ctx: TagPhaseContext) -> Vec; + + /// Pick phase: Converts the raw string arguments tagged during the `tag` phase into + /// the actual expected type. + /// + /// This function receives a slice of the raw strings that were tagged in the `tag` step + /// and converts them into an instance of `Self`. + /// + /// # Parameters + /// + /// * `raw_strs` - A slice of strings containing the raw argument values to parse. + /// + /// # Returns + /// + /// Returns [`PickerArgResult`], i.e., the `Self` instance on success, or an appropriate + /// error message on failure. + fn pick(raw_strs: &[&str]) -> PickerArgResult; +} + +/// Tag phase context, providing the necessary argument and state information for +/// [`Pickable::tag`]. +pub struct TagPhaseContext<'a> { + /// Argument information describing the structure and metadata of the argument + /// to be parsed. + pub arg_info: &'a PickerArgInfo<'a>, + + /// A read-only list of all arguments in the current [`Picker`](crate::Picker). + pub args: &'a PickerArgs<'a>, + + /// Mask indicating which argument positions have already been claimed. + /// + /// For example, if the mask is `[0, 0, 1, 0]`, then the argument at index `2` + /// has already been tagged by another `Pickable`. + pub mask: &'a [u8], +} diff --git a/arg_picker/src/pickable/multi_pickable.rs b/arg_picker/src/pickable/multi_pickable.rs new file mode 100644 index 0000000..84a8068 --- /dev/null +++ b/arg_picker/src/pickable/multi_pickable.rs @@ -0,0 +1,76 @@ +use crate::{ + Pickable, PickerArg, PickerArgAttr, PickerArgResult, SinglePickable, TagPhaseContext, + matcher_needed::Matcher, + parselib::{MultiArgMatcher, ParserStyle}, +}; + +/// Boundary check for multi-value positional parameters. +pub trait BoundaryCheck { + fn check_boundary(raw: &str) -> bool; +} + +/// Trait for multi-value parameters. +pub trait MultiPickableWithBoundary: Sized { + type Checker: BoundaryCheck; + fn pick_multi(raw: Vec) -> PickerArgResult; +} + +/// Marker: unit type that always accepts — no boundary. +pub struct NoBoundary; + +impl BoundaryCheck for NoBoundary { + #[inline(always)] + fn check_boundary(_raw: &str) -> bool { + false + } +} + +/// `Vec` is greedy — it takes everything with `NoBoundary`. +impl MultiPickableWithBoundary for Vec { + type Checker = NoBoundary; + + fn pick_multi(raw: Vec) -> PickerArgResult { + let mut result = Vec::with_capacity(raw.len()); + for s in &raw { + match T::pick_single(Some(s)) { + PickerArgResult::Parsed(v) => result.push(v), + PickerArgResult::NotFound => return PickerArgResult::NotFound, + PickerArgResult::Unparsed => {} + } + } + PickerArgResult::Parsed(result) + } +} + +/// If the first raw string looks like a named flag (starts with the +/// style's long or short prefix), strip it — it's the flag, not a value. +fn strip_flag<'a>(raw_strs: &'a [&'a str]) -> &'a [&'a str] { + if let Some(first) = raw_strs.first() { + let style = ParserStyle::global_style(); + if first.starts_with(style.long_prefix) || first.starts_with(style.short_prefix) { + return &raw_strs[1..]; + } + } + raw_strs +} + +// Pickable impl for Vec + +impl<'a, T> Pickable<'a> for Vec +where + T: SinglePickable, +{ + fn get_attr(flag: &'a PickerArg<'a, Self>) -> PickerArgAttr { + PickerArgAttr::positional_or_multi(flag) + } + + fn tag(ctx: TagPhaseContext) -> Vec { + MultiArgMatcher::match_all(ctx.into()) + } + + fn pick(raw_strs: &[&str]) -> PickerArgResult { + let strs = strip_flag(raw_strs); + let owned: Vec = strs.iter().map(|&s| s.to_string()).collect(); + as MultiPickableWithBoundary>::pick_multi(owned) + } +} diff --git a/arg_picker/src/pickable/single_pickable.rs b/arg_picker/src/pickable/single_pickable.rs new file mode 100644 index 0000000..8a5b3e6 --- /dev/null +++ b/arg_picker/src/pickable/single_pickable.rs @@ -0,0 +1,69 @@ +use crate::{Pickable, PickerArg, PickerArgAttr, PickerArgResult, TagPhaseContext}; + +/// `SinglePickable` trait defines how to parse a type from a single command-line argument. +/// +/// This trait provides a simplified interface for types that consume exactly one argument value. +/// It is automatically implemented by the blanket `impl` of [`Pickable`], so types implementing +/// `SinglePickable` will work with the full `Pickable` argument parsing system. +/// +/// Additionally, `Option` where `S: SinglePickable` also implements [`Pickable`], allowing +/// optional arguments to be parsed naturally. +/// +/// # Type Parameters +/// +/// * `Self` - The type to be parsed from a single argument string. +pub trait SinglePickable +where + Self: Sized, +{ + /// Parse a single optional string value into an instance of `Self`. + /// + /// # Parameters + /// + /// * `str` - An `Option<&str>` representing the raw argument value. If `None`, + /// it indicates that no argument value was provided (e.g., for flag-like arguments). + /// + /// # Returns + /// + /// Returns [`PickerArgResult`], i.e., the parsed `Self` instance on success, + /// or an appropriate error message on failure. + fn pick_single(str: Option<&str>) -> PickerArgResult; +} + +impl<'a, S> Pickable<'a> for S +where + S: SinglePickable, +{ + fn get_attr(flag: &'a PickerArg<'a, Self>) -> PickerArgAttr { + PickerArgAttr::positional_or_single(flag) + } + + fn tag(ctx: TagPhaseContext) -> Vec { + crate::parselib::SingleMatcher::tag(ctx) + } + + fn pick(raw_strs: &[&str]) -> PickerArgResult { + Self::pick_single(crate::parselib::seek_single(raw_strs)) + } +} + +impl<'a, S> Pickable<'a> for Option +where + S: SinglePickable, +{ + fn get_attr(flag: &'a PickerArg<'a, Self>) -> PickerArgAttr { + PickerArgAttr::positional_or_single(flag) + } + + fn tag(ctx: TagPhaseContext) -> Vec { + crate::parselib::SingleMatcher::tag(ctx) + } + + fn pick(raw_strs: &[&str]) -> PickerArgResult { + match S::pick(raw_strs) { + PickerArgResult::Unparsed => PickerArgResult::Unparsed, + PickerArgResult::Parsed(r) => PickerArgResult::Parsed(Some(r)), + PickerArgResult::NotFound => PickerArgResult::Parsed(None), + } + } +} diff --git a/arg_picker/src/picker.rs b/arg_picker/src/picker.rs new file mode 100644 index 0000000..ecab648 --- /dev/null +++ b/arg_picker/src/picker.rs @@ -0,0 +1,432 @@ +use std::{marker::PhantomData, ops::Index}; + +mod parse; + +mod patterns; +pub use patterns::*; + +mod result; +pub use result::*; + +use crate::{Pickable, PickerArg, PickerArgResult}; + +#[doc = include_str!("../README.md")] +pub struct Picker<'a, Route = ()> { + pub(crate) route_phantom: PhantomData, + + /// Internal arguments of Picker + pub(crate) args: PickerArgs<'a>, +} + +impl<'a> Picker<'a> { + /// Creates a new `Picker` from the command-line arguments (excluding the program name). + /// + /// This is equivalent to calling `std::env::args().skip(1)`, which + /// collects all arguments passed to the program except the first one + /// (the executable path). + pub fn from_args() -> Picker<'a, ()> { + Self::from_args_skip(1) + } + + /// Creates a new `Picker` from the command-line arguments, skipping the + /// first `skip` entries. + /// + /// This method is useful when you want more control over which arguments + /// are included. For example, pass `skip = 2` to skip both the program + /// name and the first argument. + pub fn from_args_skip(skip: usize) -> Picker<'a, ()> { + let args = std::env::args().skip(skip).collect::>(); + Picker { + route_phantom: PhantomData, + args: PickerArgs::Owned(args), + } + } + + /// Changes the route (phantom type parameter) of the `Picker`. + /// + /// This method allows converting a `Picker` from one route type to another, + /// while preserving the same underlying arguments. The route type is typically + /// used to distinguish different parsing contexts or to carry compile-time + /// state information through the picking chain. + pub fn with_route(self) -> Picker<'a, NewRoute> + where + Self: Sized, + { + Picker { + route_phantom: PhantomData, + args: self.args, + } + } +} + +/// 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 PickerArgs<'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> Default for PickerArgs<'a> { + fn default() -> Self { + Self::Vec(vec![]) + } +} + +impl<'a> PickerArgs<'a> { + /// Returns the number of arguments. + pub fn len(&self) -> usize { + match self { + PickerArgs::Slice(items) => items.len(), + PickerArgs::Vec(items) => items.len(), + PickerArgs::Owned(items) => items.len(), + } + } + + /// Returns `true` if there are no arguments. + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + + /// Returns an iterator over the arguments, yielding `&str` values. + pub fn iter(&'a self) -> PickerIter<'a> { + match self { + PickerArgs::Slice(items) => PickerIter::Slice(items.iter()), + PickerArgs::Vec(items) => PickerIter::Vec(items.iter()), + PickerArgs::Owned(items) => PickerIter::Owned(items.iter()), + } + } + + /// Returns a reference to the argument at `index`, if it exists. + pub fn get(&self, index: usize) -> Option<&str> { + match self { + PickerArgs::Slice(items) => items.get(index).copied(), + PickerArgs::Vec(items) => items.get(index).copied(), + PickerArgs::Owned(items) => items.get(index).map(|s| s.as_str()), + } + } +} + +impl<'a> Index for PickerArgs<'a> { + type Output = str; + + fn index(&self, index: usize) -> &Self::Output { + match self { + PickerArgs::Slice(items) => items[index], + PickerArgs::Vec(items) => items[index], + PickerArgs::Owned(items) => &items[index], + } + } +} + +impl<'a> IntoIterator for &'a PickerArgs<'a> { + type Item = &'a str; + type IntoIter = PickerIter<'a>; + + fn into_iter(self) -> Self::IntoIter { + match self { + PickerArgs::Slice(items) => PickerIter::Slice(items.iter()), + PickerArgs::Vec(items) => PickerIter::Vec(items.iter()), + PickerArgs::Owned(items) => PickerIter::Owned(items.iter()), + } + } +} + +impl<'a, Route> From<&'a [&'a str]> for Picker<'a, Route> { + fn from(value: &'a [&'a str]) -> Self { + Picker { + route_phantom: PhantomData, + args: PickerArgs::Slice(value), + } + } +} + +impl<'a, Route> From> for Picker<'a, Route> { + fn from(value: Vec<&'a str>) -> Self { + Picker { + route_phantom: PhantomData, + args: PickerArgs::Vec(value), + } + } +} + +impl<'a, Route> From> for Picker<'a, Route> { + fn from(value: Vec) -> Self { + Picker { + route_phantom: PhantomData, + args: PickerArgs::Owned(value), + } + } +} + +impl<'a, Route> Picker<'a, Route> { + /// Returns a reference to the internal `PickerArgs`. + pub fn args(&self) -> &PickerArgs<'a> { + &self.args + } + + /// Returns a mutable reference to the internal `PickerArgs`. + pub fn args_mut(&mut self) -> &mut PickerArgs<'a> { + &mut self.args + } + + /// Consumes `self` and returns the internal `PickerArgs`. + pub fn into_args(self) -> PickerArgs<'a> { + self.args + } + + /// Returns the number of arguments. + pub fn len(&self) -> usize { + self.args.len() + } + + /// Returns `true` if there are no arguments. + pub fn is_empty(&self) -> bool { + self.args.is_empty() + } + + /// Returns an iterator over the arguments, yielding `&str` values. + pub fn iter(&'a self) -> PickerIter<'a> { + self.args.iter() + } +} + +impl<'a, Route> Index for Picker<'a, Route> { + type Output = str; + + fn index(&self, index: usize) -> &Self::Output { + &self.args[index] + } +} + +impl<'a, Route> Index for &Picker<'a, Route> { + type Output = str; + + fn index(&self, index: usize) -> &Self::Output { + &self.args[index] + } +} + +impl<'a, Route> IntoIterator for &'a Picker<'a, Route> { + type Item = &'a str; + type IntoIter = PickerIter<'a>; + + fn into_iter(self) -> Self::IntoIter { + self.args.iter() + } +} + +/// Iterator for `Picker` (and `PickerArgs`), yielding `&'a str` values. +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 = &'a str; + + fn next(&mut self) -> Option { + match self { + PickerIter::Slice(iter) => iter.next().copied(), + PickerIter::Vec(iter) => iter.next().copied(), + PickerIter::Owned(iter) => iter.next().map(|s| s.as_str()), + } + } + + 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> {} + +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(self, arg: impl Into<&'a PickerArg<'a, N>>) -> PickerPattern1<'a, N, Route> + where + N: Pickable<'a>, + { + Self::build_pattern1(self.args, arg.into(), None::) + } + + /// Creates a `PickerPattern1` from the given arg. + /// If parsing fails, attempts the fallback arg. + pub fn pick_or( + self, + arg: impl Into<&'a PickerArg<'a, N>>, + or_arg: F, + ) -> PickerPattern1<'a, N, Route> + where + N: Pickable<'a>, + F: FnMut() -> N + 'static, + { + self.pick(arg).or(or_arg) + } + + /// Creates a `PickerPattern1` from the given arg. + /// If parsing fails, uses the provided default value. + pub fn pick_or_default( + self, + arg: impl Into<&'a PickerArg<'a, N>>, + ) -> PickerPattern1<'a, N, Route> + where + N: Pickable<'a> + Default, + { + self.pick(arg).or_default() + } + + /// Creates a `PickerPattern1` from the given arg. + /// If parsing fails, switches to the given error route. + pub fn pick_or_route( + self, + arg: impl Into<&'a PickerArg<'a, N>>, + error_route: F, + ) -> PickerPattern1<'a, N, Route> + where + N: Pickable<'a>, + F: FnMut() -> Route + 'static, + { + self.pick(arg).or_route(error_route) + } +} + +/// Trait for converting types into a `Picker` +/// +/// Implemented for: +/// - `&[&str]` (borrowed slice) +/// - `&[String]` (borrowed slice of owned strings) +/// - `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 arg_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, ()>; + + /// Creates a `PickerPattern1` from the given arg for the `pick` method. + /// + /// This method converts the value into a `Picker` and starts a parameter + /// picking chain with one arg. The result is initially `Unparsed`. + fn pick(self, arg: impl Into<&'a PickerArg<'a, N>>) -> PickerPattern1<'a, N, ()> + where + Self: Sized, + N: Pickable<'a> + Default + Sized, + { + Picker::build_pattern1(self.to_picker().args, arg.into(), None::<()>) + } + + /// Converts the value into a `Picker` with a specified route type. + /// + /// This method allows changing the route (phantom type parameter) of the picker. + /// The route type is typically used to distinguish different parsing contexts or + /// to carry compile-time state information through the picking chain. + fn with_route(self) -> Picker<'a, NewRoute> + where + Self: Sized, + { + Picker { + route_phantom: PhantomData, + args: self.to_picker().args, + } + } +} + +impl<'a> IntoPicker<'a> for &'a [&'a str] { + fn to_picker(self) -> Picker<'a, ()> { + Picker { + route_phantom: PhantomData, + args: PickerArgs::Slice(self), + } + } +} + +impl<'a> IntoPicker<'a> for &'a [String] { + fn to_picker(self) -> Picker<'a, ()> { + let vec: Vec<&str> = self.iter().map(|s| s.as_str()).collect(); + Picker { + route_phantom: PhantomData, + args: PickerArgs::Vec(vec), + } + } +} + +impl<'a> IntoPicker<'a> for Vec<&'a str> { + fn to_picker(self) -> Picker<'a, ()> { + Picker { + route_phantom: PhantomData, + args: PickerArgs::Vec(self), + } + } +} + +impl<'a> IntoPicker<'a> for &'a Vec { + fn to_picker(self) -> Picker<'a, ()> { + let slice: Vec<&str> = self.iter().map(|s| s.as_str()).collect(); + Picker { + route_phantom: PhantomData, + args: PickerArgs::Vec(slice), + } + } +} + +impl<'a> IntoPicker<'a> for Vec { + fn to_picker(self) -> Picker<'a, ()> { + Picker { + route_phantom: PhantomData, + args: PickerArgs::Owned(self), + } + } +} + +// Private helper: shared construction logic for `PickerPattern1`. +// Both `Picker::pick` and `IntoPicker::pick` delegate to this. +impl<'a, Route> Picker<'a, Route> { + pub(crate) fn build_pattern1( + args: PickerArgs<'a>, + arg: &'a PickerArg<'a, N>, + error_route: Option, + ) -> PickerPattern1<'a, N, Route> + where + N: Pickable<'a>, + { + PickerPattern1 { + args, + error_route, + arg_1: arg, + result_1: PickerArgResult::Unparsed, + route_1: None, + default_1: None, + post_1: None, + } + } +} diff --git a/arg_picker/src/picker/parse.rs b/arg_picker/src/picker/parse.rs new file mode 100644 index 0000000..366028b --- /dev/null +++ b/arg_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 arg_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,+)) { + 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, &[u8]) -> Vec>, + + // Pick Func + Box)>, + + // 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().into()); + } + } + 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 = 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/arg_picker/src/picker/patterns.rs b/arg_picker/src/picker/patterns.rs new file mode 100644 index 0000000..5806605 --- /dev/null +++ b/arg_picker/src/picker/patterns.rs @@ -0,0 +1,205 @@ +use arg_picker_macros::internal_repeat; + +use crate::{Pickable, 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, + ( + pub(crate) arg_$: &'a PickerArg<'a, T$>, + pub(crate) result_$: PickerArgResult, + pub(crate) default_$: Option T$>>, + pub(crate) route_$: Option Route>>, + pub(crate) post_$: Option 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 + /// + #[allow(clippy::type_complexity)] + pub fn or(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 + /// + #[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 + /// + pub fn or_route(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(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 + /// + #[allow(clippy::type_complexity)] + pub fn post(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(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_$, + +) + } + } + + /// Picks a new arg with a default value provider in a single call. + /// + /// This is a shorthand for calling `.pick(arg).or(func)`. + /// + /// # Example + /// + #[allow(clippy::type_complexity)] + pub fn pick_or(self, arg: impl Into<&'a PickerArg<'a, N>>, func: F) -> PickerPattern$+<'a, (T$,+), N, Route> + where + N: Pickable<'a>, + F: FnMut() -> N + 'static, + F: 'static, + { + self.pick(arg).or(func) + } + + /// Picks a new arg with a default value in a single call. + /// + /// This is a shorthand for calling `.pick(arg).or_default()`. + /// + /// # Example + /// + #[allow(clippy::type_complexity)] + pub fn pick_or_default(self, arg: impl Into<&'a PickerArg<'a, N>>) -> PickerPattern$+<'a, (T$,+), N, Route> + where + N: Pickable<'a> + Default, + { + self.pick(arg).or_default() + } + + /// Picks a new arg with a route in a single call. + /// + /// This is a shorthand for calling `.pick(arg).or_route(func)`. + /// + /// # Example + /// + #[allow(clippy::type_complexity)] + pub fn pick_or_route(self, arg: impl Into<&'a PickerArg<'a, N>>, func: F) -> PickerPattern$+<'a, (T$,+), N, Route> + where + N: Pickable<'a>, + F: FnMut() -> Route + 'static, + F: 'static, + { + self.pick(arg).or_route(func) + } + } +}); diff --git a/arg_picker/src/picker/result.rs b/arg_picker/src/picker/result.rs new file mode 100644 index 0000000..83ca2cd --- /dev/null +++ b/arg_picker/src/picker/result.rs @@ -0,0 +1,56 @@ +#![allow(clippy::type_complexity)] // Aha, Type Gymnastics! + +use arg_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, + + ( + #[doc = concat!("The optional value for the ", $, "th type parameter.")] + pub v$: Option, + +) + } +}); + +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,+)) { + ((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()) + } + } +}); diff --git a/arg_picker/src/value.rs b/arg_picker/src/value.rs new file mode 100644 index 0000000..995aa00 --- /dev/null +++ b/arg_picker/src/value.rs @@ -0,0 +1,5 @@ +mod flag; +pub use flag::*; + +mod vec_until; +pub use vec_until::*; diff --git a/arg_picker/src/value/flag.rs b/arg_picker/src/value/flag.rs new file mode 100644 index 0000000..c0673bd --- /dev/null +++ b/arg_picker/src/value/flag.rs @@ -0,0 +1,145 @@ +use std::{ + fmt::{Debug, Display}, + ops::{Deref, Not}, +}; + +/// Parsed result of a boolean-style command-line flag. +/// +/// `Flag` is a **value type** that can be declared in [`PickerArg`]. +/// When the user passes `--verbose` on the command line, the parsed result is `Flag::Active`; +/// when the flag is absent, the result is `Flag::Inactive`. +/// +/// # Why not just `bool`? +/// +/// Unlike a raw `bool`, `Flag` carries **explicit semantics** about whether +/// the flag was actually provided by the user (`Active`) or simply omitted +/// (`Inactive`). This distinction matters when you want to distinguish +/// "the user intentionally omitted the flag" from "the flag was processed but +/// resolved to false" — the `Pickable` implementation for `Flag` always +/// returns `Parsed(Flag::Inactive)` when no matching argument is found, +/// rather than `NotFound`, making it always succeed with a meaningful default. +/// +/// # Conversions +/// +/// `Flag` interoperates seamlessly with `bool`: `Flag::Active` is `true`, +/// `Flag::Inactive` is `false`. The [`Deref`] impl allows using a `Flag` +/// directly in boolean contexts: +/// +/// ``` +/// # use arg_picker::value::Flag; +/// let flag = Flag::Active; +/// if *flag { /* runs */ } +/// ``` +/// +/// [`PickerArg`]: crate::PickerArg +#[derive(Default, Clone, Copy, PartialEq, Eq)] +pub enum Flag { + /// The flag was **not** present on the command line. + /// + /// This is the default state, equivalent to `false`. + #[default] + Inactive, + + /// The flag **was** present on the command line. + /// + /// Equivalent to `true`. + Active, +} + +impl Debug for Flag { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Inactive => write!(f, "inactive"), + Self::Active => write!(f, "active"), + } + } +} + +impl Display for Flag { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Inactive => write!(f, "inactive"), + Self::Active => write!(f, "active"), + } + } +} + +impl Flag { + /// Converts this `Flag` into a `bool`. + /// + /// Returns `true` if the flag is [`Active`], `false` if [`Inactive`]. + /// + /// # Examples + /// + /// ``` + /// # use arg_picker::value::Flag; + /// assert!(Flag::Active.bool()); + /// assert!(!Flag::Inactive.bool()); + /// ``` + /// + /// [`Active`]: Flag::Active + /// [`Inactive`]: Flag::Inactive + #[must_use] + #[inline(always)] + pub fn bool(&self) -> bool { + *self == Flag::Active + } +} + +impl PartialEq for Flag { + fn eq(&self, other: &bool) -> bool { + self.bool() == *other + } +} + +/// Compares `bool` with `Flag` using `==`. +impl PartialEq for bool { + fn eq(&self, other: &Flag) -> bool { + *self == other.bool() + } +} + +impl From for Flag { + fn from(value: bool) -> Self { + if value { Flag::Active } else { Flag::Inactive } + } +} + +impl From for bool { + fn from(val: Flag) -> Self { + val == Flag::Active + } +} + +/// Allows `Flag` to be used in boolean contexts via `*flag`. +/// +/// # Examples +/// +/// ``` +/// # use arg_picker::value::Flag; +/// let flag = Flag::Active; +/// if *flag { +/// println!("flag is set"); +/// } +/// ``` +impl Deref for Flag { + type Target = bool; + + fn deref(&self) -> &bool { + match self { + Flag::Active => &true, + Flag::Inactive => &false, + } + } +} + +impl Not for Flag { + type Output = Flag; + + fn not(self) -> Flag { + match self { + Flag::Active => Flag::Inactive, + Flag::Inactive => Flag::Active, + } + } +} diff --git a/arg_picker/src/value/vec_until.rs b/arg_picker/src/value/vec_until.rs new file mode 100644 index 0000000..1b79641 --- /dev/null +++ b/arg_picker/src/value/vec_until.rs @@ -0,0 +1,134 @@ +use std::marker::PhantomData; +use std::ops::{Deref, DerefMut}; + +use crate::{ + BoundaryCheck, MultiPickableWithBoundary, Pickable, PickerArg, PickerArgAttr, PickerArgResult, + SinglePickable, TagPhaseContext, + matcher_needed::Matcher, + parselib::{MultiArgMatcher, ParserStyle}, +}; + +/// A `Vec`-like container that stops collecting when [`BoundaryCheck`] +/// returns `true`. +/// +/// This type exists to signal "I know what I'm doing with boundaries" +/// at the type level (as opposed to `Vec` which greedily takes +/// everything). +#[derive(Debug, Clone, PartialEq, Eq, Default)] +pub struct VecUntil { + pub(crate) inner: Vec, + _marker: PhantomData, +} + +impl VecUntil { + pub fn into_inner(self) -> Vec { + self.inner + } +} + +impl From> for VecUntil { + fn from(v: Vec) -> Self { + VecUntil { + inner: v, + _marker: PhantomData, + } + } +} + +impl From> for Vec { + fn from(v: VecUntil) -> Self { + v.inner + } +} + +impl Deref for VecUntil { + type Target = Vec; + fn deref(&self) -> &Vec { + &self.inner + } +} + +impl DerefMut for VecUntil { + fn deref_mut(&mut self) -> &mut Vec { + &mut self.inner + } +} + +// MultiPickableWithBoundary impl + +impl MultiPickableWithBoundary for VecUntil +where + T: SinglePickable + BoundaryCheck, +{ + type Checker = T; + + fn pick_multi(raw: Vec) -> PickerArgResult { + let mut inner = Vec::with_capacity(raw.len()); + for s in &raw { + match T::pick_single(Some(s)) { + PickerArgResult::Parsed(v) => inner.push(v), + PickerArgResult::NotFound => return PickerArgResult::NotFound, + PickerArgResult::Unparsed => {} + } + } + PickerArgResult::Parsed(VecUntil { + inner, + _marker: PhantomData, + }) + } +} + +// Pickable impl + +impl<'a, T> Pickable<'a> for VecUntil +where + T: SinglePickable + BoundaryCheck, +{ + fn get_attr(flag: &'a PickerArg<'a, Self>) -> PickerArgAttr { + PickerArgAttr::positional_or_multi(flag) + } + + fn tag(ctx: TagPhaseContext) -> Vec { + let args = ctx.args; + let is_positional = ctx.arg_info.positional; + let positions = MultiArgMatcher::match_all(ctx.into()); + if positions.is_empty() { + return positions; + } + + let start = if is_positional { 0 } else { 1 }; + if start >= positions.len() { + return positions; + } + + let mut cut = start; + for &idx in &positions[start..] { + if let Some(raw) = args.get(idx) + && T::check_boundary(raw) + { + break; + } + cut += 1; + } + + positions[..cut].to_vec() + } + + fn pick(raw_strs: &[&str]) -> PickerArgResult { + let strs = strip_flag(raw_strs); + let owned: Vec = strs.iter().map(|&s| s.to_string()).collect(); + as MultiPickableWithBoundary>::pick_multi(owned) + } +} + +/// If the first raw string looks like a named flag (starts with the +/// style's long or short prefix), strip it — it's the flag, not a value. +fn strip_flag<'a>(raw_strs: &'a [&'a str]) -> &'a [&'a str] { + if let Some(first) = raw_strs.first() { + let style = ParserStyle::global_style(); + if first.starts_with(style.long_prefix) || first.starts_with(style.short_prefix) { + return &raw_strs[1..]; + } + } + raw_strs +} diff --git a/arg_picker/test/Cargo.lock b/arg_picker/test/Cargo.lock new file mode 100644 index 0000000..5d44838 --- /dev/null +++ b/arg_picker/test/Cargo.lock @@ -0,0 +1,68 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "arg-picker" +version = "0.1.0" +dependencies = [ + "arg-picker-macros", + "just_fmt", +] + +[[package]] +name = "arg-picker-macros" +version = "0.1.0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "arg-picker-test" +version = "0.1.0" +dependencies = [ + "arg-picker", +] + +[[package]] +name = "just_fmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6170dccbc3ea15dfb7f2da964097f814aba1dd8f746d4ffc56f33245c38e6d96" + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" diff --git a/arg_picker/test/Cargo.toml b/arg_picker/test/Cargo.toml new file mode 100644 index 0000000..2c366c7 --- /dev/null +++ b/arg_picker/test/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "arg-picker-test" +version = "0.1.0" +edition = "2024" + +[workspace] + +[dependencies] +arg-picker = { path = "../" } diff --git a/arg_picker/test/src/lib.rs b/arg_picker/test/src/lib.rs new file mode 100644 index 0000000..0cfa575 --- /dev/null +++ b/arg_picker/test/src/lib.rs @@ -0,0 +1,23 @@ +// Using `assert_eq!(x, true)` is clearer than `assert!(x)` for expressing expected values +// +// BECAUSE `assert!` only checks if the boolean value is true, +// while `assert_eq!` explicitly shows the expected value +#![allow(clippy::bool_assert_comparison)] + +#[cfg(test)] +mod test; + +use arg_picker::parselib::MaskedArg; + +/// Create a single `MaskedArg` from a raw string and its original index. +pub fn make_masked(raw: &str, idx: usize) -> MaskedArg<'_> { + MaskedArg { raw, raw_idx: idx } +} + +/// Create a `Vec` from an array of `(raw, raw_idx)` pairs. +pub fn make_args<'a>(pairs: &'a [(&'a str, usize)]) -> Vec> { + pairs + .iter() + .map(|&(raw, idx)| MaskedArg { raw, raw_idx: idx }) + .collect() +} diff --git a/arg_picker/test/src/test.rs b/arg_picker/test/src/test.rs new file mode 100644 index 0000000..9c53514 --- /dev/null +++ b/arg_picker/test/src/test.rs @@ -0,0 +1,10 @@ +mod arg_matcher_test; +mod basic_test; +mod multi_arg_test; +mod multi_value_test; +mod pos_matcher_test; +mod priority_test; +mod route_test; +mod style_test; +mod value_flag_test; +mod value_string_test; diff --git a/arg_picker/test/src/test/arg_matcher_test.rs b/arg_picker/test/src/test/arg_matcher_test.rs new file mode 100644 index 0000000..d1d363e --- /dev/null +++ b/arg_picker/test/src/test/arg_matcher_test.rs @@ -0,0 +1,289 @@ +use arg_picker::parselib::{ArgMatcher, Matcher, POWERSHELL_STYLE, UNIX_STYLE}; +use arg_picker::PickerArgInfo; + +use crate::make_args; + +// on_match_one — Named + +#[test] +fn test_match_one_named_basic() { + let mut info = PickerArgInfo::new(); + info.set_long("name"); + let args = make_args(&[("--name", 0), ("Alice", 1)]); + let result = ArgMatcher::on_match_one(&args, &UNIX_STYLE, &info); + assert_eq!(result, Some(0)); +} + +#[test] +fn test_match_one_named_eq_mode() { + let mut info = PickerArgInfo::new(); + info.set_long("name"); + let args = make_args(&[("--name=Alice", 0)]); + let result = ArgMatcher::on_match_one(&args, &UNIX_STYLE, &info); + assert_eq!(result, Some(0)); +} + +#[test] +fn test_match_one_named_no_match() { + let mut info = PickerArgInfo::new(); + info.set_long("name"); + let args = make_args(&[("--other", 0), ("Alice", 1)]); + let result = ArgMatcher::on_match_one(&args, &UNIX_STYLE, &info); + assert_eq!(result, None); +} + +#[test] +fn test_match_one_named_short_flag() { + let mut info = PickerArgInfo::new(); + info.set_long("name"); + info.set_short('n'); + let args = make_args(&[("-n", 0), ("Alice", 1)]); + let result = ArgMatcher::on_match_one(&args, &UNIX_STYLE, &info); + assert_eq!(result, Some(0)); +} + +#[test] +fn test_match_one_named_after_end_of_options() { + // Flags after `--` should not be matched. + let mut info = PickerArgInfo::new(); + info.set_long("name"); + let args = make_args(&[("--", 0), ("--name", 1), ("Alice", 2)]); + let result = ArgMatcher::on_match_one(&args, &UNIX_STYLE, &info); + assert_eq!(result, None); +} + +// on_match_one — Positional + +#[test] +fn test_match_one_positional_basic() { + let mut info = PickerArgInfo::new(); + info.set_positional(true); + let args = make_args(&[("file.txt", 0)]); + let result = ArgMatcher::on_match_one(&args, &UNIX_STYLE, &info); + assert_eq!(result, Some(0)); +} + +#[test] +fn test_match_one_positional_takes_first() { + let mut info = PickerArgInfo::new(); + info.set_positional(true); + let args = make_args(&[("a.txt", 0), ("b.txt", 1)]); + let result = ArgMatcher::on_match_one(&args, &UNIX_STYLE, &info); + assert_eq!(result, Some(0)); +} + +// on_match_all — Named, single occurrence + +#[test] +fn test_match_all_named_flag_plus_value() { + let mut info = PickerArgInfo::new(); + info.set_long("name"); + let args = make_args(&[("--name", 0), ("Alice", 1)]); + let result = ArgMatcher::on_match_all(&args, &UNIX_STYLE, &info); + assert_eq!(result, vec![0, 1]); +} + +#[test] +fn test_match_all_named_eq_mode() { + // --name=Alice: value is inline, only tag the flag position. + let mut info = PickerArgInfo::new(); + info.set_long("name"); + let args = make_args(&[("--name=Alice", 0)]); + let result = ArgMatcher::on_match_all(&args, &UNIX_STYLE, &info); + assert_eq!(result, vec![0]); +} + +#[test] +fn test_match_all_named_no_value() { + // Flag at end with no following arg: only tag the flag. + let mut info = PickerArgInfo::new(); + info.set_long("name"); + let args = make_args(&[("--name", 0)]); + let result = ArgMatcher::on_match_all(&args, &UNIX_STYLE, &info); + assert_eq!(result, vec![0]); +} + +#[test] +fn test_match_all_named_value_looks_like_flag() { + // The next arg looks like a flag — still tag it. + // Validation is the Pickable's responsibility. + let mut info = PickerArgInfo::new(); + info.set_long("name"); + let args = make_args(&[("--name", 0), ("--other", 1)]); + let result = ArgMatcher::on_match_all(&args, &UNIX_STYLE, &info); + assert_eq!(result, vec![0, 1]); +} + +// on_match_all — Named, multiple occurrences (Single per flag) + +#[test] +fn test_match_all_named_two_occurrences() { + // --name Alice --name Bob → each occurrence gets one value. + let mut info = PickerArgInfo::new(); + info.set_long("name"); + let args = make_args(&[("--name", 0), ("Alice", 1), ("--name", 2), ("Bob", 3)]); + let result = ArgMatcher::on_match_all(&args, &UNIX_STYLE, &info); + assert_eq!(result, vec![0, 1, 2, 3]); +} + +#[test] +fn test_match_all_named_skips_non_matching_args() { + // --val a b --val d → only pairs (0,1) and (3,4); idx 2 ("b") left free. + let mut info = PickerArgInfo::new(); + info.set_long("val"); + let args = make_args(&[("--val", 0), ("a", 1), ("b", 2), ("--val", 3), ("d", 4)]); + let result = ArgMatcher::on_match_all(&args, &UNIX_STYLE, &info); + assert_eq!(result, vec![0, 1, 3, 4]); +} + +// on_match_all — Named, short flag + +#[test] +fn test_match_all_named_short_flag() { + let mut info = PickerArgInfo::new(); + info.set_long("name"); + info.set_short('n'); + let args = make_args(&[("-n", 0), ("Alice", 1)]); + let result = ArgMatcher::on_match_all(&args, &UNIX_STYLE, &info); + assert_eq!(result, vec![0, 1]); +} + +// on_match_all — Named, eq + non-eq mixed + +#[test] +fn test_match_all_named_mixed_eq_and_regular() { + let mut info = PickerArgInfo::new(); + info.set_long("name"); + let args = make_args(&[("--name=Alice", 0), ("--name", 1), ("Bob", 2)]); + let result = ArgMatcher::on_match_all(&args, &UNIX_STYLE, &info); + assert_eq!(result, vec![0, 1, 2]); +} + +// on_match_all — Named, case insensitive (PowerShell) + +#[test] +fn test_match_all_named_powershell_case_insensitive() { + let mut info = PickerArgInfo::new(); + info.set_long("Name"); + let args = make_args(&[("-name", 0), ("Alice", 1)]); + let result = ArgMatcher::on_match_all(&args, &POWERSHELL_STYLE, &info); + assert_eq!(result, vec![0, 1]); +} + +// on_match_all — Positional + +#[test] +fn test_match_all_positional_single() { + let mut info = PickerArgInfo::new(); + info.set_positional(true); + let args = make_args(&[("file.txt", 0)]); + let result = ArgMatcher::on_match_all(&args, &UNIX_STYLE, &info); + assert_eq!(result, vec![0]); +} + +#[test] +fn test_match_all_positional_multiple() { + let mut info = PickerArgInfo::new(); + info.set_positional(true); + let args = make_args(&[("a.txt", 0), ("b.txt", 1)]); + let result = ArgMatcher::on_match_all(&args, &UNIX_STYLE, &info); + assert_eq!(result, vec![0, 1]); +} + +// End-of-options marker (`--`) + +#[test] +fn test_match_all_named_stops_at_end_of_options() { + // --name before `--` should match, --name after should not. + let mut info = PickerArgInfo::new(); + info.set_long("name"); + let args = make_args(&[ + ("--name", 0), + ("Alice", 1), + ("--", 2), + ("--name", 3), + ("Bob", 4), + ]); + let result = ArgMatcher::on_match_all(&args, &UNIX_STYLE, &info); + assert_eq!(result, vec![0, 1]); +} + +#[test] +fn test_match_all_positional_stops_at_end_of_options() { + let mut info = PickerArgInfo::new(); + info.set_positional(true); + let args = make_args(&[("before", 0), ("--", 1), ("after", 2)]); + let result = ArgMatcher::on_match_all(&args, &UNIX_STYLE, &info); + assert_eq!(result, vec![0]); +} + +// Empty args + +#[test] +fn test_match_one_empty() { + let mut info = PickerArgInfo::new(); + info.set_long("name"); + let args = vec![]; + let result = ArgMatcher::on_match_one(&args, &UNIX_STYLE, &info); + assert_eq!(result, None); +} + +#[test] +fn test_match_all_empty() { + let mut info = PickerArgInfo::new(); + info.set_long("name"); + let args = vec![]; + let result = ArgMatcher::on_match_all(&args, &UNIX_STYLE, &info); + assert!(result.is_empty()); +} + +// Verify that -- itself is never matched as a flag + +#[test] +fn test_match_all_end_of_options_not_matched() { + // `--` should neither match as a flag nor take a value. + let mut info = PickerArgInfo::new(); + info.set_long("name"); + let args = make_args(&[("--", 0)]); + let result = ArgMatcher::on_match_all(&args, &UNIX_STYLE, &info); + assert!(result.is_empty()); +} + +// `--` should never be consumed as a value by a named flag. + +#[test] +fn test_match_all_named_does_not_consume_end_marker() { + // `--flag` before `--`, but the next position IS `--`. + // Only tag the flag, don't consume the end-of-options marker. + let mut info = PickerArgInfo::new(); + info.set_long("flag"); + let args = make_args(&[("--flag", 0), ("--", 1), ("value", 2)]); + let result = ArgMatcher::on_match_all(&args, &UNIX_STYLE, &info); + assert_eq!(result, vec![0], "should NOT consume -- as a value"); +} + +#[test] +fn test_match_all_named_does_not_consume_end_marker_flag_before_end() { + // Simulates: --flag -- you where -- is end-of-options. + // The flag should be tagged but -- should NOT be consumed as its value. + let mut info = PickerArgInfo::new(); + info.set_long("flag"); + + let args = make_args(&[("--flag", 0), ("--", 1), ("you", 2)]); + let result = ArgMatcher::on_match_all(&args, &UNIX_STYLE, &info); + assert_eq!( + result, + vec![0], + "flag before --: only tag flag, leave -- for positional" + ); +} + +#[test] +fn test_match_all_flag_after_end_has_value() { + // Flag after `--` should not match at all. + let mut info = PickerArgInfo::new(); + info.set_long("flag"); + let args = make_args(&[("--", 0), ("--flag", 1), ("value", 2)]); + let result = ArgMatcher::on_match_all(&args, &UNIX_STYLE, &info); + assert!(result.is_empty()); +} diff --git a/arg_picker/test/src/test/basic_test.rs b/arg_picker/test/src/test/basic_test.rs new file mode 100644 index 0000000..b01adb6 --- /dev/null +++ b/arg_picker/test/src/test/basic_test.rs @@ -0,0 +1,223 @@ +use arg_picker::{macros::arg, IntoPicker}; + +// Basic bool flag — present / absent + +#[test] +fn test_bool_flag_present() { + let args = vec!["--verbose"]; + let parsed = args + .to_picker() + .pick(&arg![verbose: bool]) + .or_default() + .unwrap(); + assert_eq!(parsed, true); +} + +#[test] +fn test_bool_flag_absent() { + let args: Vec<&str> = vec![]; + let parsed = args + .to_picker() + .pick(&arg![verbose: bool]) + .or_default() + .unwrap(); + assert_eq!(parsed, false); +} + +// Short flag — '-v' + +#[test] +fn test_bool_short_flag_present() { + let args = vec!["-v"]; + let parsed = args + .to_picker() + .pick(&arg![verbose: bool, 'v']) + .or_default() + .unwrap(); + assert_eq!(parsed, true); +} + +// Multiple bool flags at once + +#[test] +fn test_two_bool_flags_both_present() { + // UNIX_STYLE now uses Kebab naming: `flag_a` → "flag-a" → --flag-a + let args = vec!["--flag-a", "--flag-b"]; + let (a, b) = args + .to_picker() + .pick(&arg![flag_a: bool]) + .or_default() + .pick(&arg![flag_b: bool]) + .or_default() + .unwrap(); + assert_eq!(a, true); + assert_eq!(b, true); +} + +#[test] +fn test_two_bool_flags_one_present() { + let args = vec!["--flag-a"]; + let (a, b) = args + .to_picker() + .pick(&arg![flag_a: bool]) + .or_default() + .pick(&arg![flag_b: bool]) + .or_default() + .unwrap(); + assert_eq!(a, true); + assert_eq!(b, false); +} + +#[test] +fn test_two_bool_flags_neither_present() { + let args: Vec<&str> = vec![]; + let (a, b) = args + .to_picker() + .pick(&arg![flag_a: bool]) + .or_default() + .pick(&arg![flag_b: bool]) + .or_default() + .unwrap(); + assert_eq!(a, false); + assert_eq!(b, false); +} + +// Mixed short and long flags + +#[test] +fn test_short_and_long_flags() { + let args = vec!["-a", "--long-b"]; + let (a, b) = args + .to_picker() + .pick(&arg![flag_a: bool, 'a']) + .or_default() + .pick(&arg![long_b: bool]) + .or_default() + .unwrap(); + assert_eq!(a, true); + assert_eq!(b, true); +} + +// Flags after `--` (end-of-options marker) should not be parsed. + +#[test] +fn test_flag_after_end_of_options() { + let args = vec!["--", "--verbose"]; + let parsed = args + .to_picker() + .pick(&arg![verbose: bool]) + .or_default() + .unwrap(); + assert_eq!(parsed, false); +} + +// Alias matching for bool flags + +#[test] +fn test_bool_flag_with_alias() { + let args = vec!["--cfg"]; + let parsed = args + .to_picker() + .pick(&arg![config: bool, "cfg"]) + .or_default() + .unwrap(); + assert_eq!(parsed, true); +} + +#[test] +fn test_bool_flag_primary_name() { + let args = vec!["--config"]; + let parsed = args + .to_picker() + .pick(&arg![config: bool, "cfg"]) + .or_default() + .unwrap(); + assert_eq!(parsed, true); +} + +// Short flag + alias for bool flag + +#[test] +fn test_bool_flag_short_and_alias() { + let args = vec!["-v"]; + let parsed = args + .to_picker() + .pick(&arg![verbose: bool, 'v', "cfg"]) + .or_default() + .unwrap(); + assert_eq!(parsed, true); +} + +// Default values: .or() / .or_default() + +#[test] +fn test_or_default_without_args() { + let args: Vec<&str> = vec![]; + let parsed = args + .to_picker() + .pick(&arg![verbose: bool]) + .or_default() + .unwrap(); + assert_eq!(parsed, false); +} + +#[test] +fn test_or_custom_default() { + let args: Vec<&str> = vec![]; + let parsed = args + .to_picker() + .pick(&arg![verbose: bool]) + .or(|| true) + .unwrap(); + assert_eq!(parsed, true); +} + +// to_result / to_option interface + +#[test] +fn test_to_result_ok() { + let args = vec!["--verbose"]; + let result = args + .to_picker() + .pick(&arg![verbose: bool]) + .or_default() + .to_result(); + assert_eq!(result, Ok(true)); +} + +#[test] +fn test_to_option_some() { + let args = vec!["--verbose"]; + let opt = args + .to_picker() + .pick(&arg![verbose: bool]) + .or_default() + .to_option(); + assert_eq!(opt, Some(true)); +} + +// Chain with_route passthrough + +#[test] +fn test_with_route_chain() { + let args = vec!["--flag"]; + let parsed = args + .with_route::() + .pick(&arg![flag: bool]) + .or_default() + .unwrap(); + assert_eq!(parsed, true); +} + +// Unrelated flag should not match + +#[test] +fn test_unrelated_flag_does_not_match() { + let args = vec!["--other"]; + let parsed = args + .to_picker() + .pick(&arg![verbose: bool]) + .or_default() + .unwrap(); + assert_eq!(parsed, false); +} diff --git a/arg_picker/test/src/test/multi_arg_test.rs b/arg_picker/test/src/test/multi_arg_test.rs new file mode 100644 index 0000000..49517f1 --- /dev/null +++ b/arg_picker/test/src/test/multi_arg_test.rs @@ -0,0 +1,186 @@ +use arg_picker::parselib::{Matcher, MultiArgMatcher, UNIX_STYLE}; +use arg_picker::PickerArgInfo; + +use crate::make_args; + +// on_match_one — basic + +#[test] +fn test_multi_one_finds_first_flag() { + let mut info = PickerArgInfo::new(); + info.set_long("val"); + let args = make_args(&[("--other", 0), ("--val", 1), ("a", 2), ("b", 3)]); + let result = MultiArgMatcher::on_match_one(&args, &UNIX_STYLE, &info); + assert_eq!(result, Some(1)); +} + +#[test] +fn test_multi_one_no_match() { + let mut info = PickerArgInfo::new(); + info.set_long("val"); + let args = make_args(&[("--other", 0)]); + let result = MultiArgMatcher::on_match_one(&args, &UNIX_STYLE, &info); + assert_eq!(result, None); +} + +// on_match_all — named, basic multi-value + +#[test] +fn test_multi_all_flag_takes_all_values_until_next_flag() { + // --val a b c → all three values belong to --val + let mut info = PickerArgInfo::new(); + info.set_long("val"); + let args = make_args(&[("--val", 0), ("a", 1), ("b", 2), ("c", 3)]); + let result = MultiArgMatcher::on_match_all(&args, &UNIX_STYLE, &info); + assert_eq!(result, vec![0, 1, 2, 3]); +} + +#[test] +fn test_multi_all_stops_at_next_flag() { + // --val a b --other c d → only a,b belong to --val + let mut info = PickerArgInfo::new(); + info.set_long("val"); + let args = make_args(&[("--val", 0), ("a", 1), ("b", 2), ("--other", 3), ("c", 4)]); + let result = MultiArgMatcher::on_match_all(&args, &UNIX_STYLE, &info); + assert_eq!(result, vec![0, 1, 2]); +} + +#[test] +fn test_multi_all_flag_no_values() { + // --val at end with no values → just the flag + let mut info = PickerArgInfo::new(); + info.set_long("val"); + let args = make_args(&[("--val", 0)]); + let result = MultiArgMatcher::on_match_all(&args, &UNIX_STYLE, &info); + assert_eq!(result, vec![0]); +} + +#[test] +fn test_multi_all_empty() { + let mut info = PickerArgInfo::new(); + info.set_long("val"); + let args = vec![]; + let result = MultiArgMatcher::on_match_all(&args, &UNIX_STYLE, &info); + assert!(result.is_empty()); +} + +// on_match_all — named, multiple occurrences of same flag + +#[test] +fn test_multi_all_two_occurrences() { + // --val a b --val c d → two groups + let mut info = PickerArgInfo::new(); + info.set_long("val"); + let args = make_args(&[ + ("--val", 0), + ("a", 1), + ("b", 2), + ("--val", 3), + ("c", 4), + ("d", 5), + ]); + let result = MultiArgMatcher::on_match_all(&args, &UNIX_STYLE, &info); + assert_eq!(result, vec![0, 1, 2, 3, 4, 5]); +} + +#[test] +fn test_multi_all_skips_non_matching_args() { + // --val a --other b --val c → only --val groups + let mut info = PickerArgInfo::new(); + info.set_long("val"); + let args = make_args(&[ + ("--val", 0), + ("a", 1), + ("--other", 2), + ("b", 3), + ("--val", 4), + ("c", 5), + ]); + let result = MultiArgMatcher::on_match_all(&args, &UNIX_STYLE, &info); + assert_eq!(result, vec![0, 1, 4, 5]); +} + +// on_match_all — named, eq mode + +#[test] +fn test_multi_all_eq_mode() { + // --val=a b → eq mode + one extra value + let mut info = PickerArgInfo::new(); + info.set_long("val"); + let args = make_args(&[("--val=a", 0), ("b", 1)]); + let result = MultiArgMatcher::on_match_all(&args, &UNIX_STYLE, &info); + assert_eq!(result, vec![0, 1]); +} + +#[test] +fn test_multi_all_eq_mode_no_extra() { + // --val=a alone → just the eq flag + let mut info = PickerArgInfo::new(); + info.set_long("val"); + let args = make_args(&[("--val=a", 0)]); + let result = MultiArgMatcher::on_match_all(&args, &UNIX_STYLE, &info); + assert_eq!(result, vec![0]); +} + +#[test] +fn test_multi_all_mixed_eq_and_regular() { + // --val=a b --val c d + let mut info = PickerArgInfo::new(); + info.set_long("val"); + let args = make_args(&[("--val=a", 0), ("b", 1), ("--val", 2), ("c", 3), ("d", 4)]); + let result = MultiArgMatcher::on_match_all(&args, &UNIX_STYLE, &info); + assert_eq!(result, vec![0, 1, 2, 3, 4]); +} + +// on_match_all — short flag + +#[test] +fn test_multi_all_short_flag() { + let mut info = PickerArgInfo::new(); + info.set_short('v'); + info.set_long("val"); + let args = make_args(&[("-v", 0), ("a", 1), ("b", 2)]); + let result = MultiArgMatcher::on_match_all(&args, &UNIX_STYLE, &info); + assert_eq!(result, vec![0, 1, 2]); +} + +// on_match_all — end-of-options marker + +#[test] +fn test_multi_all_stops_at_end_of_options() { + // --val a -- b → stops before `--` + let mut info = PickerArgInfo::new(); + info.set_long("val"); + let args = make_args(&[("--val", 0), ("a", 1), ("--", 2), ("b", 3)]); + let result = MultiArgMatcher::on_match_all(&args, &UNIX_STYLE, &info); + assert_eq!(result, vec![0, 1]); +} + +#[test] +fn test_multi_all_flag_after_end_ignored() { + let mut info = PickerArgInfo::new(); + info.set_long("val"); + let args = make_args(&[("--", 0), ("--val", 1), ("a", 2)]); + let result = MultiArgMatcher::on_match_all(&args, &UNIX_STYLE, &info); + assert!(result.is_empty()); +} + +// on_match_all — positional + +#[test] +fn test_multi_all_positional() { + let mut info = PickerArgInfo::new(); + info.set_positional(true); + let args = make_args(&[("a.txt", 0), ("b.txt", 1)]); + let result = MultiArgMatcher::on_match_all(&args, &UNIX_STYLE, &info); + assert_eq!(result, vec![0, 1]); +} + +#[test] +fn test_multi_all_positional_stops_at_end_of_options() { + let mut info = PickerArgInfo::new(); + info.set_positional(true); + let args = make_args(&[("a.txt", 0), ("--", 1), ("b.txt", 2)]); + let result = MultiArgMatcher::on_match_all(&args, &UNIX_STYLE, &info); + assert_eq!(result, vec![0]); +} diff --git a/arg_picker/test/src/test/multi_value_test.rs b/arg_picker/test/src/test/multi_value_test.rs new file mode 100644 index 0000000..cbae645 --- /dev/null +++ b/arg_picker/test/src/test/multi_value_test.rs @@ -0,0 +1,66 @@ +use arg_picker::value::{Flag, VecUntil}; +use arg_picker::{macros::arg, IntoPicker}; + +#[test] +fn test_vec_until_i16_named() { + let (nums, rest): (VecUntil, Flag) = vec!["--nums", "1", "2", "3", "abc"] + .to_picker() + .pick(&arg![nums: VecUntil]) + .pick(&arg![rest: Flag]) + .unwrap(); + assert_eq!(*nums, vec![1i16, 2, 3]); + assert_eq!(rest, Flag::Inactive); +} + +#[test] +fn test_vec_until_i16_stops_at_non_number() { + let nums: VecUntil = vec!["--nums", "42", "abc", "100"] + .to_picker() + .pick(&arg![nums: VecUntil]) + .unwrap(); + assert_eq!(*nums, vec![42i16]); +} + +#[test] +fn test_vec_until_i16_empty() { + let nums: VecUntil = vec!["--nums"] + .to_picker() + .pick(&arg![nums: VecUntil]) + .unwrap(); + assert!(nums.is_empty()); +} + +#[test] +fn test_vec_until_i16_stops_at_next_flag() { + let nums: VecUntil = vec!["--nums", "1", "2", "--other", "3"] + .to_picker() + .pick(&arg![nums: VecUntil]) + .unwrap(); + assert_eq!(*nums, vec![1i16, 2]); +} + +#[test] +fn test_vec_until_i16_stops_at_end_of_options() { + let nums: VecUntil = vec!["--nums", "1", "2", "--", "3"] + .to_picker() + .pick(&arg![nums: VecUntil]) + .unwrap(); + assert_eq!(*nums, vec![1i16, 2]); +} + +// Two VecUntil with different boundary behaviours + +#[test] +fn test_vec_until_f64_and_i16_positional() { + // Both are positional VecUntil. f64 takes all valid floats, + // i16 takes what's left. But note: "1", "2", "3" are also valid + // f64 values, so f64's check_boundary never fires — it consumes + // everything, and i16 gets nothing. + let (floats, ints): (VecUntil, VecUntil) = vec!["1.5", "2.5", "3.5", "1", "2", "3"] + .to_picker() + .pick(&arg![VecUntil]) + .pick(&arg![VecUntil]) + .unwrap(); + assert_eq!(*floats, vec![1.5, 2.5, 3.5]); + assert_eq!(*ints, vec![1i16, 2, 3]); +} diff --git a/arg_picker/test/src/test/pos_matcher_test.rs b/arg_picker/test/src/test/pos_matcher_test.rs new file mode 100644 index 0000000..0d4f87e --- /dev/null +++ b/arg_picker/test/src/test/pos_matcher_test.rs @@ -0,0 +1,141 @@ +use arg_picker::parselib::{MaskedArg, Matcher, PositionalMatcher, UNIX_STYLE, WINDOWS_STYLE}; +use arg_picker::PickerArgInfo; + +fn make_args<'a>(pairs: &'a [(&'a str, usize)]) -> Vec> { + pairs + .iter() + .map(|&(raw, idx)| MaskedArg { raw, raw_idx: idx }) + .collect() +} + +// on_match_one — basic + +#[test] +fn test_pos_one_takes_first_non_flag() { + let info = PickerArgInfo::new(); + let args = make_args(&[("--verbose", 0), ("file.txt", 1)]); + let result = PositionalMatcher::on_match_one(&args, &UNIX_STYLE, &info); + assert_eq!(result, Some(1)); +} + +#[test] +fn test_pos_one_takes_first_if_no_flag() { + let info = PickerArgInfo::new(); + let args = make_args(&[("file.txt", 0)]); + let result = PositionalMatcher::on_match_one(&args, &UNIX_STYLE, &info); + assert_eq!(result, Some(0)); +} + +#[test] +fn test_pos_one_all_flags_returns_none() { + let info = PickerArgInfo::new(); + let args = make_args(&[("--verbose", 0), ("--name", 1)]); + let result = PositionalMatcher::on_match_one(&args, &UNIX_STYLE, &info); + assert_eq!(result, None); +} + +#[test] +fn test_pos_one_empty_returns_none() { + let info = PickerArgInfo::new(); + let args = vec![]; + let result = PositionalMatcher::on_match_one(&args, &UNIX_STYLE, &info); + assert_eq!(result, None); +} + +// on_match_one — after `--` + +#[test] +fn test_pos_one_after_end_takes_even_flag_like() { + // After `--`, accept everything including `--verbose`. + let info = PickerArgInfo::new(); + let args = make_args(&[("--", 0), ("--verbose", 1), ("file.txt", 2)]); + let result = PositionalMatcher::on_match_one(&args, &UNIX_STYLE, &info); + assert_eq!(result, Some(1)); +} + +#[test] +fn test_pos_one_only_end_returns_none() { + let info = PickerArgInfo::new(); + let args = make_args(&[("--", 0)]); + let result = PositionalMatcher::on_match_one(&args, &UNIX_STYLE, &info); + assert_eq!(result, None); +} + +// on_match_one — Windows style prefix + +#[test] +fn test_pos_one_windows_skips_slash_prefix() { + let info = PickerArgInfo::new(); + let args = make_args(&[("/Verbose", 0), ("file.txt", 1)]); + let result = PositionalMatcher::on_match_one(&args, &WINDOWS_STYLE, &info); + assert_eq!(result, Some(1)); +} + +// on_match_all — basic + +#[test] +fn test_pos_all_collects_non_flags() { + let info = PickerArgInfo::new(); + let args = make_args(&[("--verbose", 0), ("a.txt", 1), ("--name", 2), ("b.txt", 3)]); + let result = PositionalMatcher::on_match_all(&args, &UNIX_STYLE, &info); + assert_eq!(result, vec![1, 3]); +} + +#[test] +fn test_pos_all_only_flags_returns_empty() { + let info = PickerArgInfo::new(); + let args = make_args(&[("--a", 0), ("--b", 1)]); + let result = PositionalMatcher::on_match_all(&args, &UNIX_STYLE, &info); + assert!(result.is_empty()); +} + +#[test] +fn test_pos_all_empty() { + let info = PickerArgInfo::new(); + let args = vec![]; + let result = PositionalMatcher::on_match_all(&args, &UNIX_STYLE, &info); + assert!(result.is_empty()); +} + +// on_match_all — after `--` + +#[test] +fn test_pos_all_after_end_accepts_everything() { + // After `--`, even `--verbose` is accepted as positional. + let info = PickerArgInfo::new(); + let args = make_args(&[ + ("--verbose", 0), + ("a.txt", 1), + ("--", 2), + ("--flag", 3), + ("b.txt", 4), + ]); + let result = PositionalMatcher::on_match_all(&args, &UNIX_STYLE, &info); + assert_eq!(result, vec![1, 3, 4]); +} + +#[test] +fn test_pos_all_only_after_end() { + let info = PickerArgInfo::new(); + let args = make_args(&[("--", 0), ("arg1", 1), ("--arg2", 2)]); + let result = PositionalMatcher::on_match_all(&args, &UNIX_STYLE, &info); + assert_eq!(result, vec![1, 2]); +} + +// on_match_all — mixed before/after `--` + +#[test] +fn test_pos_all_mixed() { + let info = PickerArgInfo::new(); + let args = make_args(&[ + ("infile", 0), + ("--verbose", 1), + ("--", 2), + ("outfile", 3), + ("--extra", 4), + ]); + let result = PositionalMatcher::on_match_all(&args, &UNIX_STYLE, &info); + // Before `--`: "infile" (skip --verbose). + // After `--`: everything — "outfile", "--extra". + assert_eq!(result, vec![0, 3, 4]); +} diff --git a/arg_picker/test/src/test/priority_test.rs b/arg_picker/test/src/test/priority_test.rs new file mode 100644 index 0000000..8f0a452 --- /dev/null +++ b/arg_picker/test/src/test/priority_test.rs @@ -0,0 +1,85 @@ +use arg_picker::value::Flag; +use arg_picker::{macros::arg, IntoPicker}; + +// Same flag name, different Pickable types +// +// PickerArgAttr priority: Flag < Single +// So Single and Multi should parse BEFORE Flag when sharing +// the same flag name. + +#[test] +fn test_single_takes_flag_when_sharing_name() { + // --name Alice: String consumes it, Flag sees nothing. + let (name, verbose): (String, Flag) = vec!["--name", "Alice"] + .to_picker() + .pick(&arg![name: String]) // Single, parsed first + .pick(&arg![name: Flag]) // Flag, parsed second, nothing left + .unwrap(); + assert_eq!(name, "Alice"); + assert_eq!(verbose, Flag::Inactive); +} + +#[test] +fn test_flag_only_triggers_when_single_missing() { + // --verbose: only Flag matches, String gets default. + let (name, verbose): (String, Flag) = vec!["--verbose"] + .to_picker() + .pick(&arg![name: String]) // Single, no match → default "" + .or_default() + .pick(&arg![name: Flag]) // Flag, no --name in args → Inactive + .unwrap(); + assert_eq!(name, ""); + assert_eq!(verbose, Flag::Inactive); +} + +#[test] +fn test_flag_gets_leftovers_after_single_consumes_value() { + // --name Alice --name: String takes "--name Alice", Flag takes "--name". + let (name, verbose): (String, Flag) = vec!["--name", "Alice", "--name"] + .to_picker() + .pick(&arg![name: String]) // Single: tag [0, 1], consumes --name Alice + .pick(&arg![name: Flag]) // Flag: tags position 2 (--name) + .unwrap(); + assert_eq!(name, "Alice"); + assert_eq!( + verbose, + Flag::Active, + "Flag should see --name at position 2 after String consumed positions 0-1" + ); +} + +#[test] +fn test_short_flag_sharing_same_letter() { + // -n Alice: String takes it, Flag misses. + let (name, verbose): (String, Flag) = vec!["-n", "Alice"] + .to_picker() + .pick(&arg![name: String, 'n']) // Single, parsed first + .pick(&arg![name: Flag, 'n']) // Flag, parsed second + .unwrap(); + assert_eq!(name, "Alice"); + assert_eq!(verbose, Flag::Inactive); +} + +#[test] +fn test_flag_captures_remaining_after_single_partial_consume() { + // --name Alice --verbose: String takes --name Alice, Flag takes --verbose. + let (name, verbose): (String, Flag) = vec!["--name", "Alice", "--verbose"] + .to_picker() + .pick(&arg![name: String]) // Single: tag [0, 1] + .pick(&arg![verbose: Flag]) // Flag: tag [2] + .unwrap(); + assert_eq!(name, "Alice"); + assert_eq!(verbose, Flag::Active); +} + +#[test] +fn test_single_skips_already_claimed_positions() { + // --verbose --name Alice: Flag takes --verbose, String takes --name Alice. + let (verbose, name): (Flag, String) = vec!["--verbose", "--name", "Alice"] + .to_picker() + .pick(&arg![verbose: Flag]) // Flag: tag [0] + .pick(&arg![name: String]) // Single: tag [1, 2] + .unwrap(); + assert_eq!(verbose, Flag::Active); + assert_eq!(name, "Alice"); +} diff --git a/arg_picker/test/src/test/route_test.rs b/arg_picker/test/src/test/route_test.rs new file mode 100644 index 0000000..7594c6e --- /dev/null +++ b/arg_picker/test/src/test/route_test.rs @@ -0,0 +1,200 @@ +use arg_picker::{macros::arg, IntoPicker}; + +// Route mechanism — or_route + +#[test] +fn test_or_route_triggered_to_result() { + // flag not present and no default value → route triggered → Err + let args: Vec<&str> = vec![]; + let result: Result = args + .with_route::<&'static str>() + .pick(&arg![verbose: bool]) + .or_route(|| "missing_verbose") + .to_result(); + assert_eq!(result, Err("missing_verbose")); +} + +#[test] +fn test_or_route_not_triggered_when_flag_present() { + // flag present → route not triggered → Ok + let args = vec!["--verbose"]; + let result: Result = args + .with_route::<&'static str>() + .pick(&arg![verbose: bool]) + .or_route(|| "missing_verbose") + .to_result(); + assert_eq!(result, Ok(true)); +} + +#[test] +fn test_or_default_priority_over_or_route() { + // or_default takes priority over or_route: even if the flag is absent, + // having a default prevents the route from being triggered + let args: Vec<&str> = vec![]; + let result: Result = args + .with_route::<&'static str>() + .pick(&arg![verbose: bool]) + .or_default() + .or_route(|| "should_not_reach") + .to_result(); + assert_eq!(result, Ok(false)); +} + +#[test] +fn test_or_route_to_option_returns_none() { + // When route is triggered, to_option returns None + let args: Vec<&str> = vec![]; + let opt: Option = args + .with_route::<&'static str>() + .pick(&arg![verbose: bool]) + .or_route(|| "missing") + .to_option(); + assert_eq!(opt, None); +} + +#[test] +#[should_panic(expected = "called `Option::unwrap()` on a `None` value")] +fn test_or_route_unwrap_panics() { + let args: Vec<&str> = vec![]; + args.with_route::<&'static str>() + .pick(&arg![verbose: bool]) + .or_route(|| "missing") + .unwrap(); +} + +#[test] +fn test_or_route_with_string_route() { + // Route type is String + let args: Vec<&str> = vec![]; + let result: Result = args + .with_route::() + .pick(&arg![verbose: bool]) + .or_route(|| "route_hit".to_string()) + .to_result(); + assert_eq!(result, Err("route_hit".to_string())); +} + +#[test] +fn test_or_route_with_custom_enum() { + // Route type is a custom enum + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + #[allow(dead_code)] + enum Redirect { + Help, + Version, + } + + let args: Vec<&str> = vec![]; + let result: Result = args + .with_route::() + .pick(&arg![verbose: bool]) + .or_route(|| Redirect::Help) + .to_result(); + assert_eq!(result, Err(Redirect::Help)); +} + +// Route mechanism — multiple flags + +#[test] +fn test_two_flags_first_missing_triggers_route() { + // Both flags missing; first has or_route → Err("first"); + // first route wins (first-checked, first-triggered) + let args: Vec<&str> = vec![]; + let result: Result<(bool, bool), &'static str> = args + .with_route::<&'static str>() + .pick(&arg![flag_a: bool]) + .or_route(|| "first_missing") + .pick(&arg![flag_b: bool]) + .or_route(|| "second_missing") + .to_result(); + assert_eq!(result, Err("first_missing")); +} + +#[test] +fn test_two_flags_second_missing_triggers_route() { + // First has or_default (no route triggered), second missing with or_route → Err("second") + let args: Vec<&str> = vec![]; + let result: Result<(bool, bool), &'static str> = args + .with_route::<&'static str>() + .pick(&arg![flag_a: bool]) + .or_default() + .pick(&arg![flag_b: bool]) + .or_route(|| "second_missing") + .to_result(); + assert_eq!(result, Err("second_missing")); +} + +#[test] +fn test_two_flags_both_present_route_not_triggered() { + // Both flags present → route not triggered → Ok((true, true)) + let args = vec!["--flag-a", "--flag-b"]; + let result: Result<(bool, bool), &'static str> = args + .with_route::<&'static str>() + .pick(&arg![flag_a: bool]) + .or_route(|| "first_missing") + .pick(&arg![flag_b: bool]) + .or_route(|| "second_missing") + .to_result(); + assert_eq!(result, Ok((true, true))); +} + +#[test] +fn test_two_flags_first_missing_no_route_second_has_route() { + // First missing but has no or_route, second missing with or_route → Err("second") + // Note: the first has neither route nor default, so pick failure does not modify error_route + let args: Vec<&str> = vec![]; + let result: Result<(bool, bool), &'static str> = args + .with_route::<&'static str>() + .pick(&arg![flag_a: bool]) + // No or_default, no or_route either + .pick(&arg![flag_b: bool]) + .or_route(|| "second_missing") + .to_result(); + assert_eq!(result, Err("second_missing")); +} + +#[test] +fn test_two_flags_only_second_has_default() { + // First is missing with no default/route (v1=NotFound), second is missing but has or_default + // Use unpack to check both results + let args: Vec<&str> = vec![]; + let (a, b) = args + .to_picker() + .pick(&arg![flag_a: bool]) + .pick(&arg![flag_b: bool]) + .or_default() + .unpack(); + assert_eq!(a, None); + assert_eq!(b, Some(false)); +} + +// Route with with_route + +#[test] +fn test_with_route_and_or_route() { + // with_route::<String>() sets the Route type + or_route triggers + let args: Vec<&str> = vec![]; + let result: Result = args + .with_route::() + .pick(&arg![verbose: bool]) + .or_route(|| "redirected".to_string()) + .to_result(); + assert_eq!(result, Err("redirected".to_string())); +} + +#[test] +fn test_with_route_type_switch() { + // with_route switching Route type clears old route_$ and error_route + let args: Vec<&str> = vec![]; + let result: Result<(bool, bool), i32> = args + .with_route::() + .pick(&arg![verbose: bool]) + .or_route(|| "string_route".to_string()) + .with_route::() + .pick(&arg![other: bool]) + .or_route(|| -1) + .to_result(); + // The first flag is missing, but its or_route is cleared when with_route switches (route_$ = None) + // The second flag is missing and has or_route → Err(-1) + assert_eq!(result, Err(-1)); +} diff --git a/arg_picker/test/src/test/style_test.rs b/arg_picker/test/src/test/style_test.rs new file mode 100644 index 0000000..b470d4f --- /dev/null +++ b/arg_picker/test/src/test/style_test.rs @@ -0,0 +1,300 @@ +use arg_picker::parselib::{ + build_possible_flags, FlagMatcher, Matcher, ParserStyle, ParserStyleNamingCase, + POWERSHELL_STYLE, UNIX_STYLE, WINDOWS_STYLE, +}; +use arg_picker::PickerArgInfo; + +use crate::make_masked; + +// Style: formatting utilities + +#[test] +fn test_unix_style_flag_string() { + assert_eq!(UNIX_STYLE.flag_string('v'), "-v"); + assert_eq!(UNIX_STYLE.flag_string("verbose"), "--verbose"); +} + +#[test] +fn test_windows_style_flag_string() { + assert_eq!(WINDOWS_STYLE.flag_string('v'), "/v"); + assert_eq!(WINDOWS_STYLE.flag_string("verbose"), "/verbose"); +} + +#[test] +fn test_powershell_style_flag_string() { + assert_eq!(POWERSHELL_STYLE.flag_string('v'), "-v"); + assert_eq!(POWERSHELL_STYLE.flag_string("Verbose"), "-Verbose"); +} + +#[test] +fn test_build_possible_flags_windows() { + // Build PickerArgInfo from a flag definition: `verbose: bool` + let mut info = PickerArgInfo::new(); + info.set_long("verbose"); + let flags = build_possible_flags(&WINDOWS_STYLE, &info); + assert_eq!(flags, vec!["/Verbose"]); +} + +#[test] +fn test_build_possible_flags_with_short_and_alias() { + let mut info = PickerArgInfo::new(); + info.set_short('n'); + info.set_long("name"); + info.set_alias(vec!["nickname"]); + let flags = build_possible_flags(&UNIX_STYLE, &info); + assert_eq!(flags, vec!["-n", "--name", "--nickname"]); +} + +// Style: matching with different styles via Matcher trait + +#[test] +fn test_windows_style_match() { + // Windows style: /verbose (case insensitive) + let mut info = PickerArgInfo::new(); + info.set_long("verbose"); + + let args = vec![make_masked("/verbose", 0)]; + let result = FlagMatcher::on_match_one(&args, &WINDOWS_STYLE, &info); + assert_eq!(result, Some(0)); +} + +#[test] +fn test_windows_style_match_case_insensitive() { + // Windows style is case-insensitive: /VERBOSE should match "verbose" + let mut info = PickerArgInfo::new(); + info.set_long("verbose"); + + let args = vec![make_masked("/VERBOSE", 0)]; + let result = FlagMatcher::on_match_one(&args, &WINDOWS_STYLE, &info); + assert_eq!(result, Some(0)); +} + +#[test] +fn test_windows_style_no_match_on_unrelated_flag() { + // Different flag should not match + let mut info = PickerArgInfo::new(); + info.set_long("verbose"); + + let args = vec![make_masked("/output", 0)]; + let result = FlagMatcher::on_match_one(&args, &WINDOWS_STYLE, &info); + assert_eq!(result, None); +} + +#[test] +fn test_powershell_style_match() { + // PowerShell style: -Verbose (case insensitive) + let mut info = PickerArgInfo::new(); + info.set_long("Verbose"); + + let args = vec![make_masked("-Verbose", 0)]; + let result = FlagMatcher::on_match_one(&args, &POWERSHELL_STYLE, &info); + assert_eq!(result, Some(0)); +} + +#[test] +fn test_powershell_style_match_case_insensitive() { + let mut info = PickerArgInfo::new(); + info.set_long("Verbose"); + + let args = vec![make_masked("-VERBOSE", 0)]; + let result = FlagMatcher::on_match_one(&args, &POWERSHELL_STYLE, &info); + assert_eq!(result, Some(0)); +} + +#[test] +fn test_unix_style_case_sensitive_no_match() { + // UNIX style is case-sensitive: --VERBOSE should NOT match --verbose + let mut info = PickerArgInfo::new(); + info.set_long("verbose"); + + let args = vec![make_masked("--VERBOSE", 0)]; + let result = FlagMatcher::on_match_one(&args, &UNIX_STYLE, &info); + assert_eq!(result, None); +} + +#[test] +fn test_windows_style_match_all() { + // on_match_all should find all matching flags + let mut info = PickerArgInfo::new(); + info.set_long("verbose"); + info.set_short('v'); + + let args = vec![ + make_masked("/v", 0), + make_masked("/output", 1), + make_masked("/VERBOSE", 2), + ]; + let result = FlagMatcher::on_match_all(&args, &WINDOWS_STYLE, &info); + assert_eq!(result, vec![0, 2]); +} + +#[test] +fn test_windows_style_match_after_end_of_options() { + // end_of_options is always "--" regardless of style + // Flags after -- should not match + let mut info = PickerArgInfo::new(); + info.set_long("verbose"); + + let args = vec![ + make_masked("/verbose", 0), + make_masked("--", 1), + make_masked("/verbose", 2), + ]; + let result = FlagMatcher::on_match_all(&args, &WINDOWS_STYLE, &info); + // end_of_options is always "--" regardless of style + assert_eq!(result, vec![0]); +} + +// Naming case conversion + +/// A Unix-like style with kebab-case naming. +const KEBAB_STYLE: ParserStyle<'static> = ParserStyle { + naming_case: ParserStyleNamingCase::Kebab, + ..UNIX_STYLE +}; + +#[test] +fn test_kebab_case_naming_for_multiword_flag() { + // flag name `flag_a` → Kebab → `flag-a` → `--flag-a` + let mut info = PickerArgInfo::new(); + info.set_long("flag_a"); + + let args = vec![make_masked("--flag-a", 0)]; + let result = FlagMatcher::on_match_one(&args, &KEBAB_STYLE, &info); + assert_eq!( + result, + Some(0), + "--flag-a should match flag_a via kebab-case conversion" + ); +} + +#[test] +fn test_snake_case_should_not_match_as_long_flag() { + // With kebab naming, `--flag_a` (snake) should NOT match `flag_a` + let mut info = PickerArgInfo::new(); + info.set_long("flag_a"); + + let args = vec![make_masked("--flag_a", 0)]; + let result = FlagMatcher::on_match_one(&args, &KEBAB_STYLE, &info); + assert_eq!( + result, None, + "--flag_a should NOT match in kebab-style context" + ); +} + +#[test] +fn test_kebab_case_naming_for_unix_style() { + // UNIX_STYLE now uses Kebab case: `flag_a` → `flag-a` → `--flag-a` + let mut info = PickerArgInfo::new(); + info.set_long("flag_a"); + + let args = vec![make_masked("--flag-a", 0)]; + let result = FlagMatcher::on_match_one(&args, &UNIX_STYLE, &info); + assert_eq!( + result, + Some(0), + "--flag-a should match with kebab-style UNIX_STYLE" + ); +} + +#[test] +fn test_snake_case_rejected_by_unix_style() { + // UNIX_STYLE uses Kebab: `--flag_a` (snake) should NOT match + let mut info = PickerArgInfo::new(); + info.set_long("flag_a"); + + let args = vec![make_masked("--flag_a", 0)]; + let result = FlagMatcher::on_match_one(&args, &UNIX_STYLE, &info); + assert_eq!( + result, None, + "--flag_a should NOT match under kebab-style UNIX_STYLE" + ); +} + +#[test] +fn test_powershell_pascal_case_naming() { + // POWERSHELL_STYLE uses Pascal case: `verbose` → `Verbose` → `-Verbose` + let mut info = PickerArgInfo::new(); + info.set_long("verbose"); + + let args = vec![make_masked("-Verbose", 0)]; + let result = FlagMatcher::on_match_one(&args, &POWERSHELL_STYLE, &info); + assert_eq!( + result, + Some(0), + "-Verbose should match verbose via Pascal case" + ); +} + +#[test] +fn test_flag_naming_kebab_matches_my_name() { + // UNIX_STYLE now uses Kebab: `my_name` → `my-name` → `--my-name` + let mut info = PickerArgInfo::new(); + info.set_long("my_name"); + + let args = vec![make_masked("--my-name", 0)]; + let result = FlagMatcher::on_match_one(&args, &UNIX_STYLE, &info); + assert_eq!( + result, + Some(0), + "--my-name should match via kebab conversion" + ); +} + +#[test] +fn test_flag_naming_kebab_rejects_my_name_underscore() { + // Kebab style: `--my_name` (snake) should NOT match + let mut info = PickerArgInfo::new(); + info.set_long("my_name"); + + let args = vec![make_masked("--my_name", 0)]; + let result = FlagMatcher::on_match_one(&args, &UNIX_STYLE, &info); + assert_eq!( + result, None, + "--my_name should NOT match under kebab-style UNIX_STYLE" + ); +} + +#[test] +fn test_flag_naming_pascal_matches_my_name() { + // `my_name` under Pascal → `MyName` → `-MyName` + let mut info = PickerArgInfo::new(); + info.set_long("my_name"); + + let args = vec![make_masked("-MyName", 0)]; + let result = FlagMatcher::on_match_one(&args, &POWERSHELL_STYLE, &info); + assert_eq!( + result, + Some(0), + "-MyName should match via Pascal conversion" + ); +} + +#[test] +fn test_flag_naming_pascal_matches_lowercase() { + // PowerShell is case-insensitive: `-myname` should also match + let mut info = PickerArgInfo::new(); + info.set_long("my_name"); + + let args = vec![make_masked("-myname", 0)]; + let result = FlagMatcher::on_match_one(&args, &POWERSHELL_STYLE, &info); + assert_eq!( + result, + Some(0), + "-myname (lowercase) should match via case-insensitive Pascal" + ); +} + +#[test] +fn test_flag_naming_pascal_rejects_my_name_underscore() { + // Pascal style: `-my_name` (underscore) should NOT match + let mut info = PickerArgInfo::new(); + info.set_long("my_name"); + + let args = vec![make_masked("-my_name", 0)]; + let result = FlagMatcher::on_match_one(&args, &POWERSHELL_STYLE, &info); + assert_eq!( + result, None, + "-my_name should NOT match under Pascal-style naming" + ); +} diff --git a/arg_picker/test/src/test/value_flag_test.rs b/arg_picker/test/src/test/value_flag_test.rs new file mode 100644 index 0000000..3df053e --- /dev/null +++ b/arg_picker/test/src/test/value_flag_test.rs @@ -0,0 +1,174 @@ +use arg_picker::value::Flag; +use arg_picker::{macros::arg, IntoPicker}; + +// Basic Flag — present / absent + +#[test] +fn test_flag_present() { + let flag: Flag = vec!["--verbose"] + .to_picker() + .pick(&arg![verbose: Flag]) + .unwrap(); + assert_eq!(flag, Flag::Active); +} + +#[test] +fn test_flag_absent_returns_inactive() { + // Unlike bool, Flag::pick returns Parsed(Inactive) when no match is found, + // so or_default() is NOT required — unwrap() works directly. + let flag: Flag = Vec::<&str>::new() + .to_picker() + .pick(&arg![verbose: Flag]) + .unwrap(); + assert_eq!(flag, Flag::Inactive); +} + +// Short Flag + +#[test] +fn test_flag_short_present() { + let flag: Flag = vec!["-v"] + .to_picker() + .pick(&arg![verbose: Flag, 'v']) + .unwrap(); + assert_eq!(flag, Flag::Active); +} + +// Multiple Flags + +#[test] +fn test_two_flags_both_present() { + let (a, b): (Flag, Flag) = vec!["--flag-a", "--flag-b"] + .to_picker() + .pick(&arg![flag_a: Flag]) + .pick(&arg![flag_b: Flag]) + .unwrap(); + assert_eq!(a, Flag::Active); + assert_eq!(b, Flag::Active); +} + +#[test] +fn test_two_flags_one_present() { + let (a, b): (Flag, Flag) = vec!["--flag-a"] + .to_picker() + .pick(&arg![flag_a: Flag]) + .pick(&arg![flag_b: Flag]) + .unwrap(); + assert_eq!(a, Flag::Active); + assert_eq!(b, Flag::Inactive); +} + +#[test] +fn test_two_flags_neither_present() { + let (a, b): (Flag, Flag) = Vec::<&str>::new() + .to_picker() + .pick(&arg![flag_a: Flag]) + .pick(&arg![flag_b: Flag]) + .unwrap(); + assert_eq!(a, Flag::Inactive); + assert_eq!(b, Flag::Inactive); +} + +// After `--` (end-of-options) + +#[test] +fn test_flag_after_end_of_options() { + let flag: Flag = vec!["--", "--verbose"] + .to_picker() + .pick(&arg![verbose: Flag]) + .unwrap(); + assert_eq!(flag, Flag::Inactive); +} + +// Alias + +#[test] +fn test_flag_with_alias() { + let flag: Flag = vec!["--cfg"] + .to_picker() + .pick(&arg![config: Flag, "cfg"]) + .unwrap(); + assert_eq!(flag, Flag::Active); +} + +#[test] +fn test_flag_primary_name() { + let flag: Flag = vec!["--config"] + .to_picker() + .pick(&arg![config: Flag, "cfg"]) + .unwrap(); + assert_eq!(flag, Flag::Active); +} + +// Unrelated flag should not match + +#[test] +fn test_unrelated_flag_does_not_match() { + let flag: Flag = vec!["--other"] + .to_picker() + .pick(&arg![verbose: Flag]) + .unwrap(); + assert_eq!(flag, Flag::Inactive); +} + +// to_result / to_option + +#[test] +fn test_flag_to_result() { + let result: Result = vec!["--verbose"] + .to_picker() + .pick(&arg![verbose: Flag]) + .to_result(); + assert_eq!(result, Ok(Flag::Active)); +} + +#[test] +fn test_flag_to_option() { + let opt: Option = vec!["--verbose"] + .to_picker() + .pick(&arg![verbose: Flag]) + .to_option(); + assert_eq!(opt, Some(Flag::Active)); +} + +// Bool conversions + +#[test] +fn test_flag_converts_to_bool() { + let flag = Flag::Active; + assert!(bool::from(flag)); + + let flag = Flag::Inactive; + assert!(!bool::from(flag)); +} + +#[test] +fn test_flag_from_bool() { + assert_eq!(Flag::from(true), Flag::Active); + assert_eq!(Flag::from(false), Flag::Inactive); +} + +#[test] +fn test_flag_deref_to_bool() { + let active = Flag::Active; + assert!(*active); + + let inactive = Flag::Inactive; + assert!(!*inactive); +} + +// Flag never triggers route (unlike bool) +// +// Flag::pick always returns Parsed, so the fallback chain +// (default → route) is never entered. + +#[test] +fn test_flag_absent_does_not_trigger_route() { + // Even without or_default / or_route, absent flag returns Inactive, not a route + let result: Result = Vec::<&str>::new() + .with_route::<&str>() + .pick(&arg![verbose: Flag]) + .or_route(|| "should_not_fire") + .to_result(); + assert_eq!(result, Ok(Flag::Inactive)); +} diff --git a/arg_picker/test/src/test/value_string_test.rs b/arg_picker/test/src/test/value_string_test.rs new file mode 100644 index 0000000..2c3b50b --- /dev/null +++ b/arg_picker/test/src/test/value_string_test.rs @@ -0,0 +1,171 @@ +use arg_picker::{macros::arg, IntoPicker}; + +// Basic named String — present / absent + +#[test] +fn test_string_named_present() { + let val: String = vec!["--name", "Alice"] + .to_picker() + .pick(&arg![name: String]) + .or_default() + .unwrap(); + assert_eq!(val, "Alice"); +} + +#[test] +fn test_string_named_absent_uses_default() { + let val: String = Vec::<&str>::new() + .to_picker() + .pick(&arg![name: String]) + .or_default() + .unwrap(); + assert_eq!(val, ""); +} + +// Named String — eq mode + +#[test] +fn test_string_named_eq_mode() { + let val: String = vec!["--name=Alice"] + .to_picker() + .pick(&arg![name: String]) + .or_default() + .unwrap(); + assert_eq!(val, "Alice"); +} + +// Named String — short flag + +#[test] +fn test_string_named_short_flag() { + let val: String = vec!["-n", "Alice"] + .to_picker() + .pick(&arg![name: String, 'n']) + .or_default() + .unwrap(); + assert_eq!(val, "Alice"); +} + +// Named String — no value after flag + +#[test] +fn test_string_named_missing_value_triggers_default() { + // --name at end with no following arg → pick returns NotFound → or_default gives "" + let val: String = vec!["--name"] + .to_picker() + .pick(&arg![name: String]) + .or_default() + .unwrap(); + assert_eq!(val, ""); +} + +// Positional String + +#[test] +fn test_string_positional() { + let val: String = vec!["file.txt"] + .to_picker() + .pick(&arg![String]) + .or_default() + .unwrap(); + assert_eq!(val, "file.txt"); +} + +#[test] +fn test_string_positional_takes_first() { + let val: String = vec!["first", "second"] + .to_picker() + .pick(&arg![String]) + .or_default() + .unwrap(); + assert_eq!(val, "first"); +} + +// Multiple occurrences (Single only tags one occurrence per Parseable) + +#[test] +fn test_string_two_named_flags() { + let (a, b): (String, String) = vec!["--name", "Alice", "--greeting", "Hello"] + .to_picker() + .pick(&arg![name: String]) + .or_default() + .pick(&arg![greeting: String]) + .or_default() + .unwrap(); + assert_eq!(a, "Alice"); + assert_eq!(b, "Hello"); +} + +// Mixed named + positional + +#[test] +fn test_string_named_and_positional() { + let (name, file): (String, String) = vec!["--name", "Alice", "file.txt"] + .to_picker() + .pick(&arg![name: String]) + .or_default() + .pick(&arg![String]) + .or_default() + .unwrap(); + assert_eq!(name, "Alice"); + assert_eq!(file, "file.txt"); +} + +// After `--` (end-of-options) + +#[test] +fn test_string_named_after_end_of_options() { + // Named arg after `--` should not be matched → default + let val: String = vec!["--", "--name", "Alice"] + .to_picker() + .pick(&arg![name: String]) + .or_default() + .unwrap(); + assert_eq!(val, ""); +} + +// Unrelated flag should not match → default + +#[test] +fn test_string_unrelated_flag() { + let val: String = vec!["--other", "value"] + .to_picker() + .pick(&arg![name: String]) + .or_default() + .unwrap(); + assert_eq!(val, ""); +} + +// to_result / to_option + +#[test] +fn test_string_to_result() { + let result: Result = vec!["--name", "Alice"] + .to_picker() + .pick(&arg![name: String]) + .or_default() + .to_result(); + assert_eq!(result, Ok("Alice".to_string())); +} + +#[test] +fn test_string_to_option() { + let opt: Option = vec!["--name", "Alice"] + .to_picker() + .pick(&arg![name: String]) + .or_default() + .to_option(); + assert_eq!(opt, Some("Alice".to_string())); +} + +// Custom default via .or() + +#[test] +fn test_string_custom_default() { + let val: String = Vec::<&str>::new() + .to_picker() + .pick(&arg![name: String]) + .or(|| "default_name".to_string()) + .unwrap(); + assert_eq!(val, "default_name"); +} diff --git a/arg_picker_macros/Cargo.toml b/arg_picker_macros/Cargo.toml new file mode 100644 index 0000000..c83cb9e --- /dev/null +++ b/arg_picker_macros/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "arg-picker-macros" +version = "0.1.0" +edition = "2024" +license = "MIT OR Apache-2.0" +repository = "https://github.com/mingling-rs/mingling/tree/main/arg-picker" +authors = ["Weicao-CatilGrass"] +readme = "README.md" +description = "Mingling's lightweight argument parser macros" + +[lib] +proc-macro = true + +[features] +mingling_support = [] + +[dependencies] +syn.workspace = true +quote.workspace = true +proc-macro2.workspace = true diff --git a/arg_picker_macros/README.md b/arg_picker_macros/README.md new file mode 100644 index 0000000..5a3f220 --- /dev/null +++ b/arg_picker_macros/README.md @@ -0,0 +1,40 @@ +# Mingling Picker Macros + +Procedural macros for [Mingling Picker](https://github.com/mingling-rs/mingling/tree/main/mingling_picker), enabled by the `mingling/picker` feature. + +```toml +[dependencies.mingling] +version = "0.3.0" +features = [ + "picker" +] +``` + +## Provided Macros + +### Macro `arg!` + +Declares a parameter definition for use with `Picker`'s `.pick()` method: + +```rust,ignore +use mingling_picker_macros::arg; + +// Named flag with a value +let flag = arg![name: String]; + +// Named flag with short form +let flag = arg![name: String, 'n']; + +// Named flag with alias +let flag = arg![name: String, 'n', "nickname"]; + +// Positional parameter +let flag = arg![String]; + +// Flag-only parameter (boolean) +let flag = arg![verbose: Flag]; +``` + +### Macro `internal_repeat!` (Internal) + +Internal macro used by Picker to generate `PickerPattern1..=32` and their parsing logic. Not intended for direct use. diff --git a/arg_picker_macros/src/arg.rs b/arg_picker_macros/src/arg.rs new file mode 100644 index 0000000..55860c4 --- /dev/null +++ b/arg_picker_macros/src/arg.rs @@ -0,0 +1,205 @@ +use proc_macro::{TokenStream, TokenTree}; +use proc_macro2::TokenStream as TS2; +use quote::quote; + +pub(crate) fn arg(input: TokenStream) -> TokenStream { + let tokens: Vec = input.into_iter().collect(); + let args = split_at_commas(&tokens); + if args.is_empty() { + return quote! { compile_error!("arg! flaguires at least one argument") }.into(); + } + + let first = &args[0]; + let rest = &args[1..]; + + // Validate: at most one char literal + let char_count = rest.iter().filter(|a| is_char_literal(a)).count(); + if char_count > 1 { + return quote! { compile_error!("arg! only supports at most one short name") }.into(); + } + + // Extract short char and string aliases + let short_char: Option = rest + .iter() + .find(|a| is_char_literal(a)) + .and_then(|a| extract_char(a)); + let aliases: Vec = rest + .iter() + .filter(|a| is_string_literal(a)) + .filter_map(|a| extract_string(a)) + .collect(); + + // Parse first argument + // arg![name: Type] → name, with explicit type + // arg![Type] → positional type, no name + let colon_pos = first + .iter() + .position(|t| matches!(t, TokenTree::Punct(p) if p.as_char() == ':')); + let has_named_colon = colon_pos.is_some_and(|pos| pos > 0 && !is_double_colon(first, pos)); + + let first_slice = first.as_slice(); + let (name, ty, is_named) = if has_named_colon { + let pos = colon_pos.unwrap(); + // `name : Type` + ( + Some(&first_slice[..pos]), + Some(&first_slice[pos + 1..]), + true, + ) + } else { + // No `name:` prefix → the entire first argument is the type + (None, Some(first_slice), false) + }; + + // Build full names + let full_names: Vec = match name { + Some(n) => { + let mut v = vec![join_idents(n)]; + v.extend(aliases); + v + } + None => aliases, + }; + + let path: TS2 = { + let ty_ts: TS2 = ty + .map(|t| { + let ts: TokenStream = t.iter().cloned().collect(); + TS2::from(ts) + }) + .unwrap_or(TS2::new()); + + #[cfg(feature = "mingling_support")] + let import = quote! { ::mingling::picker::PickerArg }; + + #[cfg(not(feature = "mingling_support"))] + let import = quote! { ::arg_picker::PickerArg }; + + if ty.is_some() { + quote! { #import::<#ty_ts> } + } else { + quote! { #import::<_> } + } + }; + + // full: &["name", "alias", ...] or &[] + let full_value: TS2 = if !full_names.is_empty() { + let strs: Vec = full_names + .iter() + .map(|s| proc_macro2::Literal::string(s)) + .collect(); + quote! { &[#(#strs),*] } + } else { + quote! { &[] } + }; + + // short: Some('c') or None + let short_value: TS2 = match short_char { + Some(c) => { + let lit = proc_macro2::Literal::character(c); + quote! { ::std::option::Option::Some(#lit) } + } + None => quote! { ::std::option::Option::None }, + }; + + let positional_value: bool = !is_named; + + let result = quote! { + #path { + full: #full_value, + short: #short_value, + positional: #positional_value, + internal_type: ::std::marker::PhantomData, + } + }; + + result.into() +} + +fn split_at_commas(tokens: &[TokenTree]) -> Vec> { + let mut result = vec![Vec::new()]; + let mut depth = 0u32; + for t in tokens { + match t { + TokenTree::Group(_g) => { + depth += 1; + result.last_mut().unwrap().push(t.clone()); + depth -= 1; + } + TokenTree::Punct(p) if p.as_char() == ',' && depth == 0 => { + result.push(Vec::new()); + } + _ => result.last_mut().unwrap().push(t.clone()), + } + } + result +} + +fn is_double_colon(tokens: &[TokenTree], pos: usize) -> bool { + pos > 0 && matches!(&tokens[pos - 1], TokenTree::Punct(p) if p.as_char() == ':') +} + +fn is_char_literal(tokens: &[TokenTree]) -> bool { + tokens.len() == 1 + && matches!(&tokens[0], TokenTree::Literal(l) if { + let s = l.to_string(); + s.starts_with('\'') && s.len() >= 3 + }) +} + +fn is_string_literal(tokens: &[TokenTree]) -> bool { + tokens.len() == 1 + && matches!(&tokens[0], TokenTree::Literal(l) if { + l.to_string().starts_with('"') + }) +} + +fn extract_char(tokens: &[TokenTree]) -> Option { + match &tokens[0] { + TokenTree::Literal(l) => { + let s = l.to_string(); + let cs: Vec = s.chars().collect(); + if cs.len() >= 3 && cs[0] == '\'' && cs[cs.len() - 1] == '\'' { + let inner: String = cs[1..cs.len() - 1].iter().collect(); + // inner is the character between the quotes of a char literal. + // For a literal `'n'`, inner is `"n"` (the character n). + // For an escape `'\n'`, inner is the actual newline character. + // The catch-all handles both literal single chars and escape + // sequences (the escaped char IS the actual control character). + match inner.as_str() { + "\\\\" => Some('\\'), + "\\'" => Some('\''), + _ => inner.chars().next(), + } + } else { + None + } + } + _ => None, + } +} + +fn extract_string(tokens: &[TokenTree]) -> Option { + match &tokens[0] { + TokenTree::Literal(l) => { + let s = l.to_string(); + let cs: Vec = s.chars().collect(); + if cs.len() >= 2 && cs[0] == '"' && cs[cs.len() - 1] == '"' { + Some(cs[1..cs.len() - 1].iter().collect()) + } else { + None + } + } + _ => None, + } +} + +fn join_idents(tokens: &[TokenTree]) -> String { + tokens + .iter() + .map(|t| match t { + TokenTree::Ident(id) => id.to_string(), + _ => String::new(), + }) + .collect() +} diff --git a/arg_picker_macros/src/internal_repeat.rs b/arg_picker_macros/src/internal_repeat.rs new file mode 100644 index 0000000..4b2242b --- /dev/null +++ b/arg_picker_macros/src/internal_repeat.rs @@ -0,0 +1,315 @@ +use proc_macro::{Delimiter, Group, Ident, Literal, TokenStream, TokenTree}; + +pub(crate) fn internal_repeat(input: TokenStream) -> TokenStream { + let tokens: Vec = input.into_iter().collect(); + let (range_start, range_end, body_start) = parse_range(&tokens); + + let mut body: Vec = tokens[body_start..].to_vec(); + if body.len() == 1 + && let TokenTree::Group(g) = &body[0] + && g.delimiter() == Delimiter::Brace + { + body = g.stream().into_iter().collect(); + } + + let mut result = Vec::new(); + for i in range_start..=range_end { + result.extend(expand_body(&body, i, range_start, range_end)); + } + result.into_iter().collect() +} + +/// Parse `start .. end =>` or `start ..= end =>` or `count =>` (backward compat). +/// Returns `(start, end_inclusive, body_start_index)`. +fn parse_range(tokens: &[TokenTree]) -> (usize, usize, usize) { + // Find => separator + let arrow_pos = tokens.windows(2).position(|w| { + matches!(&w[0], TokenTree::Punct(p) if p.as_char() == '=') + && matches!(&w[1], TokenTree::Punct(p) if p.as_char() == '>') + }); + + let (arrow_pos, body_start) = match arrow_pos { + Some(p) => (p, p + 2), + None => return (1, 12, 0), // fallback + }; + + let before: Vec<&TokenTree> = tokens[..arrow_pos].iter().collect(); + + // Try to find `..` or `..=` pattern + // `..` is two Punct('.') tokens + let dotdot = before.windows(2).position(|w| { + matches!(w[0], TokenTree::Punct(p) if p.as_char() == '.') + && matches!(w[1], TokenTree::Punct(p) if p.as_char() == '.') + }); + + if let Some(dd) = dotdot { + // Start value: tokens before `..` + let start = parse_usize_tokens(&before[..dd]); + let after_dd = &before[dd + 2..]; + + // Check for `..=` (inclusive range) + let (inclusive, end_tokens) = if after_dd + .first() + .is_some_and(|t| matches!(t, TokenTree::Punct(p) if p.as_char() == '=')) + { + (true, &after_dd[1..]) + } else { + (false, after_dd) + }; + + let end = parse_usize_tokens(end_tokens); + + if inclusive { + (start, end, body_start) + } else { + // Exclusive end: if end >= start, iterate start..end, so end_inclusive = end - 1 + if end > start { + (start, end - 1, body_start) + } else { + (1, 12, body_start) // fallback + } + } + } else { + // No `..` found — fallback to simple count + let count = parse_usize_tokens(&before); + (1, count, body_start) + } +} + +/// Parse a sequence of tokens as a single usize value. +fn parse_usize_tokens(tokens: &[&TokenTree]) -> usize { + let s: String = tokens + .iter() + .map(|t| match t { + TokenTree::Literal(l) => l.to_string(), + TokenTree::Ident(id) => id.to_string(), + _ => String::new(), + }) + .collect::>() + .join("") + .replace(' ', ""); + + s.parse().unwrap_or(12) +} + +/// Walk tokens, replacing: +/// `$` → current +/// `^$` → max +/// `$^` → min +/// `$+` → current + 1 (clamped) +/// `$-` → current - 1 (clamped) +/// `ident$` → ident{current} +/// and expanding `( … )+` / `( … ,)+` / `( … ;)+` groups. +fn expand_body(tokens: &[TokenTree], current: usize, min: usize, max: usize) -> Vec { + let mut out = Vec::new(); + let mut i = 0; + while i < tokens.len() { + // Check for a parenthesized repetition group: ( ... ) sep? + + if let Some(exp) = try_expand_paren_group(tokens, i, current, min, max) { + let (items, consumed) = exp; + out.extend(items); + i += consumed; + continue; + } + + // `^$` — max value + if let TokenTree::Punct(p) = &tokens[i] + && p.as_char() == '^' + && i + 1 < tokens.len() + && let TokenTree::Punct(p2) = &tokens[i + 1] + && p2.as_char() == '$' + { + out.push(TokenTree::Literal(Literal::usize_suffixed(max))); + i += 2; + continue; + } + + // `ident$` / `ident$+` / `ident$-` / `ident$^` + // → {ident}{current} / {ident}{current+1} / {ident}{current-1} / {ident}{min} + if let TokenTree::Ident(id) = &tokens[i] { + if i + 1 < tokens.len() + && let TokenTree::Punct(p) = &tokens[i + 1] + && p.as_char() == '$' + { + // Check for ident$+ / ident$- / ident$^ + if i + 2 < tokens.len() { + match &tokens[i + 2] { + TokenTree::Punct(p2) if p2.as_char() == '+' => { + // ident$+ → {ident}{current+1} + let name = format!("{}{}", id, current + 1); + out.push(TokenTree::Ident(Ident::new(&name, id.span()))); + i += 3; + continue; + } + TokenTree::Punct(p2) if p2.as_char() == '-' => { + // ident$- → {ident}{current-1} + let val = current.saturating_sub(1); + let name = format!("{}{}", id, val); + out.push(TokenTree::Ident(Ident::new(&name, id.span()))); + i += 3; + continue; + } + TokenTree::Punct(p2) if p2.as_char() == '^' => { + let name = format!("{}{}", id, min); + out.push(TokenTree::Ident(Ident::new(&name, id.span()))); + i += 3; + continue; + } + _ => {} + } + } + // ident$ alone → {ident}{current} + let name = format!("{}{}", id, current); + out.push(TokenTree::Ident(Ident::new(&name, id.span()))); + i += 2; + continue; + } + out.push(tokens[i].clone()); + i += 1; + continue; + } + + match &tokens[i] { + TokenTree::Punct(p) if p.as_char() == '$' => { + // lookahead for $^, $+, $- + if i + 1 < tokens.len() { + match &tokens[i + 1] { + TokenTree::Punct(p2) if p2.as_char() == '^' => { + // $^ → min + out.push(TokenTree::Literal(Literal::usize_suffixed(min))); + i += 2; + continue; + } + TokenTree::Punct(p2) if p2.as_char() == '+' => { + // $+ → current + 1 + out.push(TokenTree::Literal(Literal::usize_suffixed(current + 1))); + i += 2; + continue; + } + TokenTree::Punct(p2) if p2.as_char() == '-' => { + // $- → current - 1 + let val = current.saturating_sub(1); + out.push(TokenTree::Literal(Literal::usize_suffixed(val))); + i += 2; + continue; + } + _ => {} + } + } + // `$` alone → current + out.push(TokenTree::Literal(Literal::usize_suffixed(current))); + i += 1; + continue; + } + TokenTree::Group(g) => { + let inner = expand_body_vec(&g.stream(), current, min, max); + out.push(TokenTree::Group(Group::new( + g.delimiter(), + inner.into_iter().collect(), + ))); + } + other => out.push(other.clone()), + } + i += 1; + } + out +} + +fn expand_body_vec(stream: &TokenStream, current: usize, min: usize, max: usize) -> Vec { + let v: Vec = stream.clone().into_iter().collect(); + expand_body(&v, current, min, max) +} + +/// Try to expand a repetition group. +/// +/// New syntax (repetition marker `+` is the LAST token INSIDE the parens): +/// - `(group,+)` — repeat `*` times (current), `,` is the separator +/// - `(group,+)` — repeat `*` times, `,` is the separator +/// - `(group,+)` — repeat `*` times, `;` is the separator +/// - `(group +)` — repeat `*` times, no separator +/// - `(group,+)+` — repeat `*+1` times (with separator) +/// - `(group,+)--` — repeat `*-1` times (with separator) [not yet used] +/// +/// The `*` is the current counter value. An optional `+` or `-` immediately +/// after the closing paren shifts the repeat count up or down by one. +fn try_expand_paren_group( + tokens: &[TokenTree], + i: usize, + current: usize, + _min: usize, + _max: usize, +) -> Option<(Vec, usize)> { + let group = match tokens.get(i)? { + TokenTree::Group(g) if g.delimiter() == Delimiter::Parenthesis => g, + _ => return None, + }; + + let stream: Vec = group.stream().into_iter().collect(); + + // Repetition syntax: the LAST token inside the parens MUST be `+`. + // `(content,+)` — repeat * times, `,` separator + // `(content;+)` — repeat * times, `;` separator + // `(content,+)` — repeat * times, no separator + // `(content,+)+` — repeat *+1 times + // `(content,+)-` — repeat *-1 times + // An optional `+` / `-` right after `)` shifts the count by ±1. + // + // Any `+` tokens inside `content` are treated as regular Rust syntax + // (trait bounds, etc.) — the repetition marker is ONLY the final `+`. + + let last_is_plus = stream + .last() + .is_some_and(|t| matches!(t, TokenTree::Punct(p) if p.as_char() == '+')); + if !last_is_plus { + return None; + } + + // Determine separator and inner content. + let (inner, sep_str): (Vec, &str) = { + if stream.len() >= 2 { + let sep_idx = stream.len() - 2; + match &stream[sep_idx] { + TokenTree::Punct(p) if p.as_char() == ',' => { + // (...,content,+) — inner is everything before the last `,` + let content: Vec = stream[..sep_idx].into(); + (content, ",") + } + TokenTree::Punct(p) if p.as_char() == ';' => { + let content: Vec = stream[..sep_idx].into(); + (content, ";") + } + _ => { + // (content+) — no separator, the `+` is the only special token + let content: Vec = stream[..stream.len() - 1].into(); + (content, "") + } + } + } else { + // (+) — bare repetition marker, empty content + (vec![], "") + } + }; + + // Handle modifier after `)` + let rest = &tokens[i + 1..]; + let modifier: isize = match rest.first() { + Some(TokenTree::Punct(p)) if p.as_char() == '+' => 1, + Some(TokenTree::Punct(p)) if p.as_char() == '-' => -1, + _ => 0, + }; + let consumed = if modifier != 0 { 2 } else { 1 }; + let repeat_count = (current as isize + modifier) as usize; + + let mut out = Vec::new(); + for n in 1..=repeat_count { + if n > 1 && !sep_str.is_empty() { + out.push(TokenTree::Punct(proc_macro::Punct::new( + sep_str.chars().next().unwrap(), + proc_macro::Spacing::Alone, + ))); + } + out.extend(expand_body(&inner, n, 1, repeat_count)); + } + + Some((out, consumed)) +} diff --git a/arg_picker_macros/src/lib.rs b/arg_picker_macros/src/lib.rs new file mode 100644 index 0000000..18cce84 --- /dev/null +++ b/arg_picker_macros/src/lib.rs @@ -0,0 +1,32 @@ +#![doc = include_str!("../README.md")] + +use proc_macro::TokenStream; + +mod arg; +mod internal_repeat; + +/// Core proc-macro: repeats a template body `count` times. +/// +/// Internal call signature: `internal_repeat!(count => { template })` +#[proc_macro] +pub fn internal_repeat(input: TokenStream) -> TokenStream { + internal_repeat::internal_repeat(input) +} + +/// Quick builder for `PickerArg`. +/// +/// # Syntax +/// +/// ```ignore +/// use arg_picker_macros::flag; +/// +/// let basic = arg![name: String]; +/// let with_short_name = arg![name: String, 'n']; +/// let with_short_alias = arg![name: String, 'n', "alias"]; +/// let positional = arg![String]; +/// let positional_with_name = arg![String, 'n', "alias"]; +/// ``` +#[proc_macro] +pub fn arg(input: TokenStream) -> TokenStream { + arg::arg(input) +} diff --git a/mingling/Cargo.toml b/mingling/Cargo.toml index c3f9511..adf900f 100644 --- a/mingling/Cargo.toml +++ b/mingling/Cargo.toml @@ -50,7 +50,7 @@ dispatch_tree = ["mingling_core/dispatch_tree", "mingling_macros/dispatch_tree"] repl = ["mingling_core/repl", "mingling_macros/repl"] comp = ["mingling_core/comp", "mingling_macros/comp"] parser = ["dep:size"] -picker = ["dep:mingling_picker", "mingling_picker/mingling_support"] +picker = ["dep:arg-picker", "arg-picker/mingling_support"] pathf = ["mingling_core/pathf", "mingling_macros/pathf"] structural_renderer = [ @@ -90,6 +90,6 @@ extra_macros = ["mingling_macros/extra_macros"] [dependencies] mingling_core = { workspace = true, optional = true } mingling_macros = { workspace = true, optional = true } -mingling_picker = { workspace = true, optional = true } +arg-picker = { workspace = true, optional = true } serde = { workspace = true, optional = true } size = { version = "0.5", optional = true } diff --git a/mingling/src/lib.rs b/mingling/src/lib.rs index ba87f85..44812d6 100644 --- a/mingling/src/lib.rs +++ b/mingling/src/lib.rs @@ -86,10 +86,10 @@ pub mod parser; /// `Mingling` argument parser (Picker2) #[cfg(feature = "picker")] pub mod picker { - pub use mingling_picker::*; + pub use arg_picker::*; pub mod parselib { - pub use mingling_picker::parselib::*; + pub use arg_picker::parselib::*; } } @@ -107,6 +107,9 @@ pub mod picker { #[allow(unused_imports)] #[cfg(feature = "macros")] pub mod macros { + /// New Parser provided by the `picker` feature + #[cfg(feature = "picker")] + pub use arg_picker::macros::*; /// `#[chain]` - Used to generate a struct implementing the `Chain` trait via a method pub use mingling_macros::chain; /// `#[completion(EntryType)]` - Used to generate completion entry @@ -177,9 +180,6 @@ pub mod macros { /// `suggest_enum!(EnumNames)` - Used to generate enum suggestions #[cfg(feature = "comp")] pub use mingling_macros::suggest_enum; - /// New Parser provided by the `picker` feature - #[cfg(feature = "picker")] - pub use mingling_picker::macros::*; } /// derive macro `EnumTag` @@ -286,7 +286,7 @@ pub mod prelude { pub use crate::parser::AsPicker; #[cfg(feature = "picker")] - pub use mingling_picker::prelude::*; + pub use arg_picker::prelude::*; /// Used to enable the `writeln!` macro for `RenderResult` #[cfg(feature = "core")] diff --git a/mingling_picker/Cargo.toml b/mingling_picker/Cargo.toml deleted file mode 100644 index ddab33c..0000000 --- a/mingling_picker/Cargo.toml +++ /dev/null @@ -1,17 +0,0 @@ -[package] -name = "mingling_picker" -version.workspace = true -edition.workspace = true -license.workspace = true -repository.workspace = true -authors = ["Weicao-CatilGrass"] -readme = "README.md" -description = "Mingling's lightweight argument parser" - -[features] -mingling_support = ["dep:mingling_core", "mingling_picker_macros/mingling_support"] - -[dependencies] -mingling_core = { workspace = true, optional = true } -mingling_picker_macros.workspace = true -just_fmt.workspace = true diff --git a/mingling_picker/README.md b/mingling_picker/README.md deleted file mode 100644 index db67825..0000000 --- a/mingling_picker/README.md +++ /dev/null @@ -1,47 +0,0 @@ -# Mingling Picker - -A command-line argument parser for [Mingling](https://github.com/mingling-rs/mingling), enabled by the `mingling/picker` feature. - -```toml -[dependencies.mingling] -version = "0.3.0" -features = [ - "picker" -] -``` - -Of course, you can also use it as a standalone crate by replacing `mingling::picker` with `mingling_picker`: - -```toml -[dependencies] -mingling_picker = "0.3.0" -``` - -## Chained Argument Parser - -Provides a clean chained-call API for declaring arguments to parse: - -```rust -use mingling_picker::prelude::*; - -let args: Vec<&str> = vec!["--name", "Bob", "--age", "24"]; - -let (name, age) = args - .pick(&arg![name: String]) - .or(|| "Alice".to_string()) - .pick(&arg![age: i32]) - .or(|| 24) - .post(|num| num.clamp(0, 120)) - .unwrap(); - -assert_eq!(name, "Bob".to_string()); -assert_eq!(age, 24); -``` - -## Parsing Function Library - -Provides a pure function library `parselib` for analyzing the structure of command-line arguments. - -```rust -use mingling_picker::parselib::*; -``` diff --git a/mingling_picker/src/arg.rs b/mingling_picker/src/arg.rs deleted file mode 100644 index 78ad539..0000000 --- a/mingling_picker/src/arg.rs +++ /dev/null @@ -1,299 +0,0 @@ -use crate::Pickable; -use std::marker::PhantomData; - -/// 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. -/// -/// - `_type`: PhantomData to hold the type parameter. -#[derive(Default, Clone, Copy)] -pub struct PickerArg<'a, Type> -where - Type: Pickable<'a>, -{ - /// Full name, may include variant names (aliases), e.g., `["config", "cfg"]`. - pub full: &'a [&'a str], - - /// Short name, e.g., `'c'`. - pub short: Option, - - /// Whether the parameter is positional (no flag, matched by position). - pub positional: bool, - - /// PhantomData to hold the type parameter. - pub internal_type: PhantomData, -} - -impl<'a, Type> From<&'a PickerArg<'a, Type>> for PickerArg<'a, Type> -where - Type: Pickable<'a>, -{ - fn from(value: &'a PickerArg<'a, Type>) -> Self { - PickerArg { - full: value.full, - short: value.short, - positional: value.positional, - internal_type: PhantomData, - } - } -} - -impl<'a, Type> PickerArg<'a, Type> -where - Type: Pickable<'a>, -{ - /// Creates a new `PickerArg` with the provided parameters. - pub fn new(full: &'a [&'a str], short: Option, positional: bool) -> Self { - Self { - full, - short, - positional, - internal_type: PhantomData, - } - } - - /// 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 - } -} - -/// Describes the attribute (behavior) of a command-line parameter. -/// -/// The ordering reflects parse priority (higher = parsed first): -/// `PositionalMulti < Positional < Flag < Single < Multi` -#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] -pub enum PickerArgAttr { - /// Positional argument that accepts multiple values (e.g., multiple input files). - PositionalMulti, - - /// Positional argument matched by its position (e.g., an input file). - #[default] - Positional, - - /// Boolean flag with no associated value (e.g., `--verbose`). - Flag, - - /// Accepts a single value (e.g., `--name Alice`). - Single, - - /// Accepts multiple values (e.g., `--file a.txt --file b.txt`). - Multi, -} - -impl PickerArgAttr { - /// Determines if the given `PickerArg` represents a positional parameter. - /// - /// If the flag is positional (determined by `flag.is_positional()`), returns - /// `PickerArgAttr::Positional`. Otherwise, invokes the `other` closure to - /// produce and return a `PickerArgAttr`. - /// - /// # Parameters - /// - /// - `flag`: A reference to the [`PickerArg`] to evaluate. - /// - `other`: A closure that returns a [`PickerArgAttr`] when the flag is - /// **not** positional. - #[inline(always)] - pub fn positional_or_else<'a, T>( - flag: &PickerArg<'a, T>, - other: fn() -> PickerArgAttr, - ) -> PickerArgAttr - where - T: Pickable<'a>, - { - if flag.is_positional() { - PickerArgAttr::Positional - } else { - other() - } - } - - /// Determines if the given `PickerArg` represents a positional parameter and returns - /// `PickerArgAttr::Positional` if so. Otherwise, returns the provided `default` attribute. - /// - /// # Parameters - /// - /// - `flag`: A reference to the [`PickerArg`] to evaluate. - /// - `default`: The [`PickerArgAttr`] to return if the flag is not positional. - #[inline(always)] - pub fn positional_or<'a, T>(flag: &PickerArg<'a, T>, default: PickerArgAttr) -> PickerArgAttr - where - T: Pickable<'a>, - { - if flag.is_positional() { - PickerArgAttr::Positional - } else { - default - } - } - - /// Determines if the given `PickerArg` represents a positional parameter and returns - /// `PickerArgAttr::Positional` if so. Otherwise, returns `PickerArgAttr::Single`. - /// - /// # Parameters - /// - /// - `flag`: A reference to the [`PickerArg`] to evaluate. - #[inline(always)] - pub fn positional_or_single<'a, T>(flag: &PickerArg<'a, T>) -> PickerArgAttr - where - T: Pickable<'a>, - { - if flag.is_positional() { - PickerArgAttr::Positional - } else { - PickerArgAttr::Single - } - } - - /// Determines if the given `PickerArg` represents a positional parameter and returns - /// `PickerArgAttr::PositionalMulti` if so. Otherwise, returns `PickerArgAttr::Multi`. - /// - /// # Parameters - /// - /// - `flag`: A reference to the [`PickerArg`] to evaluate. - #[inline(always)] - pub fn positional_or_multi<'a, T>(flag: &PickerArg<'a, T>) -> PickerArgAttr - where - T: Pickable<'a>, - { - if flag.is_positional() { - PickerArgAttr::PositionalMulti - } else { - PickerArgAttr::Multi - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_picker_flag_attr_ordering() { - // Multi > Single > Flag > Positional > PositionalMulti - assert!(PickerArgAttr::Multi > PickerArgAttr::Single); - assert!(PickerArgAttr::Multi > PickerArgAttr::Flag); - assert!(PickerArgAttr::Multi > PickerArgAttr::Positional); - assert!(PickerArgAttr::Multi > PickerArgAttr::PositionalMulti); - - assert!(PickerArgAttr::Single > PickerArgAttr::Flag); - assert!(PickerArgAttr::Single > PickerArgAttr::Positional); - assert!(PickerArgAttr::Single > PickerArgAttr::PositionalMulti); - - assert!(PickerArgAttr::Flag > PickerArgAttr::Positional); - assert!(PickerArgAttr::Flag > PickerArgAttr::PositionalMulti); - - assert!(PickerArgAttr::Positional > PickerArgAttr::PositionalMulti); - - // PartialOrd - assert!(PickerArgAttr::Multi >= PickerArgAttr::Single); - assert!(PickerArgAttr::Single >= PickerArgAttr::Flag); - assert!(PickerArgAttr::Flag >= PickerArgAttr::Positional); - assert!(PickerArgAttr::Positional >= PickerArgAttr::PositionalMulti); - - assert!(PickerArgAttr::PositionalMulti < PickerArgAttr::Positional); - assert!(PickerArgAttr::Positional < PickerArgAttr::Flag); - assert!(PickerArgAttr::Flag < PickerArgAttr::Single); - assert!(PickerArgAttr::Single < PickerArgAttr::Multi); - } - - #[test] - fn test_picker_flag_attr_sorting() { - // Sort - let mut values = vec![ - PickerArgAttr::Flag, - PickerArgAttr::Single, - PickerArgAttr::Positional, - PickerArgAttr::Multi, - PickerArgAttr::PositionalMulti, - ]; - values.sort(); - assert_eq!( - values, - vec![ - PickerArgAttr::PositionalMulti, - PickerArgAttr::Positional, - PickerArgAttr::Flag, - PickerArgAttr::Single, - PickerArgAttr::Multi, - ] - ); - } -} diff --git a/mingling_picker/src/builtin.rs b/mingling_picker/src/builtin.rs deleted file mode 100644 index e855b08..0000000 --- a/mingling_picker/src/builtin.rs +++ /dev/null @@ -1,4 +0,0 @@ -mod pick_bool; -mod pick_flag; -mod pick_numbers; -mod pick_string; diff --git a/mingling_picker/src/builtin/pick_bool.rs b/mingling_picker/src/builtin/pick_bool.rs deleted file mode 100644 index ccc4424..0000000 --- a/mingling_picker/src/builtin/pick_bool.rs +++ /dev/null @@ -1,22 +0,0 @@ -use crate::parselib::{FlagMatcher, Matcher}; -use crate::pickable_needed::*; - -impl<'a> Pickable<'a> for bool { - fn get_attr(_: &'a PickerArg<'a, Self>) -> PickerArgAttr { - PickerArgAttr::Flag - } - - fn tag(ctx: TagPhaseContext) -> Vec { - FlagMatcher::match_all(ctx.into()) - } - - fn pick(raw_strs: &[&str]) -> PickerArgResult { - if raw_strs.is_empty() { - // No matching flag found — signal NotFound so the fallback chain - // (default → route) gets a chance to run. - PickerArgResult::NotFound - } else { - PickerArgResult::Parsed(true) - } - } -} diff --git a/mingling_picker/src/builtin/pick_flag.rs b/mingling_picker/src/builtin/pick_flag.rs deleted file mode 100644 index b642a9a..0000000 --- a/mingling_picker/src/builtin/pick_flag.rs +++ /dev/null @@ -1,21 +0,0 @@ -use crate::parselib::{FlagMatcher, Matcher}; -use crate::pickable_needed::*; -use crate::value::Flag; - -impl<'a> Pickable<'a> for Flag { - fn get_attr(_: &'a PickerArg<'a, Self>) -> PickerArgAttr { - PickerArgAttr::Flag - } - - fn tag(ctx: TagPhaseContext) -> Vec { - FlagMatcher::match_all(ctx.into()) - } - - fn pick(raw_strs: &[&str]) -> PickerArgResult { - if raw_strs.is_empty() { - PickerArgResult::Parsed(Flag::Inactive) - } else { - PickerArgResult::Parsed(Flag::Active) - } - } -} diff --git a/mingling_picker/src/builtin/pick_numbers.rs b/mingling_picker/src/builtin/pick_numbers.rs deleted file mode 100644 index a5ab0a9..0000000 --- a/mingling_picker/src/builtin/pick_numbers.rs +++ /dev/null @@ -1,80 +0,0 @@ -use crate::{BoundaryCheck, SinglePickable, pickable_needed::*}; - -macro_rules! impl_single_pickable_num { - ($($t:ty),+ $(,)?) => { - $(impl SinglePickable for $t { - fn pick_single(str: Option<&str>) -> PickerArgResult { - match str { - Some(s) => s.parse::<$t>().map(PickerArgResult::Parsed).unwrap_or(PickerArgResult::NotFound), - None => PickerArgResult::NotFound, - } - } - })+ - }; -} - -/// Returns `true` if `raw` looks like an integer (digits, optional `+`/`-` prefix, -/// no decimal point or exponent). -fn is_int_like(raw: &str) -> bool { - let s = raw.trim(); - let bytes = s.as_bytes(); - if bytes.is_empty() { - return false; - } - let mut i = 0; - if bytes[0] == b'-' || bytes[0] == b'+' { - i = 1; - } - if i >= bytes.len() { - return false; - } - for &b in &bytes[i..] { - if !b.is_ascii_digit() { - return false; - } - } - true -} - -/// Returns `true` if `raw` looks like a float (contains `.`, `e`, or `E`). -fn is_float_like(raw: &str) -> bool { - let s = raw.trim(); - s.contains('.') || s.contains('e') || s.contains('E') -} - -impl_single_pickable_num! { - i8, i16, i32, i64, i128, isize, - u8, u16, u32, u64, u128, usize, - f32, f64, -} - -// Integer boundary: only accept strings that look like integers. -// Float-like strings trigger a boundary. -macro_rules! impl_boundary_check_int { - ($($t:ty),+ $(,)?) => { - $(impl BoundaryCheck for $t { - fn check_boundary(raw: &str) -> bool { - !is_int_like(raw) - } - })+ - }; -} - -impl_boundary_check_int! { - i8, i16, i32, i64, i128, isize, - u8, u16, u32, u64, u128, usize, -} - -// Float boundary: only accept strings that look like floats. -// Integer-like strings trigger a boundary. -impl BoundaryCheck for f32 { - fn check_boundary(raw: &str) -> bool { - !is_float_like(raw) || raw.parse::().is_err() - } -} - -impl BoundaryCheck for f64 { - fn check_boundary(raw: &str) -> bool { - !is_float_like(raw) || raw.parse::().is_err() - } -} diff --git a/mingling_picker/src/builtin/pick_string.rs b/mingling_picker/src/builtin/pick_string.rs deleted file mode 100644 index c96f667..0000000 --- a/mingling_picker/src/builtin/pick_string.rs +++ /dev/null @@ -1,11 +0,0 @@ -use crate::PickerArgResult::NotFound; -use crate::{SinglePickable, pickable_needed::*}; - -impl SinglePickable for String { - fn pick_single(str: Option<&str>) -> PickerArgResult { - match str { - Some(str) => PickerArgResult::Parsed(str.to_string()), - None => NotFound, - } - } -} diff --git a/mingling_picker/src/corebind.rs b/mingling_picker/src/corebind.rs deleted file mode 100644 index 3581871..0000000 --- a/mingling_picker/src/corebind.rs +++ /dev/null @@ -1,2 +0,0 @@ -mod entry_picker; -pub use entry_picker::*; diff --git a/mingling_picker/src/corebind/entry_picker.rs b/mingling_picker/src/corebind/entry_picker.rs deleted file mode 100644 index 69bc4d8..0000000 --- a/mingling_picker/src/corebind/entry_picker.rs +++ /dev/null @@ -1,131 +0,0 @@ -use std::marker::PhantomData; - -use mingling_core::{ChainProcess, Groupped, ProgramCollect}; - -use crate::{Pickable, Picker, PickerArg, PickerArgs, PickerPattern1}; - -/// Trait for converting Mingling entry types (types that implement `Groupped` and `Into>`) -/// into [`Picker`] instances for a given route type `R`. -/// -/// This trait provides a bridge between entry definitions created with the `mingling` framework -/// and the picker argument system used for CLI argument parsing and routing. -/// -/// # Type Parameters -/// -/// * `'a` — The lifetime of the picker and its references to argument definitions. -/// * `This` — The program type used for dispatching runtime routes; must implement [`ProgramCollect`]. -/// * `Route` — The route type used for dispatching; must implement [`Groupped`]. -pub trait EntryPicker<'a, This> { - /// Converts `self` into a [`Picker`] for the given route type `Route`. - fn to_picker(self) -> Picker<'a, ChainProcess>; - - /// Starts building a picker pattern with the first argument. - /// - /// Returns a [`PickerPattern1`] that can be further chained with additional - /// arguments, defaults, routes, and post-processing. - /// - /// # Type Parameters - /// - /// * `Next` — The nominal type of the first argument; must implement [`Pickable`]. - /// - /// # Parameters - /// - /// * `arg` — The argument definition, typically obtained from [`crate::arg`]. - fn pick( - self, - arg: impl Into<&'a PickerArg<'a, Next>>, - ) -> PickerPattern1<'a, Next, ChainProcess> - where - Self: Sized, - Next: Pickable<'a> + Default + Sized, - { - let picker = Self::to_picker(self); - Picker::build_pattern1(picker.args, arg.into(), None) - } - - /// Starts building a picker pattern with the first argument, using a default value provider. - /// - /// This is a shorthand for calling `.pick(arg).or(func)`. - /// - /// # Type Parameters - /// - /// * `Next` — The nominal type of the first argument; must implement [`Pickable`]. - /// - /// # Parameters - /// - /// * `arg` — The argument definition, typically obtained from [`crate::arg`]. - /// * `func` — A closure that provides a default value if the arg is not provided by the user. - fn pick_or( - self, - arg: impl Into<&'a PickerArg<'a, Next>>, - func: F, - ) -> PickerPattern1<'a, Next, ChainProcess> - where - Self: Sized, - Next: Pickable<'a> + Default + Sized, - F: FnMut() -> Next + 'static, - { - self.pick(arg).or(func) - } - - /// Starts building a picker pattern with the first argument, using a default value. - /// - /// This is a shorthand for calling `.pick(arg).or_default()`. - /// - /// # Type Parameters - /// - /// * `Next` — The nominal type of the first argument; must implement [`Pickable`] and [`Default`]. - /// - /// # Parameters - /// - /// * `arg` — The argument definition, typically obtained from [`crate::arg`]. - fn pick_or_default( - self, - arg: impl Into<&'a PickerArg<'a, Next>>, - ) -> PickerPattern1<'a, Next, ChainProcess> - where - Self: Sized, - Next: Pickable<'a> + Default + Sized, - { - self.pick(arg).or_default() - } - - /// Starts building a picker pattern with the first argument, using a route if the arg is not provided. - /// - /// This is a shorthand for calling `.pick(arg).or_route(func)`. - /// - /// # Type Parameters - /// - /// * `Next` — The nominal type of the first argument; must implement [`Pickable`]. - /// - /// # Parameters - /// - /// * `arg` — The argument definition, typically obtained from [`crate::arg`]. - /// * `func` — A closure that produces a route value if the arg is not provided by the user. - fn pick_or_route( - self, - arg: impl Into<&'a PickerArg<'a, Next>>, - func: F, - ) -> PickerPattern1<'a, Next, ChainProcess> - where - Self: Sized, - Next: Pickable<'a> + Default + Sized, - F: FnMut() -> ChainProcess + 'static, - { - self.pick(arg).or_route(func) - } -} - -impl<'a, This, Bind> EntryPicker<'a, This> for Bind -where - This: ProgramCollect, - Bind: Groupped + Into>, -{ - fn to_picker(self) -> Picker<'a, ChainProcess> { - let args = self.into(); - Picker { - route_phantom: PhantomData, - args: PickerArgs::Owned(args), - } - } -} diff --git a/mingling_picker/src/infos.rs b/mingling_picker/src/infos.rs deleted file mode 100644 index d2a0fce..0000000 --- a/mingling_picker/src/infos.rs +++ /dev/null @@ -1,446 +0,0 @@ -use crate::{Pickable, PickerArg}; - -/// Represents the result of parsing or looking up a value. -/// -/// This enum is generic over the type being parsed. It models three possible outcomes: -/// - [`Unparsed`](PickerArgResult::Unparsed): The value has not yet been parsed (default). -/// - [`Parsed`](PickerArgResult::Parsed): The value was successfully parsed into `Type`. -/// - [`NotFound`](PickerArgResult::NotFound): The requested value could not be found. -#[derive(Default)] -pub enum PickerArgResult { - /// 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, -} - -impl From> for PickerArgResult { - /// Converts a `Result` into a `PickerArgResult`. - /// - /// - `Ok(value)` maps to [`Parsed(value)`](PickerArgResult::Parsed). - /// - `Err(_)` maps to [`NotFound`](PickerArgResult::NotFound). - fn from(result: Result) -> Self { - match result { - Ok(value) => PickerArgResult::Parsed(value), - Err(_) => PickerArgResult::NotFound, - } - } -} - -impl From> for PickerArgResult { - /// Converts an `Option` into a `PickerArgResult`. - /// - /// - `Some(value)` maps to [`Parsed(value)`](PickerArgResult::Parsed). - /// - `None` maps to [`NotFound`](PickerArgResult::NotFound). - fn from(option: Option) -> Self { - match option { - Some(value) => PickerArgResult::Parsed(value), - None => PickerArgResult::NotFound, - } - } -} - -impl PickerArgResult { - /// Returns `true` if the result is [`Parsed`](PickerArgResult::Parsed). - /// - /// # Examples - /// - /// ``` - /// use mingling_picker::PickerArgResult; - /// - /// let result: PickerArgResult = PickerArgResult::Parsed(42); - /// assert!(result.is_parsed()); - /// - /// let result: PickerArgResult = PickerArgResult::NotFound; - /// assert!(!result.is_parsed()); - /// ``` - pub fn is_parsed(&self) -> bool { - matches!(self, PickerArgResult::Parsed(_)) - } - - /// Returns `true` if the result is [`Parsed`](PickerArgResult::Parsed) or [`NotFound`](PickerArgResult::NotFound). - /// i.e., the value exists (was either found or not yet parsed). - /// Typically indicates the value was "found" in some sense. - /// - /// # Examples - /// - /// ``` - /// use mingling_picker::PickerArgResult; - /// - /// let result: PickerArgResult = PickerArgResult::Parsed(42); - /// assert!(result.is_found()); - /// - /// let result: PickerArgResult = PickerArgResult::NotFound; - /// assert!(result.is_found()); - /// ``` - pub fn is_found(&self) -> bool { - matches!(self, PickerArgResult::Parsed(_) | PickerArgResult::NotFound) - } - - /// Returns `true` if the result is [`Unparsed`](PickerArgResult::Unparsed) or [`NotFound`](PickerArgResult::NotFound). - /// - /// # Examples - /// - /// ``` - /// use mingling_picker::PickerArgResult; - /// - /// let result: PickerArgResult = PickerArgResult::Unparsed; - /// assert!(result.is_err()); - /// - /// let result: PickerArgResult = PickerArgResult::Parsed(10); - /// assert!(!result.is_err()); - /// ``` - pub fn is_err(&self) -> bool { - !matches!(self, PickerArgResult::Parsed(_)) - } - - /// Returns `Some(&Type)` if [`Parsed`](PickerArgResult::Parsed), otherwise `None`. - /// - /// # Examples - /// - /// ``` - /// use mingling_picker::PickerArgResult; - /// - /// let result: PickerArgResult = PickerArgResult::Parsed(42); - /// assert_eq!(result.parsed(), Some(&42)); - /// - /// let result: PickerArgResult = PickerArgResult::NotFound; - /// assert_eq!(result.parsed(), None); - /// ``` - pub fn parsed(&self) -> Option<&Type> { - if let PickerArgResult::Parsed(value) = self { - Some(value) - } else { - None - } - } - - /// Returns the contained [`Parsed`](PickerArgResult::Parsed) value or panics with a given message. - /// - /// # Panics - /// Panics if the value is not [`Parsed`](PickerArgResult::Parsed), with a message including the provided `msg`. - /// - /// # Examples - /// - /// ```should_panic - /// use mingling_picker::PickerArgResult; - /// - /// let result: PickerArgResult = PickerArgResult::NotFound; - /// result.expect("expected a parsed value"); - /// ``` - pub fn expect(self, msg: &str) -> Type { - match self { - PickerArgResult::Parsed(value) => value, - _ => panic!("{}", msg), - } - } - - /// Returns the contained [`Parsed`](PickerArgResult::Parsed) value or panics. - /// - /// # Panics - /// Panics if the value is not [`Parsed`](PickerArgResult::Parsed). - /// - /// # Examples - /// - /// ``` - /// use mingling_picker::PickerArgResult; - /// - /// let result: PickerArgResult = PickerArgResult::Parsed(42); - /// assert_eq!(result.unwrap(), 42); - /// ``` - /// - /// ```should_panic - /// use mingling_picker::PickerArgResult; - /// - /// let result: PickerArgResult = PickerArgResult::NotFound; - /// result.unwrap(); - /// ``` - pub fn unwrap(self) -> Type { - match self { - PickerArgResult::Parsed(value) => value, - PickerArgResult::Unparsed => { - panic!("called `PickerArgResult::unwrap()` on an `Unparsed` value") - } - PickerArgResult::NotFound => { - panic!("called `PickerArgResult::unwrap()` on a `NotFound` value") - } - } - } - - /// Returns the contained [`Parsed`](PickerArgResult::Parsed) value or a provided `default`. - /// - /// # Examples - /// - /// ``` - /// use mingling_picker::PickerArgResult; - /// - /// let result: PickerArgResult = PickerArgResult::Parsed(42); - /// assert_eq!(result.unwrap_or(0), 42); - /// - /// let result: PickerArgResult = PickerArgResult::NotFound; - /// assert_eq!(result.unwrap_or(0), 0); - /// ``` - pub fn unwrap_or(self, default: Type) -> Type { - match self { - PickerArgResult::Parsed(value) => value, - _ => default, - } - } - - /// Returns the contained [`Parsed`](PickerArgResult::Parsed) value or computes it from a closure. - /// - /// # Examples - /// - /// ``` - /// use mingling_picker::PickerArgResult; - /// - /// let result: PickerArgResult = PickerArgResult::Parsed(42); - /// assert_eq!(result.unwrap_or_else(|| 0), 42); - /// - /// let result: PickerArgResult = PickerArgResult::NotFound; - /// assert_eq!(result.unwrap_or_else(|| 0), 0); - /// ``` - pub fn unwrap_or_else Type>(self, f: F) -> Type { - match self { - PickerArgResult::Parsed(value) => value, - _ => f(), - } - } - - /// Returns the contained [`Parsed`](PickerArgResult::Parsed) value or the default value of `Type`. - /// - /// # Examples - /// - /// ``` - /// use mingling_picker::PickerArgResult; - /// - /// let result: PickerArgResult = PickerArgResult::Parsed(42); - /// assert_eq!(result.unwrap_or_default(), 42); - /// - /// let result: PickerArgResult = PickerArgResult::NotFound; - /// assert_eq!(result.unwrap_or_default(), 0); - /// ``` - pub fn unwrap_or_default(self) -> Type - where - Type: Default, - { - match self { - PickerArgResult::Parsed(value) => value, - _ => Type::default(), - } - } - - /// Converts `PickerArgResult` into `Option`. - /// - /// Returns `Some(Type)` if [`Parsed`](PickerArgResult::Parsed), otherwise `None`. - /// - /// # Examples - /// - /// ``` - /// use mingling_picker::PickerArgResult; - /// - /// let result: PickerArgResult = PickerArgResult::Parsed(42); - /// assert_eq!(result.to_option(), Some(42)); - /// - /// let result: PickerArgResult = PickerArgResult::NotFound; - /// assert_eq!(result.to_option(), None); - /// - /// let result: PickerArgResult = PickerArgResult::Unparsed; - /// assert_eq!(result.to_option(), None); - /// ``` - pub fn to_option(self) -> Option { - match self { - PickerArgResult::Parsed(value) => Some(value), - _ => None, - } - } -} - -/// Represents metadata about a command-line argument or flag. -/// -/// This struct stores all relevant information about a tag/argument that can be used -/// for parsing command-line inputs. It includes the short form (e.g., `-n`), long form -/// (e.g., `--name`), aliases, and various flags that control parsing behavior. -pub struct PickerArgInfo<'a> { - /// The short form of the tag, e.g. `'n'` for `-n`. - pub short: Option, - /// The long form of the tag, e.g. `"name"` for `--name`. - pub long: Option<&'a str>, - /// Alternative names for the tag, e.g. `["-N", "--nickname"]`. - pub alias: Option>, - /// Whether this tag is a positional argument (no `-` or `--` prefix). - pub positional: bool, - /// Whether this tag is optional or required. - pub optional: bool, - /// Whether this tag can accept multiple values. - pub multi: bool, - /// Whether this tag participates in parsing after a `--` separator. - pub is_flag: bool, -} - -impl<'a, T> From> for PickerArgInfo<'a> -where - T: Pickable<'a>, -{ - fn from(value: PickerArg<'a, T>) -> Self { - let (long, alias) = match value.full.len() { - 0 => (None, None), - _ => { - let long = Some(value.full[0]); - let alias = if value.full.len() > 1 { - Some(value.full[1..].to_vec()) - } else { - None - }; - (long, alias) - } - }; - - Self { - short: value.short, - long, - alias, - positional: value.positional, - optional: false, - multi: false, - is_flag: false, - } - } -} - -impl<'a, T: Pickable<'a>> From<&'a PickerArg<'a, T>> for PickerArgInfo<'a> { - fn from(value: &'a PickerArg<'a, T>) -> Self { - let (long, alias) = match value.full.len() { - 0 => (None, None), - _ => { - let long = Some(value.full[0]); - let alias = if value.full.len() > 1 { - Some(value.full[1..].to_vec()) - } else { - None - }; - (long, alias) - } - }; - - Self { - short: value.short, - long, - alias, - positional: value.positional, - optional: false, - multi: false, - is_flag: false, - } - } -} - -impl<'a> PickerArgInfo<'a> { - /// Create a new `PickerTag` with default values. - pub fn new() -> Self { - Self { - short: None, - long: None, - alias: None, - positional: false, - optional: false, - multi: false, - is_flag: false, - } - } - - /// Set the short flag (e.g., `'n'` for `-n`). - pub fn with_short(mut self, short: char) -> Self { - self.short = Some(short); - self - } - - /// Set the long flag (e.g., `"name"` for `--name`). - pub fn with_long(mut self, long: &'a str) -> Self { - self.long = Some(long); - self - } - - /// Set aliases for the tag. - pub fn with_alias(mut self, alias: Vec<&'a str>) -> Self { - self.alias = Some(alias); - self - } - - /// Mark the tag as positional. - pub fn with_positional(mut self, positional: bool) -> Self { - self.positional = positional; - self - } - - /// Mark the tag as optional. - pub fn with_optional(mut self, optional: bool) -> Self { - self.optional = optional; - self - } - - /// Mark the tag as multi-value. - pub fn with_multi(mut self, multi: bool) -> Self { - self.multi = multi; - self - } - - /// Mark the tag as a flag that participates in parsing after `--`. - pub fn with_is_flag(mut self, is_flag: bool) -> Self { - self.is_flag = is_flag; - self - } - - /// Set the short flag (e.g., `'n'` for `-n`). - pub fn set_short(&mut self, short: char) -> &mut Self { - self.short = Some(short); - self - } - - /// Set the long flag (e.g., `"name"` for `--name`). - pub fn set_long(&mut self, long: &'a str) -> &mut Self { - self.long = Some(long); - self - } - - /// Set aliases for the tag. - pub fn set_alias(&mut self, alias: Vec<&'a str>) -> &mut Self { - self.alias = Some(alias); - self - } - - /// Set whether this tag is positional. - pub fn set_positional(&mut self, positional: bool) -> &mut Self { - self.positional = positional; - self - } - - /// Set whether this tag is optional. - pub fn set_optional(&mut self, optional: bool) -> &mut Self { - self.optional = optional; - self - } - - /// Set whether this tag accepts multiple values. - pub fn set_multi(&mut self, multi: bool) -> &mut Self { - self.multi = multi; - self - } - - /// Set whether this tag participates in parsing after a `--` separator. - pub fn set_is_flag(&mut self, is_flag: bool) -> &mut Self { - self.is_flag = is_flag; - self - } -} - -impl<'a> Default for PickerArgInfo<'a> { - fn default() -> Self { - Self::new() - } -} diff --git a/mingling_picker/src/lib.rs b/mingling_picker/src/lib.rs deleted file mode 100644 index deb266e..0000000 --- a/mingling_picker/src/lib.rs +++ /dev/null @@ -1,62 +0,0 @@ -#![doc = include_str!("../README.md")] - -mod builtin; - -mod picker; -pub use picker::*; - -mod pickable; -pub use pickable::*; - -mod arg; -pub use arg::*; - -mod infos; -pub use infos::*; - -/// Provides the specific parsing logic for command-line arguments and common utilities, -/// as well as customization of command-line argument styles. -pub mod parselib; - -/// Parser-provided parseable command-line types -pub mod value; - -/// The prelude module, which re-exports the most commonly used traits and types. -/// -/// This module is intended to be imported with a wildcard import: -/// -/// ``` -/// use mingling_picker::prelude::*; -/// ``` -pub mod prelude { - pub use crate::macros::arg; - - #[cfg(not(feature = "mingling_support"))] - pub use crate::IntoPicker; - - #[cfg(feature = "mingling_support")] - pub use crate::corebind::EntryPicker; -} - -/// Re-export of the `mingling_picker_macros` crate -pub mod macros { - pub use mingling_picker_macros::arg; -} - -/// Provides the types necessary for implementing the `Pickable` trait -pub mod pickable_needed { - pub use crate::{Pickable, PickerArg, PickerArgAttr, PickerArgResult, TagPhaseContext}; -} - -/// Provides the types necessary for implementing the `Matcher` trait -pub mod matcher_needed { - pub use crate::PickerArgInfo; - pub use crate::parselib::{MaskedArg, Matcher, ParserStyle}; -} - -#[cfg(feature = "mingling_support")] -mod corebind; - -#[allow(unused_imports)] -#[cfg(feature = "mingling_support")] -pub use corebind::*; diff --git a/mingling_picker/src/parselib.rs b/mingling_picker/src/parselib.rs deleted file mode 100644 index 7fbd606..0000000 --- a/mingling_picker/src/parselib.rs +++ /dev/null @@ -1,144 +0,0 @@ -mod flag_matcher; -pub use flag_matcher::*; - -mod arg_matcher; -pub use arg_matcher::*; - -mod multi_arg_matcher; -pub use multi_arg_matcher::*; - -mod pos_matcher; -pub use pos_matcher::*; - -mod single_matcher; -pub use single_matcher::*; - -mod style; -pub use style::*; - -mod utils; -pub use utils::*; - -use crate::{PickerArgInfo, PickerArgs}; - -/// Represents a single argument with its original raw string and index. -/// -/// This is used during pattern matching to provide context about -/// which argument is being processed. -#[derive(Clone, Copy, PartialEq, Eq)] -pub struct MaskedArg<'a> { - /// The raw string value of the argument. - pub raw: &'a str, - /// The original index of the argument in the full argument list. - pub raw_idx: usize, -} - -/// Trait for defining matching logic against masked arguments. -/// -/// Implementors can define custom strategies for matching one or all -/// arguments that pass through a mask filter. -pub trait Matcher { - /// Called when only one match is needed. - /// - /// Returns the index of the first matched argument, or `None` if no match. - fn on_match_one( - args: &[MaskedArg], - style: &ParserStyle, - arg_info: &PickerArgInfo, - ) -> Option; - - /// Called when all matches are needed. - /// - /// Returns a vector of indices of all matched arguments. - fn on_match_all( - args: &[MaskedArg], - style: &ParserStyle, - arg_info: &PickerArgInfo, - ) -> Vec; - - /// Convenience method that builds masked arguments from `PickerArgs` and a mask, - /// then calls `on_match_one`. - fn match_one<'a>(ctx: MatcherContext<'a>) -> Option { - let masked_args = build_masked_args(ctx.args, ctx.mask); - Self::on_match_one(masked_args.as_slice(), ctx.style, ctx.arg_info) - } - - /// Convenience method that builds masked arguments from `PickerArgs` and a mask, - /// then calls `on_match_all`. - fn match_all<'a>(ctx: MatcherContext<'a>) -> Vec { - let masked_args = build_masked_args(ctx.args, ctx.mask); - Self::on_match_all(masked_args.as_slice(), ctx.style, ctx.arg_info) - } -} - -/// Context for matcher operations -/// -/// This struct bundles together the key pieces of data needed during matching: -/// - `args`: The full set of parsed arguments. -/// - `mask`: A byte mask indicating which arguments are currently active (non-zero = active). -/// - `style`: The parsing style configuration. -pub struct MatcherContext<'a> { - /// The full set of parsed arguments. - pub args: &'a PickerArgs<'a>, - - /// A byte mask where non-zero values indicate the argument at that position is active/should be matched. - pub mask: &'a [u8], - - /// The parsing style configuration. - pub style: &'a ParserStyle<'a>, - - /// Metadata about the command-line argument/flag being processed. - /// - /// Contains information such as short form (`-n`), long form (`--name`), - /// aliases, and parsing flags (positional, optional, multi, is_flag). - /// Used by matchers to make decisions based on argument characteristics. - pub arg_info: &'a PickerArgInfo<'a>, -} - -impl<'a> From<&'a crate::TagPhaseContext<'a>> for MatcherContext<'a> { - fn from(ctx: &'a crate::TagPhaseContext<'a>) -> Self { - MatcherContext { - args: ctx.args, - mask: ctx.mask, - style: ParserStyle::global_style(), - arg_info: ctx.arg_info, - } - } -} - -impl<'a> From> for MatcherContext<'a> { - fn from(ctx: crate::TagPhaseContext<'a>) -> Self { - MatcherContext { - args: ctx.args, - mask: ctx.mask, - style: ParserStyle::global_style(), - arg_info: ctx.arg_info, - } - } -} - -#[inline(always)] -fn is_masked(mask: &[u8], idx: usize) -> bool { - idx < mask.len() && mask[idx] != 0 -} - -#[inline(always)] -fn build_masked_args<'a>(args: &'a PickerArgs, mask: &'a [u8]) -> Vec> { - let mut cidx = 0; - args.iter() - .filter_map(|r| { - let idx = cidx; - cidx += 1; - // Include args where mask is 0 (available/not yet claimed). - // mask[i] = 0 means available; mask[i] != 0 means already claimed. - if !is_masked(mask, idx) { - Some(MaskedArg { - raw: r, - raw_idx: idx, - }) - } else { - None - } - }) - .collect() -} diff --git a/mingling_picker/src/parselib/arg_matcher.rs b/mingling_picker/src/parselib/arg_matcher.rs deleted file mode 100644 index 38bb9cc..0000000 --- a/mingling_picker/src/parselib/arg_matcher.rs +++ /dev/null @@ -1,142 +0,0 @@ -use crate::{ - matcher_needed::*, - parselib::{build_possible_flags, seek_end_of_options}, -}; - -/// `ArgMatcher` is used for parameters that carry a single value. -/// -/// It handles two scenarios: -/// -/// **Named** — `--name Alice` or `--name=Alice`. -/// Each flag occurrence consumes **one** following argument as its value, -/// regardless of what it is (even if it looks like a flag). -/// This ensures the mask correctly claims the value slot; validation is -/// the `Pickable`'s responsibility. -/// -/// **Positional** — no flag prefix, matched by position. -/// -/// # Examples -/// -/// | Input | `on_match_one` | `on_match_all` | -/// |-------|----------------|----------------| -/// | `--name Alice` | `[0, 1]` (via Pickable tag) | `[0, 1]` | -/// | `--name=Alice` | `[0]` | `[0]` | -/// | `--val a --val b` | `[0, 1]` | `[0, 1, 2, 3]` | -/// -/// Args after `--` are ignored. -pub struct ArgMatcher; - -impl ArgMatcher { - /// Check whether `raw` matches `flag_str`, optionally with an inline value - /// separated by the style's value separator (`=` for Unix, `:` for PowerShell). - #[inline(always)] - fn matches(raw: &str, flag_str: &str, case_sensitive: bool, sep: char) -> bool { - let eq_match = - |r: &str, f: &str| r.len() > f.len() && r.as_bytes().get(f.len()) == Some(&(sep as u8)); - - if case_sensitive { - raw == flag_str || (raw.starts_with(flag_str) && eq_match(raw, flag_str)) - } else { - raw.eq_ignore_ascii_case(flag_str) - || (raw.len() > flag_str.len() - && raw[..flag_str.len()].eq_ignore_ascii_case(flag_str) - && raw.as_bytes()[flag_str.len()] == sep as u8) - } - } - - /// Check whether the argument contains its value inline via the style's - /// value separator (eq mode), so no extra mask slot is needed. - #[inline(always)] - fn is_inline_value(raw: &str, flag_str: &str, sep: char) -> bool { - raw.len() > flag_str.len() && raw.as_bytes().get(flag_str.len()) == Some(&(sep as u8)) - } -} - -impl Matcher for ArgMatcher { - fn on_match_one( - args: &[MaskedArg], - style: &ParserStyle, - arg_info: &PickerArgInfo, - ) -> Option { - if arg_info.positional { - return args.first().map(|a| a.raw_idx); - } - - let possible_flags = build_possible_flags(style, arg_info); - let end = seek_end_of_options(args, style); - let sep = style.value_separator; - - for arg in args { - if end.is_some_and(|e| arg.raw_idx >= e) { - break; - } - - let matched = possible_flags - .iter() - .any(|f| Self::matches(arg.raw, f, style.case_sensitive, sep)); - if matched { - return Some(arg.raw_idx); - } - } - - None - } - - fn on_match_all( - args: &[MaskedArg], - style: &ParserStyle, - arg_info: &PickerArgInfo, - ) -> Vec { - if arg_info.positional { - let end = seek_end_of_options(args, style); - return args - .iter() - .take_while(|a| end.is_none_or(|e| a.raw_idx < e)) - .map(|a| a.raw_idx) - .collect(); - } - - let possible_flags = build_possible_flags(style, arg_info); - let end = seek_end_of_options(args, style); - let sep = style.value_separator; - - let mut result = Vec::new(); - let mut i = 0; - while i < args.len() { - if end.is_some_and(|e| args[i].raw_idx >= e) { - break; - } - - let matched = possible_flags - .iter() - .any(|f| Self::matches(args[i].raw, f, style.case_sensitive, sep)); - - if matched { - let flag_str = possible_flags - .iter() - .find(|f| Self::matches(args[i].raw, f, style.case_sensitive, sep)) - .expect("already matched"); - - result.push(args[i].raw_idx); - - if !Self::is_inline_value(args[i].raw, flag_str, sep) { - if i + 1 < args.len() - // Don't consume `--` (end-of-options marker) as a value. - && end.is_none_or(|e| args[i + 1].raw_idx < e) - { - result.push(args[i + 1].raw_idx); - i += 2; - continue; - } - i += 1; - continue; - } - i += 1; - continue; - } - i += 1; - } - - result - } -} diff --git a/mingling_picker/src/parselib/flag_matcher.rs b/mingling_picker/src/parselib/flag_matcher.rs deleted file mode 100644 index e93d35a..0000000 --- a/mingling_picker/src/parselib/flag_matcher.rs +++ /dev/null @@ -1,82 +0,0 @@ -use crate::{ - matcher_needed::*, - parselib::{build_possible_flags, get_seeked_first, multi_seek_eq, seek_end_of_options}, -}; - -/// `FlagMatcher` is used to match flags in command-line arguments. -/// -/// Flags typically start with `-` or `--` (e.g., `-h`, `--help`), -/// and do not carry additional values. This matcher is responsible for finding -/// these flags in the argument list, taking into account that flags after `--` -/// (end-of-options marker) should not be matched. -pub struct FlagMatcher; - -impl Matcher for FlagMatcher { - fn on_match_one( - args: &[MaskedArg], - style: &ParserStyle, - arg_info: &PickerArgInfo, - ) -> Option { - let possible_flags = build_possible_flags(style, arg_info); - let flag_refs: Vec<&str> = possible_flags.iter().map(|s| s.as_str()).collect(); - let end_of_options = seek_end_of_options(args, style); - - let result = get_seeked_first(multi_seek_eq(args, &flag_refs, style.case_sensitive)); - - match (end_of_options, result) { - (Some(end), Some(current)) if current > end => None, - _ => result, - } - } - - fn on_match_all( - args: &[MaskedArg], - style: &ParserStyle, - arg_info: &PickerArgInfo, - ) -> Vec { - let possible_flags = build_possible_flags(style, arg_info); - single_pass_match_all(args, style, &possible_flags) - } -} - -/// Single-pass match: finds the `--` marker and matching flags in one iteration. -fn single_pass_match_all( - args: &[MaskedArg], - style: &ParserStyle, - possible_flags: &[String], -) -> Vec { - let flag_refs: Vec<&str> = possible_flags.iter().map(|s| s.as_str()).collect(); - let eoo = style.end_of_options; - let case_sensitive = style.case_sensitive; - - let mut end_pos: Option = None; - let mut matches: Vec = Vec::new(); - - for arg in args { - if end_pos.is_none() { - let is_eoo = if case_sensitive { - arg.raw == eoo - } else { - arg.raw.eq_ignore_ascii_case(eoo) - }; - if is_eoo { - end_pos = Some(arg.raw_idx); - continue; - } - } - - // Only match flags before the end-of-options marker. - if end_pos.is_none() { - let matched = if case_sensitive { - flag_refs.contains(&arg.raw) - } else { - flag_refs.iter().any(|s| arg.raw.eq_ignore_ascii_case(s)) - }; - if matched { - matches.push(arg.raw_idx); - } - } - } - - matches -} diff --git a/mingling_picker/src/parselib/multi_arg_matcher.rs b/mingling_picker/src/parselib/multi_arg_matcher.rs deleted file mode 100644 index 748b1be..0000000 --- a/mingling_picker/src/parselib/multi_arg_matcher.rs +++ /dev/null @@ -1,141 +0,0 @@ -use crate::{ - matcher_needed::*, - parselib::{build_possible_flags, seek_end_of_options}, -}; - -/// `MultiArgMatcher` matches a named flag and **all** consecutive arguments -/// that follow it, stopping at the next flag, the `--` marker, or the end -/// of the argument list. -/// -/// This is the tag implementation for `Multi` and `GreedyMulti` types -/// such as `Vec` (`--files a.txt b.txt`). -/// -/// # Behavior -/// -/// | Input | `on_match_all` | -/// |-------|----------------| -/// | `--val a b --val d e` | `[0, 1, 2, 5, 6]` (two groups) | -/// | `--val=1 2` | `[0, 1]` (eq mode + one extra value) | -/// -/// Args after `--` are ignored. -pub struct MultiArgMatcher; - -impl Matcher for MultiArgMatcher { - fn on_match_one( - args: &[MaskedArg], - style: &ParserStyle, - arg_info: &PickerArgInfo, - ) -> Option { - if arg_info.positional { - return args.first().map(|a| a.raw_idx); - } - - let possible_flags = build_possible_flags(style, arg_info); - let end = seek_end_of_options(args, style); - let sep = style.value_separator; - - for arg in args { - if end.is_some_and(|e| arg.raw_idx >= e) { - break; - } - let matched = possible_flags - .iter() - .any(|f| Self::flag_match(arg.raw, f, style.case_sensitive, sep)); - if matched { - return Some(arg.raw_idx); - } - } - None - } - - fn on_match_all( - args: &[MaskedArg], - style: &ParserStyle, - arg_info: &PickerArgInfo, - ) -> Vec { - if arg_info.positional { - let end = seek_end_of_options(args, style); - return args - .iter() - .take_while(|a| end.is_none_or(|e| a.raw_idx < e)) - .map(|a| a.raw_idx) - .collect(); - } - - let possible_flags = build_possible_flags(style, arg_info); - let end = seek_end_of_options(args, style); - let sep = style.value_separator; - let is_flag = - |raw: &str| raw.starts_with(style.long_prefix) || raw.starts_with(style.short_prefix); - let is_our_flag = |raw: &str| { - possible_flags - .iter() - .any(|f| Self::flag_match(raw, f, style.case_sensitive, sep)) - }; - - let mut result = Vec::new(); - let mut i = 0; - while i < args.len() { - if end.is_some_and(|e| args[i].raw_idx >= e) { - break; - } - - let matched = is_our_flag(args[i].raw); - - if matched { - result.push(args[i].raw_idx); - - if Self::is_eq_match(args[i].raw, &possible_flags, style.case_sensitive, sep) { - i += 1; - while i < args.len() - && end.is_none_or(|e| args[i].raw_idx < e) - && !is_flag(args[i].raw) - { - result.push(args[i].raw_idx); - i += 1; - } - continue; - } - - i += 1; - while i < args.len() - && end.is_none_or(|e| args[i].raw_idx < e) - && !is_flag(args[i].raw) - { - result.push(args[i].raw_idx); - i += 1; - } - continue; - } - i += 1; - } - - result - } -} - -impl MultiArgMatcher { - #[inline(always)] - fn flag_match(raw: &str, flag_str: &str, case_sensitive: bool, sep: char) -> bool { - let eq = - |r: &str, f: &str| r.len() > f.len() && r.as_bytes().get(f.len()) == Some(&(sep as u8)); - - if case_sensitive { - raw == flag_str || (raw.starts_with(flag_str) && eq(raw, flag_str)) - } else { - raw.eq_ignore_ascii_case(flag_str) - || (raw.len() > flag_str.len() - && raw[..flag_str.len()].eq_ignore_ascii_case(flag_str) - && raw.as_bytes()[flag_str.len()] == sep as u8) - } - } - - #[inline(always)] - fn is_eq_match(raw: &str, flags: &[String], case_sensitive: bool, sep: char) -> bool { - flags.iter().any(|f| { - Self::flag_match(raw, f, case_sensitive, sep) - && raw.len() > f.len() - && raw.as_bytes().get(f.len()) == Some(&(sep as u8)) - }) - } -} diff --git a/mingling_picker/src/parselib/pos_matcher.rs b/mingling_picker/src/parselib/pos_matcher.rs deleted file mode 100644 index 279e01e..0000000 --- a/mingling_picker/src/parselib/pos_matcher.rs +++ /dev/null @@ -1,71 +0,0 @@ -use crate::{matcher_needed::*, parselib::seek_end_of_options}; - -/// `PositionalMatcher` matches positional arguments — values not associated -/// with any named flag. -/// -/// # Rules -/// -/// * Before `--`: skips any argument that starts with the style's long or short -/// prefix (those belong to named matchers). -/// * After `--`: takes **everything** — the `--` marker signals that all -/// remaining values are positional, even if they look like flags. -/// * Runs at the lowest priority (see [`PickerArgAttr::Positional`](crate::PickerArgAttr::Positional)). -pub struct PositionalMatcher; - -impl PositionalMatcher { - /// Check whether `raw` looks like a named flag (starts with a prefix). - #[inline(always)] - fn is_flag_like(raw: &str, style: &ParserStyle) -> bool { - raw.starts_with(style.long_prefix) || raw.starts_with(style.short_prefix) - } -} - -impl Matcher for PositionalMatcher { - fn on_match_one( - args: &[MaskedArg], - style: &ParserStyle, - _arg_info: &PickerArgInfo, - ) -> Option { - let end = seek_end_of_options(args, style); - - for arg in args { - if end.is_some_and(|e| arg.raw_idx == e) { - // Hit `--`: everything from here on is positional, - // including the first arg after `--`. - continue; - } - if end.is_some_and(|e| arg.raw_idx > e) { - // After `--`: accept everything. - return Some(arg.raw_idx); - } - // Before `--`: skip flag-like args. - if !Self::is_flag_like(arg.raw, style) { - return Some(arg.raw_idx); - } - } - - None - } - - fn on_match_all( - args: &[MaskedArg], - style: &ParserStyle, - _arg_info: &PickerArgInfo, - ) -> Vec { - let end = seek_end_of_options(args, style); - let mut after_end = false; - let mut result = Vec::new(); - - for arg in args { - if end.is_some_and(|e| arg.raw_idx == e) { - after_end = true; - continue; - } - if after_end || !Self::is_flag_like(arg.raw, style) { - result.push(arg.raw_idx); - } - } - - result - } -} diff --git a/mingling_picker/src/parselib/single_matcher.rs b/mingling_picker/src/parselib/single_matcher.rs deleted file mode 100644 index 25c4741..0000000 --- a/mingling_picker/src/parselib/single_matcher.rs +++ /dev/null @@ -1,59 +0,0 @@ -use crate::TagPhaseContext; -use crate::parselib::{ArgMatcher, Matcher, ParserStyle, PositionalMatcher}; - -/// `SingleMatcher` is a composite matcher for single-value parameters. -/// -/// It delegates to [`PositionalMatcher`] for positional args and -/// [`ArgMatcher`] for named args, adding a guard: if a named flag -/// captures only itself with no inline value (eq mode), the result -/// is cleared so that [`Pickable::pick`](crate::Pickable::pick) receives `[]` → `NotFound`. -/// -/// This is the standard tag implementation for all `Single`-type -/// `Pickable` implementations (e.g., `String`, `i32`, `u64`). -pub struct SingleMatcher; - -impl SingleMatcher { - /// Match a single positional value or a named flag+value pair. - /// - /// For named args, only complete pairs (flag + value) are kept. - /// Flag occurrences without a following value or inline separator - /// are dropped so they remain available for other matchers. - #[inline(always)] - pub fn tag(ctx: TagPhaseContext) -> Vec { - if ctx.arg_info.positional { - PositionalMatcher::match_one(ctx.into()) - .map(|i| vec![i]) - .unwrap_or_default() - } else { - let args = ctx.args; - let positions = ArgMatcher::match_all(ctx.into()); - let sep = ParserStyle::global_style().value_separator; - - // Walk pairs: [flag, value, flag, value, ...] - // Drop any flag that has no following value and no inline separator. - let mut i = 0; - let mut result = Vec::with_capacity(positions.len()); - while i < positions.len() { - let flag_idx = positions[i]; - if let Some(raw) = args.get(flag_idx) - && raw.contains(sep) - { - // Eq mode: value is inline, keep just the flag. - result.push(flag_idx); - i += 1; - continue; - } - if i + 1 < positions.len() { - // Pair: flag + value. - result.push(flag_idx); - result.push(positions[i + 1]); - i += 2; - } else { - // Flag without value — drop it. - i += 1; - } - } - result - } - } -} diff --git a/mingling_picker/src/parselib/style.rs b/mingling_picker/src/parselib/style.rs deleted file mode 100644 index dd01125..0000000 --- a/mingling_picker/src/parselib/style.rs +++ /dev/null @@ -1,258 +0,0 @@ -use std::sync::OnceLock; -use std::sync::atomic::{AtomicBool, Ordering}; - -use crate::parselib::ParserStyleNamingCase::{Kebab, Pascal}; - -/// Defines the style of command-line argument parsing (prefixes, separators, etc.). -#[derive(Clone, Copy, PartialEq, Eq)] -pub struct ParserStyle<'a> { - /// End-of-options marker (e.g., `--`) - pub end_of_options: &'a str, - - /// Prefix for long options (e.g., `--` or `/`) - pub long_prefix: &'a str, - - /// Prefix for short options (e.g., `-` or `/`) - pub short_prefix: &'a str, - - /// Prefix for combined short flags (e.g., `-abc`) - pub combine_prefix: &'a str, - - /// Separator between name and value (e.g., `=` or `:`) - pub value_separator: char, - - /// Whether option names are case-sensitive - pub case_sensitive: bool, - - /// Whether combining short flags is allowed (e.g., `-abc` for `-a -b -c`) - pub allow_combine: bool, - - /// Naming case - pub naming_case: ParserStyleNamingCase, -} - -impl<'a> ParserStyle<'a> { - /// Formats a flag (short or long) into a full command-line option string. - /// - /// This method takes any type that can be converted into a `FlagStr` and produces - /// a complete option string by prepending the appropriate prefix. - /// - /// # Examples - /// - /// ``` - /// # use mingling_picker::parselib::{ParserStyle, FlagStr, UNIX_STYLE}; - /// let style = &UNIX_STYLE; - /// - /// assert_eq!(style.flag_string('v'), "-v"); - /// assert_eq!(style.flag_string("verbose"), "--verbose"); - /// ``` - /// - /// # Parameters - /// - /// * `flag` - A value that can be converted to `FlagStr`, either a `char` for short flags - /// or a `&str` for long flags. - /// - /// # Returns - /// - /// A `String` with the prefix and the flag name combined. - #[must_use] - #[inline(always)] - pub fn flag_string(&self, flag: F) -> String - where - F: Into>, - { - match flag.into() { - FlagStr::Short(short) => format!("{}{}", self.short_prefix, short), - FlagStr::Long(long) => format!("{}{}", self.long_prefix, long), - } - } -} - -/// Represents a flag name for command-line argument parsing. -/// -/// This enum can hold either a short flag (a single character, e.g., `'v'` for `-v`) -/// or a long flag (a string, e.g., `"verbose"` for `--verbose`). -/// -/// # Examples -/// -/// ``` -/// use mingling_picker::parselib::FlagStr; -/// -/// let short: FlagStr = 'v'.into(); -/// let long: FlagStr = "verbose".into(); -/// ``` -pub enum FlagStr<'a> { - /// A short flag represented by a single character. - Short(char), - /// A long flag represented by a string slice. - Long(&'a str), -} - -impl<'a> From for FlagStr<'a> { - /// Converts a single character into a `FlagStr::Short`. - fn from(c: char) -> Self { - FlagStr::Short(c) - } -} - -impl<'a> From<&'a str> for FlagStr<'a> { - /// Converts a string slice into a `FlagStr::Long`. - fn from(s: &'a str) -> Self { - FlagStr::Long(s) - } -} - -impl<'a> From<&'a String> for FlagStr<'a> { - /// Converts a reference to a `String` into a `FlagStr::Long`. - fn from(s: &'a String) -> Self { - FlagStr::Long(s.as_str()) - } -} - -/// Defines the naming convention for command-line option names. -/// -/// Each variant represents a different case format that can be applied -/// to option names (e.g., long option names) during parsing or generation. -/// -/// # Examples -/// -/// ``` -/// # use mingling_picker::IntoPicker; -/// use mingling_picker::parselib::ParserStyleNamingCase; -/// -/// let case = ParserStyleNamingCase::Kebab; -/// assert_eq!( -/// case.convert("brew_coffee".to_string()), -/// "brew-coffee".to_string() -/// ); -/// ``` -#[repr(u8)] -#[derive(Default, Clone, Copy, PartialEq, Eq)] -pub enum ParserStyleNamingCase { - /// snake_case format: words are separated by underscores, all lowercase. - /// - /// Example: `brew_coffee` - #[default] - Snake, - /// camelCase format: first word is lowercase, subsequent words are capitalized. - /// - /// Example: `brewCoffee` - Camel, - /// PascalCase format: every word starts with an uppercase letter. - /// - /// Example: `BrewCoffee` - Pascal, - /// kebab-case format: words are separated by hyphens, all lowercase. - /// - /// Example: `brew-coffee` - Kebab, - /// dot.case format: words are separated by dots, all lowercase. - /// - /// Example: `brew.coffee` - Dot, - /// Title Case format: words are separated by spaces, each word capitalized. - /// - /// Example: `Brew Coffee` - Title, - /// lower case format: words are separated by spaces, all lowercase. - /// - /// Example: `brew coffee` - Lower, - /// UPPER CASE format: words are separated by spaces, all uppercase. - /// - /// Example: `BREW COFFEE` - Upper, -} - -impl ParserStyleNamingCase { - /// Converts the input string `s` to the naming case represented by this variant. - /// - /// This method takes any type `S` that can be converted into a `String` and - /// produced from a `String`, applies the corresponding case transformation, - /// and returns the result. - /// - /// # Examples - /// - /// ``` - /// use mingling_picker::parselib::ParserStyleNamingCase; - /// - /// let camel = ParserStyleNamingCase::Camel; - /// assert_eq!(camel.convert("brew_coffee".to_string()), "brewCoffee"); - /// - /// let kebab = ParserStyleNamingCase::Kebab; - /// assert_eq!(kebab.convert("BrewCoffee".to_string()), "brew-coffee"); - /// ``` - pub fn convert(&self, s: S) -> S - where - S: Into + From, - { - match self { - ParserStyleNamingCase::Camel => just_fmt::camel_case!(s.into()).into(), - ParserStyleNamingCase::Pascal => just_fmt::pascal_case!(s.into()).into(), - ParserStyleNamingCase::Kebab => just_fmt::kebab_case!(s.into()).into(), - ParserStyleNamingCase::Snake => just_fmt::snake_case!(s.into()).into(), - ParserStyleNamingCase::Dot => just_fmt::dot_case!(s.into()).into(), - ParserStyleNamingCase::Title => just_fmt::title_case!(s.into()).into(), - ParserStyleNamingCase::Lower => just_fmt::lower_case!(s.into()).into(), - ParserStyleNamingCase::Upper => just_fmt::upper_case!(s.into()).into(), - } - } -} - -/// Unix-like style (e.g., `--verbose`, `-v`, `--name=value`) -pub const UNIX_STYLE: ParserStyle = ParserStyle { - end_of_options: "--", - long_prefix: "--", - short_prefix: "-", - combine_prefix: "-", - value_separator: '=', - case_sensitive: true, - allow_combine: true, - naming_case: Kebab, -}; - -/// PowerShell style (e.g., `-Verbose`, `-Name:value`) -pub const POWERSHELL_STYLE: ParserStyle = ParserStyle { - end_of_options: "--", - long_prefix: "-", - short_prefix: "-", - combine_prefix: "-", - value_separator: ':', - case_sensitive: false, - allow_combine: false, - naming_case: Pascal, -}; - -/// Windows-style command-line (e.g., `/Verbose`, `/Name:value`) -pub const WINDOWS_STYLE: ParserStyle = ParserStyle { - end_of_options: "--", - long_prefix: "/", - short_prefix: "/", - combine_prefix: "/", - value_separator: ':', - case_sensitive: false, - allow_combine: false, - naming_case: Pascal, -}; - -static GLOBAL_STYLE: OnceLock> = OnceLock::new(); -static GLOBAL_STYLE_SET: AtomicBool = AtomicBool::new(false); - -impl<'a> ParserStyle<'a> { - /// Sets the global parser style. - /// - /// This function can only be called once. Subsequent calls will have no effect. - /// The style is stored as a static reference; the provided style must be a static - /// constant (e.g., `&'static ParserStyle`). Use the built-in constants like - /// `UNIX_STYLE`, `POWERSHELL_STYLE`, or `WINDOWS_STYLE`. - pub fn set_global_style(style: &'static ParserStyle<'static>) { - if !GLOBAL_STYLE_SET.load(Ordering::Acquire) && GLOBAL_STYLE.set(*style).is_ok() { - GLOBAL_STYLE_SET.store(true, Ordering::Release); - } - } - - /// Returns the global parser style, falling back to `UNIX_STYLE` if not set. - pub fn global_style() -> &'static ParserStyle<'static> { - GLOBAL_STYLE.get().unwrap_or(&UNIX_STYLE) - } -} diff --git a/mingling_picker/src/parselib/utils.rs b/mingling_picker/src/parselib/utils.rs deleted file mode 100644 index 47c5b55..0000000 --- a/mingling_picker/src/parselib/utils.rs +++ /dev/null @@ -1,276 +0,0 @@ -use crate::{ - PickerArgInfo, - parselib::{MaskedArg, ParserStyle}, -}; - -/// Builds a list of possible flag strings for the given argument info -/// -/// This function generates formatted flag strings (e.g., `-h`, `--help`) from the short flag, -/// long flag, and any aliases defined in the argument info. The long flag and alias names -/// are converted according to the style's naming case convention before being formatted. -#[inline(always)] -pub fn build_possible_flags(style: &ParserStyle, arg_info: &PickerArgInfo) -> Vec { - let mut possible_flags = vec![]; - - if let Some(short) = arg_info.short { - possible_flags.push(style.flag_string(short)); - } - - if let Some(long) = arg_info.long { - let converted = style.naming_case.convert(long.to_string()); - possible_flags.push(style.flag_string(&converted)); - } - - if let Some(aliases) = &arg_info.alias { - for alias in aliases { - let converted = style.naming_case.convert(alias.to_string()); - possible_flags.push(style.flag_string(&converted)); - } - } - - possible_flags -} - -/// Extract a single value from the raw strings tagged by [`SingleMatcher`](crate::parselib::SingleMatcher). -/// -/// Returns `None` if no value is available (empty slice), -/// the inline value after the style separator if present (eq mode), -/// or the value directly (positional or flag-following). -/// -/// This is the standard `pick` helper for all `Single`-type -/// [`Pickable`](crate::Pickable) implementations. -#[must_use] -pub fn seek_single<'a>(raw_strs: &'a [&'a str]) -> Option<&'a str> { - match raw_strs.len() { - 0 => None, - 1 => { - let s = raw_strs[0]; - let sep = ParserStyle::global_style().value_separator; - if let Some(pos) = s.rfind(sep) { - Some(&s[pos + 1..]) - } else { - Some(s) - } - } - _ => Some(raw_strs[1]), - } -} - -/// Seeks the index of the end-of-options marker (`--`) in the argument list. -/// -/// This function searches for the standard end-of-options separator (`--`) -/// in the given argument list, respecting the parser's style settings -/// (e.g., case sensitivity). The end-of-options marker indicates that all -/// subsequent arguments should be treated as positional arguments, not flags. -#[must_use] -pub fn seek_end_of_options(args: &[MaskedArg], style: &ParserStyle) -> Option { - args.iter() - .find(|arg| { - if style.case_sensitive { - arg.raw == style.end_of_options - } else { - arg.raw.eq_ignore_ascii_case(style.end_of_options) - } - }) - .map(|arg| arg.raw_idx) -} - -/// Seeks arguments in `args` that are exactly equal to the given `string`. -/// -/// Returns the indices of matching arguments. -#[must_use] -#[inline(always)] -pub fn seek_eq(args: &[MaskedArg], string: &str, case_sensitive: bool) -> Vec { - args.iter() - .filter(|arg| { - if case_sensitive { - arg.raw == string - } else { - arg.raw.eq_ignore_ascii_case(string) - } - }) - .map(|arg| arg.raw_idx) - .collect() -} - -/// Seeks arguments in `args` that contain the given `string` as a substring. -/// -/// Returns the indices of matching arguments. -#[must_use] -#[inline(always)] -pub fn seek_contains(args: &[MaskedArg], string: &str, case_sensitive: bool) -> Vec { - args.iter() - .filter(|arg| { - if case_sensitive { - arg.raw.contains(string) - } else { - arg.raw.to_lowercase().contains(&string.to_lowercase()) - } - }) - .map(|arg| arg.raw_idx) - .collect() -} - -/// Seeks arguments in `args` that start with the given `string`. -/// -/// Returns the indices of matching arguments. -#[must_use] -#[inline(always)] -pub fn seek_start_with(args: &[MaskedArg], string: &str, case_sensitive: bool) -> Vec { - args.iter() - .filter(|arg| { - if case_sensitive { - arg.raw.starts_with(string) - } else { - arg.raw.to_lowercase().starts_with(&string.to_lowercase()) - } - }) - .map(|arg| arg.raw_idx) - .collect() -} - -/// Seeks arguments in `args` that end with the given `string`. -/// -/// Returns the indices of matching arguments. -#[must_use] -#[inline(always)] -pub fn seek_end_with(args: &[MaskedArg], string: &str, case_sensitive: bool) -> Vec { - args.iter() - .filter(|arg| { - if case_sensitive { - arg.raw.ends_with(string) - } else { - arg.raw.to_lowercase().ends_with(&string.to_lowercase()) - } - }) - .map(|arg| arg.raw_idx) - .collect() -} - -/// Seeks arguments in `args` that are exactly equal to any of the given `strings`. -/// -/// Returns the indices of matching arguments. -#[must_use] -#[inline(always)] -pub fn multi_seek_eq(args: &[MaskedArg], strings: &[&str], case_sensitive: bool) -> Vec { - args.iter() - .filter(|arg| { - if case_sensitive { - strings.contains(&arg.raw) - } else { - strings.iter().any(|s| arg.raw.eq_ignore_ascii_case(s)) - } - }) - .map(|arg| arg.raw_idx) - .collect() -} - -/// Seeks arguments in `args` that contain any of the given `strings` as a substring. -/// -/// Returns the indices of matching arguments. -#[must_use] -#[inline(always)] -pub fn multi_seek_contains( - args: &[MaskedArg], - strings: &[&str], - case_sensitive: bool, -) -> Vec { - args.iter() - .filter(|arg| { - if case_sensitive { - strings.iter().any(|s| arg.raw.contains(s)) - } else { - let lower_raw = arg.raw.to_lowercase(); - strings - .iter() - .any(|s| lower_raw.contains(&s.to_lowercase())) - } - }) - .map(|arg| arg.raw_idx) - .collect() -} - -/// Seeks arguments in `args` that start with any of the given `strings`. -/// -/// Returns the indices of matching arguments. -#[must_use] -#[inline(always)] -pub fn multi_seek_start_with( - args: &[MaskedArg], - strings: &[&str], - case_sensitive: bool, -) -> Vec { - args.iter() - .filter(|arg| { - if case_sensitive { - strings.iter().any(|s| arg.raw.starts_with(s)) - } else { - let lower_raw = arg.raw.to_lowercase(); - strings - .iter() - .any(|s| lower_raw.starts_with(&s.to_lowercase())) - } - }) - .map(|arg| arg.raw_idx) - .collect() -} - -/// Seeks arguments in `args` that end with any of the given `strings`. -/// -/// Returns the indices of matching arguments. -#[must_use] -#[inline(always)] -pub fn multi_seek_end_with( - args: &[MaskedArg], - strings: &[&str], - case_sensitive: bool, -) -> Vec { - args.iter() - .filter(|arg| { - if case_sensitive { - strings.iter().any(|s| arg.raw.ends_with(s)) - } else { - let lower_raw = arg.raw.to_lowercase(); - strings - .iter() - .any(|s| lower_raw.ends_with(&s.to_lowercase())) - } - }) - .map(|arg| arg.raw_idx) - .collect() -} - -/// Converts a `&Vec` into a `Vec<&str>` by borrowing each string's slice. -/// -/// This is useful for converting owned `String` vectors into borrowed `&str` slices -/// for functions that take `&[&str]` or similar parameters. -#[must_use] -#[inline(always)] -#[doc(hidden)] -pub fn vec_string_to_vec_str(input: &[String]) -> Vec<&str> { - input.iter().map(|s| s.as_str()).collect() -} - -/// Converts a `&Vec` into a `Vec<&str>` by borrowing each string's slice. -/// -/// This is useful for converting owned `String` vectors into borrowed `&str` slices -/// for functions that take `&[&str]` or similar parameters. -#[macro_export] -#[doc(hidden)] -macro_rules! vec_string_slice { - ($v:expr) => { - $v.iter() - .map(|s| s.as_str()) - .collect::>() - .as_slice() - }; -} - -/// Gets the first element from a vector of seek results, if any. -/// -/// Returns `Some(index)` if the vector is non-empty, otherwise `None`. -#[must_use] -#[inline(always)] -pub fn get_seeked_first(seeked: Vec) -> Option { - seeked.into_iter().next() -} diff --git a/mingling_picker/src/pickable.rs b/mingling_picker/src/pickable.rs deleted file mode 100644 index 758ae9a..0000000 --- a/mingling_picker/src/pickable.rs +++ /dev/null @@ -1,91 +0,0 @@ -use crate::{PickerArg, PickerArgAttr, PickerArgInfo, PickerArgResult, PickerArgs}; - -mod single_pickable; -pub use single_pickable::*; - -mod multi_pickable; -pub use multi_pickable::*; - -/// `Pickable` trait defines how to parse a type instance from command-line arguments. -/// -/// This trait is the core abstraction of the `Picker` argument parsing system, dividing the -/// parsing process into two phases: -/// -/// 1. **Tag phase ([`Pickable::tag`])**: Determines which argument positions the `Pickable` needs to handle. -/// 2. **Pick phase ([`Pickable::pick`])**: Converts the raw strings at the tagged positions into the actual type. -/// -/// Types implementing this trait must also implement [`Default`], so that a default value -/// can be used as a fallback when parsing fails. -/// -/// # Type Parameters -/// -/// * `'a` - Lifetime parameter, used to associate references in [`PickerArg`]. -pub trait Pickable<'a> -where - Self: Sized, -{ - /// Returns the parse-order attribute of this flag. - /// - /// This attribute is used to inform the parser about the parse order - /// between different `Pickable` types. - /// See [`PickerArgAttr`] for specific ordering definitions. - /// - /// # Parameters - /// - /// * `flag` - The current flag instance, which contains a reference to `Self`. - /// - /// # Returns - /// - /// Returns a [`PickerArgAttr`] describing the parse-order attribute of this flag. - fn get_attr(flag: &'a PickerArg<'a, Self>) -> PickerArgAttr; - - /// Tag phase: Determines which argument positions the `Pickable` needs to handle. - /// - /// This function receives a [`TagPhaseContext`] containing argument context information. - /// During this phase, the parser invokes each `Pickable` and collects the position indices - /// they return, in order to determine which arguments to parse later. - /// - /// # Parameters - /// - /// * `ctx` - The tag phase context, containing argument information, all parameters of the - /// current Picker, and an availability mask. - /// - /// # Returns - /// - /// Returns a `Vec` representing the indices of the arguments in the argument list - /// that this `Pickable` needs to handle. - fn tag(ctx: TagPhaseContext) -> Vec; - - /// Pick phase: Converts the raw string arguments tagged during the `tag` phase into - /// the actual expected type. - /// - /// This function receives a slice of the raw strings that were tagged in the `tag` step - /// and converts them into an instance of `Self`. - /// - /// # Parameters - /// - /// * `raw_strs` - A slice of strings containing the raw argument values to parse. - /// - /// # Returns - /// - /// Returns [`PickerArgResult`], i.e., the `Self` instance on success, or an appropriate - /// error message on failure. - fn pick(raw_strs: &[&str]) -> PickerArgResult; -} - -/// Tag phase context, providing the necessary argument and state information for -/// [`Pickable::tag`]. -pub struct TagPhaseContext<'a> { - /// Argument information describing the structure and metadata of the argument - /// to be parsed. - pub arg_info: &'a PickerArgInfo<'a>, - - /// A read-only list of all arguments in the current [`Picker`](crate::Picker). - pub args: &'a PickerArgs<'a>, - - /// Mask indicating which argument positions have already been claimed. - /// - /// For example, if the mask is `[0, 0, 1, 0]`, then the argument at index `2` - /// has already been tagged by another `Pickable`. - pub mask: &'a [u8], -} diff --git a/mingling_picker/src/pickable/multi_pickable.rs b/mingling_picker/src/pickable/multi_pickable.rs deleted file mode 100644 index 84a8068..0000000 --- a/mingling_picker/src/pickable/multi_pickable.rs +++ /dev/null @@ -1,76 +0,0 @@ -use crate::{ - Pickable, PickerArg, PickerArgAttr, PickerArgResult, SinglePickable, TagPhaseContext, - matcher_needed::Matcher, - parselib::{MultiArgMatcher, ParserStyle}, -}; - -/// Boundary check for multi-value positional parameters. -pub trait BoundaryCheck { - fn check_boundary(raw: &str) -> bool; -} - -/// Trait for multi-value parameters. -pub trait MultiPickableWithBoundary: Sized { - type Checker: BoundaryCheck; - fn pick_multi(raw: Vec) -> PickerArgResult; -} - -/// Marker: unit type that always accepts — no boundary. -pub struct NoBoundary; - -impl BoundaryCheck for NoBoundary { - #[inline(always)] - fn check_boundary(_raw: &str) -> bool { - false - } -} - -/// `Vec` is greedy — it takes everything with `NoBoundary`. -impl MultiPickableWithBoundary for Vec { - type Checker = NoBoundary; - - fn pick_multi(raw: Vec) -> PickerArgResult { - let mut result = Vec::with_capacity(raw.len()); - for s in &raw { - match T::pick_single(Some(s)) { - PickerArgResult::Parsed(v) => result.push(v), - PickerArgResult::NotFound => return PickerArgResult::NotFound, - PickerArgResult::Unparsed => {} - } - } - PickerArgResult::Parsed(result) - } -} - -/// If the first raw string looks like a named flag (starts with the -/// style's long or short prefix), strip it — it's the flag, not a value. -fn strip_flag<'a>(raw_strs: &'a [&'a str]) -> &'a [&'a str] { - if let Some(first) = raw_strs.first() { - let style = ParserStyle::global_style(); - if first.starts_with(style.long_prefix) || first.starts_with(style.short_prefix) { - return &raw_strs[1..]; - } - } - raw_strs -} - -// Pickable impl for Vec - -impl<'a, T> Pickable<'a> for Vec -where - T: SinglePickable, -{ - fn get_attr(flag: &'a PickerArg<'a, Self>) -> PickerArgAttr { - PickerArgAttr::positional_or_multi(flag) - } - - fn tag(ctx: TagPhaseContext) -> Vec { - MultiArgMatcher::match_all(ctx.into()) - } - - fn pick(raw_strs: &[&str]) -> PickerArgResult { - let strs = strip_flag(raw_strs); - let owned: Vec = strs.iter().map(|&s| s.to_string()).collect(); - as MultiPickableWithBoundary>::pick_multi(owned) - } -} diff --git a/mingling_picker/src/pickable/single_pickable.rs b/mingling_picker/src/pickable/single_pickable.rs deleted file mode 100644 index 8a5b3e6..0000000 --- a/mingling_picker/src/pickable/single_pickable.rs +++ /dev/null @@ -1,69 +0,0 @@ -use crate::{Pickable, PickerArg, PickerArgAttr, PickerArgResult, TagPhaseContext}; - -/// `SinglePickable` trait defines how to parse a type from a single command-line argument. -/// -/// This trait provides a simplified interface for types that consume exactly one argument value. -/// It is automatically implemented by the blanket `impl` of [`Pickable`], so types implementing -/// `SinglePickable` will work with the full `Pickable` argument parsing system. -/// -/// Additionally, `Option` where `S: SinglePickable` also implements [`Pickable`], allowing -/// optional arguments to be parsed naturally. -/// -/// # Type Parameters -/// -/// * `Self` - The type to be parsed from a single argument string. -pub trait SinglePickable -where - Self: Sized, -{ - /// Parse a single optional string value into an instance of `Self`. - /// - /// # Parameters - /// - /// * `str` - An `Option<&str>` representing the raw argument value. If `None`, - /// it indicates that no argument value was provided (e.g., for flag-like arguments). - /// - /// # Returns - /// - /// Returns [`PickerArgResult`], i.e., the parsed `Self` instance on success, - /// or an appropriate error message on failure. - fn pick_single(str: Option<&str>) -> PickerArgResult; -} - -impl<'a, S> Pickable<'a> for S -where - S: SinglePickable, -{ - fn get_attr(flag: &'a PickerArg<'a, Self>) -> PickerArgAttr { - PickerArgAttr::positional_or_single(flag) - } - - fn tag(ctx: TagPhaseContext) -> Vec { - crate::parselib::SingleMatcher::tag(ctx) - } - - fn pick(raw_strs: &[&str]) -> PickerArgResult { - Self::pick_single(crate::parselib::seek_single(raw_strs)) - } -} - -impl<'a, S> Pickable<'a> for Option -where - S: SinglePickable, -{ - fn get_attr(flag: &'a PickerArg<'a, Self>) -> PickerArgAttr { - PickerArgAttr::positional_or_single(flag) - } - - fn tag(ctx: TagPhaseContext) -> Vec { - crate::parselib::SingleMatcher::tag(ctx) - } - - fn pick(raw_strs: &[&str]) -> PickerArgResult { - match S::pick(raw_strs) { - PickerArgResult::Unparsed => PickerArgResult::Unparsed, - PickerArgResult::Parsed(r) => PickerArgResult::Parsed(Some(r)), - PickerArgResult::NotFound => PickerArgResult::Parsed(None), - } - } -} diff --git a/mingling_picker/src/picker.rs b/mingling_picker/src/picker.rs deleted file mode 100644 index 7cf1525..0000000 --- a/mingling_picker/src/picker.rs +++ /dev/null @@ -1,432 +0,0 @@ -use std::{marker::PhantomData, ops::Index}; - -mod parse; - -mod patterns; -pub use patterns::*; - -mod result; -pub use result::*; - -use crate::{Pickable, PickerArg, PickerArgResult}; - -#[doc = include_str!("../README.md")] -pub struct Picker<'a, Route = ()> { - pub(crate) route_phantom: PhantomData, - - /// Internal arguments of Picker - pub(crate) args: PickerArgs<'a>, -} - -impl<'a> Picker<'a> { - /// Creates a new `Picker` from the command-line arguments (excluding the program name). - /// - /// This is equivalent to calling `std::env::args().skip(1)`, which - /// collects all arguments passed to the program except the first one - /// (the executable path). - pub fn from_args() -> Picker<'a, ()> { - Self::from_args_skip(1) - } - - /// Creates a new `Picker` from the command-line arguments, skipping the - /// first `skip` entries. - /// - /// This method is useful when you want more control over which arguments - /// are included. For example, pass `skip = 2` to skip both the program - /// name and the first argument. - pub fn from_args_skip(skip: usize) -> Picker<'a, ()> { - let args = std::env::args().skip(skip).collect::>(); - Picker { - route_phantom: PhantomData, - args: PickerArgs::Owned(args), - } - } - - /// Changes the route (phantom type parameter) of the `Picker`. - /// - /// This method allows converting a `Picker` from one route type to another, - /// while preserving the same underlying arguments. The route type is typically - /// used to distinguish different parsing contexts or to carry compile-time - /// state information through the picking chain. - pub fn with_route(self) -> Picker<'a, NewRoute> - where - Self: Sized, - { - Picker { - route_phantom: PhantomData, - args: self.args, - } - } -} - -/// 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 PickerArgs<'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> Default for PickerArgs<'a> { - fn default() -> Self { - Self::Vec(vec![]) - } -} - -impl<'a> PickerArgs<'a> { - /// Returns the number of arguments. - pub fn len(&self) -> usize { - match self { - PickerArgs::Slice(items) => items.len(), - PickerArgs::Vec(items) => items.len(), - PickerArgs::Owned(items) => items.len(), - } - } - - /// Returns `true` if there are no arguments. - pub fn is_empty(&self) -> bool { - self.len() == 0 - } - - /// Returns an iterator over the arguments, yielding `&str` values. - pub fn iter(&'a self) -> PickerIter<'a> { - match self { - PickerArgs::Slice(items) => PickerIter::Slice(items.iter()), - PickerArgs::Vec(items) => PickerIter::Vec(items.iter()), - PickerArgs::Owned(items) => PickerIter::Owned(items.iter()), - } - } - - /// Returns a reference to the argument at `index`, if it exists. - pub fn get(&self, index: usize) -> Option<&str> { - match self { - PickerArgs::Slice(items) => items.get(index).copied(), - PickerArgs::Vec(items) => items.get(index).copied(), - PickerArgs::Owned(items) => items.get(index).map(|s| s.as_str()), - } - } -} - -impl<'a> Index for PickerArgs<'a> { - type Output = str; - - fn index(&self, index: usize) -> &Self::Output { - match self { - PickerArgs::Slice(items) => items[index], - PickerArgs::Vec(items) => items[index], - PickerArgs::Owned(items) => &items[index], - } - } -} - -impl<'a> IntoIterator for &'a PickerArgs<'a> { - type Item = &'a str; - type IntoIter = PickerIter<'a>; - - fn into_iter(self) -> Self::IntoIter { - match self { - PickerArgs::Slice(items) => PickerIter::Slice(items.iter()), - PickerArgs::Vec(items) => PickerIter::Vec(items.iter()), - PickerArgs::Owned(items) => PickerIter::Owned(items.iter()), - } - } -} - -impl<'a, Route> From<&'a [&'a str]> for Picker<'a, Route> { - fn from(value: &'a [&'a str]) -> Self { - Picker { - route_phantom: PhantomData, - args: PickerArgs::Slice(value), - } - } -} - -impl<'a, Route> From> for Picker<'a, Route> { - fn from(value: Vec<&'a str>) -> Self { - Picker { - route_phantom: PhantomData, - args: PickerArgs::Vec(value), - } - } -} - -impl<'a, Route> From> for Picker<'a, Route> { - fn from(value: Vec) -> Self { - Picker { - route_phantom: PhantomData, - args: PickerArgs::Owned(value), - } - } -} - -impl<'a, Route> Picker<'a, Route> { - /// Returns a reference to the internal `PickerArgs`. - pub fn args(&self) -> &PickerArgs<'a> { - &self.args - } - - /// Returns a mutable reference to the internal `PickerArgs`. - pub fn args_mut(&mut self) -> &mut PickerArgs<'a> { - &mut self.args - } - - /// Consumes `self` and returns the internal `PickerArgs`. - pub fn into_args(self) -> PickerArgs<'a> { - self.args - } - - /// Returns the number of arguments. - pub fn len(&self) -> usize { - self.args.len() - } - - /// Returns `true` if there are no arguments. - pub fn is_empty(&self) -> bool { - self.args.is_empty() - } - - /// Returns an iterator over the arguments, yielding `&str` values. - pub fn iter(&'a self) -> PickerIter<'a> { - self.args.iter() - } -} - -impl<'a, Route> Index for Picker<'a, Route> { - type Output = str; - - fn index(&self, index: usize) -> &Self::Output { - &self.args[index] - } -} - -impl<'a, Route> Index for &Picker<'a, Route> { - type Output = str; - - fn index(&self, index: usize) -> &Self::Output { - &self.args[index] - } -} - -impl<'a, Route> IntoIterator for &'a Picker<'a, Route> { - type Item = &'a str; - type IntoIter = PickerIter<'a>; - - fn into_iter(self) -> Self::IntoIter { - self.args.iter() - } -} - -/// Iterator for `Picker` (and `PickerArgs`), yielding `&'a str` values. -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 = &'a str; - - fn next(&mut self) -> Option { - match self { - PickerIter::Slice(iter) => iter.next().copied(), - PickerIter::Vec(iter) => iter.next().copied(), - PickerIter::Owned(iter) => iter.next().map(|s| s.as_str()), - } - } - - 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> {} - -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(self, arg: impl Into<&'a PickerArg<'a, N>>) -> PickerPattern1<'a, N, Route> - where - N: Pickable<'a>, - { - Self::build_pattern1(self.args, arg.into(), None::) - } - - /// Creates a `PickerPattern1` from the given arg. - /// If parsing fails, attempts the fallback arg. - pub fn pick_or( - self, - arg: impl Into<&'a PickerArg<'a, N>>, - or_arg: F, - ) -> PickerPattern1<'a, N, Route> - where - N: Pickable<'a>, - F: FnMut() -> N + 'static, - { - self.pick(arg).or(or_arg) - } - - /// Creates a `PickerPattern1` from the given arg. - /// If parsing fails, uses the provided default value. - pub fn pick_or_default( - self, - arg: impl Into<&'a PickerArg<'a, N>>, - ) -> PickerPattern1<'a, N, Route> - where - N: Pickable<'a> + Default, - { - self.pick(arg).or_default() - } - - /// Creates a `PickerPattern1` from the given arg. - /// If parsing fails, switches to the given error route. - pub fn pick_or_route( - self, - arg: impl Into<&'a PickerArg<'a, N>>, - error_route: F, - ) -> PickerPattern1<'a, N, Route> - where - N: Pickable<'a>, - F: FnMut() -> Route + 'static, - { - self.pick(arg).or_route(error_route) - } -} - -/// Trait for converting types into a `Picker` -/// -/// Implemented for: -/// - `&[&str]` (borrowed slice) -/// - `&[String]` (borrowed slice of owned strings) -/// - `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, ()>; - - /// Creates a `PickerPattern1` from the given arg for the `pick` method. - /// - /// This method converts the value into a `Picker` and starts a parameter - /// picking chain with one arg. The result is initially `Unparsed`. - fn pick(self, arg: impl Into<&'a PickerArg<'a, N>>) -> PickerPattern1<'a, N, ()> - where - Self: Sized, - N: Pickable<'a> + Default + Sized, - { - Picker::build_pattern1(self.to_picker().args, arg.into(), None::<()>) - } - - /// Converts the value into a `Picker` with a specified route type. - /// - /// This method allows changing the route (phantom type parameter) of the picker. - /// The route type is typically used to distinguish different parsing contexts or - /// to carry compile-time state information through the picking chain. - fn with_route(self) -> Picker<'a, NewRoute> - where - Self: Sized, - { - Picker { - route_phantom: PhantomData, - args: self.to_picker().args, - } - } -} - -impl<'a> IntoPicker<'a> for &'a [&'a str] { - fn to_picker(self) -> Picker<'a, ()> { - Picker { - route_phantom: PhantomData, - args: PickerArgs::Slice(self), - } - } -} - -impl<'a> IntoPicker<'a> for &'a [String] { - fn to_picker(self) -> Picker<'a, ()> { - let vec: Vec<&str> = self.iter().map(|s| s.as_str()).collect(); - Picker { - route_phantom: PhantomData, - args: PickerArgs::Vec(vec), - } - } -} - -impl<'a> IntoPicker<'a> for Vec<&'a str> { - fn to_picker(self) -> Picker<'a, ()> { - Picker { - route_phantom: PhantomData, - args: PickerArgs::Vec(self), - } - } -} - -impl<'a> IntoPicker<'a> for &'a Vec { - fn to_picker(self) -> Picker<'a, ()> { - let slice: Vec<&str> = self.iter().map(|s| s.as_str()).collect(); - Picker { - route_phantom: PhantomData, - args: PickerArgs::Vec(slice), - } - } -} - -impl<'a> IntoPicker<'a> for Vec { - fn to_picker(self) -> Picker<'a, ()> { - Picker { - route_phantom: PhantomData, - args: PickerArgs::Owned(self), - } - } -} - -// Private helper: shared construction logic for `PickerPattern1`. -// Both `Picker::pick` and `IntoPicker::pick` delegate to this. -impl<'a, Route> Picker<'a, Route> { - pub(crate) fn build_pattern1( - args: PickerArgs<'a>, - arg: &'a PickerArg<'a, N>, - error_route: Option, - ) -> PickerPattern1<'a, N, Route> - where - N: Pickable<'a>, - { - PickerPattern1 { - args, - error_route, - arg_1: arg, - result_1: PickerArgResult::Unparsed, - route_1: None, - default_1: None, - post_1: None, - } - } -} diff --git a/mingling_picker/src/picker/parse.rs b/mingling_picker/src/picker/parse.rs deleted file mode 100644 index 698baac..0000000 --- a/mingling_picker/src/picker/parse.rs +++ /dev/null @@ -1,177 +0,0 @@ -// -------------------------------------------------------------------------------------------- -// 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,+)) { - 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, &[u8]) -> Vec>, - - // Pick Func - Box)>, - - // 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().into()); - } - } - 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 = 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 deleted file mode 100644 index 78ae2b0..0000000 --- a/mingling_picker/src/picker/patterns.rs +++ /dev/null @@ -1,205 +0,0 @@ -use mingling_picker_macros::internal_repeat; - -use crate::{Pickable, 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, - ( - pub(crate) arg_$: &'a PickerArg<'a, T$>, - pub(crate) result_$: PickerArgResult, - pub(crate) default_$: Option T$>>, - pub(crate) route_$: Option Route>>, - pub(crate) post_$: Option 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 - /// - #[allow(clippy::type_complexity)] - pub fn or(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 - /// - #[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 - /// - pub fn or_route(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(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 - /// - #[allow(clippy::type_complexity)] - pub fn post(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(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_$, - +) - } - } - - /// Picks a new arg with a default value provider in a single call. - /// - /// This is a shorthand for calling `.pick(arg).or(func)`. - /// - /// # Example - /// - #[allow(clippy::type_complexity)] - pub fn pick_or(self, arg: impl Into<&'a PickerArg<'a, N>>, func: F) -> PickerPattern$+<'a, (T$,+), N, Route> - where - N: Pickable<'a>, - F: FnMut() -> N + 'static, - F: 'static, - { - self.pick(arg).or(func) - } - - /// Picks a new arg with a default value in a single call. - /// - /// This is a shorthand for calling `.pick(arg).or_default()`. - /// - /// # Example - /// - #[allow(clippy::type_complexity)] - pub fn pick_or_default(self, arg: impl Into<&'a PickerArg<'a, N>>) -> PickerPattern$+<'a, (T$,+), N, Route> - where - N: Pickable<'a> + Default, - { - self.pick(arg).or_default() - } - - /// Picks a new arg with a route in a single call. - /// - /// This is a shorthand for calling `.pick(arg).or_route(func)`. - /// - /// # Example - /// - #[allow(clippy::type_complexity)] - pub fn pick_or_route(self, arg: impl Into<&'a PickerArg<'a, N>>, func: F) -> PickerPattern$+<'a, (T$,+), N, Route> - where - N: Pickable<'a>, - F: FnMut() -> Route + 'static, - F: 'static, - { - self.pick(arg).or_route(func) - } - } -}); diff --git a/mingling_picker/src/picker/result.rs b/mingling_picker/src/picker/result.rs deleted file mode 100644 index 9cd78ae..0000000 --- a/mingling_picker/src/picker/result.rs +++ /dev/null @@ -1,56 +0,0 @@ -#![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, - - ( - #[doc = concat!("The optional value for the ", $, "th type parameter.")] - pub v$: Option, - +) - } -}); - -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,+)) { - ((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()) - } - } -}); diff --git a/mingling_picker/src/value.rs b/mingling_picker/src/value.rs deleted file mode 100644 index 995aa00..0000000 --- a/mingling_picker/src/value.rs +++ /dev/null @@ -1,5 +0,0 @@ -mod flag; -pub use flag::*; - -mod vec_until; -pub use vec_until::*; diff --git a/mingling_picker/src/value/flag.rs b/mingling_picker/src/value/flag.rs deleted file mode 100644 index ee0d6ee..0000000 --- a/mingling_picker/src/value/flag.rs +++ /dev/null @@ -1,145 +0,0 @@ -use std::{ - fmt::{Debug, Display}, - ops::{Deref, Not}, -}; - -/// Parsed result of a boolean-style command-line flag. -/// -/// `Flag` is a **value type** that can be declared in [`PickerArg`]. -/// When the user passes `--verbose` on the command line, the parsed result is `Flag::Active`; -/// when the flag is absent, the result is `Flag::Inactive`. -/// -/// # Why not just `bool`? -/// -/// Unlike a raw `bool`, `Flag` carries **explicit semantics** about whether -/// the flag was actually provided by the user (`Active`) or simply omitted -/// (`Inactive`). This distinction matters when you want to distinguish -/// "the user intentionally omitted the flag" from "the flag was processed but -/// resolved to false" — the `Pickable` implementation for `Flag` always -/// returns `Parsed(Flag::Inactive)` when no matching argument is found, -/// rather than `NotFound`, making it always succeed with a meaningful default. -/// -/// # Conversions -/// -/// `Flag` interoperates seamlessly with `bool`: `Flag::Active` is `true`, -/// `Flag::Inactive` is `false`. The [`Deref`] impl allows using a `Flag` -/// directly in boolean contexts: -/// -/// ``` -/// # use mingling_picker::value::Flag; -/// let flag = Flag::Active; -/// if *flag { /* runs */ } -/// ``` -/// -/// [`PickerArg`]: crate::PickerArg -#[derive(Default, Clone, Copy, PartialEq, Eq)] -pub enum Flag { - /// The flag was **not** present on the command line. - /// - /// This is the default state, equivalent to `false`. - #[default] - Inactive, - - /// The flag **was** present on the command line. - /// - /// Equivalent to `true`. - Active, -} - -impl Debug for Flag { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::Inactive => write!(f, "inactive"), - Self::Active => write!(f, "active"), - } - } -} - -impl Display for Flag { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::Inactive => write!(f, "inactive"), - Self::Active => write!(f, "active"), - } - } -} - -impl Flag { - /// Converts this `Flag` into a `bool`. - /// - /// Returns `true` if the flag is [`Active`], `false` if [`Inactive`]. - /// - /// # Examples - /// - /// ``` - /// # use mingling_picker::value::Flag; - /// assert!(Flag::Active.bool()); - /// assert!(!Flag::Inactive.bool()); - /// ``` - /// - /// [`Active`]: Flag::Active - /// [`Inactive`]: Flag::Inactive - #[must_use] - #[inline(always)] - pub fn bool(&self) -> bool { - *self == Flag::Active - } -} - -impl PartialEq for Flag { - fn eq(&self, other: &bool) -> bool { - self.bool() == *other - } -} - -/// Compares `bool` with `Flag` using `==`. -impl PartialEq for bool { - fn eq(&self, other: &Flag) -> bool { - *self == other.bool() - } -} - -impl From for Flag { - fn from(value: bool) -> Self { - if value { Flag::Active } else { Flag::Inactive } - } -} - -impl From for bool { - fn from(val: Flag) -> Self { - val == Flag::Active - } -} - -/// Allows `Flag` to be used in boolean contexts via `*flag`. -/// -/// # Examples -/// -/// ``` -/// # use mingling_picker::value::Flag; -/// let flag = Flag::Active; -/// if *flag { -/// println!("flag is set"); -/// } -/// ``` -impl Deref for Flag { - type Target = bool; - - fn deref(&self) -> &bool { - match self { - Flag::Active => &true, - Flag::Inactive => &false, - } - } -} - -impl Not for Flag { - type Output = Flag; - - fn not(self) -> Flag { - match self { - Flag::Active => Flag::Inactive, - Flag::Inactive => Flag::Active, - } - } -} diff --git a/mingling_picker/src/value/vec_until.rs b/mingling_picker/src/value/vec_until.rs deleted file mode 100644 index 1b79641..0000000 --- a/mingling_picker/src/value/vec_until.rs +++ /dev/null @@ -1,134 +0,0 @@ -use std::marker::PhantomData; -use std::ops::{Deref, DerefMut}; - -use crate::{ - BoundaryCheck, MultiPickableWithBoundary, Pickable, PickerArg, PickerArgAttr, PickerArgResult, - SinglePickable, TagPhaseContext, - matcher_needed::Matcher, - parselib::{MultiArgMatcher, ParserStyle}, -}; - -/// A `Vec`-like container that stops collecting when [`BoundaryCheck`] -/// returns `true`. -/// -/// This type exists to signal "I know what I'm doing with boundaries" -/// at the type level (as opposed to `Vec` which greedily takes -/// everything). -#[derive(Debug, Clone, PartialEq, Eq, Default)] -pub struct VecUntil { - pub(crate) inner: Vec, - _marker: PhantomData, -} - -impl VecUntil { - pub fn into_inner(self) -> Vec { - self.inner - } -} - -impl From> for VecUntil { - fn from(v: Vec) -> Self { - VecUntil { - inner: v, - _marker: PhantomData, - } - } -} - -impl From> for Vec { - fn from(v: VecUntil) -> Self { - v.inner - } -} - -impl Deref for VecUntil { - type Target = Vec; - fn deref(&self) -> &Vec { - &self.inner - } -} - -impl DerefMut for VecUntil { - fn deref_mut(&mut self) -> &mut Vec { - &mut self.inner - } -} - -// MultiPickableWithBoundary impl - -impl MultiPickableWithBoundary for VecUntil -where - T: SinglePickable + BoundaryCheck, -{ - type Checker = T; - - fn pick_multi(raw: Vec) -> PickerArgResult { - let mut inner = Vec::with_capacity(raw.len()); - for s in &raw { - match T::pick_single(Some(s)) { - PickerArgResult::Parsed(v) => inner.push(v), - PickerArgResult::NotFound => return PickerArgResult::NotFound, - PickerArgResult::Unparsed => {} - } - } - PickerArgResult::Parsed(VecUntil { - inner, - _marker: PhantomData, - }) - } -} - -// Pickable impl - -impl<'a, T> Pickable<'a> for VecUntil -where - T: SinglePickable + BoundaryCheck, -{ - fn get_attr(flag: &'a PickerArg<'a, Self>) -> PickerArgAttr { - PickerArgAttr::positional_or_multi(flag) - } - - fn tag(ctx: TagPhaseContext) -> Vec { - let args = ctx.args; - let is_positional = ctx.arg_info.positional; - let positions = MultiArgMatcher::match_all(ctx.into()); - if positions.is_empty() { - return positions; - } - - let start = if is_positional { 0 } else { 1 }; - if start >= positions.len() { - return positions; - } - - let mut cut = start; - for &idx in &positions[start..] { - if let Some(raw) = args.get(idx) - && T::check_boundary(raw) - { - break; - } - cut += 1; - } - - positions[..cut].to_vec() - } - - fn pick(raw_strs: &[&str]) -> PickerArgResult { - let strs = strip_flag(raw_strs); - let owned: Vec = strs.iter().map(|&s| s.to_string()).collect(); - as MultiPickableWithBoundary>::pick_multi(owned) - } -} - -/// If the first raw string looks like a named flag (starts with the -/// style's long or short prefix), strip it — it's the flag, not a value. -fn strip_flag<'a>(raw_strs: &'a [&'a str]) -> &'a [&'a str] { - if let Some(first) = raw_strs.first() { - let style = ParserStyle::global_style(); - if first.starts_with(style.long_prefix) || first.starts_with(style.short_prefix) { - return &raw_strs[1..]; - } - } - raw_strs -} diff --git a/mingling_picker/test/Cargo.lock b/mingling_picker/test/Cargo.lock deleted file mode 100644 index 0fef84b..0000000 --- a/mingling_picker/test/Cargo.lock +++ /dev/null @@ -1,68 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "just_fmt" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6170dccbc3ea15dfb7f2da964097f814aba1dd8f746d4ffc56f33245c38e6d96" - -[[package]] -name = "mingling_picker" -version = "0.3.0" -dependencies = [ - "just_fmt", - "mingling_picker_macros", -] - -[[package]] -name = "mingling_picker_macros" -version = "0.3.0" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "proc-macro2" -version = "1.0.106" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "quote" -version = "1.0.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "syn" -version = "2.0.119" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "test-mingling-picker" -version = "0.1.0" -dependencies = [ - "mingling_picker", -] - -[[package]] -name = "unicode-ident" -version = "1.0.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" diff --git a/mingling_picker/test/Cargo.toml b/mingling_picker/test/Cargo.toml deleted file mode 100644 index 127546c..0000000 --- a/mingling_picker/test/Cargo.toml +++ /dev/null @@ -1,9 +0,0 @@ -[package] -name = "test-mingling-picker" -version = "0.1.0" -edition = "2024" - -[workspace] - -[dependencies] -mingling_picker = { path = "../" } diff --git a/mingling_picker/test/src/lib.rs b/mingling_picker/test/src/lib.rs deleted file mode 100644 index eac6cad..0000000 --- a/mingling_picker/test/src/lib.rs +++ /dev/null @@ -1,23 +0,0 @@ -// Using `assert_eq!(x, true)` is clearer than `assert!(x)` for expressing expected values -// -// BECAUSE `assert!` only checks if the boolean value is true, -// while `assert_eq!` explicitly shows the expected value -#![allow(clippy::bool_assert_comparison)] - -#[cfg(test)] -mod test; - -use mingling_picker::parselib::MaskedArg; - -/// Create a single `MaskedArg` from a raw string and its original index. -pub fn make_masked(raw: &str, idx: usize) -> MaskedArg<'_> { - MaskedArg { raw, raw_idx: idx } -} - -/// Create a `Vec` from an array of `(raw, raw_idx)` pairs. -pub fn make_args<'a>(pairs: &'a [(&'a str, usize)]) -> Vec> { - pairs - .iter() - .map(|&(raw, idx)| MaskedArg { raw, raw_idx: idx }) - .collect() -} diff --git a/mingling_picker/test/src/test.rs b/mingling_picker/test/src/test.rs deleted file mode 100644 index 9c53514..0000000 --- a/mingling_picker/test/src/test.rs +++ /dev/null @@ -1,10 +0,0 @@ -mod arg_matcher_test; -mod basic_test; -mod multi_arg_test; -mod multi_value_test; -mod pos_matcher_test; -mod priority_test; -mod route_test; -mod style_test; -mod value_flag_test; -mod value_string_test; diff --git a/mingling_picker/test/src/test/arg_matcher_test.rs b/mingling_picker/test/src/test/arg_matcher_test.rs deleted file mode 100644 index 7b37bc8..0000000 --- a/mingling_picker/test/src/test/arg_matcher_test.rs +++ /dev/null @@ -1,289 +0,0 @@ -use mingling_picker::PickerArgInfo; -use mingling_picker::parselib::{ArgMatcher, Matcher, POWERSHELL_STYLE, UNIX_STYLE}; - -use crate::make_args; - -// on_match_one — Named - -#[test] -fn test_match_one_named_basic() { - let mut info = PickerArgInfo::new(); - info.set_long("name"); - let args = make_args(&[("--name", 0), ("Alice", 1)]); - let result = ArgMatcher::on_match_one(&args, &UNIX_STYLE, &info); - assert_eq!(result, Some(0)); -} - -#[test] -fn test_match_one_named_eq_mode() { - let mut info = PickerArgInfo::new(); - info.set_long("name"); - let args = make_args(&[("--name=Alice", 0)]); - let result = ArgMatcher::on_match_one(&args, &UNIX_STYLE, &info); - assert_eq!(result, Some(0)); -} - -#[test] -fn test_match_one_named_no_match() { - let mut info = PickerArgInfo::new(); - info.set_long("name"); - let args = make_args(&[("--other", 0), ("Alice", 1)]); - let result = ArgMatcher::on_match_one(&args, &UNIX_STYLE, &info); - assert_eq!(result, None); -} - -#[test] -fn test_match_one_named_short_flag() { - let mut info = PickerArgInfo::new(); - info.set_long("name"); - info.set_short('n'); - let args = make_args(&[("-n", 0), ("Alice", 1)]); - let result = ArgMatcher::on_match_one(&args, &UNIX_STYLE, &info); - assert_eq!(result, Some(0)); -} - -#[test] -fn test_match_one_named_after_end_of_options() { - // Flags after `--` should not be matched. - let mut info = PickerArgInfo::new(); - info.set_long("name"); - let args = make_args(&[("--", 0), ("--name", 1), ("Alice", 2)]); - let result = ArgMatcher::on_match_one(&args, &UNIX_STYLE, &info); - assert_eq!(result, None); -} - -// on_match_one — Positional - -#[test] -fn test_match_one_positional_basic() { - let mut info = PickerArgInfo::new(); - info.set_positional(true); - let args = make_args(&[("file.txt", 0)]); - let result = ArgMatcher::on_match_one(&args, &UNIX_STYLE, &info); - assert_eq!(result, Some(0)); -} - -#[test] -fn test_match_one_positional_takes_first() { - let mut info = PickerArgInfo::new(); - info.set_positional(true); - let args = make_args(&[("a.txt", 0), ("b.txt", 1)]); - let result = ArgMatcher::on_match_one(&args, &UNIX_STYLE, &info); - assert_eq!(result, Some(0)); -} - -// on_match_all — Named, single occurrence - -#[test] -fn test_match_all_named_flag_plus_value() { - let mut info = PickerArgInfo::new(); - info.set_long("name"); - let args = make_args(&[("--name", 0), ("Alice", 1)]); - let result = ArgMatcher::on_match_all(&args, &UNIX_STYLE, &info); - assert_eq!(result, vec![0, 1]); -} - -#[test] -fn test_match_all_named_eq_mode() { - // --name=Alice: value is inline, only tag the flag position. - let mut info = PickerArgInfo::new(); - info.set_long("name"); - let args = make_args(&[("--name=Alice", 0)]); - let result = ArgMatcher::on_match_all(&args, &UNIX_STYLE, &info); - assert_eq!(result, vec![0]); -} - -#[test] -fn test_match_all_named_no_value() { - // Flag at end with no following arg: only tag the flag. - let mut info = PickerArgInfo::new(); - info.set_long("name"); - let args = make_args(&[("--name", 0)]); - let result = ArgMatcher::on_match_all(&args, &UNIX_STYLE, &info); - assert_eq!(result, vec![0]); -} - -#[test] -fn test_match_all_named_value_looks_like_flag() { - // The next arg looks like a flag — still tag it. - // Validation is the Pickable's responsibility. - let mut info = PickerArgInfo::new(); - info.set_long("name"); - let args = make_args(&[("--name", 0), ("--other", 1)]); - let result = ArgMatcher::on_match_all(&args, &UNIX_STYLE, &info); - assert_eq!(result, vec![0, 1]); -} - -// on_match_all — Named, multiple occurrences (Single per flag) - -#[test] -fn test_match_all_named_two_occurrences() { - // --name Alice --name Bob → each occurrence gets one value. - let mut info = PickerArgInfo::new(); - info.set_long("name"); - let args = make_args(&[("--name", 0), ("Alice", 1), ("--name", 2), ("Bob", 3)]); - let result = ArgMatcher::on_match_all(&args, &UNIX_STYLE, &info); - assert_eq!(result, vec![0, 1, 2, 3]); -} - -#[test] -fn test_match_all_named_skips_non_matching_args() { - // --val a b --val d → only pairs (0,1) and (3,4); idx 2 ("b") left free. - let mut info = PickerArgInfo::new(); - info.set_long("val"); - let args = make_args(&[("--val", 0), ("a", 1), ("b", 2), ("--val", 3), ("d", 4)]); - let result = ArgMatcher::on_match_all(&args, &UNIX_STYLE, &info); - assert_eq!(result, vec![0, 1, 3, 4]); -} - -// on_match_all — Named, short flag - -#[test] -fn test_match_all_named_short_flag() { - let mut info = PickerArgInfo::new(); - info.set_long("name"); - info.set_short('n'); - let args = make_args(&[("-n", 0), ("Alice", 1)]); - let result = ArgMatcher::on_match_all(&args, &UNIX_STYLE, &info); - assert_eq!(result, vec![0, 1]); -} - -// on_match_all — Named, eq + non-eq mixed - -#[test] -fn test_match_all_named_mixed_eq_and_regular() { - let mut info = PickerArgInfo::new(); - info.set_long("name"); - let args = make_args(&[("--name=Alice", 0), ("--name", 1), ("Bob", 2)]); - let result = ArgMatcher::on_match_all(&args, &UNIX_STYLE, &info); - assert_eq!(result, vec![0, 1, 2]); -} - -// on_match_all — Named, case insensitive (PowerShell) - -#[test] -fn test_match_all_named_powershell_case_insensitive() { - let mut info = PickerArgInfo::new(); - info.set_long("Name"); - let args = make_args(&[("-name", 0), ("Alice", 1)]); - let result = ArgMatcher::on_match_all(&args, &POWERSHELL_STYLE, &info); - assert_eq!(result, vec![0, 1]); -} - -// on_match_all — Positional - -#[test] -fn test_match_all_positional_single() { - let mut info = PickerArgInfo::new(); - info.set_positional(true); - let args = make_args(&[("file.txt", 0)]); - let result = ArgMatcher::on_match_all(&args, &UNIX_STYLE, &info); - assert_eq!(result, vec![0]); -} - -#[test] -fn test_match_all_positional_multiple() { - let mut info = PickerArgInfo::new(); - info.set_positional(true); - let args = make_args(&[("a.txt", 0), ("b.txt", 1)]); - let result = ArgMatcher::on_match_all(&args, &UNIX_STYLE, &info); - assert_eq!(result, vec![0, 1]); -} - -// End-of-options marker (`--`) - -#[test] -fn test_match_all_named_stops_at_end_of_options() { - // --name before `--` should match, --name after should not. - let mut info = PickerArgInfo::new(); - info.set_long("name"); - let args = make_args(&[ - ("--name", 0), - ("Alice", 1), - ("--", 2), - ("--name", 3), - ("Bob", 4), - ]); - let result = ArgMatcher::on_match_all(&args, &UNIX_STYLE, &info); - assert_eq!(result, vec![0, 1]); -} - -#[test] -fn test_match_all_positional_stops_at_end_of_options() { - let mut info = PickerArgInfo::new(); - info.set_positional(true); - let args = make_args(&[("before", 0), ("--", 1), ("after", 2)]); - let result = ArgMatcher::on_match_all(&args, &UNIX_STYLE, &info); - assert_eq!(result, vec![0]); -} - -// Empty args - -#[test] -fn test_match_one_empty() { - let mut info = PickerArgInfo::new(); - info.set_long("name"); - let args = vec![]; - let result = ArgMatcher::on_match_one(&args, &UNIX_STYLE, &info); - assert_eq!(result, None); -} - -#[test] -fn test_match_all_empty() { - let mut info = PickerArgInfo::new(); - info.set_long("name"); - let args = vec![]; - let result = ArgMatcher::on_match_all(&args, &UNIX_STYLE, &info); - assert!(result.is_empty()); -} - -// Verify that -- itself is never matched as a flag - -#[test] -fn test_match_all_end_of_options_not_matched() { - // `--` should neither match as a flag nor take a value. - let mut info = PickerArgInfo::new(); - info.set_long("name"); - let args = make_args(&[("--", 0)]); - let result = ArgMatcher::on_match_all(&args, &UNIX_STYLE, &info); - assert!(result.is_empty()); -} - -// `--` should never be consumed as a value by a named flag. - -#[test] -fn test_match_all_named_does_not_consume_end_marker() { - // `--flag` before `--`, but the next position IS `--`. - // Only tag the flag, don't consume the end-of-options marker. - let mut info = PickerArgInfo::new(); - info.set_long("flag"); - let args = make_args(&[("--flag", 0), ("--", 1), ("value", 2)]); - let result = ArgMatcher::on_match_all(&args, &UNIX_STYLE, &info); - assert_eq!(result, vec![0], "should NOT consume -- as a value"); -} - -#[test] -fn test_match_all_named_does_not_consume_end_marker_flag_before_end() { - // Simulates: --flag -- you where -- is end-of-options. - // The flag should be tagged but -- should NOT be consumed as its value. - let mut info = PickerArgInfo::new(); - info.set_long("flag"); - - let args = make_args(&[("--flag", 0), ("--", 1), ("you", 2)]); - let result = ArgMatcher::on_match_all(&args, &UNIX_STYLE, &info); - assert_eq!( - result, - vec![0], - "flag before --: only tag flag, leave -- for positional" - ); -} - -#[test] -fn test_match_all_flag_after_end_has_value() { - // Flag after `--` should not match at all. - let mut info = PickerArgInfo::new(); - info.set_long("flag"); - let args = make_args(&[("--", 0), ("--flag", 1), ("value", 2)]); - let result = ArgMatcher::on_match_all(&args, &UNIX_STYLE, &info); - assert!(result.is_empty()); -} diff --git a/mingling_picker/test/src/test/basic_test.rs b/mingling_picker/test/src/test/basic_test.rs deleted file mode 100644 index 78b154e..0000000 --- a/mingling_picker/test/src/test/basic_test.rs +++ /dev/null @@ -1,223 +0,0 @@ -use mingling_picker::{IntoPicker, macros::arg}; - -// Basic bool flag — present / absent - -#[test] -fn test_bool_flag_present() { - let args = vec!["--verbose"]; - let parsed = args - .to_picker() - .pick(&arg![verbose: bool]) - .or_default() - .unwrap(); - assert_eq!(parsed, true); -} - -#[test] -fn test_bool_flag_absent() { - let args: Vec<&str> = vec![]; - let parsed = args - .to_picker() - .pick(&arg![verbose: bool]) - .or_default() - .unwrap(); - assert_eq!(parsed, false); -} - -// Short flag — '-v' - -#[test] -fn test_bool_short_flag_present() { - let args = vec!["-v"]; - let parsed = args - .to_picker() - .pick(&arg![verbose: bool, 'v']) - .or_default() - .unwrap(); - assert_eq!(parsed, true); -} - -// Multiple bool flags at once - -#[test] -fn test_two_bool_flags_both_present() { - // UNIX_STYLE now uses Kebab naming: `flag_a` → "flag-a" → --flag-a - let args = vec!["--flag-a", "--flag-b"]; - let (a, b) = args - .to_picker() - .pick(&arg![flag_a: bool]) - .or_default() - .pick(&arg![flag_b: bool]) - .or_default() - .unwrap(); - assert_eq!(a, true); - assert_eq!(b, true); -} - -#[test] -fn test_two_bool_flags_one_present() { - let args = vec!["--flag-a"]; - let (a, b) = args - .to_picker() - .pick(&arg![flag_a: bool]) - .or_default() - .pick(&arg![flag_b: bool]) - .or_default() - .unwrap(); - assert_eq!(a, true); - assert_eq!(b, false); -} - -#[test] -fn test_two_bool_flags_neither_present() { - let args: Vec<&str> = vec![]; - let (a, b) = args - .to_picker() - .pick(&arg![flag_a: bool]) - .or_default() - .pick(&arg![flag_b: bool]) - .or_default() - .unwrap(); - assert_eq!(a, false); - assert_eq!(b, false); -} - -// Mixed short and long flags - -#[test] -fn test_short_and_long_flags() { - let args = vec!["-a", "--long-b"]; - let (a, b) = args - .to_picker() - .pick(&arg![flag_a: bool, 'a']) - .or_default() - .pick(&arg![long_b: bool]) - .or_default() - .unwrap(); - assert_eq!(a, true); - assert_eq!(b, true); -} - -// Flags after `--` (end-of-options marker) should not be parsed. - -#[test] -fn test_flag_after_end_of_options() { - let args = vec!["--", "--verbose"]; - let parsed = args - .to_picker() - .pick(&arg![verbose: bool]) - .or_default() - .unwrap(); - assert_eq!(parsed, false); -} - -// Alias matching for bool flags - -#[test] -fn test_bool_flag_with_alias() { - let args = vec!["--cfg"]; - let parsed = args - .to_picker() - .pick(&arg![config: bool, "cfg"]) - .or_default() - .unwrap(); - assert_eq!(parsed, true); -} - -#[test] -fn test_bool_flag_primary_name() { - let args = vec!["--config"]; - let parsed = args - .to_picker() - .pick(&arg![config: bool, "cfg"]) - .or_default() - .unwrap(); - assert_eq!(parsed, true); -} - -// Short flag + alias for bool flag - -#[test] -fn test_bool_flag_short_and_alias() { - let args = vec!["-v"]; - let parsed = args - .to_picker() - .pick(&arg![verbose: bool, 'v', "cfg"]) - .or_default() - .unwrap(); - assert_eq!(parsed, true); -} - -// Default values: .or() / .or_default() - -#[test] -fn test_or_default_without_args() { - let args: Vec<&str> = vec![]; - let parsed = args - .to_picker() - .pick(&arg![verbose: bool]) - .or_default() - .unwrap(); - assert_eq!(parsed, false); -} - -#[test] -fn test_or_custom_default() { - let args: Vec<&str> = vec![]; - let parsed = args - .to_picker() - .pick(&arg![verbose: bool]) - .or(|| true) - .unwrap(); - assert_eq!(parsed, true); -} - -// to_result / to_option interface - -#[test] -fn test_to_result_ok() { - let args = vec!["--verbose"]; - let result = args - .to_picker() - .pick(&arg![verbose: bool]) - .or_default() - .to_result(); - assert_eq!(result, Ok(true)); -} - -#[test] -fn test_to_option_some() { - let args = vec!["--verbose"]; - let opt = args - .to_picker() - .pick(&arg![verbose: bool]) - .or_default() - .to_option(); - assert_eq!(opt, Some(true)); -} - -// Chain with_route passthrough - -#[test] -fn test_with_route_chain() { - let args = vec!["--flag"]; - let parsed = args - .with_route::() - .pick(&arg![flag: bool]) - .or_default() - .unwrap(); - assert_eq!(parsed, true); -} - -// Unrelated flag should not match - -#[test] -fn test_unrelated_flag_does_not_match() { - let args = vec!["--other"]; - let parsed = args - .to_picker() - .pick(&arg![verbose: bool]) - .or_default() - .unwrap(); - assert_eq!(parsed, false); -} diff --git a/mingling_picker/test/src/test/multi_arg_test.rs b/mingling_picker/test/src/test/multi_arg_test.rs deleted file mode 100644 index 377357e..0000000 --- a/mingling_picker/test/src/test/multi_arg_test.rs +++ /dev/null @@ -1,181 +0,0 @@ -use mingling_picker::parselib::{Matcher, MultiArgMatcher, UNIX_STYLE}; -use mingling_picker::PickerArgInfo; - -use crate::make_args; - -// on_match_one — basic - -#[test] -fn test_multi_one_finds_first_flag() { - let mut info = PickerArgInfo::new(); - info.set_long("val"); - let args = make_args(&[("--other", 0), ("--val", 1), ("a", 2), ("b", 3)]); - let result = MultiArgMatcher::on_match_one(&args, &UNIX_STYLE, &info); - assert_eq!(result, Some(1)); -} - -#[test] -fn test_multi_one_no_match() { - let mut info = PickerArgInfo::new(); - info.set_long("val"); - let args = make_args(&[("--other", 0)]); - let result = MultiArgMatcher::on_match_one(&args, &UNIX_STYLE, &info); - assert_eq!(result, None); -} - -// on_match_all — named, basic multi-value - -#[test] -fn test_multi_all_flag_takes_all_values_until_next_flag() { - // --val a b c → all three values belong to --val - let mut info = PickerArgInfo::new(); - info.set_long("val"); - let args = make_args(&[("--val", 0), ("a", 1), ("b", 2), ("c", 3)]); - let result = MultiArgMatcher::on_match_all(&args, &UNIX_STYLE, &info); - assert_eq!(result, vec![0, 1, 2, 3]); -} - -#[test] -fn test_multi_all_stops_at_next_flag() { - // --val a b --other c d → only a,b belong to --val - let mut info = PickerArgInfo::new(); - info.set_long("val"); - let args = make_args(&[("--val", 0), ("a", 1), ("b", 2), ("--other", 3), ("c", 4)]); - let result = MultiArgMatcher::on_match_all(&args, &UNIX_STYLE, &info); - assert_eq!(result, vec![0, 1, 2]); -} - -#[test] -fn test_multi_all_flag_no_values() { - // --val at end with no values → just the flag - let mut info = PickerArgInfo::new(); - info.set_long("val"); - let args = make_args(&[("--val", 0)]); - let result = MultiArgMatcher::on_match_all(&args, &UNIX_STYLE, &info); - assert_eq!(result, vec![0]); -} - -#[test] -fn test_multi_all_empty() { - let mut info = PickerArgInfo::new(); - info.set_long("val"); - let args = vec![]; - let result = MultiArgMatcher::on_match_all(&args, &UNIX_STYLE, &info); - assert!(result.is_empty()); -} - -// on_match_all — named, multiple occurrences of same flag - -#[test] -fn test_multi_all_two_occurrences() { - // --val a b --val c d → two groups - let mut info = PickerArgInfo::new(); - info.set_long("val"); - let args = make_args(&[ - ("--val", 0), ("a", 1), ("b", 2), - ("--val", 3), ("c", 4), ("d", 5), - ]); - let result = MultiArgMatcher::on_match_all(&args, &UNIX_STYLE, &info); - assert_eq!(result, vec![0, 1, 2, 3, 4, 5]); -} - -#[test] -fn test_multi_all_skips_non_matching_args() { - // --val a --other b --val c → only --val groups - let mut info = PickerArgInfo::new(); - info.set_long("val"); - let args = make_args(&[ - ("--val", 0), ("a", 1), - ("--other", 2), ("b", 3), - ("--val", 4), ("c", 5), - ]); - let result = MultiArgMatcher::on_match_all(&args, &UNIX_STYLE, &info); - assert_eq!(result, vec![0, 1, 4, 5]); -} - -// on_match_all — named, eq mode - -#[test] -fn test_multi_all_eq_mode() { - // --val=a b → eq mode + one extra value - let mut info = PickerArgInfo::new(); - info.set_long("val"); - let args = make_args(&[("--val=a", 0), ("b", 1)]); - let result = MultiArgMatcher::on_match_all(&args, &UNIX_STYLE, &info); - assert_eq!(result, vec![0, 1]); -} - -#[test] -fn test_multi_all_eq_mode_no_extra() { - // --val=a alone → just the eq flag - let mut info = PickerArgInfo::new(); - info.set_long("val"); - let args = make_args(&[("--val=a", 0)]); - let result = MultiArgMatcher::on_match_all(&args, &UNIX_STYLE, &info); - assert_eq!(result, vec![0]); -} - -#[test] -fn test_multi_all_mixed_eq_and_regular() { - // --val=a b --val c d - let mut info = PickerArgInfo::new(); - info.set_long("val"); - let args = make_args(&[ - ("--val=a", 0), ("b", 1), ("--val", 2), ("c", 3), ("d", 4), - ]); - let result = MultiArgMatcher::on_match_all(&args, &UNIX_STYLE, &info); - assert_eq!(result, vec![0, 1, 2, 3, 4]); -} - -// on_match_all — short flag - -#[test] -fn test_multi_all_short_flag() { - let mut info = PickerArgInfo::new(); - info.set_short('v'); - info.set_long("val"); - let args = make_args(&[("-v", 0), ("a", 1), ("b", 2)]); - let result = MultiArgMatcher::on_match_all(&args, &UNIX_STYLE, &info); - assert_eq!(result, vec![0, 1, 2]); -} - -// on_match_all — end-of-options marker - -#[test] -fn test_multi_all_stops_at_end_of_options() { - // --val a -- b → stops before `--` - let mut info = PickerArgInfo::new(); - info.set_long("val"); - let args = make_args(&[("--val", 0), ("a", 1), ("--", 2), ("b", 3)]); - let result = MultiArgMatcher::on_match_all(&args, &UNIX_STYLE, &info); - assert_eq!(result, vec![0, 1]); -} - -#[test] -fn test_multi_all_flag_after_end_ignored() { - let mut info = PickerArgInfo::new(); - info.set_long("val"); - let args = make_args(&[("--", 0), ("--val", 1), ("a", 2)]); - let result = MultiArgMatcher::on_match_all(&args, &UNIX_STYLE, &info); - assert!(result.is_empty()); -} - -// on_match_all — positional - -#[test] -fn test_multi_all_positional() { - let mut info = PickerArgInfo::new(); - info.set_positional(true); - let args = make_args(&[("a.txt", 0), ("b.txt", 1)]); - let result = MultiArgMatcher::on_match_all(&args, &UNIX_STYLE, &info); - assert_eq!(result, vec![0, 1]); -} - -#[test] -fn test_multi_all_positional_stops_at_end_of_options() { - let mut info = PickerArgInfo::new(); - info.set_positional(true); - let args = make_args(&[("a.txt", 0), ("--", 1), ("b.txt", 2)]); - let result = MultiArgMatcher::on_match_all(&args, &UNIX_STYLE, &info); - assert_eq!(result, vec![0]); -} diff --git a/mingling_picker/test/src/test/multi_value_test.rs b/mingling_picker/test/src/test/multi_value_test.rs deleted file mode 100644 index 0f56517..0000000 --- a/mingling_picker/test/src/test/multi_value_test.rs +++ /dev/null @@ -1,66 +0,0 @@ -use mingling_picker::value::{Flag, VecUntil}; -use mingling_picker::{IntoPicker, macros::arg}; - -#[test] -fn test_vec_until_i16_named() { - let (nums, rest): (VecUntil, Flag) = vec!["--nums", "1", "2", "3", "abc"] - .to_picker() - .pick(&arg![nums: VecUntil]) - .pick(&arg![rest: Flag]) - .unwrap(); - assert_eq!(*nums, vec![1i16, 2, 3]); - assert_eq!(rest, Flag::Inactive); -} - -#[test] -fn test_vec_until_i16_stops_at_non_number() { - let nums: VecUntil = vec!["--nums", "42", "abc", "100"] - .to_picker() - .pick(&arg![nums: VecUntil]) - .unwrap(); - assert_eq!(*nums, vec![42i16]); -} - -#[test] -fn test_vec_until_i16_empty() { - let nums: VecUntil = vec!["--nums"] - .to_picker() - .pick(&arg![nums: VecUntil]) - .unwrap(); - assert!(nums.is_empty()); -} - -#[test] -fn test_vec_until_i16_stops_at_next_flag() { - let nums: VecUntil = vec!["--nums", "1", "2", "--other", "3"] - .to_picker() - .pick(&arg![nums: VecUntil]) - .unwrap(); - assert_eq!(*nums, vec![1i16, 2]); -} - -#[test] -fn test_vec_until_i16_stops_at_end_of_options() { - let nums: VecUntil = vec!["--nums", "1", "2", "--", "3"] - .to_picker() - .pick(&arg![nums: VecUntil]) - .unwrap(); - assert_eq!(*nums, vec![1i16, 2]); -} - -// Two VecUntil with different boundary behaviours - -#[test] -fn test_vec_until_f64_and_i16_positional() { - // Both are positional VecUntil. f64 takes all valid floats, - // i16 takes what's left. But note: "1", "2", "3" are also valid - // f64 values, so f64's check_boundary never fires — it consumes - // everything, and i16 gets nothing. - let (floats, ints): (VecUntil, VecUntil) = vec!["1.5", "2.5", "3.5", "1", "2", "3"] - .to_picker() - .pick(&arg![VecUntil]) - .pick(&arg![VecUntil]) - .unwrap(); - assert_eq!(*floats, vec![1.5, 2.5, 3.5]); - assert_eq!(*ints, vec![1i16, 2, 3]); -} diff --git a/mingling_picker/test/src/test/pos_matcher_test.rs b/mingling_picker/test/src/test/pos_matcher_test.rs deleted file mode 100644 index b5319f5..0000000 --- a/mingling_picker/test/src/test/pos_matcher_test.rs +++ /dev/null @@ -1,141 +0,0 @@ -use mingling_picker::PickerArgInfo; -use mingling_picker::parselib::{MaskedArg, Matcher, PositionalMatcher, UNIX_STYLE, WINDOWS_STYLE}; - -fn make_args<'a>(pairs: &'a [(&'a str, usize)]) -> Vec> { - pairs - .iter() - .map(|&(raw, idx)| MaskedArg { raw, raw_idx: idx }) - .collect() -} - -// on_match_one — basic - -#[test] -fn test_pos_one_takes_first_non_flag() { - let info = PickerArgInfo::new(); - let args = make_args(&[("--verbose", 0), ("file.txt", 1)]); - let result = PositionalMatcher::on_match_one(&args, &UNIX_STYLE, &info); - assert_eq!(result, Some(1)); -} - -#[test] -fn test_pos_one_takes_first_if_no_flag() { - let info = PickerArgInfo::new(); - let args = make_args(&[("file.txt", 0)]); - let result = PositionalMatcher::on_match_one(&args, &UNIX_STYLE, &info); - assert_eq!(result, Some(0)); -} - -#[test] -fn test_pos_one_all_flags_returns_none() { - let info = PickerArgInfo::new(); - let args = make_args(&[("--verbose", 0), ("--name", 1)]); - let result = PositionalMatcher::on_match_one(&args, &UNIX_STYLE, &info); - assert_eq!(result, None); -} - -#[test] -fn test_pos_one_empty_returns_none() { - let info = PickerArgInfo::new(); - let args = vec![]; - let result = PositionalMatcher::on_match_one(&args, &UNIX_STYLE, &info); - assert_eq!(result, None); -} - -// on_match_one — after `--` - -#[test] -fn test_pos_one_after_end_takes_even_flag_like() { - // After `--`, accept everything including `--verbose`. - let info = PickerArgInfo::new(); - let args = make_args(&[("--", 0), ("--verbose", 1), ("file.txt", 2)]); - let result = PositionalMatcher::on_match_one(&args, &UNIX_STYLE, &info); - assert_eq!(result, Some(1)); -} - -#[test] -fn test_pos_one_only_end_returns_none() { - let info = PickerArgInfo::new(); - let args = make_args(&[("--", 0)]); - let result = PositionalMatcher::on_match_one(&args, &UNIX_STYLE, &info); - assert_eq!(result, None); -} - -// on_match_one — Windows style prefix - -#[test] -fn test_pos_one_windows_skips_slash_prefix() { - let info = PickerArgInfo::new(); - let args = make_args(&[("/Verbose", 0), ("file.txt", 1)]); - let result = PositionalMatcher::on_match_one(&args, &WINDOWS_STYLE, &info); - assert_eq!(result, Some(1)); -} - -// on_match_all — basic - -#[test] -fn test_pos_all_collects_non_flags() { - let info = PickerArgInfo::new(); - let args = make_args(&[("--verbose", 0), ("a.txt", 1), ("--name", 2), ("b.txt", 3)]); - let result = PositionalMatcher::on_match_all(&args, &UNIX_STYLE, &info); - assert_eq!(result, vec![1, 3]); -} - -#[test] -fn test_pos_all_only_flags_returns_empty() { - let info = PickerArgInfo::new(); - let args = make_args(&[("--a", 0), ("--b", 1)]); - let result = PositionalMatcher::on_match_all(&args, &UNIX_STYLE, &info); - assert!(result.is_empty()); -} - -#[test] -fn test_pos_all_empty() { - let info = PickerArgInfo::new(); - let args = vec![]; - let result = PositionalMatcher::on_match_all(&args, &UNIX_STYLE, &info); - assert!(result.is_empty()); -} - -// on_match_all — after `--` - -#[test] -fn test_pos_all_after_end_accepts_everything() { - // After `--`, even `--verbose` is accepted as positional. - let info = PickerArgInfo::new(); - let args = make_args(&[ - ("--verbose", 0), - ("a.txt", 1), - ("--", 2), - ("--flag", 3), - ("b.txt", 4), - ]); - let result = PositionalMatcher::on_match_all(&args, &UNIX_STYLE, &info); - assert_eq!(result, vec![1, 3, 4]); -} - -#[test] -fn test_pos_all_only_after_end() { - let info = PickerArgInfo::new(); - let args = make_args(&[("--", 0), ("arg1", 1), ("--arg2", 2)]); - let result = PositionalMatcher::on_match_all(&args, &UNIX_STYLE, &info); - assert_eq!(result, vec![1, 2]); -} - -// on_match_all — mixed before/after `--` - -#[test] -fn test_pos_all_mixed() { - let info = PickerArgInfo::new(); - let args = make_args(&[ - ("infile", 0), - ("--verbose", 1), - ("--", 2), - ("outfile", 3), - ("--extra", 4), - ]); - let result = PositionalMatcher::on_match_all(&args, &UNIX_STYLE, &info); - // Before `--`: "infile" (skip --verbose). - // After `--`: everything — "outfile", "--extra". - assert_eq!(result, vec![0, 3, 4]); -} diff --git a/mingling_picker/test/src/test/priority_test.rs b/mingling_picker/test/src/test/priority_test.rs deleted file mode 100644 index 8179389..0000000 --- a/mingling_picker/test/src/test/priority_test.rs +++ /dev/null @@ -1,85 +0,0 @@ -use mingling_picker::value::Flag; -use mingling_picker::{IntoPicker, macros::arg}; - -// Same flag name, different Pickable types -// -// PickerArgAttr priority: Flag < Single -// So Single and Multi should parse BEFORE Flag when sharing -// the same flag name. - -#[test] -fn test_single_takes_flag_when_sharing_name() { - // --name Alice: String consumes it, Flag sees nothing. - let (name, verbose): (String, Flag) = vec!["--name", "Alice"] - .to_picker() - .pick(&arg![name: String]) // Single, parsed first - .pick(&arg![name: Flag]) // Flag, parsed second, nothing left - .unwrap(); - assert_eq!(name, "Alice"); - assert_eq!(verbose, Flag::Inactive); -} - -#[test] -fn test_flag_only_triggers_when_single_missing() { - // --verbose: only Flag matches, String gets default. - let (name, verbose): (String, Flag) = vec!["--verbose"] - .to_picker() - .pick(&arg![name: String]) // Single, no match → default "" - .or_default() - .pick(&arg![name: Flag]) // Flag, no --name in args → Inactive - .unwrap(); - assert_eq!(name, ""); - assert_eq!(verbose, Flag::Inactive); -} - -#[test] -fn test_flag_gets_leftovers_after_single_consumes_value() { - // --name Alice --name: String takes "--name Alice", Flag takes "--name". - let (name, verbose): (String, Flag) = vec!["--name", "Alice", "--name"] - .to_picker() - .pick(&arg![name: String]) // Single: tag [0, 1], consumes --name Alice - .pick(&arg![name: Flag]) // Flag: tags position 2 (--name) - .unwrap(); - assert_eq!(name, "Alice"); - assert_eq!( - verbose, - Flag::Active, - "Flag should see --name at position 2 after String consumed positions 0-1" - ); -} - -#[test] -fn test_short_flag_sharing_same_letter() { - // -n Alice: String takes it, Flag misses. - let (name, verbose): (String, Flag) = vec!["-n", "Alice"] - .to_picker() - .pick(&arg![name: String, 'n']) // Single, parsed first - .pick(&arg![name: Flag, 'n']) // Flag, parsed second - .unwrap(); - assert_eq!(name, "Alice"); - assert_eq!(verbose, Flag::Inactive); -} - -#[test] -fn test_flag_captures_remaining_after_single_partial_consume() { - // --name Alice --verbose: String takes --name Alice, Flag takes --verbose. - let (name, verbose): (String, Flag) = vec!["--name", "Alice", "--verbose"] - .to_picker() - .pick(&arg![name: String]) // Single: tag [0, 1] - .pick(&arg![verbose: Flag]) // Flag: tag [2] - .unwrap(); - assert_eq!(name, "Alice"); - assert_eq!(verbose, Flag::Active); -} - -#[test] -fn test_single_skips_already_claimed_positions() { - // --verbose --name Alice: Flag takes --verbose, String takes --name Alice. - let (verbose, name): (Flag, String) = vec!["--verbose", "--name", "Alice"] - .to_picker() - .pick(&arg![verbose: Flag]) // Flag: tag [0] - .pick(&arg![name: String]) // Single: tag [1, 2] - .unwrap(); - assert_eq!(verbose, Flag::Active); - assert_eq!(name, "Alice"); -} diff --git a/mingling_picker/test/src/test/route_test.rs b/mingling_picker/test/src/test/route_test.rs deleted file mode 100644 index 9261db1..0000000 --- a/mingling_picker/test/src/test/route_test.rs +++ /dev/null @@ -1,200 +0,0 @@ -use mingling_picker::{IntoPicker, macros::arg}; - -// Route mechanism — or_route - -#[test] -fn test_or_route_triggered_to_result() { - // flag not present and no default value → route triggered → Err - let args: Vec<&str> = vec![]; - let result: Result = args - .with_route::<&'static str>() - .pick(&arg![verbose: bool]) - .or_route(|| "missing_verbose") - .to_result(); - assert_eq!(result, Err("missing_verbose")); -} - -#[test] -fn test_or_route_not_triggered_when_flag_present() { - // flag present → route not triggered → Ok - let args = vec!["--verbose"]; - let result: Result = args - .with_route::<&'static str>() - .pick(&arg![verbose: bool]) - .or_route(|| "missing_verbose") - .to_result(); - assert_eq!(result, Ok(true)); -} - -#[test] -fn test_or_default_priority_over_or_route() { - // or_default takes priority over or_route: even if the flag is absent, - // having a default prevents the route from being triggered - let args: Vec<&str> = vec![]; - let result: Result = args - .with_route::<&'static str>() - .pick(&arg![verbose: bool]) - .or_default() - .or_route(|| "should_not_reach") - .to_result(); - assert_eq!(result, Ok(false)); -} - -#[test] -fn test_or_route_to_option_returns_none() { - // When route is triggered, to_option returns None - let args: Vec<&str> = vec![]; - let opt: Option = args - .with_route::<&'static str>() - .pick(&arg![verbose: bool]) - .or_route(|| "missing") - .to_option(); - assert_eq!(opt, None); -} - -#[test] -#[should_panic(expected = "called `Option::unwrap()` on a `None` value")] -fn test_or_route_unwrap_panics() { - let args: Vec<&str> = vec![]; - args.with_route::<&'static str>() - .pick(&arg![verbose: bool]) - .or_route(|| "missing") - .unwrap(); -} - -#[test] -fn test_or_route_with_string_route() { - // Route type is String - let args: Vec<&str> = vec![]; - let result: Result = args - .with_route::() - .pick(&arg![verbose: bool]) - .or_route(|| "route_hit".to_string()) - .to_result(); - assert_eq!(result, Err("route_hit".to_string())); -} - -#[test] -fn test_or_route_with_custom_enum() { - // Route type is a custom enum - #[derive(Debug, Clone, Copy, PartialEq, Eq)] - #[allow(dead_code)] - enum Redirect { - Help, - Version, - } - - let args: Vec<&str> = vec![]; - let result: Result = args - .with_route::() - .pick(&arg![verbose: bool]) - .or_route(|| Redirect::Help) - .to_result(); - assert_eq!(result, Err(Redirect::Help)); -} - -// Route mechanism — multiple flags - -#[test] -fn test_two_flags_first_missing_triggers_route() { - // Both flags missing; first has or_route → Err("first"); - // first route wins (first-checked, first-triggered) - let args: Vec<&str> = vec![]; - let result: Result<(bool, bool), &'static str> = args - .with_route::<&'static str>() - .pick(&arg![flag_a: bool]) - .or_route(|| "first_missing") - .pick(&arg![flag_b: bool]) - .or_route(|| "second_missing") - .to_result(); - assert_eq!(result, Err("first_missing")); -} - -#[test] -fn test_two_flags_second_missing_triggers_route() { - // First has or_default (no route triggered), second missing with or_route → Err("second") - let args: Vec<&str> = vec![]; - let result: Result<(bool, bool), &'static str> = args - .with_route::<&'static str>() - .pick(&arg![flag_a: bool]) - .or_default() - .pick(&arg![flag_b: bool]) - .or_route(|| "second_missing") - .to_result(); - assert_eq!(result, Err("second_missing")); -} - -#[test] -fn test_two_flags_both_present_route_not_triggered() { - // Both flags present → route not triggered → Ok((true, true)) - let args = vec!["--flag-a", "--flag-b"]; - let result: Result<(bool, bool), &'static str> = args - .with_route::<&'static str>() - .pick(&arg![flag_a: bool]) - .or_route(|| "first_missing") - .pick(&arg![flag_b: bool]) - .or_route(|| "second_missing") - .to_result(); - assert_eq!(result, Ok((true, true))); -} - -#[test] -fn test_two_flags_first_missing_no_route_second_has_route() { - // First missing but has no or_route, second missing with or_route → Err("second") - // Note: the first has neither route nor default, so pick failure does not modify error_route - let args: Vec<&str> = vec![]; - let result: Result<(bool, bool), &'static str> = args - .with_route::<&'static str>() - .pick(&arg![flag_a: bool]) - // No or_default, no or_route either - .pick(&arg![flag_b: bool]) - .or_route(|| "second_missing") - .to_result(); - assert_eq!(result, Err("second_missing")); -} - -#[test] -fn test_two_flags_only_second_has_default() { - // First is missing with no default/route (v1=NotFound), second is missing but has or_default - // Use unpack to check both results - let args: Vec<&str> = vec![]; - let (a, b) = args - .to_picker() - .pick(&arg![flag_a: bool]) - .pick(&arg![flag_b: bool]) - .or_default() - .unpack(); - assert_eq!(a, None); - assert_eq!(b, Some(false)); -} - -// Route with with_route - -#[test] -fn test_with_route_and_or_route() { - // with_route::<String>() sets the Route type + or_route triggers - let args: Vec<&str> = vec![]; - let result: Result = args - .with_route::() - .pick(&arg![verbose: bool]) - .or_route(|| "redirected".to_string()) - .to_result(); - assert_eq!(result, Err("redirected".to_string())); -} - -#[test] -fn test_with_route_type_switch() { - // with_route switching Route type clears old route_$ and error_route - let args: Vec<&str> = vec![]; - let result: Result<(bool, bool), i32> = args - .with_route::() - .pick(&arg![verbose: bool]) - .or_route(|| "string_route".to_string()) - .with_route::() - .pick(&arg![other: bool]) - .or_route(|| -1) - .to_result(); - // The first flag is missing, but its or_route is cleared when with_route switches (route_$ = None) - // The second flag is missing and has or_route → Err(-1) - assert_eq!(result, Err(-1)); -} diff --git a/mingling_picker/test/src/test/style_test.rs b/mingling_picker/test/src/test/style_test.rs deleted file mode 100644 index 3c337b9..0000000 --- a/mingling_picker/test/src/test/style_test.rs +++ /dev/null @@ -1,300 +0,0 @@ -use mingling_picker::PickerArgInfo; -use mingling_picker::parselib::{ - FlagMatcher, Matcher, POWERSHELL_STYLE, ParserStyle, ParserStyleNamingCase, UNIX_STYLE, - WINDOWS_STYLE, build_possible_flags, -}; - -use crate::make_masked; - -// Style: formatting utilities - -#[test] -fn test_unix_style_flag_string() { - assert_eq!(UNIX_STYLE.flag_string('v'), "-v"); - assert_eq!(UNIX_STYLE.flag_string("verbose"), "--verbose"); -} - -#[test] -fn test_windows_style_flag_string() { - assert_eq!(WINDOWS_STYLE.flag_string('v'), "/v"); - assert_eq!(WINDOWS_STYLE.flag_string("verbose"), "/verbose"); -} - -#[test] -fn test_powershell_style_flag_string() { - assert_eq!(POWERSHELL_STYLE.flag_string('v'), "-v"); - assert_eq!(POWERSHELL_STYLE.flag_string("Verbose"), "-Verbose"); -} - -#[test] -fn test_build_possible_flags_windows() { - // Build PickerArgInfo from a flag definition: `verbose: bool` - let mut info = PickerArgInfo::new(); - info.set_long("verbose"); - let flags = build_possible_flags(&WINDOWS_STYLE, &info); - assert_eq!(flags, vec!["/Verbose"]); -} - -#[test] -fn test_build_possible_flags_with_short_and_alias() { - let mut info = PickerArgInfo::new(); - info.set_short('n'); - info.set_long("name"); - info.set_alias(vec!["nickname"]); - let flags = build_possible_flags(&UNIX_STYLE, &info); - assert_eq!(flags, vec!["-n", "--name", "--nickname"]); -} - -// Style: matching with different styles via Matcher trait - -#[test] -fn test_windows_style_match() { - // Windows style: /verbose (case insensitive) - let mut info = PickerArgInfo::new(); - info.set_long("verbose"); - - let args = vec![make_masked("/verbose", 0)]; - let result = FlagMatcher::on_match_one(&args, &WINDOWS_STYLE, &info); - assert_eq!(result, Some(0)); -} - -#[test] -fn test_windows_style_match_case_insensitive() { - // Windows style is case-insensitive: /VERBOSE should match "verbose" - let mut info = PickerArgInfo::new(); - info.set_long("verbose"); - - let args = vec![make_masked("/VERBOSE", 0)]; - let result = FlagMatcher::on_match_one(&args, &WINDOWS_STYLE, &info); - assert_eq!(result, Some(0)); -} - -#[test] -fn test_windows_style_no_match_on_unrelated_flag() { - // Different flag should not match - let mut info = PickerArgInfo::new(); - info.set_long("verbose"); - - let args = vec![make_masked("/output", 0)]; - let result = FlagMatcher::on_match_one(&args, &WINDOWS_STYLE, &info); - assert_eq!(result, None); -} - -#[test] -fn test_powershell_style_match() { - // PowerShell style: -Verbose (case insensitive) - let mut info = PickerArgInfo::new(); - info.set_long("Verbose"); - - let args = vec![make_masked("-Verbose", 0)]; - let result = FlagMatcher::on_match_one(&args, &POWERSHELL_STYLE, &info); - assert_eq!(result, Some(0)); -} - -#[test] -fn test_powershell_style_match_case_insensitive() { - let mut info = PickerArgInfo::new(); - info.set_long("Verbose"); - - let args = vec![make_masked("-VERBOSE", 0)]; - let result = FlagMatcher::on_match_one(&args, &POWERSHELL_STYLE, &info); - assert_eq!(result, Some(0)); -} - -#[test] -fn test_unix_style_case_sensitive_no_match() { - // UNIX style is case-sensitive: --VERBOSE should NOT match --verbose - let mut info = PickerArgInfo::new(); - info.set_long("verbose"); - - let args = vec![make_masked("--VERBOSE", 0)]; - let result = FlagMatcher::on_match_one(&args, &UNIX_STYLE, &info); - assert_eq!(result, None); -} - -#[test] -fn test_windows_style_match_all() { - // on_match_all should find all matching flags - let mut info = PickerArgInfo::new(); - info.set_long("verbose"); - info.set_short('v'); - - let args = vec![ - make_masked("/v", 0), - make_masked("/output", 1), - make_masked("/VERBOSE", 2), - ]; - let result = FlagMatcher::on_match_all(&args, &WINDOWS_STYLE, &info); - assert_eq!(result, vec![0, 2]); -} - -#[test] -fn test_windows_style_match_after_end_of_options() { - // end_of_options is always "--" regardless of style - // Flags after -- should not match - let mut info = PickerArgInfo::new(); - info.set_long("verbose"); - - let args = vec![ - make_masked("/verbose", 0), - make_masked("--", 1), - make_masked("/verbose", 2), - ]; - let result = FlagMatcher::on_match_all(&args, &WINDOWS_STYLE, &info); - // end_of_options is always "--" regardless of style - assert_eq!(result, vec![0]); -} - -// Naming case conversion - -/// A Unix-like style with kebab-case naming. -const KEBAB_STYLE: ParserStyle<'static> = ParserStyle { - naming_case: ParserStyleNamingCase::Kebab, - ..UNIX_STYLE -}; - -#[test] -fn test_kebab_case_naming_for_multiword_flag() { - // flag name `flag_a` → Kebab → `flag-a` → `--flag-a` - let mut info = PickerArgInfo::new(); - info.set_long("flag_a"); - - let args = vec![make_masked("--flag-a", 0)]; - let result = FlagMatcher::on_match_one(&args, &KEBAB_STYLE, &info); - assert_eq!( - result, - Some(0), - "--flag-a should match flag_a via kebab-case conversion" - ); -} - -#[test] -fn test_snake_case_should_not_match_as_long_flag() { - // With kebab naming, `--flag_a` (snake) should NOT match `flag_a` - let mut info = PickerArgInfo::new(); - info.set_long("flag_a"); - - let args = vec![make_masked("--flag_a", 0)]; - let result = FlagMatcher::on_match_one(&args, &KEBAB_STYLE, &info); - assert_eq!( - result, None, - "--flag_a should NOT match in kebab-style context" - ); -} - -#[test] -fn test_kebab_case_naming_for_unix_style() { - // UNIX_STYLE now uses Kebab case: `flag_a` → `flag-a` → `--flag-a` - let mut info = PickerArgInfo::new(); - info.set_long("flag_a"); - - let args = vec![make_masked("--flag-a", 0)]; - let result = FlagMatcher::on_match_one(&args, &UNIX_STYLE, &info); - assert_eq!( - result, - Some(0), - "--flag-a should match with kebab-style UNIX_STYLE" - ); -} - -#[test] -fn test_snake_case_rejected_by_unix_style() { - // UNIX_STYLE uses Kebab: `--flag_a` (snake) should NOT match - let mut info = PickerArgInfo::new(); - info.set_long("flag_a"); - - let args = vec![make_masked("--flag_a", 0)]; - let result = FlagMatcher::on_match_one(&args, &UNIX_STYLE, &info); - assert_eq!( - result, None, - "--flag_a should NOT match under kebab-style UNIX_STYLE" - ); -} - -#[test] -fn test_powershell_pascal_case_naming() { - // POWERSHELL_STYLE uses Pascal case: `verbose` → `Verbose` → `-Verbose` - let mut info = PickerArgInfo::new(); - info.set_long("verbose"); - - let args = vec![make_masked("-Verbose", 0)]; - let result = FlagMatcher::on_match_one(&args, &POWERSHELL_STYLE, &info); - assert_eq!( - result, - Some(0), - "-Verbose should match verbose via Pascal case" - ); -} - -#[test] -fn test_flag_naming_kebab_matches_my_name() { - // UNIX_STYLE now uses Kebab: `my_name` → `my-name` → `--my-name` - let mut info = PickerArgInfo::new(); - info.set_long("my_name"); - - let args = vec![make_masked("--my-name", 0)]; - let result = FlagMatcher::on_match_one(&args, &UNIX_STYLE, &info); - assert_eq!( - result, - Some(0), - "--my-name should match via kebab conversion" - ); -} - -#[test] -fn test_flag_naming_kebab_rejects_my_name_underscore() { - // Kebab style: `--my_name` (snake) should NOT match - let mut info = PickerArgInfo::new(); - info.set_long("my_name"); - - let args = vec![make_masked("--my_name", 0)]; - let result = FlagMatcher::on_match_one(&args, &UNIX_STYLE, &info); - assert_eq!( - result, None, - "--my_name should NOT match under kebab-style UNIX_STYLE" - ); -} - -#[test] -fn test_flag_naming_pascal_matches_my_name() { - // `my_name` under Pascal → `MyName` → `-MyName` - let mut info = PickerArgInfo::new(); - info.set_long("my_name"); - - let args = vec![make_masked("-MyName", 0)]; - let result = FlagMatcher::on_match_one(&args, &POWERSHELL_STYLE, &info); - assert_eq!( - result, - Some(0), - "-MyName should match via Pascal conversion" - ); -} - -#[test] -fn test_flag_naming_pascal_matches_lowercase() { - // PowerShell is case-insensitive: `-myname` should also match - let mut info = PickerArgInfo::new(); - info.set_long("my_name"); - - let args = vec![make_masked("-myname", 0)]; - let result = FlagMatcher::on_match_one(&args, &POWERSHELL_STYLE, &info); - assert_eq!( - result, - Some(0), - "-myname (lowercase) should match via case-insensitive Pascal" - ); -} - -#[test] -fn test_flag_naming_pascal_rejects_my_name_underscore() { - // Pascal style: `-my_name` (underscore) should NOT match - let mut info = PickerArgInfo::new(); - info.set_long("my_name"); - - let args = vec![make_masked("-my_name", 0)]; - let result = FlagMatcher::on_match_one(&args, &POWERSHELL_STYLE, &info); - assert_eq!( - result, None, - "-my_name should NOT match under Pascal-style naming" - ); -} diff --git a/mingling_picker/test/src/test/value_flag_test.rs b/mingling_picker/test/src/test/value_flag_test.rs deleted file mode 100644 index d267a27..0000000 --- a/mingling_picker/test/src/test/value_flag_test.rs +++ /dev/null @@ -1,174 +0,0 @@ -use mingling_picker::value::Flag; -use mingling_picker::{IntoPicker, macros::arg}; - -// Basic Flag — present / absent - -#[test] -fn test_flag_present() { - let flag: Flag = vec!["--verbose"] - .to_picker() - .pick(&arg![verbose: Flag]) - .unwrap(); - assert_eq!(flag, Flag::Active); -} - -#[test] -fn test_flag_absent_returns_inactive() { - // Unlike bool, Flag::pick returns Parsed(Inactive) when no match is found, - // so or_default() is NOT required — unwrap() works directly. - let flag: Flag = Vec::<&str>::new() - .to_picker() - .pick(&arg![verbose: Flag]) - .unwrap(); - assert_eq!(flag, Flag::Inactive); -} - -// Short Flag - -#[test] -fn test_flag_short_present() { - let flag: Flag = vec!["-v"] - .to_picker() - .pick(&arg![verbose: Flag, 'v']) - .unwrap(); - assert_eq!(flag, Flag::Active); -} - -// Multiple Flags - -#[test] -fn test_two_flags_both_present() { - let (a, b): (Flag, Flag) = vec!["--flag-a", "--flag-b"] - .to_picker() - .pick(&arg![flag_a: Flag]) - .pick(&arg![flag_b: Flag]) - .unwrap(); - assert_eq!(a, Flag::Active); - assert_eq!(b, Flag::Active); -} - -#[test] -fn test_two_flags_one_present() { - let (a, b): (Flag, Flag) = vec!["--flag-a"] - .to_picker() - .pick(&arg![flag_a: Flag]) - .pick(&arg![flag_b: Flag]) - .unwrap(); - assert_eq!(a, Flag::Active); - assert_eq!(b, Flag::Inactive); -} - -#[test] -fn test_two_flags_neither_present() { - let (a, b): (Flag, Flag) = Vec::<&str>::new() - .to_picker() - .pick(&arg![flag_a: Flag]) - .pick(&arg![flag_b: Flag]) - .unwrap(); - assert_eq!(a, Flag::Inactive); - assert_eq!(b, Flag::Inactive); -} - -// After `--` (end-of-options) - -#[test] -fn test_flag_after_end_of_options() { - let flag: Flag = vec!["--", "--verbose"] - .to_picker() - .pick(&arg![verbose: Flag]) - .unwrap(); - assert_eq!(flag, Flag::Inactive); -} - -// Alias - -#[test] -fn test_flag_with_alias() { - let flag: Flag = vec!["--cfg"] - .to_picker() - .pick(&arg![config: Flag, "cfg"]) - .unwrap(); - assert_eq!(flag, Flag::Active); -} - -#[test] -fn test_flag_primary_name() { - let flag: Flag = vec!["--config"] - .to_picker() - .pick(&arg![config: Flag, "cfg"]) - .unwrap(); - assert_eq!(flag, Flag::Active); -} - -// Unrelated flag should not match - -#[test] -fn test_unrelated_flag_does_not_match() { - let flag: Flag = vec!["--other"] - .to_picker() - .pick(&arg![verbose: Flag]) - .unwrap(); - assert_eq!(flag, Flag::Inactive); -} - -// to_result / to_option - -#[test] -fn test_flag_to_result() { - let result: Result = vec!["--verbose"] - .to_picker() - .pick(&arg![verbose: Flag]) - .to_result(); - assert_eq!(result, Ok(Flag::Active)); -} - -#[test] -fn test_flag_to_option() { - let opt: Option = vec!["--verbose"] - .to_picker() - .pick(&arg![verbose: Flag]) - .to_option(); - assert_eq!(opt, Some(Flag::Active)); -} - -// Bool conversions - -#[test] -fn test_flag_converts_to_bool() { - let flag = Flag::Active; - assert!(bool::from(flag)); - - let flag = Flag::Inactive; - assert!(!bool::from(flag)); -} - -#[test] -fn test_flag_from_bool() { - assert_eq!(Flag::from(true), Flag::Active); - assert_eq!(Flag::from(false), Flag::Inactive); -} - -#[test] -fn test_flag_deref_to_bool() { - let active = Flag::Active; - assert!(*active); - - let inactive = Flag::Inactive; - assert!(!*inactive); -} - -// Flag never triggers route (unlike bool) -// -// Flag::pick always returns Parsed, so the fallback chain -// (default → route) is never entered. - -#[test] -fn test_flag_absent_does_not_trigger_route() { - // Even without or_default / or_route, absent flag returns Inactive, not a route - let result: Result = Vec::<&str>::new() - .with_route::<&str>() - .pick(&arg![verbose: Flag]) - .or_route(|| "should_not_fire") - .to_result(); - assert_eq!(result, Ok(Flag::Inactive)); -} diff --git a/mingling_picker/test/src/test/value_string_test.rs b/mingling_picker/test/src/test/value_string_test.rs deleted file mode 100644 index b1e5c0b..0000000 --- a/mingling_picker/test/src/test/value_string_test.rs +++ /dev/null @@ -1,171 +0,0 @@ -use mingling_picker::{IntoPicker, macros::arg}; - -// Basic named String — present / absent - -#[test] -fn test_string_named_present() { - let val: String = vec!["--name", "Alice"] - .to_picker() - .pick(&arg![name: String]) - .or_default() - .unwrap(); - assert_eq!(val, "Alice"); -} - -#[test] -fn test_string_named_absent_uses_default() { - let val: String = Vec::<&str>::new() - .to_picker() - .pick(&arg![name: String]) - .or_default() - .unwrap(); - assert_eq!(val, ""); -} - -// Named String — eq mode - -#[test] -fn test_string_named_eq_mode() { - let val: String = vec!["--name=Alice"] - .to_picker() - .pick(&arg![name: String]) - .or_default() - .unwrap(); - assert_eq!(val, "Alice"); -} - -// Named String — short flag - -#[test] -fn test_string_named_short_flag() { - let val: String = vec!["-n", "Alice"] - .to_picker() - .pick(&arg![name: String, 'n']) - .or_default() - .unwrap(); - assert_eq!(val, "Alice"); -} - -// Named String — no value after flag - -#[test] -fn test_string_named_missing_value_triggers_default() { - // --name at end with no following arg → pick returns NotFound → or_default gives "" - let val: String = vec!["--name"] - .to_picker() - .pick(&arg![name: String]) - .or_default() - .unwrap(); - assert_eq!(val, ""); -} - -// Positional String - -#[test] -fn test_string_positional() { - let val: String = vec!["file.txt"] - .to_picker() - .pick(&arg![String]) - .or_default() - .unwrap(); - assert_eq!(val, "file.txt"); -} - -#[test] -fn test_string_positional_takes_first() { - let val: String = vec!["first", "second"] - .to_picker() - .pick(&arg![String]) - .or_default() - .unwrap(); - assert_eq!(val, "first"); -} - -// Multiple occurrences (Single only tags one occurrence per Parseable) - -#[test] -fn test_string_two_named_flags() { - let (a, b): (String, String) = vec!["--name", "Alice", "--greeting", "Hello"] - .to_picker() - .pick(&arg![name: String]) - .or_default() - .pick(&arg![greeting: String]) - .or_default() - .unwrap(); - assert_eq!(a, "Alice"); - assert_eq!(b, "Hello"); -} - -// Mixed named + positional - -#[test] -fn test_string_named_and_positional() { - let (name, file): (String, String) = vec!["--name", "Alice", "file.txt"] - .to_picker() - .pick(&arg![name: String]) - .or_default() - .pick(&arg![String]) - .or_default() - .unwrap(); - assert_eq!(name, "Alice"); - assert_eq!(file, "file.txt"); -} - -// After `--` (end-of-options) - -#[test] -fn test_string_named_after_end_of_options() { - // Named arg after `--` should not be matched → default - let val: String = vec!["--", "--name", "Alice"] - .to_picker() - .pick(&arg![name: String]) - .or_default() - .unwrap(); - assert_eq!(val, ""); -} - -// Unrelated flag should not match → default - -#[test] -fn test_string_unrelated_flag() { - let val: String = vec!["--other", "value"] - .to_picker() - .pick(&arg![name: String]) - .or_default() - .unwrap(); - assert_eq!(val, ""); -} - -// to_result / to_option - -#[test] -fn test_string_to_result() { - let result: Result = vec!["--name", "Alice"] - .to_picker() - .pick(&arg![name: String]) - .or_default() - .to_result(); - assert_eq!(result, Ok("Alice".to_string())); -} - -#[test] -fn test_string_to_option() { - let opt: Option = vec!["--name", "Alice"] - .to_picker() - .pick(&arg![name: String]) - .or_default() - .to_option(); - assert_eq!(opt, Some("Alice".to_string())); -} - -// Custom default via .or() - -#[test] -fn test_string_custom_default() { - let val: String = Vec::<&str>::new() - .to_picker() - .pick(&arg![name: String]) - .or(|| "default_name".to_string()) - .unwrap(); - assert_eq!(val, "default_name"); -} diff --git a/mingling_picker_macros/Cargo.toml b/mingling_picker_macros/Cargo.toml deleted file mode 100644 index 58488ea..0000000 --- a/mingling_picker_macros/Cargo.toml +++ /dev/null @@ -1,20 +0,0 @@ -[package] -name = "mingling_picker_macros" -version.workspace = true -edition.workspace = true -license.workspace = true -repository.workspace = true -authors = ["Weicao-CatilGrass"] -readme = "README.md" -description = "Mingling's lightweight argument parser macros" - -[lib] -proc-macro = true - -[features] -mingling_support = [] - -[dependencies] -syn.workspace = true -quote.workspace = true -proc-macro2.workspace = true diff --git a/mingling_picker_macros/README.md b/mingling_picker_macros/README.md deleted file mode 100644 index 5a3f220..0000000 --- a/mingling_picker_macros/README.md +++ /dev/null @@ -1,40 +0,0 @@ -# Mingling Picker Macros - -Procedural macros for [Mingling Picker](https://github.com/mingling-rs/mingling/tree/main/mingling_picker), enabled by the `mingling/picker` feature. - -```toml -[dependencies.mingling] -version = "0.3.0" -features = [ - "picker" -] -``` - -## Provided Macros - -### Macro `arg!` - -Declares a parameter definition for use with `Picker`'s `.pick()` method: - -```rust,ignore -use mingling_picker_macros::arg; - -// Named flag with a value -let flag = arg![name: String]; - -// Named flag with short form -let flag = arg![name: String, 'n']; - -// Named flag with alias -let flag = arg![name: String, 'n', "nickname"]; - -// Positional parameter -let flag = arg![String]; - -// Flag-only parameter (boolean) -let flag = arg![verbose: Flag]; -``` - -### Macro `internal_repeat!` (Internal) - -Internal macro used by Picker to generate `PickerPattern1..=32` and their parsing logic. Not intended for direct use. diff --git a/mingling_picker_macros/src/arg.rs b/mingling_picker_macros/src/arg.rs deleted file mode 100644 index 70b27b0..0000000 --- a/mingling_picker_macros/src/arg.rs +++ /dev/null @@ -1,205 +0,0 @@ -use proc_macro::{TokenStream, TokenTree}; -use proc_macro2::TokenStream as TS2; -use quote::quote; - -pub(crate) fn arg(input: TokenStream) -> TokenStream { - let tokens: Vec = input.into_iter().collect(); - let args = split_at_commas(&tokens); - if args.is_empty() { - return quote! { compile_error!("arg! flaguires at least one argument") }.into(); - } - - let first = &args[0]; - let rest = &args[1..]; - - // Validate: at most one char literal - let char_count = rest.iter().filter(|a| is_char_literal(a)).count(); - if char_count > 1 { - return quote! { compile_error!("arg! only supports at most one short name") }.into(); - } - - // Extract short char and string aliases - let short_char: Option = rest - .iter() - .find(|a| is_char_literal(a)) - .and_then(|a| extract_char(a)); - let aliases: Vec = rest - .iter() - .filter(|a| is_string_literal(a)) - .filter_map(|a| extract_string(a)) - .collect(); - - // Parse first argument - // arg![name: Type] → name, with explicit type - // arg![Type] → positional type, no name - let colon_pos = first - .iter() - .position(|t| matches!(t, TokenTree::Punct(p) if p.as_char() == ':')); - let has_named_colon = colon_pos.is_some_and(|pos| pos > 0 && !is_double_colon(first, pos)); - - let first_slice = first.as_slice(); - let (name, ty, is_named) = if has_named_colon { - let pos = colon_pos.unwrap(); - // `name : Type` - ( - Some(&first_slice[..pos]), - Some(&first_slice[pos + 1..]), - true, - ) - } else { - // No `name:` prefix → the entire first argument is the type - (None, Some(first_slice), false) - }; - - // Build full names - let full_names: Vec = match name { - Some(n) => { - let mut v = vec![join_idents(n)]; - v.extend(aliases); - v - } - None => aliases, - }; - - let path: TS2 = { - let ty_ts: TS2 = ty - .map(|t| { - let ts: TokenStream = t.iter().cloned().collect(); - TS2::from(ts) - }) - .unwrap_or(TS2::new()); - - #[cfg(feature = "mingling_support")] - let import = quote! { ::mingling::picker::PickerArg }; - - #[cfg(not(feature = "mingling_support"))] - let import = quote! { ::mingling_picker::PickerArg }; - - if ty.is_some() { - quote! { #import::<#ty_ts> } - } else { - quote! { #import::<_> } - } - }; - - // full: &["name", "alias", ...] or &[] - let full_value: TS2 = if !full_names.is_empty() { - let strs: Vec = full_names - .iter() - .map(|s| proc_macro2::Literal::string(s)) - .collect(); - quote! { &[#(#strs),*] } - } else { - quote! { &[] } - }; - - // short: Some('c') or None - let short_value: TS2 = match short_char { - Some(c) => { - let lit = proc_macro2::Literal::character(c); - quote! { ::std::option::Option::Some(#lit) } - } - None => quote! { ::std::option::Option::None }, - }; - - let positional_value: bool = !is_named; - - let result = quote! { - #path { - full: #full_value, - short: #short_value, - positional: #positional_value, - internal_type: ::std::marker::PhantomData, - } - }; - - result.into() -} - -fn split_at_commas(tokens: &[TokenTree]) -> Vec> { - let mut result = vec![Vec::new()]; - let mut depth = 0u32; - for t in tokens { - match t { - TokenTree::Group(_g) => { - depth += 1; - result.last_mut().unwrap().push(t.clone()); - depth -= 1; - } - TokenTree::Punct(p) if p.as_char() == ',' && depth == 0 => { - result.push(Vec::new()); - } - _ => result.last_mut().unwrap().push(t.clone()), - } - } - result -} - -fn is_double_colon(tokens: &[TokenTree], pos: usize) -> bool { - pos > 0 && matches!(&tokens[pos - 1], TokenTree::Punct(p) if p.as_char() == ':') -} - -fn is_char_literal(tokens: &[TokenTree]) -> bool { - tokens.len() == 1 - && matches!(&tokens[0], TokenTree::Literal(l) if { - let s = l.to_string(); - s.starts_with('\'') && s.len() >= 3 - }) -} - -fn is_string_literal(tokens: &[TokenTree]) -> bool { - tokens.len() == 1 - && matches!(&tokens[0], TokenTree::Literal(l) if { - l.to_string().starts_with('"') - }) -} - -fn extract_char(tokens: &[TokenTree]) -> Option { - match &tokens[0] { - TokenTree::Literal(l) => { - let s = l.to_string(); - let cs: Vec = s.chars().collect(); - if cs.len() >= 3 && cs[0] == '\'' && cs[cs.len() - 1] == '\'' { - let inner: String = cs[1..cs.len() - 1].iter().collect(); - // inner is the character between the quotes of a char literal. - // For a literal `'n'`, inner is `"n"` (the character n). - // For an escape `'\n'`, inner is the actual newline character. - // The catch-all handles both literal single chars and escape - // sequences (the escaped char IS the actual control character). - match inner.as_str() { - "\\\\" => Some('\\'), - "\\'" => Some('\''), - _ => inner.chars().next(), - } - } else { - None - } - } - _ => None, - } -} - -fn extract_string(tokens: &[TokenTree]) -> Option { - match &tokens[0] { - TokenTree::Literal(l) => { - let s = l.to_string(); - let cs: Vec = s.chars().collect(); - if cs.len() >= 2 && cs[0] == '"' && cs[cs.len() - 1] == '"' { - Some(cs[1..cs.len() - 1].iter().collect()) - } else { - None - } - } - _ => None, - } -} - -fn join_idents(tokens: &[TokenTree]) -> String { - tokens - .iter() - .map(|t| match t { - TokenTree::Ident(id) => id.to_string(), - _ => String::new(), - }) - .collect() -} diff --git a/mingling_picker_macros/src/internal_repeat.rs b/mingling_picker_macros/src/internal_repeat.rs deleted file mode 100644 index 4b2242b..0000000 --- a/mingling_picker_macros/src/internal_repeat.rs +++ /dev/null @@ -1,315 +0,0 @@ -use proc_macro::{Delimiter, Group, Ident, Literal, TokenStream, TokenTree}; - -pub(crate) fn internal_repeat(input: TokenStream) -> TokenStream { - let tokens: Vec = input.into_iter().collect(); - let (range_start, range_end, body_start) = parse_range(&tokens); - - let mut body: Vec = tokens[body_start..].to_vec(); - if body.len() == 1 - && let TokenTree::Group(g) = &body[0] - && g.delimiter() == Delimiter::Brace - { - body = g.stream().into_iter().collect(); - } - - let mut result = Vec::new(); - for i in range_start..=range_end { - result.extend(expand_body(&body, i, range_start, range_end)); - } - result.into_iter().collect() -} - -/// Parse `start .. end =>` or `start ..= end =>` or `count =>` (backward compat). -/// Returns `(start, end_inclusive, body_start_index)`. -fn parse_range(tokens: &[TokenTree]) -> (usize, usize, usize) { - // Find => separator - let arrow_pos = tokens.windows(2).position(|w| { - matches!(&w[0], TokenTree::Punct(p) if p.as_char() == '=') - && matches!(&w[1], TokenTree::Punct(p) if p.as_char() == '>') - }); - - let (arrow_pos, body_start) = match arrow_pos { - Some(p) => (p, p + 2), - None => return (1, 12, 0), // fallback - }; - - let before: Vec<&TokenTree> = tokens[..arrow_pos].iter().collect(); - - // Try to find `..` or `..=` pattern - // `..` is two Punct('.') tokens - let dotdot = before.windows(2).position(|w| { - matches!(w[0], TokenTree::Punct(p) if p.as_char() == '.') - && matches!(w[1], TokenTree::Punct(p) if p.as_char() == '.') - }); - - if let Some(dd) = dotdot { - // Start value: tokens before `..` - let start = parse_usize_tokens(&before[..dd]); - let after_dd = &before[dd + 2..]; - - // Check for `..=` (inclusive range) - let (inclusive, end_tokens) = if after_dd - .first() - .is_some_and(|t| matches!(t, TokenTree::Punct(p) if p.as_char() == '=')) - { - (true, &after_dd[1..]) - } else { - (false, after_dd) - }; - - let end = parse_usize_tokens(end_tokens); - - if inclusive { - (start, end, body_start) - } else { - // Exclusive end: if end >= start, iterate start..end, so end_inclusive = end - 1 - if end > start { - (start, end - 1, body_start) - } else { - (1, 12, body_start) // fallback - } - } - } else { - // No `..` found — fallback to simple count - let count = parse_usize_tokens(&before); - (1, count, body_start) - } -} - -/// Parse a sequence of tokens as a single usize value. -fn parse_usize_tokens(tokens: &[&TokenTree]) -> usize { - let s: String = tokens - .iter() - .map(|t| match t { - TokenTree::Literal(l) => l.to_string(), - TokenTree::Ident(id) => id.to_string(), - _ => String::new(), - }) - .collect::>() - .join("") - .replace(' ', ""); - - s.parse().unwrap_or(12) -} - -/// Walk tokens, replacing: -/// `$` → current -/// `^$` → max -/// `$^` → min -/// `$+` → current + 1 (clamped) -/// `$-` → current - 1 (clamped) -/// `ident$` → ident{current} -/// and expanding `( … )+` / `( … ,)+` / `( … ;)+` groups. -fn expand_body(tokens: &[TokenTree], current: usize, min: usize, max: usize) -> Vec { - let mut out = Vec::new(); - let mut i = 0; - while i < tokens.len() { - // Check for a parenthesized repetition group: ( ... ) sep? + - if let Some(exp) = try_expand_paren_group(tokens, i, current, min, max) { - let (items, consumed) = exp; - out.extend(items); - i += consumed; - continue; - } - - // `^$` — max value - if let TokenTree::Punct(p) = &tokens[i] - && p.as_char() == '^' - && i + 1 < tokens.len() - && let TokenTree::Punct(p2) = &tokens[i + 1] - && p2.as_char() == '$' - { - out.push(TokenTree::Literal(Literal::usize_suffixed(max))); - i += 2; - continue; - } - - // `ident$` / `ident$+` / `ident$-` / `ident$^` - // → {ident}{current} / {ident}{current+1} / {ident}{current-1} / {ident}{min} - if let TokenTree::Ident(id) = &tokens[i] { - if i + 1 < tokens.len() - && let TokenTree::Punct(p) = &tokens[i + 1] - && p.as_char() == '$' - { - // Check for ident$+ / ident$- / ident$^ - if i + 2 < tokens.len() { - match &tokens[i + 2] { - TokenTree::Punct(p2) if p2.as_char() == '+' => { - // ident$+ → {ident}{current+1} - let name = format!("{}{}", id, current + 1); - out.push(TokenTree::Ident(Ident::new(&name, id.span()))); - i += 3; - continue; - } - TokenTree::Punct(p2) if p2.as_char() == '-' => { - // ident$- → {ident}{current-1} - let val = current.saturating_sub(1); - let name = format!("{}{}", id, val); - out.push(TokenTree::Ident(Ident::new(&name, id.span()))); - i += 3; - continue; - } - TokenTree::Punct(p2) if p2.as_char() == '^' => { - let name = format!("{}{}", id, min); - out.push(TokenTree::Ident(Ident::new(&name, id.span()))); - i += 3; - continue; - } - _ => {} - } - } - // ident$ alone → {ident}{current} - let name = format!("{}{}", id, current); - out.push(TokenTree::Ident(Ident::new(&name, id.span()))); - i += 2; - continue; - } - out.push(tokens[i].clone()); - i += 1; - continue; - } - - match &tokens[i] { - TokenTree::Punct(p) if p.as_char() == '$' => { - // lookahead for $^, $+, $- - if i + 1 < tokens.len() { - match &tokens[i + 1] { - TokenTree::Punct(p2) if p2.as_char() == '^' => { - // $^ → min - out.push(TokenTree::Literal(Literal::usize_suffixed(min))); - i += 2; - continue; - } - TokenTree::Punct(p2) if p2.as_char() == '+' => { - // $+ → current + 1 - out.push(TokenTree::Literal(Literal::usize_suffixed(current + 1))); - i += 2; - continue; - } - TokenTree::Punct(p2) if p2.as_char() == '-' => { - // $- → current - 1 - let val = current.saturating_sub(1); - out.push(TokenTree::Literal(Literal::usize_suffixed(val))); - i += 2; - continue; - } - _ => {} - } - } - // `$` alone → current - out.push(TokenTree::Literal(Literal::usize_suffixed(current))); - i += 1; - continue; - } - TokenTree::Group(g) => { - let inner = expand_body_vec(&g.stream(), current, min, max); - out.push(TokenTree::Group(Group::new( - g.delimiter(), - inner.into_iter().collect(), - ))); - } - other => out.push(other.clone()), - } - i += 1; - } - out -} - -fn expand_body_vec(stream: &TokenStream, current: usize, min: usize, max: usize) -> Vec { - let v: Vec = stream.clone().into_iter().collect(); - expand_body(&v, current, min, max) -} - -/// Try to expand a repetition group. -/// -/// New syntax (repetition marker `+` is the LAST token INSIDE the parens): -/// - `(group,+)` — repeat `*` times (current), `,` is the separator -/// - `(group,+)` — repeat `*` times, `,` is the separator -/// - `(group,+)` — repeat `*` times, `;` is the separator -/// - `(group +)` — repeat `*` times, no separator -/// - `(group,+)+` — repeat `*+1` times (with separator) -/// - `(group,+)--` — repeat `*-1` times (with separator) [not yet used] -/// -/// The `*` is the current counter value. An optional `+` or `-` immediately -/// after the closing paren shifts the repeat count up or down by one. -fn try_expand_paren_group( - tokens: &[TokenTree], - i: usize, - current: usize, - _min: usize, - _max: usize, -) -> Option<(Vec, usize)> { - let group = match tokens.get(i)? { - TokenTree::Group(g) if g.delimiter() == Delimiter::Parenthesis => g, - _ => return None, - }; - - let stream: Vec = group.stream().into_iter().collect(); - - // Repetition syntax: the LAST token inside the parens MUST be `+`. - // `(content,+)` — repeat * times, `,` separator - // `(content;+)` — repeat * times, `;` separator - // `(content,+)` — repeat * times, no separator - // `(content,+)+` — repeat *+1 times - // `(content,+)-` — repeat *-1 times - // An optional `+` / `-` right after `)` shifts the count by ±1. - // - // Any `+` tokens inside `content` are treated as regular Rust syntax - // (trait bounds, etc.) — the repetition marker is ONLY the final `+`. - - let last_is_plus = stream - .last() - .is_some_and(|t| matches!(t, TokenTree::Punct(p) if p.as_char() == '+')); - if !last_is_plus { - return None; - } - - // Determine separator and inner content. - let (inner, sep_str): (Vec, &str) = { - if stream.len() >= 2 { - let sep_idx = stream.len() - 2; - match &stream[sep_idx] { - TokenTree::Punct(p) if p.as_char() == ',' => { - // (...,content,+) — inner is everything before the last `,` - let content: Vec = stream[..sep_idx].into(); - (content, ",") - } - TokenTree::Punct(p) if p.as_char() == ';' => { - let content: Vec = stream[..sep_idx].into(); - (content, ";") - } - _ => { - // (content+) — no separator, the `+` is the only special token - let content: Vec = stream[..stream.len() - 1].into(); - (content, "") - } - } - } else { - // (+) — bare repetition marker, empty content - (vec![], "") - } - }; - - // Handle modifier after `)` - let rest = &tokens[i + 1..]; - let modifier: isize = match rest.first() { - Some(TokenTree::Punct(p)) if p.as_char() == '+' => 1, - Some(TokenTree::Punct(p)) if p.as_char() == '-' => -1, - _ => 0, - }; - let consumed = if modifier != 0 { 2 } else { 1 }; - let repeat_count = (current as isize + modifier) as usize; - - let mut out = Vec::new(); - for n in 1..=repeat_count { - if n > 1 && !sep_str.is_empty() { - out.push(TokenTree::Punct(proc_macro::Punct::new( - sep_str.chars().next().unwrap(), - proc_macro::Spacing::Alone, - ))); - } - out.extend(expand_body(&inner, n, 1, repeat_count)); - } - - Some((out, consumed)) -} diff --git a/mingling_picker_macros/src/lib.rs b/mingling_picker_macros/src/lib.rs deleted file mode 100644 index a12e3ed..0000000 --- a/mingling_picker_macros/src/lib.rs +++ /dev/null @@ -1,32 +0,0 @@ -#![doc = include_str!("../README.md")] - -use proc_macro::TokenStream; - -mod arg; -mod internal_repeat; - -/// Core proc-macro: repeats a template body `count` times. -/// -/// Internal call signature: `internal_repeat!(count => { template })` -#[proc_macro] -pub fn internal_repeat(input: TokenStream) -> TokenStream { - internal_repeat::internal_repeat(input) -} - -/// Quick builder for `PickerArg`. -/// -/// # Syntax -/// -/// ```ignore -/// use mingling_picker_macros::flag; -/// -/// let basic = arg![name: String]; -/// let with_short_name = arg![name: String, 'n']; -/// let with_short_alias = arg![name: String, 'n', "alias"]; -/// let positional = arg![String]; -/// let positional_with_name = arg![String, 'n', "alias"]; -/// ``` -#[proc_macro] -pub fn arg(input: TokenStream) -> TokenStream { - arg::arg(input) -} -- cgit