aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author魏曹先生 <1992414357@qq.com>2026-07-20 01:11:44 +0800
committer魏曹先生 <1992414357@qq.com>2026-07-20 01:11:44 +0800
commit779d7f6b3b5a22406c61d94efc45ad4422e50e0d (patch)
tree9a346a0aa1d8323cc657ff86cb59a57e9a01d959
parent1f583f625a7d541dc7d47a8136b31d29a5ed5441 (diff)
feat(extensions): add routeify extension macro for error routing
-rw-r--r--CHANGELOG.md26
-rw-r--r--mingling/src/lib.rs4
-rw-r--r--mingling_macros/src/extensions.rs46
-rw-r--r--mingling_macros/src/extensions/routeify.rs39
-rw-r--r--mingling_macros/src/lib.rs21
5 files changed, 96 insertions, 40 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f19922c..54cffcf 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -236,6 +236,32 @@ None
This system is designed for future extensibility: as new cross-cutting concerns (e.g., logging, metrics, validation) are identified, they can be added as simple extension identifiers without touching the core macro logic.
+11. **[`extensions`]** **[`macros`]** Added the `#[routeify]` extension attribute macro that transforms `expr?` into `route!(expr)`, enabling concise error routing in chain functions using the `?` operator syntax.
+
+ The `#[routeify]` macro can be used:
+ - **Standalone** — as a direct attribute: `#[routeify] fn handle(...) { ... }`
+ - **As an extension** — via the extension point system: `#[chain(routeify)] fn handle(...) { ... }`
+
+ When used as a `#[chain]` extension, the `routeify` identifier is detected by the extension point mechanism, stripped from the `#[chain]` attribute arguments, and `#[routeify]` is applied as an outer attribute on top of `#[chain]`. The re-dispatch token stream now correctly generates `#[#exts]*` (i.e., `#[routeify] #[chain]`) instead of the previous bare `#exts` — fixing a bug where extension identifiers were emitted without the `#[...]` attribute delimiter, producing invalid token streams.
+
+ ```rust
+ use mingling::macros::routeify;
+
+ #[chain(routeify)]
+ fn handle_calc(args: EntryCalculate) -> Next {
+ let a = args.pick(&arg![f32]).to_result()?;
+ let op = args.pick(&arg![Operator]).to_result()?;
+ StateCalculate { number_a: a, operator: op, ... }.to_chain()
+ }
+ ```
+
+ The `#[routeify]` macro is feature-gated behind `extra_macros` and re-exported as `mingling::macros::routeify`.
+
+ Internal changes:
+ - Added `mingling_macros/src/extensions/routeify.rs` with `routeify_impl` implementation.
+ - Updated `try_redispatch_simple` and `try_redispatch_completion` to emit `#[#exts]` instead of bare `#exts`, ensuring proper attribute syntax in the re-dispatched token stream.
+ - Registered `#[proc_macro_attribute] pub fn routeify` in `mingling_macros/src/lib.rs`.
+
#### **BREAKING CHANGES** (API CHANGES):
1. **[`macros:renderer`]** **[`macros:help`]** Removed `r_println!` and `r_print!` macros. The `#[renderer]` and `#[help]` macros no longer implicitly inject an internal `RenderResult` variable or provide `r_println!` / `r_print!` macros.
diff --git a/mingling/src/lib.rs b/mingling/src/lib.rs
index f4bc9dc..5cd53db 100644
--- a/mingling/src/lib.rs
+++ b/mingling/src/lib.rs
@@ -106,6 +106,10 @@ pub mod macros {
/// `route! { /* ... */ }` - Used to generate a route that either returns a successful result or early returns an error.
#[cfg(feature = "extra_macros")]
pub use mingling_macros::route;
+ /// `#[routeify]` - An extension attribute macro that transforms `expr?` into `route!(expr)`.
+ /// Can be used standalone or as a chain/renderer extension: `#[chain(routeify, ...)]`.
+ #[cfg(feature = "extra_macros")]
+ pub use mingling_macros::routeify;
/// `suggest! { "hello", "bye" }` - Used to generate suggestions
#[cfg(feature = "comp")]
pub use mingling_macros::suggest;
diff --git a/mingling_macros/src/extensions.rs b/mingling_macros/src/extensions.rs
index 97ef636..aff20e2 100644
--- a/mingling_macros/src/extensions.rs
+++ b/mingling_macros/src/extensions.rs
@@ -3,38 +3,18 @@
//! This module provides a way for attribute macros like `#[chain]`, `#[renderer]`,
//! `#[help]`, and `#[completion]` to accept extension identifiers that are
//! applied as outer attributes before the bare macro.
-//!
-//! # How it works
-//!
-//! When a user writes `#[chain(routeify, other_ext)]`, the extension point mechanism
-//! detects `routeify` and `other_ext` as extension identifiers, and generates a
-//! re-dispatch token stream equivalent to:
-//!
-//! ```ignore
-//! #[other_ext]
-//! #[routeify]
-//! #[chain]
-//! fn handle(...) { ... }
-//! ```
-//!
-//! The original bare macro (`#[chain]` without extensions) sees no arguments
-//! and proceeds as normal — the extensions have already been stripped.
use proc_macro::TokenStream;
use quote::quote;
use syn::parse::{Parse, ParseStream};
use syn::{Ident, Token};
+/// Extension: `#[routeify]` — transforms `expr?` into `route!(expr)`.
+#[cfg(feature = "extra_macros")]
+pub(crate) mod routeify;
+
/// Parsed extensions from an attribute macro like `#[chain(routeify, other_ext)]`.
-///
-/// For macros with no fixed arguments (chain, renderer, help):
-/// - `exts` contains all parsed extension identifiers
-/// - `fixed` is empty
-///
-/// For macros with fixed arguments (completion):
-/// - Use `parse_completion_attr_extensions` instead.
pub(crate) struct Extensions {
- /// Extension identifiers (e.g., `routeify`, `other_ext`)
pub(crate) exts: Vec<Ident>,
}
@@ -53,23 +33,16 @@ impl Parse for Extensions {
}
/// Parsed extensions for `#[completion(EntryType, routeify, ...)]`.
-///
-/// The first argument is the entry type path, the rest are extension identifiers.
#[cfg(feature = "comp")]
pub(crate) struct CompletionExt {
- /// The entry type path (first argument, e.g., `HelloEntry` or `crate::Entry`)
pub(crate) entry_type: proc_macro2::TokenStream,
- /// Extension identifiers (e.g., `routeify`, `other_ext`)
pub(crate) exts: Vec<Ident>,
}
#[cfg(feature = "comp")]
impl Parse for CompletionExt {
fn parse(input: ParseStream) -> syn::Result<Self> {
- // Parse the first argument as the entry type path
let entry_type: proc_macro2::TokenStream = input.parse()?;
-
- // Parse remaining arguments as extensions
let mut exts = Vec::new();
while !input.is_empty() {
let _ = input.parse::<Token![,]>();
@@ -79,16 +52,12 @@ impl Parse for CompletionExt {
let ident: Ident = input.parse()?;
exts.push(ident);
}
-
Ok(CompletionExt { entry_type, exts })
}
}
/// Generates a re-dispatch token stream for attribute macros that take **no fixed arguments**
/// (chain, renderer, help).
-///
-/// If `attr` contains extensions, the output is `#[ext1] #[ext2] ...[#bare_attr] #item`.
-/// If `attr` is empty or contains no extensions, returns `None`.
pub(crate) fn try_redispatch_simple(
attr: TokenStream,
item: &TokenStream,
@@ -109,7 +78,7 @@ pub(crate) fn try_redispatch_simple(
Some(
quote! {
- #(#exts)*
+ #(#[#exts])*
#[#bare]
#item
}
@@ -118,9 +87,6 @@ pub(crate) fn try_redispatch_simple(
}
/// Generates a re-dispatch token stream for `#[completion(EntryType, ...)]`.
-///
-/// If extensions are found, the output is `#[ext1] #[ext2] #[completion(EntryType)] #item`.
-/// If no extensions, returns `None`.
#[cfg(feature = "comp")]
pub(crate) fn try_redispatch_completion(
attr: TokenStream,
@@ -141,7 +107,7 @@ pub(crate) fn try_redispatch_completion(
Some(
quote! {
- #(#exts)*
+ #(#[#exts])*
#[::mingling::macros::completion(#entry_type)]
#item
}
diff --git a/mingling_macros/src/extensions/routeify.rs b/mingling_macros/src/extensions/routeify.rs
new file mode 100644
index 0000000..f011fb9
--- /dev/null
+++ b/mingling_macros/src/extensions/routeify.rs
@@ -0,0 +1,39 @@
+//! The `#[routeify]` extension — transforms `expr?` into `route!(expr)`.
+//!
+//! Designed as an extension for the Mingling attribute macro system, intended
+//! to be used with `#[chain(routeify)]` or standalone as `#[routeify]`.
+//!
+//! # How it works
+//!
+//! The macro parses the function AST and replaces every `Expr::Try` node with an
+//! equivalent `route!(expr)` invocation.
+
+use proc_macro::TokenStream;
+use quote::ToTokens;
+use syn::visit_mut::VisitMut;
+use syn::{Expr, ItemFn, parse_macro_input};
+
+struct RouteifyTransform;
+
+impl VisitMut for RouteifyTransform {
+ 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();
+
+ if let Ok(macro_expr) = syn::parse2::<Expr>(quote::quote! {
+ ::mingling::macros::route!(#inner_tokens)
+ }) {
+ *expr = macro_expr;
+ }
+ }
+ }
+}
+
+pub(crate) fn routeify_impl(_attr: TokenStream, item: TokenStream) -> TokenStream {
+ let mut input_fn = parse_macro_input!(item as ItemFn);
+ RouteifyTransform.visit_item_fn_mut(&mut input_fn);
+ input_fn.to_token_stream().into()
+}
diff --git a/mingling_macros/src/lib.rs b/mingling_macros/src/lib.rs
index 3743df8..a85648f 100644
--- a/mingling_macros/src/lib.rs
+++ b/mingling_macros/src/lib.rs
@@ -1239,6 +1239,27 @@ pub fn help(attr: TokenStream, item: TokenStream) -> TokenStream {
help::help_attr(item)
}
+/// Extension attribute macro that transforms `expr?` into `route!(expr)`.
+///
+/// Designed for use with `#[chain(routeify, ...)]` to enable concise error
+/// routing in chain functions using the `?` operator syntax.
+///
+/// # Example
+///
+/// ```rust,ignore
+/// #[chain(routeify)]
+/// fn handle_calc(args: EntryCalculate) -> Next {
+/// let a = args.pick(&arg![f32]).to_result()?;
+/// let op = args.pick(&arg![Operator]).to_result()?;
+/// StateCalculate { number_a: a, operator: op, ... }.to_chain()
+/// }
+/// ```
+#[cfg(feature = "extra_macros")]
+#[proc_macro_attribute]
+pub fn routeify(attr: TokenStream, item: TokenStream) -> TokenStream {
+ extensions::routeify::routeify_impl(attr, item)
+}
+
/// Derive macro for automatically implementing the `Grouped` trait on a struct.
///
/// The `#[derive(Grouped)]` macro: