dragonfly_plugin_macro/
lib.rs

1//! Procedural macros for the `dragonfly-plugin` Rust SDK.
2//!
3//! This crate exposes three main macros:
4//! - `#[derive(Plugin)]` with a `#[plugin(...)]` attribute to describe
5//!   the plugin metadata and registered commands.
6//! - `#[event_handler]` to generate an `EventSubscriptions` implementation
7//!   based on the `on_*` methods you override in an `impl EventHandler`.
8//! - `#[derive(Command)]` together with `#[command(...)]` /
9//!   `#[subcommand(...)]` to generate strongly-typed command parsers and
10//!   handler traits.
11//!
12//! These macros are re-exported by the `dragonfly-plugin` crate, so plugin
13//! authors should depend on that crate directly.
14
15mod command;
16mod plugin;
17
18use heck::ToPascalCase;
19use proc_macro::TokenStream;
20use quote::{format_ident, quote};
21use syn::{parse_macro_input, Attribute, DeriveInput, ImplItem, ItemImpl};
22
23use crate::{command::generate_command_impls, plugin::generate_plugin_impl};
24
25#[proc_macro_derive(Plugin, attributes(plugin, events))]
26pub fn handler_derive(input: TokenStream) -> TokenStream {
27    let ast = parse_macro_input!(input as DeriveInput);
28
29    let derive_name = &ast.ident;
30
31    let info_attr = match find_attribute(
32        &ast,
33        "plugin",
34        "Missing `#[plugin(...)]` attribute with metadata.",
35    ) {
36        Ok(attr) => attr,
37        Err(e) => return e.to_compile_error().into(),
38    };
39
40    let plugin_impl = generate_plugin_impl(info_attr, derive_name);
41
42    quote! {
43        #plugin_impl
44    }
45    .into()
46}
47
48fn find_attribute<'a>(
49    ast: &'a syn::DeriveInput,
50    name: &str,
51    error: &str,
52) -> Result<&'a Attribute, syn::Error> {
53    ast.attrs
54        .iter()
55        .find(|a| a.path().is_ident(name))
56        .ok_or_else(|| syn::Error::new(ast.ident.span(), error))
57}
58
59#[proc_macro_attribute]
60pub fn event_handler(_attr: TokenStream, item: TokenStream) -> TokenStream {
61    let item_clone = item.clone();
62
63    // Try to parse the input tokens as an `impl` block
64    let impl_block = match syn::parse::<ItemImpl>(item) {
65        Ok(block) => block,
66        Err(_) => {
67            // Parse failed, which means the user is probably in the
68            // middle of typing. Return the original, un-parsed tokens
69            // to keep the LSP alive
70            return item_clone;
71        }
72    };
73
74    // ensure its our EventHandler.
75    let is_event_handler_impl = if let Some((_, trait_path, _)) = &impl_block.trait_ {
76        trait_path
77            .segments
78            .last()
79            .is_some_and(|segment| segment.ident == "EventHandler")
80    } else {
81        return item_clone;
82    };
83
84    if !is_event_handler_impl {
85        // This is an `impl` for some *other* trait.
86        // We shouldn't touch it. Return the original tokens.
87        return item_clone;
88    }
89
90    let mut event_variants = Vec::new();
91    for item in &impl_block.items {
92        if let ImplItem::Fn(method) = item {
93            let fn_name = method.sig.ident.to_string();
94
95            if let Some(event_name_snake) = fn_name.strip_prefix("on_") {
96                let event_name_pascal = event_name_snake.to_pascal_case();
97
98                let variant_ident = format_ident!("{}", event_name_pascal);
99                event_variants.push(quote! { types::EventType::#variant_ident });
100            }
101        }
102    }
103
104    let self_ty = &impl_block.self_ty;
105
106    let subscriptions_impl = quote! {
107        impl dragonfly_plugin::EventSubscriptions for #self_ty {
108            fn get_subscriptions(&self) -> Vec<types::EventType> {
109                vec![
110                    #( #event_variants ),*
111                ]
112            }
113        }
114    };
115
116    let original_impl_tokens = quote! { #impl_block };
117
118    let final_output = quote! {
119        #original_impl_tokens
120        #subscriptions_impl
121    };
122
123    final_output.into()
124}
125
126#[proc_macro_derive(Command, attributes(command, subcommand))]
127pub fn command_derive(input: TokenStream) -> TokenStream {
128    let ast = parse_macro_input!(input as DeriveInput);
129
130    let command_attr = match find_attribute(
131        &ast,
132        "command",
133        "Missing `#[command(...)]` attribute with metadata.",
134    ) {
135        Ok(attr) => attr,
136        Err(e) => return e.to_compile_error().into(),
137    };
138
139    generate_command_impls(&ast, command_attr).into()
140}