diff options
90 files changed, 2385 insertions, 1205 deletions
diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..30cf57e --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,10 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Ignored default folder with query files +/queries/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/mingling.iml b/.idea/mingling.iml new file mode 100644 index 0000000..ca57e6c --- /dev/null +++ b/.idea/mingling.iml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8"?> +<module type="EMPTY_MODULE" version="4"> + <component name="NewModuleRootManager"> + <content url="file://$MODULE_DIR$"> + <sourceFolder url="file://$MODULE_DIR$/arg_picker/src" isTestSource="false" /> + <sourceFolder url="file://$MODULE_DIR$/arg_picker_macros/src" isTestSource="false" /> + <sourceFolder url="file://$MODULE_DIR$/mingling/src" isTestSource="false" /> + <sourceFolder url="file://$MODULE_DIR$/mingling_core/src" isTestSource="false" /> + <sourceFolder url="file://$MODULE_DIR$/mingling_core/tests" isTestSource="true" /> + <sourceFolder url="file://$MODULE_DIR$/mingling_macros/src" isTestSource="false" /> + <sourceFolder url="file://$MODULE_DIR$/mingling_pathf/src" isTestSource="false" /> + <sourceFolder url="file://$MODULE_DIR$/mling/src" isTestSource="false" /> + <excludeFolder url="file://$MODULE_DIR$/target" /> + </content> + <orderEntry type="inheritedJdk" /> + <orderEntry type="sourceFolder" forTests="false" /> + </component> +</module>
\ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..764f612 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project version="4"> + <component name="ProjectModuleManager"> + <modules> + <module fileurl="file://$PROJECT_DIR$/.idea/mingling.iml" filepath="$PROJECT_DIR$/.idea/mingling.iml" /> + </modules> + </component> +</project>
\ No newline at end of file diff --git a/.idea/runConfigurations/Check__All.xml b/.idea/runConfigurations/Check__All.xml new file mode 100644 index 0000000..95af540 --- /dev/null +++ b/.idea/runConfigurations/Check__All.xml @@ -0,0 +1,26 @@ +<component name="ProjectRunConfigurationManager"> + <configuration + default="false" + name="Check: All" + type="CargoCommandRunConfiguration" + factoryName="Cargo Command" + folderName="CI" + > + <option + name="command" + value="run --manifest-path .run/Cargo.toml --bin ci --quiet -- -y" + /> + <option name="workingDirectory" value="file://$PROJECT_DIR$" /> + <envs /> + <option name="emulateTerminal" value="true" /> + <option name="channel" value="DEFAULT" /> + <option name="requiredFeatures" value="true" /> + <option name="allFeatures" value="false" /> + <option name="withSudo" value="false" /> + <option name="buildTarget" value="REMOTE" /> + <option name="backtrace" value="SHORT" /> + <option name="isRedirectInput" value="false" /> + <option name="redirectInputPath" value="" /> + <method v="2" /> + </configuration> +</component> diff --git a/.idea/runConfigurations/Check__Codes.xml b/.idea/runConfigurations/Check__Codes.xml new file mode 100644 index 0000000..f117f8f --- /dev/null +++ b/.idea/runConfigurations/Check__Codes.xml @@ -0,0 +1,26 @@ +<component name="ProjectRunConfigurationManager"> + <configuration + default="false" + name="Check: Codes" + type="CargoCommandRunConfiguration" + factoryName="Cargo Command" + folderName="CI" + > + <option + name="command" + value="run --manifest-path .run/Cargo.toml --bin ci --quiet -- --test-codes -y" + /> + <option name="workingDirectory" value="file://$PROJECT_DIR$" /> + <envs /> + <option name="emulateTerminal" value="true" /> + <option name="channel" value="DEFAULT" /> + <option name="requiredFeatures" value="true" /> + <option name="allFeatures" value="false" /> + <option name="withSudo" value="false" /> + <option name="buildTarget" value="REMOTE" /> + <option name="backtrace" value="SHORT" /> + <option name="isRedirectInput" value="false" /> + <option name="redirectInputPath" value="" /> + <method v="2" /> + </configuration> +</component> diff --git a/.idea/runConfigurations/Check__Documents.xml b/.idea/runConfigurations/Check__Documents.xml new file mode 100644 index 0000000..b5a503b --- /dev/null +++ b/.idea/runConfigurations/Check__Documents.xml @@ -0,0 +1,26 @@ +<component name="ProjectRunConfigurationManager"> + <configuration + default="false" + name="Check: Documents" + type="CargoCommandRunConfiguration" + factoryName="Cargo Command" + folderName="CI" + > + <option + name="command" + value="run --manifest-path .run/Cargo.toml --bin ci --quiet -- --test-docs -y" + /> + <option name="workingDirectory" value="file://$PROJECT_DIR$" /> + <envs /> + <option name="emulateTerminal" value="true" /> + <option name="channel" value="DEFAULT" /> + <option name="requiredFeatures" value="true" /> + <option name="allFeatures" value="false" /> + <option name="withSudo" value="false" /> + <option name="buildTarget" value="REMOTE" /> + <option name="backtrace" value="SHORT" /> + <option name="isRedirectInput" value="false" /> + <option name="redirectInputPath" value="" /> + <method v="2" /> + </configuration> +</component> diff --git a/.idea/runConfigurations/Refresh_All.xml b/.idea/runConfigurations/Refresh_All.xml new file mode 100644 index 0000000..24be845 --- /dev/null +++ b/.idea/runConfigurations/Refresh_All.xml @@ -0,0 +1,27 @@ +<component name="ProjectRunConfigurationManager"> + <configuration + default="false" + name="Refresh All" + type="CompoundRunConfigurationType" + > + <toRun name="Run: Cargo Format" type="CargoCommandRunConfiguration" /> + <toRun + name="Run: Fix Documents Codebox" + type="CargoCommandRunConfiguration" + /> + <toRun + name="Run: Generate Docsify Sidebar" + type="CargoCommandRunConfiguration" + /> + <toRun + name="Run: Sync Examples To Crates" + type="CargoCommandRunConfiguration" + /> + <toRun + name="Run: Sync Examples To Helpdoc" + type="CargoCommandRunConfiguration" + /> + <toRun name="Run: Sync Feature List" type="CargoCommandRunConfiguration" /> + <method v="2" /> + </configuration> +</component> diff --git a/.idea/runConfigurations/Run__Cargo_Format.xml b/.idea/runConfigurations/Run__Cargo_Format.xml new file mode 100644 index 0000000..a99a91d --- /dev/null +++ b/.idea/runConfigurations/Run__Cargo_Format.xml @@ -0,0 +1,23 @@ +<component name="ProjectRunConfigurationManager"> + <configuration + default="false" + name="Run: Cargo Format" + type="CargoCommandRunConfiguration" + factoryName="Cargo Command" + folderName="Tools" + > + <option name="command" value="fmt" /> + <option name="workingDirectory" value="file://$PROJECT_DIR$" /> + <envs /> + <option name="emulateTerminal" value="true" /> + <option name="channel" value="DEFAULT" /> + <option name="requiredFeatures" value="true" /> + <option name="allFeatures" value="false" /> + <option name="withSudo" value="false" /> + <option name="buildTarget" value="REMOTE" /> + <option name="backtrace" value="SHORT" /> + <option name="isRedirectInput" value="false" /> + <option name="redirectInputPath" value="" /> + <method v="2" /> + </configuration> +</component> diff --git a/.idea/runConfigurations/Run__Deploy_API_Docs.xml b/.idea/runConfigurations/Run__Deploy_API_Docs.xml new file mode 100644 index 0000000..1a32a64 --- /dev/null +++ b/.idea/runConfigurations/Run__Deploy_API_Docs.xml @@ -0,0 +1,28 @@ +<component name="ProjectRunConfigurationManager"> + <configuration + default="false" + name="Run: Deploy API Docs" + type="CargoCommandRunConfiguration" + factoryName="Cargo Command" + folderName="Tools" + > + <option + name="command" + value="run --manifest-path .run/Cargo.toml --quiet --bin deploy-api-docs" + /> + <option name="workingDirectory" value="file://$PROJECT_DIR$" /> + <envs /> + <option name="emulateTerminal" value="true" /> + <option name="channel" value="DEFAULT" /> + <option name="requiredFeatures" value="true" /> + <option name="allFeatures" value="false" /> + <option name="withSudo" value="false" /> + <option name="buildTarget" value="REMOTE" /> + <option name="backtrace" value="SHORT" /> + <option name="isRedirectInput" value="false" /> + <option name="redirectInputPath" value="" /> + <method v="2"> + <option name="CARGO.BUILD_TASK_PROVIDER" enabled="true" /> + </method> + </configuration> +</component> diff --git a/.idea/runConfigurations/Run__Fix_Documents_Codebox.xml b/.idea/runConfigurations/Run__Fix_Documents_Codebox.xml new file mode 100644 index 0000000..0863ef1 --- /dev/null +++ b/.idea/runConfigurations/Run__Fix_Documents_Codebox.xml @@ -0,0 +1,28 @@ +<component name="ProjectRunConfigurationManager"> + <configuration + default="false" + name="Run: Fix Documents Codebox" + type="CargoCommandRunConfiguration" + factoryName="Cargo Command" + folderName="Tools" + > + <option + name="command" + value="run --manifest-path .run/Cargo.toml --quiet --bin docs-code-box-fix" + /> + <option name="workingDirectory" value="file://$PROJECT_DIR$" /> + <envs /> + <option name="emulateTerminal" value="true" /> + <option name="channel" value="DEFAULT" /> + <option name="requiredFeatures" value="true" /> + <option name="allFeatures" value="false" /> + <option name="withSudo" value="false" /> + <option name="buildTarget" value="REMOTE" /> + <option name="backtrace" value="SHORT" /> + <option name="isRedirectInput" value="false" /> + <option name="redirectInputPath" value="" /> + <method v="2"> + <option name="CARGO.BUILD_TASK_PROVIDER" enabled="true" /> + </method> + </configuration> +</component> diff --git a/.idea/runConfigurations/Run__Generate_Docsify_Sidebar.xml b/.idea/runConfigurations/Run__Generate_Docsify_Sidebar.xml new file mode 100644 index 0000000..a50a2dd --- /dev/null +++ b/.idea/runConfigurations/Run__Generate_Docsify_Sidebar.xml @@ -0,0 +1,28 @@ +<component name="ProjectRunConfigurationManager"> + <configuration + default="false" + name="Run: Generate Docsify Sidebar" + type="CargoCommandRunConfiguration" + factoryName="Cargo Command" + folderName="Tools" + > + <option + name="command" + value="run --manifest-path .run/Cargo.toml --quiet --bin docsify-sidebar-gen" + /> + <option name="workingDirectory" value="file://$PROJECT_DIR$" /> + <envs /> + <option name="emulateTerminal" value="true" /> + <option name="channel" value="DEFAULT" /> + <option name="requiredFeatures" value="true" /> + <option name="allFeatures" value="false" /> + <option name="withSudo" value="false" /> + <option name="buildTarget" value="REMOTE" /> + <option name="backtrace" value="SHORT" /> + <option name="isRedirectInput" value="false" /> + <option name="redirectInputPath" value="" /> + <method v="2"> + <option name="CARGO.BUILD_TASK_PROVIDER" enabled="true" /> + </method> + </configuration> +</component> diff --git a/.idea/runConfigurations/Run__Package_All_Crates.xml b/.idea/runConfigurations/Run__Package_All_Crates.xml new file mode 100644 index 0000000..336c9b3 --- /dev/null +++ b/.idea/runConfigurations/Run__Package_All_Crates.xml @@ -0,0 +1,28 @@ +<component name="ProjectRunConfigurationManager"> + <configuration + default="false" + name="Run: Package All Crates" + type="CargoCommandRunConfiguration" + factoryName="Cargo Command" + folderName="Tools" + > + <option + name="command" + value="run --manifest-path .run/Cargo.toml --quiet --bin package-all" + /> + <option name="workingDirectory" value="file://$PROJECT_DIR$" /> + <envs /> + <option name="emulateTerminal" value="true" /> + <option name="channel" value="DEFAULT" /> + <option name="requiredFeatures" value="true" /> + <option name="allFeatures" value="false" /> + <option name="withSudo" value="false" /> + <option name="buildTarget" value="REMOTE" /> + <option name="backtrace" value="SHORT" /> + <option name="isRedirectInput" value="false" /> + <option name="redirectInputPath" value="" /> + <method v="2"> + <option name="CARGO.BUILD_TASK_PROVIDER" enabled="true" /> + </method> + </configuration> +</component> diff --git a/.idea/runConfigurations/Run__Sync_Examples_To_Crates.xml b/.idea/runConfigurations/Run__Sync_Examples_To_Crates.xml new file mode 100644 index 0000000..43db9bb --- /dev/null +++ b/.idea/runConfigurations/Run__Sync_Examples_To_Crates.xml @@ -0,0 +1,28 @@ +<component name="ProjectRunConfigurationManager"> + <configuration + default="false" + name="Run: Sync Examples To Crates" + type="CargoCommandRunConfiguration" + factoryName="Cargo Command" + folderName="Tools" + > + <option + name="command" + value="run --manifest-path .run/Cargo.toml --quiet --bin refresh-docs" + /> + <option name="workingDirectory" value="file://$PROJECT_DIR$" /> + <envs /> + <option name="emulateTerminal" value="true" /> + <option name="channel" value="DEFAULT" /> + <option name="requiredFeatures" value="true" /> + <option name="allFeatures" value="false" /> + <option name="withSudo" value="false" /> + <option name="buildTarget" value="REMOTE" /> + <option name="backtrace" value="SHORT" /> + <option name="isRedirectInput" value="false" /> + <option name="redirectInputPath" value="" /> + <method v="2"> + <option name="CARGO.BUILD_TASK_PROVIDER" enabled="true" /> + </method> + </configuration> +</component> diff --git a/.idea/runConfigurations/Run__Sync_Examples_To_Helpdoc.xml b/.idea/runConfigurations/Run__Sync_Examples_To_Helpdoc.xml new file mode 100644 index 0000000..8e7faff --- /dev/null +++ b/.idea/runConfigurations/Run__Sync_Examples_To_Helpdoc.xml @@ -0,0 +1,28 @@ +<component name="ProjectRunConfigurationManager"> + <configuration + default="false" + name="Run: Sync Examples To Helpdoc" + type="CargoCommandRunConfiguration" + factoryName="Cargo Command" + folderName="Tools" + > + <option + name="command" + value="run --manifest-path .run/Cargo.toml --quiet --bin sync-examples" + /> + <option name="workingDirectory" value="file://$PROJECT_DIR$" /> + <envs /> + <option name="emulateTerminal" value="true" /> + <option name="channel" value="DEFAULT" /> + <option name="requiredFeatures" value="true" /> + <option name="allFeatures" value="false" /> + <option name="withSudo" value="false" /> + <option name="buildTarget" value="REMOTE" /> + <option name="backtrace" value="SHORT" /> + <option name="isRedirectInput" value="false" /> + <option name="redirectInputPath" value="" /> + <method v="2"> + <option name="CARGO.BUILD_TASK_PROVIDER" enabled="true" /> + </method> + </configuration> +</component> diff --git a/.idea/runConfigurations/Run__Sync_Feature_List.xml b/.idea/runConfigurations/Run__Sync_Feature_List.xml new file mode 100644 index 0000000..1e6ed8f --- /dev/null +++ b/.idea/runConfigurations/Run__Sync_Feature_List.xml @@ -0,0 +1,28 @@ +<component name="ProjectRunConfigurationManager"> + <configuration + default="false" + name="Run: Sync Feature List" + type="CargoCommandRunConfiguration" + factoryName="Cargo Command" + folderName="Tools" + > + <option + name="command" + value="run --manifest-path .run/Cargo.toml --quiet --bin refresh-feature-mod" + /> + <option name="workingDirectory" value="file://$PROJECT_DIR$" /> + <envs /> + <option name="emulateTerminal" value="true" /> + <option name="channel" value="DEFAULT" /> + <option name="requiredFeatures" value="true" /> + <option name="allFeatures" value="false" /> + <option name="withSudo" value="false" /> + <option name="buildTarget" value="REMOTE" /> + <option name="backtrace" value="SHORT" /> + <option name="isRedirectInput" value="false" /> + <option name="redirectInputPath" value="" /> + <method v="2"> + <option name="CARGO.BUILD_TASK_PROVIDER" enabled="true" /> + </method> + </configuration> +</component> diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project version="4"> + <component name="VcsDirectoryMappings"> + <mapping directory="" vcs="Git" /> + </component> +</project>
\ No newline at end of file diff --git a/.run/Cargo.lock b/.run/Cargo.lock index bb510bd..c255ad0 100644 --- a/.run/Cargo.lock +++ b/.run/Cargo.lock @@ -3,6 +3,18 @@ version = 4 [[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "bitflags" +version = "2.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" + +[[package]] name = "bumpalo" version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -36,6 +48,15 @@ dependencies = [ ] [[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] name = "encode_unicode" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -48,6 +69,36 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "filetime" +version = "0.2.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759" +dependencies = [ + "cfg-if", + "libc", +] + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] name = "futures-core" version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -139,12 +190,28 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] name = "memchr" version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] name = "once_cell" version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -181,6 +248,19 @@ dependencies = [ ] [[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] name = "rustversion" version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -239,6 +319,12 @@ dependencies = [ ] [[package]] +name = "simd-adler32" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" + +[[package]] name = "slab" version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -256,6 +342,17 @@ dependencies = [ ] [[package]] +name = "tar" +version = "0.4.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f6221d9a6003c78398e3b239969f352578258df48c8eb051caadae0015bc840" +dependencies = [ + "filetime", + "libc", + "xattr", +] + +[[package]] name = "tokio" version = "1.52.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -322,11 +419,13 @@ name = "tools" version = "0.1.0" dependencies = [ "colored", + "flate2", "indicatif", "just_fmt", "just_template", "serde", "serde_json", + "tar", "tokio", "toml", ] @@ -429,6 +528,16 @@ dependencies = [ ] [[package]] +name = "xattr" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" +dependencies = [ + "libc", + "rustix", +] + +[[package]] name = "zmij" version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" diff --git a/.run/Cargo.toml b/.run/Cargo.toml index 283aa47..caeb506 100644 --- a/.run/Cargo.toml +++ b/.run/Cargo.toml @@ -19,5 +19,7 @@ serde = { version = "1", features = ["derive"] } serde_json = "1" tokio = { version = "1", features = ["rt-multi-thread", "macros"] } indicatif = "0.18.4" +flate2 = "1" +tar = "0.4" [workspace] diff --git a/.run/src/bin/clippy-fix.sh b/.run/src/bin/clippy-fix.sh index 9771ad4..9771ad4 100644..100755 --- a/.run/src/bin/clippy-fix.sh +++ b/.run/src/bin/clippy-fix.sh diff --git a/.run/src/bin/package-all.rs b/.run/src/bin/package-all.rs index 1f1f50d..5d7cbbb 100644 --- a/.run/src/bin/package-all.rs +++ b/.run/src/bin/package-all.rs @@ -1,6 +1,8 @@ use std::path::{Path, PathBuf}; +use flate2::read::GzDecoder; use serde::Deserialize; +use tar::Archive; use tools::{ dependency_order::find_workspace_root, eprintln_cargo_style, println_cargo_style, run_cmd_capture_with_dir, wprintln_cargo_style, @@ -230,20 +232,18 @@ fn main() { std::fs::create_dir_all(&target_dir) .unwrap_or_else(|e| panic!("failed to create {}: {e}", target_dir.display())); - // Extract using tar + gzip - let extract_cmd = format!( - "tar -xzf \"{}\" -C \"{}\"", - path.display(), - target_dir.display() - ); - let extract_ok = std::process::Command::new("sh") - .arg("-c") - .arg(&extract_cmd) - .status() - .expect("failed to run tar"); - - if !extract_ok.success() { - eprintln_cargo_style!("Failed to extract {}", fname); + // Extract using flate2 + tar (cross-platform) + let file = match std::fs::File::open(&path) { + Ok(f) => f, + Err(e) => { + eprintln_cargo_style!("Failed to open {}: {e}", path.display()); + continue; + } + }; + let decoder = GzDecoder::new(file); + let mut archive = Archive::new(decoder); + if let Err(e) = archive.unpack(&target_dir) { + eprintln_cargo_style!("Failed to extract {}: {e}", fname); continue; } diff --git a/.run/src/dependency_order.rs b/.run/src/dependency_order.rs index 99317ae..145bdbd 100644 --- a/.run/src/dependency_order.rs +++ b/.run/src/dependency_order.rs @@ -1,5 +1,5 @@ use std::collections::{HashMap, HashSet}; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; /// Parse Cargo.toml content and return dependency names that start with `prefix`. fn parse_mingling_deps(content: &str, prefix: &str) -> Vec<String> { @@ -119,10 +119,23 @@ fn topological_sort( result } +/// Strip the `\\?\` prefix that `std::fs::canonicalize` may add on Windows. +fn strip_verbatim_prefix(p: &Path) -> PathBuf { + let s = p.to_string_lossy(); + let s_ref: &str = &s; + if let Some(rest) = s_ref.strip_prefix("\\\\?\\") { + PathBuf::from(rest) + } else { + p.to_path_buf() + } +} + /// Find the workspace root by looking for a Cargo.toml with `[workspace]` members. /// Starts from `start` and walks up the directory tree. pub fn find_workspace_root(start: &std::path::Path) -> Option<PathBuf> { - let mut current = Some(std::fs::canonicalize(start).unwrap_or_else(|_| start.to_path_buf())); + let mut current = Some(strip_verbatim_prefix( + &std::fs::canonicalize(start).unwrap_or_else(|_| start.to_path_buf()), + )); while let Some(dir) = current { let members = get_workspace_members(&dir); if !members.is_empty() { diff --git a/.vscode/settings.json b/.vscode/settings.json index 389a476..1b585d1 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,11 +1,18 @@ { - "rust-analyzer.check.command": "clippy", - "rust-analyzer.checkOnSave": true, - "rust-analyzer.linkedProjects": [ - ".run/Cargo.toml", - "mingling_pathf/test/Cargo.toml", - "arg_picker/Cargo.toml", - "arg_picker/test/Cargo.toml" - ], - "rust-analyzer.cargo.features": ["mingling_support"] + "rust-analyzer.check.command": "clippy", + "rust-analyzer.checkOnSave": true, + "rust-analyzer.linkedProjects": [ + ".run/Cargo.toml", + "mingling_pathf/test/Cargo.toml", + "arg_picker/Cargo.toml", + "arg_picker/test/Cargo.toml", + ], + "rust-analyzer.cargo.features": [ + "structural_renderer", + "mingling_support", + "all_serde_fmt", + "picker", + "parser", + "comp", + ], } diff --git a/.zed/settings.json b/.zed/settings.json index 103cc28..727f955 100644 --- a/.zed/settings.json +++ b/.zed/settings.json @@ -1,19 +1,26 @@ { - "lsp": { - "rust-analyzer": { - "initialization_options": { - "checkOnSave": true, - "check": { "command": "clippy" }, - "linkedProjects": [ - ".run/Cargo.toml", - "mingling_pathf/test/Cargo.toml", - "arg_picker/Cargo.toml", - "arg_picker/test/Cargo.toml" - ], - "cargo": { - "features": ["mingling_support"] - } - } - } - } + "lsp": { + "rust-analyzer": { + "initialization_options": { + "checkOnSave": true, + "check": { "command": "clippy" }, + "linkedProjects": [ + ".run/Cargo.toml", + "mingling_pathf/test/Cargo.toml", + "arg_picker/Cargo.toml", + "arg_picker/test/Cargo.toml", + ], + "cargo": { + "features": [ + "structural_renderer", + "mingling_support", + "all_serde_fmt", + "picker", + "parser", + "comp", + ], + }, + }, + }, + }, } diff --git a/CHANGELOG.md b/CHANGELOG.md index ac00643..6c70d3d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -108,6 +108,109 @@ None _No migration is required for existing `parser` users — the old API continues to work unchanged._ +4. **[`core`]** Added multiple `From` implementations for `RenderResult`: + + - **From `()`** — Allows constructing an empty `RenderResult` from a unit value, enabling ergonomic returns like `fn my_renderer() -> RenderResult { }` (via `}` → `}` with implicit `()` return). + - **From integer types** (`i32`, `i16`, `i8`, `u32`, `u16`, `u8`, `usize`) — Allows constructing a `RenderResult` with a specific exit code and empty text, enabling `fn my_renderer() -> RenderResult { 0 }` or `42.into()`. + - **From `String`**, **`&String`**, and **`&str`** — Allows constructing a `RenderResult` with the given text and exit code `0`, enabling `fn my_renderer() -> RenderResult { "Hello".into() }` or passing a `String` directly. + + These implementations make `RenderResult` more flexible as a return type, allowing renderer functions to return simple values without manually constructing a `RenderResult` via `new()` and `write!`/`writeln!`. + +5. **[`macros:renderer`]** Removed the restriction that `#[renderer]` functions must return `RenderResult`. The `#[renderer]` macro now accepts any return type (including no return type), and automatically converts the return value to `RenderResult` via `Into::into`. + + - Functions returning `RenderResult` work as before. + - Functions returning other types (e.g., `String`, `i32`, `()`) are converted via the `Into<RenderResult>` trait. + - Functions with no return type (`-> ()` or omitted) return `()` which is converted to an empty `RenderResult` via `From<()>`. + + This makes `#[renderer]` more flexible and consistent with the ergonomic `From` implementations added in item 4 above. + + ```rust + #[renderer] + fn render_greeting(prev: ResultGreeting) -> String { + format!("Hello, {}!", *prev) + } + + #[renderer] + fn render_exit_code(prev: ResultExit) -> i32 { + 42 + } + + #[renderer] + fn render_void(prev: ResultVoid) { + // side effects only, returns empty RenderResult + } + ``` + +6. **[`macros:help`]** Removed the restriction that `#[help]` functions must return `::mingling::RenderResult`. The `#[help]` macro now accepts any return type (including no return type), and automatically converts the return value to `RenderResult` via `Into::into`. + + - Functions returning `RenderResult` work as before. + - Functions returning other types (e.g., `String`, `i32`, `()`) are converted via `Into<RenderResult>`. + - Functions with no return type (`-> ()` or omitted) return `()` which is converted to an empty `RenderResult` via `From<()>`. + + This makes `#[help]` consistent with the `#[renderer]` macro's ergonomic return type handling introduced in item 5 above. + + ```rust + #[help] + fn help_greeting(prev: EntryGreeting) -> String { + format!("Displaying help for greeting: {}", *prev) + } + + #[help] + fn help_void(prev: EntryVoid) { + // side effects only, returns empty RenderResult + } + ``` + +7. **[`setups`]** Refactored `BasicProgramSetup`, `HelpFlagSetup`, `QuietFlagSetup`, `ConfirmFlagSetup`, `StructuralRendererSetup`, and `StructuralRendererSimpleSetup` into the `picker` subsystem under `mingling::setups::picker`. These setups now use the `arg_picker` (`picker`) chained argument parsing API internally instead of directly manipulating global arguments. + + - The `BasicProgramSetup`, `HelpFlagSetup`, `QuietFlagSetup`, and `ConfirmFlagSetup` structs now use `PickerArg<Flag>` and chained `.pick()` calls to detect flags from the argument list, replacing the previous `global_argument`-based approach. + - The `StructuralRendererSetup` struct now uses `PickerArg<Flag>` constants (e.g., `JSON_FLAG`, `YAML_FLAG`) and chained `.pick()` calls to detect format-specifying flags, replacing the previous `global_argument` approach. + - The `StructuralRendererSimpleSetup` struct still uses the legacy `global_argument("--renderer", ...)` approach, preserving backward compatibility with the `--renderer <FORMAT>` syntax. + - New `PickerArg<Flag>` constants have been added in `mingling::setups::picker::consts`: `HELP_FLAG`, `QUIET_FLAG`, `CONFIRM_FLAG`, `JSON_FLAG`, `JSON_PRETTY_FLAG`, `YAML_FLAG`, `TOML_FLAG`, `RON_FLAG`, and `RON_PRETTY_FLAG`. The format-specific flags are feature-gated behind their respective `json_serde_fmt`, `yaml_serde_fmt`, `toml_serde_fmt`, and `ron_serde_fmt` features. + - The module structure is: + - `mingling::setups::picker` — re-exports all picker-based setup types + - `mingling::setups::picker::basic` — `BasicProgramSetup`, `HelpFlagSetup`, `QuietFlagSetup`, `ConfirmFlagSetup` + - `mingling::setups::picker::consts` — reusable `PickerArg<Flag>` constants + - `mingling::setups::picker::structural_renderer` — `StructuralRendererSetup`, `StructuralRendererSimpleSetup` + - All setup types remain available from `mingling::setups::*` as before — this is purely an internal refactoring; no public API surface changes. + + The `picker` feature must be enabled for these refactored setups to be available. When the feature is disabled, the original implementations (using `global_argument`) remain in effect. + +8. **[`core`]** Added `get_args_mut()`, `take_args()`, and `replace_args()` methods to `Program` for more flexible argument manipulation: + + - **`get_args_mut(&mut self) -> &mut [String]`** — Returns a mutable reference to the program's command-line arguments, allowing in-place modification of individual arguments. + - **`take_args(&mut self) -> Vec<String>`** — Takes ownership of the program's command-line arguments, replacing them with an empty `Vec`. Useful for transferring arguments to another context or processing them with ownership. + - **`replace_args(&mut self, args: Vec<String>) -> Vec<String>`** — Replaces the program's command-line arguments with a new set and returns the old ones. Enables swapping argument sets during program execution. + + These methods complement the existing read-only `get_args(&self)` method, providing full control over argument mutation and ownership. + +9. **[`macros:chain`]** Relaxed the `#[chain]` return type validation. Previously, `#[chain]` functions were restricted to returning `Next`, `ChainProcess<ThisProgram>`, `()`, or omitting the return type. Now, any return type is accepted, and the generated `proc` function performs an explicit `Into<ChainProcess<ThisProgram>>` conversion using a fully-qualified turbofish based on the user-declared return type. + + This means `#[chain]` functions can now return any pack type directly, without needing an explicit `.into()` call in the function body: + + ```rust + // Before — required explicit .into() + #[chain] + fn handle_greet(args: EntryGreet) -> Next { + let name = /* ... */; + ResultGreeting::new(name).into() + } + + // After — return any pack type directly + #[chain] + fn handle_greet(args: EntryGreet) -> ResultGreeting { + let name = /* ... */; + ResultGreeting::new(name) + } + ``` + + The generated `proc` function now wraps the body result in `<UserReturnType as Into<ChainProcess<ThisProgram>>>::into(...)`, which: + - Works for `Next` / `ChainProcess` via the identity `From<T> for T` implementation. + - Works for any pack type (`ResultGreeting`, etc.) via the `.into()` conversion generated by `pack!` / `#[derive(Groupped)]`. + - Works for `()` via the `From<()>` implementation on `ChainProcess`. + + The return type validation has been removed entirely — any valid Rust return type is accepted. If the type does not implement `Into<ChainProcess<ThisProgram>>`, a standard Rust compilation error will be produced at the call site. + #### **BREAKING CHANGES** (API CHANGES): 1. **[`macros:renderer`]** **[`macros:help`]** Removed `r_println!` and `r_print!` macros. The `#[renderer]` and `#[help]` macros no longer implicitly inject an internal `RenderResult` variable or provide `r_println!` / `r_print!` macros. @@ -127,6 +230,39 @@ None All examples, docs, and test cases across the repository have been updated to use the new pattern: creating a `RenderResult` with `RenderResult::new()` or `RenderResult::default()`, writing with `write!`/`writeln!` from `std::io::Write`, and returning the result. +2. **[`macros:chain`]** The `#[chain]` macro's return type requirement has been relaxed. Previously, chain functions were required to return `Next` or `()` (with `()` auto-converting to `ResultEmpty`). Now, chain functions can also return `ChainProcess<ThisProgram>` directly, or omit the return type entirely (which defaults to `()` → `ResultEmpty`). + + The return value of chain functions is now wrapped in an explicit `.into()` call inside the generated `proc` function, ensuring consistent conversion to `ChainProcess<ProgramType>`. As a result, **all downstream code that previously relied on implicit conversion from packed types to `Next`/`ChainProcess` must now call `.into()` explicitly**. + + ```rust + // Before — implicit conversion worked because the generated proc + // function was `fn proc(...) -> impl Into<ChainProcess<...>>` + #[chain] + fn handle_greet(args: EntryGreet) -> Next { + let name = /* ... */; + ResultGreeting::new(name) // implicitly converted + } + + // After — the generated proc function is `fn proc(...) -> ChainProcess<...>`, + // so the body must produce ChainProcess explicitly + #[chain] + fn handle_greet(args: EntryGreet) -> Next { + let name = /* ... */; + ResultGreeting::new(name).into() // explicit conversion required + } + ``` + + The key advantage of this design is that **the original function body and the expanded `proc` function body are now identical** — the macro only adjusts the function signature and inserts an outermost `.into()` wrapper, without rewriting the internal return expressions. This means the semantics of the original code are perfectly preserved: there is no invisible type coercion happening mid-body, and the behavior you write in the source is exactly what executes at runtime. If a bug arises, the expanded code mirrors the source almost one-to-one, making debugging straightforward. + + This change also applies to: + - Chain functions returning `()` (unit), where the body's final expression with `.into()` is replaced by an explicit `ResultEmpty::to_chain()` call. + - Chain functions using `&mut` resource injection with non-unit returns: the inner closure now calls `__modify_res_and_return_route` (which returns `ChainProcess<C>` directly) instead of relying on `.into()` conversion. + - The `__modify_res_and_return_route` method signature changed from accepting `impl Into<ChainProcess<C>>` to returning `ChainProcess<C>` directly. + + All examples, docs, and test cases across the repository have been updated to use `.into()` where packed types are returned from chain functions. + +3. **[`core`]** **[`ExitCodeSetup`]** Updated `ExitCodeSetup` to only override the exit code when `ResExitCode` has been modified (i.e., `exit_code != 0`). Previously, it unconditionally overrode the exit code, which could interfere with exit codes set by other hooks or the program's default exit flow. The `on_finish` hook now returns `ProgramControlUnit::OverrideExitCode(...)` only when the exit code is non-zero, and `ProgramControls::Empty` otherwise. The import of `ProgramControls` has been added accordingly. + --- ## Release 0.2.2 (2026-07-10) @@ -76,7 +76,6 @@ version = "0.1.0" dependencies = [ "arg-picker-macros", "just_fmt 0.2.0", - "mingling_core", ] [[package]] diff --git a/GETTING_STARTED.md b/GETTING_STARTED.md new file mode 100644 index 0000000..dbea3d2 --- /dev/null +++ b/GETTING_STARTED.md @@ -0,0 +1,775 @@ +# Writing with Mingling + +## The Big Picture + +Mingling organizes your CLI program into three distinct phases: + +``` +Input → [Dispatcher] → Entry → [Chain(s)] → Result → [Renderer] → Output +``` + +**Step 1: Input** — The user's raw arguments flow in. +**Step 2: Dispatch** — The **Dispatcher** receives them and wraps them into an **Entry** type. +**Step 3: Chain** — The Entry is handed to a **Chain** function for processing. +**Step 4: Render** — The **Renderer** receives the result and writes it to the terminal. + +> [!NOTE] +> A Chain can produce a **State** type, passed to the next Chain for further processing, +> +> or it can produce a **Result** type, handed to the Renderer for output. + +Every step in this pipeline is a **pure function** annotated with an attribute macro, and the framework recognizes them as long as they meet the requirements. + +--- + +## 1. Defining Commands — `dispatcher!` + +The entry point for every subcommand is the `dispatcher!` macro. It generates two structs for you: a **Dispatcher** (used to register the command with the program) and an **Entry** (a wrapper around `Vec<String>` that holds the raw arguments). + +```rust +use mingling::prelude::*; + +// command.name Dispatcher EntryType +// │ │ │ +dispatcher!("greet", CMDGreet => EntryGreet); + +// Nested subcommand: `remote add` +dispatcher!("remote.add", CMDRemoteAdd => EntryRemoteAdd); +``` + +Then in `main()`, register the dispatcher with the program: + +```rust +dispatcher!("greet", CMDGreet => EntryGreet); + +fn main() { + let mut program = ThisProgram::new(); + program.with_dispatcher(CMDGreet); + program.exec_and_exit(); +} +``` + +Mingling also supports an abbreviated form (with the `extra_macros` feature): + +```rust +// Features: ["extra_macros"] + +// Auto-generates CMDGreet / EntryGreet from "greet" +dispatcher!("greet"); +``` + +--- + +## 2. The Chain — "#[chain]" — Where Logic Lives + +The `#[chain]` attribute turns a plain function into an execution step. Think of it as "the logic that transforms one typed value into another." + +```rust +dispatcher!("greet", CMDGreet => EntryGreet); + +pack!(ResultGreeting = String); + +#[chain] +fn handle_greet(args: EntryGreet) -> Next { + let greeting = args + .inner + .first() + .cloned() + .unwrap_or_else(|| "World".to_string()); + ResultGreeting::new(greeting).into() +} +``` + +Key points: + +- The return type is `Next` — a type alias for `ChainProcess<ThisProgram>`. +- You chain results by calling `.to_chain()` on any `pack!`-ed type. +- You can have **multiple chain functions** for the same command, each transforming the data further. +- With the `async` feature, chain functions can be `async fn`. + +--- + +## 3. The Renderer — "#[renderer]" — How Output Works + +The `#[renderer]` attribute turns a function into an output handler. It receives the final result of a chain and returns a `RenderResult`. + +```rust +use mingling::macros::pack; +use mingling::prelude::*; +use std::io::Write; + +pack!(ResultGreeting = String); + +#[renderer] +fn render_greeting(greeting: ResultGreeting) -> RenderResult { + let mut result = RenderResult::new(); + writeln!(result, "Hello, {}!", *greeting).ok(); + result +} +``` + +Inside a renderer, create a `RenderResult`, write to it using `write!` / `writeln!` (from [`std::io::Write`](https://doc.rust-lang.org/std/io/trait.Write.html)), and return it. The output is captured in the buffer and flushed by the framework at the end of the pipeline. + +You can write renderers for **any type** in your program, including error types: + +```rust +use mingling::prelude::*; +use std::io::Write; + +#[renderer] +fn render_dispatcher_not_found(err: ErrorDispatcherNotFound) -> RenderResult { + let mut result = RenderResult::new(); + writeln!(result, "Command not found: [{}]", err.join(" ")).ok(); + result +} +``` + +--- + +## 4. Parsing Arguments — The Picker + +Mingling provides a **Picker** for zero-cost argument extraction. You use `pick()` or `pick_or()` on an entry to extract typed values, then `unpack()` to get the final tuple. + +```rust +// Features: ["parser"] + +use mingling::parser::Picker; + +dispatcher!("greet", CMDGreet => EntryGreet); +pack!(ResultGreeting = String); + +#[chain] +fn handle_greet(args: EntryGreet) -> Next { + let (name, count) = Picker::new(args.inner) + .pick::<String>(()) // positional: first string + .pick_or::<u8>(["-r", "--repeat"], 1) // optional flag with default + .unpack(); + ResultGreeting::new(format!("{} x{}", name, count)).into() +} +``` + +With the `parser` feature, the `AsPicker` trait provides a shorthand directly on entries: + +```rust +// Features: ["parser"] + +dispatcher!("greet", CMDGreet => EntryGreet); +pack!(ResultGreeting = String); + +#[chain] +fn handle(args: EntryGreet) -> Next { + let (name, count) = args + .pick::<Option<String>>(()) + .pick_or::<u8>(["-r", "--repeat"], 1) + .unpack(); + ResultGreeting::new(format!("{} x{}", name.unwrap_or_default(), count)).into() +} +``` + +For enums, derive `EnumTag` and implement `PickableEnum` to parse enum variants from strings: + +```rust +// Features: ["parser", "extra_macros"] + +use mingling::{EnumTag, Groupped}; +use mingling::parser::PickableEnum; + +dispatcher!("lang.select", CMDLang => EntryLang); + +#[derive(Debug, Default, EnumTag, Groupped)] +pub enum Language { + #[default] + Rust, + #[enum_rename("C++")] + CPlusPlus, +} + +impl PickableEnum for Language {} + +#[chain] +fn handle(args: EntryLang) -> Next { + let lang: Language = args.pick(()).unpack(); + lang.into() +} +``` + +--- + +## 5. The Help System — "#[help]" + +Help is just another attribute macro. When the user passes `--help` or `-h`, the program skips the normal chain/render pipeline and routes directly to your `#[help]` function. + +Enable it by adding `BasicProgramSetup`: + +```rust +use mingling::{macros::help, prelude::*, setup::BasicProgramSetup}; +use std::io::Write; + +dispatcher!("greet", CMDGreet => EntryGreet); + +#[help] +fn help_greet(_prev: EntryGreet) -> RenderResult { + let mut result = RenderResult::new(); + writeln!(result, "Usage: greet <NAME>").ok(); + writeln!(result, "Greets the user with the given name.").ok(); + result +} + +fn main() { + let mut program = ThisProgram::new(); + program.with_setup(BasicProgramSetup); // enables --help / -h + program.with_dispatcher(CMDGreet); + program.exec_and_exit(); +} + +gen_program!(); +``` + +The flow is: + +- User types `greet --help` +- `BasicProgramSetup` sets `program.user_context.help = true` +- The dispatcher sees this flag and routes to the `#[help]` function instead of the `#[chain]` + +--- + +## 6. Completion — "#[completion]" — Dynamic Shell Completions + +With the `comp` feature, Mingling provides a fully dynamic completion system. You write a function that returns `Suggest` based on the current shell context, and Mingling generates the completion scripts for bash, zsh, fish, and pwsh. + +```rust +// Features: ["comp", "extra_macros"] + +use mingling::{macros::suggest, ShellContext, Suggest}; + +dispatcher!("greet", CMDGreet => EntryGreet); +pack!(ResultName = (u8, String)); + +#[completion(EntryGreet)] +fn complete_greet(ctx: &ShellContext) -> Suggest { + // Suggest positional arguments + if ctx.previous_word == "greet" { + return suggest! { + "Alice": "Likes to receive messages", + "Bob": "Likes to pass messages", + "World" + }; + } + + // Suggest flag arguments + if ctx.typing_argument() { + return suggest! { + "-r": "Number of repetitions", + "--repeat": "Number of repetitions", + } + .strip_typed_argument(ctx); + } + + suggest!() // no suggestions +} +``` + +You also need to register the built-in completion dispatcher: + +```rust +// Features: ["comp"] + +fn main() { + let mut program = ThisProgram::new(); + program.with_dispatcher(crate::CMDCompletion); + program.exec_and_exit(); +} +``` + +In your `build.rs`, generate the shell scripts: + +```rust +// BUILD TIME +// Features: ["comp", "builds"] +mingling::build::build_comp_scripts(env!("CARGO_PKG_NAME")).unwrap(); +``` + +For enum-based completions, use `suggest_enum!`: + +```rust +// Features: ["comp", "extra_macros"] + +use mingling::{ShellContext, Suggest}; +use mingling::macros::suggest_enum; +use mingling::EnumTag; + +dispatcher!("lang.select", CMDLang => EntryLang); + +#[derive(EnumTag)] +pub enum ProgrammingLanguages { + Rust, + Python, + JavaScript, +} + +#[completion(EntryLang)] +fn complete_lang(_: &ShellContext) -> Suggest { + suggest_enum!(ProgrammingLanguages) +} +``` + +--- + +## 7. Error Handling + +Mingling doesn't use `?` operator propagation. Instead, errors are just **alternative results** that flow through the same chain/render pipeline. Create error types with `pack!` and route to them with `.to_render()`: + +```rust +use mingling::macros::pack; +use mingling::prelude::*; +use std::io::Write; + +dispatcher!("hello", CMDHello => EntryHello); +pack!(ResultName = String); +pack!(ErrorNoNameProvided = ()); +pack!(ErrorNameTooLong = u16); + +#[chain] +fn handle(args: EntryHello) -> Next { + let Some(name) = args.inner.first().cloned() else { + return ErrorNoNameProvided::default().to_render(); // ← early return to error renderer + }; + + if name.len() > 10 { + return ErrorNameTooLong::new(name.len() as u16).to_render(); + } + + ResultName::new(name).to_render() // ← success path +} + +#[renderer] +fn render_no_name(_: ErrorNoNameProvided) -> RenderResult { + let mut result = RenderResult::new(); + writeln!(result, "No name provided").ok(); + result +} + +#[renderer] +fn render_too_long(len: ErrorNameTooLong) -> RenderResult { + let mut result = RenderResult::new(); + writeln!(result, "Name too long: {} > 10", *len).ok(); + result +} +``` + +Two built-in fallback types are always available: + +- `ErrorDispatcherNotFound` — rendered when no dispatcher matches the input +- `ErrorRendererNotFound` — rendered when no renderer is found for a result type + +--- + +## 8. Resource Injection + +Chain and renderer functions can accept **additional parameters** for the program's global state. Resources are singleton values registered with `program.with_resource(...)`. + +```rust +// Features: ["parser", "extra_macros"] + +use std::path::PathBuf; + +dispatcher!("current", CMDCurrent => EntryCurrent); +dispatcher!("cd", CMDCd => EntryCd); + +#[derive(Default, Clone)] +struct ResCurrentDir { + current_dir: PathBuf, +} + +fn main() { + let mut program = ThisProgram::new(); + program.with_resource(ResCurrentDir { + current_dir: std::env::current_dir().unwrap(), + }); + program.with_dispatcher(CMDCurrent); + program.with_dispatcher(CMDCd); + program.exec_and_exit(); +} + +// Read-only access (shared reference): +#[chain] +fn show_current(_prev: EntryCurrent, current_dir: &ResCurrentDir) -> Next { + println!("Current: {}", current_dir.current_dir.display()); + empty_result!() +} + +// Mutable access: +#[chain] +fn change_dir(prev: EntryCd, current_dir: &mut ResCurrentDir) -> Next { + let path: String = prev.pick(()).unpack(); + current_dir.current_dir = current_dir.current_dir.join(path); + empty_result!() +} +``` + +Resources can also be injected into `#[renderer]`: + +```rust +use mingling::prelude::*; +use std::io::Write; + +dispatcher!("current", CMDCurrent => EntryCurrent); + +#[derive(Default, Clone)] +struct ResCurrentDir { + current_dir: std::path::PathBuf, +} + +#[renderer] +fn render_current(_: EntryCurrent, current_dir: &ResCurrentDir) -> RenderResult { + let mut result = RenderResult::new(); + writeln!(result, "Current directory: {}", current_dir.current_dir.display()).ok(); + result +} +``` + +--- + +## 9. Dispatch Tree — Compile-Time Command Trie + +As your program grows to dozens or hundreds of subcommands, linear dispatcher lookup becomes slow. Enable the `dispatch_tree` feature to convert the command structure into a **prefix tree (Trie)** at compile time. + +```rust +// Features: ["dispatch_tree"] + +dispatcher!("cmd1", CMD1 => Entry1); +dispatcher!("cmd2.sub1", CMD2Sub1 => Entry2Sub1); +dispatcher!("cmd2.sub2", CMD2Sub2 => Entry2Sub2); +dispatcher!("cmd3.sub1.leaf1", CMD3Sub1Leaf1 => Entry3Sub1Leaf1); +dispatcher!("cmd3.sub1.leaf2", CMD3Sub1Leaf2 => Entry3Sub1Leaf2); +// ... dozens more + +fn main() { + let program = ThisProgram::new(); + // No more with_dispatcher calls — it's all compile-time! + program.exec_and_exit(); +} +``` + +With `dispatch_tree` enabled: + +- Dispatchers are auto-collected at compile time +- `Program` no longer stores a dispatcher list +- `program.with_dispatcher(...)` is not compiled +- Lookup is **O(n)** where _n_ is input length, not number of commands + +--- + +## 10. Clap Binding — Using Clap's Parser + +If you prefer clap's powerful argument parsing, use `#[dispatcher_clap]`. It generates a dispatcher from a `clap::Parser` struct. + +```rust +// Features: ["clap"] +// Dependencies: +// clap = "4" + +use mingling::macros::dispatcher_clap; +use mingling::prelude::*; +use std::io::Write; + +#[derive(Default, clap::Parser, Groupped)] +#[dispatcher_clap( + "greet", CMDGreet, + help = true, // auto-generate #[help] from clap + error = ErrorGreetParsed, // capture parse errors as a renderable type +)] +pub struct EntryGreet { + #[clap(default_value = "World")] + name: String, + + #[arg(short, long, default_value_t = 1)] + repeat: i32, +} + +#[renderer] +fn render_greet(greet: EntryGreet) -> RenderResult { + let mut result = RenderResult::new(); + write!(result, "Hello, ").ok(); + for _ in 0..greet.repeat { write!(result, "{}", greet.name).ok(); } + writeln!(result, "!").ok(); + result +} + +#[renderer] +fn render_parse_error(err: ErrorGreetParsed) -> RenderResult { + let mut result = RenderResult::new(); + writeln!(result, "{}", *err).ok(); + result +} +``` + +You can control how clap help is displayed: + +```rust +// Features: ["clap"] + +dispatcher!("greet", CMDGreet => EntryGreet); + +fn main() { + let mut program = ThisProgram::new(); + program.with_dispatcher(CMDGreet); + program.stdout_setting.clap_help_print_behaviour = + mingling::ClapHelpPrintBehaviour::WriteToRenderResult; + // or: PrintDirectly — writes clap help straight to stdout + program.exec_and_exit(); +} +``` + +--- + +## 11. REPL Mode + +With the `repl` feature, turn your CLI into an interactive shell with one method call: + +```rust +// Features: ["repl"] + +fn main() { + ThisProgram::new().exec_repl(); +} +``` + +Mingling provides built-in REPL setups: + +```rust +// Features: ["repl", "extra_macros"] + +use mingling::{ + res::ResREPL, + setup::{BasicREPLReadlineSetup, BasicREPLOutputSetup, BasicREPLPromptSetup}, +}; + +dispatcher!("cd", CMDCd => EntryCd); +dispatcher!("exit", CMDExit => EntryExit); + +fn main() { + let mut program = ThisProgram::new(); + + program.with_dispatcher(CMDCd); + program.with_dispatcher(CMDExit); + + // Enable line reading from stdin + program.with_setup(BasicREPLReadlineSetup); + + // Enable output flushing after each render + program.with_setup(BasicREPLOutputSetup); + + // Custom prompt + program.with_setup(BasicREPLPromptSetup::func(|| "> ".to_string())); + + program.exec_repl(); // ← interactive loop +} + +// Exit the REPL via the ResREPL resource: +#[chain] +fn handle_exit(_prev: EntryExit, repl: &mut ResREPL) { + repl.exit = true; +} +``` + +--- + +## 12. Hooks — Observing the Pipeline + +Mingling provides a `ProgramHook` system for observing every stage of the execution pipeline. Useful for debugging, logging, or telemetry. + +```rust +use mingling::{ + hook::{ProgramControlUnit, ProgramHook}, +}; + +dispatcher!("greet", CMDGreet => EntryGreet); + +fn main() { + let mut program = ThisProgram::new(); + + program.with_hook( + ProgramHook::<ThisProgram>::empty() + .on_begin::<_, ()>(|_| println!("[DEBUG] Program is begin")) + .on_pre_dispatch(|info| println!("[DEBUG] Pre dispatch: {}", info.arguments.join(" "))) + .on_post_dispatch(|info| println!("[DEBUG] Post dispatch: {}", info.entry)) + .on_pre_chain(|info| { + println!("[DEBUG] Pre chain: {}", info.input); + }) + .on_post_chain(|info| println!("[DEBUG] Post chain: {}", info.output.member_id)) + .on_finish(|_| { + println!("[DEBUG] Loop end"); + ProgramControlUnit::OverrideExitCode(0) // Override exit code + }) + .on_pre_render(|info| println!("[DEBUG] Pre render: {}", info.input)) + .on_post_render(|_| println!("[DEBUG] Post render")), + ); + + program.with_dispatcher(CMDGreet); + program.exec_and_exit(); +} +``` + +--- + +## 13. Structural Renderer — Structured Output (JSON/YAML) + +With the `structural_renderer` feature, users can add `--json` or `--yaml` flags to get structured output instead of human-readable text. + +```rust +// Features: ["structural_renderer", "parser"] +// Dependencies: +// serde = "1" + +use mingling::{prelude::*, setup::StructuralRendererSetup}; +use mingling::Groupped; +use mingling::StructuralData; +use serde::Serialize; +use std::io::Write; + +dispatcher!("render", CMDRender => EntryRender); + +#[derive(Default, StructuralData, Serialize, Groupped)] +struct ResultInfo { + name: String, + age: i32, +} + +#[chain] +fn render_info(args: EntryRender) -> Next { + let (name, age) = args.pick::<String>(()).pick::<i32>(()).unpack(); + ResultInfo { name, age }.to_chain() +} + +#[renderer] +fn render_info_result(info: ResultInfo) -> RenderResult { + let mut result = RenderResult::new(); + writeln!(result, "{} is {} years old", info.name, info.age).ok(); + result +} + +fn main() { + let mut program = ThisProgram::new(); + program.with_setup(StructuralRendererSetup); // enables --json / --yaml + program.with_dispatcher(CMDRender); + let _ = program.exec(); +} +``` + +Then users can do: + +```bash +$ myapp render Bob 22 +Bob is 22 years old + +$ myapp render Bob 22 --json +{"name":"Bob","age":22} + +$ myapp render Bob 22 --yaml +name: Bob +age: 22 +``` + +--- + +## 14. Async Support + +Enable the `async` feature to use `async fn` inside `#[chain]`: + +```rust +// Features: ["async", "parser"] +// Dependencies: +// tokio = { version = "1", features = ["full"] } + +use std::io::Write; +use std::time::Duration; + +dispatcher!("download", CMDDownload => EntryDownload); +pack!(ResultDownloaded = String); + +#[chain] +pub async fn handle_download(args: EntryDownload) -> Next { + let file = args.pick(()).unpack(); + download_file(file).await.into() +} + +async fn download_file(name: String) -> ResultDownloaded { + tokio::time::sleep(Duration::from_secs(1)).await; + ResultDownloaded::new(name) +} + +#[renderer] +fn render_downloaded(result: ResultDownloaded) -> RenderResult { + let mut r = RenderResult::new(); + writeln!(r, "\"{}\" downloaded.", *result).ok(); + r +} +``` + +> [!NOTE] +> +> `#[renderer]` functions cannot be async. When `async` is enabled, `program.exec_and_exit().await` returns a Future. + +--- + +## 15. Wrapping Up — `gen_program!()` + +At the very end of your crate root (main.rs / lib.rs), call `gen_program!()` to generate the `ThisProgram` struct, the `Next` type alias, and all internal plumbing. + +```rust +use mingling::macros::gen_program; + +gen_program!(); +``` + +It must be placed **after** all your `dispatcher!`, `pack!`, `#[chain]`, `#[renderer]`, and `#[help]` declarations. + +--- + +## Putting It All Together + +Here's a complete, runnable program: + +```rust +use mingling::macros::pack; +use mingling::prelude::*; +use std::io::Write; + +dispatcher!("greet", CMDGreet => EntryGreet); + +fn main() { + let mut program = ThisProgram::new(); + program.with_dispatcher(CMDGreet); + program.exec_and_exit(); +} + +pack!(ResultGreeting = String); + +#[chain] +fn handle_greet(args: EntryGreet) -> Next { + let greeting = args + .inner + .first() + .cloned() + .unwrap_or_else(|| "World".to_string()); + ResultGreeting::new(greeting).into() +} + +#[renderer] +fn render_greeting(greeting: ResultGreeting) -> RenderResult { + let mut result = RenderResult::new(); + writeln!(result, "Hello, {}!", *greeting).ok(); + result +} + +gen_program!(); +``` + +```bash +$ myapp greet +Hello, World! + +$ myapp greet Alice +Hello, Alice! +``` @@ -1,6 +1,6 @@ <p align="center"> <a href="https://github.com/mingling-rs/mingling"> - <img alt="Mingling" src="https://github.com/mingling-rs/mingling/raw/main/docs/res/icon2.png" width="50%"> + <img alt="Mingling" src="https://github.com/mingling-rs/mingling/raw/main/docs/res/icon2.png" width="30%"> </a> </p> <h1 align="center">Mìng Lìng - 命令</h1> @@ -21,852 +21,100 @@ <img src="https://img.shields.io/github/actions/workflow/status/mingling-rs/mingling/ci.yml"> </p> -> [!WARNING] -> -> **Note**: Mingling is still under active development, and its API may change. Feel free to try it out and give us feedback! -> **Hint**: This note will be removed in version `0.5.0` +## What is Mingling? -<h1 align="center"> - What is Mingling? -</h1> +[`Mingling`](https://github.com/mingling-rs/mingling) is a **state-driven and data-driven** CLI workflow orchestration framework built in Rust. -[`Mingling`](https://github.com/mingling-rs/mingling) is a **proc-macro and type-system based** Rust CLI framework, suitable for developing complex command-line programs with numerous subcommands. +💡 Its name comes from the Chinese pinyin **"Mìng Lìng"**, which means **"command"**. -> Its name comes from the Chinese Pinyin **"Mìng Lìng"**, meaning **"Command"**. +## WARNING -### Mingling's Core Capabilities +Mingling is currently usable at a basic level, but it is still under active development, so many APIs are not yet mature. Any changes to the public API will be documented in detail in the [Changelog](https://github.com/mingling-rs/mingling/blob/main/CHANGELOG.md). -1. **Separation of Concerns, Clear Logic**: Mingling decouples logic by responsibility, helping you organize your CLI program more clearly. - See example: [Example](https://github.com/mingling-rs/mingling/blob/main/examples/example-basic/src/main.rs) -2. **"All Logic is Functions"**: Execution logic, rendering logic, completion logic, help logic — everything is a function. Just attach the corresponding attribute macro to bind them to your program. -3. **Fully Dynamic Completion System**: With the `comp` feature, you can flexibly implement dynamic completion logic for any subcommand. - See examples: [Example](https://github.com/mingling-rs/mingling/blob/main/examples/example-completion/src/main.rs) -4. **Lightning-Fast Subcommand Dispatch**: With the `dispatch_tree` feature, Mingling hardens the subcommand structure into a prefix tree at **compile time**, enabling blazing-fast subcommand lookup. - See examples: [Example](https://github.com/mingling-rs/mingling/blob/main/examples/example-dispatch-tree/src/main.rs) -5. **Lightweight Dependencies, On-Demand Importing**: Minimal core dependencies keep builds fast; enhanced features are imported on demand through fine-grained feature flags. -6. **Structured Output**: Enabling the `structural_renderer` feature adds support for flags like `--json` and `--yaml`, providing structured output capabilities. - See examples: [Example](https://github.com/mingling-rs/mingling/blob/main/examples/example-structural-renderer/src/main.rs) +Additionally, the project is currently developed by me alone ([Weicao-CatilGrass](https://github.com/Weicao-CatilGrass)). If you are interested in this project, I warmly welcome your [contributions](https://github.com/mingling-rs/mingling/blob/main/CONTRIBUTING.md). You can reach me directly via [Github Issue](https://github.com/mingling-rs/mingling/issues). ---- +## About Mingling's Design -**💡 To learn more, check out the following links:** - -- 📖 [Mainpage](https://mingling-rs.github.io/mingling/) -- 📖 [Examples](https://mingling-rs.github.io/mingling/docs/examples.html) -- 📖 [docs.rs](https://docs.rs/mingling/latest/mingling/) -- 📖 [Helpdoc](https://mingling-rs.github.io/mingling/docs/doc.html#/) Or [帮助文档](https://mingling-rs.github.io/mingling/docs/_zh_CN/index.html#/) -- 🔍 [Devdoc](https://mingling-rs.github.io/mingling/docs/dev/) - -<h1 align="center"> - Getting Started -</h1> - -Add Mingling to your `Cargo.toml`: - -```toml -[dependencies.mingling] -version = "0.3.0" -features = [] -``` - -Or use the github version - -```toml -[dependencies.mingling] -git = "https://github.com/mingling-rs/mingling.git" -tag = "unreleased" -features = [] -``` - -Or use the [template project](https://github.com/mingling-rs/mingling-template): - -```bash -cargo generate --git mingling-rs/mingling-template -``` - -<h1 align="center"> - Writing with Mingling -</h1> - -### The Big Picture - -Mingling organizes your CLI program into three distinct phases: - -``` -User Input → [Dispatcher] → Entry → [Chain(s)] → Result → [Renderer] → Output -``` - -**Step1: Input** — The user's raw arguments flow in. -**Step2: Dispatch** — A **Dispatcher** picks them up and wraps them into an **Entry** type. -**Step3: Chain** — The entry is handed off to a **Chain** function, which processes it. -**Step4: Render** — A **Renderer** takes that result and writes it to the terminal. - -> [!NOTE] -> A Chain can produce a **State** type to be passed to the next Chain for further processing, -> -> or it can produce a **Result** type to be handed off to a Renderer. - -Everything in this pipeline is a **plain Rust function** with an attribute macro on top. - -You never need to manually implement traits or construct boilerplate. - ---- - -### 1. Defining Commands — `dispatcher!` - -The entry point for every subcommand is the `dispatcher!` macro. It generates two structs for you: a **Dispatcher** (used to register the command with the program) and an **Entry** (a wrapper around `Vec<String>` that holds the raw arguments). - -```rust -use mingling::prelude::*; - -// "command.name" Dispatcher EntryType -// │ │ │ -dispatcher!("greet", CMDGreet => EntryGreet); - -// Nested subcommand: `remote add` -dispatcher!("remote.add", CMDRemoteAdd => EntryRemoteAdd); -``` - -Then in `main()`, register the dispatcher with the program: - -```rust -dispatcher!("greet", CMDGreet => EntryGreet); - -fn main() { - let mut program = ThisProgram::new(); - program.with_dispatcher(CMDGreet); - program.exec_and_exit(); -} -``` - -Mingling also supports an abbreviated form (with the `extra_macros` feature): - -```rust -// Features: ["extra_macros"] - -// Auto-generates CMDGreet / EntryGreet from "greet" -dispatcher!("greet"); -``` - ---- - -### 2. The Chain — "#[chain]" — Where Logic Lives - -The `#[chain]` attribute turns a plain function into an execution step. Think of it as "the logic that transforms one typed value into another." - -```rust -dispatcher!("greet", CMDGreet => EntryGreet); - -pack!(ResultGreeting = String); - -#[chain] -fn handle_greet(args: EntryGreet) -> Next { - let greeting = args - .inner - .first() - .cloned() - .unwrap_or_else(|| "World".to_string()); - ResultGreeting::new(greeting) -} -``` - -Key points: - -- The return type is `Next` — a type alias for `ChainProcess<ThisProgram>`. -- You chain results by calling `.to_chain()` on any `pack!`-ed type. -- You can have **multiple chain functions** for the same command, each transforming the data further. -- With the `async` feature, chain functions can be `async fn`. - ---- - -### 3. The Renderer — "#[renderer]" — How Output Works - -The `#[renderer]` attribute turns a function into an output handler. It receives the final result of a chain and returns a `RenderResult`. - -```rust -use mingling::macros::pack; -use mingling::prelude::*; -use std::io::Write; - -pack!(ResultGreeting = String); - -#[renderer] -fn render_greeting(greeting: ResultGreeting) -> RenderResult { - let mut result = RenderResult::new(); - writeln!(result, "Hello, {}!", *greeting).ok(); - result -} -``` - -Inside a renderer, create a `RenderResult`, write to it using `write!` / `writeln!` (from [`std::io::Write`](https://doc.rust-lang.org/std/io/trait.Write.html)), and return it. The output is captured in the buffer and flushed by the framework at the end of the pipeline. - -You can write renderers for **any type** in your program, including error types: - -```rust -use mingling::prelude::*; -use std::io::Write; - -#[renderer] -fn render_dispatcher_not_found(err: ErrorDispatcherNotFound) -> RenderResult { - let mut result = RenderResult::new(); - writeln!(result, "Command not found: [{}]", err.join(" ")).ok(); - result -} -``` - ---- - -### 4. Parsing Arguments — The Picker - -Mingling provides a **Picker** for zero-cost argument extraction. You use `pick()` or `pick_or()` on an entry to extract typed values, then `unpack()` to get the final tuple. - -```rust -// Features: ["parser"] - -use mingling::parser::Picker; - -dispatcher!("greet", CMDGreet => EntryGreet); -pack!(ResultGreeting = String); - -#[chain] -fn handle_greet(args: EntryGreet) -> Next { - let (name, count) = Picker::new(args.inner) - .pick::<String>(()) // positional: first string - .pick_or::<u8>(["-r", "--repeat"], 1) // optional flag with default - .unpack(); - ResultGreeting::new(format!("{} x{}", name, count)) -} -``` - -With the `parser` feature, the `AsPicker` trait provides a shorthand directly on entries: - -```rust -// Features: ["parser"] - -dispatcher!("greet", CMDGreet => EntryGreet); -pack!(ResultGreeting = String); - -#[chain] -fn handle(args: EntryGreet) -> Next { - let (name, count) = args - .pick::<Option<String>>(()) - .pick_or::<u8>(["-r", "--repeat"], 1) - .unpack(); - ResultGreeting::new(format!("{} x{}", name.unwrap_or_default(), count)) -} -``` - -For enums, derive `EnumTag` and implement `PickableEnum` to parse enum variants from strings: +Mingling abstracts the behavior of a program's lifecycle into three phases: **Dispatch**, **Execution**, and **Rendering**. Each phase is connected by types — the output of the current phase becomes the input of the next phase. For example: ```rust -// Features: ["parser", "extra_macros"] - -use mingling::{EnumTag, Groupped}; -use mingling::parser::PickableEnum; - -dispatcher!("lang.select", CMDLang => EntryLang); - -#[derive(Debug, Default, EnumTag, Groupped)] -pub enum Language { - #[default] - Rust, - #[enum_rename("C++")] - CPlusPlus, -} - -impl PickableEnum for Language {} - -#[chain] -fn handle(args: EntryLang) -> Next { - let lang: Language = args.pick(()).unpack(); - lang -} -``` - ---- - -### 5. The Help System — "#[help]" - -Help is just another attribute macro. When the user passes `--help` or `-h`, the program skips the normal chain/render pipeline and routes directly to your `#[help]` function. - -Enable it by adding `BasicProgramSetup`: - -```rust -use mingling::{macros::help, prelude::*, setup::BasicProgramSetup}; -use std::io::Write; - -dispatcher!("greet", CMDGreet => EntryGreet); - -#[help] -fn help_greet(_prev: EntryGreet) -> RenderResult { - let mut result = RenderResult::new(); - writeln!(result, "Usage: greet <NAME>").ok(); - writeln!(result, "Greets the user with the given name.").ok(); - result -} - -fn main() { - let mut program = ThisProgram::new(); - program.with_setup(BasicProgramSetup); // enables --help / -h - program.with_dispatcher(CMDGreet); - program.exec_and_exit(); -} - -gen_program!(); -``` - -The flow is: - -- User types `greet --help` -- `BasicProgramSetup` sets `program.user_context.help = true` -- The dispatcher sees this flag and routes to the `#[help]` function instead of the `#[chain]` - ---- - -### 6. Completion — "#[completion]" — Dynamic Shell Completions - -With the `comp` feature, Mingling provides a fully dynamic completion system. You write a function that returns `Suggest` based on the current shell context, and Mingling generates the completion scripts for bash, zsh, fish, and pwsh. - -```rust -// Features: ["comp", "extra_macros"] - -use mingling::{macros::suggest, ShellContext, Suggest}; - -dispatcher!("greet", CMDGreet => EntryGreet); -pack!(ResultName = (u8, String)); - -#[completion(EntryGreet)] -fn complete_greet(ctx: &ShellContext) -> Suggest { - // Suggest positional arguments - if ctx.previous_word == "greet" { - return suggest! { - "Alice": "Likes to receive messages", - "Bob": "Likes to pass messages", - "World" - }; - } - - // Suggest flag arguments - if ctx.typing_argument() { - return suggest! { - "-r": "Number of repetitions", - "--repeat": "Number of repetitions", - } - .strip_typed_argument(ctx); - } - - suggest!() // no suggestions -} -``` - -You also need to register the built-in completion dispatcher: - -```rust -// Features: ["comp"] - -fn main() { - let mut program = ThisProgram::new(); - program.with_dispatcher(crate::CMDCompletion); - program.exec_and_exit(); -} -``` - -In your `build.rs`, generate the shell scripts: - -```rust -// BUILD TIME -// Features: ["comp", "builds"] -mingling::build::build_comp_scripts(env!("CARGO_PKG_NAME")).unwrap(); -``` - -For enum-based completions, use `suggest_enum!`: - -```rust -// Features: ["comp", "extra_macros"] - -use mingling::{ShellContext, Suggest}; -use mingling::macros::suggest_enum; -use mingling::EnumTag; - -dispatcher!("lang.select", CMDLang => EntryLang); - -#[derive(EnumTag)] -pub enum ProgrammingLanguages { - Rust, - Python, - JavaScript, -} - -#[completion(EntryLang)] -fn complete_lang(_: &ShellContext) -> Suggest { - suggest_enum!(ProgrammingLanguages) -} -``` - ---- - -### 7. Error Handling - -Mingling doesn't use `?` operator propagation. Instead, errors are just **alternative results** that flow through the same chain/render pipeline. Create error types with `pack!` and route to them with `.to_render()`: - -```rust -use mingling::macros::pack; -use mingling::prelude::*; -use std::io::Write; - -dispatcher!("hello", CMDHello => EntryHello); -pack!(ResultName = String); -pack!(ErrorNoNameProvided = ()); -pack!(ErrorNameTooLong = u16); - -#[chain] -fn handle(args: EntryHello) -> Next { - let Some(name) = args.inner.first().cloned() else { - return ErrorNoNameProvided::default().to_render(); // ← early return to error renderer - }; - - if name.len() > 10 { - return ErrorNameTooLong::new(name.len() as u16).to_render(); - } - - ResultName::new(name).to_render() // ← success path -} - -#[renderer] -fn render_no_name(_: ErrorNoNameProvided) -> RenderResult { - let mut result = RenderResult::new(); - writeln!(result, "No name provided").ok(); - result -} - -#[renderer] -fn render_too_long(len: ErrorNameTooLong) -> RenderResult { - let mut result = RenderResult::new(); - writeln!(result, "Name too long: {} > 10", *len).ok(); - result -} -``` - -Two built-in fallback types are always available: - -- `ErrorDispatcherNotFound` — rendered when no dispatcher matches the input -- `ErrorRendererNotFound` — rendered when no renderer is found for a result type - ---- - -### 8. Resource Injection - -Chain and renderer functions can accept **additional parameters** for the program's global state. Resources are singleton values registered with `program.with_resource(...)`. - -```rust -// Features: ["parser", "extra_macros"] - -use std::path::PathBuf; - dispatcher!("current", CMDCurrent => EntryCurrent); -dispatcher!("cd", CMDCd => EntryCd); +pack!(StateNext = ()); -#[derive(Default, Clone)] -struct ResCurrentDir { - current_dir: PathBuf, -} - -fn main() { - let mut program = ThisProgram::new(); - program.with_resource(ResCurrentDir { - current_dir: std::env::current_dir().unwrap(), - }); - program.with_dispatcher(CMDCurrent); - program.with_dispatcher(CMDCd); - program.exec_and_exit(); -} - -// Read-only access (shared reference): #[chain] -fn show_current(_prev: EntryCurrent, current_dir: &ResCurrentDir) -> Next { - println!("Current: {}", current_dir.current_dir.display()); - empty_result!() -} - -// Mutable access: -#[chain] -fn change_dir(prev: EntryCd, current_dir: &mut ResCurrentDir) -> Next { - let path: String = prev.pick(()).unpack(); - current_dir.current_dir = current_dir.current_dir.join(path); - empty_result!() +fn handle_current(_: EntryCurrent) -> StateNext { + // 1. The first phase outputs the StateNext value + StateNext::default() // ^^^^^^^^^ +} // | + // | + // 2. The second phase takes StateNext as input +#[chain] // | +fn handle_state_next(_: StateNext) { + todo!() } ``` -Resources can also be injected into `#[renderer]`: +See? `handle_current` and `handle_state_next` have no direct connection! -```rust -use mingling::prelude::*; -use std::io::Write; - -dispatcher!("current", CMDCurrent => EntryCurrent); - -#[derive(Default, Clone)] -struct ResCurrentDir { - current_dir: std::path::PathBuf, -} - -#[renderer] -fn render_current(_: EntryCurrent, current_dir: &ResCurrentDir) -> RenderResult { - let mut result = RenderResult::new(); - writeln!(result, "Current directory: {}", current_dir.current_dir.display()).ok(); - result -} -``` - ---- - -### 9. Dispatch Tree — Compile-Time Command Trie - -As your program grows to dozens or hundreds of subcommands, linear dispatcher lookup becomes slow. Enable the `dispatch_tree` feature to convert the command structure into a **prefix tree (Trie)** at compile time. - -```rust -// Features: ["dispatch_tree"] - -dispatcher!("cmd1", CMD1 => Entry1); -dispatcher!("cmd2.sub1", CMD2Sub1 => Entry2Sub1); -dispatcher!("cmd2.sub2", CMD2Sub2 => Entry2Sub2); -dispatcher!("cmd3.sub1.leaf1", CMD3Sub1Leaf1 => Entry3Sub1Leaf1); -dispatcher!("cmd3.sub1.leaf2", CMD3Sub1Leaf2 => Entry3Sub1Leaf2); -// ... dozens more - -fn main() { - let program = ThisProgram::new(); - // No more with_dispatcher calls — it's all compile-time! - program.exec_and_exit(); -} -``` - -With `dispatch_tree` enabled: - -- Dispatchers are auto-collected at compile time -- `Program` no longer stores a dispatcher list -- `program.with_dispatcher(...)` is not compiled -- Lookup is **O(n)** where _n_ is input length, not number of commands - ---- - -### 10. Clap Binding — Using Clap's Parser - -If you prefer clap's powerful argument parsing, use `#[dispatcher_clap]`. It generates a dispatcher from a `clap::Parser` struct. - -```rust -// Features: ["clap"] -// Dependencies: -// clap = "4" - -use mingling::macros::dispatcher_clap; -use mingling::prelude::*; -use std::io::Write; - -#[derive(Default, clap::Parser, Groupped)] -#[dispatcher_clap( - "greet", CMDGreet, - help = true, // auto-generate #[help] from clap - error = ErrorGreetParsed, // capture parse errors as a renderable type -)] -pub struct EntryGreet { - #[clap(default_value = "World")] - name: String, - - #[arg(short, long, default_value_t = 1)] - repeat: i32, -} - -#[renderer] -fn render_greet(greet: EntryGreet) -> RenderResult { - let mut result = RenderResult::new(); - write!(result, "Hello, ").ok(); - for _ in 0..greet.repeat { write!(result, "{}", greet.name).ok(); } - writeln!(result, "!").ok(); - result -} - -#[renderer] -fn render_parse_error(err: ErrorGreetParsed) -> RenderResult { - let mut result = RenderResult::new(); - writeln!(result, "{}", *err).ok(); - result -} -``` - -You can control how clap help is displayed: - -```rust -// Features: ["clap"] - -dispatcher!("greet", CMDGreet => EntryGreet); - -fn main() { - let mut program = ThisProgram::new(); - program.with_dispatcher(CMDGreet); - program.stdout_setting.clap_help_print_behaviour = - mingling::ClapHelpPrintBehaviour::WriteToRenderResult; - // or: PrintDirectly — writes clap help straight to stdout - program.exec_and_exit(); -} -``` - ---- - -### 11. REPL Mode - -With the `repl` feature, turn your CLI into an interactive shell with one method call: - -```rust -// Features: ["repl"] - -fn main() { - ThisProgram::new().exec_repl(); -} -``` +They are bridged by `StateNext` and automatically linked by the framework. -Mingling provides built-in REPL setups: +You can use this approach to separate computation from result rendering, like this: ```rust -// Features: ["repl", "extra_macros"] +// Features: ["picker"] +dispatcher!("calc", CMDCalculate => EntryCalculate); +pack!(StateSumNumbers = Vec<i32>); +pack!(ResultNumber = i32); -use mingling::{ - res::ResREPL, - setup::{BasicREPLReadlineSetup, BasicREPLOutputSetup, BasicREPLPromptSetup}, -}; - -dispatcher!("cd", CMDCd => EntryCd); -dispatcher!("exit", CMDExit => EntryExit); - -fn main() { - let mut program = ThisProgram::new(); - - program.with_dispatcher(CMDCd); - program.with_dispatcher(CMDExit); - - // Enable line reading from stdin - program.with_setup(BasicREPLReadlineSetup); - - // Enable output flushing after each render - program.with_setup(BasicREPLOutputSetup); - - // Custom prompt - program.with_setup(BasicREPLPromptSetup::func(|| "> ".to_string())); - - program.exec_repl(); // ← interactive loop -} - -// Exit the REPL via the ResREPL resource: +// Entry: parse arguments and pass state to the calculation step #[chain] -fn handle_exit(_prev: EntryExit, repl: &mut ResREPL) { - repl.exit = true; -} -``` - ---- - -### 12. Hooks — Observing the Pipeline - -Mingling provides a `ProgramHook` system for observing every stage of the execution pipeline. Useful for debugging, logging, or telemetry. - -```rust -use mingling::{ - hook::{ProgramControlUnit, ProgramHook}, -}; - -dispatcher!("greet", CMDGreet => EntryGreet); - -fn main() { - let mut program = ThisProgram::new(); - - program.with_hook( - ProgramHook::<ThisProgram>::empty() - .on_begin::<_, ()>(|_| println!("[DEBUG] Program is begin")) - .on_pre_dispatch(|info| println!("[DEBUG] Pre dispatch: {}", info.arguments.join(" "))) - .on_post_dispatch(|info| println!("[DEBUG] Post dispatch: {}", info.entry)) - .on_pre_chain(|info| { - println!("[DEBUG] Pre chain: {}", info.input); - }) - .on_post_chain(|info| println!("[DEBUG] Post chain: {}", info.output.member_id)) - .on_finish(|_| { - println!("[DEBUG] Loop end"); - ProgramControlUnit::OverrideExitCode(0) // Override exit code - }) - .on_pre_render(|info| println!("[DEBUG] Pre render: {}", info.input)) - .on_post_render(|_| println!("[DEBUG] Post render")), - ); - - program.with_dispatcher(CMDGreet); - program.exec_and_exit(); -} -``` - ---- - -### 13. Structural Renderer — Structured Output (JSON/YAML) - -With the `structural_renderer` feature, users can add `--json` or `--yaml` flags to get structured output instead of human-readable text. - -```rust -// Features: ["structural_renderer", "parser"] -// Dependencies: -// serde = "1" - -use mingling::{prelude::*, setup::StructuralRendererSetup}; -use mingling::Groupped; -use mingling::StructuralData; -use serde::Serialize; -use std::io::Write; - -dispatcher!("render", CMDRender => EntryRender); - -#[derive(Default, StructuralData, Serialize, Groupped)] -struct ResultInfo { - name: String, - age: i32, +fn handle_calc(args: EntryCalculate) -> StateSumNumbers { + let numbers = args.pick(&arg![Vec<i32>]).unwrap(); + StateSumNumbers::new(numbers) } +// Calculate: pass the result to the rendering step #[chain] -fn render_info(args: EntryRender) -> Next { - let (name, age) = args.pick::<String>(()).pick::<i32>(()).unpack(); - ResultInfo { name, age }.to_chain() +fn handle_state_sum_numbers(sum: StateSumNumbers) -> ResultNumber { + let numbers = sum.inner; + let total: i32 = numbers.iter().sum(); + ResultNumber::new(total) } +// Renderer: return the render result and let the framework handle output #[renderer] -fn render_info_result(info: ResultInfo) -> RenderResult { +fn render_number(number: ResultNumber) -> RenderResult { let mut result = RenderResult::new(); - writeln!(result, "{} is {} years old", info.name, info.age).ok(); + writeln!(result, "Number: {}", *number).ok(); result } - -fn main() { - let mut program = ThisProgram::new(); - program.with_setup(StructuralRendererSetup); // enables --json / --yaml - program.with_dispatcher(CMDRender); - let _ = program.exec(); -} ``` -Then users can do: +Although this may make your program slightly more verbose, each step is a **pure function**, making it extremely easy to test! -```bash -$ myapp render Bob 22 -Bob is 22 years old +## Getting Started -$ myapp render Bob 22 --json -{"name":"Bob","age":22} +Add Mingling to your `Cargo.toml`: -$ myapp render Bob 22 --yaml -name: Bob -age: 22 +```toml +[dependencies.mingling] +version = "0.3.0" +features = [] ``` ---- - -### 14. Async Support - -Enable the `async` feature to use `async fn` inside `#[chain]`: - -```rust -// Features: ["async", "parser"] -// Dependencies: -// tokio = { version = "1", features = ["full"] } - -use std::io::Write; -use std::time::Duration; - -dispatcher!("download", CMDDownload => EntryDownload); -pack!(ResultDownloaded = String); - -#[chain] -pub async fn handle_download(args: EntryDownload) -> Next { - let file = args.pick(()).unpack(); - download_file(file).await -} - -async fn download_file(name: String) -> ResultDownloaded { - tokio::time::sleep(Duration::from_secs(1)).await; - ResultDownloaded::new(name) -} +Or use the github version -#[renderer] -fn render_downloaded(result: ResultDownloaded) -> RenderResult { - let mut r = RenderResult::new(); - writeln!(r, "\"{}\" downloaded.", *result).ok(); - r -} +```toml +[dependencies.mingling] +git = "https://github.com/mingling-rs/mingling.git" +tag = "unreleased" +features = [] ``` > [!NOTE] -> -> `#[renderer]` functions cannot be async. When `async` is enabled, `program.exec_and_exit().await` returns a Future. - ---- - -### 15. Wrapping Up — `gen_program!()` - -At the very end of your crate root (main.rs / lib.rs), call `gen_program!()` to generate the `ThisProgram` struct, the `Next` type alias, and all internal plumbing. - -```rust -use mingling::macros::gen_program; - -gen_program!(); -``` - -It must be placed **after** all your `dispatcher!`, `pack!`, `#[chain]`, `#[renderer]`, and `#[help]` declarations. - ---- - -### Putting It All Together - -Here's a complete, runnable program: - -```rust -use mingling::macros::pack; -use mingling::prelude::*; -use std::io::Write; +> To learn more, check out [Writing with Mingling](https://github.com/mingling-rs/mingling/blob/main/GETTING_STARTED.md) -dispatcher!("greet", CMDGreet => EntryGreet); - -fn main() { - let mut program = ThisProgram::new(); - program.with_dispatcher(CMDGreet); - program.exec_and_exit(); -} - -pack!(ResultGreeting = String); - -#[chain] -fn handle_greet(args: EntryGreet) -> Next { - let greeting = args - .inner - .first() - .cloned() - .unwrap_or_else(|| "World".to_string()); - ResultGreeting::new(greeting) -} - -#[renderer] -fn render_greeting(greeting: ResultGreeting) -> RenderResult { - let mut result = RenderResult::new(); - writeln!(result, "Hello, {}!", *greeting).ok(); - result -} - -gen_program!(); -``` - -```bash -$ myapp greet -Hello, World! - -$ myapp greet Alice -Hello, Alice! -``` - -<h1 align="center"> - 🗺️ Roadmap 🗺️ -</h1> +## Roadmap - [x] Milestone.1 "MVP" 🎉 - [x] [[0.1.4](https://docs.rs/mingling/0.1.4/mingling/)] [`core`] [`structural_renderer`] **Mingling** can render data into serializable formats via `--json` and `--yaml` flags @@ -880,19 +128,17 @@ Hello, Alice! - [x] [[0.1.9](https://docs.rs/mingling/0.1.9/mingling/)] [`core`] [`repl`] Provides REPL capability (`program.exec_repl();`) - [x] [[0.2.0](https://docs.rs/mingling/0.2.0/mingling/)] Complete documentation, tests, and examples - [ ] Milestone.2 "More Comfortable Dev and User Experience" - - [ ] [`mling` / `mingling-cli`] - - [ ] **Mingling** Linter - - [ ] **Mingling** Project Generator - - [ ] **Mingling** Program Installer & Manager (For development) - - [ ] Helpdoc Editor - - [ ] [`picker`] A more efficient and intelligent argument parser - - [x] [`macros`] Remove `r_print!` / `r_println!` macros + - [ ] [`mling` / `mingling-cli`] + - [ ] **Mingling** Linter + - [ ] **Mingling** Project Generator + - [ ] **Mingling** Program Installer & Manager (For development) + - [ ] Helpdoc Editor + - [ ] [`picker`] A more efficient and intelligent argument parser + - [x] [`macros`] Remove `r_print!` / `r_println!` macros - [ ] Milestone.3 "Unplanned" - - [ ] ... + - [ ] ... -<h1 align="center"> - 🚫 Unplanned Features 🚫 -</h1> +## Unplanned Features While Mingling has several common CLI features that are **NOT PLANNED** to be directly included in the framework. This is because the Rust ecosystem already has excellent and mature crates to handle these issues, and Mingling's design is intended to be used in combination with them. @@ -902,10 +148,19 @@ This is because the Rust ecosystem already has excellent and mature crates to ha - **Progress Bars**: To display progress indicators, the [`indicatif`](https://crates.io/crates/indicatif) crate is the standard choice. - **TUI**: To build full-screen interactive terminal applications, it is recommended to use a framework like [`ratatui`](https://crates.io/crates/ratatui) (formerly `tui-rs`). -<h1 align="center"> - 📄 Open Source License 📄 -</h1> +## License This project is licensed under the MIT License. See [LICENSE-MIT](LICENSE-MIT) or [LICENSE-APACHE](LICENSE-APACHE) file for details. + +## Learn More + +**To learn more, check out the following links:** + +- 📦 Repo - [Github](https://github.com/mingling-rs/mingling) | [Gitee](https://gitee.com/mingling-rs/mingling) | [Origin](https://catilgrass.cn/mingling.git) +- 🚪 Mainpage - [Github](https://mingling-rs.github.io/mingling/) | [crates.io](https://crates.io/crates/mingling) +- 💡 Examples - [Github](https://mingling-rs.github.io/mingling/docs/examples.html) +- 📖 Help Doc - [EN](https://mingling-rs.github.io/mingling/docs/doc.html#/) | [中文](https://mingling-rs.github.io/mingling/docs/_zh_CN/index.html#/) +- 📖 API Doc - [docs.rs](https://docs.rs/mingling/latest/mingling/) | [latest](https://mingling-rs.github.io/mingling/docs/api-docs/mingling/) +- 📖 Dev Doc - [Github](https://mingling-rs.github.io/mingling/docs/dev/) diff --git a/arg_picker/Cargo.toml b/arg_picker/Cargo.toml index f7672c1..d87a844 100644 --- a/arg_picker/Cargo.toml +++ b/arg_picker/Cargo.toml @@ -9,9 +9,8 @@ readme = "README.md" description = "A lightweight, type-safe CLI argument parser" [features] -mingling_support = ["dep:mingling_core", "arg-picker-macros/mingling_support"] +mingling_support = ["arg-picker-macros/mingling_support"] [dependencies] -mingling_core = { workspace = true, optional = true } arg-picker-macros.workspace = true just_fmt.workspace = true diff --git a/arg_picker/src/arg.rs b/arg_picker/src/arg.rs index 78ad539..a352418 100644 --- a/arg_picker/src/arg.rs +++ b/arg_picker/src/arg.rs @@ -138,9 +138,27 @@ where /// Describes the attribute (behavior) of a command-line parameter. /// /// The ordering reflects parse priority (higher = parsed first): -/// `PositionalMulti < Positional < Flag < Single < Multi` +/// `Postprocess < Final < PositionalMulti < Positional < Flag < Single < Multi < Begin < Preprocess` +/// +/// # Variants +/// +/// - `Postprocess` — Reserved lowest priority, used only in special cases. +/// - `Final` — Reserved post-processing priority, used only in special cases. +/// - `PositionalMulti` — Positional argument that accepts multiple values (e.g., multiple input files). +/// - `Positional` — Positional argument matched by its position (e.g., an input file). +/// - `Flag` — Boolean flag with no associated value (e.g., `--verbose`). +/// - `Single` — Accepts a single value (e.g., `--name Alice`). +/// - `Multi` — Accepts multiple values (e.g., `--file a.txt --file b.txt`). +/// - `Begin` — Reserved pre-processing priority, used only in special cases. +/// - `Preprocess` — Reserved highest priority, used only in special cases. #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] pub enum PickerArgAttr { + /// Reserved lowest priority, used only in special cases. + Postprocess, + + /// Reserved post-processing priority, used only in special cases. + Final, + /// Positional argument that accepts multiple values (e.g., multiple input files). PositionalMulti, @@ -156,6 +174,12 @@ pub enum PickerArgAttr { /// Accepts multiple values (e.g., `--file a.txt --file b.txt`). Multi, + + /// Reserved pre-processing priority, used only in special cases. + Begin, + + /// Reserved highest priority, used only in special cases. + Preprocess, } impl PickerArgAttr { diff --git a/arg_picker/src/builtin.rs b/arg_picker/src/builtin.rs index e855b08..1c698ba 100644 --- a/arg_picker/src/builtin.rs +++ b/arg_picker/src/builtin.rs @@ -1,4 +1,5 @@ mod pick_bool; mod pick_flag; mod pick_numbers; +mod pick_picker_args; mod pick_string; diff --git a/arg_picker/src/builtin/pick_picker_args.rs b/arg_picker/src/builtin/pick_picker_args.rs new file mode 100644 index 0000000..419cbc8 --- /dev/null +++ b/arg_picker/src/builtin/pick_picker_args.rs @@ -0,0 +1,21 @@ +use crate::{PickerArgResult::Parsed, PickerArgs, parselib::build_masked_args, pickable_needed::*}; + +impl<'a> Pickable<'a> for PickerArgs<'a> { + fn get_attr(_flag: &'a PickerArg<'a, Self>) -> PickerArgAttr { + // Use the lowest priority attribute + PickerArgAttr::Postprocess + } + + fn tag(ctx: TagPhaseContext) -> Vec<usize> { + // Collect all remaining raw index values + build_masked_args(ctx.args, ctx.mask) + .iter() + .map(|m| m.raw_idx) + .collect() + } + + fn pick(raw_strs: &[&str]) -> PickerArgResult<Self> { + let remains: Vec<String> = raw_strs.iter().map(|s| s.to_string()).collect(); + Parsed(PickerArgs::Owned(remains)) + } +} diff --git a/arg_picker/src/lib.rs b/arg_picker/src/lib.rs index 21a0d35..c65e793 100644 --- a/arg_picker/src/lib.rs +++ b/arg_picker/src/lib.rs @@ -29,13 +29,8 @@ pub mod value; /// use arg_picker::prelude::*; /// ``` pub mod prelude { - pub use crate::macros::arg; - - #[cfg(not(feature = "mingling_support"))] pub use crate::IntoPicker; - - #[cfg(feature = "mingling_support")] - pub use crate::corebind::EntryPicker; + pub use crate::macros::arg; } /// Re-export of the `arg_picker_macros` crate @@ -53,10 +48,3 @@ pub mod matcher_needed { pub use crate::PickerArgInfo; pub use crate::parselib::{MaskedArg, Matcher, ParserStyle}; } - -#[cfg(feature = "mingling_support")] -mod corebind; - -#[allow(unused_imports)] -#[cfg(feature = "mingling_support")] -pub use corebind::*; diff --git a/arg_picker/src/parselib.rs b/arg_picker/src/parselib.rs index 7fbd606..0fcd583 100644 --- a/arg_picker/src/parselib.rs +++ b/arg_picker/src/parselib.rs @@ -117,13 +117,32 @@ impl<'a> From<crate::TagPhaseContext<'a>> for MatcherContext<'a> { } } +/// Checks whether the argument at index `idx` is already claimed (masked). +/// +/// Returns `true` if `idx` is within the mask bounds and the mask value is non-zero, +/// indicating the argument has been claimed by a previous matcher. +/// +/// # Arguments +/// +/// * `mask` - A byte slice where non-zero values indicate claimed arguments. +/// * `idx` - The index to check in the mask. #[inline(always)] -fn is_masked(mask: &[u8], idx: usize) -> bool { +pub fn is_masked(mask: &[u8], idx: usize) -> bool { idx < mask.len() && mask[idx] != 0 } +/// Builds a vector of [`MaskedArg`] from the given `PickerArgs` and mask. +/// +/// Only arguments whose mask entry is `0` (i.e., available/not yet claimed) are included. +/// Each resulting [`MaskedArg`] retains its original raw string and its index in the full +/// argument list for later reference. +/// +/// # Arguments +/// +/// * `args` - The full set of parsed arguments. +/// * `mask` - A byte slice where `0` means available and non-zero means already claimed. #[inline(always)] -fn build_masked_args<'a>(args: &'a PickerArgs, mask: &'a [u8]) -> Vec<MaskedArg<'a>> { +pub fn build_masked_args<'a>(args: &'a PickerArgs, mask: &'a [u8]) -> Vec<MaskedArg<'a>> { let mut cidx = 0; args.iter() .filter_map(|r| { diff --git a/arg_picker/src/picker.rs b/arg_picker/src/picker.rs index ecab648..f31a5b6 100644 --- a/arg_picker/src/picker.rs +++ b/arg_picker/src/picker.rs @@ -73,6 +73,26 @@ pub enum PickerArgs<'a> { Owned(Vec<String>), } +impl<'a> From<PickerArgs<'a>> for Vec<String> { + fn from(value: PickerArgs<'a>) -> Self { + match value { + PickerArgs::Slice(items) => items.iter().map(|s| s.to_string()).collect(), + PickerArgs::Vec(items) => items.into_iter().map(|s| s.to_string()).collect(), + PickerArgs::Owned(items) => items, + } + } +} + +impl<'a> From<&'a PickerArgs<'a>> for Vec<&'a str> { + fn from(value: &'a PickerArgs<'a>) -> Self { + match value { + PickerArgs::Slice(items) => items.to_vec(), + PickerArgs::Vec(items) => items.clone(), + PickerArgs::Owned(items) => items.iter().map(|s| s.as_str()).collect(), + } + } +} + impl<'a> Default for PickerArgs<'a> { fn default() -> Self { Self::Vec(vec![]) @@ -138,6 +158,15 @@ impl<'a> IntoIterator for &'a PickerArgs<'a> { } } +impl<'a, Route> From<PickerArgs<'a>> for Picker<'a, Route> { + fn from(args: PickerArgs<'a>) -> Self { + Picker { + route_phantom: PhantomData, + args, + } + } +} + impl<'a, Route> From<&'a [&'a str]> for Picker<'a, Route> { fn from(value: &'a [&'a str]) -> Self { Picker { @@ -408,10 +437,9 @@ impl<'a> IntoPicker<'a> for Vec<String> { } } -// Private helper: shared construction logic for `PickerPattern1`. -// Both `Picker::pick` and `IntoPicker::pick` delegate to this. impl<'a, Route> Picker<'a, Route> { - pub(crate) fn build_pattern1<N>( + /// Build the PickerPattern via Arguments + pub fn build_pattern1<N>( args: PickerArgs<'a>, arg: &'a PickerArg<'a, N>, error_route: Option<Route>, diff --git a/arg_picker/src/picker/parse.rs b/arg_picker/src/picker/parse.rs index 366028b..9db5bd9 100644 --- a/arg_picker/src/picker/parse.rs +++ b/arg_picker/src/picker/parse.rs @@ -21,14 +21,16 @@ internal_repeat!(1..=32 => { 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. + /// Unwraps the result, panicking if a route was selected or a required + /// value is missing. /// /// # Panics /// - /// Panics if a route was selected. + /// Panics if a route was selected, or if a required argument was not + /// provided by the user. pub fn unwrap(self) -> ((T$,+)) { let p = self.parse(); - ((p.v$.unwrap(),+)) + ((p.v$.expect(concat!("missing required argument at position ", $)),+)) } /// Returns the individual option values without checking the route. diff --git a/arg_picker/src/picker/result.rs b/arg_picker/src/picker/result.rs index 83ca2cd..2099825 100644 --- a/arg_picker/src/picker/result.rs +++ b/arg_picker/src/picker/result.rs @@ -21,13 +21,15 @@ internal_repeat!(1..=32 => { internal_repeat!(1..=32 => { impl<(T$,+), Route> PickerResult$<(T$,+), Route> { - /// Unwraps the result, panicking if a route was selected. + /// Unwraps the result, panicking if a route was selected or a required + /// value is missing. /// /// # Panics /// - /// Panics if `self.route` is `Some(...)`. + /// Panics if `self.route` is `Some(...)`, or if a required argument was + /// not provided by the user. pub fn unwrap(self) -> ((T$,+)) { - ((self.v$.unwrap(),+)) + ((self.v$.expect(concat!("missing required argument at position ", $)),+)) } /// Returns the individual option values without checking the route. diff --git a/arg_picker/test/src/test/route_test.rs b/arg_picker/test/src/test/route_test.rs index 7594c6e..c9cd5ab 100644 --- a/arg_picker/test/src/test/route_test.rs +++ b/arg_picker/test/src/test/route_test.rs @@ -1,4 +1,4 @@ -use arg_picker::{macros::arg, IntoPicker}; +use arg_picker::{IntoPicker, macros::arg}; // Route mechanism — or_route @@ -53,7 +53,7 @@ fn test_or_route_to_option_returns_none() { } #[test] -#[should_panic(expected = "called `Option::unwrap()` on a `None` value")] +#[should_panic(expected = "missing required argument")] fn test_or_route_unwrap_panics() { let args: Vec<&str> = vec![]; args.with_route::<&'static str>() diff --git a/docs/_zh_CN/pages/11-resource-system.md b/docs/_zh_CN/pages/11-resource-system.md index 152e5d6..a8aab4d 100644 --- a/docs/_zh_CN/pages/11-resource-system.md +++ b/docs/_zh_CN/pages/11-resource-system.md @@ -57,7 +57,7 @@ fn render_path(result: ResultPath) -> RenderResult { #[chain] fn handle_visit(_args: EntryVisit, counter: &mut ResVisitCount) -> Next { counter.0 += 1; - ResultDone::default() + ResultDone::default().into() } #[renderer] diff --git a/docs/_zh_CN/pages/3-define-a-chain.md b/docs/_zh_CN/pages/3-define-a-chain.md index 7ac5c60..b2850c5 100644 --- a/docs/_zh_CN/pages/3-define-a-chain.md +++ b/docs/_zh_CN/pages/3-define-a-chain.md @@ -24,7 +24,7 @@ fn handle_greet(args: EntryGreet) -> Next { // args 就是用户输入经过匹配后剩下的参数 let name = args.inner.first().cloned().unwrap_or_else(|| "World".to_string()); // 把结果包装成 Next,告诉调度器下一步去哪 - ResultName::new(name) + ResultName::new(name).into() } ``` @@ -88,7 +88,7 @@ fn handle_greet(args: EntryGreet) -> Next { .cloned() .unwrap_or_else(|| "World".to_string()); - ResultName::new(name) + ResultName::new(name).into() } ``` @@ -112,7 +112,7 @@ fn handle_greet(args: EntryGreet) -> Next { .first() .cloned() .unwrap_or_else(|| "World".to_string()); - ResultName::new(name) + ResultName::new(name).into() } fn main() { diff --git a/docs/_zh_CN/pages/4-render-result.md b/docs/_zh_CN/pages/4-render-result.md index 7cf2d2b..81c5102 100644 --- a/docs/_zh_CN/pages/4-render-result.md +++ b/docs/_zh_CN/pages/4-render-result.md @@ -51,7 +51,7 @@ fn handle_greet(args: EntryGreet) -> Next { .first() .cloned() .unwrap_or_else(|| "World".to_string()); - ResultName::new(name) + ResultName::new(name).into() } // 4. 用 Renderer 输出结果 diff --git a/docs/_zh_CN/pages/5-multiple-commands.md b/docs/_zh_CN/pages/5-multiple-commands.md index 7d2617b..38ce2cf 100644 --- a/docs/_zh_CN/pages/5-multiple-commands.md +++ b/docs/_zh_CN/pages/5-multiple-commands.md @@ -20,13 +20,13 @@ pack!(ResultSum = i32); #[chain] fn handle_greet(args: EntryGreet) -> Next { let name = args.inner.first().cloned().unwrap_or_else(|| "World".to_string()); - ResultGreeting::new(name) + ResultGreeting::new(name).into() } #[chain] fn handle_add(args: EntryAdd) -> Next { let sum: i32 = args.inner.iter().filter_map(|s| s.parse::<i32>().ok()).sum(); - ResultSum::new(sum) + ResultSum::new(sum).into() } #[renderer] @@ -70,9 +70,9 @@ Sum: 6 @@@dispatcher!("add", CMDAdd => EntryAdd); @@@pack!(ResultGreeting = String); @@@pack!(ResultSum = i32); -@@@#[chain] fn handle_greet(_args: EntryGreet) -> Next { ResultGreeting::new("ok".into()) } +@@@#[chain] fn handle_greet(_args: EntryGreet) -> Next { ResultGreeting::new("ok".into()).into() } @@@#[renderer] fn render_greet(_greeting: ResultGreeting) -> RenderResult { RenderResult::new() } -@@@#[chain] fn handle_add(_args: EntryAdd) -> Next { ResultSum::new(0) } +@@@#[chain] fn handle_add(_args: EntryAdd) -> Next { ResultSum::new(0).into() } @@@#[renderer] fn render_sum(_sum: ResultSum) -> RenderResult { RenderResult::new() } fn main() { let mut program = ThisProgram::new(); diff --git a/docs/_zh_CN/pages/6-argument-parse-picker.md b/docs/_zh_CN/pages/6-argument-parse-picker.md index b41f839..4bf162c 100644 --- a/docs/_zh_CN/pages/6-argument-parse-picker.md +++ b/docs/_zh_CN/pages/6-argument-parse-picker.md @@ -32,7 +32,7 @@ features = ["parser"] #[chain] fn handle_greet_entry(prev: EntryGreet) -> Next { let name = prev.pick_or((), "World").unpack(); - ResultName::new(name) + ResultName::new(name).into() } ``` @@ -47,7 +47,7 @@ fn handle_greet_entry(prev: EntryGreet) -> Next { @@@#[chain] @@@fn handle_greet_entry(prev: EntryGreet) -> Next { let name = prev.pick_or((), "World").unpack(); -@@@ResultName::new(name) +@@@ResultName::new(name).into() @@@} ``` @@ -82,7 +82,7 @@ let name = prev.pick_or((), "World").unpack(); #[chain] fn handle_greet_entry(prev: EntryGreet) -> Next { let name = prev.pick_or(["--name", "-n"], "World").unpack(); - ResultName::new(name) + ResultName::new(name).into() } ``` @@ -124,7 +124,7 @@ fn handle_test_entry(prev: EntryTest) -> Next { .pick::<u32>(["--id", "-I"]) .unpack(); - ResultInfo::new((name, age, id)) + ResultInfo::new((name, age, id)).into() } ``` @@ -199,7 +199,7 @@ let name = match pick_result { 在您使用 `pick` 提取了用户输入后,可以使用 `after` 立刻处理该参数 -```rust +````rust // Features: ["parser"] @@@dispatcher!("greet", CMDGreet => EntryGreet); @@@pack!(ResultName = String); @@ -217,7 +217,7 @@ fn handle_greet_entry(prev: EntryGreet) -> Next { }) .unpack(); - ResultName::new(name) + ResultName::new(name).into() } ``` @@ -338,7 +338,7 @@ impl Pickable for Address { #[chain] fn handle_connect_entry(prev: EntryConnect) -> Next { let address: Address = prev.pick("--addr").unpack(); - ResultConnected::new(address) + ResultConnected::new(address).into() } #[renderer] @@ -379,7 +379,7 @@ impl PickableEnum for Fruits {} #[chain] fn handle_eat_entry(prev: EntryEat) -> Next { let fruit: Fruits = prev.pick("--fruit").unpack(); - ResultFruit::new(fruit) + ResultFruit::new(fruit).into() } #[renderer] diff --git a/docs/_zh_CN/pages/advanced/2-structural-renderer.md b/docs/_zh_CN/pages/advanced/2-structural-renderer.md index 9a4f111..6b6b0f9 100644 --- a/docs/_zh_CN/pages/advanced/2-structural-renderer.md +++ b/docs/_zh_CN/pages/advanced/2-structural-renderer.md @@ -37,7 +37,7 @@ pack_structural!(ResultInfo = (String, i32)); fn handle_render(args: EntryRender) -> Next { let name = args.inner.first().cloned().unwrap_or_default(); let age = args.inner.get(1).and_then(|s| s.parse().ok()).unwrap_or(0); - ResultInfo::new((name, age)) + ResultInfo::new((name, age)).into() } #[renderer] diff --git a/docs/_zh_CN/pages/other/features.md b/docs/_zh_CN/pages/other/features.md index bfd9efc..d92e597 100644 --- a/docs/_zh_CN/pages/other/features.md +++ b/docs/_zh_CN/pages/other/features.md @@ -24,7 +24,7 @@ pack!(StateFoo = ()); #[chain] async fn handle_state_foo(foo: StateFoo) -> Next { - StateFoo::new(()) + StateFoo::new(()).into() } ``` @@ -160,7 +160,7 @@ use mingling::macros::entry; pack!(EntryHello = Vec<String>); fn main() { - let result = handle_hello(entry!("--name", "Bob")).into(); + let result: Next = handle_hello(entry!("--name", "Bob")).into(); // ... 此处为断言逻辑 } diff --git a/docs/pages/11-resource-system.md b/docs/pages/11-resource-system.md index bc2197f..9491212 100644 --- a/docs/pages/11-resource-system.md +++ b/docs/pages/11-resource-system.md @@ -57,7 +57,7 @@ Use `&mut T` to inject a mutable resource: #[chain] fn handle_visit(_args: EntryVisit, counter: &mut ResVisitCount) -> Next { counter.0 += 1; - ResultDone::default() + ResultDone::default().into() } #[renderer] diff --git a/docs/pages/3-define-a-chain.md b/docs/pages/3-define-a-chain.md index c6d3f8e..b2822bc 100644 --- a/docs/pages/3-define-a-chain.md +++ b/docs/pages/3-define-a-chain.md @@ -24,7 +24,7 @@ fn handle_greet(args: EntryGreet) -> Next { // args contains the remaining params after matching user input let name = args.inner.first().cloned().unwrap_or_else(|| "World".to_string()); // Wrap the result into Next, telling the dispatcher where to go next - ResultName::new(name) + ResultName::new(name).into() } ``` @@ -88,7 +88,7 @@ fn handle_greet(args: EntryGreet) -> Next { .cloned() .unwrap_or_else(|| "World".to_string()); - ResultName::new(name) + ResultName::new(name).into() } ``` @@ -112,7 +112,7 @@ fn handle_greet(args: EntryGreet) -> Next { .first() .cloned() .unwrap_or_else(|| "World".to_string()); - ResultName::new(name) + ResultName::new(name).into() } fn main() { diff --git a/docs/pages/4-render-result.md b/docs/pages/4-render-result.md index b1365b8..ca1e563 100644 --- a/docs/pages/4-render-result.md +++ b/docs/pages/4-render-result.md @@ -51,7 +51,7 @@ fn handle_greet(args: EntryGreet) -> Next { .first() .cloned() .unwrap_or_else(|| "World".to_string()); - ResultName::new(name) + ResultName::new(name).into() } // 4. Output results with a Renderer diff --git a/docs/pages/5-multiple-commands.md b/docs/pages/5-multiple-commands.md index a56f7b0..d9a335a 100644 --- a/docs/pages/5-multiple-commands.md +++ b/docs/pages/5-multiple-commands.md @@ -20,13 +20,13 @@ pack!(ResultSum = i32); #[chain] fn handle_greet(args: EntryGreet) -> Next { let name = args.inner.first().cloned().unwrap_or_else(|| "World".to_string()); - ResultGreeting::new(name) + ResultGreeting::new(name).into() } #[chain] fn handle_add(args: EntryAdd) -> Next { let sum: i32 = args.inner.iter().filter_map(|s| s.parse::<i32>().ok()).sum(); - ResultSum::new(sum) + ResultSum::new(sum).into() } #[renderer] @@ -70,9 +70,9 @@ Notice `with_dispatchers`? When you need to register multiple dispatchers, just @@@dispatcher!("add", CMDAdd => EntryAdd); @@@pack!(ResultGreeting = String); @@@pack!(ResultSum = i32); -@@@#[chain] fn handle_greet(_args: EntryGreet) -> Next { ResultGreeting::new("ok".into()) } +@@@#[chain] fn handle_greet(_args: EntryGreet) -> Next { ResultGreeting::new("ok".into()).into() } @@@#[renderer] fn render_greet(_greeting: ResultGreeting) -> RenderResult { RenderResult::new() } -@@@#[chain] fn handle_add(_args: EntryAdd) -> Next { ResultSum::new(0) } +@@@#[chain] fn handle_add(_args: EntryAdd) -> Next { ResultSum::new(0).into() } @@@#[renderer] fn render_sum(_sum: ResultSum) -> RenderResult { RenderResult::new() } fn main() { let mut program = ThisProgram::new(); diff --git a/docs/pages/6-argument-parse-picker.md b/docs/pages/6-argument-parse-picker.md index c80c25c..398cd3c 100644 --- a/docs/pages/6-argument-parse-picker.md +++ b/docs/pages/6-argument-parse-picker.md @@ -32,7 +32,7 @@ Now let's look at `Picker` in action: #[chain] fn handle_greet_entry(prev: EntryGreet) -> Next { let name = prev.pick_or((), "World").unpack(); - ResultName::new(name) + ResultName::new(name).into() } ``` @@ -47,7 +47,7 @@ Breaking down the example above: @@@#[chain] @@@fn handle_greet_entry(prev: EntryGreet) -> Next { let name = prev.pick_or((), "World").unpack(); -@@@ResultName::new(name) +@@@ResultName::new(name).into() @@@} ``` @@ -82,7 +82,7 @@ If your program needs to parse flag args (e.g., `greet --name Alice`), do this: #[chain] fn handle_greet_entry(prev: EntryGreet) -> Next { let name = prev.pick_or(["--name", "-n"], "World").unpack(); - ResultName::new(name) + ResultName::new(name).into() } ``` @@ -124,7 +124,7 @@ fn handle_test_entry(prev: EntryTest) -> Next { .pick::<u32>(["--id", "-I"]) .unpack(); - ResultInfo::new((name, age, id)) + ResultInfo::new((name, age, id)).into() } ``` @@ -224,7 +224,7 @@ fn handle_greet_entry(prev: EntryGreet) -> Next { }) .unpack(); - ResultName::new(name) + ResultName::new(name).into() } ``` @@ -345,7 +345,7 @@ impl Pickable for Address { #[chain] fn handle_connect_entry(prev: EntryConnect) -> Next { let address: Address = prev.pick("--addr").unpack(); - ResultConnected::new(address) + ResultConnected::new(address).into() } #[renderer] @@ -387,7 +387,7 @@ impl PickableEnum for Fruits {} #[chain] fn handle_eat_entry(prev: EntryEat) -> Next { let fruit: Fruits = prev.pick("--fruit").unpack(); - ResultFruit::new(fruit) + ResultFruit::new(fruit).into() } #[renderer] diff --git a/docs/pages/advanced/2-structural-renderer.md b/docs/pages/advanced/2-structural-renderer.md index f31f758..fab7530 100644 --- a/docs/pages/advanced/2-structural-renderer.md +++ b/docs/pages/advanced/2-structural-renderer.md @@ -37,7 +37,7 @@ pack_structural!(ResultInfo = (String, i32)); fn handle_render(args: EntryRender) -> Next { let name = args.inner.first().cloned().unwrap_or_default(); let age = args.inner.get(1).and_then(|s| s.parse().ok()).unwrap_or(0); - ResultInfo::new((name, age)) + ResultInfo::new((name, age)).into() } #[renderer] diff --git a/docs/pages/other/features.md b/docs/pages/other/features.md index 813ccdd..325bb75 100644 --- a/docs/pages/other/features.md +++ b/docs/pages/other/features.md @@ -24,7 +24,7 @@ pack!(StateFoo = ()); #[chain] async fn handle_state_foo(foo: StateFoo) -> Next { - StateFoo::new(()) + StateFoo::new(()).into() } ``` @@ -160,7 +160,7 @@ use mingling::macros::entry; pack!(EntryHello = Vec<String>); fn main() { - let result = handle_hello(entry!("--name", "Bob")).into(); + let result: Next = handle_hello(entry!("--name", "Bob")).into(); // ... assertion logic here } diff --git a/examples/example-argument-parse/src/main.rs b/examples/example-argument-parse/src/main.rs index 59499a2..eb07672 100644 --- a/examples/example-argument-parse/src/main.rs +++ b/examples/example-argument-parse/src/main.rs @@ -46,7 +46,7 @@ fn handle_transfer_parse(args: EntryTransfer) -> Next { // Convert into ResultFile .into(); // --------- IMPORTANT --------- - result + result.into() } pack!(ErrorNoNameProvided = ()); diff --git a/examples/example-async-support/src/main.rs b/examples/example-async-support/src/main.rs index ac85aaf..af86408 100644 --- a/examples/example-async-support/src/main.rs +++ b/examples/example-async-support/src/main.rs @@ -50,7 +50,7 @@ pack!(ResultDownloaded = String); // vvvvv_ `async` keyword can be used directly here pub async fn handle_download(args: EntryDownload) -> Next { let file_name = args.pick(()).unpack(); - fake_download(file_name).await + fake_download(file_name).await.into() } /// Renders the downloaded file name. diff --git a/examples/example-basic/src/main.rs b/examples/example-basic/src/main.rs index 7fc0bce..17077e2 100644 --- a/examples/example-basic/src/main.rs +++ b/examples/example-basic/src/main.rs @@ -55,7 +55,7 @@ fn handle_greet(args: EntryGreet) -> Next { .cloned() .unwrap_or_else(|| "World".to_string()) .into(); - name + name.into() } // Define renderer `render_name`, used to render `ResultName` diff --git a/examples/example-combine-pathf-dispatch-tree/src/sub/mod.rs b/examples/example-combine-pathf-dispatch-tree/src/sub/mod.rs index 5ab0ece..2b7aba9 100644 --- a/examples/example-combine-pathf-dispatch-tree/src/sub/mod.rs +++ b/examples/example-combine-pathf-dispatch-tree/src/sub/mod.rs @@ -14,7 +14,7 @@ pub fn handle_my(args: EntryHello) -> Next { .cloned() .unwrap_or_else(|| "World".to_string()) .into(); - name + name.into() } #[renderer] diff --git a/examples/example-completion/src/main.rs b/examples/example-completion/src/main.rs index 0159807..45cc8ef 100644 --- a/examples/example-completion/src/main.rs +++ b/examples/example-completion/src/main.rs @@ -114,7 +114,7 @@ fn handle_greet(args: EntryGreet) -> Next { .pick_or((), "World") .unpack() .into(); - result + result.into() } /// Renders the greeting with the result name and repeat count. diff --git a/examples/example-enum-tag/src/main.rs b/examples/example-enum-tag/src/main.rs index 01c7767..91c5358 100644 --- a/examples/example-enum-tag/src/main.rs +++ b/examples/example-enum-tag/src/main.rs @@ -79,7 +79,7 @@ dispatcher!("lang-select", CMDLanguageSelection => EntryLanguageSelection); fn handle_language_selection(args: EntryLanguageSelection) -> Next { // You can use Picker to directly parse ProgrammingLanguages let lang: ProgrammingLanguages = args.pick(()).unpack(); - lang + lang.into() } /// Renders the selected programming language with its name and description. diff --git a/examples/example-hook/src/main.rs b/examples/example-hook/src/main.rs index 41928ca..23b87c7 100644 --- a/examples/example-hook/src/main.rs +++ b/examples/example-hook/src/main.rs @@ -64,7 +64,7 @@ fn handle_greet(args: EntryGreet) -> Next { .cloned() .unwrap_or_else(|| "World".to_string()) .into(); - name + name.into() } /// Renders the greeting message with the provided name. diff --git a/examples/example-panic-unwind/src/main.rs b/examples/example-panic-unwind/src/main.rs index a829158..d5f746f 100644 --- a/examples/example-panic-unwind/src/main.rs +++ b/examples/example-panic-unwind/src/main.rs @@ -47,7 +47,7 @@ fn handle_panic(prev: EntryPanic) -> Next { // Panic happens here, will be caught panic!("{}", s) } - None => NotPanic::default(), + None => NotPanic::default().into(), } } diff --git a/examples/example-pathfinder/src/sub/mod.rs b/examples/example-pathfinder/src/sub/mod.rs index 6d15930..8cbc1c5 100644 --- a/examples/example-pathfinder/src/sub/mod.rs +++ b/examples/example-pathfinder/src/sub/mod.rs @@ -13,7 +13,7 @@ pub fn handle_greet(args: EntryGreet) -> Next { .cloned() .unwrap_or_else(|| "World".to_string()) .into(); - name + name.into() } /// Renders the name. diff --git a/examples/example-repl-basic/src/main.rs b/examples/example-repl-basic/src/main.rs index d15319e..cfd00d1 100644 --- a/examples/example-repl-basic/src/main.rs +++ b/examples/example-repl-basic/src/main.rs @@ -94,7 +94,7 @@ pack!(ResultList = Vec<String>); #[chain] fn parse_cd_args(prev: EntryCd) -> Next { let join = prev.pick(()).unpack(); - StateChangeDirectory::new(join) + StateChangeDirectory::new(join).into() } // Execute directory change diff --git a/examples/example-resources/src/main.rs b/examples/example-resources/src/main.rs index b08ee26..60c5c08 100644 --- a/examples/example-resources/src/main.rs +++ b/examples/example-resources/src/main.rs @@ -51,7 +51,7 @@ fn render_modify_current(args: EntryModifyCurrent, current_dir: &mut ResCurrentD current_dir.current_dir = current_dir .current_dir .join(args.pick::<String>(()).unpack()); - EntryCurrent::default() + EntryCurrent::default().into() } // Define renderer for output current path _____________ Injected resource diff --git a/mingling/src/constants.rs b/mingling/src/constants.rs new file mode 100644 index 0000000..65d02e8 --- /dev/null +++ b/mingling/src/constants.rs @@ -0,0 +1,8 @@ +#[cfg(feature = "picker")] +mod picker; + +#[cfg(feature = "picker")] +pub use picker::*; + +mod exit_codes; +pub use exit_codes::*; diff --git a/mingling/src/constants/exit_codes.rs b/mingling/src/constants/exit_codes.rs new file mode 100644 index 0000000..2359f42 --- /dev/null +++ b/mingling/src/constants/exit_codes.rs @@ -0,0 +1,14 @@ +/// Exit code indicating successful command execution. +pub const EXIT_SUCCESS: i32 = 0; + +/// Exit code for general errors. +pub const EXIT_GENERAL_ERR: i32 = 1; + +/// Exit code for incorrect command usage (or invalid arguments). +pub const EXIT_USAGE_ERR: i32 = 2; + +/// Exit code indicating permission denied (or the command is not executable). +pub const EXIT_PERM_DENIED: i32 = 126; + +/// Exit code for command not found (or PATH error). +pub const EXIT_CMD_NOT_FOUND: i32 = 127; diff --git a/mingling/src/constants/picker.rs b/mingling/src/constants/picker.rs new file mode 100644 index 0000000..f2a448b --- /dev/null +++ b/mingling/src/constants/picker.rs @@ -0,0 +1,129 @@ +use std::marker::PhantomData; + +use arg_picker::{PickerArg, PickerArgs, value::Flag}; + +/// Remaining positional arguments (anything not consumed as an option). +/// - `full`: `[]` (empty — not triggered by any `--` prefix). +/// - `short`: (none) +/// - `positional`: `false` (this is a meta‑argument that collects everything left). +/// This constant is used internally to access any leftover arguments after +/// all defined flags/options have been processed. +pub const REMAINS: PickerArg<PickerArgs> = PickerArg::<PickerArgs> { + full: &[], + short: None, + positional: false, + internal_type: PhantomData, +}; + +/// Help flag: display usage information. +/// - `full`: `["help"]` +/// - `short`: `'h'` +pub const HELP_FLAG: PickerArg<Flag> = PickerArg::<Flag> { + full: &["help"], + short: Some('h'), + positional: false, + internal_type: PhantomData, +}; + +/// Quiet flag: suppress output. +/// - `full`: `["quiet"]` +/// - `short`: `'q'` +pub const QUIET_FLAG: PickerArg<Flag> = PickerArg::<Flag> { + full: &["quiet"], + short: Some('q'), + positional: false, + internal_type: PhantomData, +}; + +/// Confirm flag: require user confirmation before proceeding. +/// - `full`: `["confirm"]` +/// - `short`: `'C'` +pub const CONFIRM_FLAG: PickerArg<Flag> = PickerArg::<Flag> { + full: &["confirm"], + short: Some('C'), + positional: false, + internal_type: PhantomData, +}; + +/// Renderer flag: explicitly specify the Structural Renderer argument. +/// - `full`: `["renderer"]` +/// - `short`: (none) +#[cfg(feature = "structural_renderer")] +pub const RENDERER_ARG: PickerArg<String> = PickerArg::<String> { + full: &["renderer"], + short: None, + positional: false, + internal_type: PhantomData, +}; + +/// JSON flag: enable JSON output format. +/// - `full`: `["json"]` +/// - `short`: (none) +/// Available only when the `json_serde_fmt` feature is enabled. +#[cfg(feature = "json_serde_fmt")] +pub const JSON_FLAG: PickerArg<Flag> = PickerArg::<Flag> { + full: &["json"], + short: None, + positional: false, + internal_type: PhantomData, +}; + +/// JSON pretty flag: enable pretty-printed JSON output format. +/// - `full`: `["json_pretty"]` +/// - `short`: (none) +/// Available only when the `json_serde_fmt` feature is enabled. +#[cfg(feature = "json_serde_fmt")] +pub const JSON_PRETTY_FLAG: PickerArg<Flag> = PickerArg::<Flag> { + full: &["json_pretty"], + short: None, + positional: false, + internal_type: PhantomData, +}; + +/// YAML flag: enable YAML output format. +/// - `full`: `["yaml"]` +/// - `short`: (none) +/// Available only when the `yaml_serde_fmt` feature is enabled. +#[cfg(feature = "yaml_serde_fmt")] +pub const YAML_FLAG: PickerArg<Flag> = PickerArg::<Flag> { + full: &["yaml"], + short: None, + positional: false, + internal_type: PhantomData, +}; + +/// TOML flag: enable TOML output format. +/// - `full`: `["toml"]` +/// - `short`: (none) +/// Available only when the `toml_serde_fmt` feature is enabled. +#[cfg(feature = "toml_serde_fmt")] +pub const TOML_FLAG: PickerArg<Flag> = PickerArg::<Flag> { + full: &["toml"], + short: None, + positional: false, + internal_type: PhantomData, +}; + +/// RON flag: enable RON output format. +/// - `full`: `["ron"]` +/// - `short`: (none) +/// Available only when the `ron_serde_fmt` feature is enabled. +#[cfg(feature = "ron_serde_fmt")] +pub const RON_FLAG: PickerArg<Flag> = PickerArg::<Flag> { + full: &["ron"], + short: None, + positional: false, + internal_type: PhantomData, +}; + +/// RON pretty flag: enable pretty-printed RON output format. +/// - `full`: `["ron_pretty"]` +/// - `short`: (none) +/// Available only when the `ron_serde_fmt` feature is enabled. +#[cfg(feature = "ron_serde_fmt")] +pub const RON_PRETTY_FLAG: PickerArg<Flag> = PickerArg::<Flag> { + full: &["ron_pretty"], + short: None, + positional: false, + internal_type: PhantomData, +}; diff --git a/mingling/src/example_docs.rs b/mingling/src/example_docs.rs index 4699a50..c4f59c9 100644 --- a/mingling/src/example_docs.rs +++ b/mingling/src/example_docs.rs @@ -66,7 +66,7 @@ /// // Convert into ResultFile /// .into(); /// // --------- IMPORTANT --------- -/// result +/// result.into() /// } /// /// pack!(ErrorNoNameProvided = ()); @@ -199,7 +199,7 @@ pub mod example_argument_parse {} /// // vvvvv_ `async` keyword can be used directly here /// pub async fn handle_download(args: EntryDownload) -> Next { /// let file_name = args.pick(()).unpack(); -/// fake_download(file_name).await +/// fake_download(file_name).await.into() /// } /// /// /// Renders the downloaded file name. @@ -292,7 +292,7 @@ pub mod example_async_support {} /// .cloned() /// .unwrap_or_else(|| "World".to_string()) /// .into(); -/// name +/// name.into() /// } /// /// // Define renderer `render_name`, used to render `ResultName` @@ -672,7 +672,7 @@ pub mod example_combine_pathf_dispatch_tree {} /// .pick_or((), "World") /// .unpack() /// .into(); -/// result +/// result.into() /// } /// /// /// Renders the greeting with the result name and repeat count. @@ -1026,7 +1026,7 @@ pub mod example_dispatch_tree {} /// fn handle_language_selection(args: EntryLanguageSelection) -> Next { /// // You can use Picker to directly parse ProgrammingLanguages /// let lang: ProgrammingLanguages = args.pick(()).unpack(); -/// lang +/// lang.into() /// } /// /// /// Renders the selected programming language with its name and description. @@ -1428,7 +1428,7 @@ pub mod example_help {} /// .cloned() /// .unwrap_or_else(|| "World".to_string()) /// .into(); -/// name +/// name.into() /// } /// /// /// Renders the greeting message with the provided name. @@ -1967,7 +1967,7 @@ pub mod example_pack_err {} /// // Panic happens here, will be caught /// panic!("{}", s) /// } -/// None => NotPanic::default(), +/// None => NotPanic::default().into(), /// } /// } /// @@ -2161,7 +2161,7 @@ pub mod example_pathfinder {} /// #[chain] /// fn parse_cd_args(prev: EntryCd) -> Next { /// let join = prev.pick(()).unpack(); -/// StateChangeDirectory::new(join) +/// StateChangeDirectory::new(join).into() /// } /// /// // Execute directory change @@ -2323,7 +2323,7 @@ pub mod example_repl_basic {} /// current_dir.current_dir = current_dir /// .current_dir /// .join(args.pick::<String>(()).unpack()); -/// EntryCurrent::default() +/// EntryCurrent::default().into() /// } /// /// // Define renderer for output current path _____________ Injected resource diff --git a/mingling/src/lib.md b/mingling/src/lib.md index cd89b96..03fa61d 100644 --- a/mingling/src/lib.md +++ b/mingling/src/lib.md @@ -40,7 +40,7 @@ fn handle_greet(args: EntryGreet) -> Next { .cloned() .unwrap_or_else(|| "World".to_string()) .into(); - name + name.into() } #[renderer] diff --git a/mingling/src/lib.rs b/mingling/src/lib.rs index 718d282..4b3ced6 100644 --- a/mingling/src/lib.rs +++ b/mingling/src/lib.rs @@ -16,12 +16,13 @@ pub mod parser; /// `Mingling` argument parser (Picker2) #[cfg(feature = "picker")] -pub mod picker { - pub use arg_picker::*; +pub mod picker; - pub mod parselib { - pub use arg_picker::parselib::*; - } +mod constants; + +/// Constants used throughout the Mingling framework. +pub mod consts { + pub use crate::constants::*; } /// Re-export of all macros from `mingling_macros`. @@ -217,7 +218,10 @@ pub mod prelude { pub use crate::parser::AsPicker; #[cfg(feature = "picker")] - pub use arg_picker::prelude::*; + pub use arg_picker::prelude::arg; + + #[cfg(feature = "picker")] + pub use crate::picker::EntryPicker; /// Used to enable the `writeln!` macro for `RenderResult` #[cfg(feature = "core")] diff --git a/mingling/src/parser/picker.rs b/mingling/src/parser/picker.rs index 601cd3f..ca4561c 100644 --- a/mingling/src/parser/picker.rs +++ b/mingling/src/parser/picker.rs @@ -743,6 +743,10 @@ where } } +/// Trait for types that can be converted into a `Picker` to extract values from command-line arguments. +/// +/// This trait provides a convenient way to convert a value (such as `Vec<String>`, `&[String]`, etc.) +/// into a `Picker` and immediately start extracting values associated with specific flags. pub trait AsPicker where Self: Into<Vec<String>>, diff --git a/mingling/src/picker.rs b/mingling/src/picker.rs new file mode 100644 index 0000000..f370656 --- /dev/null +++ b/mingling/src/picker.rs @@ -0,0 +1,13 @@ +/// Provides the specific parsing logic for command-line arguments and common utilities, +/// as well as customization of command-line argument styles. +pub mod parselib { + pub use arg_picker::parselib::*; +} + +pub use arg_picker::*; + +mod entry_picker; +pub use entry_picker::*; + +mod global; +pub use global::*; diff --git a/arg_picker/src/corebind/entry_picker.rs b/mingling/src/picker/entry_picker.rs index d543f98..7a980c6 100644 --- a/arg_picker/src/corebind/entry_picker.rs +++ b/mingling/src/picker/entry_picker.rs @@ -1,8 +1,6 @@ -use std::marker::PhantomData; - use mingling_core::{ChainProcess, Groupped, ProgramCollect}; -use crate::{Pickable, Picker, PickerArg, PickerArgs, PickerPattern1}; +use crate::{picker::Pickable, picker::Picker, picker::PickerArg, picker::PickerPattern1}; /// Trait for converting Mingling entry types (types that implement `Groupped<R>` and `Into<Vec<String>>`) /// into [`Picker`] instances for a given route type `R`. @@ -40,7 +38,7 @@ pub trait EntryPicker<'a, This> { Next: Pickable<'a> + Default + Sized, { let picker = Self::to_picker(self); - Picker::build_pattern1(picker.args, arg.into(), None) + Picker::build_pattern1(picker.into_args(), arg.into(), None) } /// Starts building a picker pattern with the first argument, using a default value provider. @@ -123,9 +121,6 @@ where { fn to_picker(self) -> Picker<'a, ChainProcess<This>> { let args = self.into(); - Picker { - route_phantom: PhantomData, - args: PickerArgs::Owned(args), - } + Picker::from(args) } } diff --git a/mingling/src/picker/global.rs b/mingling/src/picker/global.rs new file mode 100644 index 0000000..c203524 --- /dev/null +++ b/mingling/src/picker/global.rs @@ -0,0 +1,35 @@ +use arg_picker::{IntoPicker, Pickable, PickerArg, value::Flag}; +use mingling_core::{Program, ProgramCollect}; + +use crate::consts::REMAINS; + +/// Picks a global flag from the program's arguments. +/// +/// This function takes ownership of the program's current arguments, picks the specified `flag` +/// from them, and then returns the remaining arguments back to the program. It returns the +/// boolean value of the flag. +pub fn pick_global_flag<C>(program: &mut Program<C>, flag: &PickerArg<Flag>) -> bool +where + C: ProgramCollect<Enum = C>, +{ + let args = program.take_args(); + let (flag, args) = args.pick(flag).pick(&REMAINS).unwrap(); + program.replace_args(args.into()); + *flag +} + +/// Picks a global argument from the program's arguments. +/// +/// This function takes ownership of the program's current arguments, picks the specified `arg` +/// from them, and then returns the remaining arguments back to the program. It returns the +/// picked argument value, or `None` if the argument was not present. +pub fn pick_global_argument<C, A>(program: &mut Program<C>, arg: &PickerArg<A>) -> Option<A> +where + A: for<'a> Pickable<'a> + Default, + C: ProgramCollect<Enum = C>, +{ + let args = program.take_args(); + let (arg, remains) = args.pick(arg).pick(&REMAINS).unpack(); + program.replace_args(remains.unwrap().into()); + arg +} diff --git a/mingling/src/setups.rs b/mingling/src/setups.rs index e572cf5..6fd728a 100644 --- a/mingling/src/setups.rs +++ b/mingling/src/setups.rs @@ -7,6 +7,13 @@ pub use dirs::*; mod exit_code; pub use exit_code::*; +/// Picker's `ProgramSetup` variant. +/// +/// Internally does not use its own argument parsing, +/// but relies on `arg_picker`'s argument parsing capability. +#[cfg(feature = "picker")] +pub mod picker; + #[cfg(feature = "structural_renderer")] mod structural_renderer; diff --git a/mingling/src/setups/basic.rs b/mingling/src/setups/basic.rs index 6a69733..e081c3e 100644 --- a/mingling/src/setups/basic.rs +++ b/mingling/src/setups/basic.rs @@ -1,3 +1,5 @@ +#![allow(deprecated)] + use mingling_core::{Flag, Program, ProgramCollect, setup::ProgramSetup}; /// Performs basic program initialization: @@ -5,6 +7,12 @@ use mingling_core::{Flag, Program, ProgramCollect, setup::ProgramSetup}; /// - Collects `--quiet` flag to control message rendering /// - Collects `--help` flag to enable help mode /// - Collects `--confirm` flag to skip user confirmation +#[cfg_attr( + feature = "picker", + deprecated( + note = "When the `picker` feature is enabled, you can use `mingling::setup::picker::BasicProgramSetup` instead" + ) +)] pub struct BasicProgramSetup; impl<C> ProgramSetup<C> for BasicProgramSetup @@ -21,6 +29,12 @@ where /// Provides setup for parsing the user help flag /// /// The default value is `--help / -h` +#[cfg_attr( + feature = "picker", + deprecated( + note = "When the `picker` feature is enabled, you can use `mingling::setup::picker::HelpFlagSetup` instead" + ) +)] pub struct HelpFlagSetup { flag: Flag, } @@ -54,6 +68,12 @@ impl Default for HelpFlagSetup { /// Provides setup for parsing the quiet flag /// /// The default value is `--quiet / -q` +#[cfg_attr( + feature = "picker", + deprecated( + note = "When the `picker` feature is enabled, you can use `mingling::setup::picker::QuietFlagSetup` instead" + ) +)] pub struct QuietFlagSetup { flag: Flag, } @@ -88,6 +108,12 @@ impl Default for QuietFlagSetup { /// Provides setup for parsing the confirm flag /// /// The default value is `--confirm / -C` +#[cfg_attr( + feature = "picker", + deprecated( + note = "When the `picker` feature is enabled, you can use `mingling::setup::picker::ConfirmFlagSetup` instead" + ) +)] pub struct ConfirmFlagSetup { flag: Flag, } diff --git a/mingling/src/setups/exit_code.rs b/mingling/src/setups/exit_code.rs index 025ed8a..6b8a1ef 100644 --- a/mingling/src/setups/exit_code.rs +++ b/mingling/src/setups/exit_code.rs @@ -2,7 +2,7 @@ use std::marker::PhantomData; use mingling_core::{ ProgramCollect, - hook::{ProgramControlUnit, ProgramHook}, + hook::{ProgramControlUnit, ProgramControls, ProgramHook}, setup::ProgramSetup, this, }; @@ -39,7 +39,14 @@ where // Insert hook to override exit code before program ends program.with_hook(ProgramHook::empty().on_finish(|_| { let this = this::<C>().res_or_default::<ResExitCode>(); - ProgramControlUnit::OverrideExitCode(this.exit_code) + let ec = this.exit_code; + + // Only override when ResExitCode has been modified + if ec != 0 { + ProgramControlUnit::OverrideExitCode(this.exit_code).into() + } else { + ProgramControls::Empty + } })); } } diff --git a/mingling/src/setups/picker.rs b/mingling/src/setups/picker.rs new file mode 100644 index 0000000..0b7bd33 --- /dev/null +++ b/mingling/src/setups/picker.rs @@ -0,0 +1,7 @@ +mod basic; +pub use basic::*; + +#[cfg(feature = "structural_renderer")] +mod structural_renderer; +#[cfg(feature = "structural_renderer")] +pub use structural_renderer::*; diff --git a/mingling/src/setups/picker/basic.rs b/mingling/src/setups/picker/basic.rs new file mode 100644 index 0000000..c9f82b3 --- /dev/null +++ b/mingling/src/setups/picker/basic.rs @@ -0,0 +1,123 @@ +use arg_picker::{PickerArg, value::Flag}; +use mingling_core::{Program, ProgramCollect, setup::ProgramSetup}; + +use crate::{ + consts::{CONFIRM_FLAG, HELP_FLAG, QUIET_FLAG}, + picker::pick_global_flag, +}; + +/// Performs basic program initialization: +/// +/// - Collects `--quiet` flag to control message rendering +/// - Collects `--help` flag to enable help mode +/// - Collects `--confirm` flag to skip user confirmation +pub struct BasicProgramSetup; + +impl<C> ProgramSetup<C> for BasicProgramSetup +where + C: ProgramCollect<Enum = C>, +{ + fn setup(self, program: &mut Program<C>) { + program.with_setup(HelpFlagSetup::default()); + program.with_setup(QuietFlagSetup::default()); + program.with_setup(ConfirmFlagSetup::default()); + } +} + +/// Provides setup for parsing the user help flag +/// +/// The default value is `--help / -h` +pub struct HelpFlagSetup<'a> { + flag: &'a PickerArg<'a, Flag>, +} + +impl<'a> HelpFlagSetup<'a> { + /// Creates a new `HelpFlagSetup` with the given flag aliases. + pub fn new(flag: &'a PickerArg<Flag>) -> Self { + Self { flag } + } +} + +impl<'a, C> ProgramSetup<C> for HelpFlagSetup<'a> +where + C: ProgramCollect<Enum = C>, +{ + fn setup(self, program: &mut Program<C>) { + let help = pick_global_flag(program, self.flag); + if help { + program.user_context.help = true; + } + } +} + +impl<'a> Default for HelpFlagSetup<'a> { + fn default() -> Self { + Self { flag: &HELP_FLAG } + } +} + +/// Provides setup for parsing the quiet flag +/// +/// The default value is `--quiet / -q` +pub struct QuietFlagSetup<'a> { + flag: &'a PickerArg<'a, Flag>, +} + +impl<'a> QuietFlagSetup<'a> { + /// Creates a new `QuietFlagSetup` with the given flag aliases. + pub fn new(flag: &'a PickerArg<Flag>) -> Self { + Self { flag } + } +} + +impl<'a, C> ProgramSetup<C> for QuietFlagSetup<'a> +where + C: ProgramCollect<Enum = C>, +{ + fn setup(self, program: &mut Program<C>) { + let help = pick_global_flag(program, self.flag); + if help { + program.stdout_setting.quiet = true; + } + } +} + +impl<'a> Default for QuietFlagSetup<'a> { + fn default() -> Self { + Self { flag: &QUIET_FLAG } + } +} + +/// Provides setup for parsing the confirm flag +/// +/// The default value is `--confirm / -C` +pub struct ConfirmFlagSetup<'a> { + flag: &'a PickerArg<'a, Flag>, +} + +impl<'a> ConfirmFlagSetup<'a> { + /// Creates a new `ConfirmFlagSetup` with the given flag aliases. + pub fn new(flag: &'a PickerArg<Flag>) -> Self { + Self { flag } + } +} + +impl<'a, C> ProgramSetup<C> for ConfirmFlagSetup<'a> +where + C: ProgramCollect<Enum = C>, +{ + fn setup(self, program: &mut Program<C>) { + let help = pick_global_flag(program, self.flag); + if help { + program.user_context.confirm = true; + } + } +} + +impl<'a> Default for ConfirmFlagSetup<'a> { + fn default() -> Self { + Self { + flag: &CONFIRM_FLAG, + } + } +} diff --git a/mingling/src/setups/picker/structural_renderer.rs b/mingling/src/setups/picker/structural_renderer.rs new file mode 100644 index 0000000..1fa48fd --- /dev/null +++ b/mingling/src/setups/picker/structural_renderer.rs @@ -0,0 +1,76 @@ +use mingling_core::{Program, ProgramCollect, setup::ProgramSetup}; + +use crate::{ + consts::RENDERER_ARG, + picker::{pick_global_argument, pick_global_flag}, +}; + +/// Sets up the structural renderer for the program: +/// +/// - Adds a `--renderer` global argument to specify the renderer type +pub struct StructuralRendererSimpleSetup; + +impl<C> ProgramSetup<C> for StructuralRendererSimpleSetup +where + C: ProgramCollect<Enum = C>, +{ + fn setup(self, program: &mut Program<C>) { + if let Some(renderer) = pick_global_argument(program, &RENDERER_ARG) { + program.structural_renderer_name = renderer.into(); + } + } +} + +/// Sets up the structural renderer for the program: +/// +/// - Adds global flags to specify the renderer type: +/// * `--json` for JSON output +/// * `--json-pretty` for pretty-printed JSON output +/// * `--yaml` for YAML output +/// * `--toml` for TOML output +/// * `--ron` for RON output +/// * `--ron-pretty` for pretty-printed RON output +/// +/// # Flag priority +/// +/// If multiple flags are specified, the last matching flag in the following +/// declaration order takes precedence: +/// 1. `--json` +/// 2. `--json-pretty` +/// 3. `--yaml` +/// 4. `--toml` +/// 5. `--ron` +/// 6. `--ron-pretty` +pub struct StructuralRendererSetup; + +impl<C> ProgramSetup<C> for StructuralRendererSetup +where + C: ProgramCollect<Enum = C>, +{ + fn setup(self, program: &mut Program<C>) { + #[cfg(feature = "json_serde_fmt")] + if pick_global_flag(program, &crate::consts::JSON_FLAG) { + program.structural_renderer_name = crate::StructuralRendererSetting::Json; + } + #[cfg(feature = "json_serde_fmt")] + if pick_global_flag(program, &crate::consts::JSON_PRETTY_FLAG) { + program.structural_renderer_name = crate::StructuralRendererSetting::JsonPretty; + } + #[cfg(feature = "yaml_serde_fmt")] + if pick_global_flag(program, &crate::consts::YAML_FLAG) { + program.structural_renderer_name = crate::StructuralRendererSetting::Yaml; + } + #[cfg(feature = "toml_serde_fmt")] + if pick_global_flag(program, &crate::consts::TOML_FLAG) { + program.structural_renderer_name = crate::StructuralRendererSetting::Toml; + } + #[cfg(feature = "ron_serde_fmt")] + if pick_global_flag(program, &crate::consts::RON_FLAG) { + program.structural_renderer_name = crate::StructuralRendererSetting::Ron; + } + #[cfg(feature = "ron_serde_fmt")] + if pick_global_flag(program, &crate::consts::RON_PRETTY_FLAG) { + program.structural_renderer_name = crate::StructuralRendererSetting::RonPretty; + } + } +} diff --git a/mingling/src/setups/structural_renderer.rs b/mingling/src/setups/structural_renderer.rs index af3ed91..0ab2347 100644 --- a/mingling/src/setups/structural_renderer.rs +++ b/mingling/src/setups/structural_renderer.rs @@ -1,8 +1,16 @@ +#![allow(deprecated)] + use mingling_core::{Program, ProgramCollect, setup::ProgramSetup}; /// Sets up the structural renderer for the program: /// /// - Adds a `--renderer` global argument to specify the renderer type +#[cfg_attr( + feature = "picker", + deprecated( + note = "When the `picker` feature is enabled, you can use `mingling::setup::picker::StructuralRendererSimpleSetup` instead" + ) +)] pub struct StructuralRendererSimpleSetup; impl<C> ProgramSetup<C> for StructuralRendererSimpleSetup @@ -25,6 +33,12 @@ where /// * `--toml` for TOML output /// * `--ron` for RON output /// * `--ron-pretty` for pretty-printed RON output +#[cfg_attr( + feature = "picker", + deprecated( + note = "When the `picker` feature is enabled, you can use `mingling::setup::picker::StructuralRendererSetup` instead" + ) +)] pub struct StructuralRendererSetup; impl<C> ProgramSetup<C> for StructuralRendererSetup diff --git a/mingling_core/src/asset/global_resource.rs b/mingling_core/src/asset/global_resource.rs index 3d0af7b..29e1136 100644 --- a/mingling_core/src/asset/global_resource.rs +++ b/mingling_core/src/asset/global_resource.rs @@ -45,13 +45,12 @@ where /// Internal syntax for the `&mut MyResource` syntax of #[chain], do not use directly #[doc(hidden)] - pub fn __modify_res_and_return_route<Res, Return>( + pub fn __modify_res_and_return_route<Res>( &self, - f: impl FnOnce(&mut Res) -> Return, - ) -> impl Into<ChainProcess<C>> + f: impl FnOnce(&mut Res) -> ChainProcess<C>, + ) -> ChainProcess<C> where Res: 'static + Default + ResourceMarker + Send + Sync, - Return: Into<ChainProcess<C>>, { let Ok(mut guard) = self.resources.lock() else { let mut default_res = Res::res_default(); diff --git a/mingling_core/src/program.rs b/mingling_core/src/program.rs index 71d5290..0d409b7 100644 --- a/mingling_core/src/program.rs +++ b/mingling_core/src/program.rs @@ -130,6 +130,33 @@ where &self.args } + /// Returns a mutable reference to the program's command-line arguments. + #[must_use] + pub fn get_args_mut(&mut self) -> &mut [String] { + &mut self.args + } + + /// Takes ownership of the program's command-line arguments, replacing them with an empty Vec. + /// This is useful when you need to transfer the arguments to another context or process them + /// and then replace them later. + #[must_use] + pub fn take_args(&mut self) -> Vec<String> { + std::mem::take(&mut self.args) + } + + /// Replaces the program's command-line arguments with a new set and returns the old ones. + /// + /// # Arguments + /// + /// * `args` - The new command-line arguments to set. + /// + /// # Returns + /// + /// The previous command-line arguments. + pub fn replace_args(&mut self, args: Vec<String>) -> Vec<String> { + std::mem::replace(&mut self.args, args) + } + /// Get all registered dispatcher names from the program #[must_use] pub fn get_nodes( diff --git a/mingling_core/src/program/setup.rs b/mingling_core/src/program/setup.rs index f248fb6..838c29a 100644 --- a/mingling_core/src/program/setup.rs +++ b/mingling_core/src/program/setup.rs @@ -1,9 +1,26 @@ use crate::{ProgramCollect, program::Program}; +/// Trait for defining initialization/setup logic for a `Program`. +/// +/// Implementors can define custom setup behavior that will be executed +/// when the program is initialized via [`Program::with_setup`]. +/// +/// # Type Parameters +/// +/// * `C` - The program collect type, which must implement [`ProgramCollect`] +/// and have `Enum = C` (i.e., it collects itself). pub trait ProgramSetup<C> where C: ProgramCollect<Enum = C>, { + /// Perform setup on the given program. + /// + /// This method consumes the setup instance (`self`) and is called once + /// during program initialization. + /// + /// # Arguments + /// + /// * `program` - A mutable reference to the [`Program`] being set up. fn setup(self, program: &mut Program<C>); } diff --git a/mingling_core/src/renderer/render_result.rs b/mingling_core/src/renderer/render_result.rs index 82a745c..e57a5b9 100644 --- a/mingling_core/src/renderer/render_result.rs +++ b/mingling_core/src/renderer/render_result.rs @@ -39,6 +39,56 @@ impl Deref for RenderResult { } } +impl From<()> for RenderResult { + fn from(_value: ()) -> Self { + RenderResult::new() + } +} + +macro_rules! impl_from_int { + ($($ty:ty),+ $(,)?) => { + $( + impl From<$ty> for RenderResult { + fn from(exit_code: $ty) -> Self { + RenderResult { + exit_code: exit_code as i32, + ..Default::default() + } + } + } + )+ + }; +} + +impl_from_int!(i32, i16, i8, u32, u16, u8, usize); + +impl From<&String> for RenderResult { + fn from(value: &String) -> Self { + RenderResult { + render_text: value.clone(), + exit_code: 0, + } + } +} + +impl From<String> for RenderResult { + fn from(value: String) -> Self { + RenderResult { + render_text: value, + exit_code: 0, + } + } +} + +impl From<&str> for RenderResult { + fn from(value: &str) -> Self { + RenderResult { + render_text: value.to_string(), + exit_code: 0, + } + } +} + impl From<RenderResult> for String { fn from(result: RenderResult) -> Self { result.render_text diff --git a/mingling_macros/src/chain.rs b/mingling_macros/src/chain.rs index 5f72422..566e782 100644 --- a/mingling_macros/src/chain.rs +++ b/mingling_macros/src/chain.rs @@ -9,8 +9,7 @@ use quote::{ToTokens, quote}; use syn::spanned::Spanned; use syn::{Ident, ItemFn, Pat, ReturnType, Signature, Type, TypePath, parse_macro_input}; -/// Validates that the return type of the function is `Next`. -/// Checks whether the return type is `()` (unit). +/// Checks whether the return type is `()` fn is_unit_return_type(sig: &Signature) -> bool { match &sig.output { ReturnType::Type(_, ty) => match &**ty { @@ -21,48 +20,22 @@ fn is_unit_return_type(sig: &Signature) -> bool { } } +/// Validates that the return type is acceptable. +/// Accepts `()`, `Next`, `ChainProcess<...>`, or any type that can +/// be converted to `ChainProcess` via `.into()` (i.e. any pack type). fn validate_return_type(sig: &Signature) -> Result<(), proc_macro2::TokenStream> { - // If return type is `()`, it's valid (no Next required) + // `()` or omitted is always valid if is_unit_return_type(sig) { return Ok(()); } - match &sig.output { - ReturnType::Type(_, ty) => match &**ty { - Type::Path(type_path) => { - let last_segment = type_path.path.segments.last().unwrap(); - if last_segment.ident != "Next" { - return Err(syn::Error::new( - ty.span(), - "Chain function must return `Next` or `()`", - ) - .to_compile_error()); - } - } - _ => { - return Err(syn::Error::new( - ty.span(), - "Chain function must return `Next` or `()`", - ) - .to_compile_error()); - } - }, - ReturnType::Default => { - return Err(syn::Error::new( - sig.span(), - "Chain function must specify a return type (must be `Next` or `()`)", - ) - .to_compile_error()); - } - } Ok(()) } -/// Builds the `proc` function implementation that serves as the actual chain -/// entry point inside the generated `Chain` impl. +/// Builds the `proc` function implementation inside the generated `Chain` impl. /// -/// * Without resources: delegates directly to the original function. -/// * With resources: inlines the body and prepends resource bindings. +/// The user's function body is inlined directly, and its result is converted +/// via `.into()` to `ChainProcess<ProgramType>`. #[allow(unused_variables)] fn generate_proc_fn( has_resources: bool, @@ -70,67 +43,55 @@ fn generate_proc_fn( program_type: &proc_macro2::TokenStream, previous_type: &TypePath, prev_param: &Pat, - fn_name: &Ident, fn_body_stmts: &[syn::Stmt], is_async_fn: bool, is_unit_return: bool, + origin_return_type: &proc_macro2::TokenStream, ) -> proc_macro2::TokenStream { let immut_resource_stmts = generate_immut_resource_bindings(resources.iter(), program_type); let mut_resources: Vec<_> = resources.iter().filter(|r| r.is_mut).collect(); - let body_stmts: &[syn::Stmt] = if is_unit_return && has_resources { - let mut stmts = fn_body_stmts.to_vec(); - stmts.push(syn::Stmt::Expr( - syn::parse_quote! { - <crate::ResultEmpty as ::mingling::Groupped::<crate::ThisProgram>> - ::to_chain(crate::ResultEmpty) - }, - None, - )); - // Box::leak to get a &'static [syn::Stmt] - Box::leak(Box::new(stmts)) - } else { - fn_body_stmts - }; - let wrapped_body = if is_async_fn && !mut_resources.is_empty() { - wrap_body_with_mut_resources_async(body_stmts, &mut_resources, program_type) + wrap_body_with_mut_resources_async(fn_body_stmts, &mut_resources, program_type) } else { - wrap_body_with_mut_resources(body_stmts, &mut_resources, program_type) + wrap_body_with_mut_resources(fn_body_stmts, &mut_resources, program_type, is_unit_return) }; - // When the function returns `()`, wrap the result with ResultEmpty - let call_or_wrapped = if is_unit_return { - if has_resources { + let proc_body = if is_unit_return { + let body_with_ending = if has_resources { quote! { #(#immut_resource_stmts)* - #wrapped_body + #wrapped_body; + <crate::ResultEmpty as ::mingling::Groupped::<crate::ThisProgram>> + ::to_chain(crate::ResultEmpty) } } else { - let call = if is_async_fn { - quote! { #fn_name(#prev_param).await; } - } else { - quote! { #fn_name(#prev_param); } - }; quote! { - #call + #wrapped_body; <crate::ResultEmpty as ::mingling::Groupped::<crate::ThisProgram>> ::to_chain(crate::ResultEmpty) } - } - } else if has_resources { - quote! { - #(#immut_resource_stmts)* - #wrapped_body - } + }; + quote! { #body_with_ending } } else { - let call = if is_async_fn { - quote! { #fn_name(#prev_param).await.into() } + let body = if has_resources { + quote! { + #(#immut_resource_stmts)* + #wrapped_body + } } else { - quote! { #fn_name(#prev_param).into() } + quote! { #wrapped_body } }; + // Convert the body result to `ChainProcess` using the user-declared + // return type as the source type for a fully-qualified `Into` call. + // This works for both: + // - `-> Next` / `-> ChainProcess`: identity `From<T> for T` + // - `-> PackType`: `Into<ChainProcess>` from pack!/derive quote! { - #call + let __chain_result = { #body }; + <#origin_return_type as ::std::convert::Into< + ::mingling::ChainProcess<#program_type> + >>::into(__chain_result) } }; @@ -138,7 +99,7 @@ fn generate_proc_fn( { quote! { async fn proc(#prev_param: #previous_type) -> ::mingling::ChainProcess<#program_type> { - #call_or_wrapped + #proc_body } } } @@ -147,73 +108,14 @@ fn generate_proc_fn( { quote! { fn proc(#prev_param: #previous_type) -> ::mingling::ChainProcess<#program_type> { - #call_or_wrapped - } - } - } -} - -/// Generates the original function signature (kept for backwards compatibility / -/// internal use), with its return type changed to `impl Into<ChainProcess<..>>`. -#[allow(unused_variables)] -fn generate_original_fn( - fn_attrs: &[syn::Attribute], - vis: &syn::Visibility, - fn_name: &Ident, - inputs: &syn::punctuated::Punctuated<syn::FnArg, syn::Token![,]>, - fn_body: &syn::Block, - is_async_fn: bool, - program_type: &proc_macro2::TokenStream, - is_unit_return: bool, -) -> proc_macro2::TokenStream { - // Both unit and Next return types need to produce `impl Into<ChainProcess<ProgramType>>` - let return_type = quote! { impl Into<::mingling::ChainProcess<#program_type>> }; - - let body = if is_unit_return { - quote! { - { - #fn_body - <crate::ResultEmpty as ::mingling::Groupped::<crate::ThisProgram>>::to_chain(crate::ResultEmpty) - } - } - } else { - quote! { - { - let _: crate::Next; - let _: Next; - #fn_body - } - } - }; - - #[cfg(feature = "async")] - { - let async_kw = if is_async_fn { - quote! { async } - } else { - quote! {} - }; - quote! { - #(#fn_attrs)* - #vis #async_kw fn #fn_name(#inputs) -> #return_type { - #body - } - } - } - - #[cfg(not(feature = "async"))] - { - quote! { - #(#fn_attrs)* - #vis fn #fn_name(#inputs) -> #return_type { - #body + #proc_body } } } } /// Assembles the final expanded output: hidden struct, `register_chain!` invocation, -/// `Chain` impl with the `proc` method, and the original function. +/// `Chain` impl with the `proc` method, and the preserved original function. fn generate_struct_and_impl( fn_attrs: &[syn::Attribute], vis: &syn::Visibility, @@ -222,7 +124,7 @@ fn generate_struct_and_impl( previous_type_str: &proc_macro2::TokenStream, program_type: &proc_macro2::TokenStream, proc_fn: &proc_macro2::TokenStream, - origin_proc_fn: &proc_macro2::TokenStream, + original_fn: &proc_macro2::TokenStream, ) -> proc_macro2::TokenStream { quote! { #(#fn_attrs)* @@ -238,8 +140,8 @@ fn generate_struct_and_impl( #proc_fn } - // Keep the original function for internal use - #origin_proc_fn + // Keep the original function unchanged + #original_fn } } @@ -296,8 +198,6 @@ pub fn chain_attr(attr: TokenStream, item: TokenStream) -> TokenStream { }; // Prepare building blocks - let sig = &input_fn.sig; - let inputs = &sig.inputs; let fn_body = &input_fn.block; let mut fn_attrs = input_fn.attrs.clone(); fn_attrs.retain(|attr| !attr.path().is_ident("chain")); @@ -315,36 +215,36 @@ pub fn chain_attr(attr: TokenStream, item: TokenStream) -> TokenStream { // Always use the default crate-defined program path let program_type = crate::default_program_path(); - // Generate the `proc` function + // Extract the user's return type for the explicit Into turbofish + let origin_return_type = match &input_fn.sig.output { + ReturnType::Type(_, ty) => quote! { #ty }, + ReturnType::Default => quote! { () }, + }; + + // Generate the `proc` function for the Chain impl let proc_fn = generate_proc_fn( has_resources, &resources, &program_type, &previous_type, &prev_param, - fn_name, &fn_body.stmts, #[cfg(feature = "async")] is_async_fn, #[cfg(not(feature = "async"))] false, is_unit_return, + &origin_return_type, ); - // Generate the original function - let origin_proc_fn = generate_original_fn( - &fn_attrs, - vis, - fn_name, - inputs, - fn_body, - #[cfg(feature = "async")] - is_async_fn, - #[cfg(not(feature = "async"))] - false, - &program_type, - is_unit_return, - ); + // Preserve the original function untouched, with dead_code allowed + // since it may only be called through the Chain trait dispatch. + // Note: do NOT add `#vis` here — `input_fn` (ItemFn) already contains its own visibility. + let original_fn = quote! { + #[allow(dead_code)] + #(#fn_attrs)* + #input_fn + }; // Assemble the final output let previous_type_str = quote! { #previous_type }; @@ -356,7 +256,7 @@ pub fn chain_attr(attr: TokenStream, item: TokenStream) -> TokenStream { &previous_type_str, &program_type, &proc_fn, - &origin_proc_fn, + &original_fn, ); expanded.into() diff --git a/mingling_macros/src/help.rs b/mingling_macros/src/help.rs index 1903d07..6e0d12e 100644 --- a/mingling_macros/src/help.rs +++ b/mingling_macros/src/help.rs @@ -1,34 +1,16 @@ use proc_macro::TokenStream; use quote::{ToTokens, quote}; use syn::spanned::Spanned; -use syn::{Ident, ItemFn, ReturnType, Type, TypePath, parse_macro_input}; +use syn::{Ident, ItemFn, ReturnType, Signature, TypePath, parse_macro_input}; use crate::get_global_set; use crate::res_injection::{extract_args_info, generate_immut_resource_bindings}; -/// Validates that the function returns `::mingling::RenderResult`. -fn validate_render_result_return(sig: &syn::Signature) -> syn::Result<()> { +/// Extracts the user's return type, returning `None` for no return type. +fn extract_user_return_type(sig: &Signature) -> Option<proc_macro2::TokenStream> { match &sig.output { - ReturnType::Type(_, ty) => match &**ty { - Type::Path(type_path) => { - let last_seg = type_path.path.segments.last().map(|s| s.ident.to_string()); - match last_seg.as_deref() { - Some("RenderResult") => Ok(()), - _ => Err(syn::Error::new( - ty.span(), - "Help function must return `RenderResult`", - )), - } - } - _ => Err(syn::Error::new( - ty.span(), - "Help function must return `RenderResult`", - )), - }, - ReturnType::Default => Err(syn::Error::new( - sig.span(), - "Help function must have a return type `-> RenderResult`", - )), + ReturnType::Type(_, ty) => Some(quote! { #ty }), + ReturnType::Default => None, } } @@ -49,10 +31,8 @@ pub fn help_attr(item: TokenStream) -> TokenStream { Err(e) => return e.to_compile_error().into(), }; - // Validate return type is RenderResult - if let Err(e) = validate_render_result_return(&input_fn.sig) { - return e.to_compile_error().into(); - } + // Determine the user's return type for preserving the original function + let user_return_type = extract_user_return_type(&input_fn.sig); // Get the function body let fn_body = &input_fn.block; @@ -69,8 +49,8 @@ pub fn help_attr(item: TokenStream) -> TokenStream { let fn_name = &input_fn.sig.ident; // Get original inputs to keep the original function - let original_inputs = input_fn.sig.inputs.clone(); + let original_return_type = user_return_type.clone().unwrap_or(quote! { () }); // Generate internal name using snake_case let internal_name = format!( @@ -149,17 +129,18 @@ pub fn help_attr(item: TokenStream) -> TokenStream { type Entry = #entry_type; fn render_help(#prev_param: Self::Entry) -> ::mingling::RenderResult { - #help_render_body + let __help_result = { #help_render_body }; + ::std::convert::Into::into(__help_result) } } ::mingling::macros::register_help!(#entry_type, #struct_name); // Keep the original function unchanged - + #[allow(dead_code)] #(#fn_attrs)* - #vis fn #fn_name(#original_inputs) -> ::mingling::RenderResult { - #fn_body + #vis fn #fn_name(#original_inputs) -> #original_return_type { + #(#fn_body_stmts)* } }; diff --git a/mingling_macros/src/lib.rs b/mingling_macros/src/lib.rs index d0f603a..9419f39 100644 --- a/mingling_macros/src/lib.rs +++ b/mingling_macros/src/lib.rs @@ -867,7 +867,8 @@ pub fn dispatcher(input: TokenStream) -> TokenStream { /// /// - The function must have at least **one** parameter (the previous type in the chain). /// - The first parameter must be taken **by move**. -/// - The function must return `Next` (the type alias generated by `gen_program!`, which equals `ChainProcess<ProgramName>`). +/// - The function may return `Next`, `ChainProcess<ProgramName>`, `()`, or omit the return type. +/// - The original function signature is preserved unchanged. /// - With the `async` feature, async functions are supported; without it, async functions are rejected. #[proc_macro_attribute] pub fn chain(attr: TokenStream, item: TokenStream) -> TokenStream { @@ -1234,7 +1235,7 @@ pub fn register_dispatcher(input: TokenStream) -> TokenStream { /// /// - The function must have exactly one parameter (the entry type to provide help for). /// - The parameter type must be a single-segment type path (e.g., `MyEntry`, not `other::MyEntry`). -/// - The function must return `RenderResult`. +/// - The function may return `RenderResult`, `()`, or any type that implements `Into<RenderResult>`. /// - The function cannot be async. /// /// # See also diff --git a/mingling_macros/src/renderer.rs b/mingling_macros/src/renderer.rs index f47511c..d124ec9 100644 --- a/mingling_macros/src/renderer.rs +++ b/mingling_macros/src/renderer.rs @@ -1,38 +1,16 @@ use proc_macro::TokenStream; use quote::{ToTokens, quote}; use syn::spanned::Spanned; -use syn::{ItemFn, ReturnType, Signature, Type, TypePath, parse_macro_input}; +use syn::{ItemFn, ReturnType, Signature, TypePath, parse_macro_input}; use crate::get_global_set; use crate::res_injection::{extract_args_info, generate_immut_resource_bindings}; -/// Validates that the function returns `::mingling::RenderResult`. -fn validate_render_result_return(sig: &Signature) -> syn::Result<()> { +/// Extracts the user's return type, returning `None` for no return type. +fn extract_user_return_type(sig: &Signature) -> Option<proc_macro2::TokenStream> { match &sig.output { - ReturnType::Type(_, ty) => { - // Check if the return type is RenderResult - match &**ty { - Type::Path(type_path) => { - let segments = &type_path.path.segments; - let last_seg = segments.last().map(|s| s.ident.to_string()); - match last_seg.as_deref() { - Some("RenderResult") => Ok(()), - _ => Err(syn::Error::new( - ty.span(), - "Renderer function must return `RenderResult`", - )), - } - } - _ => Err(syn::Error::new( - ty.span(), - "Renderer function must return `RenderResult`", - )), - } - } - ReturnType::Default => Err(syn::Error::new( - sig.span(), - "Renderer function must have a return type `-> RenderResult`", - )), + ReturnType::Type(_, ty) => Some(quote! { #ty }), + ReturnType::Default => None, } } @@ -59,10 +37,8 @@ pub fn renderer_attr(attr: TokenStream, item: TokenStream) -> TokenStream { Err(e) => return e.to_compile_error().into(), }; - // Validate that the function returns RenderResult - if let Err(e) = validate_render_result_return(&input_fn.sig) { - return e.to_compile_error().into(); - } + // Determine the user's return type and whether it needs to be converted to RenderResult + let user_return_type = extract_user_return_type(&input_fn.sig); // Get function body statements let fn_body_stmts: Vec<syn::Stmt> = input_fn.block.stmts.clone(); @@ -123,10 +99,7 @@ pub fn renderer_attr(attr: TokenStream, item: TokenStream) -> TokenStream { // The original function preserves the user's exact signature and body. // Resource parameters are passed directly by the caller, NOT injected from context. let original_inputs = input_fn.sig.inputs.clone(); - let original_return_type = match &input_fn.sig.output { - ReturnType::Type(_, ty) => quote! { #ty }, - ReturnType::Default => unreachable!("Already validated that return type is RenderResult"), - }; + let original_return_type = user_return_type.clone().unwrap_or(quote! { () }); let expanded = quote! { #(#fn_attrs)* @@ -140,11 +113,13 @@ pub fn renderer_attr(attr: TokenStream, item: TokenStream) -> TokenStream { type Previous = #previous_type; fn render(#prev_param: Self::Previous) -> ::mingling::RenderResult { - #render_fn_body + let __renderer_result = { #render_fn_body }; + ::std::convert::Into::into(__renderer_result) } } // Keep the original function unchanged + #[allow(dead_code)] #(#fn_attrs)* #vis fn #fn_name(#original_inputs) -> #original_return_type { #(#fn_body_stmts)* diff --git a/mingling_macros/src/res_injection.rs b/mingling_macros/src/res_injection.rs index 09da889..606b9a6 100644 --- a/mingling_macros/src/res_injection.rs +++ b/mingling_macros/src/res_injection.rs @@ -163,8 +163,10 @@ fn mut_res_binding_name(var_name: &Ident) -> Ident { syn::Ident::new(&format!("__{}_binding", var_name), var_name.span()) } -/// Wraps the function body in nested `__modify_res_and_return_route` closures for -/// each mutable resource parameter (sync version). +/// Wraps the function body in mutable resource closures (sync version). +/// +/// For unit return types: uses `modify_res` (closure returns `()`). +/// For non-unit return types: uses `__modify_res_and_return_route` (closure returns `ChainProcess<C>`). /// /// The innermost closure gets the original body, and each mutable parameter wraps /// outward from last to first. @@ -172,6 +174,7 @@ pub(crate) fn wrap_body_with_mut_resources( fn_body_stmts: &[syn::Stmt], mut_resources: &[&ResourceInjection], program_type: &proc_macro2::TokenStream, + is_unit_return: bool, ) -> proc_macro2::TokenStream { let mut wrapped = quote! { #(#fn_body_stmts)* @@ -180,11 +183,19 @@ pub(crate) fn wrap_body_with_mut_resources( for res in mut_resources { let var_name = &res.var_name; let inner_type = &res.inner_type; - wrapped = quote! { - ::mingling::this::<#program_type>().__modify_res_and_return_route(|#var_name: &mut #inner_type| { - #wrapped - }).into() - }; + if is_unit_return { + wrapped = quote! { + ::mingling::this::<#program_type>().modify_res(|#var_name: &mut #inner_type| { + #wrapped + }) + }; + } else { + wrapped = quote! { + ::mingling::this::<#program_type>().__modify_res_and_return_route(|#var_name: &mut #inner_type| { + #wrapped + }) + }; + } } wrapped diff --git a/mling/src/cli.rs b/mling/src/cli.rs index 67d2ef0..45a49d0 100644 --- a/mling/src/cli.rs +++ b/mling/src/cli.rs @@ -1,3 +1,6 @@ +// `mling` has not been replaced from `parser` to `picker` yet +#![allow(deprecated)] + use crate::{ CMDCompletion, ErrorDispatcherNotFound, Next, ThisProgram, display::markdown, diff --git a/verified-docs.toml b/verified-docs.toml index d66967f..f3b1f85 100644 --- a/verified-docs.toml +++ b/verified-docs.toml @@ -3,5 +3,6 @@ [verified] readme = "./README.md" +getting_started = "./GETTING_STARTED.md" documents_en_us = "./docs/pages/**" documents_zh_cn = "./docs/_zh_CN/pages/**" |
