aboutsummaryrefslogtreecommitdiff
path: root/mingling_macros/src/extensions.rs
diff options
context:
space:
mode:
author魏曹先生 <1992414357@qq.com>2026-07-20 00:34:21 +0800
committer魏曹先生 <1992414357@qq.com>2026-07-20 00:34:21 +0800
commite8ae63988d562969df47ae6d92129974e7fb6980 (patch)
treefbf3c827982f72555f1195bee508204cad6e2f65 /mingling_macros/src/extensions.rs
parente3c75c660e9d9387184ff43b14c070afedd069c6 (diff)
feat(macros): add extension point mechanism for attribute macros
Implement an `extensions` module in `mingling_macros` that allows attribute macros to accept extension identifiers (e.g., `routeify`) which are re-dispatched as outer attributes before the core macro logic runs, enabling extensibility without modifying existing macros
Diffstat (limited to 'mingling_macros/src/extensions.rs')
-rw-r--r--mingling_macros/src/extensions.rs150
1 files changed, 150 insertions, 0 deletions
diff --git a/mingling_macros/src/extensions.rs b/mingling_macros/src/extensions.rs
new file mode 100644
index 0000000..97ef636
--- /dev/null
+++ b/mingling_macros/src/extensions.rs
@@ -0,0 +1,150 @@
+//! Extension point mechanism for Mingling attribute macros.
+//!
+//! 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};
+
+/// 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>,
+}
+
+impl Parse for Extensions {
+ fn parse(input: ParseStream) -> syn::Result<Self> {
+ let mut exts = Vec::new();
+ while !input.is_empty() {
+ let ident: Ident = input.parse()?;
+ exts.push(ident);
+ if input.peek(Token![,]) {
+ let _ = input.parse::<Token![,]>();
+ }
+ }
+ Ok(Extensions { exts })
+ }
+}
+
+/// 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![,]>();
+ if input.is_empty() {
+ break;
+ }
+ 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,
+ bare_attr_name: &str,
+) -> Option<TokenStream> {
+ if attr.is_empty() {
+ return None;
+ }
+
+ let exts: Extensions = syn::parse(attr).ok()?;
+ if exts.exts.is_empty() {
+ return None;
+ }
+
+ let bare = Ident::new(bare_attr_name, proc_macro2::Span::call_site());
+ let exts = &exts.exts;
+ let item = proc_macro2::TokenStream::from(item.clone());
+
+ Some(
+ quote! {
+ #(#exts)*
+ #[#bare]
+ #item
+ }
+ .into(),
+ )
+}
+
+/// 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,
+ item: &TokenStream,
+) -> Option<TokenStream> {
+ if attr.is_empty() {
+ return None;
+ }
+
+ let parsed: CompletionExt = syn::parse(attr).ok()?;
+ if parsed.exts.is_empty() {
+ return None;
+ }
+
+ let entry_type = &parsed.entry_type;
+ let exts = &parsed.exts;
+ let item = proc_macro2::TokenStream::from(item.clone());
+
+ Some(
+ quote! {
+ #(#exts)*
+ #[::mingling::macros::completion(#entry_type)]
+ #item
+ }
+ .into(),
+ )
+}