use proc_macro2::TokenStream;
use quote::quote;
use syn::{parse2, Expr, Ident, ItemFn, Meta, Token};
pub fn expand(attr: TokenStream, item: TokenStream) -> syn::Result<TokenStream> {
let func: ItemFn = parse2(item)?;
let seed_hints = parse_seeds_hint(attr)?;
let value_arg_count = func
.sig
.inputs
.iter()
.enumerate()
.filter(|(i, _)| *i > 0)
.count();
if value_arg_count > 0 {
return Err(syn::Error::new_spanned(
&func.sig,
"#[hopper::crank] requires a zero-arg handler. Cranks are autonomous; if this instruction needs user input, use `hopper manager invoke` instead.",
));
}
let fn_name = &func.sig.ident;
let const_name = quote::format_ident!("__HOPPER_CRANK_{}", fn_name);
let seeds_entries: Vec<TokenStream> = seed_hints
.iter()
.map(|(field, seeds)| {
let field_lit = field.to_string();
let seed_exprs = seeds;
quote! {
(#field_lit, &[ #( #seed_exprs ),* ] as &[&[u8]])
}
})
.collect();
let expanded = quote! {
#func
#[allow(non_upper_case_globals, dead_code)]
pub const #const_name: ::hopper::__runtime::CrankMarker = ::hopper::__runtime::CrankMarker {
handler_name: stringify!(#fn_name),
seed_hints: &[ #( #seeds_entries ),* ],
};
};
Ok(expanded)
}
fn parse_seeds_hint(attr: TokenStream) -> syn::Result<Vec<(Ident, Vec<Expr>)>> {
if attr.is_empty() {
return Ok(Vec::new());
}
let metas: syn::punctuated::Punctuated<Meta, Token![,]> = syn::parse::Parser::parse2(
syn::punctuated::Punctuated::<Meta, Token![,]>::parse_terminated,
attr,
)?;
let mut out: Vec<(Ident, Vec<Expr>)> = Vec::new();
for meta in metas {
let Meta::List(list) = meta else {
return Err(syn::Error::new_spanned(
meta,
"#[hopper::crank] accepts only `seeds(...)` today",
));
};
if !list.path.is_ident("seeds") {
return Err(syn::Error::new_spanned(
&list.path,
"#[hopper::crank] accepts only `seeds(...)` today",
));
}
let parser = |input: syn::parse::ParseStream| -> syn::Result<Vec<(Ident, Vec<Expr>)>> {
let mut pairs: Vec<(Ident, Vec<Expr>)> = Vec::new();
while !input.is_empty() {
let name: Ident = input.parse()?;
let _eq: Token![=] = input.parse()?;
let arr: syn::ExprArray = input.parse()?;
let seeds: Vec<Expr> = arr.elems.into_iter().collect();
pairs.push((name, seeds));
if input.peek(Token![,]) {
let _: Token![,] = input.parse()?;
}
}
Ok(pairs)
};
let pairs = syn::parse::Parser::parse2(parser, list.tokens.clone())?;
out.extend(pairs);
}
Ok(out)
}