Lines
0 %
Functions
Branches
100 %
use proc_macro::TokenStream;
use quote::quote;
use syn::{ItemFn, Token, parse_macro_input};
enum TriggerType {
EntityType(syn::Ident),
Function(syn::Ident),
}
struct ScriptArgs {
trigger: TriggerType,
output_size: Option<u32>,
impl syn::parse::Parse for ScriptArgs {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let mut trigger = None;
let mut output_size = None;
while !input.is_empty() {
let ident: syn::Ident = input.parse()?;
input.parse::<Token![=]>()?;
match ident.to_string().as_str() {
"trigger" => {
let value: syn::Ident = input.parse()?;
trigger = Some(TriggerType::EntityType(value));
"trigger_fn" => {
trigger = Some(TriggerType::Function(value));
"output_size" => {
let lit: syn::LitInt = input.parse()?;
output_size = Some(lit.base10_parse()?);
_ => {
return Err(syn::Error::new(ident.span(), "unknown attribute"));
if input.peek(Token![,]) {
input.parse::<Token![,]>()?;
let trigger = trigger.ok_or_else(|| {
syn::Error::new(
proc_macro2::Span::call_site(),
"missing `trigger` or `trigger_fn` argument",
)
})?;
Ok(ScriptArgs {
trigger,
output_size,
})
/// Attribute macro for defining Nomisync WASM scripts.
///
/// # Arguments
/// - `trigger`: The entity type that triggers this script (Transaction, Split, Tag, etc.)
/// - `trigger_fn`: A function that receives `&Context` and returns `bool`
/// - `output_size` (optional): Size of output buffer in bytes (default: 4096)
/// # Example with entity type trigger
/// ```ignore
/// use scripting_sdk::prelude::*;
/// #[script(trigger = Transaction)]
/// fn add_tag(ctx: &mut Context) -> Result<()> {
/// ctx.tag_primary("processed", "true")?;
/// Ok(())
/// }
/// ```
/// # Example with function trigger
/// fn is_groceries(ctx: &Context) -> bool {
/// // Check if transaction has tag "note" = "groceries"
/// for tag in ctx.tags_for(ctx.primary_entity_idx()).flatten() {
/// if tag.name().ok() == Some("note") && tag.value().ok() == Some("groceries") {
/// return true;
/// false
/// #[script(trigger_fn = is_groceries)]
/// fn categorize(ctx: &mut Context) -> Result<()> {
/// // Tag all splits with category
/// for split in ctx.splits_for(ctx.primary_entity_idx()).flatten() {
/// ctx.create_tag(split.parent_idx(), "category", "groceries")?;
#[proc_macro_attribute]
pub fn script(attr: TokenStream, item: TokenStream) -> TokenStream {
let args = parse_macro_input!(attr as ScriptArgs);
let input_fn = parse_macro_input!(item as ItemFn);
let fn_name = &input_fn.sig.ident;
let fn_block = &input_fn.block;
let context_creation = if let Some(size) = args.output_size {
quote! { Context::with_output_size(#size)? }
} else {
quote! { Context::new()? }
};
let should_apply_body = match &args.trigger {
TriggerType::EntityType(entity_type) => quote! {
let Ok(ctx) = Context::new() else {
return 0;
let Ok(entity_type) = ctx.primary_entity_type() else {
if entity_type == EntityType::#entity_type {
1
0
},
TriggerType::Function(trigger_fn) => quote! {
if #trigger_fn(&ctx) {
let expanded = quote! {
use core::panic::PanicInfo;
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
loop {}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn should_apply() -> i32 {
#should_apply_body
pub unsafe extern "C" fn process() {
let _ = __script_impl();
fn __script_impl() -> Result<()> {
let mut ctx = #context_creation;
let result = #fn_name(&mut ctx);
ctx.finalize()?;
result
fn #fn_name(ctx: &mut Context) -> Result<()>
#fn_block
TokenStream::from(expanded)