diff options
| -rw-r--r-- | CHANGELOG.md | 8 | ||||
| -rw-r--r-- | examples/example-outside-type/Cargo.lock | 76 | ||||
| -rw-r--r-- | examples/example-outside-type/Cargo.toml | 12 | ||||
| -rw-r--r-- | examples/example-outside-type/page.toml | 10 | ||||
| -rw-r--r-- | examples/example-outside-type/src/main.rs | 71 | ||||
| -rw-r--r-- | examples/test-examples.toml | 10 | ||||
| -rw-r--r-- | mingling/src/lib.rs | 3 | ||||
| -rw-r--r-- | mingling_macros/src/group_impl.rs | 115 | ||||
| -rw-r--r-- | mingling_macros/src/lib.rs | 53 |
9 files changed, 358 insertions, 0 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a9b6b1..f32bb60 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -180,6 +180,14 @@ This macro is only available with the `extra_macros` feature. 9. **\[mingling\]** Added `Groupped` trait to the `mingling::prelude` module, so it can now be imported via `use mingling::prelude::*` without needing to separately import the trait from the `mingling` crate root. +10. **\[macros\]** Added the `group!` macro for registering outside-types from external crates as group members without modifying their definitions. This macro generates a `Groupped` implementation and registers the type's simple name as an enum variant. + +```rust,ignore +group!(std::io::Error); +``` + +This macro is only available with the `extra_macros` feature. + #### **BREAKING CHANGES** (API CHANGES): 1. **\[core\]** Changed the signature of `ProgramSetup::setup` from `fn setup(&mut self, program: &mut Program<C>) -> S` to `fn setup(self, program: &mut Program<C>)`, consuming `self` instead of taking a mutable reference. Correspondingly, `Program::with_setup` now accepts `S` by value (`&mut self, setup: S`) instead of by mutable reference (`&mut self, setup: &mut S`). diff --git a/examples/example-outside-type/Cargo.lock b/examples/example-outside-type/Cargo.lock new file mode 100644 index 0000000..1ca10d7 --- /dev/null +++ b/examples/example-outside-type/Cargo.lock @@ -0,0 +1,76 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "example-outside-type" +version = "0.1.0" +dependencies = [ + "mingling", +] + +[[package]] +name = "just_fmt" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5454cda0d57db59778608d7a47bff5b16c6705598265869fb052b657f66cf05e" + +[[package]] +name = "mingling" +version = "0.2.0" +dependencies = [ + "mingling_core", + "mingling_macros", +] + +[[package]] +name = "mingling_core" +version = "0.2.0" +dependencies = [ + "just_fmt", +] + +[[package]] +name = "mingling_macros" +version = "0.2.0" +dependencies = [ + "just_fmt", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "syn" +version = "2.0.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" diff --git a/examples/example-outside-type/Cargo.toml b/examples/example-outside-type/Cargo.toml new file mode 100644 index 0000000..e2ca5ba --- /dev/null +++ b/examples/example-outside-type/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "example-outside-type" +version = "0.1.0" +edition = "2024" + +[dependencies.mingling] +path = "../../mingling" +features = [ + "extra_macros", +] + +[workspace] diff --git a/examples/example-outside-type/page.toml b/examples/example-outside-type/page.toml new file mode 100644 index 0000000..41e543f --- /dev/null +++ b/examples/example-outside-type/page.toml @@ -0,0 +1,10 @@ +[example] +id = "example-outside-type" +name = "Outside Type" +icon = "🆕" +category = "advanced" +desc = """ +Demonstrates how to use the `group!()` macro to convert an external type into a type recognizable by Mingling +""" +tags = ["group!", "extra_macros"] +files = ["src/main.rs", "Cargo.toml"] diff --git a/examples/example-outside-type/src/main.rs b/examples/example-outside-type/src/main.rs new file mode 100644 index 0000000..6cdd672 --- /dev/null +++ b/examples/example-outside-type/src/main.rs @@ -0,0 +1,71 @@ +//! Example: Using the `group!()` Macro to Register Outside Types +//! +//! This example demonstrates how to use the `group!()` macro to make outside +//! types (from `std` or other crates) recognizable by the Mingling framework, +//! without modifying the original type definition. +//! +//! Run: +//! ```bash +//! cargo run --manifest-path examples/example-outside-type/Cargo.toml --quiet -- parse 42 +//! cargo run --manifest-path examples/example-outside-type/Cargo.toml --quiet -- parse hello +//! ``` +//! +//! Output: +//! ```plaintext +//! Parsed number: 42 +//! Parse error: invalid digit found in string +//! ``` + +use mingling::{macros::group, prelude::*}; +use std::num::ParseIntError; + +dispatcher!("parse"); + +// --------- IMPORTANT --------- +// You can directly use the `group!` macro to define outside types as types +// recognizable by Mingling +// _____________ from std::num::ParseIntError +// / +// vvvvvvvvvvvvv +group!(ParseIntError); +// --------- IMPORTANT --------- + +pack!(ParsedNumber = i32); + +/// Parse the first argument as an `i32` +/// +/// On success, routes to `render_number`. +/// On failure, routes to `render_parse_error` via the registered outside type. +#[chain] +fn parse_number(args: EntryParse) -> Next { + let input = args.inner.first().cloned().unwrap_or_default(); + match input.parse::<i32>() { + Ok(num) => ParsedNumber::new(num).to_chain(), + Err(e) => e.to_chain(), + } +} + +/// Renderer for successful parse — displays the parsed integer. +// _____________ Using std::num::ParseIntError as a chain input +// / +#[renderer] // vvvvvvvvvvvv +fn render_number(num: ParsedNumber) { + r_println!("Parsed number: {}", *num); +} + +/// Renderer for parse errors — using the outside `ParseIntError` type. +/// +/// The `ParseIntError` type is registered via `group!` above, so it implements +/// `Groupped<ThisProgram>` and can be used directly in a `#[renderer]` function. +#[renderer] +fn render_parse_error(err: ParseIntError) { + r_println!("Parse error: {}", err); +} + +fn main() { + let mut program = ThisProgram::new(); + program.with_dispatcher(CMDParse); + program.exec_and_exit(); +} + +gen_program!(); diff --git a/examples/test-examples.toml b/examples/test-examples.toml index 4a50ab1..490361b 100644 --- a/examples/test-examples.toml +++ b/examples/test-examples.toml @@ -1,3 +1,13 @@ +[[test.example-outside-type]] +command = "parse 42" +expect.exit-code = 0 +expect.result = "Parsed number: 42" + +[[test.example-outside-type]] +command = "parse hello" +expect.exit-code = 0 +expect.result = "Parse error: invalid digit found in string" + [[test.example-lazy-resources]] command = "none" expect.exit-code = 0 diff --git a/mingling/src/lib.rs b/mingling/src/lib.rs index f5c02fa..a295af9 100644 --- a/mingling/src/lib.rs +++ b/mingling/src/lib.rs @@ -93,6 +93,9 @@ pub mod macros { pub use mingling_macros::entry; /// Used to collect data and create a command-line context pub use mingling_macros::gen_program; + /// Used to register an external type as a group member + #[cfg(feature = "extra_macros")] + pub use mingling_macros::group; /// Used to generate a struct implementing the `HelpRequest` trait via a method pub use mingling_macros::help; /// Used to create a `Node` struct via a literal diff --git a/mingling_macros/src/group_impl.rs b/mingling_macros/src/group_impl.rs new file mode 100644 index 0000000..1b765f3 --- /dev/null +++ b/mingling_macros/src/group_impl.rs @@ -0,0 +1,115 @@ +use proc_macro::TokenStream; +use quote::quote; +use syn::parse::{Parse, ParseStream}; +use syn::{Ident, Path, Result as SynResult, Token, TypePath}; + +/// Input for the `group!` macro +/// +/// # Syntax +/// +/// ```rust,ignore +/// // Explicit mode: specify both program path and type path +/// group!(crate::ThisProgram, std::io::Error); +/// +/// // Implicit mode: only type path, uses default `crate::ThisProgram` as program +/// group!(std::io::Error); +/// ``` +enum GroupInput { + Explicit { + program_path: Path, + type_path: TypePath, + }, + Implicit { + type_path: TypePath, + }, +} + +impl Parse for GroupInput { + fn parse(input: ParseStream) -> SynResult<Self> { + // Parse the first path (could be program path or type path) + let first_path: Path = input.parse()?; + + // If followed by a comma, it's explicit mode: Path, TypePath + if input.peek(Token![,]) { + input.parse::<Token![,]>()?; + let type_path: TypePath = input.parse()?; + Ok(GroupInput::Explicit { + program_path: first_path, + type_path, + }) + } else { + // Otherwise it's implicit mode: just a type path + Ok(GroupInput::Implicit { + type_path: TypePath { + qself: None, + path: first_path, + }, + }) + } + } +} + +/// Convert a type path into a valid module name segment +/// +/// e.g. `std::io::Error` -> `internal_group_std_io_error` +fn module_name_from_type(type_path: &TypePath) -> Ident { + let segments: Vec<String> = type_path + .path + .segments + .iter() + .map(|seg| seg.ident.to_string().to_lowercase()) + .collect(); + Ident::new( + &format!("internal_group_{}", segments.join("_")), + proc_macro2::Span::call_site(), + ) +} + +/// Get the last segment name of a type path (the simple type name) +/// +/// e.g. `std::io::Error` -> `Error` +fn type_simple_name(type_path: &TypePath) -> Ident { + type_path + .path + .segments + .last() + .expect("TypePath must have at least one segment") + .ident + .clone() +} + +pub fn group_macro(input: TokenStream) -> TokenStream { + let input = syn::parse_macro_input!(input as GroupInput); + + let (program_path, type_path) = match input { + GroupInput::Explicit { + program_path, + type_path, + } => (quote! { #program_path }, type_path), + GroupInput::Implicit { type_path } => (crate::default_program_path(), type_path), + }; + + // Use the type's simple name as the enum variant identifier + let type_name = type_simple_name(&type_path); + // Create a unique module name from the full type path + let module_name = module_name_from_type(&type_path); + + // Generate the module with the Groupped implementation + let expanded = quote! { + #[allow(non_camel_case_types)] + mod #module_name { + use #program_path as __MinglingProgram; + use #type_path; + + impl ::mingling::Groupped<__MinglingProgram> for #type_name { + fn member_id() -> __MinglingProgram { + __MinglingProgram::#type_name + } + } + + ::mingling::macros::register_type!(#type_name); + } + }; + + expanded.into() +} diff --git a/mingling_macros/src/lib.rs b/mingling_macros/src/lib.rs index 9880cd6..bb296ef 100644 --- a/mingling_macros/src/lib.rs +++ b/mingling_macros/src/lib.rs @@ -153,6 +153,8 @@ mod dispatcher_clap; #[cfg(feature = "extra_macros")] mod entry; mod enum_tag; +#[cfg(feature = "extra_macros")] +mod group_impl; mod groupped; mod help; mod node; @@ -230,6 +232,56 @@ pub(crate) fn check_single_segment_type( } } +/// Registers an outside-type as a member of a program group without modifying its definition. +/// +/// This macro allows you to use outside-types from external crates (like `std::io::Error`) +/// within the Mingling framework by generating a `Groupped` implementation and registering +/// the type's simple name as an enum variant. +/// +/// # Syntax +/// +/// Two forms are supported: +/// +/// ```rust,ignore +/// // Explicit mode — specify both program path and outside-type: +/// group!(crate::ThisProgram, std::io::Error); +/// +/// // Implicit mode — uses default `crate::ThisProgram` as the program: +/// group!(std::io::Error); +/// ``` +/// +/// # How it works +/// +/// The macro generates a module containing: +/// - A `use` import for the program path and the outside-type +/// - An `impl Groupped<Program>` for the outside-type +/// - A `register_type!` call with the type's simple name +/// +/// The type's simple name (e.g. `Error`) is used as the enum variant in the generated +/// program enum, just like `#[derive(Groupped)]` or `pack!`. +/// +/// # Example +/// +/// ```rust,ignore +/// use mingling::macros::group; +/// +/// // Register std::io::Error as a group member +/// group!(std::io::Error); +/// +/// // With explicit program path: +/// group!(crate::MyProgram, serde_json::Error); +/// ``` +/// +/// After expansion, the type can be used in chains and renderers like any +/// `#[derive(Groupped)]` type. +/// +/// This macro is only available with the `extra_macros` feature. +#[cfg(feature = "extra_macros")] +#[proc_macro] +pub fn group(input: TokenStream) -> TokenStream { + group_impl::group_macro(input) +} + /// Creates a `Node` from a dot-separated path string. /// /// Each segment is converted to kebab-case (unless it starts with `_`). @@ -1801,6 +1853,7 @@ pub fn program_final_gen(input: TokenStream) -> TokenStream { let expanded = quote! { #[derive(Debug, PartialEq, Eq, Clone)] #[repr(#repr_type)] + #[allow(nonstandard_style)] pub enum #name { #(#packed_types),* } |
