From b9f208deed7b8e012fbcd84202e2fb1d5eb8eeb9 Mon Sep 17 00:00:00 2001 From: 魏曹先生 <1992414357@qq.com> Date: Fri, 17 Jul 2026 03:24:51 +0800 Subject: feat(picker2): complete Picker2 prototype MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Picker2 replaces the original Picker1 with a two-phase (tag → pick) + mask-bitmap architecture, decoupling argument matching from type conversion via a composable matcher pipeline. Architecture - FlagMatcher — boolean flags (--verbose) - ArgMatcher — single flag+value pairs (--name Alice) - MultiArgMatcher — multi-value flag groups (--files a.txt b.txt) - PositionalMatcher — positional arguments, respects `--` - SingleMatcher — composite for Single-type Pickables Types - Flag (Active/Inactive) — semantic bool flag value - String + 14 numeric types via SinglePickable trait - Vec — greedy multi-value (all SinglePickable types) - VecUntil — bounded multi-value via BoundaryCheck trait - VecUntil, pick_string, pick_numbers, pick_bool, pick_flag Style system - UNIX_STYLE (kebab), POWERSHELL_STYLE (Pascal), WINDOWS_STYLE (Pascal) - naming_case auto-conversion via just_fmt integration - Style-aware separator (= for Unix, : for PS/Windows) Infrastructure - internal_repeat! macro generates PickerPattern1..=32 - SinglePickable blanket impl → Pickable - MultiPickableWithBoundary trait with greedy/bounded variants - 151 integration tests - Docs updated for parser feature --- .run/src/bin/doc.ps1 | 2 +- .run/src/bin/doc.sh | 2 +- .run/version-files.toml | 8 + .vscode/settings.json | 8 +- .zed/settings.json | 10 +- Cargo.lock | 19 +- Cargo.toml | 2 +- docs/dev/pages/issues/add-picker2.md | 14 + examples/example-argument-parse/Cargo.lock | 4 +- examples/example-async-support/Cargo.lock | 4 +- examples/example-basic/Cargo.lock | 4 +- examples/example-clap-binding/Cargo.lock | 4 +- .../example-combine-pathf-dispatch-tree/Cargo.lock | 4 +- examples/example-completion/Cargo.lock | 12 +- examples/example-custom-pickable/Cargo.lock | 4 +- examples/example-dispatch-tree/Cargo.lock | 4 +- examples/example-enum-tag/Cargo.lock | 12 +- examples/example-error-handling/Cargo.lock | 4 +- examples/example-exitcode/Cargo.lock | 4 +- examples/example-help/Cargo.lock | 4 +- examples/example-hook/Cargo.lock | 4 +- examples/example-implicit-dispatcher/Cargo.lock | 4 +- examples/example-lazy-resources/Cargo.lock | 4 +- examples/example-outside-type/Cargo.lock | 4 +- examples/example-pack-err/Cargo.lock | 4 +- examples/example-panic-unwind/Cargo.lock | 4 +- examples/example-pathfinder/Cargo.lock | 4 +- examples/example-repl-basic/Cargo.lock | 12 +- examples/example-resources/Cargo.lock | 4 +- examples/example-setup/Cargo.lock | 4 +- examples/example-structural-renderer/Cargo.lock | 4 +- examples/example-unit-test/Cargo.lock | 4 +- examples/full-todolist/Cargo.lock | 4 +- mingling/Cargo.toml | 15 +- mingling/src/lib.rs | 41 +- mingling_core/tests/test-all/Cargo.lock | 12 +- mingling_core/tests/test-basic/Cargo.lock | 4 +- mingling_core/tests/test-comp/Cargo.lock | 12 +- mingling_core/tests/test-dispatch-tree/Cargo.lock | 4 +- mingling_core/tests/test-repl/Cargo.lock | 4 +- .../tests/test-structural-renderer/Cargo.lock | 4 +- mingling_pathf/test/Cargo.lock | 10 +- mingling_picker/Cargo.toml | 7 +- mingling_picker/README.md | 42 ++ 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 | 1 + mingling_picker/src/infos.rs | 446 +++++++++++++++++++++ mingling_picker/src/lib.rs | 44 +- 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 | 225 +++++++++++ mingling_picker/src/parselib/utils.rs | 271 +++++++++++++ mingling_picker/src/pickable.rs | 91 +++++ mingling_picker/src/pickable/multi_pickable.rs | 77 ++++ mingling_picker/src/pickable/single_pickable.rs | 69 ++++ mingling_picker/src/picker.rs | 360 +++++++++++++++++ mingling_picker/src/picker/parse.rs | 177 ++++++++ mingling_picker/src/picker/patterns.rs | 199 +++++++++ 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 | 3 + mingling_picker_macros/src/arg.rs | 205 ++++++++++ mingling_picker_macros/src/internal_repeat.rs | 315 +++++++++++++++ mingling_picker_macros/src/lib.rs | 29 ++ 89 files changed, 6119 insertions(+), 93 deletions(-) create mode 100644 mingling_picker/src/arg.rs create mode 100644 mingling_picker/src/builtin.rs create mode 100644 mingling_picker/src/builtin/pick_bool.rs create mode 100644 mingling_picker/src/builtin/pick_flag.rs create mode 100644 mingling_picker/src/builtin/pick_numbers.rs create mode 100644 mingling_picker/src/builtin/pick_string.rs create mode 100644 mingling_picker/src/corebind.rs create mode 100644 mingling_picker/src/infos.rs create mode 100644 mingling_picker/src/parselib.rs create mode 100644 mingling_picker/src/parselib/arg_matcher.rs create mode 100644 mingling_picker/src/parselib/flag_matcher.rs create mode 100644 mingling_picker/src/parselib/multi_arg_matcher.rs create mode 100644 mingling_picker/src/parselib/pos_matcher.rs create mode 100644 mingling_picker/src/parselib/single_matcher.rs create mode 100644 mingling_picker/src/parselib/style.rs create mode 100644 mingling_picker/src/parselib/utils.rs create mode 100644 mingling_picker/src/pickable.rs create mode 100644 mingling_picker/src/pickable/multi_pickable.rs create mode 100644 mingling_picker/src/pickable/single_pickable.rs create mode 100644 mingling_picker/src/picker.rs create mode 100644 mingling_picker/src/picker/parse.rs create mode 100644 mingling_picker/src/picker/patterns.rs create mode 100644 mingling_picker/src/picker/result.rs create mode 100644 mingling_picker/src/value.rs create mode 100644 mingling_picker/src/value/flag.rs create mode 100644 mingling_picker/src/value/vec_until.rs create mode 100644 mingling_picker/test/Cargo.lock create mode 100644 mingling_picker/test/Cargo.toml create mode 100644 mingling_picker/test/src/lib.rs create mode 100644 mingling_picker/test/src/test.rs create mode 100644 mingling_picker/test/src/test/arg_matcher_test.rs create mode 100644 mingling_picker/test/src/test/basic_test.rs create mode 100644 mingling_picker/test/src/test/multi_arg_test.rs create mode 100644 mingling_picker/test/src/test/multi_value_test.rs create mode 100644 mingling_picker/test/src/test/pos_matcher_test.rs create mode 100644 mingling_picker/test/src/test/priority_test.rs create mode 100644 mingling_picker/test/src/test/route_test.rs create mode 100644 mingling_picker/test/src/test/style_test.rs create mode 100644 mingling_picker/test/src/test/value_flag_test.rs create mode 100644 mingling_picker/test/src/test/value_string_test.rs create mode 100644 mingling_picker_macros/src/arg.rs create mode 100644 mingling_picker_macros/src/internal_repeat.rs diff --git a/.run/src/bin/doc.ps1 b/.run/src/bin/doc.ps1 index 987f0de..fd7afaa 100644 --- a/.run/src/bin/doc.ps1 +++ b/.run/src/bin/doc.ps1 @@ -1 +1 @@ -cargo doc --workspace --no-deps --features builds,structural_renderer,repl,comp,parser,clap,extra_macros --open +cargo doc --workspace --no-deps --features core,macros,builds,structural_renderer,repl,comp,parser,picker,clap,extra_macros --open diff --git a/.run/src/bin/doc.sh b/.run/src/bin/doc.sh index 5e8a311..2d59896 100644 --- a/.run/src/bin/doc.sh +++ b/.run/src/bin/doc.sh @@ -1,3 +1,3 @@ #!/bin/bash -cargo doc --workspace --no-deps --features builds,structural_renderer,repl,comp,parser,clap,extra_macros --open +cargo doc --workspace --no-deps --features core,macros,builds,structural_renderer,repl,comp,parser,picker,clap,extra_macros --open diff --git a/.run/version-files.toml b/.run/version-files.toml index ca104d2..f00ae4c 100644 --- a/.run/version-files.toml +++ b/.run/version-files.toml @@ -17,3 +17,11 @@ pattern = "version = \"{VER}\"" [[file]] file = "./docs/res/guide.txt" pattern = "mingling = \"{VER}\"" + +[[file]] +file = "./mingling_picker/README.md" +pattern = "version = \"{VER}\"" + +[[file]] +file = "./mingling_picker/README.md" +pattern = "mingling_picker = \"{VER}\"" diff --git a/.vscode/settings.json b/.vscode/settings.json index 0aa6db4..7472e1c 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,11 @@ { "rust-analyzer.check.command": "clippy", "rust-analyzer.checkOnSave": true, - "rust-analyzer.linkedProjects": [".run/Cargo.toml"], + "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"], } diff --git a/.zed/settings.json b/.zed/settings.json index 983506d..12c901e 100644 --- a/.zed/settings.json +++ b/.zed/settings.json @@ -4,7 +4,15 @@ "initialization_options": { "checkOnSave": true, "check": { "command": "clippy" }, - "linkedProjects": [".run/Cargo.toml"], + "linkedProjects": [ + ".run/Cargo.toml", + "mingling_picker/Cargo.toml", + "mingling_pathf/test/Cargo.toml", + "mingling_picker/test/Cargo.toml", + ], + "cargo": { + "features": ["mingling_support"], + }, }, }, }, diff --git a/Cargo.lock b/Cargo.lock index 5ca9565..a283e0f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -342,13 +342,19 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5454cda0d57db59778608d7a47bff5b16c6705598265869fb052b657f66cf05e" +[[package]] +name = "just_fmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6170dccbc3ea15dfb7f2da964097f814aba1dd8f746d4ffc56f33245c38e6d96" + [[package]] name = "just_template" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4fb99a3c1dee7299c57b26ef927f15535da0fbc93d6deb1d1114cae1337be4fb" dependencies = [ - "just_fmt", + "just_fmt 0.1.2", "just_template_macros", ] @@ -407,7 +413,6 @@ dependencies = [ "mingling_core", "mingling_macros", "mingling_picker", - "mingling_picker_macros", "serde", "size", "tokio", @@ -420,7 +425,7 @@ dependencies = [ "chrono", "colored", "dirs", - "just_fmt", + "just_fmt 0.1.2", "mingling", "serde", "serde_json", @@ -432,7 +437,7 @@ name = "mingling_core" version = "0.3.0" dependencies = [ "env_logger", - "just_fmt", + "just_fmt 0.2.0", "just_template", "log", "mingling_pathf", @@ -447,7 +452,7 @@ dependencies = [ name = "mingling_macros" version = "0.3.0" dependencies = [ - "just_fmt", + "just_fmt 0.2.0", "proc-macro2", "quote", "syn", @@ -457,7 +462,7 @@ dependencies = [ name = "mingling_pathf" version = "0.3.0" dependencies = [ - "just_fmt", + "just_fmt 0.2.0", "proc-macro2", "syn", ] @@ -466,7 +471,9 @@ dependencies = [ name = "mingling_picker" version = "0.3.0" dependencies = [ + "just_fmt 0.2.0", "mingling_core", + "mingling_picker_macros", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index b3c3496..90989fc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,7 +39,7 @@ 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 } -just_fmt = "0.1.2" +just_fmt = "0.2.0" just_template = "0.2.0" serde = { version = "1.0.228", features = ["derive"] } diff --git a/docs/dev/pages/issues/add-picker2.md b/docs/dev/pages/issues/add-picker2.md index 1f0f941..f5c3ca7 100644 --- a/docs/dev/pages/issues/add-picker2.md +++ b/docs/dev/pages/issues/add-picker2.md @@ -84,3 +84,17 @@ pub struct PickerResult { pub remains_argument: Arguments, } ``` + +--- + +## 🕘 Progress + +- [x] In Progress + - [x] Added `Picker` struct and related call chain + - [x] Added `parselib` providing parsing logic + - [x] Added `Pickable` for extensibility + - [ ] Comprehensive testing! + - [ ] Improve documentation + - [ ] Add examples + - [ ] Update README +- [ ] Complete diff --git a/examples/example-argument-parse/Cargo.lock b/examples/example-argument-parse/Cargo.lock index 369add5..696dcb6 100644 --- a/examples/example-argument-parse/Cargo.lock +++ b/examples/example-argument-parse/Cargo.lock @@ -11,9 +11,9 @@ dependencies = [ [[package]] name = "just_fmt" -version = "0.1.2" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5454cda0d57db59778608d7a47bff5b16c6705598265869fb052b657f66cf05e" +checksum = "6170dccbc3ea15dfb7f2da964097f814aba1dd8f746d4ffc56f33245c38e6d96" [[package]] name = "mingling" diff --git a/examples/example-async-support/Cargo.lock b/examples/example-async-support/Cargo.lock index e457c63..83ce343 100644 --- a/examples/example-async-support/Cargo.lock +++ b/examples/example-async-support/Cargo.lock @@ -12,9 +12,9 @@ dependencies = [ [[package]] name = "just_fmt" -version = "0.1.2" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5454cda0d57db59778608d7a47bff5b16c6705598265869fb052b657f66cf05e" +checksum = "6170dccbc3ea15dfb7f2da964097f814aba1dd8f746d4ffc56f33245c38e6d96" [[package]] name = "mingling" diff --git a/examples/example-basic/Cargo.lock b/examples/example-basic/Cargo.lock index ace5fca..90b8e4a 100644 --- a/examples/example-basic/Cargo.lock +++ b/examples/example-basic/Cargo.lock @@ -11,9 +11,9 @@ dependencies = [ [[package]] name = "just_fmt" -version = "0.1.2" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5454cda0d57db59778608d7a47bff5b16c6705598265869fb052b657f66cf05e" +checksum = "6170dccbc3ea15dfb7f2da964097f814aba1dd8f746d4ffc56f33245c38e6d96" [[package]] name = "mingling" diff --git a/examples/example-clap-binding/Cargo.lock b/examples/example-clap-binding/Cargo.lock index b45bd86..b747c40 100644 --- a/examples/example-clap-binding/Cargo.lock +++ b/examples/example-clap-binding/Cargo.lock @@ -120,9 +120,9 @@ checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" [[package]] name = "just_fmt" -version = "0.1.2" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5454cda0d57db59778608d7a47bff5b16c6705598265869fb052b657f66cf05e" +checksum = "6170dccbc3ea15dfb7f2da964097f814aba1dd8f746d4ffc56f33245c38e6d96" [[package]] name = "mingling" diff --git a/examples/example-combine-pathf-dispatch-tree/Cargo.lock b/examples/example-combine-pathf-dispatch-tree/Cargo.lock index 0903c86..f91e03f 100644 --- a/examples/example-combine-pathf-dispatch-tree/Cargo.lock +++ b/examples/example-combine-pathf-dispatch-tree/Cargo.lock @@ -11,9 +11,9 @@ dependencies = [ [[package]] name = "just_fmt" -version = "0.1.2" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5454cda0d57db59778608d7a47bff5b16c6705598265869fb052b657f66cf05e" +checksum = "6170dccbc3ea15dfb7f2da964097f814aba1dd8f746d4ffc56f33245c38e6d96" [[package]] name = "mingling" diff --git a/examples/example-completion/Cargo.lock b/examples/example-completion/Cargo.lock index cfca7b3..568bec5 100644 --- a/examples/example-completion/Cargo.lock +++ b/examples/example-completion/Cargo.lock @@ -15,13 +15,19 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5454cda0d57db59778608d7a47bff5b16c6705598265869fb052b657f66cf05e" +[[package]] +name = "just_fmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6170dccbc3ea15dfb7f2da964097f814aba1dd8f746d4ffc56f33245c38e6d96" + [[package]] name = "just_template" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4fb99a3c1dee7299c57b26ef927f15535da0fbc93d6deb1d1114cae1337be4fb" dependencies = [ - "just_fmt", + "just_fmt 0.1.2", "just_template_macros", ] @@ -49,7 +55,7 @@ dependencies = [ name = "mingling_core" version = "0.3.0" dependencies = [ - "just_fmt", + "just_fmt 0.2.0", "just_template", ] @@ -57,7 +63,7 @@ dependencies = [ name = "mingling_macros" version = "0.3.0" dependencies = [ - "just_fmt", + "just_fmt 0.2.0", "proc-macro2", "quote", "syn", diff --git a/examples/example-custom-pickable/Cargo.lock b/examples/example-custom-pickable/Cargo.lock index b773df1..cb2d3ac 100644 --- a/examples/example-custom-pickable/Cargo.lock +++ b/examples/example-custom-pickable/Cargo.lock @@ -11,9 +11,9 @@ dependencies = [ [[package]] name = "just_fmt" -version = "0.1.2" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5454cda0d57db59778608d7a47bff5b16c6705598265869fb052b657f66cf05e" +checksum = "6170dccbc3ea15dfb7f2da964097f814aba1dd8f746d4ffc56f33245c38e6d96" [[package]] name = "mingling" diff --git a/examples/example-dispatch-tree/Cargo.lock b/examples/example-dispatch-tree/Cargo.lock index 7d8b260..5751ecd 100644 --- a/examples/example-dispatch-tree/Cargo.lock +++ b/examples/example-dispatch-tree/Cargo.lock @@ -11,9 +11,9 @@ dependencies = [ [[package]] name = "just_fmt" -version = "0.1.2" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5454cda0d57db59778608d7a47bff5b16c6705598265869fb052b657f66cf05e" +checksum = "6170dccbc3ea15dfb7f2da964097f814aba1dd8f746d4ffc56f33245c38e6d96" [[package]] name = "mingling" diff --git a/examples/example-enum-tag/Cargo.lock b/examples/example-enum-tag/Cargo.lock index 333bd33..211719f 100644 --- a/examples/example-enum-tag/Cargo.lock +++ b/examples/example-enum-tag/Cargo.lock @@ -15,13 +15,19 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5454cda0d57db59778608d7a47bff5b16c6705598265869fb052b657f66cf05e" +[[package]] +name = "just_fmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6170dccbc3ea15dfb7f2da964097f814aba1dd8f746d4ffc56f33245c38e6d96" + [[package]] name = "just_template" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4fb99a3c1dee7299c57b26ef927f15535da0fbc93d6deb1d1114cae1337be4fb" dependencies = [ - "just_fmt", + "just_fmt 0.1.2", "just_template_macros", ] @@ -49,7 +55,7 @@ dependencies = [ name = "mingling_core" version = "0.3.0" dependencies = [ - "just_fmt", + "just_fmt 0.2.0", "just_template", ] @@ -57,7 +63,7 @@ dependencies = [ name = "mingling_macros" version = "0.3.0" dependencies = [ - "just_fmt", + "just_fmt 0.2.0", "proc-macro2", "quote", "syn", diff --git a/examples/example-error-handling/Cargo.lock b/examples/example-error-handling/Cargo.lock index 7407809..d139c14 100644 --- a/examples/example-error-handling/Cargo.lock +++ b/examples/example-error-handling/Cargo.lock @@ -11,9 +11,9 @@ dependencies = [ [[package]] name = "just_fmt" -version = "0.1.2" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5454cda0d57db59778608d7a47bff5b16c6705598265869fb052b657f66cf05e" +checksum = "6170dccbc3ea15dfb7f2da964097f814aba1dd8f746d4ffc56f33245c38e6d96" [[package]] name = "mingling" diff --git a/examples/example-exitcode/Cargo.lock b/examples/example-exitcode/Cargo.lock index d54648e..df8b935 100644 --- a/examples/example-exitcode/Cargo.lock +++ b/examples/example-exitcode/Cargo.lock @@ -11,9 +11,9 @@ dependencies = [ [[package]] name = "just_fmt" -version = "0.1.2" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5454cda0d57db59778608d7a47bff5b16c6705598265869fb052b657f66cf05e" +checksum = "6170dccbc3ea15dfb7f2da964097f814aba1dd8f746d4ffc56f33245c38e6d96" [[package]] name = "mingling" diff --git a/examples/example-help/Cargo.lock b/examples/example-help/Cargo.lock index dd3829c..7a88e8e 100644 --- a/examples/example-help/Cargo.lock +++ b/examples/example-help/Cargo.lock @@ -11,9 +11,9 @@ dependencies = [ [[package]] name = "just_fmt" -version = "0.1.2" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5454cda0d57db59778608d7a47bff5b16c6705598265869fb052b657f66cf05e" +checksum = "6170dccbc3ea15dfb7f2da964097f814aba1dd8f746d4ffc56f33245c38e6d96" [[package]] name = "mingling" diff --git a/examples/example-hook/Cargo.lock b/examples/example-hook/Cargo.lock index 7801069..cce863a 100644 --- a/examples/example-hook/Cargo.lock +++ b/examples/example-hook/Cargo.lock @@ -11,9 +11,9 @@ dependencies = [ [[package]] name = "just_fmt" -version = "0.1.2" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5454cda0d57db59778608d7a47bff5b16c6705598265869fb052b657f66cf05e" +checksum = "6170dccbc3ea15dfb7f2da964097f814aba1dd8f746d4ffc56f33245c38e6d96" [[package]] name = "mingling" diff --git a/examples/example-implicit-dispatcher/Cargo.lock b/examples/example-implicit-dispatcher/Cargo.lock index b5ab8db..150104b 100644 --- a/examples/example-implicit-dispatcher/Cargo.lock +++ b/examples/example-implicit-dispatcher/Cargo.lock @@ -11,9 +11,9 @@ dependencies = [ [[package]] name = "just_fmt" -version = "0.1.2" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5454cda0d57db59778608d7a47bff5b16c6705598265869fb052b657f66cf05e" +checksum = "6170dccbc3ea15dfb7f2da964097f814aba1dd8f746d4ffc56f33245c38e6d96" [[package]] name = "mingling" diff --git a/examples/example-lazy-resources/Cargo.lock b/examples/example-lazy-resources/Cargo.lock index a5c8dc2..9ff79d2 100644 --- a/examples/example-lazy-resources/Cargo.lock +++ b/examples/example-lazy-resources/Cargo.lock @@ -11,9 +11,9 @@ dependencies = [ [[package]] name = "just_fmt" -version = "0.1.2" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5454cda0d57db59778608d7a47bff5b16c6705598265869fb052b657f66cf05e" +checksum = "6170dccbc3ea15dfb7f2da964097f814aba1dd8f746d4ffc56f33245c38e6d96" [[package]] name = "mingling" diff --git a/examples/example-outside-type/Cargo.lock b/examples/example-outside-type/Cargo.lock index 6528afd..4c00bc8 100644 --- a/examples/example-outside-type/Cargo.lock +++ b/examples/example-outside-type/Cargo.lock @@ -11,9 +11,9 @@ dependencies = [ [[package]] name = "just_fmt" -version = "0.1.2" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5454cda0d57db59778608d7a47bff5b16c6705598265869fb052b657f66cf05e" +checksum = "6170dccbc3ea15dfb7f2da964097f814aba1dd8f746d4ffc56f33245c38e6d96" [[package]] name = "mingling" diff --git a/examples/example-pack-err/Cargo.lock b/examples/example-pack-err/Cargo.lock index 7384680..26bad2b 100644 --- a/examples/example-pack-err/Cargo.lock +++ b/examples/example-pack-err/Cargo.lock @@ -18,9 +18,9 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "just_fmt" -version = "0.1.2" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5454cda0d57db59778608d7a47bff5b16c6705598265869fb052b657f66cf05e" +checksum = "6170dccbc3ea15dfb7f2da964097f814aba1dd8f746d4ffc56f33245c38e6d96" [[package]] name = "memchr" diff --git a/examples/example-panic-unwind/Cargo.lock b/examples/example-panic-unwind/Cargo.lock index be8d21a..aafe504 100644 --- a/examples/example-panic-unwind/Cargo.lock +++ b/examples/example-panic-unwind/Cargo.lock @@ -11,9 +11,9 @@ dependencies = [ [[package]] name = "just_fmt" -version = "0.1.2" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5454cda0d57db59778608d7a47bff5b16c6705598265869fb052b657f66cf05e" +checksum = "6170dccbc3ea15dfb7f2da964097f814aba1dd8f746d4ffc56f33245c38e6d96" [[package]] name = "mingling" diff --git a/examples/example-pathfinder/Cargo.lock b/examples/example-pathfinder/Cargo.lock index 6dcee1d..3fe98bb 100644 --- a/examples/example-pathfinder/Cargo.lock +++ b/examples/example-pathfinder/Cargo.lock @@ -11,9 +11,9 @@ dependencies = [ [[package]] name = "just_fmt" -version = "0.1.2" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5454cda0d57db59778608d7a47bff5b16c6705598265869fb052b657f66cf05e" +checksum = "6170dccbc3ea15dfb7f2da964097f814aba1dd8f746d4ffc56f33245c38e6d96" [[package]] name = "mingling" diff --git a/examples/example-repl-basic/Cargo.lock b/examples/example-repl-basic/Cargo.lock index 07cdd4b..d405eaa 100644 --- a/examples/example-repl-basic/Cargo.lock +++ b/examples/example-repl-basic/Cargo.lock @@ -6,7 +6,7 @@ version = 4 name = "example-repl-basic" version = "0.1.0" dependencies = [ - "just_fmt", + "just_fmt 0.1.2", "mingling", ] @@ -16,6 +16,12 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5454cda0d57db59778608d7a47bff5b16c6705598265869fb052b657f66cf05e" +[[package]] +name = "just_fmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6170dccbc3ea15dfb7f2da964097f814aba1dd8f746d4ffc56f33245c38e6d96" + [[package]] name = "mingling" version = "0.3.0" @@ -29,14 +35,14 @@ dependencies = [ name = "mingling_core" version = "0.3.0" dependencies = [ - "just_fmt", + "just_fmt 0.2.0", ] [[package]] name = "mingling_macros" version = "0.3.0" dependencies = [ - "just_fmt", + "just_fmt 0.2.0", "proc-macro2", "quote", "syn", diff --git a/examples/example-resources/Cargo.lock b/examples/example-resources/Cargo.lock index 1397cf4..68d2138 100644 --- a/examples/example-resources/Cargo.lock +++ b/examples/example-resources/Cargo.lock @@ -11,9 +11,9 @@ dependencies = [ [[package]] name = "just_fmt" -version = "0.1.2" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5454cda0d57db59778608d7a47bff5b16c6705598265869fb052b657f66cf05e" +checksum = "6170dccbc3ea15dfb7f2da964097f814aba1dd8f746d4ffc56f33245c38e6d96" [[package]] name = "mingling" diff --git a/examples/example-setup/Cargo.lock b/examples/example-setup/Cargo.lock index c2ec3e8..c6b660b 100644 --- a/examples/example-setup/Cargo.lock +++ b/examples/example-setup/Cargo.lock @@ -11,9 +11,9 @@ dependencies = [ [[package]] name = "just_fmt" -version = "0.1.2" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5454cda0d57db59778608d7a47bff5b16c6705598265869fb052b657f66cf05e" +checksum = "6170dccbc3ea15dfb7f2da964097f814aba1dd8f746d4ffc56f33245c38e6d96" [[package]] name = "mingling" diff --git a/examples/example-structural-renderer/Cargo.lock b/examples/example-structural-renderer/Cargo.lock index bacbd70..959c670 100644 --- a/examples/example-structural-renderer/Cargo.lock +++ b/examples/example-structural-renderer/Cargo.lock @@ -18,9 +18,9 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "just_fmt" -version = "0.1.2" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5454cda0d57db59778608d7a47bff5b16c6705598265869fb052b657f66cf05e" +checksum = "6170dccbc3ea15dfb7f2da964097f814aba1dd8f746d4ffc56f33245c38e6d96" [[package]] name = "memchr" diff --git a/examples/example-unit-test/Cargo.lock b/examples/example-unit-test/Cargo.lock index a3a8813..2be0d51 100644 --- a/examples/example-unit-test/Cargo.lock +++ b/examples/example-unit-test/Cargo.lock @@ -11,9 +11,9 @@ dependencies = [ [[package]] name = "just_fmt" -version = "0.1.2" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5454cda0d57db59778608d7a47bff5b16c6705598265869fb052b657f66cf05e" +checksum = "6170dccbc3ea15dfb7f2da964097f814aba1dd8f746d4ffc56f33245c38e6d96" [[package]] name = "mingling" diff --git a/examples/full-todolist/Cargo.lock b/examples/full-todolist/Cargo.lock index 716e51a..f4f0000 100644 --- a/examples/full-todolist/Cargo.lock +++ b/examples/full-todolist/Cargo.lock @@ -19,9 +19,9 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "just_fmt" -version = "0.1.2" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5454cda0d57db59778608d7a47bff5b16c6705598265869fb052b657f66cf05e" +checksum = "6170dccbc3ea15dfb7f2da964097f814aba1dd8f746d4ffc56f33245c38e6d96" [[package]] name = "memchr" diff --git a/mingling/Cargo.toml b/mingling/Cargo.toml index 667cfa5..c3f9511 100644 --- a/mingling/Cargo.toml +++ b/mingling/Cargo.toml @@ -22,29 +22,35 @@ mingling = { path = ".", features = [ [package.metadata.docs.rs] features = [ + "core", + "macros", "builds", "structural_renderer", "repl", "comp", "parser", + "picker", "clap", "extra_macros", ] [features] +core = ["dep:mingling_core", "mingling_core/default"] +macros = ["dep:mingling_macros", "mingling_macros/default"] + nightly = ["mingling_core/nightly", "mingling_macros/nightly"] debug = ["mingling_core/debug"] async = ["mingling_core/async", "mingling_macros/async"] builds = ["mingling_core/builds"] -default = ["mingling_core/default", "mingling_macros/default"] +default = ["core", "macros"] clap = ["mingling_core/clap", "mingling_macros/clap"] 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", "dep:mingling_picker_macros"] +picker = ["dep:mingling_picker", "mingling_picker/mingling_support"] pathf = ["mingling_core/pathf", "mingling_macros/pathf"] structural_renderer = [ @@ -82,9 +88,8 @@ ron_serde_fmt = ["mingling_core/ron_serde_fmt"] extra_macros = ["mingling_macros/extra_macros"] [dependencies] -mingling_core.workspace = true -mingling_macros.workspace = true +mingling_core = { workspace = true, optional = true } +mingling_macros = { workspace = true, optional = true } mingling_picker = { workspace = true, optional = true } -mingling_picker_macros = { 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 76d73b6..ba87f85 100644 --- a/mingling/src/lib.rs +++ b/mingling/src/lib.rs @@ -69,10 +69,14 @@ //! # About Features //! All features of `Mingling` are opt-in. To learn what each feature provides, see [Features](feature/index.html) or [Helpdoc](https://mingling-rs.github.io/mingling/docs/doc.html#/pages/other/features) +#[cfg(feature = "core")] mod example_docs; // Re-export Core lib +#[cfg(feature = "core")] pub use mingling::*; + +#[cfg(feature = "core")] pub use mingling_core as mingling; /// `Mingling` argument parser (Built-in) @@ -83,6 +87,10 @@ pub mod parser; #[cfg(feature = "picker")] pub mod picker { pub use mingling_picker::*; + + pub mod parselib { + pub use mingling_picker::parselib::*; + } } /// Re-export of all macros from `mingling_macros`. @@ -97,6 +105,7 @@ pub mod picker { /// /// #[allow(unused_imports)] +#[cfg(feature = "macros")] pub mod macros { /// `#[chain]` - Used to generate a struct implementing the `Chain` trait via a method pub use mingling_macros::chain; @@ -170,13 +179,15 @@ pub mod macros { pub use mingling_macros::suggest_enum; /// New Parser provided by the `picker` feature #[cfg(feature = "picker")] - pub use mingling_macros::*; + pub use mingling_picker::macros::*; } /// derive macro `EnumTag` +#[cfg(feature = "macros")] pub use mingling_macros::EnumTag; /// derive macro Groupped +#[cfg(feature = "macros")] pub use mingling_macros::Groupped; /// derive macro `StructuralData` — marks a type as supporting structured output @@ -184,10 +195,12 @@ pub use mingling_macros::Groupped; pub use mingling_macros::StructuralData; /// Example projects for `Mingling`, for learning how to use `Mingling` +#[cfg(feature = "core")] pub mod _mingling_examples { pub use crate::example_docs::*; } +#[cfg(feature = "core")] mod features; /// Module for checking which features are enabled at compile time. @@ -195,19 +208,23 @@ mod features; /// Each constant re-exported from this module corresponds to a Cargo feature flag. /// They can be used for conditional compilation or runtime branching based on /// feature availability. +#[cfg(feature = "core")] pub mod feature { include!("./features.rs"); } +#[cfg(feature = "core")] mod setups; /// Setups provided by Mingling, which can extend command-line programs. +#[cfg(feature = "core")] pub mod setup { pub use crate::setups::*; pub use mingling_core::setup::*; } /// Mutable global resources provided within Mingling +#[cfg(feature = "core")] pub mod res; /// The prelude module provides convenient re-exports of commonly used macros and traits. @@ -223,34 +240,45 @@ pub mod res; /// ``` pub mod prelude { /// Re-export of the `Groupped` derive macro for grouping types. + #[cfg(feature = "core")] pub use crate::Groupped; /// Re-export of the `RenderResult` struct for outputting rendering result + #[cfg(feature = "core")] pub use crate::RenderResult; /// Re-export of the `chain` macro for defining a chain of commands. + #[cfg(feature = "macros")] pub use crate::macros::chain; /// Re-export of the `dispatcher` macro for routing commands. + #[cfg(feature = "macros")] pub use crate::macros::dispatcher; /// Re-export of the `empty_result` macro for creating an empty result value for early return. - #[cfg(feature = "extra_macros")] + #[cfg(all(feature = "extra_macros", feature = "macros"))] pub use crate::macros::empty_result; /// Re-export of the `gen_program` macro for generating the program entry point. + #[cfg(feature = "macros")] pub use crate::macros::gen_program; /// Re-export of the `pack` macro for creating wrapper types. + #[cfg(feature = "macros")] pub use crate::macros::pack; /// Re-export of the `pack_err` macro for creating error types. - #[cfg(feature = "extra_macros")] + #[cfg(all(feature = "extra_macros", feature = "macros"))] pub use crate::macros::pack_err; /// Re-export of the `renderer` macro for defining renderer functions. + #[cfg(feature = "macros")] pub use crate::macros::renderer; /// Like `pack_err!` but also marks the type for structured output - #[cfg(all(feature = "structural_renderer", feature = "extra_macros"))] + #[cfg(all( + feature = "macros", + feature = "structural_renderer", + feature = "extra_macros" + ))] pub use mingling_macros::pack_err_structural; /// Like `pack!` but also marks the type for structured output - #[cfg(feature = "structural_renderer")] + #[cfg(all(feature = "macros", feature = "structural_renderer"))] pub use mingling_macros::pack_structural; /// Re-export of the `completion` macro for generating completion entries. - #[cfg(feature = "comp")] + #[cfg(all(feature = "macros", feature = "comp"))] pub use crate::macros::completion; /// Re-export of the `AsPicker` trait for picker functionality. @@ -261,5 +289,6 @@ pub mod prelude { pub use mingling_picker::prelude::*; /// Used to enable the `writeln!` macro for `RenderResult` + #[cfg(feature = "core")] pub use std::io::Write; } diff --git a/mingling_core/tests/test-all/Cargo.lock b/mingling_core/tests/test-all/Cargo.lock index 49348cf..bb7b75b 100644 --- a/mingling_core/tests/test-all/Cargo.lock +++ b/mingling_core/tests/test-all/Cargo.lock @@ -67,13 +67,19 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5454cda0d57db59778608d7a47bff5b16c6705598265869fb052b657f66cf05e" +[[package]] +name = "just_fmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6170dccbc3ea15dfb7f2da964097f814aba1dd8f746d4ffc56f33245c38e6d96" + [[package]] name = "just_template" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4fb99a3c1dee7299c57b26ef927f15535da0fbc93d6deb1d1114cae1337be4fb" dependencies = [ - "just_fmt", + "just_fmt 0.1.2", "just_template_macros", ] @@ -123,7 +129,7 @@ dependencies = [ name = "mingling_core" version = "0.3.0" dependencies = [ - "just_fmt", + "just_fmt 0.2.0", "just_template", "ron", "serde", @@ -136,7 +142,7 @@ dependencies = [ name = "mingling_macros" version = "0.3.0" dependencies = [ - "just_fmt", + "just_fmt 0.2.0", "proc-macro2", "quote", "syn", diff --git a/mingling_core/tests/test-basic/Cargo.lock b/mingling_core/tests/test-basic/Cargo.lock index 7b14f0b..5029bbf 100644 --- a/mingling_core/tests/test-basic/Cargo.lock +++ b/mingling_core/tests/test-basic/Cargo.lock @@ -4,9 +4,9 @@ version = 4 [[package]] name = "just_fmt" -version = "0.1.2" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5454cda0d57db59778608d7a47bff5b16c6705598265869fb052b657f66cf05e" +checksum = "6170dccbc3ea15dfb7f2da964097f814aba1dd8f746d4ffc56f33245c38e6d96" [[package]] name = "mingling" diff --git a/mingling_core/tests/test-comp/Cargo.lock b/mingling_core/tests/test-comp/Cargo.lock index bf843cf..175b65e 100644 --- a/mingling_core/tests/test-comp/Cargo.lock +++ b/mingling_core/tests/test-comp/Cargo.lock @@ -8,13 +8,19 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5454cda0d57db59778608d7a47bff5b16c6705598265869fb052b657f66cf05e" +[[package]] +name = "just_fmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6170dccbc3ea15dfb7f2da964097f814aba1dd8f746d4ffc56f33245c38e6d96" + [[package]] name = "just_template" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4fb99a3c1dee7299c57b26ef927f15535da0fbc93d6deb1d1114cae1337be4fb" dependencies = [ - "just_fmt", + "just_fmt 0.1.2", "just_template_macros", ] @@ -41,7 +47,7 @@ dependencies = [ name = "mingling_core" version = "0.3.0" dependencies = [ - "just_fmt", + "just_fmt 0.2.0", "just_template", ] @@ -49,7 +55,7 @@ dependencies = [ name = "mingling_macros" version = "0.3.0" dependencies = [ - "just_fmt", + "just_fmt 0.2.0", "proc-macro2", "quote", "syn", diff --git a/mingling_core/tests/test-dispatch-tree/Cargo.lock b/mingling_core/tests/test-dispatch-tree/Cargo.lock index 2c3a382..39d82c5 100644 --- a/mingling_core/tests/test-dispatch-tree/Cargo.lock +++ b/mingling_core/tests/test-dispatch-tree/Cargo.lock @@ -4,9 +4,9 @@ version = 4 [[package]] name = "just_fmt" -version = "0.1.2" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5454cda0d57db59778608d7a47bff5b16c6705598265869fb052b657f66cf05e" +checksum = "6170dccbc3ea15dfb7f2da964097f814aba1dd8f746d4ffc56f33245c38e6d96" [[package]] name = "mingling" diff --git a/mingling_core/tests/test-repl/Cargo.lock b/mingling_core/tests/test-repl/Cargo.lock index 14384f1..2b990f2 100644 --- a/mingling_core/tests/test-repl/Cargo.lock +++ b/mingling_core/tests/test-repl/Cargo.lock @@ -4,9 +4,9 @@ version = 4 [[package]] name = "just_fmt" -version = "0.1.2" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5454cda0d57db59778608d7a47bff5b16c6705598265869fb052b657f66cf05e" +checksum = "6170dccbc3ea15dfb7f2da964097f814aba1dd8f746d4ffc56f33245c38e6d96" [[package]] name = "mingling" diff --git a/mingling_core/tests/test-structural-renderer/Cargo.lock b/mingling_core/tests/test-structural-renderer/Cargo.lock index 1d3c192..bf5291d 100644 --- a/mingling_core/tests/test-structural-renderer/Cargo.lock +++ b/mingling_core/tests/test-structural-renderer/Cargo.lock @@ -41,9 +41,9 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "just_fmt" -version = "0.1.2" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5454cda0d57db59778608d7a47bff5b16c6705598265869fb052b657f66cf05e" +checksum = "6170dccbc3ea15dfb7f2da964097f814aba1dd8f746d4ffc56f33245c38e6d96" [[package]] name = "memchr" diff --git a/mingling_pathf/test/Cargo.lock b/mingling_pathf/test/Cargo.lock index 8b567b5..1260662 100644 --- a/mingling_pathf/test/Cargo.lock +++ b/mingling_pathf/test/Cargo.lock @@ -8,11 +8,17 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5454cda0d57db59778608d7a47bff5b16c6705598265869fb052b657f66cf05e" +[[package]] +name = "just_fmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6170dccbc3ea15dfb7f2da964097f814aba1dd8f746d4ffc56f33245c38e6d96" + [[package]] name = "mingling_pathf" version = "0.3.0" dependencies = [ - "just_fmt", + "just_fmt 0.2.0", "proc-macro2", "syn", ] @@ -50,7 +56,7 @@ dependencies = [ name = "test-mingling-pathf" version = "0.1.0" dependencies = [ - "just_fmt", + "just_fmt 0.1.2", "mingling_pathf", ] diff --git a/mingling_picker/Cargo.toml b/mingling_picker/Cargo.toml index f992373..ddab33c 100644 --- a/mingling_picker/Cargo.toml +++ b/mingling_picker/Cargo.toml @@ -8,5 +8,10 @@ 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 +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 index e69de29..c83bff4 100644 --- a/mingling_picker/README.md +++ b/mingling_picker/README.md @@ -0,0 +1,42 @@ +# 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 +let args: Vec<&str> = vec!["--name", "Bob", "--age", "24"]; +let result = args + .pick(&flag![name: String]) + .or(|| "Alice".to_string()) + .pick(&flag![age: i32]) + .or(|| 24) + .post(|num| num.clamp(0, 120)) + .parse(); +let (name, age): (String, i32) = a.unwrap(); +``` + +## 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 new file mode 100644 index 0000000..78ad539 --- /dev/null +++ b/mingling_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/mingling_picker/src/builtin.rs b/mingling_picker/src/builtin.rs new file mode 100644 index 0000000..e855b08 --- /dev/null +++ b/mingling_picker/src/builtin.rs @@ -0,0 +1,4 @@ +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 new file mode 100644 index 0000000..ccc4424 --- /dev/null +++ b/mingling_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/mingling_picker/src/builtin/pick_flag.rs b/mingling_picker/src/builtin/pick_flag.rs new file mode 100644 index 0000000..b642a9a --- /dev/null +++ b/mingling_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/mingling_picker/src/builtin/pick_numbers.rs b/mingling_picker/src/builtin/pick_numbers.rs new file mode 100644 index 0000000..a5ab0a9 --- /dev/null +++ b/mingling_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/mingling_picker/src/builtin/pick_string.rs b/mingling_picker/src/builtin/pick_string.rs new file mode 100644 index 0000000..c96f667 --- /dev/null +++ b/mingling_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/mingling_picker/src/corebind.rs b/mingling_picker/src/corebind.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/mingling_picker/src/corebind.rs @@ -0,0 +1 @@ + diff --git a/mingling_picker/src/infos.rs b/mingling_picker/src/infos.rs new file mode 100644 index 0000000..d2a0fce --- /dev/null +++ b/mingling_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 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 index 3c3251d..afc7aca 100644 --- a/mingling_picker/src/lib.rs +++ b/mingling_picker/src/lib.rs @@ -1 +1,43 @@ -pub mod prelude {} +mod builtin; + +mod picker; +pub use picker::*; + +mod pickable; +pub use pickable::*; + +mod arg; +pub use arg::*; + +mod infos; +pub use infos::*; + +pub mod parselib; + +pub mod value; + +pub mod prelude { + pub use crate::IntoPicker; +} + +pub mod macros { + pub use mingling_picker_macros::*; +} + +/// 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 new file mode 100644 index 0000000..7fbd606 --- /dev/null +++ b/mingling_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/mingling_picker/src/parselib/arg_matcher.rs b/mingling_picker/src/parselib/arg_matcher.rs new file mode 100644 index 0000000..38bb9cc --- /dev/null +++ b/mingling_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/mingling_picker/src/parselib/flag_matcher.rs b/mingling_picker/src/parselib/flag_matcher.rs new file mode 100644 index 0000000..e93d35a --- /dev/null +++ b/mingling_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/mingling_picker/src/parselib/multi_arg_matcher.rs b/mingling_picker/src/parselib/multi_arg_matcher.rs new file mode 100644 index 0000000..e8bdb71 --- /dev/null +++ b/mingling_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/mingling_picker/src/parselib/pos_matcher.rs b/mingling_picker/src/parselib/pos_matcher.rs new file mode 100644 index 0000000..279e01e --- /dev/null +++ b/mingling_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/mingling_picker/src/parselib/single_matcher.rs b/mingling_picker/src/parselib/single_matcher.rs new file mode 100644 index 0000000..25c4741 --- /dev/null +++ b/mingling_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/mingling_picker/src/parselib/style.rs b/mingling_picker/src/parselib/style.rs new file mode 100644 index 0000000..4ea161f --- /dev/null +++ b/mingling_picker/src/parselib/style.rs @@ -0,0 +1,225 @@ +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 + /// + /// ```ignore + /// 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()) + } +} + +#[repr(u8)] +#[derive(Default, Clone, Copy, PartialEq, Eq)] +pub enum ParserStyleNamingCase { + /// snake_case format (e.g., `brew_coffee`) + #[default] + Snake, + /// camelCase format (e.g., `brewCoffee`) + Camel, + /// PascalCase format (e.g., `BrewCoffee`) + Pascal, + /// kebab-case format (e.g., `brew-coffee`) + Kebab, + /// dot.case format (e.g., `brew.coffee`) + Dot, + /// Title Case format (e.g., `Brew Coffee`) + Title, + /// lower case format (e.g., `brew coffee`) + Lower, + /// UPPER CASE format (e.g., `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 new file mode 100644 index 0000000..726346c --- /dev/null +++ b/mingling_picker/src/parselib/utils.rs @@ -0,0 +1,271 @@ +use crate::{ + PickerArgInfo, + parselib::{MaskedArg, ParserStyle}, +}; + +#[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 new file mode 100644 index 0000000..758ae9a --- /dev/null +++ b/mingling_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/mingling_picker/src/pickable/multi_pickable.rs b/mingling_picker/src/pickable/multi_pickable.rs new file mode 100644 index 0000000..0ab0508 --- /dev/null +++ b/mingling_picker/src/pickable/multi_pickable.rs @@ -0,0 +1,77 @@ +use crate::{ + matcher_needed::Matcher, + parselib::{MultiArgMatcher, ParserStyle}, + Pickable, PickerArg, PickerArgAttr, PickerArgResult, + SinglePickable, TagPhaseContext, +}; + +/// 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 new file mode 100644 index 0000000..8a5b3e6 --- /dev/null +++ b/mingling_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/mingling_picker/src/picker.rs b/mingling_picker/src/picker.rs new file mode 100644 index 0000000..4efb532 --- /dev/null +++ b/mingling_picker/src/picker.rs @@ -0,0 +1,360 @@ +use std::{marker::PhantomData, ops::Index}; + +mod parse; + +mod patterns; +pub use patterns::*; + +mod result; +pub use result::*; + +use crate::{Pickable, PickerArg, PickerArgResult}; + +/// Picker, used to record all states of a parameter parsing +/// +/// Includes the following: +/// +/// - Basic arguments +/// - Parsing states +/// - Parsing results +pub struct Picker<'a, Route = ()> { + route_phantom: PhantomData, + + /// Internal arguments of Picker + 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> {} + +/// 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, + { + PickerPattern1 { + args: self.to_picker().args, + arg_1: arg.into(), + result_1: PickerArgResult::Unparsed, + default_1: None, + route_1: None, + post_1: None, + error_route: 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 Vec { + fn to_picker(self) -> Picker<'a, ()> { + Picker { + route_phantom: PhantomData, + args: PickerArgs::Owned(self), + } + } +} diff --git a/mingling_picker/src/picker/parse.rs b/mingling_picker/src/picker/parse.rs new file mode 100644 index 0000000..8f7d514 --- /dev/null +++ b/mingling_picker/src/picker/parse.rs @@ -0,0 +1,177 @@ +// -------------------------------------------------------------------------------------------- +// I have to say, the code generated by this `internal_repeat!` macro is really UGLY. +// +// But I have to admit, this is a **trade-off**. To achieve the syntax of `pick().pick().pick()` +// while ensuring type safety, this is the best approach I could think of. +// +// P.S. If there's a better way, please let me know. Thanks! +// -------------------------------------------------------------------------------------------- +// +// Then, I must disable `clippy::type_complexity` — this guy is way too noisy. +#![allow(clippy::type_complexity)] + +use crate::{Pickable, PickerArgAttr, PickerArgInfo, PickerArgResult, PickerArgs, TagPhaseContext}; +use mingling_picker_macros::internal_repeat; + +internal_repeat!(1..=32 => { + use crate::PickerPattern$; + use crate::PickerResult$; +}); + +internal_repeat!(1..=32 => { + impl<'a, (T$,+), Route> PickerPattern$<'a, (T$,+), Route> + where (T$: Pickable<'a>,+) { + /// Unwraps the result, panicking if a route was selected. + /// + /// # Panics + /// + /// Panics if a route was selected. + pub fn unwrap(self) -> ((T$,+)) { + let p = self.parse(); + ((p.v$.unwrap(),+)) + } + + /// Returns the individual option values without checking the route. + pub fn unpack(self) -> ((Option,+)) { + 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()); + } + } + 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 new file mode 100644 index 0000000..3c6e73f --- /dev/null +++ b/mingling_picker/src/picker/patterns.rs @@ -0,0 +1,199 @@ +use mingling_picker_macros::internal_repeat; + +use crate::{Pickable, Picker, PickerArg, PickerArgResult, PickerArgs}; + +internal_repeat!(1..=32 => { + #[doc(hidden)] + pub struct PickerPattern$<'a, (T$,+), Route> + where (T$: Pickable<'a>,+) + { + pub args: PickerArgs<'a>, + pub error_route: Option, + ( + pub arg_$: &'a PickerArg<'a, T$>, + pub result_$: PickerArgResult, + pub default_$: Option T$>>, + pub route_$: Option Route>>, + pub 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 + /// + /// ```ignore + /// let pattern = picker + /// .pick(&my_arg) + /// .or(|| 42); + /// ``` + #[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 + /// + /// ```ignore + /// let pattern = picker + /// .pick(&my_arg) + /// .or_default(); + /// ``` + #[allow(clippy::type_complexity)] + pub fn or_default(mut self) -> Self + where + T$: Default, + { + self.default_$ = Some(Box::new(|| T$::default())); + self + } + + /// Sets a route for when the arg is not provided. + /// + /// If the arg is not provided by the user at runtime, the given closure will be + /// called to produce a route value that will be returned early. + /// + /// # Example + /// + /// ```ignore + /// let pattern = picker + /// .pick(&my_arg) + /// .or_route(|| Redirect::home()); + /// ``` + pub fn or_route(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 + /// + /// ```ignore + /// let pattern = picker + /// .pick(&my_arg) + /// .post(|val| val * 2); + /// ``` + #[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_$, + +) + } + } + } +}); + +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>, + { + PickerPattern1 { + args: self.args, + error_route: None::, + arg_1: arg.into(), + result_1: PickerArgResult::Unparsed, + route_1: None, + default_1: None, + post_1: None, + } + } +} diff --git a/mingling_picker/src/picker/result.rs b/mingling_picker/src/picker/result.rs new file mode 100644 index 0000000..9cd78ae --- /dev/null +++ b/mingling_picker/src/picker/result.rs @@ -0,0 +1,56 @@ +#![allow(clippy::type_complexity)] // Aha, Type Gymnastics! + +use mingling_picker_macros::internal_repeat; + +internal_repeat!(1..=32 => { + #[doc(hidden)] + pub struct PickerResult$<(T$,+), Route> { + /// The route selected by the picker, if any. + /// If this is `Some`, the picker chose to follow a route instead of selecting values, + /// and all value fields (`v1`, `v2`, ...) will be `None`. + /// + /// Note: "route" here refers to an alternative path/choice, not a network route. + pub route: Option, + + ( + #[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 new file mode 100644 index 0000000..995aa00 --- /dev/null +++ b/mingling_picker/src/value.rs @@ -0,0 +1,5 @@ +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 new file mode 100644 index 0000000..ee0d6ee --- /dev/null +++ b/mingling_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 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 new file mode 100644 index 0000000..1b79641 --- /dev/null +++ b/mingling_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/mingling_picker/test/Cargo.lock b/mingling_picker/test/Cargo.lock new file mode 100644 index 0000000..0fef84b --- /dev/null +++ b/mingling_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 = "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 new file mode 100644 index 0000000..127546c --- /dev/null +++ b/mingling_picker/test/Cargo.toml @@ -0,0 +1,9 @@ +[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 new file mode 100644 index 0000000..eac6cad --- /dev/null +++ b/mingling_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 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 new file mode 100644 index 0000000..9c53514 --- /dev/null +++ b/mingling_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/mingling_picker/test/src/test/arg_matcher_test.rs b/mingling_picker/test/src/test/arg_matcher_test.rs new file mode 100644 index 0000000..7b37bc8 --- /dev/null +++ b/mingling_picker/test/src/test/arg_matcher_test.rs @@ -0,0 +1,289 @@ +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 new file mode 100644 index 0000000..78b154e --- /dev/null +++ b/mingling_picker/test/src/test/basic_test.rs @@ -0,0 +1,223 @@ +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 new file mode 100644 index 0000000..377357e --- /dev/null +++ b/mingling_picker/test/src/test/multi_arg_test.rs @@ -0,0 +1,181 @@ +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 new file mode 100644 index 0000000..0f56517 --- /dev/null +++ b/mingling_picker/test/src/test/multi_value_test.rs @@ -0,0 +1,66 @@ +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 new file mode 100644 index 0000000..b5319f5 --- /dev/null +++ b/mingling_picker/test/src/test/pos_matcher_test.rs @@ -0,0 +1,141 @@ +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 new file mode 100644 index 0000000..8179389 --- /dev/null +++ b/mingling_picker/test/src/test/priority_test.rs @@ -0,0 +1,85 @@ +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 new file mode 100644 index 0000000..9261db1 --- /dev/null +++ b/mingling_picker/test/src/test/route_test.rs @@ -0,0 +1,200 @@ +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 new file mode 100644 index 0000000..3c337b9 --- /dev/null +++ b/mingling_picker/test/src/test/style_test.rs @@ -0,0 +1,300 @@ +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 new file mode 100644 index 0000000..d267a27 --- /dev/null +++ b/mingling_picker/test/src/test/value_flag_test.rs @@ -0,0 +1,174 @@ +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 new file mode 100644 index 0000000..b1e5c0b --- /dev/null +++ b/mingling_picker/test/src/test/value_string_test.rs @@ -0,0 +1,171 @@ +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 index 5bab6e5..58488ea 100644 --- a/mingling_picker_macros/Cargo.toml +++ b/mingling_picker_macros/Cargo.toml @@ -11,6 +11,9 @@ description = "Mingling's lightweight argument parser macros" [lib] proc-macro = true +[features] +mingling_support = [] + [dependencies] syn.workspace = true quote.workspace = true diff --git a/mingling_picker_macros/src/arg.rs b/mingling_picker_macros/src/arg.rs new file mode 100644 index 0000000..70b27b0 --- /dev/null +++ b/mingling_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! { ::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 new file mode 100644 index 0000000..4b2242b --- /dev/null +++ b/mingling_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/mingling_picker_macros/src/lib.rs b/mingling_picker_macros/src/lib.rs index 8b13789..33bb67d 100644 --- a/mingling_picker_macros/src/lib.rs +++ b/mingling_picker_macros/src/lib.rs @@ -1 +1,30 @@ +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