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 wasm_entry = if is_file_handler {
200 quote! {
201 #[cfg(target_arch = "wasm32")]
202 #[unsafe(no_mangle)]
203 pub extern "C" fn #fn_name(content_ptr: i32) {
204 let content = unsafe {
205 graph_as_runtime::store_read::read_asc_bytes(content_ptr as u32)
206 };
207 let ctx = graphite::FileContext::new();
208 #impl_name(&content, &ctx);
209 }
210 }
211 } else if is_call_handler {
212 quote! {
213 #[cfg(target_arch = "wasm32")]
214 #[unsafe(no_mangle)]
215 pub extern "C" fn #fn_name(call_ptr: i32) {
216 let raw = unsafe {
217 graph_as_runtime::ethereum::read_ethereum_call(call_ptr as u32)
218 };
219 let #param_name = match <#param_type as graph_as_runtime::ethereum::FromRawCall>::from_raw_call(&raw) {
220 Ok(c) => c,
221 Err(_) => return,
222 };
223 let ctx = graphite::CallContext {
224 address: raw.address,
225 block_hash: raw.block_hash,
226 block_number: raw.block_number.clone(),
227 block_timestamp: raw.block_timestamp.clone(),
228 block_gas_used: raw.block_gas_used.clone(),
229 block_gas_limit: raw.block_gas_limit.clone(),
230 block_difficulty: raw.block_difficulty.clone(),
231 block_base_fee_per_gas: raw.block_base_fee_per_gas.clone(),
232 tx_hash: raw.tx_hash,
233 tx_index: raw.tx_index.clone(),
234 from: raw.from,
235 tx_to: raw.tx_to,
236 tx_value: raw.tx_value.clone(),
237 tx_gas_limit: raw.tx_gas_limit.clone(),
238 tx_gas_price: raw.tx_gas_price.clone(),
239 tx_nonce: raw.tx_nonce.clone(),
240 };
241 #impl_name(&#param_name, &ctx);
242 }
243 }
244 } else if is_block_handler {
245 quote! {
246 #[cfg(target_arch = "wasm32")]
247 #[unsafe(no_mangle)]
248 pub extern "C" fn #fn_name(event_ptr: i32) -> i32 {
249 let raw = unsafe {
250 graph_as_runtime::ethereum::read_ethereum_event(event_ptr as u32)
251 };
252 let #param_name = match <#param_type as graph_as_runtime::ethereum::FromRawEvent>::from_raw_event(&raw) {
253 Ok(e) => e,
254 Err(_) => return 1,
255 };
256 let ctx = graphite::EventContext {
257 address: raw.address,
258 log_index: raw.log_index.clone(),
259 block_hash: raw.block_hash,
260 block_number: raw.block_number.clone(),
261 block_timestamp: raw.block_timestamp.clone(),
262 block_gas_used: raw.block_gas_used.clone(),
263 block_gas_limit: raw.block_gas_limit.clone(),
264 block_difficulty: raw.block_difficulty.clone(),
265 block_base_fee_per_gas: raw.block_base_fee_per_gas.clone(),
266 tx_hash: raw.tx_hash,
267 tx_index: raw.tx_index.clone(),
268 tx_from: raw.tx_from,
269 tx_to: raw.tx_to,
270 tx_value: raw.tx_value.clone(),
271 tx_gas_limit: raw.tx_gas_limit.clone(),
272 tx_gas_price: raw.tx_gas_price.clone(),
273 tx_nonce: raw.tx_nonce.clone(),
274 receipt: raw.receipt,
275 };
276 #impl_name(&#param_name, &ctx);
277 0
278 }
279 }
280 } else {
281 quote! {
283 #[cfg(target_arch = "wasm32")]
284 #[unsafe(no_mangle)]
285 pub extern "C" fn #fn_name(event_ptr: i32) {
286 let raw = unsafe {
287 graph_as_runtime::ethereum::read_ethereum_event(event_ptr as u32)
288 };
289 let #param_name = match <#param_type as graph_as_runtime::ethereum::FromRawEvent>::from_raw_event(&raw) {
290 Ok(e) => e,
291 Err(_) => return,
292 };
293 let ctx = graphite::EventContext {
294 address: raw.address,
295 log_index: raw.log_index.clone(),
296 block_hash: raw.block_hash,
297 block_number: raw.block_number.clone(),
298 block_timestamp: raw.block_timestamp.clone(),
299 block_gas_used: raw.block_gas_used.clone(),
300 block_gas_limit: raw.block_gas_limit.clone(),
301 block_difficulty: raw.block_difficulty.clone(),
302 block_base_fee_per_gas: raw.block_base_fee_per_gas.clone(),
303 tx_hash: raw.tx_hash,
304 tx_index: raw.tx_index.clone(),
305 tx_from: raw.tx_from,
306 tx_to: raw.tx_to,
307 tx_value: raw.tx_value.clone(),
308 tx_gas_limit: raw.tx_gas_limit.clone(),
309 tx_gas_price: raw.tx_gas_price.clone(),
310 tx_nonce: raw.tx_nonce.clone(),
311 receipt: raw.receipt,
312 };
313 #impl_name(&#param_name, &ctx);
314 }
315 }
316 };
317
318 let (ctx_type, param_override) = if is_file_handler {
320 (quote! { graphite::FileContext }, Some(quote! { alloc::vec::Vec<u8> }))
321 } else if is_call_handler {
322 (quote! { graphite::CallContext }, None)
323 } else {
324 (quote! { graphite::EventContext }, None)
325 };
326
327 let impl_param_type = if let Some(ref override_ty) = param_override {
329 quote! { #override_ty }
330 } else {
331 quote! { #param_type }
332 };
333
334 let expanded = quote! {
335 #fn_vis fn #impl_name(
340 #param_name: #impl_param_type,
341 ctx: &#ctx_type,
342 ) #fn_body
343
344 #[cfg(not(target_arch = "wasm32"))]
349 #fn_vis fn #fn_name(
350 #param_name: #impl_param_type,
351 ctx: &#ctx_type,
352 ) {
353 #impl_name(#param_name, ctx)
354 }
355
356 #wasm_entry
361 };
362
363 TokenStream::from(expanded)
364}
365
366fn to_camel_case(s: &str) -> String {
368 let mut result = String::new();
369 let mut capitalize_next = false;
370
371 for c in s.chars() {
372 if c == '_' {
373 capitalize_next = true;
374 } else if capitalize_next {
375 result.push(c.to_ascii_uppercase());
376 capitalize_next = false;
377 } else {
378 result.push(c);
379 }
380 }
381
382 result
383}