use proc_macro::TokenStream;
use quote::quote;
use syn::{DeriveInput, ItemFn, parse_macro_input};
#[proc_macro_derive(Entity, attributes(id, graphite))]
pub fn derive_entity(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = &input.ident;
let entity_type = name.to_string();
let fields = match &input.data {
syn::Data::Struct(data) => match &data.fields {
syn::Fields::Named(fields) => &fields.named,
_ => panic!("Entity derive only supports structs with named fields"),
},
_ => panic!("Entity derive only supports structs"),
};
let id_field = fields
.iter()
.find(|f| f.attrs.iter().any(|a| a.path().is_ident("id")))
.expect("Entity must have exactly one field marked with #[id]");
let id_field_name = id_field.ident.as_ref().unwrap();
let field_setters = fields.iter().map(|f| {
let field_name = f.ident.as_ref().unwrap();
let field_name_str = to_camel_case(&field_name.to_string());
quote! {
entity.set(#field_name_str, self.#field_name.clone());
}
});
let field_getters = fields.iter().map(|f| {
let field_name = f.ident.as_ref().unwrap();
let field_name_str = to_camel_case(&field_name.to_string());
let field_type = &f.ty;
quote! {
#field_name: entity
.get(#field_name_str)
.and_then(|v| <#field_type as graphite::store::FromValue>::from_value(v.clone()))
.ok_or_else(|| graphite::store::EntityError::MissingField(#field_name_str.into()))?
}
});
let field_defaults = fields.iter().map(|f| {
let field_name = f.ident.as_ref().unwrap();
if f.attrs.iter().any(|a| a.path().is_ident("id")) {
quote! { #field_name: id.into() }
} else {
quote! { #field_name: Default::default() }
}
});
let expanded = quote! {
impl #name {
pub fn new(id: impl Into<String>) -> Self {
Self {
#(#field_defaults),*
}
}
pub fn load<H: graphite::host::HostFunctions>(host: &H, id: &str) -> Option<Self> {
host.store_get(#entity_type, id)
.and_then(|e| Self::from_entity(e).ok())
}
pub fn save<H: graphite::host::HostFunctions>(&self, host: &mut H) {
host.store_set(#entity_type, &self.id(), self.to_entity());
}
pub fn remove<H: graphite::host::HostFunctions>(host: &mut H, id: &str) {
host.store_remove(#entity_type, id);
}
}
impl graphite::store::Store for #name {
const ENTITY_TYPE: &'static str = #entity_type;
fn id(&self) -> &str {
&self.#id_field_name
}
fn to_entity(&self) -> graphite::store::Entity {
let mut entity = graphite::store::Entity::new();
#(#field_setters)*
entity
}
fn from_entity(entity: graphite::store::Entity) -> Result<Self, graphite::store::EntityError> {
Ok(Self {
#(#field_getters),*
})
}
}
};
TokenStream::from(expanded)
}
#[proc_macro_attribute]
pub fn handler(attr: TokenStream, item: TokenStream) -> TokenStream {
let attr_str = attr.to_string();
let attr_str = attr_str.trim();
let is_block_handler = !attr.is_empty() && attr_str == "block";
let is_call_handler = !attr.is_empty() && attr_str == "call";
let is_file_handler = !attr.is_empty() && attr_str == "file";
let input = parse_macro_input!(item as ItemFn);
let fn_name = &input.sig.ident;
let fn_body = &input.block;
let fn_inputs = &input.sig.inputs;
let fn_vis = &input.vis;
let event_param = fn_inputs
.first()
.expect("Handler must have at least one parameter (event)");
let (param_name, param_type) = match event_param {
syn::FnArg::Typed(pat_type) => {
let name = match &*pat_type.pat {
syn::Pat::Ident(ident) => &ident.ident,
_ => panic!("Expected identifier pattern for event parameter"),
};
(name, &pat_type.ty)
}
_ => panic!("Handler cannot have self parameter"),
};
let impl_name = syn::Ident::new(&format!("{}_impl", fn_name), fn_name.span());
let event_base_type: &syn::Type = match param_type.as_ref() {
syn::Type::Reference(r) => &r.elem,
other => other,
};
let wasm_entry = if is_file_handler {
quote! {
#[cfg(target_arch = "wasm32")]
#[unsafe(no_mangle)]
pub extern "C" fn #fn_name(content_ptr: i32) {
let content = unsafe {
graph_as_runtime::store_read::read_asc_bytes(content_ptr as u32)
};
let ctx = graphite::FileContext::new();
#impl_name(content, &ctx);
}
}
} else if is_call_handler {
quote! {
#[cfg(target_arch = "wasm32")]
#[unsafe(no_mangle)]
pub extern "C" fn #fn_name(call_ptr: i32) {
let raw = unsafe {
graph_as_runtime::ethereum::read_ethereum_call(call_ptr as u32)
};
let #param_name = match <#event_base_type as graph_as_runtime::ethereum::FromRawCall>::from_raw_call(&raw) {
Ok(c) => c,
Err(_) => return,
};
let ctx = graphite::CallContext {
address: raw.address,
block_hash: raw.block_hash,
block_number: raw.block_number.clone(),
block_timestamp: raw.block_timestamp.clone(),
block_gas_used: raw.block_gas_used.clone(),
block_gas_limit: raw.block_gas_limit.clone(),
block_difficulty: raw.block_difficulty.clone(),
block_base_fee_per_gas: raw.block_base_fee_per_gas.clone(),
tx_hash: raw.tx_hash,
tx_index: raw.tx_index.clone(),
from: raw.from,
tx_to: raw.tx_to,
tx_value: raw.tx_value.clone(),
tx_gas_limit: raw.tx_gas_limit.clone(),
tx_gas_price: raw.tx_gas_price.clone(),
tx_nonce: raw.tx_nonce.clone(),
};
#impl_name(&#param_name, &ctx);
}
}
} else if is_block_handler {
quote! {
#[cfg(target_arch = "wasm32")]
#[unsafe(no_mangle)]
pub extern "C" fn #fn_name(event_ptr: i32) -> i32 {
let raw = unsafe {
graph_as_runtime::ethereum::read_ethereum_event(event_ptr as u32)
};
let #param_name = match <#event_base_type as graph_as_runtime::ethereum::FromRawEvent>::from_raw_event(&raw) {
Ok(e) => e,
Err(_) => return 1,
};
let ctx = graphite::EventContext {
address: raw.address,
log_index: raw.log_index.clone(),
block_hash: raw.block_hash,
block_number: raw.block_number.clone(),
block_timestamp: raw.block_timestamp.clone(),
block_gas_used: raw.block_gas_used.clone(),
block_gas_limit: raw.block_gas_limit.clone(),
block_difficulty: raw.block_difficulty.clone(),
block_base_fee_per_gas: raw.block_base_fee_per_gas.clone(),
tx_hash: raw.tx_hash,
tx_index: raw.tx_index.clone(),
tx_from: raw.tx_from,
tx_to: raw.tx_to,
tx_value: raw.tx_value.clone(),
tx_gas_limit: raw.tx_gas_limit.clone(),
tx_gas_price: raw.tx_gas_price.clone(),
tx_nonce: raw.tx_nonce.clone(),
receipt: raw.receipt,
};
#impl_name(&#param_name, &ctx);
0
}
}
} else {
quote! {
#[cfg(target_arch = "wasm32")]
#[unsafe(no_mangle)]
pub extern "C" fn #fn_name(event_ptr: i32) {
let raw = unsafe {
graph_as_runtime::ethereum::read_ethereum_event(event_ptr as u32)
};
let #param_name = match <#event_base_type as graph_as_runtime::ethereum::FromRawEvent>::from_raw_event(&raw) {
Ok(e) => e,
Err(_) => return,
};
let ctx = graphite::EventContext {
address: raw.address,
log_index: raw.log_index.clone(),
block_hash: raw.block_hash,
block_number: raw.block_number.clone(),
block_timestamp: raw.block_timestamp.clone(),
block_gas_used: raw.block_gas_used.clone(),
block_gas_limit: raw.block_gas_limit.clone(),
block_difficulty: raw.block_difficulty.clone(),
block_base_fee_per_gas: raw.block_base_fee_per_gas.clone(),
tx_hash: raw.tx_hash,
tx_index: raw.tx_index.clone(),
tx_from: raw.tx_from,
tx_to: raw.tx_to,
tx_value: raw.tx_value.clone(),
tx_gas_limit: raw.tx_gas_limit.clone(),
tx_gas_price: raw.tx_gas_price.clone(),
tx_nonce: raw.tx_nonce.clone(),
receipt: raw.receipt,
};
#impl_name(&#param_name, &ctx);
}
}
};
let (ctx_type, param_override) = if is_file_handler {
(quote! { graphite::FileContext }, Some(quote! { alloc::vec::Vec<u8> }))
} else if is_call_handler {
(quote! { graphite::CallContext }, None)
} else {
(quote! { graphite::EventContext }, None)
};
let impl_param_type = if let Some(ref override_ty) = param_override {
quote! { #override_ty }
} else {
quote! { #param_type }
};
let expanded = quote! {
#fn_vis fn #impl_name(
#param_name: #impl_param_type,
ctx: &#ctx_type,
) #fn_body
#[cfg(not(target_arch = "wasm32"))]
#fn_vis fn #fn_name(
#param_name: #impl_param_type,
ctx: &#ctx_type,
) {
#impl_name(#param_name, ctx)
}
#wasm_entry
};
TokenStream::from(expanded)
}
fn to_camel_case(s: &str) -> String {
let mut result = String::new();
let mut capitalize_next = false;
for c in s.chars() {
if c == '_' {
capitalize_next = true;
} else if capitalize_next {
result.push(c.to_ascii_uppercase());
capitalize_next = false;
} else {
result.push(c);
}
}
result
}