diff options
| -rw-r--r-- | CHANGELOG.md | 26 | ||||
| -rw-r--r-- | mingling/src/lib.rs | 4 | ||||
| -rw-r--r-- | mingling_macros/src/extensions.rs | 46 | ||||
| -rw-r--r-- | mingling_macros/src/extensions/routeify.rs | 39 | ||||
| -rw-r--r-- | mingling_macros/src/lib.rs | 21 |
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: |
