pyro-macro 0.1.0

Derive macros for Pyroduct
Documentation
//! This crate provides proc macros to generate FFI boilerplate for capabilities.
use ::pyro_spec::InterfaceSpec;
use syn::parse_file;

use crate::{
    ffi::{capability::CapabilityImpl, config::CapConfig, spec::build_spec},
    format::{BridgeableArgs, DocRec, magma},
    utils::has_attr,
};

pub mod capability;
pub mod config;
pub mod lifecycle;
pub mod methods;
pub mod paths;
pub mod spec;

/// For generating the interface lib.rs (module-side code)
pub fn generate_interface(
    content: &str,
    cap_name: &str,
    cap_version: &str,
) -> syn::Result<(syn::File, InterfaceSpec<'static>)> {
    let file = parse_file(content)?;
    let spec = build_spec(&file);
    let import_location: syn::Path = syn::parse_quote!(::pyroduct);

    let mut generated_code = quote::quote! {
        //! Automatically generated by pyroduct. DO NOT EDIT.
        #![allow(unused_imports, dead_code, unused_variables, nonstandard_style)]
        use pyroduct;
    };

    for item in file.items {
        match item {
            syn::Item::Impl(item_impl) => {
                if has_attr(&item_impl.attrs, "capability") {
                    let cap = CapabilityImpl::new(item_impl, true, cap_name, cap_version)?;
                    generated_code.extend(cap.expand_module());
                }
            }
            syn::Item::Struct(mut item_struct) => {
                if let Some(args) = extract_magma_args(&item_struct.attrs)? {
                    item_struct.attrs.retain(|a| !is_magma_attr(a));
                    let expanded = magma(args, &mut item_struct, &import_location)?;
                    generated_code.extend(expanded);
                }
            }
            _ => {}
        }
    }
    let code: syn::File = syn::parse2(generated_code)?;
    Ok((code, spec))
}

/// For generating the capability lib.rs (host-side code)
pub fn generate_capability(
    content: &str,
    cap_name: &str,
    cap_version: &str,
) -> syn::Result<syn::File> {
    let file = parse_file(content)?;

    let mut generated_code = quote::quote! {
        //! Automatically generated by pyroduct. DO NOT EDIT.
        #![allow(unused_imports, dead_code, unused_variables, nonstandard_style)]
        use ::pyroduct;
    };
    let import_location: syn::Path = syn::parse_quote!(::pyroduct);

    for item in file.items {
        match item {
            syn::Item::Impl(item_impl) => {
                if has_attr(&item_impl.attrs, "capability") {
                    let cap = CapabilityImpl::new(item_impl, true, cap_name, cap_version)?;
                    generated_code.extend(cap.expand_capability());
                }
            }
            syn::Item::Struct(mut item_struct) => {
                if let Some(args) = extract_magma_args(&item_struct.attrs)? {
                    let expanded = magma(args, &mut item_struct, &import_location)?;
                    generated_code.extend(expanded);
                } else if has_attr(&item_struct.attrs, "config") {
                    let found_config = CapConfig::new(item_struct, DocRec::StructDoc)?;
                    generated_code.extend(found_config.expand());
                }
            }
            _ => {}
        }
    }

    let code: syn::File = syn::parse2(generated_code)?;
    Ok(code)
}

/// Extract `BridgeableArgs` from a `#[magma(...)]` attribute.
/// Handles both `#[magma]` (no args) and `#[magma(derive(Debug))]`.
fn extract_magma_args(attrs: &[syn::Attribute]) -> syn::Result<Option<BridgeableArgs>> {
    for attr in attrs {
        if is_magma_attr(attr) {
            return match &attr.meta {
                syn::Meta::List(list) => {
                    syn::parse2::<BridgeableArgs>(list.tokens.clone()).map(Some)
                }
                syn::Meta::Path(_) => {
                    // Bare #[magma] with no arguments
                    Ok(Some(BridgeableArgs {
                        derives_to_pass: Vec::new(),
                        compares_to_add: Vec::new(),
                        doc_rec: DocRec::NoReq,
                    }))
                }
                syn::Meta::NameValue(nv) => Err(syn::Error::new_spanned(
                    nv,
                    "Invalid magma attribute format",
                )),
            };
        }
    }
    Ok(None)
}

fn is_magma_attr(attr: &syn::Attribute) -> bool {
    if attr.path().is_ident("magma") {
        return true;
    }
    if attr.path().segments.len() == 2
        && attr.path().segments[0].ident == "pyroduct"
        && attr.path().segments[1].ident == "magma"
    {
        return true;
    }
    false
}