fox-macros 0.1.1

Modern Ansible replacement
Documentation
use proc_macro::TokenStream;
use quote::quote;
use syn::{FnArg, Item, ItemFn, Pat, PatIdent, parse_macro_input};

/// Shorthand for deriving serde::{Deserialize, Serialize}
///
/// Prevents explicit `serde` dependency
#[proc_macro_attribute]
pub fn rpc(_args: TokenStream, input: TokenStream) -> TokenStream {
    let item = parse_macro_input!(input as Item);

    quote! {
        #[derive(fox::serde::Serialize, fox::serde::Deserialize)]
        #[serde(crate = "fox::serde")]
        #item
    }
    .into()
}

/// Specifies an entrypoint.
///
/// - On `feature = "orchestrator"`
///   - Compiles to normal program.
/// - On `feature = "remote"`
///   - Runs a RPC loop instead.
#[proc_macro_attribute]
pub fn main(_args: TokenStream, input: TokenStream) -> TokenStream {
    let input = parse_macro_input!(input as ItemFn);

    let (attrs, vis, sig, block) = (&input.attrs, &input.vis, &input.sig, &input.block);

    quote! {
        #(#attrs)*
        #vis #sig {
            #[cfg(not(any(feature = "orchestrator", feature = "remote")))]
            compile_error!("None of the required features were selected");

            if cfg!(feature = "orchestrator") {
                #block
            } else {
                fox::remote_rpc()
            }
        }
    }
    .into()
}

/// Marks a boundary of orchestrator<->remote communication
///
/// - On `feature = "orchestrator"`
///   - Creates a communication shim
/// - On `feature = "remote"`
///   - Registers the function
#[proc_macro_attribute]
pub fn remote(_args: TokenStream, input: TokenStream) -> TokenStream {
    let input = parse_macro_input!(input as ItemFn);

    let (attrs, vis, sig, block) = (&input.attrs, &input.vis, &input.sig, &input.block);
    let function_ident = &sig.ident;

    let mut arg_idents = vec![];
    let mut arg_types = vec![];
    for input_arg in &sig.inputs {
        match input_arg {
            FnArg::Receiver(..) => panic!("Argument `self` not allowed"),
            FnArg::Typed(pat_type) => match &*pat_type.pat {
                Pat::Ident(PatIdent { ident, .. }) => {
                    arg_idents.push(ident);
                    arg_types.push(&pat_type.ty)
                }
                other => panic!("Only standard named arguments are supported. Found: {other:?}"),
            },
        }
    }

    let type_signature = arg_types
        .iter()
        .map(|ty| quote! { #ty }.to_string().replace(' ', ""))
        .collect::<Vec<String>>()
        .join(",");
    let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_default();
    let absolute_span_path = std::path::PathBuf::from(proc_macro::Span::call_site().file());
    let relative_path = absolute_span_path
        .strip_prefix(&manifest_dir)
        .unwrap_or(&absolute_span_path)
        .to_string_lossy()
        .to_string();
    let crate_name = std::env::var("CARGO_PKG_NAME").unwrap_or_default();
    let bulletproof_seed = format!(
        "{}::{}::{} Colonial_({})",
        crate_name, relative_path, function_ident, type_signature
    );
    let uuid =
        uuid::Uuid::new_v5(&uuid::Uuid::NAMESPACE_DNS, bulletproof_seed.as_bytes()).to_string();

    quote! {
        #(#attrs)*
        #vis #sig {
            #[cfg(not(any(feature = "orchestrator", feature = "remote")))]
            compile_error!("None of the required features were selected");

            if cfg!(feature = "orchestrator") {
                let rpc_payload = fox::RpcPayload {
                    uuid: #uuid.to_string(),
                    data: fox::postcard::to_allocvec(&( #(#arg_idents),* ))
                            .expect("Failed to serialize args"),
                };

                let outbound_bytes =
                    fox::postcard::to_allocvec_cobs(&rpc_payload)
                        .expect("Failed to serialize payload");

                use ::std::io::Write as _;
                let mut stdout = ::std::io::stdout().lock();
                stdout.write_all(&outbound_bytes).expect("Failed to write to stdout");
                stdout.flush().expect("Failed to flush stdout");

                use std::io::BufRead as _;
                let mut inbound_bytes = ::std::vec::Vec::new();
                ::std::io::stdin().lock()
                    .read_until(0x00, &mut inbound_bytes)
                    .expect("Failed to read stdin");
                fox::postcard::from_bytes_cobs(&mut inbound_bytes)
                    .expect("Failed to deserialize return value")
            } else {
                #block
            }
        }

        #[cfg(feature = "remote")]
        fox::inventory::submit! {
            fox::RemoteFn {
                uuid: #uuid,
                function: |args_bytes| {
                    let ( #(#arg_idents),* ): ( #(#arg_types),* ) =
                        fox::postcard::from_bytes(&args_bytes)
                        .expect("Failed to deserialize args");

                    fox::postcard::to_allocvec_cobs(&#function_ident( #(#arg_idents),* ))
                        .expect("Failed to serialize return value")
                },
            }
        }
    }
    .into()
}