standard-plugin-macros 0.1.1

Attribute macros that export a Standard Code plugin type as a wasm component (use through standard-plugin-sdk)
Documentation
//! `#[ui]` and `#[daemon]`: export a plugin type as the `ui-plugin` or
//! `daemon-plugin` world of `standard:plugin@2.0.0`.
//!
//! Use them through `standard-plugin-sdk` as `#[standard_plugin::ui]` and
//! `#[standard_plugin::daemon]`. Put the attribute on the plugin type's
//! definition (a struct or enum) or on its `impl UiPlugin for ...` /
//! `impl DaemonPlugin for ...` block, once per plugin crate. The attribute
//! leaves the item unchanged and adds the component exports, which the SDK's
//! runtime forwards to the trait methods.

use proc_macro::TokenStream;
use quote::quote;
use syn::spanned::Spanned as _;
use syn::{Item, Type};

/// Exports the annotated type, which implements
/// `standard_plugin::UiPlugin`, as the plugin's `ui-plugin` world.
#[proc_macro_attribute]
pub fn ui(attr: TokenStream, item: TokenStream) -> TokenStream {
    export(attr, item, World::Ui)
}

/// Exports the annotated type, which implements
/// `standard_plugin::daemon::DaemonPlugin`, as the plugin's
/// `daemon-plugin` world.
#[proc_macro_attribute]
pub fn daemon(attr: TokenStream, item: TokenStream) -> TokenStream {
    export(attr, item, World::Daemon)
}

enum World {
    Ui,
    Daemon,
}

fn export(attr: TokenStream, item: TokenStream, world: World) -> TokenStream {
    if !attr.is_empty() {
        let attr = proc_macro2::TokenStream::from(attr);
        return syn::Error::new(attr.span(), "this attribute takes no arguments")
            .to_compile_error()
            .into();
    }
    let parsed = syn::parse_macro_input!(item as Item);
    let ty: Type = match &parsed {
        Item::Struct(item) => {
            if !item.generics.params.is_empty() {
                return generic_error(item.generics.span());
            }
            let ident = &item.ident;
            syn::parse_quote!(#ident)
        }
        Item::Enum(item) => {
            if !item.generics.params.is_empty() {
                return generic_error(item.generics.span());
            }
            let ident = &item.ident;
            syn::parse_quote!(#ident)
        }
        Item::Impl(item) if item.trait_.is_some() => {
            if !item.generics.params.is_empty() {
                return generic_error(item.generics.span());
            }
            (*item.self_ty).clone()
        }
        other => {
            return syn::Error::new(
                other.span(),
                "put this attribute on the plugin's struct or enum, or on its plugin trait impl",
            )
            .to_compile_error()
            .into();
        }
    };
    let export = match world {
        World::Ui => quote!(::standard_plugin::__export_ui!(#ty);),
        World::Daemon => quote!(::standard_plugin::__export_daemon!(#ty);),
    };
    quote!(#parsed #export).into()
}

fn generic_error(span: proc_macro2::Span) -> TokenStream {
    syn::Error::new(
        span,
        "a plugin type cannot be generic: the component exports one type",
    )
    .to_compile_error()
    .into()
}