diff options
59 files changed, 868 insertions, 365 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 59f0f89..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, @@ -41,8 +43,8 @@ fn main() { "cargo metadata --format-version 1 --no-deps", &workspace_root, ) - .unwrap_or_else(|(_code, msg)| { - eprintln_cargo_style!("cargo metadata failed (exit {code}):\n{msg}"); + .unwrap_or_else(|(code, _msg)| { + eprintln_cargo_style!(format!("cargo metadata failed (exit {code}):\n{{msg}}")); std::process::exit(1); }); @@ -171,7 +173,7 @@ fn main() { } Err((code, msg)) => { // Print output but don't fail yet - eprintln_cargo_style!("cargo package exited with code {code}:"); + eprintln_cargo_style!(format!("cargo package exited with code {code}:")); println!("{msg}"); } } @@ -202,7 +204,7 @@ fn main() { for entry in std::fs::read_dir(&temp_package_dir).expect("failed to read target/package") { let entry = entry.expect("failed to read entry"); let path = entry.path(); - if path.extension().map_or(true, |e| e != "crate") { + if path.extension().is_none_or(|e| e != "crate") { continue; } @@ -217,11 +219,7 @@ fn main() { .and_then(|dash| { // Check if what follows looks like a semver let ver_part = &fname[dash + 1..]; - if ver_part - .chars() - .next() - .map_or(false, |c| c.is_ascii_digit()) - { + if ver_part.chars().next().is_some_and(|c| c.is_ascii_digit()) { Some(&fname[..dash]) } else { None @@ -234,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; } @@ -317,31 +313,30 @@ fn patch_workspace_deps( in_workspace_deps = false; } - if in_workspace_deps { - if let Some(dep_name) = trimmed.split('=').next().map(|s| s.trim()) { - if let Some(version) = version_map.get(dep_name) { - let indent = line - .chars() - .take_while(|c| c.is_whitespace()) - .collect::<String>(); - - if trimmed.contains("path =") { - let path_value = extract_path_value(trimmed); - let patched_line: String = if let Some(pv) = path_value { - trimmed.replace( - &format!("path = \"{}\"", pv), - &format!("path = \"{}\", version = \"{}\"", pv, version), - ) - } else { - trimmed.to_string() - }; - result.push_str(&format!("{indent}{patched_line}\n")); - } else { - result.push_str(&format!("{indent}{dep_name} = \"{version}\"\n")); - } - continue; - } + if in_workspace_deps + && let Some(dep_name) = trimmed.split('=').next().map(|s| s.trim()) + && let Some(version) = version_map.get(dep_name) + { + let indent = line + .chars() + .take_while(|c| c.is_whitespace()) + .collect::<String>(); + + if trimmed.contains("path =") { + let path_value = extract_path_value(trimmed); + let patched_line: String = if let Some(pv) = path_value { + trimmed.replace( + &format!("path = \"{}\"", pv), + &format!("path = \"{}\", version = \"{}\"", pv, version), + ) + } else { + trimmed.to_string() + }; + result.push_str(&format!("{indent}{patched_line}\n")); + } else { + result.push_str(&format!("{indent}{dep_name} = \"{version}\"\n")); } + continue; } result.push_str(line); @@ -391,25 +386,21 @@ fn patch_member_path_deps( if (in_deps || in_build_deps) && trimmed.contains("path = \"") && !trimmed.contains("workspace = true") + && let Some(dep_name) = trimmed.split('=').next().map(|s| s.trim()) + && let Some(version) = version_map.get(dep_name.trim_end_matches(".workspace")) + && !trimmed.contains("version = \"") { - if let Some(dep_name) = trimmed.split('=').next().map(|s| s.trim()) { - let dep_name = dep_name.trim_end_matches(".workspace"); - if let Some(version) = version_map.get(dep_name) { - if !trimmed.contains("version = \"") { - let indent = line - .chars() - .take_while(|c| c.is_whitespace()) - .collect::<String>(); - let path_val = extract_path_value(trimmed).unwrap_or_default(); - let patched = trimmed.replace( - &format!("path = \"{path_val}\""), - &format!("path = \"{path_val}\", version = \"{version}\""), - ); - result.push_str(&format!("{indent}{patched}\n")); - continue; - } - } - } + let indent = line + .chars() + .take_while(|c| c.is_whitespace()) + .collect::<String>(); + let path_val = extract_path_value(trimmed).unwrap_or_default(); + let patched = trimmed.replace( + &format!("path = \"{path_val}\""), + &format!("path = \"{path_val}\", version = \"{version}\""), + ); + result.push_str(&format!("{indent}{patched}\n")); + continue; } result.push_str(line); 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/CHANGELOG.md b/CHANGELOG.md index ac00643..49b1f35 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -108,6 +108,59 @@ 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 + } + ``` + #### **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 +180,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) @@ -166,7 +166,7 @@ fn handle_greet(args: EntryGreet) -> Next { .first() .cloned() .unwrap_or_else(|| "World".to_string()); - ResultGreeting::new(greeting) + ResultGreeting::new(greeting).into() } ``` @@ -234,7 +234,7 @@ fn handle_greet(args: EntryGreet) -> Next { .pick::<String>(()) // positional: first string .pick_or::<u8>(["-r", "--repeat"], 1) // optional flag with default .unpack(); - ResultGreeting::new(format!("{} x{}", name, count)) + ResultGreeting::new(format!("{} x{}", name, count)).into() } ``` @@ -252,7 +252,7 @@ fn handle(args: EntryGreet) -> Next { .pick::<Option<String>>(()) .pick_or::<u8>(["-r", "--repeat"], 1) .unpack(); - ResultGreeting::new(format!("{} x{}", name.unwrap_or_default(), count)) + ResultGreeting::new(format!("{} x{}", name.unwrap_or_default(), count)).into() } ``` @@ -279,7 +279,7 @@ impl PickableEnum for Language {} #[chain] fn handle(args: EntryLang) -> Next { let lang: Language = args.pick(()).unpack(); - lang + lang.into() } ``` @@ -781,7 +781,7 @@ pack!(ResultDownloaded = String); #[chain] pub async fn handle_download(args: EntryDownload) -> Next { let file = args.pick(()).unpack(); - download_file(file).await + download_file(file).await.into() } async fn download_file(name: String) -> ResultDownloaded { @@ -843,7 +843,7 @@ fn handle_greet(args: EntryGreet) -> Next { .first() .cloned() .unwrap_or_else(|| "World".to_string()); - ResultGreeting::new(greeting) + ResultGreeting::new(greeting).into() } #[renderer] diff --git a/arg_picker/test/src/test/value_flag_test.rs b/arg_picker/test/src/test/value_flag_test.rs index 3df053e..c60ab1e 100644 --- a/arg_picker/test/src/test/value_flag_test.rs +++ b/arg_picker/test/src/test/value_flag_test.rs @@ -1,5 +1,5 @@ use arg_picker::value::Flag; -use arg_picker::{macros::arg, IntoPicker}; +use arg_picker::{IntoPicker, macros::arg}; // Basic Flag — present / absent @@ -136,10 +136,10 @@ fn test_flag_to_option() { #[test] fn test_flag_converts_to_bool() { let flag = Flag::Active; - assert!(bool::from(flag)); + assert_eq!(bool::from(flag), true); let flag = Flag::Inactive; - assert!(!bool::from(flag)); + assert_eq!(bool::from(flag), false); } #[test] @@ -151,10 +151,10 @@ fn test_flag_from_bool() { #[test] fn test_flag_deref_to_bool() { let active = Flag::Active; - assert!(*active); + assert_eq!(*active, true); let inactive = Flag::Inactive; - assert!(!*inactive); + assert_eq!(*inactive, false); } // Flag never triggers route (unlike bool) 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/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/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_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/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..5044227 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,8 +20,9 @@ fn is_unit_return_type(sig: &Signature) -> bool { } } +/// Validates that the return type is `Next`, `ChainProcess<ThisProgram>`, `()`, or omitted. 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(()); } @@ -31,38 +31,36 @@ fn validate_return_type(sig: &Signature) -> Result<(), proc_macro2::TokenStream> 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()); + let ident_str = last_segment.ident.to_string(); + if ident_str == "Next" || ident_str == "ChainProcess" { + return Ok(()); } - } - _ => { - return Err(syn::Error::new( + Err(syn::Error::new( ty.span(), - "Chain function must return `Next` or `()`", + "Chain function must return `Next`, `ChainProcess<ThisProgram>`, `()`, or omit the return type", ) - .to_compile_error()); + .to_compile_error()) } + _ => Err(syn::Error::new( + ty.span(), + "Chain function must return `Next`, `ChainProcess<ThisProgram>`, `()`, or omit the return type", + ) + .to_compile_error()), }, ReturnType::Default => { - return Err(syn::Error::new( + Err(syn::Error::new( sig.span(), - "Chain function must specify a return type (must be `Next` or `()`)", + "Chain function must specify a return type (must be `Next`, `ChainProcess<ThisProgram>`, or `()`)", ) - .to_compile_error()); + .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,7 +68,6 @@ 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, @@ -78,59 +75,42 @@ fn generate_proc_fn( 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 } }; + // Use a let-binding with explicit type annotation so that the inner + // body's `.into()` calls resolve correctly (same as the original function). quote! { - #call + let __chain_result: ::mingling::ChainProcess<#program_type> = { #body }; + __chain_result } }; @@ -138,7 +118,7 @@ fn generate_proc_fn( { quote! { async fn proc(#prev_param: #previous_type) -> ::mingling::ChainProcess<#program_type> { - #call_or_wrapped + #proc_body } } } @@ -147,73 +127,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 +143,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 +159,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 +217,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,14 +234,13 @@ 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 + // 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, @@ -331,20 +249,14 @@ pub fn chain_attr(attr: TokenStream, item: TokenStream) -> TokenStream { is_unit_return, ); - // 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 +268,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 |
