rmk-macro 0.8.0

Proc-macro crate of RMK
Documentation
use proc_macro::TokenStream;
use quote::{format_ident, quote};
use syn::{DeriveInput, Meta, parse_macro_input};

use super::config::InputDeviceConfig;
use super::parser::parse_input_device_config;
use super::runnable::generate_runnable;
use super::utils::{attr_matches_name, deduplicate_type_generics, has_runnable_marker};
use crate::processor::{ProcessorConfig, parse_processor_config};
use crate::utils::to_snake_case;

/// Generates InputDevice and Runnable trait implementations for single-event devices.
///
/// This macro is used to define InputDevice structs that publish a single event type:
/// ```rust,ignore
/// #[input_device(publish = BatteryEvent)]
/// pub struct BatteryReader { ... }
///
/// impl BatteryReader {
///     async fn read_battery_event(&mut self) -> BatteryEvent {
///         // Wait and return single event
///     }
/// }
/// ```
pub fn input_device_impl(attr: TokenStream, item: TokenStream) -> TokenStream {
    let mut input = parse_macro_input!(item as DeriveInput);

    // Parse attributes to extract event type using shared parser
    let device_config = match parse_input_device_config(proc_macro2::TokenStream::from(attr)) {
        Ok(config) => config,
        Err(err) => return err.into(),
    };
    let event_type = device_config.event_type;

    // Validate input is a struct
    if !matches!(input.data, syn::Data::Struct(_)) {
        return syn::Error::new_spanned(&input, "#[input_device] can only be applied to structs")
            .to_compile_error()
            .into();
    }

    // Check for runnable_generated marker
    let has_marker = has_runnable_marker(&input.attrs);

    // Check for processor attribute (for combined Runnable generation)
    // Support both simple form (#[processor]) and qualified form (#[rmk_macro::processor])
    let has_processor = input
        .attrs
        .iter()
        .any(|attr| attr_matches_name(attr, "processor"));

    // Parse processor config if present (for combined Runnable)
    let processor_config: Option<ProcessorConfig> = if has_processor {
        let attr = input
            .attrs
            .iter()
            .find(|attr| attr_matches_name(attr, "processor"))
            .unwrap(); // Safe because has_processor is true

        if let Meta::List(meta_list) = &attr.meta {
            match parse_processor_config(meta_list.tokens.clone()) {
                Ok(config) => Some(config),
                Err(err) => return err.into(),
            }
        } else {
            return syn::Error::new_spanned(
                attr,
                "#[processor] requires parameters. Use `#[processor(subscribe = [EventType])]`",
            )
            .to_compile_error()
            .into();
        }
    } else {
        None
    };

    let struct_name = &input.ident;
    let generics = &input.generics;
    let (impl_generics, _ty_generics, where_clause) = generics.split_for_impl();

    // Use deduplicated type generics to handle cfg-conditional generic parameters
    let deduped_ty_generics = deduplicate_type_generics(generics);

    // Generate method name from event type
    let type_name = event_type.segments.last().unwrap().ident.to_string();
    let base_name = type_name.strip_suffix("Event").unwrap_or(&type_name);
    let method_name = format_ident!("read_{}_event", to_snake_case(base_name));

    // 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.
    let (runnable_impl, generated_runnable) = if has_marker {
        // Runnable was already generated by processor macro
        (quote! {}, false)
    } else {
        // We're the first macro to run, generate Runnable
        let input_device_cfg = InputDeviceConfig {
            event_type: event_type.clone(),
        };
        (
            generate_runnable(
                struct_name,
                generics,
                where_clause,
                Some(&input_device_cfg),
                processor_config.as_ref(),
            ),
            true,
        )
    };

    // Remove input_device attribute to prevent duplicate expansion
    input
        .attrs
        .retain(|attr| !attr.path().is_ident("input_device"));

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

    // Generate the complete output
    let expanded = quote! {
        #input

        impl #impl_generics ::rmk::input_device::InputDevice for #struct_name #deduped_ty_generics #where_clause {
            type Event = #event_type;

            async fn read_event(&mut self) -> Self::Event {
                self.#method_name().await
            }
        }

        #runnable_impl
    };

    expanded.into()
}