From d3177c386d739a3dc0e06890ad8f14713ef44ee0 Mon Sep 17 00:00:00 2001 From: 魏曹先生 <1992414357@qq.com> Date: Sat, 20 Jun 2026 01:30:10 +0800 Subject: Add `group!` macro for registering external types --- mingling_macros/src/group_impl.rs | 115 ++++++++++++++++++++++++++++++++++++++ mingling_macros/src/lib.rs | 53 ++++++++++++++++++ 2 files changed, 168 insertions(+) create mode 100644 mingling_macros/src/group_impl.rs (limited to 'mingling_macros/src') 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 { + // 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::()?; + 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 = 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` 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),* } -- cgit