rmk-macro 0.8.0

Proc-macro crate of RMK
Documentation
//! Unified processor macro implementation.
//!
//! Generates `Processor` trait implementations for event-driven processors.

use proc_macro2::TokenStream;
use quote::quote;
use syn::{DeriveInput, Meta, parse_macro_input};

use crate::event_macros::runnable::{generate_event_enum_and_dispatch, generate_runnable};
use crate::event_macros::utils::{AttributeParser, attr_matches_name, has_runnable_marker};

/// Processor subscription config.
pub struct ProcessorConfig {
    pub event_types: Vec<syn::Path>,
    pub poll_interval_ms: Option<u64>,
}

/// Parse processor config from attribute tokens.
pub fn parse_processor_config(
    tokens: impl Into<TokenStream>,
) -> Result<ProcessorConfig, TokenStream> {
    let parser = AttributeParser::new_validated(tokens, &["subscribe", "poll_interval"])?;

    let poll_interval_ms = parser.get_int("poll_interval")?;

    Ok(ProcessorConfig {
        event_types: parser.get_path_array("subscribe")?,
        poll_interval_ms,
    })
}

/// Implementation of the unified `#[processor]` macro.
pub fn processor_impl(
    attr: proc_macro::TokenStream,
    item: proc_macro::TokenStream,
) -> proc_macro::TokenStream {
    let mut input = parse_macro_input!(item as DeriveInput);
    let mut config = match parse_processor_config(proc_macro2::TokenStream::from(attr)) {
        Ok(config) => config,
        Err(err) => return err.into(),
    };

    // Merge sibling #[processor] attributes (e.g. from cfg_attr expansion)
    for attr in &input.attrs {
        if attr.path().is_ident("processor")
            && let Meta::List(meta_list) = &attr.meta
        {
            match parse_processor_config(meta_list.tokens.clone()) {
                Ok(sibling_config) => {
                    config.event_types.extend(sibling_config.event_types);
                    if sibling_config.poll_interval_ms.is_some() {
                        if config.poll_interval_ms.is_some() {
                            return syn::Error::new_spanned(
                                attr,
                                "Conflicting poll_interval in multiple #[processor] attributes",
                            )
                            .to_compile_error()
                            .into();
                        }
                        config.poll_interval_ms = sibling_config.poll_interval_ms;
                    }
                }
                Err(err) => return err.into(),
            }
        }
    }

    // Deduplicate event types that may appear in multiple sibling #[processor] attributes
    config.event_types.retain({
        let mut seen = std::collections::HashSet::new();
        move |path| seen.insert(quote!(#path).to_string())
    });

    // Validate that subscribe list is not empty
    if config.event_types.is_empty() {
        return syn::Error::new_spanned(
            &input.ident,
            "#[processor] requires at least one event type in `subscribe`. \
             Use `#[processor(subscribe = [EventType])]`.",
        )
        .to_compile_error()
        .into();
    }

    let struct_name = &input.ident;
    let vis = &input.vis;
    let generics = &input.generics;
    let (impl_generics, _, where_clause) = generics.split_for_impl();
    let deduped_ty_generics = crate::event_macros::utils::deduplicate_type_generics(generics);

    let has_marker = has_runnable_marker(&input.attrs);

    // Check for sibling #[input_device] attribute.
    // Support both simple form (#[input_device]) and qualified form (#[rmk_macro::input_device])
    let has_input_device = input
        .attrs
        .iter()
        .any(|attr| attr_matches_name(attr, "input_device"));

    // Generate event enum, subscriber, and dispatch body
    let (event_type_tokens, event_enum_def, event_subscriber_impl, process_body) =
        generate_event_enum_and_dispatch(
            struct_name,
            vis,
            &config.event_types,
            "Processor",
            quote! { ::rmk::event::SubscribableEvent },
            quote! { subscriber },
        );

    // PollingProcessor impl when poll_interval is set
    let polling_processor_impl = if let Some(interval_ms) = config.poll_interval_ms {
        quote! {
            impl #impl_generics ::rmk::processor::PollingProcessor for #struct_name #deduped_ty_generics #where_clause {
                fn interval(&self) -> ::embassy_time::Duration {
                    ::embassy_time::Duration::from_millis(#interval_ms)
                }

                async fn update(&mut self) {
                    self.poll().await;
                }
            }
        }
    } else {
        quote! {}
    };

    // Generate Runnable implementation
    // Logic: If marker exists, Runnable was already generated by another macro, so skip.
    // Otherwise, we're the first macro to run, so generate Runnable.

    // Parse sibling input_device config if present (for combined Runnable generation)
    let input_device_config = if has_input_device {
        let attr = input
            .attrs
            .iter()
            .find(|attr| attr_matches_name(attr, "input_device"))
            .unwrap(); // Safe because has_input_device is true

        if let Meta::List(meta_list) = &attr.meta {
            use crate::event_macros::parser::parse_input_device_config;
            match parse_input_device_config(meta_list.tokens.clone()) {
                Ok(cfg) => Some(cfg),
                Err(err) => return err.into(),
            }
        } else {
            return syn::Error::new_spanned(
                attr,
                "#[input_device] requires parameters. Use `#[input_device(publish = EventType)]`",
            )
            .to_compile_error()
            .into();
        }
    } else {
        None
    };

    let runnable_impl = if has_marker {
        // Runnable was already generated by another macro
        quote! {}
    } else {
        // We're the first macro to run, generate Runnable.
        // If sibling #[input_device] exists, this becomes a combined Runnable.
        generate_runnable(
            struct_name,
            generics,
            where_clause,
            input_device_config.as_ref(),
            Some(&config),
        )
    };

    // Remove only processor attribute to allow sibling macro expansion.
    input
        .attrs
        .retain(|attr| !attr.path().is_ident("processor"));

    // Add marker only when sibling macro needs to know Runnable is already generated.
    if has_input_device && !has_marker {
        input
            .attrs
            .push(syn::parse_quote!(#[::rmk::macros::runnable_generated]));
    }

    let expanded = quote! {
        #input

        #event_enum_def
        #event_subscriber_impl

        impl #impl_generics ::rmk::processor::Processor for #struct_name #deduped_ty_generics #where_clause {
            type Event = #event_type_tokens;

            fn subscriber() -> impl ::rmk::event::EventSubscriber<Event = Self::Event> {
                <#event_type_tokens as ::rmk::event::SubscribableEvent>::subscriber()
            }

            async fn process(&mut self, event: Self::Event) {
                #process_body
            }
        }

        #polling_processor_impl

        #runnable_impl
    };

    expanded.into()
}