1use proc_macro::TokenStream;
7use quote::quote;
8use syn::{DeriveInput, ItemFn, parse_macro_input};
9
10#[proc_macro_derive(Entity, attributes(id, graphite))]
29pub fn derive_entity(input: TokenStream) -> TokenStream {
30 let input = parse_macro_input!(input as DeriveInput);
31 let name = &input.ident;
32 let entity_type = name.to_string();
33
34 let fields = match &input.data {
35 syn::Data::Struct(data) => match &data.fields {
36 syn::Fields::Named(fields) => &fields.named,
37 _ => panic!("Entity derive only supports structs with named fields"),
38 },
39 _ => panic!("Entity derive only supports structs"),
40 };
41
42 let id_field = fields
44 .iter()
45 .find(|f| f.attrs.iter().any(|a| a.path().is_ident("id")))
46 .expect("Entity must have exactly one field marked with #[id]");
47 let id_field_name = id_field.ident.as_ref().unwrap();
48
49 let field_setters = fields.iter().map(|f| {
51 let field_name = f.ident.as_ref().unwrap();
52 let field_name_str = to_camel_case(&field_name.to_string());
53 quote! {
54 entity.set(#field_name_str, self.#field_name.clone());
55 }
56 });
57
58 let field_getters = fields.iter().map(|f| {
60 let field_name = f.ident.as_ref().unwrap();
61 let field_name_str = to_camel_case(&field_name.to_string());
62 let field_type = &f.ty;
63 quote! {
64 #field_name: entity
65 .get(#field_name_str)
66 .and_then(|v| <#field_type as graphite::store::FromValue>::from_value(v.clone()))
67 .ok_or_else(|| graphite::store::EntityError::MissingField(#field_name_str.into()))?
68 }
69 });
70
71 let field_defaults = fields.iter().map(|f| {
73 let field_name = f.ident.as_ref().unwrap();
74 if f.attrs.iter().any(|a| a.path().is_ident("id")) {
75 quote! { #field_name: id.into() }
76 } else {
77 quote! { #field_name: Default::default() }
78 }
79 });
80
81 let expanded = quote! {
82 impl #name {
83 pub fn new(id: impl Into<String>) -> Self {
85 Self {
86 #(#field_defaults),*
87 }
88 }
89
90 pub fn load<H: graphite::host::HostFunctions>(host: &H, id: &str) -> Option<Self> {
92 host.store_get(#entity_type, id)
93 .and_then(|e| Self::from_entity(e).ok())
94 }
95
96 pub fn save<H: graphite::host::HostFunctions>(&self, host: &mut H) {
98 host.store_set(#entity_type, &self.id(), self.to_entity());
99 }
100
101 pub fn remove<H: graphite::host::HostFunctions>(host: &mut H, id: &str) {
103 host.store_remove(#entity_type, id);
104 }
105 }
106
107 impl graphite::store::Store for #name {
108 const ENTITY_TYPE: &'static str = #entity_type;
109
110 fn id(&self) -> &str {
111 &self.#id_field_name
112 }
113
114 fn to_entity(&self) -> graphite::store::Entity {
115 let mut entity = graphite::store::Entity::new();
116 #(#field_setters)*
117 entity
118 }
119
120 fn from_entity(entity: graphite::store::Entity) -> Result<Self, graphite::store::EntityError> {
121 Ok(Self {
122 #(#field_getters),*
123 })
124 }
125 }
126 };
127
128 TokenStream::from(expanded)
129}
130
131#[proc_macro_attribute]
164pub fn handler(attr: TokenStream, item: TokenStream) -> TokenStream {
165 let attr_str = attr.to_string();
166 let attr_str = attr_str.trim();
167
168 let is_block_handler = !attr.is_empty() && attr_str == "block";
170 let is_call_handler = !attr.is_empty() && attr_str == "call";
171 let is_file_handler = !attr.is_empty() && attr_str == "file";
172
173 let input = parse_macro_input!(item as ItemFn);
174 let fn_name = &input.sig.ident;
175 let fn_body = &input.block;
176 let fn_inputs = &input.sig.inputs;
177 let fn_vis = &input.vis;
178
179 let event_param = fn_inputs
181 .first()
182 .expect("Handler must have at least one parameter (event)");
183 let (param_name, param_type) = match event_param {
184 syn::FnArg::Typed(pat_type) => {
185 let name = match &*pat_type.pat {
186 syn::Pat::Ident(ident) => &ident.ident,
187 _ => panic!("Expected identifier pattern for event parameter"),
188 };
189 (name, &pat_type.ty)
190 }
191 _ => panic!("Handler cannot have self parameter"),
192 };
193
194 let impl_name = syn::Ident::new(&format!("{}_impl", fn_name), fn_name.span());
196
197 let event_base_type: &syn::Type = match param_type.as_ref() {
200 syn::Type::Reference(r) => &r.elem,
201 other => other,
202 };
203
204 let wasm_entry = if is_file_handler {
207 quote! {
208 #[cfg(target_arch = "wasm32")]
209 #[unsafe(no_mangle)]
210 pub extern "C" fn #fn_name(content_ptr: i32) {
211 let content = unsafe {
212 graph_as_runtime::store_read::read_asc_bytes(content_ptr as u32)
213 };
214 let ctx = graphite::FileContext::new();
215 #impl_name(content, &ctx);
216 }
217 }
218 } else if is_call_handler {
219 quote! {
220 #[cfg(target_arch = "wasm32")]
221 #[unsafe(no_mangle)]
222 pub extern "C" fn #fn_name(call_ptr: i32) {
223 let raw = unsafe {
224 graph_as_runtime::ethereum::read_ethereum_call(call_ptr as u32)
225 };
226 let #param_name = match <#event_base_type as graph_as_runtime::ethereum::FromRawCall>::from_raw_call(&raw) {
227 Ok(c) => c,
228 Err(_) => return,
229 };
230 let ctx = graphite::CallContext {
231 address: raw.address,
232 block_hash: raw.block_hash,
233 block_number: raw.block_number.clone(),
234 block_timestamp: raw.block_timestamp.clone(),
235 block_gas_used: raw.block_gas_used.clone(),
236 block_gas_limit: raw.block_gas_limit.clone(),
237 block_difficulty: raw.block_difficulty.clone(),
238 block_base_fee_per_gas: raw.block_base_fee_per_gas.clone(),
239 tx_hash: raw.tx_hash,
240 tx_index: raw.tx_index.clone(),
241 from: raw.from,
242 tx_to: raw.tx_to,
243 tx_value: raw.tx_value.clone(),
244 tx_gas_limit: raw.tx_gas_limit.clone(),
245 tx_gas_price: raw.tx_gas_price.clone(),
246 tx_nonce: raw.tx_nonce.clone(),
247 };
248 #impl_name(&#param_name, &ctx);
249 }
250 }
251 } else if is_block_handler {
252 quote! {
253 #[cfg(target_arch = "wasm32")]
254 #[unsafe(no_mangle)]
255 pub extern "C" fn #fn_name(event_ptr: i32) -> i32 {
256 let raw = unsafe {
257 graph_as_runtime::ethereum::read_ethereum_event(event_ptr as u32)
258 };
259 let #param_name = match <#event_base_type as graph_as_runtime::ethereum::FromRawEvent>::from_raw_event(&raw) {
260 Ok(e) => e,
261 Err(_) => return 1,
262 };
263 let ctx = graphite::EventContext {
264 address: raw.address,
265 log_index: raw.log_index.clone(),
266 block_hash: raw.block_hash,
267 block_number: raw.block_number.clone(),
268 block_timestamp: raw.block_timestamp.clone(),
269 block_gas_used: raw.block_gas_used.clone(),
270 block_gas_limit: raw.block_gas_limit.clone(),
271 block_difficulty: raw.block_difficulty.clone(),
272 block_base_fee_per_gas: raw.block_base_fee_per_gas.clone(),
273 tx_hash: raw.tx_hash,
274 tx_index: raw.tx_index.clone(),
275 tx_from: raw.tx_from,
276 tx_to: raw.tx_to,
277 tx_value: raw.tx_value.clone(),
278 tx_gas_limit: raw.tx_gas_limit.clone(),
279 tx_gas_price: raw.tx_gas_price.clone(),
280 tx_nonce: raw.tx_nonce.clone(),
281 receipt: raw.receipt,
282 };
283 #impl_name(&#param_name, &ctx);
284 0
285 }
286 }
287 } else {
288 quote! {
290 #[cfg(target_arch = "wasm32")]
291 #[unsafe(no_mangle)]
292 pub extern "C" fn #fn_name(event_ptr: i32) {
293 let raw = unsafe {
294 graph_as_runtime::ethereum::read_ethereum_event(event_ptr as u32)
295 };
296 let #param_name = match <#event_base_type as graph_as_runtime::ethereum::FromRawEvent>::from_raw_event(&raw) {
297 Ok(e) => e,
298 Err(_) => return,
299 };
300 let ctx = graphite::EventContext {
301 address: raw.address,
302 log_index: raw.log_index.clone(),
303 block_hash: raw.block_hash,
304 block_number: raw.block_number.clone(),
305 block_timestamp: raw.block_timestamp.clone(),
306 block_gas_used: raw.block_gas_used.clone(),
307 block_gas_limit: raw.block_gas_limit.clone(),
308 block_difficulty: raw.block_difficulty.clone(),
309 block_base_fee_per_gas: raw.block_base_fee_per_gas.clone(),
310 tx_hash: raw.tx_hash,
311 tx_index: raw.tx_index.clone(),
312 tx_from: raw.tx_from,
313 tx_to: raw.tx_to,
314 tx_value: raw.tx_value.clone(),
315 tx_gas_limit: raw.tx_gas_limit.clone(),
316 tx_gas_price: raw.tx_gas_price.clone(),
317 tx_nonce: raw.tx_nonce.clone(),
318 receipt: raw.receipt,
319 };
320 #impl_name(&#param_name, &ctx);
321 }
322 }
323 };
324
325 let (ctx_type, param_override) = if is_file_handler {
327 (quote! { graphite::FileContext }, Some(quote! { alloc::vec::Vec<u8> }))
328 } else if is_call_handler {
329 (quote! { graphite::CallContext }, None)
330 } else {
331 (quote! { graphite::EventContext }, None)
332 };
333
334 let impl_param_type = if let Some(ref override_ty) = param_override {
336 quote! { #override_ty }
337 } else {
338 quote! { #param_type }
339 };
340
341 let expanded = quote! {
342 #fn_vis fn #impl_name(
347 #param_name: #impl_param_type,
348 ctx: &#ctx_type,
349 ) #fn_body
350
351 #[cfg(not(target_arch = "wasm32"))]
356 #fn_vis fn #fn_name(
357 #param_name: #impl_param_type,
358 ctx: &#ctx_type,
359 ) {
360 #impl_name(#param_name, ctx)
361 }
362
363 #wasm_entry
368 };
369
370 TokenStream::from(expanded)
371}
372
373fn to_camel_case(s: &str) -> String {
375 let mut result = String::new();
376 let mut capitalize_next = false;
377
378 for c in s.chars() {
379 if c == '_' {
380 capitalize_next = true;
381 } else if capitalize_next {
382 result.push(c.to_ascii_uppercase());
383 capitalize_next = false;
384 } else {
385 result.push(c);
386 }
387 }
388
389 result
390}