aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md8
-rw-r--r--examples/example-outside-type/Cargo.lock76
-rw-r--r--examples/example-outside-type/Cargo.toml12
-rw-r--r--examples/example-outside-type/page.toml10
-rw-r--r--examples/example-outside-type/src/main.rs71
-rw-r--r--examples/test-examples.toml10
-rw-r--r--mingling/src/lib.rs3
-rw-r--r--mingling_macros/src/group_impl.rs115
-rw-r--r--mingling_macros/src/lib.rs53
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),*
}