use heck::ToSnakeCase;
use proc_macro2::Ident;
use quote::quote;
use syn::{DeriveInput, LitStr, Type};
pub fn into_event(input: &DeriveInput) -> proc_macro2::TokenStream {
let name = &input.ident;
let event_name = event_name_snake_case(input);
let fields = event_struct_fields(input);
let (topic_field_idents, topic_types) = fields.topics;
let (data_field_idents, data_types) = fields.data;
let has_datum = fields.has_datum;
let topic_type_tokens = topic_types.iter().map(|ty| quote!(#ty));
let data_type_tokens = data_types.iter().map(|ty| quote!(#ty));
let emit_impl = quote! {
fn emit(self, env: &stellar_axelar_std::Env) {
use stellar_axelar_std::{IntoVal, Symbol, Env, Val, Vec, vec};
let topics = (Symbol::new(env, #event_name),
#(IntoVal::<Env, Val>::into_val(&self.#topic_field_idents, env),)*
);
let data: Vec<Val> = vec![
env
#(, IntoVal::<_, Val>::into_val(&self.#data_field_idents, env))*
];
if #has_datum {
env.events().publish(topics, data.get(0));
} else {
env.events().publish(topics, data);
}
}
};
let from_event_impl = quote! {
fn from_event(env: &stellar_axelar_std::Env,
topics: stellar_axelar_std::Vec<stellar_axelar_std::Val>, data: stellar_axelar_std::Val) -> Self {
use stellar_axelar_std::{TryFromVal, Symbol, Val, Vec};
let event_name = Symbol::try_from_val(env, &topics.get(0)
.expect("missing event name in topics"))
.expect("invalid event name type");
assert_eq!(event_name, Symbol::new(env, #event_name), "event name mismatch");
let mut topic_idx = 1;
#(
let #topic_field_idents = <#topic_type_tokens>::try_from_val(env, &topics.get(topic_idx)
.expect("the number of topics does not match this function's definition"))
.expect("given topic value does not match the expected type");
topic_idx += 1;
)*
let data = if #has_datum {
let mut vec = Vec::<Val>::new(env);
vec.push_back(data);
vec
} else {
Vec::<Val>::try_from_val(env, &data)
.expect("invalid data format")
};
let mut data_idx = 0;
#(
let #data_field_idents = <#data_type_tokens>::try_from_val(env, &data.get(data_idx)
.expect("the number of data entries does not match this function's definition"))
.expect("given data value does not match the expected type");
data_idx += 1;
)*
Self {
#(#topic_field_idents,)*
#(#data_field_idents,)*
}
}
};
let attribute_name = if has_datum { "#[datum] " } else { "#[data] " };
let schema_impl = quote! {
fn schema(env: &stellar_axelar_std::Env) -> &'static str {
concat!(
#event_name, " {\n",
#(
" #[topic] ",
stringify!(#topic_field_idents),
": ",
stringify!(#topic_types),
",\n",
)*
#(
" ", #attribute_name,
stringify!(#data_field_idents),
": ",
stringify!(#data_types),
",\n",
)*
"}"
)
}
};
quote! {
impl stellar_axelar_std::events::Event for #name {
#emit_impl
#from_event_impl
#schema_impl
}
}
}
fn event_name_snake_case(input: &DeriveInput) -> String {
input
.attrs
.iter()
.find(|attr| attr.path().is_ident("event_name"))
.map(|attr| attr.parse_args::<LitStr>().unwrap().value())
.unwrap_or_else(|| {
input
.ident
.to_string()
.strip_suffix("Event")
.unwrap()
.to_snake_case()
})
}
type EventIdent<'a> = Vec<&'a Ident>;
type EventType<'a> = Vec<&'a Type>;
type EventStructFields<'a> = (EventIdent<'a>, EventType<'a>);
struct EventFields<'a> {
topics: EventStructFields<'a>,
data: EventStructFields<'a>,
has_datum: bool,
}
impl<'a> EventFields<'a> {
fn add_field(&mut self, ident: &'a Ident, ty: &'a Type, field: &syn::Field) {
match field
.attrs
.iter()
.find(|attr| attr.path().is_ident("data") || attr.path().is_ident("datum"))
{
Some(attr) if attr.path().is_ident("datum") => {
if self.has_datum {
panic!("Only one field can have the #[datum] attribute");
}
self.data.0.push(ident);
self.data.1.push(ty);
self.has_datum = true;
}
Some(_) => {
self.data.0.push(ident);
self.data.1.push(ty);
}
None => {
self.topics.0.push(ident);
self.topics.1.push(ty);
}
}
}
}
fn event_struct_fields(input: &DeriveInput) -> EventFields<'_> {
let syn::Data::Struct(data_struct) = &input.data else {
panic!("IntoEvent can only be derived for structs");
};
let mut fields = EventFields {
topics: (Vec::new(), Vec::new()),
data: (Vec::new(), Vec::new()),
has_datum: false,
};
for field in data_struct.fields.iter() {
if let Some(ident) = field.ident.as_ref() {
fields.add_field(ident, &field.ty, field);
}
}
fields
}