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.rs6
-rw-r--r--mingling_macros/src/attr/chain.rs95
-rw-r--r--mingling_macros/src/attr/command.rs365
-rw-r--r--mingling_macros/src/attr/completion.rs39
-rw-r--r--mingling_macros/src/attr/dispatcher_clap.rs14
-rw-r--r--mingling_macros/src/attr/help.rs98
-rw-r--r--mingling_macros/src/attr/metadata.rs87
-rw-r--r--mingling_macros/src/attr/mlint.rs7
-rw-r--r--mingling_macros/src/attr/renderer.rs47
-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.rs9
-rw-r--r--mingling_macros/src/extensions/buffer.rs95
-rw-r--r--mingling_macros/src/extensions/renderify.rs48
-rw-r--r--mingling_macros/src/extensions/routeify.rs9
-rw-r--r--mingling_macros/src/func.rs35
-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.rs650
-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.rs408
-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.rs85
-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_metadata.rs67
-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.rs64
-rw-r--r--mingling_macros/src/func/suggest_enum.rs24
-rw-r--r--mingling_macros/src/lib.rs641
-rw-r--r--mingling_macros/src/systems/dispatch_tree_gen.rs107
-rw-r--r--mingling_macros/src/systems/structural_data.rs342
48 files changed, 2990 insertions, 1399 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..59544a8 100644
--- a/mingling_macros/src/attr.rs
+++ b/mingling_macros/src/attr.rs
@@ -1,9 +1,13 @@
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 metadata;
+pub(crate) mod mlint;
+#[cfg(feature = "extras")]
pub(crate) mod program_setup;
pub(crate) mod renderer;
diff --git a/mingling_macros/src/attr/chain.rs b/mingling_macros/src/attr/chain.rs
index 120e65d..dc28a39 100644
--- a/mingling_macros/src/attr/chain.rs
+++ b/mingling_macros/src/attr/chain.rs
@@ -34,27 +34,72 @@ fn validate_return_type(sig: &Signature) -> Result<(), proc_macro2::TokenStream>
/// Builds the `proc` function implementation inside the generated `Chain` impl.
///
-/// The user's function body is inlined directly, and its result is converted
-/// via `.into()` to `ChainProcess<ProgramType>`.
-#[allow(unused_variables)]
+/// Instead of inlining the user's body, the trait method calls the original
+/// function by name, with resources injected from the application context.
fn generate_proc_fn(
+ fn_name: &Ident,
has_resources: bool,
resources: &[ResourceInjection],
program_type: &proc_macro2::TokenStream,
previous_type: &TypePath,
- prev_param: &Pat,
- fn_body_stmts: &[syn::Stmt],
is_async_fn: bool,
is_unit_return: bool,
- origin_return_type: &proc_macro2::TokenStream,
) -> proc_macro2::TokenStream {
let immut_resource_stmts = generate_immut_resource_bindings(resources.iter(), program_type);
let mut_resources: Vec<_> = resources.iter().filter(|r| r.is_mut).collect();
+ // Use a fixed parameter name `prev` for the trait method, regardless of
+ // the user's original parameter name (which may be `_` and cannot be
+ // referenced in expression position).
+ let fixed_prev: Pat = syn::parse_quote!(prev);
+
+ // Build the call to the original function with resource arguments injected.
+ // The variable names come from the resource injection bindings
+ // (immut bindings are `let #name = …`, mut closures receive `|#name: &mut T|`),
+ // which match the original function's parameter names.
+ let resource_args: Vec<_> = resources
+ .iter()
+ .map(|res| {
+ let var_name = &res.var_name;
+ quote! { #var_name }
+ })
+ .collect();
+
+ let fn_call = if has_resources {
+ let call = quote! { #fn_name(#fixed_prev, #(#resource_args),*) };
+ if is_async_fn {
+ quote! { #call.await }
+ } else {
+ call
+ }
+ } else {
+ let call = quote! { #fn_name(#fixed_prev) };
+ if is_async_fn {
+ quote! { #call.await }
+ } else {
+ call
+ }
+ };
+
+ // Convert the function call to a syn::Stmt so existing wrapping functions can use it.
+ // For non-unit returns, wrap in `to_chain()` so mutable-resource closures
+ // return `ChainProcess<C>` (as required by `__modify_res_and_return_route`).
+ let fn_call_expr: syn::Expr = if is_unit_return {
+ syn::parse_quote! { #fn_call }
+ } else {
+ syn::parse_quote! { ::mingling::Routable::<#program_type>::to_chain(#fn_call) }
+ };
+ let fn_call_stmt = syn::Stmt::Expr(fn_call_expr, None);
+
let wrapped_body = if is_async_fn && !mut_resources.is_empty() {
- wrap_body_with_mut_resources_async(fn_body_stmts, &mut_resources, program_type)
+ wrap_body_with_mut_resources_async(&[fn_call_stmt], &mut_resources, program_type)
} else {
- wrap_body_with_mut_resources(fn_body_stmts, &mut_resources, program_type, is_unit_return)
+ wrap_body_with_mut_resources(
+ &[fn_call_stmt],
+ &mut_resources,
+ program_type,
+ is_unit_return,
+ )
};
let proc_body = if is_unit_return {
@@ -62,13 +107,13 @@ fn generate_proc_fn(
quote! {
#(#immut_resource_stmts)*
#wrapped_body;
- <crate::ResultEmpty as ::mingling::Grouped::<crate::ThisProgram>>
+ <crate::ResultEmpty as ::mingling::Routable::<crate::ThisProgram>>
::to_chain(crate::ResultEmpty)
}
} else {
quote! {
#wrapped_body;
- <crate::ResultEmpty as ::mingling::Grouped::<crate::ThisProgram>>
+ <crate::ResultEmpty as ::mingling::Routable::<crate::ThisProgram>>
::to_chain(crate::ResultEmpty)
}
};
@@ -82,23 +127,16 @@ fn generate_proc_fn(
} else {
quote! { #wrapped_body }
};
- // Convert the body result to `ChainProcess` using the user-declared
- // return type as the source type for a fully-qualified `Into` call.
- // This works for both:
- // - `-> Next` / `-> ChainProcess`: identity `From<T> for T`
- // - `-> PackType`: `Into<ChainProcess>` from pack!/derive
quote! {
let __chain_result = { #body };
- <#origin_return_type as ::std::convert::Into<
- ::mingling::ChainProcess<#program_type>
- >>::into(__chain_result)
+ ::mingling::Routable::<#program_type>::to_chain(__chain_result)
}
};
#[cfg(feature = "async")]
{
quote! {
- async fn proc(#prev_param: #previous_type) -> ::mingling::ChainProcess<#program_type> {
+ async fn proc(#fixed_prev: #previous_type) -> ::mingling::ChainProcess<#program_type> {
#proc_body
}
}
@@ -107,7 +145,7 @@ fn generate_proc_fn(
#[cfg(not(feature = "async"))]
{
quote! {
- fn proc(#prev_param: #previous_type) -> ::mingling::ChainProcess<#program_type> {
+ fn proc(#fixed_prev: #previous_type) -> ::mingling::ChainProcess<#program_type> {
#proc_body
}
}
@@ -192,13 +230,12 @@ pub(crate) fn chain_attr(attr: TokenStream, item: TokenStream) -> TokenStream {
}
// Extract the previous type, parameter name, and resource injection params
- let (prev_param, previous_type, resources) = match extract_args_info(&input_fn.sig) {
+ let (_, previous_type, resources) = match extract_args_info(&input_fn.sig) {
Ok(info) => info,
Err(e) => return e.to_compile_error().into(),
};
// Prepare building blocks
- let fn_body = &input_fn.block;
let mut fn_attrs = input_fn.attrs.clone();
fn_attrs.retain(|attr| !attr.path().is_ident("chain"));
let vis = &input_fn.vis;
@@ -215,33 +252,23 @@ pub(crate) fn chain_attr(attr: TokenStream, item: TokenStream) -> TokenStream {
// Always use the default crate-defined program path
let program_type = crate::default_program_path();
- // Extract the user's return type for the explicit Into turbofish
- let origin_return_type = match &input_fn.sig.output {
- ReturnType::Type(_, ty) => quote! { #ty },
- ReturnType::Default => quote! { () },
- };
-
// Generate the `proc` function for the Chain impl
let proc_fn = generate_proc_fn(
+ fn_name,
has_resources,
&resources,
&program_type,
&previous_type,
- &prev_param,
- &fn_body.stmts,
#[cfg(feature = "async")]
is_async_fn,
#[cfg(not(feature = "async"))]
false,
is_unit_return,
- &origin_return_type,
);
- // Preserve the original function untouched, with dead_code allowed
- // since it may only be called through the Chain trait dispatch.
+ // Preserve the original function untouched
// Note: do NOT add `#vis` here — `input_fn` (ItemFn) already contains its own visibility.
let original_fn = quote! {
- #[allow(dead_code)]
#(#fn_attrs)*
#input_fn
};
diff --git a/mingling_macros/src/attr/command.rs b/mingling_macros/src/attr/command.rs
new file mode 100644
index 0000000..9598fa8
--- /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().is_some_and(|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 = 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/completion.rs b/mingling_macros/src/attr/completion.rs
index e917d7d..3ced091 100644
--- a/mingling_macros/src/attr/completion.rs
+++ b/mingling_macros/src/attr/completion.rs
@@ -47,11 +47,8 @@ pub(crate) fn completion_attr(attr: TokenStream, item: TokenStream) -> TokenStre
// Extract the first param pattern and type for the ctx parameter
let first_arg = &inputs[0];
- let (ctx_pat, _ctx_type) = match first_arg {
- FnArg::Typed(PatType { pat, ty, .. }) => {
- let param_pat = (**pat).clone();
- (param_pat, (**ty).clone())
- }
+ let _ctx_type = match first_arg {
+ FnArg::Typed(PatType { ty, .. }) => (**ty).clone(),
FnArg::Receiver(_) => {
return syn::Error::new(
first_arg.span(),
@@ -61,6 +58,7 @@ pub(crate) fn completion_attr(attr: TokenStream, item: TokenStream) -> TokenStre
.into();
}
};
+ let fixed_ctx: Pat = syn::parse_quote!(ctx);
// Extract resources from params 2 through N, skipping ctx
let resources = match extract_resources_from_args(sig, 1) {
@@ -70,7 +68,6 @@ pub(crate) fn completion_attr(attr: TokenStream, item: TokenStream) -> TokenStre
// Get the function body
let fn_body = &input_fn.block;
- let fn_body_stmts = &fn_body.stmts;
// Get function attributes excluding the completion attribute
let mut fn_attrs = input_fn.attrs.clone();
@@ -96,12 +93,26 @@ pub(crate) fn completion_attr(attr: TokenStream, item: TokenStream) -> TokenStre
// Generate immutable resource bindings
let immut_resource_stmts = generate_immut_resource_bindings(resources.iter(), &program_type);
- // Build the comp method body with resource injection
- // Use modify_res for mutable resources same pattern as renderer.rs
- let wrapped_body = if mut_resources.is_empty() {
- quote! { #(#fn_body_stmts)* }
+ // Build the call to the original function with resource arguments injected
+ let resource_args: Vec<_> = resources
+ .iter()
+ .map(|res| {
+ let var_name = &res.var_name;
+ quote! { #var_name }
+ })
+ .collect();
+
+ let fn_call = if has_resources {
+ quote! { #fn_name(#fixed_ctx, #(#resource_args),*) }
+ } else {
+ quote! { #fn_name(#fixed_ctx) }
+ };
+
+ // Wrap the function call with modify_res for mutable resources
+ let inner_call = if mut_resources.is_empty() {
+ fn_call
} else {
- let mut wrapped = quote! { #(#fn_body_stmts)* };
+ let mut wrapped = fn_call;
for res in mut_resources.iter().rev() {
let var_name = &res.var_name;
let inner_type = &res.inner_type;
@@ -117,10 +128,10 @@ pub(crate) fn completion_attr(attr: TokenStream, item: TokenStream) -> TokenStre
let comp_body = if has_resources {
quote! {
#(#immut_resource_stmts)*
- #wrapped_body
+ #inner_call
}
} else {
- quote! { #(#fn_body_stmts)* }
+ quote! { #inner_call }
};
// Generate the struct and implementation
@@ -135,7 +146,7 @@ pub(crate) fn completion_attr(attr: TokenStream, item: TokenStream) -> TokenStre
impl ::mingling::Completion for #struct_name {
type Previous = #previous_type_path;
- fn comp(#ctx_pat: &::mingling::ShellContext) #output {
+ fn comp(#fixed_ctx: &::mingling::ShellContext) #output {
#comp_body
}
}
diff --git a/mingling_macros/src/attr/dispatcher_clap.rs b/mingling_macros/src/attr/dispatcher_clap.rs
index 6083a52..218750c 100644
--- a/mingling_macros/src/attr/dispatcher_clap.rs
+++ b/mingling_macros/src/attr/dispatcher_clap.rs
@@ -39,7 +39,7 @@ impl Parse for ClapOptions {
error_struct = Some(value);
} else if key == "help" {
let value: LitBool = input.parse()?;
- if value.value() == false {
+ if !value.value() {
// help = false is allowed but does nothing
help_enabled = false;
} else {
@@ -108,23 +108,23 @@ pub(crate) fn dispatcher_clap_attr(attr: TokenStream, item: TokenStream) -> Toke
let begin_body = if let Some(ref error_struct) = options.error_struct {
quote! {
if ::mingling::this::<#program_path>().user_context.help {
- return #struct_name::default().to_chain();
+ return ::mingling::Routable::<#program_path>::to_chain(#struct_name::default());
}
match <#struct_name as ::clap::Parser>::try_parse_from(clap_args) {
- Ok(parsed) => parsed.to_chain(),
+ Ok(parsed) => ::mingling::Routable::<#program_path>::to_chain(parsed),
Err(e) => {
- return #error_struct::new(format!("{}", e.render().ansi())).to_render()
+ return ::mingling::Routable::<#program_path>::to_render(#error_struct::new(format!("{}", e.render().ansi())))
},
}
}
} else {
quote! {
if ::mingling::this::<#program_path>().user_context.help {
- return #struct_name::default().to_chain();
+ return ::mingling::Routable::<#program_path>::to_chain(#struct_name::default());
}
let parsed = <#struct_name as ::clap::Parser>::try_parse_from(clap_args)
.unwrap_or_else(|e| e.exit());
- parsed.to_chain()
+ ::mingling::Routable::<#program_path>::to_chain(parsed)
}
};
@@ -171,7 +171,7 @@ pub(crate) fn dispatcher_clap_attr(attr: TokenStream, item: TokenStream) -> Toke
};
let dispatch_tree_entry =
- get_dispatch_tree_entry(&command_name_str, dispatcher_struct, &struct_name);
+ get_dispatch_tree_entry(&command_name_str, dispatcher_struct, struct_name);
let expanded = quote! {
// Keep the original struct definition
diff --git a/mingling_macros/src/attr/help.rs b/mingling_macros/src/attr/help.rs
index 6defae4..b4ad989 100644
--- a/mingling_macros/src/attr/help.rs
+++ b/mingling_macros/src/attr/help.rs
@@ -1,7 +1,7 @@
use proc_macro::TokenStream;
-use quote::{ToTokens, quote};
+use quote::quote;
use syn::spanned::Spanned;
-use syn::{Ident, ItemFn, ReturnType, Signature, TypePath, parse_macro_input};
+use syn::{Ident, ItemFn, Pat, ReturnType, Signature, TypePath, parse_macro_input};
use crate::get_global_set;
use crate::res_injection::{extract_args_info, generate_immut_resource_bindings};
@@ -25,8 +25,8 @@ pub(crate) fn help_attr(item: TokenStream) -> TokenStream {
.into();
}
- // Extract the entry type, parameter name, and resource injection params
- let (prev_param, entry_type, resources) = match extract_args_info(&input_fn.sig) {
+ // Extract the entry type and resource injection params
+ let (_, entry_type, resources) = match extract_args_info(&input_fn.sig) {
Ok(info) => info,
Err(e) => return e.to_compile_error().into(),
};
@@ -66,13 +66,31 @@ pub(crate) fn help_attr(item: TokenStream) -> TokenStream {
// Generate immutable resource bindings
let immut_resource_stmts = generate_immut_resource_bindings(resources.iter(), &program_type);
- // Build the render_help body with resource injection
- // Use modify_res for mutable resources same pattern as renderer.rs
+ // Build the call to the original function with resource arguments injected
+ let resource_args: Vec<_> = resources
+ .iter()
+ .map(|res| {
+ let var_name = &res.var_name;
+ quote! { #var_name }
+ })
+ .collect();
+
+ // Use a fixed parameter name `prev` for the trait method, regardless of
+ // the user's original parameter name (which may be `_` and cannot be
+ // referenced in expression position).
+ let fixed_prev: Pat = syn::parse_quote!(prev);
- let wrapped_body = if mut_resources.is_empty() {
- quote! { #(#fn_body_stmts)* }
+ let fn_call = if has_resources {
+ quote! { #fn_name(#fixed_prev, #(#resource_args),*) }
} else {
- let mut wrapped = quote! { #(#fn_body_stmts)* };
+ quote! { #fn_name(#fixed_prev) }
+ };
+
+ // Wrap the function call with modify_res for mutable resources
+ let inner_call = if mut_resources.is_empty() {
+ fn_call
+ } else {
+ let mut wrapped = fn_call;
for res in mut_resources.iter().rev() {
let var_name = &res.var_name;
let inner_type = &res.inner_type;
@@ -88,10 +106,10 @@ pub(crate) fn help_attr(item: TokenStream) -> TokenStream {
let help_render_body = if has_resources {
quote! {
#(#immut_resource_stmts)*
- #wrapped_body
+ #inner_call
}
} else {
- quote! { #(#fn_body_stmts)* }
+ quote! { #inner_call }
};
// Register the help request mapping
@@ -128,7 +146,7 @@ pub(crate) fn help_attr(item: TokenStream) -> TokenStream {
impl ::mingling::HelpRequest for #struct_name {
type Entry = #entry_type;
- fn render_help(#prev_param: Self::Entry) -> ::mingling::RenderResult {
+ fn render_help(#fixed_prev: Self::Entry) -> ::mingling::RenderResult {
let __help_result = { #help_render_body };
::std::convert::Into::into(__help_result)
}
@@ -136,8 +154,7 @@ pub(crate) fn help_attr(item: TokenStream) -> TokenStream {
::mingling::macros::register_help!(#entry_type, #struct_name);
- // Keep the original function unchanged
- #[allow(dead_code)]
+ // Keep the original function unchanged
#(#fn_attrs)*
#vis fn #fn_name(#original_inputs) -> #original_return_type {
#(#fn_body_stmts)*
@@ -159,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/metadata.rs b/mingling_macros/src/attr/metadata.rs
new file mode 100644
index 0000000..b96e319
--- /dev/null
+++ b/mingling_macros/src/attr/metadata.rs
@@ -0,0 +1,87 @@
+use proc_macro::TokenStream;
+use quote::quote;
+use syn::spanned::Spanned;
+use syn::{Attribute, ItemFn, ReturnType, TypePath, parse_macro_input};
+
+/// Implements the `#[metadata(EntryVariant)]` attribute macro.
+///
+/// It takes the enum variant ident to attach metadata to, and rewrites the
+/// annotated function into:
+/// - an `impl ::mingling::Metadata<ReturnType> for EntryVariant` that calls the
+/// original function,
+/// - a `::mingling::macros::register_metadata!(EntryVariant, ReturnType)` call,
+/// - the preserved original function.
+pub(crate) fn metadata_attr(attr: TokenStream, item: TokenStream) -> TokenStream {
+ let entry_variant = parse_macro_input!(attr as syn::Ident);
+
+ let input_fn = parse_macro_input!(item as ItemFn);
+
+ // The metadata type is the function's return type.
+ let metadata_type = match &input_fn.sig.output {
+ ReturnType::Type(_, ty) => match syn::parse2::<TypePath>(quote! { #ty }) {
+ Ok(ty) => ty,
+ Err(e) => return e.to_compile_error().into(),
+ },
+ ReturnType::Default => {
+ return syn::Error::new(
+ input_fn.sig.span(),
+ "#[metadata] requires the function to have an explicit return type",
+ )
+ .to_compile_error()
+ .into();
+ }
+ };
+
+ // Preserve the original return type exactly as written, so the original
+ // function signature is reproduced unchanged.
+ let original_return_type = match &input_fn.sig.output {
+ ReturnType::Type(_, ty) => quote! { #ty },
+ ReturnType::Default => quote! { () },
+ };
+
+ // Reject async metadata functions: `Metadata::init_metadata` is synchronous.
+ if input_fn.sig.asyncness.is_some() {
+ return syn::Error::new(input_fn.sig.span(), "Metadata function cannot be async")
+ .to_compile_error()
+ .into();
+ }
+
+ let fn_name = &input_fn.sig.ident;
+ let vis = &input_fn.vis;
+ let original_inputs = input_fn.sig.inputs.clone();
+ let fn_body_stmts = &input_fn.block.stmts;
+
+ // Function attributes, excluding the metadata attribute itself.
+ let fn_attrs: Vec<&Attribute> = input_fn
+ .attrs
+ .iter()
+ .filter(|attr| !attr.path().is_ident("metadata"))
+ .collect();
+
+ // A metadata provider is a zero-argument function.
+ if !original_inputs.is_empty() {
+ return syn::Error::new(
+ input_fn.sig.span(),
+ "#[metadata] function cannot take any parameters",
+ )
+ .to_compile_error()
+ .into();
+ }
+
+ let expanded = quote! {
+ impl ::mingling::Metadata<#metadata_type> for #entry_variant {
+ fn init_metadata() -> #metadata_type {
+ #fn_name()
+ }
+ }
+
+ ::mingling::macros::register_metadata!(#entry_variant, #metadata_type);
+
+ #(#fn_attrs)*
+ #vis fn #fn_name(#original_inputs) -> #original_return_type {
+ #(#fn_body_stmts)*
+ }
+ };
+
+ expanded.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/attr/renderer.rs b/mingling_macros/src/attr/renderer.rs
index c7cbb0b..828dc00 100644
--- a/mingling_macros/src/attr/renderer.rs
+++ b/mingling_macros/src/attr/renderer.rs
@@ -1,7 +1,7 @@
use proc_macro::TokenStream;
use quote::{ToTokens, quote};
use syn::spanned::Spanned;
-use syn::{ItemFn, ReturnType, Signature, TypePath, parse_macro_input};
+use syn::{ItemFn, Pat, ReturnType, Signature, TypePath, parse_macro_input};
use crate::get_global_set;
use crate::res_injection::{extract_args_info, generate_immut_resource_bindings};
@@ -31,8 +31,8 @@ pub(crate) fn renderer_attr(attr: TokenStream, item: TokenStream) -> TokenStream
.into();
}
- // Extract the previous type, parameter name, and resource injection params
- let (prev_param, previous_type, resources) = match extract_args_info(&input_fn.sig) {
+ // Extract the previous type and resource injection params
+ let (_, previous_type, resources) = match extract_args_info(&input_fn.sig) {
Ok(info) => info,
Err(e) => return e.to_compile_error().into(),
};
@@ -69,31 +69,53 @@ pub(crate) fn renderer_attr(attr: TokenStream, item: TokenStream) -> TokenStream
let immut_resource_stmts = generate_immut_resource_bindings(resources.iter(), program_type);
let mut_resources: Vec<_> = resources.iter().filter(|r| r.is_mut).collect();
- let inner_body_with_resources = if has_mut_resources {
- let mut wrapped = quote! { #(#fn_body_stmts)* };
+ // Build the call to the original function with resource arguments injected
+ let resource_args: Vec<_> = resources
+ .iter()
+ .map(|res| {
+ let var_name = &res.var_name;
+ quote! { #var_name }
+ })
+ .collect();
+
+ // Use a fixed parameter name `prev` for the trait method, regardless of
+ // the user's original parameter name (which may be `_` and cannot be
+ // referenced in expression position).
+ let fixed_prev: Pat = syn::parse_quote!(prev);
+
+ let fn_call = if has_resources {
+ quote! { #fn_name(#fixed_prev, #(#resource_args),*) }
+ } else {
+ quote! { #fn_name(#fixed_prev) }
+ };
+
+ // Wrap the function call with modify_res for mutable resources
+ let inner_call = if has_mut_resources {
+ let mut wrapped = fn_call;
for res in mut_resources.iter().rev() {
let var_name = &res.var_name;
let inner_type = &res.inner_type;
wrapped = quote! {
- ::mingling::this::<#program_type>().modify_res(|#var_name: &mut #inner_type| {
+ ::mingling::this::<#program_type>()
+ .modify_res(|#var_name: &mut #inner_type| {
#wrapped
})
};
}
wrapped
} else {
- quote! { #(#fn_body_stmts)* }
+ fn_call
};
- // Build the Renderer::render body with resource injection
- // The user's body now directly creates and returns a RenderResult.
+ // Build the Renderer::render body with resource injection.
+ // The trait method injects resources and calls the original function.
let render_fn_body = if has_resources {
quote! {
#(#immut_resource_stmts)*
- #inner_body_with_resources
+ #inner_call
}
} else {
- quote! { #inner_body_with_resources }
+ quote! { #inner_call }
};
// The original function preserves the user's exact signature and body.
@@ -112,14 +134,13 @@ pub(crate) fn renderer_attr(attr: TokenStream, item: TokenStream) -> TokenStream
impl ::mingling::Renderer for #struct_name {
type Previous = #previous_type;
- fn render(#prev_param: Self::Previous) -> ::mingling::RenderResult {
+ fn render(#fixed_prev: Self::Previous) -> ::mingling::RenderResult {
let __renderer_result = { #render_fn_body };
::std::convert::Into::into(__renderer_result)
}
}
// Keep the original function unchanged
- #[allow(dead_code)]
#(#fn_attrs)*
#vis fn #fn_name(#original_inputs) -> #original_return_type {
#(#fn_body_stmts)*
diff --git a/mingling_macros/src/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 aff20e2..f4a6fde 100644
--- a/mingling_macros/src/extensions.rs
+++ b/mingling_macros/src/extensions.rs
@@ -10,9 +10,16 @@ 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 = "extras")]
+pub(crate) mod renderify;
+
+/// Extension: `#[buffer]` — wraps a unit-returning function to return `RenderResult`.
+pub(crate) mod buffer;
+
/// Parsed extensions from an attribute macro like `#[chain(routeify, other_ext)]`.
pub(crate) struct Extensions {
pub(crate) exts: Vec<Ident>,
diff --git a/mingling_macros/src/extensions/buffer.rs b/mingling_macros/src/extensions/buffer.rs
new file mode 100644
index 0000000..27eb612
--- /dev/null
+++ b/mingling_macros/src/extensions/buffer.rs
@@ -0,0 +1,95 @@
+use proc_macro::TokenStream;
+use quote::quote;
+use syn::spanned::Spanned;
+use syn::{ItemFn, ReturnType, Type, parse_macro_input};
+
+/// Checks whether the return type is unit `()`.
+fn is_unit_return_type(sig: &syn::Signature) -> bool {
+ match &sig.output {
+ ReturnType::Type(_, ty) => match &**ty {
+ Type::Tuple(tuple) => tuple.elems.is_empty(),
+ _ => false,
+ },
+ ReturnType::Default => true,
+ }
+}
+
+/// The `#[buffer]` attribute macro.
+///
+/// Wraps a unit-returning function to produce a `::mingling::RenderResult` by
+/// injecting a local `__render_result_buffer` variable. Inside the function
+/// body, the `r_print!` / `r_println!` macros can write into the buffer.
+///
+/// # Example
+///
+/// ```rust,ignore
+/// use mingling::macros::{buffer, r_println};
+///
+/// #[buffer]
+/// fn render_greeting(prev: Greeting) {
+/// r_println!("Hello, {}!", *prev);
+/// }
+/// ```
+///
+/// Expands to:
+///
+/// ```rust,ignore
+/// fn render_greeting(prev: Greeting) -> mingling::RenderResult {
+/// let mut __render_result_buffer = mingling::RenderResult::new();
+/// {
+/// r_println!("Hello, {}!", *prev);
+/// }
+/// __render_result_buffer
+/// }
+/// ```
+pub(crate) fn buffer_impl(attr: TokenStream, item: TokenStream) -> TokenStream {
+ // Reject non-empty attribute arguments; #[buffer] must be bare
+ if !attr.is_empty() {
+ return syn::Error::new(
+ attr.into_iter().next().unwrap().span().into(),
+ "#[buffer] does not accept arguments",
+ )
+ .to_compile_error()
+ .into();
+ }
+
+ // Parse the function item
+ let input_fn = parse_macro_input!(item as ItemFn);
+
+ // Validate the function is not async
+ if input_fn.sig.asyncness.is_some() {
+ return syn::Error::new(input_fn.sig.span(), "Buffer function cannot be async")
+ .to_compile_error()
+ .into();
+ }
+
+ // Validate return type is unit
+ if !is_unit_return_type(&input_fn.sig) {
+ return syn::Error::new(
+ input_fn.sig.span(),
+ "#[buffer] function must not have a return value (must return `()`)",
+ )
+ .to_compile_error()
+ .into();
+ }
+
+ // Get function attributes (excluding the buffer attribute)
+ let mut fn_attrs = input_fn.attrs.clone();
+ fn_attrs.retain(|attr| !attr.path().is_ident("buffer"));
+
+ let vis = &input_fn.vis;
+ let fn_name = &input_fn.sig.ident;
+ let inputs = &input_fn.sig.inputs;
+ let fn_body = &input_fn.block;
+
+ let expanded = quote! {
+ #(#fn_attrs)*
+ #vis fn #fn_name(#inputs) -> ::mingling::RenderResult {
+ let mut __render_result_buffer = ::mingling::RenderResult::new();
+ #fn_body
+ __render_result_buffer
+ }
+ };
+
+ expanded.into()
+}
diff --git a/mingling_macros/src/extensions/renderify.rs b/mingling_macros/src/extensions/renderify.rs
new file mode 100644
index 0000000..9be8acc
--- /dev/null
+++ b/mingling_macros/src/extensions/renderify.rs
@@ -0,0 +1,48 @@
+//! The `#[renderify]` extension — transforms `expr?` into `render_route!(expr)`.
+//!
+//! Designed as an extension for the Mingling attribute macro system, intended
+//! to be used with `#[renderer(renderify)]`, `#[help(renderify)]`,
+//! or standalone as `#[renderify]`.
+//!
+//! # How it works
+//!
+//! The macro parses the function AST and replaces every `Expr::Try` node with an
+//! equivalent `render_route!(expr)` invocation, which routes errors to the
+//! rendering pipeline via `crate::ThisProgram::render(AnyOutput::new(e))`.
+
+use proc_macro::TokenStream;
+use quote::ToTokens;
+use syn::spanned::Spanned;
+use syn::visit_mut::VisitMut;
+use syn::{Expr, ItemFn, parse_macro_input};
+
+struct RenderifyTransform;
+
+impl VisitMut for RenderifyTransform {
+ fn visit_expr_mut(&mut self, expr: &mut Expr) {
+ syn::visit_mut::visit_expr_mut(self, expr);
+
+ if let Expr::Try(try_expr) = expr {
+ let inner = &*try_expr.expr;
+ let inner_tokens = inner.to_token_stream();
+
+ // Set the span of the generated `render_route` ident to the `?` token's span,
+ // so that rust-analyzer resolves the `?` position to the `render_route!` macro
+ // instead of the standard Try trait, showing the render_route macro's docs on hover.
+ let q_span = try_expr.question_token.span();
+ let route_ident = proc_macro2::Ident::new("render_route", q_span);
+
+ if let Ok(macro_expr) = syn::parse2::<Expr>(quote::quote! {
+ ::mingling::macros::#route_ident!(#inner_tokens)
+ }) {
+ *expr = macro_expr;
+ }
+ }
+ }
+}
+
+pub(crate) fn renderify_impl(_attr: TokenStream, item: TokenStream) -> TokenStream {
+ let mut input_fn = parse_macro_input!(item as ItemFn);
+ RenderifyTransform.visit_item_fn_mut(&mut input_fn);
+ input_fn.to_token_stream().into()
+}
diff --git a/mingling_macros/src/extensions/routeify.rs b/mingling_macros/src/extensions/routeify.rs
index f011fb9..7cded54 100644
--- a/mingling_macros/src/extensions/routeify.rs
+++ b/mingling_macros/src/extensions/routeify.rs
@@ -10,6 +10,7 @@
use proc_macro::TokenStream;
use quote::ToTokens;
+use syn::spanned::Spanned;
use syn::visit_mut::VisitMut;
use syn::{Expr, ItemFn, parse_macro_input};
@@ -23,8 +24,14 @@ impl VisitMut for RouteifyTransform {
let inner = &*try_expr.expr;
let inner_tokens = inner.to_token_stream();
+ // Set the span of the generated `route` ident to the `?` token's span,
+ // so that rust-analyzer resolves the `?` position to the `route!` macro
+ // instead of the standard Try trait, showing the route macro's docs on hover.
+ let q_span = try_expr.question_token.span();
+ let route_ident = proc_macro2::Ident::new("route", q_span);
+
if let Ok(macro_expr) = syn::parse2::<Expr>(quote::quote! {
- ::mingling::macros::route!(#inner_tokens)
+ ::mingling::macros::#route_ident!(#inner_tokens)
}) {
*expr = macro_expr;
}
diff --git a/mingling_macros/src/func.rs b/mingling_macros/src/func.rs
index 720b20a..d566208 100644
--- a/mingling_macros/src/func.rs
+++ b/mingling_macros/src/func.rs
@@ -1,12 +1,41 @@
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_metadata;
+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 7128a9a..d50041e 100644
--- a/mingling_macros/src/func/gen_program.rs
+++ b/mingling_macros/src/func/gen_program.rs
@@ -1,90 +1,11 @@
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 empty map when pathf feature is not enabled.
-fn load_pathf_map() -> std::collections::HashMap<String, String> {
- if !cfg!(feature = "pathf") {
- return std::collections::HashMap::new();
- }
- let out_dir = std::env::var("OUT_DIR").ok();
- let crate_name = std::env::var("CARGO_PKG_NAME").ok();
- match (out_dir, crate_name) {
- (Some(dir), Some(name)) => {
- let path = std::path::Path::new(&dir).join(&name).join("type_using.rs");
- match std::fs::read_to_string(&path) {
- Ok(content) => 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(),
- Err(_) => std::collections::HashMap::new(),
- }
- }
- _ => std::collections::HashMap::new(),
- }
-}
-
-/// 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,509 +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);
- crate::CompletionSuggest::new((ctx, suggest)).to_render()
- }
- Err(_) => std::process::exit(1),
- }
+ // 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_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
}
};
+ #[cfg(not(feature = "pathf"))]
+ let pathf_uses: Vec<proc_macro2::TokenStream> = Vec::new();
- #[cfg(not(feature = "async"))]
- let fn_exec_comp = quote! {
- #[doc(hidden)]
- #[::mingling::macros::chain]
- pub fn __exec_completion(prev: CompletionContext) -> Next {
- use ::mingling::Grouped;
+ #[cfg(feature = "pathf")]
+ let super_use = quote! {};
- let read_ctx = ::mingling::ShellContext::try_from(prev.inner);
- match read_ctx {
- Ok(ctx) => {
- let suggest = ::mingling::CompletionHelper::exec_completion::<crate::ThisProgram>(&ctx);
- crate::CompletionSuggest::new((ctx, suggest)).to_render()
- }
- Err(_) => std::process::exit(1),
- }
- }
+ #[cfg(not(feature = "pathf"))]
+ let super_use = quote! {
+ use super::*;
};
- #[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);
+ TokenStream::from(quote! {
+ pub use __this_program_impl::*;
- #[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: std::collections::HashMap<String, String> = if cfg!(feature = "pathf") {
- load_pathf_map()
- } else {
- std::collections::HashMap::new()
- };
-
- 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(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> {
- #[allow(unused_imports)]
+ 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();
-
- #[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();
+ /// Alias for the current program type `ThisProgram`
+ pub type Next = ::mingling::ChainProcess<ThisProgram>;
- 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);
+ ::mingling::macros::pack!(Entry = Vec<String>);
- 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 {
- #[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)
- }
- }
- })
- .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)]
- #[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
+ fn to_render(self) -> ::mingling::ChainProcess<ThisProgram> {
+ match self {
+ ::mingling::ChainProcess::Ok((any, _)) => {
+ ::mingling::ChainProcess::Ok((any, mingling::NextProcess::Renderer))
+ }
+ other => other,
+ }
}
}
- #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>()
- }
+ #comp_gen
+ ::mingling::macros::program_fallback_gen!();
+ ::mingling::macros::program_final_gen!();
}
- };
-
- // 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)
+/// 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 crate_name = match std::env::var("CARGO_PKG_NAME") {
+ Ok(n) => n,
+ Err(_) => return Vec::new(),
+ };
+ 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 36e550a..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 for #type_name {}
- impl ::mingling::__private::StructuralData 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..93d8616
--- /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!(EntryFallback = 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..429e60c
--- /dev/null
+++ b/mingling_macros/src/func/program_final_gen.rs
@@ -0,0 +1,408 @@
+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::METADATA;
+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 metadata_tokens: Vec<proc_macro2::TokenStream> = get_global_set(&METADATA)
+ .lock()
+ .unwrap()
+ .clone()
+ .iter()
+ .map(|s| syn::parse_str::<proc_macro2::TokenStream>(s).unwrap())
+ .collect();
+
+ let get_metadata_fn = if metadata_tokens.is_empty() {
+ quote! {
+ fn get_metadata<T: 'static>(_member_id: Self::Enum) -> Option<T> {
+ None
+ }
+ }
+ } else {
+ let metadata_arms = metadata_tokens.iter().map(|entry| {
+ quote! {
+ #entry
+ }
+ });
+ quote! {
+ fn get_metadata<T: 'static>(member_id: Self::Enum) -> Option<T> {
+ let type_id = ::std::any::TypeId::of::<T>();
+ let any = match member_id {
+ #(#metadata_arms)*
+ _ => None,
+ };
+ any.and_then(|b| b.downcast::<T>().ok().map(|b| *b))
+ }
+ }
+ };
+
+ 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 EntryFallback = EntryFallback;
+ 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_entry_fallback(args: Vec<String>) -> ::mingling::AnyOutput<Self::Enum> {
+ ::mingling::AnyOutput::new(EntryFallback::new(args))
+ }
+ fn build_empty_result() -> ::mingling::AnyOutput<Self::Enum> {
+ ::mingling::AnyOutput::new(ResultEmpty)
+ }
+ #render_fn
+ #do_chain_fn
+ #get_metadata_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();
+ get_global_set(&METADATA).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
new file mode 100644
index 0000000..20f15b8
--- /dev/null
+++ b/mingling_macros/src/func/r_print.rs
@@ -0,0 +1,85 @@
+use proc_macro::TokenStream;
+use proc_macro2::TokenStream as TokenStream2;
+use quote::quote;
+use syn::parse::{Parse, ParseStream};
+use syn::{Ident, Token};
+
+/// Parsed input for `r_println!` and `r_print!`.
+///
+/// Two forms:
+/// - `(ident, format_args...)` — explicit buffer
+/// - `(format_args...)` — implicit `__render_result_buffer`
+pub(crate) enum PrintInput {
+ Explicit { dst: Ident, args: TokenStream2 },
+ Implicit { args: TokenStream2 },
+}
+
+impl Parse for PrintInput {
+ fn parse(input: ParseStream) -> syn::Result<Self> {
+ // Peek: if the next token is an ident followed by a comma, it's the explicit form
+ if input.peek(Ident) && input.peek2(Token![,]) {
+ let dst: Ident = input.parse()?;
+ let _comma: Token![,] = input.parse()?;
+ let args: TokenStream2 = input.parse()?;
+ Ok(PrintInput::Explicit { dst, args })
+ } else {
+ let args: TokenStream2 = input.parse()?;
+ Ok(PrintInput::Implicit { args })
+ }
+ }
+}
+
+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(),
+ };
+
+ let method_ident = Ident::new(method, proc_macro2::Span::call_site());
+
+ let expanded = match parsed {
+ PrintInput::Explicit { dst, args } => {
+ quote! {
+ #dst.#method_ident(format!(#args))
+ }
+ }
+ PrintInput::Implicit { args } => {
+ quote! {
+ __render_result_buffer.#method_ident(format!(#args))
+ }
+ }
+ };
+
+ expanded.into()
+}
+
+pub(crate) fn r_print(input: TokenStream) -> TokenStream {
+ expand_print(input, "print")
+}
+
+/// Parsed input for `r_append!`.
+///
+/// Two forms:
+/// - `(dst, src)` — explicit buffer and source
+/// - `(src)` — implicit `__render_result_buffer`
+pub(crate) struct AppendInput {
+ pub(crate) dst: Option<Ident>,
+ pub(crate) src: proc_macro2::TokenStream,
+}
+
+impl Parse for AppendInput {
+ fn parse(input: ParseStream) -> syn::Result<Self> {
+ if input.peek(Ident) && input.peek2(Token![,]) {
+ let dst: Ident = input.parse()?;
+ let _comma: Token![,] = input.parse()?;
+ let src: TokenStream2 = input.parse()?;
+ Ok(AppendInput {
+ dst: Some(dst),
+ src,
+ })
+ } else {
+ let src: TokenStream2 = input.parse()?;
+ Ok(AppendInput { dst: None, src })
+ }
+ }
+}
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_metadata.rs b/mingling_macros/src/func/register_metadata.rs
new file mode 100644
index 0000000..a1f9965
--- /dev/null
+++ b/mingling_macros/src/func/register_metadata.rs
@@ -0,0 +1,67 @@
+use proc_macro::TokenStream;
+use quote::ToTokens;
+use syn::TypePath;
+use syn::spanned::Spanned;
+
+use crate::METADATA;
+use crate::get_global_set;
+
+/// Parses and registers a metadata mapping of the form
+/// `register_metadata!(EntryGreet, Description)`.
+///
+/// Stores a match-arm-style string entry `Self::EntryGreet => { ... }` that is
+/// later consumed by `program_final_gen!` to generate the `get_metadata`
+/// method of `ProgramCollect`.
+pub(crate) fn register_metadata_impl(input: TokenStream) -> TokenStream {
+ // Parse the input as a comma-separated list of type arguments.
+ let input_parsed = syn::parse_macro_input!(
+ input with syn::punctuated::Punctuated<syn::Expr, syn::Token![,]>::parse_terminated
+ );
+
+ if input_parsed.len() != 2 {
+ return syn::Error::new(
+ input_parsed.span(),
+ "Expected exactly two comma-separated arguments: `EntryVariant, MetadataType`",
+ )
+ .to_compile_error()
+ .into();
+ }
+
+ let entry_expr = &input_parsed[0];
+ let metadata_expr = &input_parsed[1];
+
+ let entry_type = match syn::parse2::<TypePath>(entry_expr.to_token_stream()) {
+ Ok(ty) => ty,
+ Err(e) => return e.to_compile_error().into(),
+ };
+ let metadata_type = match syn::parse2::<TypePath>(metadata_expr.to_token_stream()) {
+ Ok(ty) => ty,
+ Err(e) => return e.to_compile_error().into(),
+ };
+
+ let entry_str = build_metadata_entry(&entry_type, &metadata_type).to_string();
+
+ get_global_set(&METADATA).lock().unwrap().insert(entry_str);
+
+ quote::quote! {}.into()
+}
+
+/// Builds the match-arm entry for `get_metadata`, matching on the enum variant
+/// and then on the requested `TypeId`.
+fn build_metadata_entry(
+ entry_type: &TypePath,
+ metadata_type: &TypePath,
+) -> proc_macro2::TokenStream {
+ let enum_variant = entry_type.path.segments.last().unwrap().ident.clone();
+ quote::quote! {
+ Self::#enum_variant => {
+ let __metadata_type_id = ::std::any::TypeId::of::<#metadata_type>();
+ match type_id {
+ _ if type_id == __metadata_type_id => Some(::std::boxed::Box::new(
+ <#entry_type as ::mingling::Metadata<#metadata_type>>::init_metadata(),
+ ) as ::std::boxed::Box<dyn ::std::any::Any>),
+ _ => None,
+ }
+ }
+ }
+}
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..6613a98 100644
--- a/mingling_macros/src/func/suggest.rs
+++ b/mingling_macros/src/func/suggest.rs
@@ -2,15 +2,15 @@ use proc_macro::TokenStream;
use quote::quote;
use syn::parse::{Parse, ParseStream};
use syn::punctuated::Punctuated;
-use syn::{Expr, LitStr, Token, parse_macro_input};
+use syn::{Expr, Token, parse_macro_input};
struct SuggestInput {
items: Punctuated<SuggestItem, Token![,]>,
}
enum SuggestItem {
- WithDesc(Box<(LitStr, Expr)>), // "-i" = "Insert something"
- Simple(LitStr), // "-I"
+ WithDesc(Box<(Expr, Expr)>), // "-i" = "Insert something"
+ Simple(Expr), // "-I"
}
impl Parse for SuggestInput {
@@ -22,7 +22,7 @@ impl Parse for SuggestInput {
impl Parse for SuggestItem {
fn parse(input: ParseStream) -> syn::Result<Self> {
- let key: LitStr = input.parse()?;
+ let key: Expr = input.parse()?;
if input.peek(Token![:]) {
let _colon: Token![:] = input.parse()?;
@@ -34,60 +34,58 @@ impl Parse for SuggestItem {
}
}
+/// 判断表达式是否是一个纯字符串字面量(仅由一对引号包裹)
+fn is_pure_lit_str(expr: &Expr) -> bool {
+ matches!(expr, Expr::Lit(lit) if matches!(lit.lit, syn::Lit::Str(_)))
+}
+
#[cfg(feature = "comp")]
pub(crate) fn suggest(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as SuggestInput);
let mut items = Vec::new();
+ let mut simple_items = Vec::new();
for item in input.items {
match item {
SuggestItem::WithDesc(boxed) => {
let (key, value) = *boxed;
- items.push(quote! {
- ::mingling::SuggestItem::new_with_desc(#key.to_string(), #value.to_string())
- });
+ if is_pure_lit_str(&key) {
+ items.push(quote! {
+ vec![#key .to_string()], #value
+ });
+ } else {
+ items.push(quote! {
+ #key, #value
+ });
+ }
}
SuggestItem::Simple(key) => {
- items.push(quote! {
- ::mingling::SuggestItem::new(#key.to_string())
- });
+ if is_pure_lit_str(&key) {
+ simple_items.push(quote! {
+ vec![#key .to_string()]
+ });
+ } else {
+ simple_items.push(quote! {
+ #key
+ });
+ }
}
}
}
- let expanded = if items.is_empty() {
+ let expanded = if items.is_empty() && simple_items.is_empty() {
quote! {
::mingling::Suggest::new()
}
} else {
quote! {{
let mut suggest = ::mingling::Suggest::new();
- #(suggest.insert(#items);)*
+ #(suggest.add_suggest_with_description(#items);)*
+ #(suggest.add_suggest(#simple_items);)*
suggest
}}
};
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 a85648f..ce3455e 100644
--- a/mingling_macros/src/lib.rs
+++ b/mingling_macros/src/lib.rs
@@ -36,7 +36,7 @@
//! │ V │
//! │ Reads all registries → generates ThisProgram with: │
//! │ • ProgramCollect impl (dispatch/render/chain dispatch tree) │
-//! │ • Fallback types (ErrorDispatcherNotFound, etc.) │
+//! │ • Fallback types (EntryFallback, etc.) │
//! │ • Completion logic (if `comp` feature enabled) │
//! └──────────────────────────────────────────────────────────────────┘
//! ```
@@ -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 |
//!
@@ -120,8 +120,8 @@
//! ```rust,ignore
//! // Example of what gen_program! generates (simplified):
//! impl ProgramCollect for ThisProgram {
-//! fn build_dispatcher_not_found(args: Vec<String>) -> AnyOutput {
-//! AnyOutput::new(ErrorDispatcherNotFound::new(args))
+//! fn build_entry_fallback(args: Vec<String>) -> AnyOutput {
+//! AnyOutput::new(EntryFallback::new(args))
//! }
//! fn has_chain(any: &AnyOutput) -> bool {
//! match any.member_id() {
@@ -141,11 +141,7 @@
//! }
//! ```
-#[cfg(feature = "extra_macros")]
-use quote::quote;
-
-#[cfg(feature = "extra_macros")]
-use syn::parse_macro_input;
+#![deny(missing_docs)]
use proc_macro::TokenStream;
use std::collections::BTreeSet;
@@ -167,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 attr::{chain, help, metadata, 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 }
}
@@ -216,6 +209,7 @@ pub(crate) static RENDERERS: Registry = OnceLock::new();
pub(crate) static CHAINS_EXIST: Registry = OnceLock::new();
pub(crate) static RENDERERS_EXIST: Registry = OnceLock::new();
pub(crate) static HELP_REQUESTS: Registry = OnceLock::new();
+pub(crate) static METADATA: Registry = OnceLock::new();
/// Checks if a variant name already exists in a registered set.
/// Returns a `compile_error` token stream if a duplicate is found.
@@ -270,7 +264,37 @@ fn entry_has_variant(entry: &str, variant_name: &str) -> bool {
entry.contains(&format!(":: {variant_name} =>"))
}
-#[cfg(feature = "extra_macros")]
+/// Registers an outside type (from `std` or other crates) as a type recognizable
+/// by the Mingling framework, without modifying the original type definition.
+///
+/// This macro generates a newtype wrapper around the given type that implements
+/// `Grouped`, `Into<AnyOutput>`, `Into<ChainProcess>`, and the `Routable` trait,
+/// making the outside type usable in `#[chain]` and `#[renderer]` functions.
+///
+/// # Syntax
+///
+/// ```rust,ignore
+/// // Simple form — creates a wrapper named after the type's last segment:
+/// group!(ParseIntError);
+///
+/// // Aliased form — creates a wrapper with a custom name:
+/// group!(ErrorIo = std::io::Error);
+/// ```
+///
+/// # Example
+///
+/// See the full example in the crate documentation or run:
+/// ```bash
+/// cargo run --example example-outside-type -- parse 42
+/// cargo run --example example-outside-type -- parse hello
+/// cargo run --example example-outside-type -- error
+/// ```
+///
+/// # Requirements
+///
+/// - 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 = "extras")]
#[proc_macro]
pub fn group(input: TokenStream) -> TokenStream {
group_impl::group_macro(input)
@@ -286,11 +310,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.
@@ -403,7 +427,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.
@@ -467,8 +491,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)
@@ -484,11 +508,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
@@ -511,6 +535,28 @@ pub fn pack_err_structural(input: TokenStream) -> TokenStream {
/// directly, while the `Err` value is converted via `Routable::to_chain()` and
/// returned early.
///
+/// ## Interaction with `#[routeify]`
+///
+/// The [`#[routeify]`](attr.routeify.html) attribute macro automatically replaces
+/// every `expr?` inside a function with `route!(expr)`. This means you can use the
+/// familiar `?` syntax in chain functions instead of writing `route!(...)`
+/// explicitly:
+///
+/// ```rust,ignore
+/// use mingling::macros::chain;
+///
+/// #[chain(routeify)]
+/// fn process(prev: SomeEntry) -> Next {
+/// // `?` here expands to `route!(...)` → this macro → the match block
+/// let value = some_fallible_call()?;
+/// value.to_chain()
+/// }
+/// ```
+///
+/// Because `#[routeify]` maps the span of `?` to this macro, hovering over `?` in
+/// a `#[routeify]` function will display this documentation — explaining what
+/// the `?` actually expands to.
+///
/// # Example
///
/// ```rust,ignore
@@ -523,17 +569,53 @@ 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.
+///
+/// This macro is similar to [`route!`] but instead of routing errors through
+/// `Routable::to_chain()` (which returns `ChainProcess`), it routes them
+/// directly to the renderer via `crate::ThisProgram::render(AnyOutput::new(e))`
+/// (which returns `RenderResult`).
+///
+/// This is useful in `#[renderer]` and `#[help]` functions where the return
+/// type is `RenderResult` rather than `ChainProcess`.
+///
+/// # Syntax
+///
+/// ```rust,ignore
+/// render_route!(expr)
+/// ```
+///
+/// Where `expr` is an expression of type `Result<T, E>`.
+///
+/// # Interaction with `#[routeify]`
+///
+/// When `#[routeify]` is used on a `#[renderer]` or `#[help]` function (e.g.
+/// `#[renderer(routeify)]` or `#[help(routeify)]`), every `expr?` is automatically
+/// replaced with `render_route!(expr)` instead of `route!(expr)`.
+///
+/// # Example
+///
+/// ```rust,ignore
+/// use mingling::macros::{renderer, render_route};
+/// use std::io::Write;
+///
+/// #[renderer]
+/// fn render_something(prev: SomeType) -> RenderResult {
+/// let data = render_route!(fetch_data().map_err(|e| ErrorEntry::new(e.to_string())))?;
+/// // ... render data
+/// Ok(RenderResult::new())
+/// }
+/// ```
+#[cfg(feature = "extras")]
+#[proc_macro]
+pub fn render_route(input: TokenStream) -> TokenStream {
+ func::render_route::render_route(input)
}
/// Creates an empty result value wrapped in `ChainProcess` for early return
@@ -584,13 +666,10 @@ pub fn 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::Grouped::<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.
@@ -614,9 +693,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:
///
@@ -643,7 +722,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
/// ```
///
@@ -900,11 +979,11 @@ pub fn chain(attr: TokenStream, item: TokenStream) -> TokenStream {
/// The macros `gen_program!` automatically generates two fallback types that
/// you can provide renderers for:
/// - `ErrorRendererNotFound` — triggered when no matching renderer is found
-/// - `ErrorDispatcherNotFound` — triggered when no matching dispatcher is found
+/// - `EntryFallback` — triggered when no matching dispatcher is found
///
/// ```rust,ignore
/// #[renderer]
-/// fn fallback_dispatcher_not_found(prev: ErrorDispatcherNotFound) -> RenderResult {
+/// fn fallback_dispatcher_not_found(prev: EntryFallback) -> RenderResult {
/// let mut result = RenderResult::new();
/// writeln!(result, "Unknown command: {}", prev.join(", "));
/// result
@@ -1016,12 +1095,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.**
@@ -1108,7 +1274,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)
@@ -1129,7 +1295,27 @@ 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 metadata mapping between an enum variant and a metadata type.
+///
+/// This macro is used internally by the `#[metadata]` attribute and is also
+/// available for manual registration if needed.
+///
+/// # Syntax
+///
+/// ```rust,ignore
+/// register_metadata!(EntryVariant, MetadataType);
+/// ```
+///
+/// This adds an entry to the global `METADATA` registry, mapping the enum
+/// variant for `EntryVariant` to the metadata provider trait
+/// `::mingling::Metadata<MetadataType>`. The entry is consumed by
+/// `gen_program!` to generate the `get_metadata` method of `ProgramCollect`.
+#[proc_macro]
+pub fn register_metadata(input: TokenStream) -> TokenStream {
+ func::register_metadata::register_metadata_impl(input)
}
/// Registers a dispatcher at compile time for the `dispatch_tree` feature.
@@ -1158,7 +1344,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.
@@ -1239,6 +1425,64 @@ pub fn help(attr: TokenStream, item: TokenStream) -> TokenStream {
help::help_attr(item)
}
+/// Declares compile-time metadata for an entry variant.
+///
+/// The `#[metadata]` attribute attaches an arbitrary, compile-time-typed value
+/// to an entry. The annotated function becomes the provider for the metadata:
+/// its return type is the metadata type, and the attribute argument names the
+/// entry enum variant the metadata belongs to.
+///
+/// The macro works by:
+/// 1. Generating `impl ::mingling::Metadata<ReturnType> for EntryVariant` whose
+/// `init_metadata()` calls the annotated function.
+/// 2. Registering the entry via `register_metadata!` in the global `METADATA`
+/// registry so that `gen_program!` emits the `get_metadata` method.
+/// 3. Keeping the original function unchanged for direct calls.
+///
+/// # Syntax
+///
+/// ```rust,ignore
+/// #[metadata(EntryGreet)]
+/// fn greet_desc() -> Description {
+/// Description { desc: "ok".into() }
+/// }
+/// ```
+///
+/// The metadata is later retrieved with `ProgramCollect::get_metadata`:
+///
+/// ```rust,ignore
+/// let desc = ThisProgram::get_metadata::<Description>(ThisProgram::EntryGreet);
+/// ```
+///
+/// # Requirements
+///
+/// - The attribute argument must be the enum variant to attach metadata to.
+/// - The function must take no parameters and return a concrete type.
+/// - The function cannot be async.
+#[proc_macro_attribute]
+pub fn metadata(attr: TokenStream, item: TokenStream) -> TokenStream {
+ metadata::metadata_attr(attr, item)
+}
+
+/// Marker attribute for the Mingling lint system.
+///
+/// The content of this attribute is ignored by rustc and reserved for
+/// the mlint tool to interpret. All it does is pass the item through
+/// unchanged.
+///
+/// # Examples
+///
+/// ```rust,ignore
+/// #[mlint(allow(MLINT_SOME_LINT))]
+/// #[mlint(warn(MLINT_SOME_LINT))]
+/// #[mlint(deny(MLINT_SOME_LINT))]
+/// fn some_item() {}
+/// ```
+#[proc_macro_attribute]
+pub fn mlint(attr: TokenStream, item: TokenStream) -> TokenStream {
+ attr::mlint::mlint(attr, item)
+}
+
/// Extension attribute macro that transforms `expr?` into `route!(expr)`.
///
/// Designed for use with `#[chain(routeify, ...)]` to enable concise error
@@ -1254,12 +1498,232 @@ pub fn help(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)
}
+/// Extension attribute macro that transforms `expr?` into `render_route!(expr)`.
+///
+/// Designed for use with `#[renderer(renderify, ...)]` or `#[help(renderify, ...)]`
+/// to enable concise error routing in renderer and help functions using the `?`
+/// operator syntax.
+///
+/// Unlike `#[routeify]` which routes errors to the chain pipeline via `route!`,
+/// `#[renderify]` routes errors to the rendering pipeline via `render_route!`,
+/// which matches the `RenderResult` return type of renderer and help functions.
+///
+/// # Example
+///
+/// ```rust,ignore
+/// #[renderer(renderify)]
+/// fn render_greeting(prev: Greeting) -> RenderResult {
+/// let data = load_data()?; // expands to render_route!(load_data())
+/// r_println!("{data}");
+/// Ok(RenderResult::new())
+/// }
+/// ```
+#[cfg(feature = "extras")]
+#[proc_macro_attribute]
+pub fn renderify(attr: TokenStream, item: TokenStream) -> TokenStream {
+ extensions::renderify::renderify_impl(attr, item)
+}
+
+/// Wraps a unit-returning function to produce a `RenderResult`.
+///
+/// The `#[buffer]` attribute macro injects a local `__render_result_buffer`
+/// variable of type `::mingling::RenderResult` and changes the function's
+/// return type to `::mingling::RenderResult`. Inside the body, use the
+/// `r_print!` and `r_println!` macros to write into the buffer.
+///
+/// # Example
+///
+/// ```rust,ignore
+/// use mingling::macros::{buffer, r_println};
+///
+/// #[buffer]
+/// fn render_my_type(prev: MyType) {
+/// r_println!("Value: {:?}", *prev);
+/// }
+/// ```
+///
+/// This expands to:
+///
+/// ```rust,ignore
+/// fn render_my_type(prev: MyType) -> mingling::RenderResult {
+/// let mut __render_result_buffer = mingling::RenderResult::new();
+/// {
+/// r_println!("Value: {:?}", *prev);
+/// }
+/// __render_result_buffer
+/// }
+/// ```
+///
+/// # Requirements
+///
+/// - The function must return `()` (unit).
+/// - The function cannot be async.
+#[proc_macro_attribute]
+pub fn buffer(attr: TokenStream, item: TokenStream) -> TokenStream {
+ extensions::buffer::buffer_impl(attr, item)
+}
+
+/// Prints text to a `RenderResult` buffer, with a trailing newline.
+///
+/// # Implicit buffer (inside `#[buffer]` functions)
+///
+/// ```rust,ignore
+/// use mingling::macros::{buffer, r_println};
+///
+/// #[buffer]
+/// fn render() {
+/// r_println!("Hello, {}!", name);
+/// }
+/// ```
+///
+/// # Explicit buffer
+///
+/// Pass a `RenderResult` variable as the first argument:
+///
+/// ```rust,ignore
+/// use mingling::macros::r_println;
+/// use mingling::RenderResult;
+///
+/// let mut r = RenderResult::new();
+/// r_println!(r, "value: {}", 42);
+/// assert_eq!(&*r, "value: 42\n");
+/// ```
+#[proc_macro]
+pub fn r_println(input: TokenStream) -> TokenStream {
+ func::r_println::r_println(input)
+}
+
+/// Prints text to a `RenderResult` buffer, without a trailing newline.
+///
+/// # Implicit buffer (inside `#[buffer]` functions)
+///
+/// ```rust,ignore
+/// use mingling::macros::{buffer, r_print};
+///
+/// #[buffer]
+/// fn render() {
+/// r_print!("Hello, ");
+/// r_println!("world!");
+/// }
+/// ```
+///
+/// # Explicit buffer
+///
+/// ```rust,ignore
+/// use mingling::macros::r_print;
+/// use mingling::RenderResult;
+///
+/// let mut r = RenderResult::new();
+/// r_print!(r, "value: {}", 42);
+/// assert_eq!(&*r, "value: 42");
+/// ```
+#[proc_macro]
+pub fn r_print(input: TokenStream) -> TokenStream {
+ func::r_print::r_print(input)
+}
+
+/// Prints text to a `RenderResult` buffer (standard error style), with a trailing newline.
+///
+/// This macro works identically to `r_println!` but conceptually targets
+/// "error output" — it writes into a `RenderResult` buffer with a trailing newline.
+///
+/// # Implicit buffer (inside `#[buffer]` functions)
+///
+/// ```rust,ignore
+/// use mingling::macros::{buffer, r_eprintln};
+///
+/// #[buffer]
+/// fn render() {
+/// r_eprintln!("Error: {}", err_msg);
+/// }
+/// ```
+///
+/// # Explicit buffer
+///
+/// Pass a `RenderResult` variable as the first argument:
+///
+/// ```rust,ignore
+/// use mingling::macros::r_eprintln;
+/// use mingling::RenderResult;
+///
+/// let mut r = RenderResult::new();
+/// r_eprintln!(r, "error: {}", 42);
+/// assert_eq!(&*r, "error: 42\n");
+/// ```
+#[proc_macro]
+pub fn r_eprintln(input: TokenStream) -> TokenStream {
+ func::r_eprintln::r_eprintln(input)
+}
+
+/// Prints text to a `RenderResult` buffer (standard error style), without a trailing newline.
+///
+/// This macro works identically to `r_print!` but conceptually targets
+/// "error output" — it writes into a `RenderResult` buffer without a trailing newline.
+///
+/// # Implicit buffer (inside `#[buffer]` functions)
+///
+/// ```rust,ignore
+/// use mingling::macros::{buffer, r_eprint};
+///
+/// #[buffer]
+/// fn render() {
+/// r_eprint!("Error: ");
+/// r_eprintln!("something went wrong");
+/// }
+/// ```
+///
+/// # Explicit buffer
+///
+/// ```rust,ignore
+/// use mingling::macros::r_eprint;
+/// use mingling::RenderResult;
+///
+/// let mut r = RenderResult::new();
+/// r_eprint!(r, "error: ");
+/// r_eprintln!(r, "42");
+/// assert_eq!(&*r, "error: 42\n");
+/// ```
+#[proc_macro]
+pub fn r_eprint(input: TokenStream) -> TokenStream {
+ func::r_eprint::r_eprint(input)
+}
+
+/// Appends the contents of one `RenderResult` to another.
+///
+/// # Implicit buffer (inside `#[buffer]` functions)
+///
+/// ```rust,ignore
+/// use mingling::macros::{buffer, r_append};
+///
+/// #[buffer]
+/// fn render() {
+/// let other = make_other_result();
+/// r_append!(other);
+/// }
+/// ```
+///
+/// # Explicit buffer
+///
+/// ```rust,ignore
+/// use mingling::macros::r_append;
+/// use mingling::RenderResult;
+///
+/// let mut dst = RenderResult::new();
+/// let src = RenderResult::from("hello");
+/// r_append!(dst, src);
+/// assert!(!dst.is_empty());
+/// ```
+#[proc_macro]
+pub fn r_append(input: TokenStream) -> TokenStream {
+ func::r_append::r_append(input)
+}
+
/// Derive macro for automatically implementing the `Grouped` trait on a struct.
///
/// The `#[derive(Grouped)]` macro:
@@ -1358,7 +1822,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.
@@ -1415,7 +1879,7 @@ pub fn derive_grouped_serialize(input: TokenStream) -> TokenStream {
/// 1. **`pub type Next = ChainProcess<ProgramName>`** — A convenience type alias
/// for use in chain function return types.
/// 2. **`program_comp_gen!(...)`** (with `comp` feature) — Generates completion infrastructure.
-/// 3. **`program_fallback_gen!(...)`** — Generates `ErrorRendererNotFound` and `ErrorDispatcherNotFound` types.
+/// 3. **`program_fallback_gen!(...)`** — Generates `ErrorRendererNotFound` and `EntryFallback` types.
/// 4. **`program_final_gen!(...)`** — Generates the program enum with:
/// - An enum with all packed types as variants
/// - `Display` implementation for the enum
@@ -1450,10 +1914,32 @@ pub fn gen_program(input: TokenStream) -> TokenStream {
func::gen_program::gen_program_impl(input)
}
+/// Internal macro used by `gen_program!` to generate the completion infrastructure for
+/// shell completion support.
+///
+/// **This macro is only available with the `comp` feature.**
+///
+/// The `program_comp_gen!` macro generates:
+/// 1. A hidden `__internal_completion_mod` module containing:
+/// - A `CompletionContext` packed type (wrapping `ShellContext`), dispatched via `"__comp"`.
+/// - A `CompletionSuggest` packed type (wrapping `(ShellContext, Suggest)`).
+/// - An internal dispatcher (`CMDCompletion`) for the `"__comp"` command path.
+/// 2. An internal chain function `__exec_completion` that:
+/// - Reads a `ShellContext` from the packed `CompletionContext`.
+/// - Calls `CompletionHelper::exec_completion::<ThisProgram>(&ctx)` to generate suggestions.
+/// - Routes the result to the completion renderer via `CompletionSuggest`.
+/// 3. An internal renderer `__render_completion` that renders the suggestions via
+/// `CompletionHelper::render_suggest`.
+///
+/// When the `dispatch_tree` feature is enabled, it also imports the internal dispatcher
+/// from the generated module into the parent scope for trie-based dispatch.
+///
+/// This macro is called automatically by `gen_program!` and should not be called
+/// directly by user code.
#[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
@@ -1478,22 +1964,53 @@ 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.
+///
+/// This macro is called internally by `#[chain]` and is generally not needed
+/// in user code. Each call stores a string entry containing the source-to-target
+/// type mapping, which is later consumed by `gen_program!` to generate the
+/// `has_chain` and `do_chain` dispatch logic in `ProgramCollect`.
+///
+/// The entry string format is a match arm: the source variant maps to a call
+/// that converts the value into a `ChainProcess` via the destination type.
+///
+/// # Panics
+///
+/// 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.
+///
+/// This macro is called internally by `#[renderer]` and is generally not
+/// needed in user code. Each call stores a string entry containing the
+/// type-to-render mapping, which is later consumed by `gen_program!` to
+/// generate the `has_renderer` and `render` dispatch logic in `ProgramCollect`.
+///
+/// The entry string format is a match arm: the type variant maps to a call
+/// of the registered renderer function that produces a `RenderResult`.
+///
+/// # Panics
+///
+/// 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
+/// error cases when no dispatcher or renderer is found.
+///
+/// This macro is called automatically by `gen_program!` and should not
+/// 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
@@ -1547,7 +2064,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.
@@ -1668,5 +2185,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..f390b33 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,19 @@ 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! {
+ return Ok(Self::build_entry_fallback(raw.to_vec()));
+ },
+ );
quote! {
fn dispatch_args_trie(
@@ -70,19 +64,21 @@ 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.
+/// `no_match`: fallback code to run when no node in this subtree matches the input.
+///
+/// Matching follows the same "longest registered prefix" rule used by the
+/// dynamic dispatcher: a child (longer) path is preferred over an exact
+/// endpoint at the same depth. Only when every descendant fails to match is
+/// the exact endpoint here dispatched.
fn build_dispatch_body(
nodes: &[(String, String)],
depth: usize,
- pathf_map: &HashMap<String, String>,
+ no_match: &TokenStream,
) -> TokenStream {
if nodes.is_empty() {
- return quote! {
- return Ok(Self::build_dispatcher_not_found(raw.to_vec()));
- };
+ return no_match.clone();
}
- // 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 +93,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 {
@@ -121,6 +116,20 @@ fn build_dispatch_body(
}
};
+ // Fallback code for when neither a child path nor the exact endpoint(s)
+ // here match: run the exact endpoint checks for this node first (they must
+ // win over nothing at all), then pass control back up to the caller.
+ let exact_checks: Vec<TokenStream> = exact_nodes
+ .iter()
+ .map(|(name, disp_type)| make_starts_with_arm(name, disp_type))
+ .collect();
+
+ let level_no_match = {
+ let mut body = exact_checks.clone();
+ body.push(no_match.clone());
+ quote! { #(#body)* }
+ };
+
let mut arms = Vec::new();
for (&ch, sub_nodes) in &groups {
@@ -129,14 +138,16 @@ fn build_dispatch_body(
if sub_nodes.len() == 1 {
let (name, disp_type) = &sub_nodes[0];
let arm = make_starts_with_arm(name, disp_type);
+ // Try the child first; if it does not match, fall through to the
+ // exact endpoint(s) here so the longer path wins when present.
arms.push(quote! {
Some(#ch_char) => {
#arm
- return Ok(Self::build_dispatcher_not_found(raw.to_vec()));
+ #level_no_match
}
});
} else {
- let sub_body = build_dispatch_body(sub_nodes, depth + 1, pathf_map);
+ let sub_body = build_dispatch_body(sub_nodes, depth + 1, &level_no_match);
arms.push(quote! {
Some(#ch_char) => {
#sub_body
@@ -145,36 +156,18 @@ fn build_dispatch_body(
}
}
- let exact_checks: Vec<TokenStream> = exact_nodes
- .iter()
- .map(|(name, disp_type)| make_starts_with_arm(name, disp_type))
- .collect();
-
- if !exact_checks.is_empty() && !groups.is_empty() {
- let match_body = quote! {
- match raw_chars.nth(0) {
- #(#arms)*
- _ => return Ok(Self::build_dispatcher_not_found(raw.to_vec())),
- }
- };
- quote! {
- #(#exact_checks)*
- #match_body
- }
- } else if !exact_checks.is_empty() {
- quote! {
- #(#exact_checks)*
- return Ok(Self::build_dispatcher_not_found(raw.to_vec()));
- }
- } else if arms.is_empty() {
- quote! {
- return Ok(Self::build_dispatcher_not_found(raw.to_vec()));
- }
+ if groups.is_empty() {
+ // No children exist for this node; only the exact endpoint(s) apply.
+ let mut body = exact_checks;
+ body.push(no_match.clone());
+ quote! { #(#body)* }
} else {
quote! {
match raw_chars.nth(0) {
#(#arms)*
- _ => return Ok(Self::build_dispatcher_not_found(raw.to_vec())),
+ _ => {
+ #level_no_match
+ }
}
}
}
diff --git a/mingling_macros/src/systems/structural_data.rs b/mingling_macros/src/systems/structural_data.rs
index 74bcf09..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 for #type_name {}
- impl ::mingling::__private::StructuralData 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 for #type_name {}
- impl ::mingling::__private::StructuralData 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 for #type_name {}
- impl ::mingling::__private::StructuralData 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`