aboutsummaryrefslogtreecommitdiff
path: root/mingling_macros
diff options
context:
space:
mode:
Diffstat (limited to 'mingling_macros')
-rw-r--r--mingling_macros/Cargo.toml2
-rw-r--r--mingling_macros/src/attr.rs5
-rw-r--r--mingling_macros/src/attr/command.rs365
-rw-r--r--mingling_macros/src/attr/help.rs57
-rw-r--r--mingling_macros/src/attr/mlint.rs7
-rw-r--r--mingling_macros/src/derive.rs2
-rw-r--r--mingling_macros/src/derive/grouped.rs12
-rw-r--r--mingling_macros/src/derive/structural_data.rs26
-rw-r--r--mingling_macros/src/extensions.rs4
-rw-r--r--mingling_macros/src/func.rs33
-rw-r--r--mingling_macros/src/func/dispatcher.rs108
-rw-r--r--mingling_macros/src/func/empty_result.rs11
-rw-r--r--mingling_macros/src/func/gen_program.rs670
-rw-r--r--mingling_macros/src/func/group.rs6
-rw-r--r--mingling_macros/src/func/group_structural.rs124
-rw-r--r--mingling_macros/src/func/pack.rs6
-rw-r--r--mingling_macros/src/func/pack_err.rs102
-rw-r--r--mingling_macros/src/func/pack_err_structural.rs119
-rw-r--r--mingling_macros/src/func/pack_structural.rs168
-rw-r--r--mingling_macros/src/func/program_comp_gen.rs80
-rw-r--r--mingling_macros/src/func/program_fallback_gen.rs23
-rw-r--r--mingling_macros/src/func/program_final_gen.rs373
-rw-r--r--mingling_macros/src/func/r_append.rs29
-rw-r--r--mingling_macros/src/func/r_eprint.rs7
-rw-r--r--mingling_macros/src/func/r_eprintln.rs7
-rw-r--r--mingling_macros/src/func/r_print.rs52
-rw-r--r--mingling_macros/src/func/r_println.rs7
-rw-r--r--mingling_macros/src/func/register_chain.rs7
-rw-r--r--mingling_macros/src/func/register_dispatcher.rs75
-rw-r--r--mingling_macros/src/func/register_help.rs71
-rw-r--r--mingling_macros/src/func/register_renderer.rs7
-rw-r--r--mingling_macros/src/func/register_type.rs17
-rw-r--r--mingling_macros/src/func/render_route.rs15
-rw-r--r--mingling_macros/src/func/route.rs16
-rw-r--r--mingling_macros/src/func/suggest.rs21
-rw-r--r--mingling_macros/src/func/suggest_enum.rs24
-rw-r--r--mingling_macros/src/lib.rs205
-rw-r--r--mingling_macros/src/systems/dispatch_tree_gen.rs43
-rw-r--r--mingling_macros/src/systems/structural_data.rs342
39 files changed, 1887 insertions, 1361 deletions
diff --git a/mingling_macros/Cargo.toml b/mingling_macros/Cargo.toml
index 1191015..137213a 100644
--- a/mingling_macros/Cargo.toml
+++ b/mingling_macros/Cargo.toml
@@ -25,7 +25,7 @@ structural_renderer = []
repl = []
pathf = []
-extra_macros = []
+extras = []
[dependencies]
syn.workspace = true
diff --git a/mingling_macros/src/attr.rs b/mingling_macros/src/attr.rs
index e4cd826..54fe2f1 100644
--- a/mingling_macros/src/attr.rs
+++ b/mingling_macros/src/attr.rs
@@ -1,9 +1,12 @@
pub(crate) mod chain;
+#[cfg(feature = "extras")]
+pub(crate) mod command;
#[cfg(feature = "comp")]
pub(crate) mod completion;
#[cfg(feature = "clap")]
pub(crate) mod dispatcher_clap;
pub(crate) mod help;
-#[cfg(feature = "extra_macros")]
+pub(crate) mod mlint;
+#[cfg(feature = "extras")]
pub(crate) mod program_setup;
pub(crate) mod renderer;
diff --git a/mingling_macros/src/attr/command.rs b/mingling_macros/src/attr/command.rs
new file mode 100644
index 0000000..35c5c30
--- /dev/null
+++ b/mingling_macros/src/attr/command.rs
@@ -0,0 +1,365 @@
+use proc_macro::TokenStream;
+use proc_macro2::TokenStream as TokenStream2;
+use quote::quote;
+use syn::parse::{Parse, ParseStream};
+use syn::parse_macro_input;
+use syn::spanned::Spanned;
+use syn::token::Comma;
+use syn::{FnArg, Ident, ItemFn, LitStr, PatType, Token, Type};
+
+/// Parsed arguments for `#[command(...)]`.
+///
+/// Supports:
+/// - `node = "dot.separated.path"` — explicit command path
+/// - `name = CMDName` — explicit CMD struct name
+/// - `entry = EntryName` — explicit Entry struct name
+/// - bare paths like `routeify`, `::mingling::macros::routeify` — extension attrs for the original fn
+struct CommandArgs {
+ node: Option<LitStr>,
+ name: Option<Ident>,
+ entry: Option<Ident>,
+ exts: Vec<syn::Path>,
+}
+
+impl Parse for CommandArgs {
+ fn parse(input: ParseStream) -> syn::Result<Self> {
+ let mut node = None;
+ let mut name = None;
+ let mut entry = None;
+ let mut exts = Vec::new();
+
+ while !input.is_empty() {
+ if input.peek(Ident) && input.peek2(Token![=]) {
+ // key = value pair
+ let key: Ident = input.parse()?;
+ input.parse::<Token![=]>()?;
+
+ if key == "node" {
+ if node.is_some() {
+ return Err(input.error("duplicate `node` argument"));
+ }
+ node = Some(input.parse()?);
+ } else if key == "name" {
+ if name.is_some() {
+ return Err(input.error("duplicate `name` argument"));
+ }
+ name = Some(input.parse()?);
+ } else if key == "entry" {
+ if entry.is_some() {
+ return Err(input.error("duplicate `entry` argument"));
+ }
+ entry = Some(input.parse()?);
+ } else {
+ return Err(input.error(format!(
+ "unknown key `{}`; expected `node`, `name`, or `entry`",
+ key
+ )));
+ }
+ } else {
+ // Extension path (e.g. `routeify` or `::mingling::macros::routeify`)
+ let ext: syn::Path = input.parse()?;
+ exts.push(ext);
+ }
+
+ // Skip optional trailing comma
+ if input.peek(Comma) {
+ let _ = input.parse::<Comma>();
+ }
+ }
+
+ Ok(CommandArgs {
+ node,
+ name,
+ entry,
+ exts,
+ })
+ }
+}
+
+/// Returns the default node path as a dot-separated string, derived from the function name.
+///
+/// Example: `greet_someone` -> `"greet.someone"`, `greet` -> `"greet"`
+fn default_node_from_fn(fn_name: &Ident) -> String {
+ just_fmt::dot_case!(fn_name.to_string())
+}
+
+/// Checks basic function constraints for `#[command]`.
+/// Returns `Err(compile_error token stream)` on failure.
+fn validate_function(f: &ItemFn) -> Result<(), TokenStream2> {
+ if f.sig
+ .inputs
+ .iter()
+ .any(|arg| matches!(arg, FnArg::Receiver(_)))
+ {
+ return Err(syn::Error::new(
+ f.sig.span(),
+ "#[command] function cannot have a `self` parameter",
+ )
+ .to_compile_error());
+ }
+
+ Ok(())
+}
+
+/// Returns `(wrapper_async_token, await_call)` for the chain wrapper.
+///
+/// When the original function is async and the `async` feature is enabled,
+/// the wrapper needs to be `async` and call `.await` on the original function.
+/// Without the feature, async functions are rejected.
+fn handle_async(f: &ItemFn) -> Result<(TokenStream2, TokenStream2), TokenStream2> {
+ let is_async = f.sig.asyncness.is_some();
+
+ #[cfg(not(feature = "async"))]
+ if is_async {
+ return Err(syn::Error::new(
+ f.sig.span(),
+ "#[command] function cannot be async when the `async` feature is disabled",
+ )
+ .to_compile_error());
+ }
+
+ let wrapper_async = is_async.then_some(quote! { async }).unwrap_or_default();
+ let await_call = is_async.then_some(quote! { .await }).unwrap_or_default();
+ Ok((wrapper_async, await_call))
+}
+
+/// All resolved identifiers derived from `#[command]` arguments + function name.
+struct ResolvedNames {
+ /// `node_str` as a string literal token
+ node_lit: LitStr,
+ /// Whether the user supplied any explicit override (node/name/entry)
+ has_overrides: bool,
+ /// CMD struct name (e.g. `CMDGreet`)
+ cmd_name: Ident,
+ /// Entry struct name (e.g. `EntryGreet`)
+ entry_type: Ident,
+ /// Chain wrapper function name (e.g. `__command_chain_greet`)
+ chain_fn_name: Ident,
+}
+
+/// Resolves `node`, `cmd_name`, `entry_type`, and `chain_fn_name` from
+/// the attribute args and the original function name.
+fn resolve_names(fn_name: &Ident, args: &CommandArgs) -> ResolvedNames {
+ let fn_name_str = fn_name.to_string();
+
+ let node_str = match &args.node {
+ Some(lit) => lit.value(),
+ None => default_node_from_fn(fn_name),
+ };
+ let node_lit = syn::LitStr::new(&node_str, fn_name.span());
+
+ let has_overrides = args.node.is_some() || args.name.is_some() || args.entry.is_some();
+
+ let cmd_name = args.name.clone().unwrap_or_else(|| {
+ let pascal = just_fmt::pascal_case!(&node_str);
+ Ident::new(&format!("CMD{pascal}"), fn_name.span())
+ });
+
+ let entry_type = args.entry.clone().unwrap_or_else(|| {
+ let pascal = just_fmt::pascal_case!(&node_str);
+ Ident::new(&format!("Entry{pascal}"), fn_name.span())
+ });
+
+ let chain_fn_name = Ident::new(&format!("__command_chain_{}", fn_name_str), fn_name.span());
+
+ ResolvedNames {
+ node_lit,
+ has_overrides,
+ cmd_name,
+ entry_type,
+ chain_fn_name,
+ }
+}
+
+/// Converts extension paths into `#[ext]` attribute token streams.
+fn build_ext_attrs(exts: &[syn::Path]) -> Vec<TokenStream2> {
+ exts.iter().map(|ext| quote! { #[#ext] }).collect()
+}
+
+/// Returns `true` if the function has a first non-reference (owned) parameter that
+/// serves as the "args" input.
+fn has_args_param(sig: &syn::Signature) -> bool {
+ sig.inputs.first().map_or(false, |arg| {
+ if let FnArg::Typed(pat_type) = arg {
+ !matches!(&*pat_type.ty, Type::Reference(_))
+ } else {
+ false
+ }
+ })
+}
+
+/// Builds the wrapper function's parameter list.
+///
+/// - If the function has an "args" param (first non-reference): replaces its type
+/// with the entry type (e.g. `Vec<String>` → `EntryGreet`).
+/// - If not (no params, or first param is a reference): inserts a new entry param
+/// at the front to satisfy `#[chain]`'s requirement for an owned first parameter.
+fn build_wrapper_params(
+ sig: &syn::Signature,
+ entry_type: &Ident,
+) -> syn::punctuated::Punctuated<FnArg, syn::token::Comma> {
+ if has_args_param(sig) {
+ // First param is owned (args) -> replace its type with entry type
+ let mut params = sig.inputs.clone();
+ if let Some(FnArg::Typed(first)) = params.first_mut() {
+ first.ty = Box::new(Type::Path(syn::TypePath {
+ qself: None,
+ path: syn::Path::from(entry_type.clone()),
+ }));
+ }
+ params
+ } else {
+ // No args param -> insert an entry param at the front, keep rest as-is
+ let mut params = syn::punctuated::Punctuated::new();
+ let entry_param: FnArg = syn::parse_quote! { _args: #entry_type };
+ params.push(entry_param);
+ for arg in sig.inputs.iter() {
+ params.push(arg.clone());
+ }
+ params
+ }
+}
+
+/// Builds call arguments for the original function call inside the wrapper.
+///
+/// - If the function has an "args" param (first non-reference): first arg gets
+/// `.into()` (converts `EntryGreet` → `Vec<String>`), rest pass through as-is.
+/// - If not (no args): all params are resources — pass none, return empty.
+fn build_call_args(sig: &syn::Signature) -> Vec<TokenStream2> {
+ let has_args = has_args_param(sig);
+ sig.inputs
+ .iter()
+ .enumerate()
+ .map(|(i, arg)| {
+ let pat = match arg {
+ FnArg::Typed(PatType { pat, .. }) => quote! { #pat },
+ FnArg::Receiver(_) => unreachable!(),
+ };
+ if has_args && i == 0 {
+ quote! { #pat.into() }
+ } else {
+ pat
+ }
+ })
+ .collect()
+}
+
+/// Generates the `dispatcher!(...)` call.
+///
+/// - No overrides → abbreviated form: `dispatcher!("node")`
+/// - Any override → explicit form: `dispatcher!("node", CMDName => EntryName)`
+fn build_dispatcher_invoke(names: &ResolvedNames) -> TokenStream2 {
+ let node_lit = &names.node_lit;
+ if names.has_overrides {
+ let cmd_name = &names.cmd_name;
+ let entry_type = &names.entry_type;
+ quote! { ::mingling::macros::dispatcher!(#node_lit, #cmd_name => #entry_type); }
+ } else {
+ quote! { ::mingling::macros::dispatcher!(#node_lit); }
+ }
+}
+
+pub(crate) fn command_attr(attr: TokenStream, item: TokenStream) -> TokenStream {
+ let input_fn = parse_macro_input!(item as ItemFn);
+
+ // validation
+ if let Err(err) = validate_function(&input_fn) {
+ return err.into();
+ }
+
+ // parse attribute
+ let args: CommandArgs = if attr.is_empty() {
+ CommandArgs {
+ node: None,
+ name: None,
+ entry: None,
+ exts: Vec::new(),
+ }
+ } else {
+ parse_macro_input!(attr as CommandArgs)
+ };
+
+ // async handling
+ let (wrapper_async, await_call) = match handle_async(&input_fn) {
+ Ok(pair) => pair,
+ Err(err) => return err.into(),
+ };
+
+ // resolve node / name / entry
+ let fn_name = &input_fn.sig.ident;
+ let names = resolve_names(fn_name, &args);
+ let chain_fn_name = &names.chain_fn_name;
+
+ // build extension attributes (applied to the ORIGINAL function)
+ let ext_attrs = build_ext_attrs(&args.exts);
+
+ // build wrapper
+ let wrapper_params = build_wrapper_params(&input_fn.sig, &names.entry_type);
+ let call_args = build_call_args(&input_fn.sig);
+
+ // build dispatcher invocation
+ let dispatcher_invoke = build_dispatcher_invoke(&names);
+
+ // preserve original function
+ let mut fn_attrs = input_fn.attrs.clone();
+ fn_attrs.retain(|attr| !attr.path().is_ident("command"));
+
+ let vis = &input_fn.vis;
+ let asyncness = input_fn.sig.asyncness;
+ let generics = &input_fn.sig.generics;
+ let orig_params = &input_fn.sig.inputs;
+ let orig_return = &input_fn.sig.output;
+ let fn_body = &input_fn.block;
+
+ // compute names for the re‑export module and internal structs
+ let fn_name_s = fn_name.to_string();
+ let mod_name = Ident::new(&format!("__command_{}_module", &fn_name_s), fn_name.span());
+ let wrapper_full = format!("__command_chain_{}", &fn_name_s);
+ let snaked_wrapper = just_fmt::snake_case!(wrapper_full);
+ let chain_internal = Ident::new(
+ &format!("__internal_chain_{}", snaked_wrapper),
+ fn_name.span(),
+ );
+
+ // dispatcher internal static (only exists with dispatch_tree feature)
+ #[cfg(feature = "dispatch_tree")]
+ let snaked_node = just_fmt::snake_case!(names.node_lit.value());
+ #[cfg(feature = "dispatch_tree")]
+ let dispatcher_internal = {
+ let ident = Ident::new(
+ &format!("__internal_dispatcher_{}", snaked_node),
+ fn_name.span(),
+ );
+ quote! { #vis use super::#ident; }
+ };
+ #[cfg(not(feature = "dispatch_tree"))]
+ let dispatcher_internal = quote! {};
+
+ let cmd_name = &names.cmd_name;
+ let entry_type = &names.entry_type;
+
+ // assemble output
+ let expanded = quote! {
+ #dispatcher_invoke
+
+ #[::mingling::macros::chain]
+ #vis #wrapper_async fn #chain_fn_name #generics(#wrapper_params) -> crate::Next {
+ #fn_name(#(#call_args),*)#await_call.into()
+ }
+
+ #(#fn_attrs)*
+ #(#ext_attrs)*
+ #vis #asyncness fn #fn_name #generics(#orig_params) #orig_return #fn_body
+
+ // hidden module gathering all generated types for pathf / external access
+ #[doc(hidden)]
+ #vis mod #mod_name {
+ #vis use super::#cmd_name;
+ #vis use super::#entry_type;
+ #vis use super::#chain_internal;
+ #dispatcher_internal
+ }
+ };
+
+ expanded.into()
+}
diff --git a/mingling_macros/src/attr/help.rs b/mingling_macros/src/attr/help.rs
index aa7bc88..b4ad989 100644
--- a/mingling_macros/src/attr/help.rs
+++ b/mingling_macros/src/attr/help.rs
@@ -1,5 +1,5 @@
use proc_macro::TokenStream;
-use quote::{ToTokens, quote};
+use quote::quote;
use syn::spanned::Spanned;
use syn::{Ident, ItemFn, Pat, ReturnType, Signature, TypePath, parse_macro_input};
@@ -154,7 +154,7 @@ pub(crate) fn help_attr(item: TokenStream) -> TokenStream {
::mingling::macros::register_help!(#entry_type, #struct_name);
- // Keep the original function unchanged
+ // Keep the original function unchanged
#(#fn_attrs)*
#vis fn #fn_name(#original_inputs) -> #original_return_type {
#(#fn_body_stmts)*
@@ -176,56 +176,3 @@ fn build_help_entry(struct_name: &Ident, entry_type: &TypePath) -> proc_macro2::
}
}
}
-
-pub(crate) fn register_help(input: TokenStream) -> TokenStream {
- // Parse the input as a comma-separated list of arguments
- let input_parsed = syn::parse_macro_input!(input with syn::punctuated::Punctuated<syn::Expr, syn::Token![,]>::parse_terminated);
-
- // Check if there are exactly two elements
- if input_parsed.len() != 2 {
- return syn::Error::new(
- input_parsed.span(),
- "Expected exactly two comma-separated arguments: `EntryType, StructName`",
- )
- .to_compile_error()
- .into();
- }
-
- // Extract the two elements
- let entry_type_expr = &input_parsed[0];
- let struct_name_expr = &input_parsed[1];
-
- // Convert expressions to TypePath and Ident
- let entry_type = match syn::parse2::<TypePath>(entry_type_expr.to_token_stream()) {
- Ok(ty) => ty,
- Err(e) => return e.to_compile_error().into(),
- };
-
- let struct_name = match syn::parse2::<syn::Ident>(struct_name_expr.to_token_stream()) {
- Ok(ident) => ident,
- Err(e) => return e.to_compile_error().into(),
- };
-
- // Register the help request mapping
- let help_entry = build_help_entry(&struct_name, &entry_type);
- let entry_str = help_entry.to_string();
-
- // Check if entry was already pre-inserted by `#[help]` attribute
- let mut helps = get_global_set(&crate::HELP_REQUESTS).lock().unwrap();
- if helps.contains(&entry_str) {
- // Already registered by `#[help]`, no duplicate check needed
- return quote! {}.into();
- }
-
- // Check for duplicate variant (different struct, same type)
- let variant_name = entry_type.path.segments.last().unwrap().ident.to_string();
- if let Err(err) =
- crate::check_duplicate_variant(&helps, &entry_str, &variant_name, "help", entry_type.span())
- {
- return err.into();
- }
-
- helps.insert(entry_str);
-
- quote! {}.into()
-}
diff --git a/mingling_macros/src/attr/mlint.rs b/mingling_macros/src/attr/mlint.rs
new file mode 100644
index 0000000..176ef0a
--- /dev/null
+++ b/mingling_macros/src/attr/mlint.rs
@@ -0,0 +1,7 @@
+use proc_macro::TokenStream;
+
+/// Marker attribute for the Mingling lint system.
+/// All it does is pass the item through unchanged.
+pub(crate) fn mlint(_attr: TokenStream, item: TokenStream) -> TokenStream {
+ item
+}
diff --git a/mingling_macros/src/derive.rs b/mingling_macros/src/derive.rs
index ffa405b..666a36e 100644
--- a/mingling_macros/src/derive.rs
+++ b/mingling_macros/src/derive.rs
@@ -1,2 +1,4 @@
pub(crate) mod enum_tag;
pub(crate) mod grouped;
+#[cfg(feature = "structural_renderer")]
+pub(crate) mod structural_data;
diff --git a/mingling_macros/src/derive/grouped.rs b/mingling_macros/src/derive/grouped.rs
index 307aab6..a00eea1 100644
--- a/mingling_macros/src/derive/grouped.rs
+++ b/mingling_macros/src/derive/grouped.rs
@@ -16,7 +16,11 @@ pub(crate) fn derive_grouped(input: TokenStream) -> TokenStream {
let expanded = quote! {
::mingling::macros::register_type!(#struct_name);
- impl ::mingling::Grouped<#group_ident> for #struct_name {
+ /// SAFETY: This is an internal implementation of the `Grouped` derive macro,
+ /// guaranteeing that the enum value registered by the `register_type!` macro
+ /// is exactly the same as the actual return value,
+ /// which can be confirmed via the `Ident` in the `quote!` block.
+ unsafe impl ::mingling::Grouped<#group_ident> for #struct_name {
fn member_id() -> #group_ident {
#group_ident::#struct_name
}
@@ -46,7 +50,11 @@ pub fn derive_grouped_serialize(input: TokenStream) -> TokenStream {
::mingling::macros::register_type!(#struct_name);
- impl ::mingling::Grouped<#group_ident> for #struct_name {
+ /// SAFETY: This is an internal implementation of the `Grouped` derive macro,
+ /// guaranteeing that the enum value registered by the `register_type!` macro
+ /// is exactly the same as the actual return value,
+ /// which can be confirmed via the `Ident` in the `quote!` block.
+ unsafe impl ::mingling::Grouped<#group_ident> for #struct_name {
fn member_id() -> #group_ident {
#group_ident::#struct_name
}
diff --git a/mingling_macros/src/derive/structural_data.rs b/mingling_macros/src/derive/structural_data.rs
new file mode 100644
index 0000000..acb2f28
--- /dev/null
+++ b/mingling_macros/src/derive/structural_data.rs
@@ -0,0 +1,26 @@
+use proc_macro::TokenStream;
+use quote::quote;
+use syn::{DeriveInput, parse_macro_input};
+
+use crate::get_global_set;
+
+/// Derive macro for `StructuralData`.
+pub(crate) fn derive_structural_data(input: TokenStream) -> TokenStream {
+ let input = parse_macro_input!(input as DeriveInput);
+ let type_name = input.ident;
+
+ // Register in STRUCTURED_TYPES
+ let type_name_str = type_name.to_string();
+ get_global_set(&crate::STRUCTURED_TYPES)
+ .lock()
+ .unwrap()
+ .insert(type_name_str);
+
+ // Generate BOTH the sealed impl AND the StructuralData impl.
+ let expanded = quote! {
+ impl ::mingling::__private::StructuralDataSealed<crate::ThisProgram> for #type_name {}
+ impl ::mingling::__private::StructuralData<crate::ThisProgram> for #type_name {}
+ };
+
+ expanded.into()
+}
diff --git a/mingling_macros/src/extensions.rs b/mingling_macros/src/extensions.rs
index c2fdb30..f4a6fde 100644
--- a/mingling_macros/src/extensions.rs
+++ b/mingling_macros/src/extensions.rs
@@ -10,11 +10,11 @@ use syn::parse::{Parse, ParseStream};
use syn::{Ident, Token};
/// Extension: `#[routeify]` — transforms `expr?` into `route!(expr)`.
-#[cfg(feature = "extra_macros")]
+#[cfg(feature = "extras")]
pub(crate) mod routeify;
/// Extension: `#[renderify]` — transforms `expr?` into `render_route!(expr)`.
-#[cfg(feature = "extra_macros")]
+#[cfg(feature = "extras")]
pub(crate) mod renderify;
/// Extension: `#[buffer]` — wraps a unit-returning function to return `RenderResult`.
diff --git a/mingling_macros/src/func.rs b/mingling_macros/src/func.rs
index 33bb094..9e0e15f 100644
--- a/mingling_macros/src/func.rs
+++ b/mingling_macros/src/func.rs
@@ -1,13 +1,40 @@
pub(crate) mod dispatcher;
-#[cfg(feature = "extra_macros")]
+#[cfg(feature = "extras")]
+pub(crate) mod empty_result;
+#[cfg(feature = "extras")]
pub(crate) mod entry;
pub(crate) mod gen_program;
-#[cfg(feature = "extra_macros")]
+#[cfg(feature = "extras")]
pub(crate) mod group;
+#[cfg(all(feature = "structural_renderer", feature = "extras"))]
+pub(crate) mod group_structural;
pub(crate) mod node;
pub(crate) mod pack;
-#[cfg(feature = "extra_macros")]
+#[cfg(feature = "extras")]
pub(crate) mod pack_err;
+#[cfg(all(feature = "structural_renderer", feature = "extras"))]
+pub(crate) mod pack_err_structural;
+#[cfg(feature = "structural_renderer")]
+pub(crate) mod pack_structural;
+#[cfg(feature = "comp")]
+pub(crate) mod program_comp_gen;
+pub(crate) mod program_fallback_gen;
+pub(crate) mod program_final_gen;
+pub(crate) mod r_append;
+pub(crate) mod r_eprint;
+pub(crate) mod r_eprintln;
pub(crate) mod r_print;
+pub(crate) mod r_println;
+pub(crate) mod register_chain;
+pub(crate) mod register_dispatcher;
+pub(crate) mod register_help;
+pub(crate) mod register_renderer;
+pub(crate) mod register_type;
+#[cfg(feature = "extras")]
+pub(crate) mod render_route;
+#[cfg(feature = "extras")]
+pub(crate) mod route;
#[cfg(feature = "comp")]
pub(crate) mod suggest;
+#[cfg(feature = "comp")]
+pub(crate) mod suggest_enum;
diff --git a/mingling_macros/src/func/dispatcher.rs b/mingling_macros/src/func/dispatcher.rs
index a61dd26..a9e2464 100644
--- a/mingling_macros/src/func/dispatcher.rs
+++ b/mingling_macros/src/func/dispatcher.rs
@@ -1,13 +1,8 @@
-#[cfg(feature = "dispatch_tree")]
-use just_fmt::snake_case;
use proc_macro::TokenStream;
use proc_macro2::TokenStream as TokenStream2;
use quote::quote;
use syn::parse::{Parse, ParseStream};
-use syn::{Attribute, Ident, LitStr, Result as SynResult, Token};
-
-#[cfg(feature = "dispatch_tree")]
-use crate::COMPILE_TIME_DISPATCHERS;
+use syn::{Attribute, Ident, LitStr, Token};
enum DispatcherChainInput {
Default {
@@ -17,7 +12,7 @@ enum DispatcherChainInput {
command_struct: Ident,
pack: Ident,
},
- #[cfg(feature = "extra_macros")]
+ #[cfg(feature = "extras")]
Auto {
cmd_attrs: Vec<Attribute>,
command_name: syn::LitStr,
@@ -25,7 +20,7 @@ enum DispatcherChainInput {
}
impl Parse for DispatcherChainInput {
- fn parse(input: ParseStream) -> SynResult<Self> {
+ fn parse(input: ParseStream) -> syn::Result<Self> {
// Collect outer attributes for the CMD struct
let cmd_attrs = input.call(Attribute::parse_outer)?;
@@ -35,14 +30,14 @@ impl Parse for DispatcherChainInput {
// Check if this is the abbreviated form: just "command_name" without ", CMD => Entry"
if input.is_empty() {
- #[cfg(feature = "extra_macros")]
+ #[cfg(feature = "extras")]
{
return Ok(DispatcherChainInput::Auto {
cmd_attrs,
command_name,
});
}
- #[cfg(not(feature = "extra_macros"))]
+ #[cfg(not(feature = "extras"))]
{
return Err(syn::Error::new(
command_name.span(),
@@ -79,7 +74,7 @@ pub(crate) fn dispatcher(input: TokenStream) -> TokenStream {
// Parse the input
let dispatcher_input = syn::parse_macro_input!(input as DispatcherChainInput);
- #[cfg(not(feature = "extra_macros"))]
+ #[cfg(not(feature = "extras"))]
let (command_name, command_struct, pack, cmd_attrs, entry_attrs) = match dispatcher_input {
DispatcherChainInput::Default {
cmd_attrs,
@@ -90,7 +85,7 @@ pub(crate) fn dispatcher(input: TokenStream) -> TokenStream {
} => (command_name, command_struct, pack, cmd_attrs, entry_attrs),
};
- #[cfg(feature = "extra_macros")]
+ #[cfg(feature = "extras")]
let (command_name, command_struct, pack, cmd_attrs, entry_attrs) = match dispatcher_input {
DispatcherChainInput::Default {
cmd_attrs,
@@ -104,7 +99,7 @@ pub(crate) fn dispatcher(input: TokenStream) -> TokenStream {
command_name,
} => {
let command_name_str = command_name.value();
- let pascal = dotted_to_pascal_case(&command_name_str);
+ let pascal = just_fmt::pascal_case!(&command_name_str);
let command_struct = Ident::new(&format!("CMD{pascal}"), command_name.span());
let pack = Ident::new(&format!("Entry{pascal}"), command_name.span());
(command_name, command_struct, pack, cmd_attrs, Vec::new())
@@ -126,6 +121,12 @@ pub(crate) fn dispatcher(input: TokenStream) -> TokenStream {
::mingling::macros::pack!(#(#entry_attrs)* #pack = Vec<String>);
+ impl From<#pack> for crate::Entry {
+ fn from(value: #pack) -> Self {
+ crate::Entry::new(value.inner)
+ }
+ }
+
#comp_entry
#dispatch_tree_entry
@@ -183,84 +184,3 @@ fn get_dispatch_tree_entry(
) -> TokenStream2 {
quote! {}
}
-
-#[cfg(feature = "dispatch_tree")]
-/// Input format: ("node.name", DispatcherType, EntryName)
-struct RegisterDispatcherInput {
- node_name: syn::LitStr,
- dispatcher_type: Ident,
- entry_name: Ident,
-}
-
-#[cfg(feature = "dispatch_tree")]
-impl Parse for RegisterDispatcherInput {
- fn parse(input: ParseStream) -> SynResult<Self> {
- let node_name = input.parse()?;
- input.parse::<Token![,]>()?;
- let dispatcher_type = input.parse()?;
- input.parse::<Token![,]>()?;
- let entry_name = input.parse()?;
- Ok(RegisterDispatcherInput {
- node_name,
- dispatcher_type,
- entry_name,
- })
- }
-}
-
-#[cfg(feature = "dispatch_tree")]
-pub(crate) fn register_dispatcher(input: TokenStream) -> TokenStream {
- let RegisterDispatcherInput {
- node_name,
- dispatcher_type,
- entry_name,
- } = syn::parse_macro_input!(input as RegisterDispatcherInput);
-
- let node_name_str = node_name.value();
- let static_name = format!(
- "__internal_dispatcher_{}",
- snake_case!(node_name_str.clone())
- );
- let static_ident = Ident::new(&static_name, proc_macro2::Span::call_site());
-
- // Register node info in the global collection at compile time
- // Format: "node.name:DispatcherType:EntryName"
- crate::get_global_set(&COMPILE_TIME_DISPATCHERS)
- .lock()
- .unwrap()
- .insert(format!(
- "{}:{}:{}",
- node_name_str, dispatcher_type, entry_name
- ));
-
- let expanded = quote! {
- #[doc(hidden)]
- #[allow(nonstandard_style)]
- pub static #static_ident: #dispatcher_type = #dispatcher_type;
- };
-
- expanded.into()
-}
-
-#[cfg(not(feature = "dispatch_tree"))]
-pub(crate) fn register_dispatcher(_input: TokenStream) -> TokenStream {
- quote! {}.into()
-}
-
-/// Converts a dotted command name (e.g. "remote.add") to `PascalCase` (e.g. "`RemoteAdd`").
-///
-/// Each segment is split by `.`, the first character of each segment is uppercased,
-/// and the segments are joined. This is used by the abbreviated `dispatcher!` syntax
-/// (when `Command => Entry` is omitted) to auto-derive struct names.
-#[cfg(feature = "extra_macros")]
-fn dotted_to_pascal_case(s: &str) -> String {
- s.split('.')
- .map(|segment| {
- let mut chars = segment.chars();
- match chars.next() {
- None => String::new(),
- Some(c) => c.to_uppercase().to_string() + chars.as_str(),
- }
- })
- .collect()
-}
diff --git a/mingling_macros/src/func/empty_result.rs b/mingling_macros/src/func/empty_result.rs
new file mode 100644
index 0000000..9b5c78c
--- /dev/null
+++ b/mingling_macros/src/func/empty_result.rs
@@ -0,0 +1,11 @@
+use proc_macro::TokenStream;
+use quote::quote;
+
+/// Creates an empty result value wrapped in `ChainProcess` for early return
+/// from a chain function.
+pub(crate) fn empty_result(_input: TokenStream) -> TokenStream {
+ let expanded = quote! {
+ <crate::ResultEmpty as ::mingling::Routable::<crate::ThisProgram>>::to_chain(crate::ResultEmpty)
+ };
+ TokenStream::from(expanded)
+}
diff --git a/mingling_macros/src/func/gen_program.rs b/mingling_macros/src/func/gen_program.rs
index de4da06..d50041e 100644
--- a/mingling_macros/src/func/gen_program.rs
+++ b/mingling_macros/src/func/gen_program.rs
@@ -1,90 +1,11 @@
-use std::collections::HashMap;
-
use proc_macro::TokenStream;
use quote::quote;
-use syn::parse_macro_input;
-
-use crate::CHAINS;
-use crate::CHAINS_EXIST;
-#[cfg(feature = "dispatch_tree")]
-use crate::COMPILE_TIME_DISPATCHERS;
-#[cfg(feature = "comp")]
-use crate::COMPLETIONS;
-use crate::HELP_REQUESTS;
-use crate::PACKED_TYPES;
-use crate::RENDERERS;
-use crate::RENDERERS_EXIST;
-#[cfg(feature = "structural_renderer")]
-use crate::STRUCTURAL_RENDERERS;
-use crate::attr::{chain, renderer};
-use crate::get_global_set;
-#[cfg(feature = "dispatch_tree")]
-use crate::systems::dispatch_tree_gen;
-
-#[cfg(feature = "async")]
-const ASYNC_ENABLED: bool = true;
-#[cfg(not(feature = "async"))]
-const ASYNC_ENABLED: bool = false;
-
-/// Parses an entry of the format `StructName => EnumVariant,` into a pair of idents.
-fn parse_entry_pair(entry: &proc_macro2::TokenStream) -> (proc_macro2::Ident, proc_macro2::Ident) {
- let s = entry.to_string();
- let arrow_idx = s
- .find("=>")
- .unwrap_or_else(|| panic!("Entry missing '=>': {s}"));
- let struct_str = s[..arrow_idx].trim();
- let variant_str = s[arrow_idx + 2..].trim().trim_end_matches(',').trim();
- let struct_ident = proc_macro2::Ident::new(struct_str, proc_macro2::Span::call_site());
- let variant_ident = proc_macro2::Ident::new(variant_str, proc_macro2::Span::call_site());
- (struct_ident, variant_ident)
-}
-
-/// Loads the pathf type mapping from `$OUT_DIR/{crate}/type_using.rs`.
-/// Always compiled; returns None when pathf feature is not enabled,
-/// or when the file does not exist / fails to load.
-fn load_pathf_map() -> Option<std::collections::HashMap<String, String>> {
- if !cfg!(feature = "pathf") {
- return None;
- }
- let out_dir = std::env::var("OUT_DIR").ok()?;
- let crate_name = std::env::var("CARGO_PKG_NAME").ok()?;
- let path = std::path::Path::new(&out_dir)
- .join(&crate_name)
- .join("type_using.rs");
- let content = std::fs::read_to_string(&path).ok()?;
- Some(
- content
- .lines()
- .filter_map(|line| {
- let line = line.trim();
- if let Some(rest) = line.strip_prefix("use ") {
- let path = rest.strip_suffix(';').unwrap_or(rest);
- if let Some((_mod, type_name)) = path.rsplit_once("::") {
- return Some((type_name.to_string(), path.to_string()));
- }
- }
- None
- })
- .collect(),
- )
-}
-
-/// Resolves a type name to its full path token stream using the pathf mapping.
-pub(crate) fn resolve_type(
- name: &str,
- map: &std::collections::HashMap<String, String>,
-) -> proc_macro2::TokenStream {
- if let Some(full_path) = map.get(name) {
- syn::parse_str::<proc_macro2::TokenStream>(full_path).unwrap_or_else(|_| {
- let ident = proc_macro2::Ident::new(name, proc_macro2::Span::call_site());
- quote! { #ident }
- })
- } else {
- let ident = proc_macro2::Ident::new(name, proc_macro2::Span::call_site());
- quote! { #ident }
- }
-}
+/// Entry point for `gen_program!()`.
+///
+/// Generates the `Next` type alias, `Routable` impl for `ChainProcess`,
+/// and delegates to `program_comp_gen!()`, `program_fallback_gen!()`,
+/// and `program_final_gen!()`.
pub(crate) fn gen_program_impl(_input: TokenStream) -> TokenStream {
#[cfg(feature = "comp")]
let comp_gen = quote! {
@@ -94,535 +15,100 @@ pub(crate) fn gen_program_impl(_input: TokenStream) -> TokenStream {
#[cfg(not(feature = "comp"))]
let comp_gen = quote! {};
- TokenStream::from(quote! {
- /// Alias for the current program type `crate::ThisProgram`
- pub type Next = ::mingling::ChainProcess<crate::ThisProgram>;
-
- impl ::mingling::Routable<crate::ThisProgram> for ::mingling::ChainProcess<crate::ThisProgram>
- {
- fn to_chain(self) -> ::mingling::ChainProcess<crate::ThisProgram> {
- match self {
- ::mingling::ChainProcess::Ok((any, _)) => {
- ::mingling::ChainProcess::Ok((any, mingling::NextProcess::Chain))
- }
- other => other,
- }
- }
-
- fn to_render(self) -> ::mingling::ChainProcess<crate::ThisProgram> {
- match self {
- ::mingling::ChainProcess::Ok((any, _)) => {
- ::mingling::ChainProcess::Ok((any, mingling::NextProcess::Renderer))
- }
- other => other,
- }
- }
- }
-
- #comp_gen
- ::mingling::macros::program_fallback_gen!();
- ::mingling::macros::program_final_gen!();
- })
-}
-
-#[cfg(feature = "comp")]
-pub(crate) fn program_comp_gen_impl(_input: TokenStream) -> TokenStream {
- #[cfg(feature = "async")]
- let fn_exec_comp = quote! {
- #[doc(hidden)]
- #[::mingling::macros::chain]
- pub async fn __exec_completion(prev: CompletionContext) -> Next {
- use ::mingling::Grouped;
-
- let read_ctx = ::mingling::ShellContext::try_from(prev.inner);
- match read_ctx {
- Ok(ctx) => {
- let suggest = ::mingling::CompletionHelper::exec_completion::<crate::ThisProgram>(&ctx);
- ::mingling::Routable::<crate::ThisProgram>::to_render(crate::CompletionSuggest::new((ctx, suggest)))
- }
- Err(_) => std::process::exit(1),
- }
- }
- };
-
- #[cfg(not(feature = "async"))]
- let fn_exec_comp = quote! {
- #[doc(hidden)]
- #[::mingling::macros::chain]
- pub fn __exec_completion(prev: CompletionContext) -> Next {
- use ::mingling::Grouped;
-
- let read_ctx = ::mingling::ShellContext::try_from(prev.inner);
- match read_ctx {
- Ok(ctx) => {
- let suggest = ::mingling::CompletionHelper::exec_completion::<crate::ThisProgram>(&ctx);
- ::mingling::Routable::<crate::ThisProgram>::to_render(crate::CompletionSuggest::new((ctx, suggest)))
- }
- Err(_) => std::process::exit(1),
- }
- }
- };
-
- #[cfg(feature = "dispatch_tree")]
- let internal_dispatcher_comp = quote! {
- use __internal_completion_mod::__internal_dispatcher_comp;
- };
-
- #[cfg(not(feature = "dispatch_tree"))]
- let internal_dispatcher_comp = quote! {};
-
- let comp_dispatcher = quote! {
- #[doc(hidden)]
- mod __internal_completion_mod {
- use ::mingling::Grouped;
- ::mingling::macros::dispatcher!("__comp", CMDCompletion => CompletionContext);
- ::mingling::macros::pack!(
- CompletionSuggest = (::mingling::ShellContext, ::mingling::Suggest)
- );
- }
- #internal_dispatcher_comp
- use __internal_completion_mod::CompletionContext;
- use __internal_completion_mod::CompletionSuggest;
- pub use __internal_completion_mod::CMDCompletion;
-
- #fn_exec_comp
-
- ::mingling::macros::register_type!(CompletionContext);
-
- #[allow(unused)]
- #[doc(hidden)]
- #[::mingling::macros::renderer]
- pub fn __render_completion(prev: CompletionSuggest) -> ::mingling::RenderResult {
- let result = ::mingling::RenderResult::default();
- let (ctx, suggest) = prev.inner;
- ::mingling::CompletionHelper::render_suggest::<crate::ThisProgram>(ctx, suggest);
- result
- }
- };
-
- TokenStream::from(comp_dispatcher)
-}
-
-pub(crate) fn register_type_impl(input: TokenStream) -> TokenStream {
- let type_ident = parse_macro_input!(input as syn::Ident);
- let entry_str = type_ident.to_string();
-
- get_global_set(&PACKED_TYPES)
- .lock()
- .unwrap()
- .insert(entry_str);
-
- TokenStream::new()
-}
-
-pub(crate) fn register_chain_impl(input: TokenStream) -> TokenStream {
- chain::register_chain(input)
-}
-
-pub(crate) fn register_renderer_impl(input: TokenStream) -> TokenStream {
- renderer::register_renderer(input)
-}
-
-pub(crate) fn program_fallback_gen_impl(_input: TokenStream) -> TokenStream {
- #[cfg(feature = "structural_renderer")]
- let pack_empty = quote! {
- #[derive(::serde::Serialize, ::mingling::StructuralData, ::mingling::Grouped, Default)]
- pub struct ResultEmpty;
- };
-
- #[cfg(not(feature = "structural_renderer"))]
- let pack_empty = quote! {
- #[derive(::mingling::Grouped, Default)]
- pub struct ResultEmpty;
- };
-
- let expanded = quote! {
- ::mingling::macros::pack!(ErrorRendererNotFound = String);
- ::mingling::macros::pack!(ErrorDispatcherNotFound = Vec<String>);
- #pack_empty
- };
- TokenStream::from(expanded)
-}
-
-#[allow(clippy::too_many_lines)]
-pub(crate) fn program_final_gen_impl(_input: TokenStream) -> TokenStream {
- let name = syn::Ident::new("ThisProgram", proc_macro2::Span::call_site());
-
- let packed_types = get_global_set(&PACKED_TYPES).lock().unwrap().clone();
-
- let renderers = get_global_set(&RENDERERS).lock().unwrap().clone();
- let chains = get_global_set(&CHAINS).lock().unwrap().clone();
- let renderer_exist = get_global_set(&RENDERERS_EXIST).lock().unwrap().clone();
- let chain_exist = get_global_set(&CHAINS_EXIST).lock().unwrap().clone();
-
- #[cfg(feature = "structural_renderer")]
- let structural_renderers = get_global_set(&STRUCTURAL_RENDERERS)
- .lock()
- .unwrap()
- .clone();
-
- #[cfg(feature = "comp")]
- let completions = get_global_set(&COMPLETIONS).lock().unwrap().clone();
-
- let packed_types: Vec<proc_macro2::TokenStream> = packed_types
- .iter()
- .map(|s| syn::parse_str::<proc_macro2::TokenStream>(s).unwrap())
- .collect();
-
- let renderer_tokens: Vec<proc_macro2::TokenStream> = renderers
- .iter()
- .map(|s| syn::parse_str::<proc_macro2::TokenStream>(s).unwrap())
- .collect();
-
- let chain_tokens: Vec<proc_macro2::TokenStream> = chains
- .iter()
- .map(|s| syn::parse_str::<proc_macro2::TokenStream>(s).unwrap())
- .collect();
-
- let renderer_exist_tokens: Vec<proc_macro2::TokenStream> = renderer_exist
- .iter()
- .map(|s| syn::parse_str::<proc_macro2::TokenStream>(s).unwrap())
- .collect();
-
- let chain_exist_tokens: Vec<proc_macro2::TokenStream> = chain_exist
- .iter()
- .map(|s| syn::parse_str::<proc_macro2::TokenStream>(s).unwrap())
- .collect();
-
- let pathf_map: Option<HashMap<String, String>> = load_pathf_map();
-
+ // When pathf is enabled, load the type_using.rs generated by the build script
+ // and emit its use statements so types from submodules are in scope.
#[cfg(feature = "pathf")]
- let pathf_hint: proc_macro2::TokenStream = if pathf_map.is_none() {
- quote! {
- compile_error!(
-"Cannot load type mapping computed by `pathf`.
-If not yet configured, execute `mingling::build::analyze_and_build_type_mapping()` in your `build.rs`.
-
-fn main() {
- // Require features: [\"builds\", \"pathf\"]
- mingling::build::analyze_and_build_type_mapping().unwrap();
- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\\__ Write to `build.rs`
-
-}
- ");
+ let pathf_uses: Vec<proc_macro2::TokenStream> = {
+ let uses = load_pathf_uses();
+ if uses.is_empty() {
+ // The file might not exist yet — emit a clear hint
+ let hint: proc_macro2::TokenStream = syn::parse_quote! {
+ compile_error!(
+ "pathf: `{}` not found or empty.\n\
+ Make sure `build.rs` calls `mingling::build::analyze_and_build_type_mapping().unwrap();`\n\
+ with features [\"build\", \"pathf\"] enabled."
+ );
+ };
+ vec![hint]
+ } else {
+ uses
}
- } else {
- quote! {}
};
-
#[cfg(not(feature = "pathf"))]
- let pathf_hint: proc_macro2::TokenStream = quote! {};
+ let pathf_uses: Vec<proc_macro2::TokenStream> = Vec::new();
- let pathf_map: HashMap<String, String> = if cfg!(feature = "pathf") {
- pathf_map.unwrap_or_default()
- } else {
- HashMap::new()
- };
+ #[cfg(feature = "pathf")]
+ let super_use = quote! {};
- let pathf_uses: Vec<proc_macro2::TokenStream> = if cfg!(feature = "pathf") {
- pathf_map
- .values()
- .map(|path| format!("use {};", path).parse().unwrap_or_default())
- .collect()
- } else {
- Vec::new()
+ #[cfg(not(feature = "pathf"))]
+ let super_use = quote! {
+ use super::*;
};
- #[cfg(feature = "structural_renderer")]
- let structural_renderer_tokens: Vec<proc_macro2::TokenStream> = structural_renderers
- .iter()
- .map(|s| syn::parse_str::<proc_macro2::TokenStream>(s).unwrap())
- .collect();
+ TokenStream::from(quote! {
+ pub use __this_program_impl::*;
- #[cfg(feature = "structural_renderer")]
- let structural_render = quote! {
- fn structural_render(
- any: ::mingling::AnyOutput<Self::Enum>,
- setting: &::mingling::StructuralRendererSetting,
- ) -> Result<::mingling::RenderResult, ::mingling::error::StructuralRendererSerializeError> {
- #[allow(unused_imports)]
+ #[doc(hidden)]
+ pub mod __this_program_impl {
+ #super_use
#(#pathf_uses)*
- match any.member_id {
- #(#structural_renderer_tokens)*
- _ => {
- // Non-structural types: render ResultEmpty (which implements
- // StructuralData + Serialize) instead of producing nothing.
- let mut r = ::mingling::RenderResult::default();
- ::mingling::StructuralRenderer::render(&ResultEmpty, setting, &mut r)?;
- Ok(r)
- }
- }
- }
- };
-
- #[cfg(not(feature = "structural_renderer"))]
- let structural_render = quote! {};
- #[cfg(feature = "dispatch_tree")]
- let compile_time_dispatchers: Vec<String> = get_global_set(&COMPILE_TIME_DISPATCHERS)
- .lock()
- .unwrap()
- .clone()
- .iter()
- .cloned()
- .collect();
+ /// Alias for the current program type `ThisProgram`
+ pub type Next = ::mingling::ChainProcess<ThisProgram>;
- #[cfg(feature = "dispatch_tree")]
- let dispatch_tree_nodes = {
- let entries: Vec<(String, String, String)> = compile_time_dispatchers
- .iter()
- .filter_map(|entry| {
- let parts: Vec<&str> = entry.split(':').collect();
- if parts.len() == 3 {
- Some((
- parts[0].to_string(),
- parts[1].to_string(),
- parts[2].to_string(),
- ))
- } else {
- None
- }
- })
- .collect();
-
- let get_nodes_fn = dispatch_tree_gen::gen_get_nodes(&entries, &pathf_map);
- let dispatch_trie_fn = dispatch_tree_gen::gen_dispatch_args_trie(&entries, &pathf_map);
-
- quote! {
- #get_nodes_fn
- #dispatch_trie_fn
- }
- };
+ ::mingling::macros::pack!(Entry = Vec<String>);
- #[cfg(not(feature = "dispatch_tree"))]
- let dispatch_tree_nodes = quote! {};
-
- #[cfg(feature = "comp")]
- let completion_tokens: Vec<proc_macro2::TokenStream> = completions
- .iter()
- .map(|s| syn::parse_str::<proc_macro2::TokenStream>(s).unwrap())
- .collect();
-
- #[cfg(feature = "comp")]
- let comp = quote! {
- fn do_comp(any: &::mingling::AnyOutput<Self::Enum>, ctx: &::mingling::ShellContext) -> ::mingling::Suggest {
- #[allow(unused_imports)]
- #(#pathf_uses)*
- match any.member_id {
- #(#completion_tokens)*
- _ => ::mingling::Suggest::FileCompletion,
- }
- }
- };
-
- #[cfg(not(feature = "comp"))]
- let comp = quote! {};
-
- // Build render function arms from stored entries
- let render_fn =
- if renderer_tokens.is_empty() {
- quote! {
- fn render(_any: ::mingling::AnyOutput<Self::Enum>) -> ::mingling::RenderResult {
- ::mingling::RenderResult::default()
- }
- }
- } else {
- let render_arms: Vec<_> = renderer_tokens.iter().map(|entry| {
- let (struct_ident, variant_ident) = parse_entry_pair(entry);
- let downcast_ty = resolve_type(&variant_ident.to_string(), &pathf_map);
- let resolved_struct = resolve_type(&struct_ident.to_string(), &pathf_map);
- quote! {
- Self::#variant_ident => {
- // SAFETY: The `type_id` check ensures that `any` contains a value of type `#variant_ident`,
- // so downcasting to `#variant_ident` is safe.
- let value = unsafe { any.downcast::<#downcast_ty>().unwrap_unchecked() };
- <#resolved_struct as ::mingling::Renderer>::render(value)
- }
- }
- }).collect();
- quote! {
- fn render(any: ::mingling::AnyOutput<Self::Enum>) -> ::mingling::RenderResult {
- match any.member_id {
- #(#render_arms)*
- _ => ::mingling::RenderResult::default(),
+ impl ::mingling::Routable<ThisProgram> for ::mingling::ChainProcess<ThisProgram>
+ {
+ fn to_chain(self) -> ::mingling::ChainProcess<ThisProgram> {
+ match self {
+ ::mingling::ChainProcess::Ok((any, _)) => {
+ ::mingling::ChainProcess::Ok((any, mingling::NextProcess::Chain))
+ }
+ other => other,
}
}
- }
- };
-
- // Build do_chain function (async and sync versions)
- let chain_arms_async: Vec<_> = chain_tokens.iter().map(|entry| {
- let (struct_ident, variant_ident) = parse_entry_pair(entry);
- let downcast_ty = resolve_type(&variant_ident.to_string(), &pathf_map);
- let resolved_struct = resolve_type(&struct_ident.to_string(), &pathf_map);
- quote! {
- Self::#variant_ident => {
- // SAFETY: The `type_id` check ensures that `any` contains a value of type `#variant_ident`,
- // so downcasting to `#variant_ident` is safe.
- let value = unsafe { any.downcast::<#downcast_ty>().unwrap_unchecked() };
- let fut = async { <#resolved_struct as ::mingling::Chain<Self::Enum>>::proc(value).await };
- ::std::boxed::Box::pin(fut)
- }
- }
- }).collect();
- let chain_arms_sync: Vec<_> = chain_tokens
- .iter()
- .map(|entry| {
- let (struct_ident, variant_ident) = parse_entry_pair(entry);
- let downcast_ty = resolve_type(&variant_ident.to_string(), &pathf_map);
- let resolved_struct = resolve_type(&struct_ident.to_string(), &pathf_map);
- quote! {
- Self::#variant_ident => {
- // SAFETY: The `type_id` check ensures that `any` contains a value of type `#variant_ident`,
- // so downcasting to `#variant_ident` is safe.
- let value = unsafe { any.downcast::<#downcast_ty>().unwrap_unchecked() };
- <#resolved_struct as ::mingling::Chain<Self::Enum>>::proc(value)
+ fn to_render(self) -> ::mingling::ChainProcess<ThisProgram> {
+ match self {
+ ::mingling::ChainProcess::Ok((any, _)) => {
+ ::mingling::ChainProcess::Ok((any, mingling::NextProcess::Renderer))
+ }
+ other => other,
+ }
}
}
- })
- .collect();
- let do_chain_fn = if chain_tokens.is_empty() {
- quote! {
- fn do_chain(_any: ::mingling::AnyOutput<Self::Enum>) -> ::mingling::ChainProcess<Self::Enum> {
- ::core::panic!("No chain found for type id")
- }
- }
- } else if ASYNC_ENABLED {
- quote! {
- fn do_chain(
- any: ::mingling::AnyOutput<Self::Enum>,
- ) -> ::std::pin::Pin<::std::boxed::Box<dyn ::std::future::Future<Output = ::mingling::ChainProcess<Self::Enum>> + ::std::marker::Send>> {
- match any.member_id {
- #(#chain_arms_async)*
- _ => ::core::panic!("No chain found for type id: {:?}", any.type_id),
- }
- }
+ #comp_gen
+ ::mingling::macros::program_fallback_gen!();
+ ::mingling::macros::program_final_gen!();
}
- } else {
- quote! {
- fn do_chain(
- any: ::mingling::AnyOutput<Self::Enum>,
- ) -> ::mingling::ChainProcess<Self::Enum> {
- match any.member_id {
- #(#chain_arms_sync)*
- _ => ::core::panic!("No chain found for type id: {:?}", any.type_id),
- }
- }
- }
- };
-
- let help_tokens: Vec<proc_macro2::TokenStream> = get_global_set(&HELP_REQUESTS)
- .lock()
- .unwrap()
- .clone()
- .iter()
- .map(|s| syn::parse_str::<proc_macro2::TokenStream>(s).unwrap())
- .collect();
+ })
+}
- let num_variants = packed_types.len();
- let repr_type = if u8::try_from(num_variants).is_ok() {
- quote! { u8 }
- } else if u16::try_from(num_variants).is_ok() {
- quote! { u16 }
- } else if u32::try_from(num_variants).is_ok() {
- quote! { u32 }
- } else {
- quote! { u128 }
+/// Loads `type_using.rs` generated by the pathf build script and returns each
+/// `use ...;` line as a token stream, ready to be emitted in the generated output.
+#[cfg(feature = "pathf")]
+fn load_pathf_uses() -> Vec<proc_macro2::TokenStream> {
+ let out_dir = match std::env::var("OUT_DIR") {
+ Ok(d) => d,
+ Err(_) => return Vec::new(),
};
-
- let expanded = quote! {
- #pathf_hint
-
- #[derive(Debug, PartialEq, Eq, Clone)]
- #[repr(#repr_type)]
- #[allow(nonstandard_style)]
- pub enum #name {
- #(#packed_types),*
- }
-
- impl ::std::fmt::Display for #name {
- fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
- match self {
- #(#name::#packed_types => write!(f, stringify!(#packed_types)),)*
- }
- }
- }
-
- impl ::mingling::ProgramCollect for #name {
- type Enum = #name;
- type ErrorDispatcherNotFound = ErrorDispatcherNotFound;
- type ErrorRendererNotFound = ErrorRendererNotFound;
- type ResultEmpty = ResultEmpty;
- fn build_renderer_not_found(member_id: Self::Enum) -> ::mingling::AnyOutput<Self::Enum> {
- ::mingling::AnyOutput::new(ErrorRendererNotFound::new(member_id.to_string()))
- }
- fn build_dispatcher_not_found(args: Vec<String>) -> ::mingling::AnyOutput<Self::Enum> {
- ::mingling::AnyOutput::new(ErrorDispatcherNotFound::new(args))
- }
- fn build_empty_result() -> ::mingling::AnyOutput<Self::Enum> {
- ::mingling::AnyOutput::new(ResultEmpty)
- }
- #render_fn
- #do_chain_fn
- fn render_help(any: ::mingling::AnyOutput<Self::Enum>) -> ::mingling::RenderResult {
- #[allow(unused_imports)]
- #(#pathf_uses)*
- match any.member_id {
- #(#help_tokens)*
- _ => ::mingling::RenderResult::default(),
- }
- }
- fn has_renderer(any: &::mingling::AnyOutput<Self::Enum>) -> bool {
- match any.member_id {
- #(#renderer_exist_tokens)*
- _ => false
- }
- }
- fn has_chain(any: &::mingling::AnyOutput<Self::Enum>) -> bool {
- match any.member_id {
- #(#chain_exist_tokens)*
- _ => false
- }
- }
- #dispatch_tree_nodes
- #structural_render
- #comp
- }
-
- impl #name {
- /// Creates a new `Program<#name>` instance with default configuration.
- pub fn new() -> ::mingling::Program<#name> {
- ::mingling::Program::new()
- }
-
- /// Returns a static reference to the global `Program<#name>` singleton.
- pub fn this() -> &'static ::mingling::Program<#name> {
- &::mingling::this::<#name>()
- }
- }
+ let crate_name = match std::env::var("CARGO_PKG_NAME") {
+ Ok(n) => n,
+ Err(_) => return Vec::new(),
};
-
- // Clear all global registries to prevent stale state in Rust Analyzer
- get_global_set(&PACKED_TYPES).lock().unwrap().clear();
- get_global_set(&CHAINS).lock().unwrap().clear();
- get_global_set(&CHAINS_EXIST).lock().unwrap().clear();
- get_global_set(&RENDERERS).lock().unwrap().clear();
- get_global_set(&RENDERERS_EXIST).lock().unwrap().clear();
- get_global_set(&HELP_REQUESTS).lock().unwrap().clear();
- #[cfg(feature = "comp")]
- get_global_set(&COMPLETIONS).lock().unwrap().clear();
- #[cfg(feature = "dispatch_tree")]
- get_global_set(&COMPILE_TIME_DISPATCHERS)
- .lock()
- .unwrap()
- .clear();
- #[cfg(feature = "structural_renderer")]
- get_global_set(&STRUCTURAL_RENDERERS)
- .lock()
- .unwrap()
- .clear();
-
- TokenStream::from(expanded)
+ let path = std::path::Path::new(&out_dir)
+ .join(&crate_name)
+ .join("type_using.rs");
+ let content = match std::fs::read_to_string(&path) {
+ Ok(c) => c,
+ Err(_) => return Vec::new(),
+ };
+ content
+ .lines()
+ .map(|line| line.trim().to_string())
+ .filter(|line| !line.is_empty())
+ .filter_map(|line| line.parse::<proc_macro2::TokenStream>().ok())
+ .collect()
}
diff --git a/mingling_macros/src/func/group.rs b/mingling_macros/src/func/group.rs
index b865913..edb1fe1 100644
--- a/mingling_macros/src/func/group.rs
+++ b/mingling_macros/src/func/group.rs
@@ -133,7 +133,11 @@ pub(crate) fn group_macro(input: TokenStream) -> TokenStream {
#type_use
#alias_use
- impl ::mingling::Grouped<__MinglingProgram> for #type_name {
+ /// SAFETY: This is an internal implementation of the `group!` macro,
+ /// guaranteeing that the enum value registered by the `register_type!` macro
+ /// is exactly the same as the actual return value,
+ /// which can be confirmed via the `Ident` in the `quote!` block.
+ unsafe impl ::mingling::Grouped<__MinglingProgram> for #type_name {
fn member_id() -> __MinglingProgram {
__MinglingProgram::#type_name
}
diff --git a/mingling_macros/src/func/group_structural.rs b/mingling_macros/src/func/group_structural.rs
new file mode 100644
index 0000000..2bd2f83
--- /dev/null
+++ b/mingling_macros/src/func/group_structural.rs
@@ -0,0 +1,124 @@
+use proc_macro::TokenStream;
+use quote::quote;
+use syn::{Ident, TypePath};
+
+use crate::get_global_set;
+
+/// `group_structural!` — like `group!` but also marks the type as supporting
+/// structured output via `StructuralData`.
+pub(crate) fn group_structural(input: TokenStream) -> TokenStream {
+ // Parse the same input as group!
+ let input_parsed = syn::parse_macro_input!(input as GroupStructuralInput);
+
+ let is_aliased = matches!(&input_parsed, GroupStructuralInput::Aliased { .. });
+
+ let (type_path, type_name, alias_stmt) = match &input_parsed {
+ GroupStructuralInput::Plain(type_path) => {
+ let name = type_path
+ .path
+ .segments
+ .last()
+ .expect("TypePath must have at least one segment")
+ .ident
+ .clone();
+ (type_path.clone(), name, quote! {})
+ }
+ GroupStructuralInput::Aliased { alias, type_path } => {
+ let alias_stmt = quote! {
+ pub(crate) type #alias = #type_path;
+ };
+ (type_path.clone(), alias.clone(), alias_stmt)
+ }
+ };
+
+ let type_name_str = type_name.to_string();
+
+ // Register in STRUCTURED_TYPES
+ get_global_set(&crate::STRUCTURED_TYPES)
+ .lock()
+ .unwrap()
+ .insert(type_name_str);
+
+ let program_path = crate::default_program_path();
+
+ // Generate unique module name
+ let segments: Vec<String> = type_path
+ .path
+ .segments
+ .iter()
+ .map(|seg| seg.ident.to_string().to_lowercase())
+ .collect();
+ let module_name = Ident::new(
+ &format!("internal_group_{}", segments.join("_")),
+ proc_macro2::Span::call_site(),
+ );
+
+ // Generate the appropriate `use` statement for the original type
+ let type_use = if type_path.path.segments.len() > 1 {
+ quote! { #[allow(unused_imports)] use #type_path; }
+ } else {
+ let ident = type_path
+ .path
+ .segments
+ .last()
+ .expect("TypePath must have at least one segment")
+ .ident
+ .clone();
+ quote! { #[allow(unused_imports)] use super::#ident; }
+ };
+
+ let alias_use = if is_aliased {
+ quote! { use super::#type_name; }
+ } else {
+ quote! {}
+ };
+
+ let expanded = quote! {
+ #alias_stmt
+ #[allow(non_camel_case_types)]
+ mod #module_name {
+ use #program_path as __MinglingProgram;
+ #type_use
+ #alias_use
+
+ /// SAFETY: This is an internal implementation of the `pack_structural!` macro,
+ /// guaranteeing that the enum value registered by the `register_type!` macro
+ /// is exactly the same as the actual return value,
+ /// which can be confirmed via the `Ident` in the `quote!` block.
+ unsafe impl ::mingling::Grouped<__MinglingProgram> for #type_name {
+ fn member_id() -> __MinglingProgram {
+ __MinglingProgram::#type_name
+ }
+ }
+
+ impl ::mingling::__private::StructuralDataSealed<crate::ThisProgram> for #type_name {}
+ impl ::mingling::__private::StructuralData<crate::ThisProgram> for #type_name {}
+
+ ::mingling::macros::register_type!(#type_name);
+ }
+ };
+
+ expanded.into()
+}
+
+/// Input for `group_structural!` — same format as `group!`.
+enum GroupStructuralInput {
+ Plain(TypePath),
+ Aliased { alias: Ident, type_path: TypePath },
+}
+
+impl syn::parse::Parse for GroupStructuralInput {
+ fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
+ let fork = input.fork();
+ let _first: Ident = fork.parse()?;
+ if fork.peek(syn::Token![=]) {
+ let alias: Ident = input.parse()?;
+ let _eq: syn::Token![=] = input.parse()?;
+ let type_path: TypePath = input.parse()?;
+ Ok(GroupStructuralInput::Aliased { alias, type_path })
+ } else {
+ let type_path: TypePath = input.parse()?;
+ Ok(GroupStructuralInput::Plain(type_path))
+ }
+ }
+}
diff --git a/mingling_macros/src/func/pack.rs b/mingling_macros/src/func/pack.rs
index a1a7e6b..7206b8e 100644
--- a/mingling_macros/src/func/pack.rs
+++ b/mingling_macros/src/func/pack.rs
@@ -138,7 +138,11 @@ pub(crate) fn pack(input: TokenStream) -> TokenStream {
}
}
- impl ::mingling::Grouped<#group_name> for #type_name {
+ /// SAFETY: This is an internal implementation of the `pack!` macro,
+ /// guaranteeing that the enum value registered by the `register_type!` macro
+ /// is exactly the same as the actual return value,
+ /// which can be confirmed via the `Ident` in the `quote!` block.
+ unsafe impl ::mingling::Grouped<#group_name> for #type_name {
fn member_id() -> #group_name {
#group_name::#type_name
}
diff --git a/mingling_macros/src/func/pack_err.rs b/mingling_macros/src/func/pack_err.rs
index 2a318bc..8b224b0 100644
--- a/mingling_macros/src/func/pack_err.rs
+++ b/mingling_macros/src/func/pack_err.rs
@@ -105,105 +105,3 @@ pub(crate) fn pack_err(input: TokenStream) -> TokenStream {
}
}
}
-
-/// `pack_err_structural!` — like `pack_err!` but also marks the type as
-/// supporting structured output via `StructuralData`.
-///
-/// # Syntax
-///
-/// ```rust,ignore
-/// pack_err_structural!(ErrorNotFound);
-/// pack_err_structural!(ErrorNotDir = PathBuf);
-/// ```
-///
-/// This is equivalent to:
-/// ```rust,ignore
-/// pack_err!(ErrorNotFound);
-/// impl ::mingling::__private::StructuralDataSealed for ErrorNotFound {}
-/// impl ::mingling::__private::StructuralData for ErrorNotFound {}
-/// ```
-#[cfg(feature = "structural_renderer")]
-pub(crate) fn pack_err_structural(input: TokenStream) -> TokenStream {
- let parsed = parse_macro_input!(input as PackErrInput);
-
- let type_name = match &parsed {
- PackErrInput::Simple { type_name } => type_name.clone(),
- PackErrInput::Typed { type_name, .. } => type_name.clone(),
- };
-
- // Register in STRUCTURED_TYPES
- let type_name_str = type_name.to_string();
- crate::get_global_set(&crate::STRUCTURED_TYPES)
- .lock()
- .unwrap()
- .insert(type_name_str);
-
- let structural_data = quote! {
- impl ::mingling::__private::StructuralDataSealed<crate::ThisProgram> for #type_name {}
- impl ::mingling::__private::StructuralData<crate::ThisProgram> for #type_name {}
- };
-
- // Generate the struct + impls (same as pack_err! but with Serialize derive + sealed)
- match parsed {
- PackErrInput::Simple { type_name } => {
- let name_str = type_name.to_string();
- let snake_name = snake_case!(&name_str);
-
- let expanded = quote! {
- #[derive(::mingling::Grouped, ::serde::Serialize)]
- pub struct #type_name {
- /// The snake_case name of this error, automatically set at compile time.
- pub name: String,
- }
-
- impl ::std::default::Default for #type_name {
- fn default() -> Self {
- Self {
- name: #snake_name.into(),
- }
- }
- }
-
- ::mingling::macros::register_type!(#type_name);
-
- #structural_data
- };
-
- expanded.into()
- }
- PackErrInput::Typed {
- type_name,
- inner_type,
- } => {
- let name_str = type_name.to_string();
- let snake_name = snake_case!(&name_str);
-
- let expanded = quote! {
- #[derive(::mingling::Grouped, ::serde::Serialize)]
- pub struct #type_name {
- /// The snake_case name of this error, automatically set at compile time.
- pub name: String,
- /// Additional context info for this error.
- pub info: #inner_type,
- }
-
- impl #type_name {
- /// Creates a new error with the given info.
- /// The `name` field is automatically set to the snake_case of the struct name.
- pub fn new(info: #inner_type) -> Self {
- Self {
- name: #snake_name.into(),
- info,
- }
- }
- }
-
- ::mingling::macros::register_type!(#type_name);
-
- #structural_data
- };
-
- expanded.into()
- }
- }
-}
diff --git a/mingling_macros/src/func/pack_err_structural.rs b/mingling_macros/src/func/pack_err_structural.rs
new file mode 100644
index 0000000..7d3a6f8
--- /dev/null
+++ b/mingling_macros/src/func/pack_err_structural.rs
@@ -0,0 +1,119 @@
+use just_fmt::snake_case;
+use proc_macro::TokenStream;
+use quote::quote;
+use syn::{Ident, Token, Type, parse_macro_input};
+
+/// `pack_err_structural!` — like `pack_err!` but also marks the type as
+/// supporting structured output via `StructuralData`.
+pub(crate) fn pack_err_structural(input: TokenStream) -> TokenStream {
+ let parsed = parse_macro_input!(input as PackErrInput);
+
+ let type_name = match &parsed {
+ PackErrInput::Simple { type_name } => type_name.clone(),
+ PackErrInput::Typed { type_name, .. } => type_name.clone(),
+ };
+
+ // Register in STRUCTURED_TYPES
+ let type_name_str = type_name.to_string();
+ crate::get_global_set(&crate::STRUCTURED_TYPES)
+ .lock()
+ .unwrap()
+ .insert(type_name_str);
+
+ let structural_data = quote! {
+ impl ::mingling::__private::StructuralDataSealed<crate::ThisProgram> for #type_name {}
+ impl ::mingling::__private::StructuralData<crate::ThisProgram> for #type_name {}
+ };
+
+ // Generate the struct + impls (same as pack_err! but with Serialize derive + sealed)
+ match parsed {
+ PackErrInput::Simple { type_name } => {
+ let name_str = type_name.to_string();
+ let snake_name = snake_case!(&name_str);
+
+ let expanded = quote! {
+ #[derive(::mingling::Grouped, ::serde::Serialize)]
+ pub struct #type_name {
+ /// The snake_case name of this error, automatically set at compile time.
+ pub name: String,
+ }
+
+ impl ::std::default::Default for #type_name {
+ fn default() -> Self {
+ Self {
+ name: #snake_name.into(),
+ }
+ }
+ }
+
+ ::mingling::macros::register_type!(#type_name);
+
+ #structural_data
+ };
+
+ expanded.into()
+ }
+ PackErrInput::Typed {
+ type_name,
+ inner_type,
+ } => {
+ let name_str = type_name.to_string();
+ let snake_name = snake_case!(&name_str);
+
+ let expanded = quote! {
+ #[derive(::mingling::Grouped, ::serde::Serialize)]
+ pub struct #type_name {
+ /// The snake_case name of this error, automatically set at compile time.
+ pub name: String,
+ /// Additional context info for this error.
+ pub info: #inner_type,
+ }
+
+ impl #type_name {
+ /// Creates a new error with the given info.
+ /// The `name` field is automatically set to the snake_case of the struct name.
+ pub fn new(info: #inner_type) -> Self {
+ Self {
+ name: #snake_name.into(),
+ info,
+ }
+ }
+ }
+
+ ::mingling::macros::register_type!(#type_name);
+
+ #structural_data
+ };
+
+ expanded.into()
+ }
+ }
+}
+
+// Re-use pack_err's input parser
+enum PackErrInput {
+ Simple {
+ type_name: Ident,
+ },
+ Typed {
+ type_name: Ident,
+ inner_type: Box<Type>,
+ },
+}
+
+impl syn::parse::Parse for PackErrInput {
+ fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
+ let type_name: Ident = input.parse()?;
+
+ if input.peek(Token![=]) {
+ input.parse::<Token![=]>()?;
+ let inner_type: Type = input.parse()?;
+ Ok(PackErrInput::Typed {
+ type_name,
+ inner_type: Box::new(inner_type),
+ })
+ } else {
+ Ok(PackErrInput::Simple { type_name })
+ }
+ }
+}
diff --git a/mingling_macros/src/func/pack_structural.rs b/mingling_macros/src/func/pack_structural.rs
new file mode 100644
index 0000000..9399959
--- /dev/null
+++ b/mingling_macros/src/func/pack_structural.rs
@@ -0,0 +1,168 @@
+use proc_macro::TokenStream;
+use quote::quote;
+use syn::Ident;
+
+use crate::get_global_set;
+
+/// `pack_structural!` — like `pack!` but also marks the type as supporting
+/// structured output via `StructuralData`.
+pub(crate) fn pack_structural(input: TokenStream) -> TokenStream {
+ // Parse same input format as `pack!`
+ let input_parsed = syn::parse_macro_input!(input as PackStructuralInput);
+ let type_name = input_parsed.type_name;
+ let inner_type = input_parsed.inner_type;
+ let attrs = input_parsed.attrs;
+ let program_path = crate::default_program_path();
+
+ // Register in STRUCTURED_TYPES
+ let type_name_str = type_name.to_string();
+ get_global_set(&crate::STRUCTURED_TYPES)
+ .lock()
+ .unwrap()
+ .insert(type_name_str);
+
+ // Struct definition (with Serialize derive, same as pack! under structural_renderer)
+ #[cfg(not(feature = "structural_renderer"))]
+ let struct_def = quote! {
+ #(#attrs)*
+ pub struct #type_name {
+ pub inner: #inner_type,
+ }
+ };
+
+ #[cfg(feature = "structural_renderer")]
+ let struct_def = quote! {
+ #(#attrs)*
+ #[derive(serde::Serialize)]
+ pub struct #type_name {
+ pub inner: #inner_type,
+ }
+ };
+
+ // Helper impls (same as pack!)
+ let new_impl = quote! {
+ impl #type_name {
+ pub fn new(inner: #inner_type) -> Self {
+ Self { inner }
+ }
+ }
+ };
+
+ let from_into_impl = quote! {
+ impl From<#inner_type> for #type_name {
+ fn from(inner: #inner_type) -> Self {
+ Self::new(inner)
+ }
+ }
+ impl From<#type_name> for #inner_type {
+ fn from(wrapper: #type_name) -> #inner_type {
+ wrapper.inner
+ }
+ }
+ };
+
+ let as_ref_impl = quote! {
+ impl ::std::convert::AsRef<#inner_type> for #type_name {
+ fn as_ref(&self) -> &#inner_type {
+ &self.inner
+ }
+ }
+ impl ::std::convert::AsMut<#inner_type> for #type_name {
+ fn as_mut(&mut self) -> &mut #inner_type {
+ &mut self.inner
+ }
+ }
+ };
+
+ let deref_impl = quote! {
+ impl ::std::ops::Deref for #type_name {
+ type Target = #inner_type;
+ fn deref(&self) -> &Self::Target {
+ &self.inner
+ }
+ }
+ impl ::std::ops::DerefMut for #type_name {
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ &mut self.inner
+ }
+ }
+ };
+
+ let default_impl = quote! {
+ impl ::std::default::Default for #type_name
+ where
+ #inner_type: ::std::default::Default,
+ {
+ fn default() -> Self {
+ Self::new(::std::default::Default::default())
+ }
+ }
+ };
+
+ let register_impl = quote! {
+ ::mingling::macros::register_type!(#type_name);
+ };
+
+ // StructuralData impl + sealed + registration
+ let structural_impl = quote! {
+ impl ::mingling::__private::StructuralDataSealed<crate::ThisProgram> for #type_name {}
+ impl ::mingling::__private::StructuralData<crate::ThisProgram> for #type_name {}
+ };
+
+ let expanded = quote! {
+ #struct_def
+
+ #new_impl
+ #from_into_impl
+ #as_ref_impl
+ #deref_impl
+ #default_impl
+ #register_impl
+ #structural_impl
+
+ impl Into<::mingling::AnyOutput<#program_path>> for #type_name {
+ fn into(self) -> ::mingling::AnyOutput<#program_path> {
+ ::mingling::AnyOutput::new(self)
+ }
+ }
+
+ impl Into<::mingling::ChainProcess<#program_path>> for #type_name {
+ fn into(self) -> ::mingling::ChainProcess<#program_path> {
+ ::mingling::AnyOutput::new(self).route_chain()
+ }
+ }
+
+ /// SAFETY: This is an internal implementation of the `pack_structural!` macro,
+ /// guaranteeing that the enum value registered by the `register_type!` macro
+ /// is exactly the same as the actual return value,
+ /// which can be confirmed via the `Ident` in the `quote!` block.
+ unsafe impl ::mingling::Grouped<#program_path> for #type_name {
+ fn member_id() -> #program_path {
+ #program_path::#type_name
+ }
+ }
+ };
+
+ expanded.into()
+}
+
+/// Input for `pack_structural!` — same format as `pack!`.
+struct PackStructuralInput {
+ attrs: Vec<syn::Attribute>,
+ type_name: Ident,
+ inner_type: syn::Type,
+}
+
+impl syn::parse::Parse for PackStructuralInput {
+ fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
+ let attrs = input.call(syn::Attribute::parse_outer)?;
+ let type_name: Ident = input.parse()?;
+ input.parse::<syn::Token![=]>()?;
+ let inner_type: syn::Type = input.parse()?;
+ Ok(PackStructuralInput {
+ attrs,
+ type_name,
+ inner_type,
+ })
+ }
+}
diff --git a/mingling_macros/src/func/program_comp_gen.rs b/mingling_macros/src/func/program_comp_gen.rs
new file mode 100644
index 0000000..2fbb0e0
--- /dev/null
+++ b/mingling_macros/src/func/program_comp_gen.rs
@@ -0,0 +1,80 @@
+use proc_macro::TokenStream;
+use quote::quote;
+
+#[cfg(feature = "comp")]
+pub(crate) fn program_comp_gen_impl(_input: TokenStream) -> TokenStream {
+ #[cfg(feature = "async")]
+ let fn_exec_comp = quote! {
+ #[doc(hidden)]
+ #[::mingling::macros::chain]
+ pub async fn __exec_completion(prev: CompletionContext) -> Next {
+ use ::mingling::Grouped;
+
+ let read_ctx = ::mingling::ShellContext::try_from(prev.inner);
+ match read_ctx {
+ Ok(ctx) => {
+ let suggest = ::mingling::CompletionHelper::exec_completion::<crate::ThisProgram>(&ctx);
+ ::mingling::Routable::<crate::ThisProgram>::to_render(CompletionSuggest::new((ctx, suggest)))
+ }
+ Err(_) => std::process::exit(1),
+ }
+ }
+ };
+
+ #[cfg(not(feature = "async"))]
+ let fn_exec_comp = quote! {
+ #[doc(hidden)]
+ #[::mingling::macros::chain]
+ pub fn __exec_completion(prev: CompletionContext) -> Next {
+ use ::mingling::Grouped;
+
+ let read_ctx = ::mingling::ShellContext::try_from(prev.inner);
+ match read_ctx {
+ Ok(ctx) => {
+ let suggest = ::mingling::CompletionHelper::exec_completion::<crate::ThisProgram>(&ctx);
+ ::mingling::Routable::<crate::ThisProgram>::to_render(CompletionSuggest::new((ctx, suggest)))
+ }
+ Err(_) => std::process::exit(1),
+ }
+ }
+ };
+
+ #[cfg(feature = "dispatch_tree")]
+ let internal_dispatcher_comp = quote! {
+ use __internal_completion_mod::__internal_dispatcher_comp;
+ };
+
+ #[cfg(not(feature = "dispatch_tree"))]
+ let internal_dispatcher_comp = quote! {};
+
+ let comp_dispatcher = quote! {
+ #[doc(hidden)]
+ mod __internal_completion_mod {
+ use ::mingling::Grouped;
+ ::mingling::macros::dispatcher!("__comp", CMDCompletion => CompletionContext);
+ ::mingling::macros::pack!(
+ CompletionSuggest = (::mingling::ShellContext, ::mingling::Suggest)
+ );
+ }
+ #internal_dispatcher_comp
+ use __internal_completion_mod::CompletionContext;
+ use __internal_completion_mod::CompletionSuggest;
+ pub use __internal_completion_mod::CMDCompletion;
+
+ #fn_exec_comp
+
+ ::mingling::macros::register_type!(CompletionContext);
+
+ #[allow(unused)]
+ #[doc(hidden)]
+ #[::mingling::macros::renderer]
+ pub fn __render_completion(prev: CompletionSuggest) -> ::mingling::RenderResult {
+ let result = ::mingling::RenderResult::default();
+ let (ctx, suggest) = prev.inner;
+ ::mingling::CompletionHelper::render_suggest::<crate::ThisProgram>(ctx, suggest);
+ result
+ }
+ };
+
+ TokenStream::from(comp_dispatcher)
+}
diff --git a/mingling_macros/src/func/program_fallback_gen.rs b/mingling_macros/src/func/program_fallback_gen.rs
new file mode 100644
index 0000000..3d095e5
--- /dev/null
+++ b/mingling_macros/src/func/program_fallback_gen.rs
@@ -0,0 +1,23 @@
+use proc_macro::TokenStream;
+use quote::quote;
+
+pub(crate) fn program_fallback_gen_impl(_input: TokenStream) -> TokenStream {
+ #[cfg(feature = "structural_renderer")]
+ let pack_empty = quote! {
+ #[derive(::serde::Serialize, ::mingling::StructuralData, ::mingling::Grouped, Default)]
+ pub struct ResultEmpty;
+ };
+
+ #[cfg(not(feature = "structural_renderer"))]
+ let pack_empty = quote! {
+ #[derive(::mingling::Grouped, Default)]
+ pub struct ResultEmpty;
+ };
+
+ let expanded = quote! {
+ ::mingling::macros::pack!(ErrorRendererNotFound = String);
+ ::mingling::macros::pack!(ErrorDispatcherNotFound = Vec<String>);
+ #pack_empty
+ };
+ TokenStream::from(expanded)
+}
diff --git a/mingling_macros/src/func/program_final_gen.rs b/mingling_macros/src/func/program_final_gen.rs
new file mode 100644
index 0000000..0eed1db
--- /dev/null
+++ b/mingling_macros/src/func/program_final_gen.rs
@@ -0,0 +1,373 @@
+use proc_macro::TokenStream;
+use quote::quote;
+
+use crate::CHAINS;
+use crate::CHAINS_EXIST;
+#[cfg(feature = "dispatch_tree")]
+use crate::COMPILE_TIME_DISPATCHERS;
+#[cfg(feature = "comp")]
+use crate::COMPLETIONS;
+use crate::HELP_REQUESTS;
+use crate::PACKED_TYPES;
+use crate::RENDERERS;
+use crate::RENDERERS_EXIST;
+#[cfg(feature = "structural_renderer")]
+use crate::STRUCTURAL_RENDERERS;
+use crate::get_global_set;
+#[cfg(feature = "dispatch_tree")]
+use crate::systems::dispatch_tree_gen;
+
+#[cfg(feature = "async")]
+const ASYNC_ENABLED: bool = true;
+#[cfg(not(feature = "async"))]
+const ASYNC_ENABLED: bool = false;
+
+/// Parses an entry of the format `StructName => EnumVariant,` into a pair of idents.
+fn parse_entry_pair(entry: &proc_macro2::TokenStream) -> (proc_macro2::Ident, proc_macro2::Ident) {
+ let s = entry.to_string();
+ let arrow_idx = s
+ .find("=>")
+ .unwrap_or_else(|| panic!("Entry missing '=>': {s}"));
+ let struct_str = s[..arrow_idx].trim();
+ let variant_str = s[arrow_idx + 2..].trim().trim_end_matches(',').trim();
+ let struct_ident = proc_macro2::Ident::new(struct_str, proc_macro2::Span::call_site());
+ let variant_ident = proc_macro2::Ident::new(variant_str, proc_macro2::Span::call_site());
+ (struct_ident, variant_ident)
+}
+
+/// Helper: convert a string ident into a token stream for the generated code.
+/// Types are expected to be in scope (e.g. via pathf glob re-exports), so bare
+/// idents suffice.
+fn ident_tokens(name: &str) -> proc_macro2::TokenStream {
+ let ident = proc_macro2::Ident::new(name, proc_macro2::Span::call_site());
+ quote! { #ident }
+}
+
+#[allow(clippy::too_many_lines)]
+pub(crate) fn program_final_gen_impl(_input: TokenStream) -> TokenStream {
+ let name = syn::Ident::new("ThisProgram", proc_macro2::Span::call_site());
+
+ let packed_types = get_global_set(&PACKED_TYPES).lock().unwrap().clone();
+
+ let renderers = get_global_set(&RENDERERS).lock().unwrap().clone();
+ let chains = get_global_set(&CHAINS).lock().unwrap().clone();
+ let renderer_exist = get_global_set(&RENDERERS_EXIST).lock().unwrap().clone();
+ let chain_exist = get_global_set(&CHAINS_EXIST).lock().unwrap().clone();
+
+ #[cfg(feature = "structural_renderer")]
+ let structural_renderers = get_global_set(&STRUCTURAL_RENDERERS)
+ .lock()
+ .unwrap()
+ .clone();
+
+ #[cfg(feature = "comp")]
+ let completions = get_global_set(&COMPLETIONS).lock().unwrap().clone();
+
+ let packed_types: Vec<proc_macro2::TokenStream> = packed_types
+ .iter()
+ .map(|s| syn::parse_str::<proc_macro2::TokenStream>(s).unwrap())
+ .collect();
+
+ let renderer_tokens: Vec<proc_macro2::TokenStream> = renderers
+ .iter()
+ .map(|s| syn::parse_str::<proc_macro2::TokenStream>(s).unwrap())
+ .collect();
+
+ let chain_tokens: Vec<proc_macro2::TokenStream> = chains
+ .iter()
+ .map(|s| syn::parse_str::<proc_macro2::TokenStream>(s).unwrap())
+ .collect();
+
+ let renderer_exist_tokens: Vec<proc_macro2::TokenStream> = renderer_exist
+ .iter()
+ .map(|s| syn::parse_str::<proc_macro2::TokenStream>(s).unwrap())
+ .collect();
+
+ let chain_exist_tokens: Vec<proc_macro2::TokenStream> = chain_exist
+ .iter()
+ .map(|s| syn::parse_str::<proc_macro2::TokenStream>(s).unwrap())
+ .collect();
+
+ #[cfg(feature = "structural_renderer")]
+ let structural_renderer_tokens: Vec<proc_macro2::TokenStream> = structural_renderers
+ .iter()
+ .map(|s| syn::parse_str::<proc_macro2::TokenStream>(s).unwrap())
+ .collect();
+
+ #[cfg(feature = "structural_renderer")]
+ let structural_render = quote! {
+ fn structural_render(
+ any: ::mingling::AnyOutput<Self::Enum>,
+ setting: &::mingling::StructuralRendererSetting,
+ ) -> Result<::mingling::RenderResult, ::mingling::error::StructuralRendererSerializeError> {
+ match any.member_id() {
+ #(#structural_renderer_tokens)*
+ _ => {
+ let mut r = ::mingling::RenderResult::default();
+ ::mingling::StructuralRenderer::render(&ResultEmpty, setting, &mut r)?;
+ Ok(r)
+ }
+ }
+ }
+ };
+
+ #[cfg(not(feature = "structural_renderer"))]
+ let structural_render = quote! {};
+
+ #[cfg(feature = "dispatch_tree")]
+ let compile_time_dispatchers: Vec<String> = get_global_set(&COMPILE_TIME_DISPATCHERS)
+ .lock()
+ .unwrap()
+ .clone()
+ .iter()
+ .cloned()
+ .collect();
+
+ #[cfg(feature = "dispatch_tree")]
+ let dispatch_tree_nodes = {
+ let entries: Vec<(String, String, String)> = compile_time_dispatchers
+ .iter()
+ .filter_map(|entry| {
+ let parts: Vec<&str> = entry.split(':').collect();
+ if parts.len() == 3 {
+ Some((
+ parts[0].to_string(),
+ parts[1].to_string(),
+ parts[2].to_string(),
+ ))
+ } else {
+ None
+ }
+ })
+ .collect();
+
+ let get_nodes_fn = dispatch_tree_gen::gen_get_nodes(&entries);
+ let dispatch_trie_fn = dispatch_tree_gen::gen_dispatch_args_trie(&entries);
+
+ quote! {
+ #get_nodes_fn
+ #dispatch_trie_fn
+ }
+ };
+
+ #[cfg(not(feature = "dispatch_tree"))]
+ let dispatch_tree_nodes = quote! {};
+
+ #[cfg(feature = "comp")]
+ let completion_tokens: Vec<proc_macro2::TokenStream> = completions
+ .iter()
+ .map(|s| syn::parse_str::<proc_macro2::TokenStream>(s).unwrap())
+ .collect();
+
+ #[cfg(feature = "comp")]
+ let comp = quote! {
+ fn do_comp(any: &::mingling::AnyOutput<Self::Enum>, ctx: &::mingling::ShellContext) -> ::mingling::Suggest {
+ match any.member_id() {
+ #(#completion_tokens)*
+ _ => ::mingling::Suggest::FileCompletion,
+ }
+ }
+ };
+
+ #[cfg(not(feature = "comp"))]
+ let comp = quote! {};
+
+ // Build render function arms from stored entries
+ let render_fn =
+ if renderer_tokens.is_empty() {
+ quote! {
+ fn render(_any: ::mingling::AnyOutput<Self::Enum>) -> ::mingling::RenderResult {
+ ::mingling::RenderResult::default()
+ }
+ }
+ } else {
+ let render_arms: Vec<_> = renderer_tokens.iter().map(|entry| {
+ let (struct_ident, variant_ident) = parse_entry_pair(entry);
+ let downcast_ty = ident_tokens(&variant_ident.to_string());
+ let resolved_struct = ident_tokens(&struct_ident.to_string());
+ quote! {
+ Self::#variant_ident => {
+ let value = unsafe { any.downcast::<#downcast_ty>().unwrap_unchecked() };
+ <#resolved_struct as ::mingling::Renderer>::render(value)
+ }
+ }
+ }).collect();
+ quote! {
+ fn render(any: ::mingling::AnyOutput<Self::Enum>) -> ::mingling::RenderResult {
+ match any.member_id() {
+ #(#render_arms)*
+ _ => ::mingling::RenderResult::default(),
+ }
+ }
+ }
+ };
+
+ // Build do_chain function (async and sync versions)
+ let chain_arms_async: Vec<_> = chain_tokens.iter().map(|entry| {
+ let (struct_ident, variant_ident) = parse_entry_pair(entry);
+ let downcast_ty = ident_tokens(&variant_ident.to_string());
+ let resolved_struct = ident_tokens(&struct_ident.to_string());
+ quote! {
+ Self::#variant_ident => {
+ let value = unsafe { any.downcast::<#downcast_ty>().unwrap_unchecked() };
+ let fut = async { <#resolved_struct as ::mingling::Chain<Self::Enum>>::proc(value).await };
+ ::std::boxed::Box::pin(fut)
+ }
+ }
+ }).collect();
+
+ let chain_arms_sync: Vec<_> = chain_tokens
+ .iter()
+ .map(|entry| {
+ let (struct_ident, variant_ident) = parse_entry_pair(entry);
+ let downcast_ty = ident_tokens(&variant_ident.to_string());
+ let resolved_struct = ident_tokens(&struct_ident.to_string());
+ quote! {
+ Self::#variant_ident => {
+ let value = unsafe { any.downcast::<#downcast_ty>().unwrap_unchecked() };
+ <#resolved_struct as ::mingling::Chain<Self::Enum>>::proc(value)
+ }
+ }
+ })
+ .collect();
+
+ let do_chain_fn = if chain_tokens.is_empty() {
+ quote! {
+ fn do_chain(_any: ::mingling::AnyOutput<Self::Enum>) -> ::mingling::ChainProcess<Self::Enum> {
+ ::core::panic!("No chain found for type id")
+ }
+ }
+ } else if ASYNC_ENABLED {
+ quote! {
+ fn do_chain(
+ any: ::mingling::AnyOutput<Self::Enum>,
+ ) -> ::std::pin::Pin<::std::boxed::Box<dyn ::std::future::Future<Output = ::mingling::ChainProcess<Self::Enum>> + ::std::marker::Send>> {
+ match any.member_id() {
+ #(#chain_arms_async)*
+ _ => ::core::panic!("No chain found for type id: {:?}", any.type_id()),
+ }
+ }
+ }
+ } else {
+ quote! {
+ fn do_chain(
+ any: ::mingling::AnyOutput<Self::Enum>,
+ ) -> ::mingling::ChainProcess<Self::Enum> {
+ match any.member_id() {
+ #(#chain_arms_sync)*
+ _ => ::core::panic!("No chain found for type id: {:?}", any.type_id()),
+ }
+ }
+ }
+ };
+
+ let help_tokens: Vec<proc_macro2::TokenStream> = get_global_set(&HELP_REQUESTS)
+ .lock()
+ .unwrap()
+ .clone()
+ .iter()
+ .map(|s| syn::parse_str::<proc_macro2::TokenStream>(s).unwrap())
+ .collect();
+
+ let num_variants = packed_types.len();
+ let repr_type = if u8::try_from(num_variants).is_ok() {
+ quote! { u8 }
+ } else if u16::try_from(num_variants).is_ok() {
+ quote! { u16 }
+ } else if u32::try_from(num_variants).is_ok() {
+ quote! { u32 }
+ } else {
+ quote! { u128 }
+ };
+
+ let expanded = quote! {
+ #[derive(Debug, PartialEq, Eq, Clone, Copy)]
+ #[repr(#repr_type)]
+ #[allow(nonstandard_style)]
+ pub enum #name {
+ #(#packed_types),*
+ }
+
+ impl ::std::fmt::Display for #name {
+ fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
+ match self {
+ #(#name::#packed_types => write!(f, stringify!(#packed_types)),)*
+ }
+ }
+ }
+
+ impl ::mingling::ProgramCollect for #name {
+ type Enum = #name;
+ type ErrorDispatcherNotFound = ErrorDispatcherNotFound;
+ type ErrorRendererNotFound = ErrorRendererNotFound;
+ type ResultEmpty = ResultEmpty;
+
+ fn build_renderer_not_found(member_id: Self::Enum) -> ::mingling::AnyOutput<Self::Enum> {
+ ::mingling::AnyOutput::new(ErrorRendererNotFound::new(member_id.to_string()))
+ }
+ fn build_dispatcher_not_found(args: Vec<String>) -> ::mingling::AnyOutput<Self::Enum> {
+ ::mingling::AnyOutput::new(ErrorDispatcherNotFound::new(args))
+ }
+ fn build_empty_result() -> ::mingling::AnyOutput<Self::Enum> {
+ ::mingling::AnyOutput::new(ResultEmpty)
+ }
+ #render_fn
+ #do_chain_fn
+ fn render_help(any: ::mingling::AnyOutput<Self::Enum>) -> ::mingling::RenderResult {
+ match any.member_id() {
+ #(#help_tokens)*
+ _ => ::mingling::RenderResult::default(),
+ }
+ }
+ fn has_renderer(any: &::mingling::AnyOutput<Self::Enum>) -> bool {
+ match any.member_id() {
+ #(#renderer_exist_tokens)*
+ _ => false
+ }
+ }
+ fn has_chain(any: &::mingling::AnyOutput<Self::Enum>) -> bool {
+ match any.member_id() {
+ #(#chain_exist_tokens)*
+ _ => false
+ }
+ }
+ #dispatch_tree_nodes
+ #structural_render
+ #comp
+ }
+
+ impl #name {
+ /// Creates a new `Program<#name>` instance with default configuration.
+ pub fn new() -> ::mingling::Program<#name> {
+ ::mingling::Program::new()
+ }
+
+ /// Returns a static reference to the global `Program<#name>` singleton.
+ pub fn this() -> &'static ::mingling::Program<#name> {
+ &::mingling::this::<#name>()
+ }
+ }
+ };
+
+ // Clear all global registries to prevent stale state in Rust Analyzer
+ get_global_set(&PACKED_TYPES).lock().unwrap().clear();
+ get_global_set(&CHAINS).lock().unwrap().clear();
+ get_global_set(&CHAINS_EXIST).lock().unwrap().clear();
+ get_global_set(&RENDERERS).lock().unwrap().clear();
+ get_global_set(&RENDERERS_EXIST).lock().unwrap().clear();
+ get_global_set(&HELP_REQUESTS).lock().unwrap().clear();
+ #[cfg(feature = "comp")]
+ get_global_set(&COMPLETIONS).lock().unwrap().clear();
+ #[cfg(feature = "dispatch_tree")]
+ get_global_set(&COMPILE_TIME_DISPATCHERS)
+ .lock()
+ .unwrap()
+ .clear();
+ #[cfg(feature = "structural_renderer")]
+ get_global_set(&STRUCTURAL_RENDERERS)
+ .lock()
+ .unwrap()
+ .clear();
+
+ TokenStream::from(expanded)
+}
diff --git a/mingling_macros/src/func/r_append.rs b/mingling_macros/src/func/r_append.rs
new file mode 100644
index 0000000..247da27
--- /dev/null
+++ b/mingling_macros/src/func/r_append.rs
@@ -0,0 +1,29 @@
+use proc_macro::TokenStream;
+use quote::quote;
+
+use crate::func::r_print::AppendInput;
+
+pub(crate) fn r_append(input: TokenStream) -> TokenStream {
+ let parsed: AppendInput = match syn::parse(input) {
+ Ok(p) => p,
+ Err(e) => return e.to_compile_error().into(),
+ };
+
+ let dst_ident = parsed.dst.clone();
+ let src_tokens = parsed.src;
+
+ let expanded = match dst_ident {
+ Some(dst) => {
+ quote! {
+ #dst.append_other(#src_tokens);
+ }
+ }
+ None => {
+ quote! {
+ __render_result_buffer.append_other(#src_tokens);
+ }
+ }
+ };
+
+ expanded.into()
+}
diff --git a/mingling_macros/src/func/r_eprint.rs b/mingling_macros/src/func/r_eprint.rs
new file mode 100644
index 0000000..97023b0
--- /dev/null
+++ b/mingling_macros/src/func/r_eprint.rs
@@ -0,0 +1,7 @@
+use proc_macro::TokenStream;
+
+use crate::func::r_print::expand_print;
+
+pub(crate) fn r_eprint(input: TokenStream) -> TokenStream {
+ expand_print(input, "eprint")
+}
diff --git a/mingling_macros/src/func/r_eprintln.rs b/mingling_macros/src/func/r_eprintln.rs
new file mode 100644
index 0000000..6bb86b8
--- /dev/null
+++ b/mingling_macros/src/func/r_eprintln.rs
@@ -0,0 +1,7 @@
+use proc_macro::TokenStream;
+
+use crate::func::r_print::expand_print;
+
+pub(crate) fn r_eprintln(input: TokenStream) -> TokenStream {
+ expand_print(input, "eprintln")
+}
diff --git a/mingling_macros/src/func/r_print.rs b/mingling_macros/src/func/r_print.rs
index 86ea52f..20f15b8 100644
--- a/mingling_macros/src/func/r_print.rs
+++ b/mingling_macros/src/func/r_print.rs
@@ -9,7 +9,7 @@ use syn::{Ident, Token};
/// Two forms:
/// - `(ident, format_args...)` — explicit buffer
/// - `(format_args...)` — implicit `__render_result_buffer`
-enum PrintInput {
+pub(crate) enum PrintInput {
Explicit { dst: Ident, args: TokenStream2 },
Implicit { args: TokenStream2 },
}
@@ -29,7 +29,7 @@ impl Parse for PrintInput {
}
}
-fn expand_print(input: TokenStream, method: &str) -> TokenStream {
+pub(crate) fn expand_print(input: TokenStream, method: &str) -> TokenStream {
let parsed: PrintInput = match syn::parse(input) {
Ok(p) => p,
Err(e) => return e.to_compile_error().into(),
@@ -53,30 +53,18 @@ fn expand_print(input: TokenStream, method: &str) -> TokenStream {
expanded.into()
}
-pub(crate) fn r_println(input: TokenStream) -> TokenStream {
- expand_print(input, "println")
-}
-
pub(crate) fn r_print(input: TokenStream) -> TokenStream {
expand_print(input, "print")
}
-pub(crate) fn r_eprintln(input: TokenStream) -> TokenStream {
- expand_print(input, "eprintln")
-}
-
-pub(crate) fn r_eprint(input: TokenStream) -> TokenStream {
- expand_print(input, "eprint")
-}
-
/// Parsed input for `r_append!`.
///
/// Two forms:
/// - `(dst, src)` — explicit buffer and source
/// - `(src)` — implicit `__render_result_buffer`
-struct AppendInput {
- dst: Option<Ident>,
- src: proc_macro2::TokenStream,
+pub(crate) struct AppendInput {
+ pub(crate) dst: Option<Ident>,
+ pub(crate) src: proc_macro2::TokenStream,
}
impl Parse for AppendInput {
@@ -95,33 +83,3 @@ impl Parse for AppendInput {
}
}
}
-
-/// `r_append!` macro: appends the contents of another `RenderResult` to this one.
-///
-/// Two forms:
-/// - `r_append!(dst, src)` — appends `src` into `dst`
-/// - `r_append!(src)` — appends `src` into `__render_result_buffer`
-pub(crate) fn r_append(input: TokenStream) -> TokenStream {
- let parsed: AppendInput = match syn::parse(input) {
- Ok(p) => p,
- Err(e) => return e.to_compile_error().into(),
- };
-
- let dst_ident = parsed.dst.clone();
- let src_tokens = parsed.src;
-
- let expanded = match dst_ident {
- Some(dst) => {
- quote! {
- #dst.append_other(#src_tokens);
- }
- }
- None => {
- quote! {
- __render_result_buffer.append_other(#src_tokens);
- }
- }
- };
-
- expanded.into()
-}
diff --git a/mingling_macros/src/func/r_println.rs b/mingling_macros/src/func/r_println.rs
new file mode 100644
index 0000000..2ba915a
--- /dev/null
+++ b/mingling_macros/src/func/r_println.rs
@@ -0,0 +1,7 @@
+use proc_macro::TokenStream;
+
+use crate::func::r_print::expand_print;
+
+pub(crate) fn r_println(input: TokenStream) -> TokenStream {
+ expand_print(input, "println")
+}
diff --git a/mingling_macros/src/func/register_chain.rs b/mingling_macros/src/func/register_chain.rs
new file mode 100644
index 0000000..4cd139b
--- /dev/null
+++ b/mingling_macros/src/func/register_chain.rs
@@ -0,0 +1,7 @@
+use proc_macro::TokenStream;
+
+use crate::attr::chain;
+
+pub(crate) fn register_chain_impl(input: TokenStream) -> TokenStream {
+ chain::register_chain(input)
+}
diff --git a/mingling_macros/src/func/register_dispatcher.rs b/mingling_macros/src/func/register_dispatcher.rs
new file mode 100644
index 0000000..0b3fbdf
--- /dev/null
+++ b/mingling_macros/src/func/register_dispatcher.rs
@@ -0,0 +1,75 @@
+#[cfg(feature = "dispatch_tree")]
+use just_fmt::snake_case;
+use proc_macro::TokenStream;
+use quote::quote;
+#[cfg(feature = "dispatch_tree")]
+use syn::parse::{Parse, ParseStream};
+#[cfg(feature = "dispatch_tree")]
+use syn::{Ident, LitStr, Result as SynResult, Token};
+
+#[cfg(feature = "dispatch_tree")]
+use crate::COMPILE_TIME_DISPATCHERS;
+#[cfg(feature = "dispatch_tree")]
+use crate::get_global_set;
+
+#[cfg(feature = "dispatch_tree")]
+struct RegisterDispatcherInput {
+ node_name: LitStr,
+ dispatcher_type: Ident,
+ entry_name: Ident,
+}
+
+#[cfg(feature = "dispatch_tree")]
+impl Parse for RegisterDispatcherInput {
+ fn parse(input: ParseStream) -> SynResult<Self> {
+ let node_name: LitStr = input.parse()?;
+ input.parse::<Token![,]>()?;
+ let dispatcher_type: Ident = input.parse()?;
+ input.parse::<Token![,]>()?;
+ let entry_name: Ident = input.parse()?;
+ Ok(RegisterDispatcherInput {
+ node_name,
+ dispatcher_type,
+ entry_name,
+ })
+ }
+}
+
+#[cfg(feature = "dispatch_tree")]
+pub(crate) fn register_dispatcher(input: TokenStream) -> TokenStream {
+ let RegisterDispatcherInput {
+ node_name,
+ dispatcher_type,
+ entry_name,
+ } = syn::parse_macro_input!(input as RegisterDispatcherInput);
+
+ let node_name_str = node_name.value();
+ let static_name = format!(
+ "__internal_dispatcher_{}",
+ snake_case!(node_name_str.clone())
+ );
+ let static_ident = Ident::new(&static_name, proc_macro2::Span::call_site());
+
+ // Register node info in the global collection at compile time
+ // Format: "node.name:DispatcherType:EntryName"
+ get_global_set(&COMPILE_TIME_DISPATCHERS)
+ .lock()
+ .unwrap()
+ .insert(format!(
+ "{}:{}:{}",
+ node_name_str, dispatcher_type, entry_name
+ ));
+
+ let expanded = quote! {
+ #[doc(hidden)]
+ #[allow(nonstandard_style)]
+ pub static #static_ident: #dispatcher_type = #dispatcher_type;
+ };
+
+ expanded.into()
+}
+
+#[cfg(not(feature = "dispatch_tree"))]
+pub(crate) fn register_dispatcher(_input: TokenStream) -> TokenStream {
+ quote! {}.into()
+}
diff --git a/mingling_macros/src/func/register_help.rs b/mingling_macros/src/func/register_help.rs
new file mode 100644
index 0000000..e715244
--- /dev/null
+++ b/mingling_macros/src/func/register_help.rs
@@ -0,0 +1,71 @@
+use proc_macro::TokenStream;
+use quote::ToTokens;
+use syn::TypePath;
+use syn::spanned::Spanned;
+
+use crate::get_global_set;
+
+pub(crate) fn register_help(input: TokenStream) -> TokenStream {
+ // Parse the input as a comma-separated list of arguments
+ let input_parsed = syn::parse_macro_input!(input with syn::punctuated::Punctuated<syn::Expr, syn::Token![,]>::parse_terminated);
+
+ // Check if there are exactly two elements
+ if input_parsed.len() != 2 {
+ return syn::Error::new(
+ input_parsed.span(),
+ "Expected exactly two comma-separated arguments: `EntryType, StructName`",
+ )
+ .to_compile_error()
+ .into();
+ }
+
+ // Extract the two elements
+ let entry_type_expr = &input_parsed[0];
+ let struct_name_expr = &input_parsed[1];
+
+ // Convert expressions to TypePath and Ident
+ let entry_type = match syn::parse2::<TypePath>(entry_type_expr.to_token_stream()) {
+ Ok(ty) => ty,
+ Err(e) => return e.to_compile_error().into(),
+ };
+
+ let struct_name = match syn::parse2::<syn::Ident>(struct_name_expr.to_token_stream()) {
+ Ok(ident) => ident,
+ Err(e) => return e.to_compile_error().into(),
+ };
+
+ // Register the help request mapping
+ let help_entry = build_help_entry(&struct_name, &entry_type);
+ let entry_str = help_entry.to_string();
+
+ // Check if entry was already pre-inserted by `#[help]` attribute
+ let mut helps = get_global_set(&crate::HELP_REQUESTS).lock().unwrap();
+ if helps.contains(&entry_str) {
+ // Already registered by `#[help]`, no duplicate check needed
+ return quote::quote! {}.into();
+ }
+
+ // Check for duplicate variant (different struct, same type)
+ let variant_name = entry_type.path.segments.last().unwrap().ident.to_string();
+ if let Err(err) =
+ crate::check_duplicate_variant(&helps, &entry_str, &variant_name, "help", entry_type.span())
+ {
+ return err.into();
+ }
+
+ helps.insert(entry_str);
+
+ quote::quote! {}.into()
+}
+
+fn build_help_entry(struct_name: &syn::Ident, entry_type: &TypePath) -> proc_macro2::TokenStream {
+ let enum_variant = entry_type.path.segments.last().unwrap().ident.clone();
+ quote::quote! {
+ Self::#enum_variant => {
+ // SAFETY: The member_id check ensures that `any` contains a value of type `#entry_type`,
+ // so downcasting to `#entry_type` is safe.
+ let value = unsafe { any.downcast::<#entry_type>().unwrap_unchecked() };
+ <#struct_name as ::mingling::HelpRequest>::render_help(value)
+ }
+ }
+}
diff --git a/mingling_macros/src/func/register_renderer.rs b/mingling_macros/src/func/register_renderer.rs
new file mode 100644
index 0000000..c5324d9
--- /dev/null
+++ b/mingling_macros/src/func/register_renderer.rs
@@ -0,0 +1,7 @@
+use proc_macro::TokenStream;
+
+use crate::attr::renderer;
+
+pub(crate) fn register_renderer_impl(input: TokenStream) -> TokenStream {
+ renderer::register_renderer(input)
+}
diff --git a/mingling_macros/src/func/register_type.rs b/mingling_macros/src/func/register_type.rs
new file mode 100644
index 0000000..593a2ec
--- /dev/null
+++ b/mingling_macros/src/func/register_type.rs
@@ -0,0 +1,17 @@
+use proc_macro::TokenStream;
+use syn::parse_macro_input;
+
+use crate::PACKED_TYPES;
+use crate::get_global_set;
+
+pub(crate) fn register_type_impl(input: TokenStream) -> TokenStream {
+ let type_ident = parse_macro_input!(input as syn::Ident);
+ let entry_str = type_ident.to_string();
+
+ get_global_set(&PACKED_TYPES)
+ .lock()
+ .unwrap()
+ .insert(entry_str);
+
+ TokenStream::new()
+}
diff --git a/mingling_macros/src/func/render_route.rs b/mingling_macros/src/func/render_route.rs
new file mode 100644
index 0000000..8f390e6
--- /dev/null
+++ b/mingling_macros/src/func/render_route.rs
@@ -0,0 +1,15 @@
+use proc_macro::TokenStream;
+use quote::quote;
+use syn::parse_macro_input;
+
+/// Routes errors to the rendering pipeline instead of the chain pipeline.
+pub(crate) fn render_route(input: TokenStream) -> TokenStream {
+ let expr = parse_macro_input!(input as syn::Expr);
+ let expanded = quote! {
+ match #expr {
+ Ok(r) => r,
+ Err(e) => return <crate::ThisProgram as ::mingling::ProgramCollect>::render(::mingling::AnyOutput::new(e)),
+ }
+ };
+ TokenStream::from(expanded)
+}
diff --git a/mingling_macros/src/func/route.rs b/mingling_macros/src/func/route.rs
new file mode 100644
index 0000000..b338fb0
--- /dev/null
+++ b/mingling_macros/src/func/route.rs
@@ -0,0 +1,16 @@
+use proc_macro::TokenStream;
+use quote::quote;
+use syn::parse_macro_input;
+
+/// Routes execution depending on a condition — early-returns the error from a `Result`,
+/// converting the `Ok` branch to the next chain process value.
+pub(crate) fn route(input: TokenStream) -> TokenStream {
+ let expr = parse_macro_input!(input as syn::Expr);
+ let expanded = quote! {
+ match #expr {
+ Ok(r) => r,
+ Err(e) => return ::mingling::Routable::to_chain(e),
+ }
+ };
+ TokenStream::from(expanded)
+}
diff --git a/mingling_macros/src/func/suggest.rs b/mingling_macros/src/func/suggest.rs
index 0f2026f..85f1afe 100644
--- a/mingling_macros/src/func/suggest.rs
+++ b/mingling_macros/src/func/suggest.rs
@@ -70,24 +70,3 @@ pub(crate) fn suggest(input: TokenStream) -> TokenStream {
expanded.into()
}
-
-pub(crate) fn suggest_enum(input: TokenStream) -> TokenStream {
- let enum_type = parse_macro_input!(input as syn::Type);
-
- let expanded = quote! {{
- let mut enum_suggest = ::mingling::Suggest::new();
- for (name, desc) in <#enum_type>::enums() {
- if desc.is_empty() {
- enum_suggest.insert(::mingling::SuggestItem::new(name.to_string()));
- } else {
- enum_suggest.insert(::mingling::SuggestItem::new_with_desc(
- name.to_string(),
- desc.to_string(),
- ));
- }
- }
- enum_suggest
- }};
-
- expanded.into()
-}
diff --git a/mingling_macros/src/func/suggest_enum.rs b/mingling_macros/src/func/suggest_enum.rs
new file mode 100644
index 0000000..c8e9db0
--- /dev/null
+++ b/mingling_macros/src/func/suggest_enum.rs
@@ -0,0 +1,24 @@
+use proc_macro::TokenStream;
+use quote::quote;
+use syn::parse_macro_input;
+
+pub(crate) fn suggest_enum(input: TokenStream) -> TokenStream {
+ let enum_type = parse_macro_input!(input as syn::Type);
+
+ let expanded = quote! {{
+ let mut enum_suggest = ::mingling::Suggest::new();
+ for (name, desc) in <#enum_type>::enums() {
+ if desc.is_empty() {
+ enum_suggest.insert(::mingling::SuggestItem::new(name.to_string()));
+ } else {
+ enum_suggest.insert(::mingling::SuggestItem::new_with_desc(
+ name.to_string(),
+ desc.to_string(),
+ ));
+ }
+ }
+ enum_suggest
+ }};
+
+ expanded.into()
+}
diff --git a/mingling_macros/src/lib.rs b/mingling_macros/src/lib.rs
index 534efa5..b6656da 100644
--- a/mingling_macros/src/lib.rs
+++ b/mingling_macros/src/lib.rs
@@ -100,10 +100,10 @@
//! |---------|---------------|
//! | `clap` | `dispatcher_clap!` |
//! | `comp` | [`#[completion]`](attr.completion.html), `suggest!`, `suggest_enum!` |
-//! | `extra_macros` | `entry!`, `empty_result!`, `route!`, [`#[program_setup]`](attr.program_setup.html), `group!` |
+//! | `extras` | `entry!`, `empty_result!`, `route!`, [`#[program_setup]`](attr.program_setup.html), `group!` |
//! | `dispatch_tree` | `register_dispatcher!` (enables trie-based command dispatch) |
//! | `structural_renderer` | `#[derive(StructuralData)]`, `pack_structural!`, `pack_err_structural!`, `group_structural!` |
-//! | `structural_renderer` + `extra_macros` | `group_structural!`, `pack_err_structural!` |
+//! | `structural_renderer` + `extras` | `group_structural!`, `pack_err_structural!` |
//! | `async` | Enables async `#[chain]` functions |
//! | `repl` | Enables REPL execution loop |
//!
@@ -143,12 +143,6 @@
#![deny(missing_docs)]
-#[cfg(feature = "extra_macros")]
-use quote::quote;
-
-#[cfg(feature = "extra_macros")]
-use syn::parse_macro_input;
-
use proc_macro::TokenStream;
use std::collections::BTreeSet;
use std::sync::Mutex;
@@ -169,23 +163,20 @@ mod utils;
use attr::completion;
#[cfg(feature = "clap")]
use attr::dispatcher_clap;
-#[cfg(feature = "extra_macros")]
+#[cfg(feature = "extras")]
use attr::program_setup;
use attr::{chain, help, renderer};
use derive::{enum_tag, grouped};
-#[cfg(feature = "extra_macros")]
+#[cfg(feature = "extras")]
use func::entry;
-#[cfg(feature = "extra_macros")]
+#[cfg(feature = "extras")]
pub(crate) use func::group as group_impl;
-#[cfg(feature = "extra_macros")]
+#[cfg(feature = "extras")]
use func::pack_err;
#[cfg(feature = "comp")]
use func::suggest;
use func::{dispatcher, node, pack};
use systems::res_injection;
-#[cfg(feature = "structural_renderer")]
-pub(crate) use systems::structural_data;
-
pub(crate) fn default_program_path() -> proc_macro2::TokenStream {
quote::quote! { crate::ThisProgram }
}
@@ -302,7 +293,7 @@ fn entry_has_variant(entry: &str, variant_name: &str) -> bool {
///
/// - The type must be accessible at the call site (imported or fully qualified).
/// - The alias name (if provided) must not conflict with existing types.
-#[cfg(feature = "extra_macros")]
+#[cfg(feature = "extras")]
#[proc_macro]
pub fn group(input: TokenStream) -> TokenStream {
group_impl::group_macro(input)
@@ -318,11 +309,11 @@ pub fn group(input: TokenStream) -> TokenStream {
/// group_structural!(IoError = std::io::Error);
/// ```
///
-/// Requires the `structural_renderer` and `extra_macros` features.
-#[cfg(all(feature = "structural_renderer", feature = "extra_macros"))]
+/// Requires the `structural_renderer` and `extras` features.
+#[cfg(all(feature = "structural_renderer", feature = "extras"))]
#[proc_macro]
pub fn group_structural(input: TokenStream) -> TokenStream {
- structural_data::group_structural(input)
+ func::group_structural::group_structural(input)
}
/// Creates a `Node` from a dot-separated path string.
@@ -435,7 +426,7 @@ pub fn pack(input: TokenStream) -> TokenStream {
#[cfg(feature = "structural_renderer")]
#[proc_macro]
pub fn pack_structural(input: TokenStream) -> TokenStream {
- structural_data::pack_structural(input)
+ func::pack_structural::pack_structural(input)
}
/// Creates an error struct with a `name: String` field and optional `info: Type` field.
@@ -499,8 +490,8 @@ pub fn pack_structural(input: TokenStream) -> TokenStream {
/// When the `structural_renderer` feature is enabled, the struct also gets
/// `#[derive(serde::Serialize)]`.
///
-/// This macro is only available with the `extra_macros` feature.
-#[cfg(feature = "extra_macros")]
+/// This macro is only available with the `extras` feature.
+#[cfg(feature = "extras")]
#[proc_macro]
pub fn pack_err(input: TokenStream) -> TokenStream {
pack_err::pack_err(input)
@@ -516,11 +507,11 @@ pub fn pack_err(input: TokenStream) -> TokenStream {
/// pack_err_structural!(ErrorNotDir = PathBuf);
/// ```
///
-/// Requires the `structural_renderer` and `extra_macros` features.
-#[cfg(all(feature = "structural_renderer", feature = "extra_macros"))]
+/// Requires the `structural_renderer` and `extras` features.
+#[cfg(all(feature = "structural_renderer", feature = "extras"))]
#[proc_macro]
pub fn pack_err_structural(input: TokenStream) -> TokenStream {
- pack_err::pack_err_structural(input)
+ func::pack_err_structural::pack_err_structural(input)
}
/// Early-returns the error from a `Result`, converting the `Ok` branch to the
@@ -577,17 +568,10 @@ pub fn pack_err_structural(input: TokenStream) -> TokenStream {
/// value.to_chain()
/// }
/// ```
-#[cfg(feature = "extra_macros")]
+#[cfg(feature = "extras")]
#[proc_macro]
pub fn route(input: TokenStream) -> TokenStream {
- let expr = parse_macro_input!(input as syn::Expr);
- let expanded = quote! {
- match #expr {
- Ok(r) => r,
- Err(e) => return ::mingling::Routable::to_chain(e),
- }
- };
- TokenStream::from(expanded)
+ func::route::route(input)
}
/// Routes errors to the rendering pipeline instead of the chain pipeline.
@@ -627,17 +611,10 @@ pub fn route(input: TokenStream) -> TokenStream {
/// Ok(RenderResult::new())
/// }
/// ```
-#[cfg(feature = "extra_macros")]
+#[cfg(feature = "extras")]
#[proc_macro]
pub fn render_route(input: TokenStream) -> TokenStream {
- let expr = parse_macro_input!(input as syn::Expr);
- let expanded = quote! {
- match #expr {
- Ok(r) => r,
- Err(e) => return crate::ThisProgram::render(::mingling::AnyOutput::new(e)),
- }
- };
- TokenStream::from(expanded)
+ func::render_route::render_route(input)
}
/// Creates an empty result value wrapped in `ChainProcess` for early return
@@ -688,13 +665,10 @@ pub fn render_route(input: TokenStream) -> TokenStream {
///
/// [`ResultEmpty`]: https://docs.rs/mingling/latest/mingling/type.ResultEmpty.html
/// [`ChainProcess`]: https://docs.rs/mingling/latest/mingling/enum.ChainProcess.html
-#[cfg(feature = "extra_macros")]
+#[cfg(feature = "extras")]
#[proc_macro]
-pub fn empty_result(_input: TokenStream) -> TokenStream {
- let expanded = quote! {
- <crate::ResultEmpty as ::mingling::Routable::<crate::ThisProgram>>::to_chain(crate::ResultEmpty)
- };
- TokenStream::from(expanded)
+pub fn empty_result(input: TokenStream) -> TokenStream {
+ func::empty_result::empty_result(input)
}
/// Creates a `Dispatcher` implementation for a subcommand.
@@ -718,9 +692,9 @@ pub fn empty_result(_input: TokenStream) -> TokenStream {
/// dispatcher!(MyProgram, "command.path", CommandStruct => EntryStruct);
/// ```
///
-/// ## Abbreviated syntax (requires `extra_macros` feature)
+/// ## Abbreviated syntax (requires `extras` feature)
///
-/// When the `extra_macros` feature is enabled, the `CommandStruct => EntryStruct`
+/// When the `extras` feature is enabled, the `CommandStruct => EntryStruct`
/// portion can be omitted. Struct names are auto-derived from the command path
/// using `PascalCase` conversion:
///
@@ -747,7 +721,7 @@ pub fn empty_result(_input: TokenStream) -> TokenStream {
/// // With explicit program:
/// dispatcher!(MyApp, "status", StatusCommand => StatusEntry);
///
-/// // Abbreviated form (extra_macros required):
+/// // Abbreviated form (extras required):
/// // dispatcher!("remote.add"); // → CMDRemoteAdd, EntryRemoteAdd
/// ```
///
@@ -1120,12 +1094,99 @@ pub fn completion(attr: TokenStream, item: TokenStream) -> TokenStream {
/// - The function must have exactly one parameter of type `&mut Program<G>`.
/// - The function must return `()`.
/// - The function cannot be async.
-#[cfg(feature = "extra_macros")]
+#[cfg(feature = "extras")]
#[proc_macro_attribute]
pub fn program_setup(attr: TokenStream, item: TokenStream) -> TokenStream {
program_setup::setup_attr(attr, item)
}
+/// Declares a command from a plain function.
+///
+/// **This macro is only available with the `extras` feature.**
+///
+/// The `#[command]` attribute converts a function taking `Vec<String>` into a
+/// Mingling command by:
+/// 1. Calling `dispatcher!("command_name")` to register the dispatcher entry.
+/// 2. Generating a `#[chain]` wrapper that bridges the entry type (`Entry{Pascal}`)
+/// to the original function.
+/// 3. Preserving the original function unchanged (including attributes, extensions,
+/// visibility, and asyncness).
+///
+/// # Syntax
+///
+/// ## Simple form (auto-derives names from function name)
+///
+/// ```rust,ignore
+/// #[command]
+/// fn greet(args: Vec<String>) -> Next {
+/// // ...
+/// }
+/// ```
+///
+/// This deduces:
+/// - Command path: `"greet"` (via `dot_case` of function name)
+/// - Dispatcher struct: `CMDGreet`
+/// - Entry struct: `EntryGreet`
+/// - Dispatches via `dispatcher!("greet")`
+///
+/// ## Explicit attributes
+///
+/// ```rust,ignore
+/// #[command(node = "hello.world")]
+/// fn greet(args: Vec<String>) -> Next {
+/// // ...
+/// }
+/// // → dispatcher!("hello.world", CMDGreet => EntryGreet)
+/// ```
+///
+/// ```rust,ignore
+/// #[command(name = MyDispatcher, entry = MyEntry)]
+/// fn greet(args: Vec<String>) -> Next {
+/// // ...
+/// }
+/// // → dispatcher!("greet", MyDispatcher => MyEntry)
+/// ```
+///
+/// ## Extension attributes
+///
+/// Extra bare paths (e.g. `buffer`, `routeify`, `::mingling::macros::routeify`)
+/// are emitted as `#[ext]` attributes **on the original function**, not on the
+/// chain wrapper. The chain wrapper always uses bare `#[::mingling::macros::chain]`.
+///
+/// ```rust,ignore
+/// #[command(buffer)]
+/// fn greet(args: Vec<String>) {
+/// r_println!("Hello!");
+/// }
+/// ```
+///
+/// # Resource injection
+///
+/// Parameters after the first are treated as resource injections and passed
+/// through to the generated `#[chain]` wrapper unchanged (as reference params):
+///
+/// ```rust,ignore
+/// #[command]
+/// fn greet(args: Vec<String>, ec: &mut ResExitCode) -> Next {
+/// ec.exit_code = 0;
+/// // ...
+/// }
+/// ```
+///
+/// The generated chain wrapper calls the original function with `entry.into()`
+/// for the first argument and passes all subsequent arguments directly.
+///
+/// # Requirements
+///
+/// - The function must have at least one parameter (the `Vec<String>` entry argument).
+/// - The function must not have a `self` parameter.
+/// - Visibility (`pub` etc.) and `async` are preserved on the original function.
+#[cfg(feature = "extras")]
+#[proc_macro_attribute]
+pub fn command(attr: TokenStream, item: TokenStream) -> TokenStream {
+ attr::command::command_attr(attr, item)
+}
+
/// Declares a `Dispatcher` that uses `clap::Parser` for argument parsing.
///
/// **This macro is only available with the `clap` feature.**
@@ -1212,7 +1273,7 @@ pub fn dispatcher_clap(attr: TokenStream, item: TokenStream) -> TokenStream {
///
/// - `pack!` — For creating the wrapper types used with `entry!`.
/// - `dispatcher!` — Which implicitly creates entry types via `pack!`.
-#[cfg(feature = "extra_macros")]
+#[cfg(feature = "extras")]
#[proc_macro]
pub fn entry(input: TokenStream) -> TokenStream {
entry::entry(input)
@@ -1233,7 +1294,7 @@ pub fn entry(input: TokenStream) -> TokenStream {
/// enum variant for `EntryType` to the help rendering logic in `HelpStruct`.
#[proc_macro]
pub fn register_help(input: TokenStream) -> TokenStream {
- help::register_help(input)
+ func::register_help::register_help(input)
}
/// Registers a dispatcher at compile time for the `dispatch_tree` feature.
@@ -1262,7 +1323,7 @@ pub fn register_help(input: TokenStream) -> TokenStream {
/// - `dispatch_tree_gen` module — The trie generation logic.
#[proc_macro]
pub fn register_dispatcher(input: TokenStream) -> TokenStream {
- dispatcher::register_dispatcher(input)
+ func::register_dispatcher::register_dispatcher(input)
}
/// Declares a help rendering function for an entry type.
@@ -1358,8 +1419,8 @@ pub fn help(attr: TokenStream, item: TokenStream) -> TokenStream {
/// fn some_item() {}
/// ```
#[proc_macro_attribute]
-pub fn mlint(_attr: TokenStream, item: TokenStream) -> TokenStream {
- item
+pub fn mlint(attr: TokenStream, item: TokenStream) -> TokenStream {
+ attr::mlint::mlint(attr, item)
}
/// Extension attribute macro that transforms `expr?` into `route!(expr)`.
@@ -1377,7 +1438,7 @@ pub fn mlint(_attr: TokenStream, item: TokenStream) -> TokenStream {
/// StateCalculate { number_a: a, operator: op, ... }.to_chain()
/// }
/// ```
-#[cfg(feature = "extra_macros")]
+#[cfg(feature = "extras")]
#[proc_macro_attribute]
pub fn routeify(attr: TokenStream, item: TokenStream) -> TokenStream {
extensions::routeify::routeify_impl(attr, item)
@@ -1403,7 +1464,7 @@ pub fn routeify(attr: TokenStream, item: TokenStream) -> TokenStream {
/// Ok(RenderResult::new())
/// }
/// ```
-#[cfg(feature = "extra_macros")]
+#[cfg(feature = "extras")]
#[proc_macro_attribute]
pub fn renderify(attr: TokenStream, item: TokenStream) -> TokenStream {
extensions::renderify::renderify_impl(attr, item)
@@ -1475,7 +1536,7 @@ pub fn buffer(attr: TokenStream, item: TokenStream) -> TokenStream {
/// ```
#[proc_macro]
pub fn r_println(input: TokenStream) -> TokenStream {
- func::r_print::r_println(input)
+ func::r_println::r_println(input)
}
/// Prints text to a `RenderResult` buffer, without a trailing newline.
@@ -1537,7 +1598,7 @@ pub fn r_print(input: TokenStream) -> TokenStream {
/// ```
#[proc_macro]
pub fn r_eprintln(input: TokenStream) -> TokenStream {
- func::r_print::r_eprintln(input)
+ func::r_eprintln::r_eprintln(input)
}
/// Prints text to a `RenderResult` buffer (standard error style), without a trailing newline.
@@ -1570,7 +1631,7 @@ pub fn r_eprintln(input: TokenStream) -> TokenStream {
/// ```
#[proc_macro]
pub fn r_eprint(input: TokenStream) -> TokenStream {
- func::r_print::r_eprint(input)
+ func::r_eprint::r_eprint(input)
}
/// Appends the contents of one `RenderResult` to another.
@@ -1600,7 +1661,7 @@ pub fn r_eprint(input: TokenStream) -> TokenStream {
/// ```
#[proc_macro]
pub fn r_append(input: TokenStream) -> TokenStream {
- func::r_print::r_append(input)
+ func::r_append::r_append(input)
}
/// Derive macro for automatically implementing the `Grouped` trait on a struct.
@@ -1701,7 +1762,7 @@ pub fn derive_enum_tag(input: TokenStream) -> TokenStream {
#[cfg(feature = "structural_renderer")]
#[proc_macro_derive(StructuralData)]
pub fn derive_structural_data(input: TokenStream) -> TokenStream {
- structural_data::derive_structural_data(input)
+ derive::structural_data::derive_structural_data(input)
}
/// Derive macro for implementing both `Grouped` and `serde::Serialize` on a struct.
@@ -1818,7 +1879,7 @@ pub fn gen_program(input: TokenStream) -> TokenStream {
#[cfg(feature = "comp")]
#[proc_macro]
pub fn program_comp_gen(input: TokenStream) -> TokenStream {
- func::gen_program::program_comp_gen_impl(input)
+ func::program_comp_gen::program_comp_gen_impl(input)
}
/// Registers a type into the global packed types registry for inclusion in
@@ -1843,7 +1904,7 @@ pub fn program_comp_gen(input: TokenStream) -> TokenStream {
/// Panics if the global `PACKED_TYPES` mutex is poisoned.
#[proc_macro]
pub fn register_type(input: TokenStream) -> TokenStream {
- func::gen_program::register_type_impl(input)
+ func::register_type::register_type_impl(input)
}
/// Registers a chain mapping function into the global chain registry.
@@ -1861,7 +1922,7 @@ pub fn register_type(input: TokenStream) -> TokenStream {
/// Panics if the global `CHAINS` mutex is poisoned.
#[proc_macro]
pub fn register_chain(input: TokenStream) -> TokenStream {
- func::gen_program::register_chain_impl(input)
+ func::register_chain::register_chain_impl(input)
}
/// Registers a renderer mapping function into the global renderer registry.
@@ -1879,7 +1940,7 @@ pub fn register_chain(input: TokenStream) -> TokenStream {
/// Panics if the global `RENDERERS` mutex is poisoned.
#[proc_macro]
pub fn register_renderer(input: TokenStream) -> TokenStream {
- func::gen_program::register_renderer_impl(input)
+ func::register_renderer::register_renderer_impl(input)
}
/// Internal macro used by `gen_program!` to generate the fallback types for
@@ -1889,7 +1950,7 @@ pub fn register_renderer(input: TokenStream) -> TokenStream {
/// be called directly by user code.
#[proc_macro]
pub fn program_fallback_gen(input: TokenStream) -> TokenStream {
- func::gen_program::program_fallback_gen_impl(input)
+ func::program_fallback_gen::program_fallback_gen_impl(input)
}
/// Internal macro used by `gen_program!` to generate the final program enum
@@ -1943,7 +2004,7 @@ pub fn program_fallback_gen(input: TokenStream) -> TokenStream {
/// ```
#[proc_macro]
pub fn program_final_gen(input: TokenStream) -> TokenStream {
- func::gen_program::program_final_gen_impl(input)
+ func::program_final_gen::program_final_gen_impl(input)
}
/// Builds a `Suggest` instance with inline suggestion items.
@@ -2064,5 +2125,5 @@ pub fn suggest(input: TokenStream) -> TokenStream {
#[cfg(feature = "comp")]
#[proc_macro]
pub fn suggest_enum(input: TokenStream) -> TokenStream {
- suggest::suggest_enum(input)
+ func::suggest_enum::suggest_enum(input)
}
diff --git a/mingling_macros/src/systems/dispatch_tree_gen.rs b/mingling_macros/src/systems/dispatch_tree_gen.rs
index 7383421..fe44a49 100644
--- a/mingling_macros/src/systems/dispatch_tree_gen.rs
+++ b/mingling_macros/src/systems/dispatch_tree_gen.rs
@@ -1,27 +1,22 @@
-use std::collections::{BTreeMap, HashMap};
+use std::collections::BTreeMap;
use just_fmt::snake_case;
use proc_macro2::TokenStream;
use quote::quote;
-use crate::func::gen_program::resolve_type;
-
/// Generate the `get_nodes()` function body for a ProgramCollect impl.
-/// If `pathf_map` is non-empty, resolves internal dispatcher statics using full paths.
-pub(crate) fn gen_get_nodes(
- entries: &[(String, String, String)],
- pathf_map: &HashMap<String, String>,
-) -> TokenStream {
+pub(crate) fn gen_get_nodes(entries: &[(String, String, String)]) -> TokenStream {
let mut node_entries = Vec::new();
for (node_name, _disp_type, _entry_name) in entries {
let static_name_str = format!("__internal_dispatcher_{}", snake_case!(node_name));
- let resolved = resolve_type(&static_name_str, pathf_map);
+ let static_ident =
+ proc_macro2::Ident::new(&static_name_str, proc_macro2::Span::call_site());
let node_display_name = node_name.replace('.', " ");
let node_display_lit = syn::LitStr::new(&node_display_name, proc_macro2::Span::call_site());
node_entries.push(quote! {
- (#node_display_lit.to_string(), & #resolved)
+ (#node_display_lit.to_string(), &#static_ident)
});
}
@@ -38,20 +33,13 @@ pub(crate) fn gen_get_nodes(
///
/// Builds a hardcoded match tree: at each depth, group nodes by character.
/// Single-node groups use `starts_with`; multi-node groups recurse with `nth()` match.
-///
-/// If `pathf_map` is non-empty, resolves dispatcher types using full paths.
-pub(crate) fn gen_dispatch_args_trie(
- entries: &[(String, String, String)],
- pathf_map: &HashMap<String, String>,
-) -> TokenStream {
- // Prepare (display_name, disp_type) pairs.
- // display_name = node_name.replace('.', " ")
+pub(crate) fn gen_dispatch_args_trie(entries: &[(String, String, String)]) -> TokenStream {
let nodes: Vec<(String, String)> = entries
.iter()
.map(|(name, disp, _)| (name.replace('.', " "), disp.clone()))
.collect();
- let dispatch_body = build_dispatch_body(&nodes, 0, pathf_map);
+ let dispatch_body = build_dispatch_body(&nodes, 0);
quote! {
fn dispatch_args_trie(
@@ -70,19 +58,13 @@ pub(crate) fn gen_dispatch_args_trie(
///
/// `nodes`: slice of (display_name, disp_type) for commands that share the same prefix so far.
/// `depth`: The character index currently being matched.
-/// `pathf_map`: optional mapping from type name to full path for resolving dispatchers.
-fn build_dispatch_body(
- nodes: &[(String, String)],
- depth: usize,
- pathf_map: &HashMap<String, String>,
-) -> TokenStream {
+fn build_dispatch_body(nodes: &[(String, String)], depth: usize) -> TokenStream {
if nodes.is_empty() {
return quote! {
return Ok(Self::build_dispatcher_not_found(raw.to_vec()));
};
}
- // Group by character at `depth`
let mut groups: BTreeMap<char, Vec<(String, String)>> = BTreeMap::new();
let mut exact_nodes: Vec<(String, String)> = Vec::new();
@@ -97,18 +79,17 @@ fn build_dispatch_body(
}
}
- // Build a dispatch arm for a single node via `starts_with`
let make_starts_with_arm = |name: &str, disp_type: &str| -> TokenStream {
let name_space = format!("{} ", name);
let name_lit = syn::LitStr::new(&name_space, proc_macro2::Span::call_site());
- let disp_resolved = resolve_type(disp_type, pathf_map);
+ let disp_ident = proc_macro2::Ident::new(disp_type, proc_macro2::Span::call_site());
let prefix_word_count = name.split_whitespace().count();
quote! {
if raw_str.starts_with(#name_lit) {
let prefix_len = #prefix_word_count;
let trimmed_args: Vec<String> = raw.iter().skip(prefix_len).cloned().collect();
- let __cp = <#disp_resolved as ::mingling::Dispatcher<Self::Enum>>::begin(
- &#disp_resolved::default(),
+ let __cp = <#disp_ident as ::mingling::Dispatcher<Self::Enum>>::begin(
+ &#disp_ident::default(),
trimmed_args,
);
return match __cp {
@@ -136,7 +117,7 @@ fn build_dispatch_body(
}
});
} else {
- let sub_body = build_dispatch_body(sub_nodes, depth + 1, pathf_map);
+ let sub_body = build_dispatch_body(sub_nodes, depth + 1);
arms.push(quote! {
Some(#ch_char) => {
#sub_body
diff --git a/mingling_macros/src/systems/structural_data.rs b/mingling_macros/src/systems/structural_data.rs
index 0a783ec..512d318 100644
--- a/mingling_macros/src/systems/structural_data.rs
+++ b/mingling_macros/src/systems/structural_data.rs
@@ -1,336 +1,6 @@
-#![allow(dead_code)]
-
-use proc_macro::TokenStream;
-use quote::quote;
-use syn::{DeriveInput, Ident, TypePath, parse_macro_input};
-
-use crate::get_global_set;
-
-/// Derive macro for `StructuralData`.
-///
-/// This marks a type as eligible for structured output (JSON / YAML / TOML / RON).
-/// The type must also implement `serde::Serialize` — the generated `impl StructuralData`
-/// will fail to compile if `Serialize` is not in scope or implemented.
-///
-/// Also registers the type name in the global `STRUCTURED_TYPES` registry so that
-/// the `structural_render` match arm is generated by `gen_program!()`.
-pub(crate) fn derive_structural_data(input: TokenStream) -> TokenStream {
- let input = parse_macro_input!(input as DeriveInput);
- let type_name = input.ident;
-
- // Register in STRUCTURED_TYPES
- let type_name_str = type_name.to_string();
- get_global_set(&crate::STRUCTURED_TYPES)
- .lock()
- .unwrap()
- .insert(type_name_str);
-
- // Generate BOTH the sealed impl AND the StructuralData impl.
- // Users cannot implement StructuralDataSealed manually (it's #[doc(hidden)]),
- // so the only way to get StructuralData is through this derive macro.
- let expanded = quote! {
- impl ::mingling::__private::StructuralDataSealed<crate::ThisProgram> for #type_name {}
- impl ::mingling::__private::StructuralData<crate::ThisProgram> for #type_name {}
- };
-
- expanded.into()
-}
-
-/// `pack_structural!` — like `pack!` but also marks the type as supporting
-/// structured output via `StructuralData`.
-///
-/// # Syntax
-///
-/// ```rust,ignore
-/// pack_structural!(Info = (String, i32));
-/// ```
-///
-/// This is equivalent to:
-/// ```rust,ignore
-/// pack!(Info = (String, i32));
-/// impl ::mingling::StructuralData for Info {}
-/// ```
-pub(crate) fn pack_structural(input: TokenStream) -> TokenStream {
- // Parse same input format as `pack!`
- let input_parsed = syn::parse_macro_input!(input as PackStructuralInput);
- let type_name = input_parsed.type_name;
- let inner_type = input_parsed.inner_type;
- let attrs = input_parsed.attrs;
- let program_path = crate::default_program_path();
-
- // Register in STRUCTURED_TYPES
- let type_name_str = type_name.to_string();
- get_global_set(&crate::STRUCTURED_TYPES)
- .lock()
- .unwrap()
- .insert(type_name_str);
-
- // Struct definition (with Serialize derive, same as pack! under structural_renderer)
- #[cfg(not(feature = "structural_renderer"))]
- let struct_def = quote! {
- #(#attrs)*
- pub struct #type_name {
- pub inner: #inner_type,
- }
- };
-
- #[cfg(feature = "structural_renderer")]
- let struct_def = quote! {
- #(#attrs)*
- #[derive(serde::Serialize)]
- pub struct #type_name {
- pub inner: #inner_type,
- }
- };
-
- // Helper impls (same as pack!)
- let new_impl = quote! {
- impl #type_name {
- pub fn new(inner: #inner_type) -> Self {
- Self { inner }
- }
- }
- };
-
- let from_into_impl = quote! {
- impl From<#inner_type> for #type_name {
- fn from(inner: #inner_type) -> Self {
- Self::new(inner)
- }
- }
- impl From<#type_name> for #inner_type {
- fn from(wrapper: #type_name) -> #inner_type {
- wrapper.inner
- }
- }
- };
-
- let as_ref_impl = quote! {
- impl ::std::convert::AsRef<#inner_type> for #type_name {
- fn as_ref(&self) -> &#inner_type {
- &self.inner
- }
- }
- impl ::std::convert::AsMut<#inner_type> for #type_name {
- fn as_mut(&mut self) -> &mut #inner_type {
- &mut self.inner
- }
- }
- };
-
- let deref_impl = quote! {
- impl ::std::ops::Deref for #type_name {
- type Target = #inner_type;
- fn deref(&self) -> &Self::Target {
- &self.inner
- }
- }
- impl ::std::ops::DerefMut for #type_name {
- fn deref_mut(&mut self) -> &mut Self::Target {
- &mut self.inner
- }
- }
- };
-
- let default_impl = quote! {
- impl ::std::default::Default for #type_name
- where
- #inner_type: ::std::default::Default,
- {
- fn default() -> Self {
- Self::new(::std::default::Default::default())
- }
- }
- };
-
- let register_impl = quote! {
- ::mingling::macros::register_type!(#type_name);
- };
-
- // StructuralData impl + sealed + registration
- let structural_impl = quote! {
- impl ::mingling::__private::StructuralDataSealed<crate::ThisProgram> for #type_name {}
- impl ::mingling::__private::StructuralData<crate::ThisProgram> for #type_name {}
- };
-
- let expanded = quote! {
- #struct_def
-
- #new_impl
- #from_into_impl
- #as_ref_impl
- #deref_impl
- #default_impl
- #register_impl
- #structural_impl
-
- impl Into<::mingling::AnyOutput<#program_path>> for #type_name {
- fn into(self) -> ::mingling::AnyOutput<#program_path> {
- ::mingling::AnyOutput::new(self)
- }
- }
-
- impl Into<::mingling::ChainProcess<#program_path>> for #type_name {
- fn into(self) -> ::mingling::ChainProcess<#program_path> {
- ::mingling::AnyOutput::new(self).route_chain()
- }
- }
-
- impl ::mingling::Grouped<#program_path> for #type_name {
- fn member_id() -> #program_path {
- #program_path::#type_name
- }
- }
- };
-
- expanded.into()
-}
-
-/// Input for `pack_structural!` — same format as `pack!`.
-struct PackStructuralInput {
- attrs: Vec<syn::Attribute>,
- type_name: Ident,
- inner_type: syn::Type,
-}
-
-impl syn::parse::Parse for PackStructuralInput {
- fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
- let attrs = input.call(syn::Attribute::parse_outer)?;
- let type_name: Ident = input.parse()?;
- input.parse::<syn::Token![=]>()?;
- let inner_type: syn::Type = input.parse()?;
- Ok(PackStructuralInput {
- attrs,
- type_name,
- inner_type,
- })
- }
-}
-
-/// `group_structural!` — like `group!` but also marks the type as supporting
-/// structured output via `StructuralData`.
-///
-/// # Syntax
-///
-/// ```rust,ignore
-/// group_structural!(Info = (String, i32));
-/// ```
-///
-/// This is equivalent to:
-/// ```rust,ignore
-/// group!(Info = (String, i32));
-/// impl ::mingling::StructuralData for Info {}
-/// ```
-pub(crate) fn group_structural(input: TokenStream) -> TokenStream {
- // Parse the same input as group!
- let input_parsed = syn::parse_macro_input!(input as GroupStructuralInput);
-
- let is_aliased = matches!(&input_parsed, GroupStructuralInput::Aliased { .. });
-
- let (type_path, type_name, alias_stmt) = match &input_parsed {
- GroupStructuralInput::Plain(type_path) => {
- let name = type_path
- .path
- .segments
- .last()
- .expect("TypePath must have at least one segment")
- .ident
- .clone();
- (type_path.clone(), name, quote! {})
- }
- GroupStructuralInput::Aliased { alias, type_path } => {
- let alias_stmt = quote! {
- pub(crate) type #alias = #type_path;
- };
- (type_path.clone(), alias.clone(), alias_stmt)
- }
- };
-
- let type_name_str = type_name.to_string();
-
- // Register in STRUCTURED_TYPES
- get_global_set(&crate::STRUCTURED_TYPES)
- .lock()
- .unwrap()
- .insert(type_name_str);
-
- let program_path = crate::default_program_path();
-
- // Generate unique module name
- let segments: Vec<String> = type_path
- .path
- .segments
- .iter()
- .map(|seg| seg.ident.to_string().to_lowercase())
- .collect();
- let module_name = Ident::new(
- &format!("internal_group_{}", segments.join("_")),
- proc_macro2::Span::call_site(),
- );
-
- // Generate the appropriate `use` statement for the original type
- // (consistent with gen_type_use in group_impl.rs)
- let type_use = if type_path.path.segments.len() > 1 {
- quote! { #[allow(unused_imports)] use #type_path; }
- } else {
- let ident = type_path
- .path
- .segments
- .last()
- .expect("TypePath must have at least one segment")
- .ident
- .clone();
- quote! { #[allow(unused_imports)] use super::#ident; }
- };
-
- let alias_use = if is_aliased {
- quote! { use super::#type_name; }
- } else {
- quote! {}
- };
-
- let expanded = quote! {
- #alias_stmt
- #[allow(non_camel_case_types)]
- mod #module_name {
- use #program_path as __MinglingProgram;
- #type_use
- #alias_use
-
- impl ::mingling::Grouped<__MinglingProgram> for #type_name {
- fn member_id() -> __MinglingProgram {
- __MinglingProgram::#type_name
- }
- }
-
- impl ::mingling::__private::StructuralDataSealed<crate::ThisProgram> for #type_name {}
- impl ::mingling::__private::StructuralData<crate::ThisProgram> for #type_name {}
-
- ::mingling::macros::register_type!(#type_name);
- }
- };
-
- expanded.into()
-}
-
-/// Input for `group_structural!` — same format as `group!`.
-enum GroupStructuralInput {
- Plain(TypePath),
- Aliased { alias: Ident, type_path: TypePath },
-}
-
-impl syn::parse::Parse for GroupStructuralInput {
- fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
- let fork = input.fork();
- let _first: Ident = fork.parse()?;
- if fork.peek(syn::Token![=]) {
- let alias: Ident = input.parse()?;
- let _eq: syn::Token![=] = input.parse()?;
- let type_path: TypePath = input.parse()?;
- Ok(GroupStructuralInput::Aliased { alias, type_path })
- } else {
- let type_path: TypePath = input.parse()?;
- Ok(GroupStructuralInput::Plain(type_path))
- }
- }
-}
+//! Legacy structural data module.
+//!
+//! Functions have been moved to:
+//! - `derive::structural_data` — `derive_structural_data`
+//! - `func::pack_structural` — `pack_structural`
+//! - `func::group_structural` — `group_structural`