turbo_genesis_macros/
lib.rs

1//! Turbo Genesis Macros
2//!
3//! This module implements the core procedural macros used by Turbo Genesis:
4//!
5//! - `#[serialize]`: Automatically derive Borsh and Serde traits.
6//! - `#[game]`: Generate entry points for hot-reload and release game loops.
7//! - `#[command]`: Embed command metadata, derive serialization, and export FFI bindings.
8//! - `#[channel]`: Embed channel metadata, derive serialization, and export subscription bindings.
9//! - `#[document]`: Derive serialization and implement `HasProgramId` for document types.
10//!
11//! Utilities within include:
12//! - Argument parsers (`CommandArgs`, `ChannelArgs`).
13//! - Helpers to inline modules and compute project metadata from `Cargo.toml`.
14
15use proc_macro::TokenStream;
16use proc_macro2::Ident;
17use quote::{format_ident, quote};
18use std::{
19    collections::BTreeSet,
20    path::{Path, PathBuf},
21};
22use syn::{
23    parse::{Parse, ParseStream},
24    parse_macro_input,
25    spanned::Spanned,
26    Error, Fields, Item, ItemEnum, ItemStruct, LitStr,
27};
28use turbo_genesis_abi::{
29    TurboProgramChannelMetadata, TurboProgramCommandMetadata, TurboProgramMetadata,
30};
31
32// =============================================================================
33// Helpers
34// =============================================================================
35
36/// Retrieves the project’s root directory from the `CARGO_MANIFEST_DIR` env var.
37///
38/// Panics if the environment variable is empty or not set.
39fn get_project_path() -> PathBuf {
40    let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_default();
41    assert!(
42        !manifest_dir.is_empty(),
43        "CARGO_MANIFEST_DIR unavailable. Could not determine project directory."
44    );
45    Path::new(&manifest_dir).to_path_buf()
46}
47
48/// Reads the user’s UUID from `[package.metadata.turbo].user` in Cargo.toml
49/// and combines it with `program_name` to produce a unique, stable `program_id`.
50///
51/// # Steps
52/// 1. Load and parse `Cargo.toml`.  
53/// 2. Extract the `user` field under `[package.metadata.turbo]`.  
54/// 3. Compute SHA-256 hash over the UUID bytes and `program_name`.  
55/// 4. Base64-url-encode the hash to produce `program_id`.
56fn create_program_metadata(program_name: &str) -> TurboProgramMetadata {
57    use base64::{engine::general_purpose::URL_SAFE_NO_PAD as b64_url_safe, Engine};
58    use sha2::{Digest, Sha256};
59
60    // Locate and read Cargo.toml
61    let project_dir = get_project_path();
62    let cargo_toml_path = project_dir.join("Cargo.toml");
63    let cargo_toml = std::fs::read_to_string(&cargo_toml_path)
64        .unwrap_or_else(|e| panic!("Could not read Cargo.toml: {e:?}"));
65
66    // Parse TOML and extract user UUID
67    let parsed: toml_edit::DocumentMut = cargo_toml
68        .parse()
69        .unwrap_or_else(|e| panic!("Invalid Cargo.toml syntax: {e:?}"));
70    let user_id = parsed["package"]["metadata"]["turbo"]["user"]
71        .as_str()
72        .expect("Missing [package.metadata.turbo].user entry");
73
74    // Hash UUID + program name
75    let uuid = uuid::Uuid::parse_str(user_id).expect("Invalid UUID format");
76    let mut hasher = Sha256::new();
77    hasher.update(uuid.as_bytes());
78    hasher.update(program_name.as_bytes());
79    let program_id = b64_url_safe.encode(hasher.finalize());
80
81    TurboProgramMetadata {
82        name: program_name.to_string(),
83        program_id,
84        owner_id: user_id.to_string(),
85        commands: BTreeSet::new(),
86        channels: BTreeSet::new(),
87    }
88}
89
90// =============================================================================
91// Serialize Macro
92// =============================================================================
93
94/// Derive Borsh and Serde serialization for structs and enums.
95///
96/// Expands to:
97/// - `#[derive(Debug, Clone, BorshDeserialize, BorshSerialize, Deserialize, Serialize)]`
98/// - Configures `#[borsh(crate = "turbo::borsh")]` and `#[serde(crate = "turbo::serde")]`.
99///
100/// # Usage
101/// ```ignore
102/// #[turbo::serialize]
103/// struct MyType { /* fields */ }
104/// ```
105#[proc_macro_attribute]
106pub fn serialize(_attr: TokenStream, item: TokenStream) -> TokenStream {
107    // Parse the input token stream as a syn Item (struct, enum, etc.)
108    let input = parse_macro_input!(item as Item);
109
110    // Only allow structs and enums
111    match &input {
112        Item::Struct(_) | Item::Enum(_) => (),
113        _ => {
114            // Emit a compile error if used on an unsupported item
115            return quote! {
116                compile_error!("#[turbo::serialize] only supports structs and enums.");
117            }
118            .into();
119        }
120    };
121
122    // Expand to derive the necessary traits and set crate paths
123    let expanded = quote! {
124        #[derive(
125            Debug,
126            Clone,
127            turbo::borsh::BorshDeserialize,
128            turbo::borsh::BorshSerialize,
129            turbo::serde::Deserialize,
130            turbo::serde::Serialize,
131        )]
132        #[borsh(crate = "turbo::borsh")]
133        #[serde(crate = "turbo::serde")]
134        #input
135    };
136
137    TokenStream::from(expanded)
138}
139
140// =============================================================================
141// Game Macro
142// =============================================================================
143
144/// Procedural macro for defining a Turbo game entry point.
145///
146/// Injects a `run()` function that:
147/// - On hot-reload, deserializes previous state or creates new state.
148/// - Calls `state.update()` and `turbo::camera::update()`.
149/// - Persists state via Borsh back into host.
150///
151/// # Usage
152/// ```ignore
153/// #[turbo::game]
154/// struct GameState { /* ... */ }
155/// ```
156#[proc_macro_attribute]
157pub fn game(_attr: TokenStream, item: TokenStream) -> TokenStream {
158    let item = parse_macro_input!(item as Item);
159    let (ident, default_state) = match &item {
160        Item::Struct(ItemStruct { ident, fields, .. }) => {
161            if *fields == Fields::Unit {
162                (ident, quote! { #ident })
163            } else if fields.is_empty() {
164                (ident, quote! { #ident {} })
165            } else {
166                (ident, quote! { #ident::new() })
167            }
168        }
169        Item::Enum(ItemEnum { ident, .. }) => (ident, quote! { #ident::new() }),
170        _ => {
171            return quote! {
172                compile_error!("#[turbo::game] only supports structs and enums.");
173            }
174            .into();
175        }
176    };
177
178    quote! {
179        #[turbo::serialize]
180        #item
181
182        #[no_mangle]
183        #[cfg(all(turbo_hot_reload, not(turbo_no_run)))]
184        pub unsafe extern "C" fn run() {
185            use turbo::borsh::*;
186            let mut state = match hot::load() {
187                Ok(bytes) => <#ident>::try_from_slice(&bytes).unwrap_or_else(|_| #default_state),
188                Err(err) => {
189                    log!("[turbo] Hot reload deserialization failed: {err:?}");
190                    #default_state
191                },
192            };
193            state.update();
194            if let Err(err) = turbo::lifecycle::on_update() {
195                turbo::log!("turbo::on_update error: {err:?}");
196            }
197            if let Ok(bytes) = borsh::to_vec(&state) {
198                if let Err(err) = hot::save(&bytes) {
199                    log!("[turbo] hot save failed: Error code {err}");
200                }
201            }
202        }
203
204        #[no_mangle]
205        #[cfg(all(turbo_hot_reload, not(turbo_no_run)))]
206        pub unsafe extern "C" fn on_before_hot_reload() {
207            if let Err(err) = turbo::lifecycle::on_before_hot_reload() {
208                turbo::log!("turbo::on_before_hot_reload error: {err:?}");
209            }
210        }
211
212        #[no_mangle]
213        #[cfg(all(turbo_hot_reload, not(turbo_no_run)))]
214        pub unsafe extern "C" fn on_after_hot_reload() {
215            if let Err(err) = turbo::lifecycle::on_after_hot_reload() {
216                turbo::log!("turbo::on_after_hot_reload error: {err:?}");
217            }
218        }
219
220        #[no_mangle]
221        #[cfg(all(turbo_hot_reload, not(turbo_no_run)))]
222        pub unsafe extern "C" fn on_reset() {
223            if let Err(err) = turbo::lifecycle::on_reset() {
224                turbo::log!("turbo::on_reset error: {err:?}");
225            }
226        }
227
228        #[no_mangle]
229        #[cfg(all(not(turbo_hot_reload), not(turbo_no_run)))]
230        pub unsafe extern "C" fn run() {
231            static mut GAME_STATE: Option<#ident> = None;
232            let mut state = GAME_STATE.take().unwrap_or_else(|| #default_state);
233            state.update();
234            if let Err(err) = turbo::lifecycle::on_update() {
235                turbo::log!("turbo::on_update error: {err:?}");
236            }
237            GAME_STATE = Some(state);
238        }
239
240        #[doc(hidden)]
241        #[cfg(turbo_no_run)]
242        pub(crate) mod __turbo_os_program_utils {
243            // Logs the incoming input (parsed via Borsh) as pretty JSON
244            pub fn log_input_as_json<T: turbo::borsh::BorshDeserialize + turbo::serde::Serialize>() {
245                use turbo::borsh::BorshDeserialize;
246                use turbo::serde_json::json;
247                let bytes = turbo::os::server::command::read_input();
248                if bytes.is_empty() {
249                    return turbo::log!("null");
250                }
251                let data = match T::try_from_slice(&bytes) {
252                    Ok(data) => data,
253                    Err(err) => return turbo::log!("{}", json!({ "error": err.to_string(), "input": bytes })),
254                };
255                let json = json!(data);
256                turbo::log!("{}", json)
257            }
258        }
259    }.into()
260}
261
262// =============================================================================
263// Command Macro
264// =============================================================================
265
266/// Parsed arguments for `#[command(program = "...", name = "...")]`.
267#[derive(Debug, Clone, Default)]
268struct CommandArgs {
269    /// A program name
270    pub program: String,
271    /// A command name
272    pub name: String,
273}
274impl Parse for CommandArgs {
275    fn parse(input: ParseStream) -> syn::Result<Self> {
276        let mut args = Self::default();
277        while !input.is_empty() {
278            let ident: syn::Ident = input.parse()?;
279            let _: syn::Token![=] = input.parse()?;
280            let value: LitStr = input.parse()?;
281            match ident.to_string().as_str() {
282                "program" => args.program = value.value(),
283                "name" => args.name = value.value(),
284                _ => return Err(syn::Error::new_spanned(ident, "unexpected attribute key")),
285            }
286            if input.peek(syn::Token![,]) {
287                let _: syn::Token![,] = input.parse()?;
288            }
289        }
290        if args.program.is_empty() {
291            return Err(input.error("missing `program`"));
292        }
293        if args.name.is_empty() {
294            return Err(input.error("missing `name`"));
295        }
296        Ok(args)
297    }
298}
299
300/// Attribute macro `#[command]` for generating FFI bindings and client helpers
301/// on a struct or enum that implements `CommandHandler`.
302///
303/// # Parameters
304/// - `attr`: TokenStream of attribute arguments (e.g. `program = "foo", name = "bar"`).
305/// - `item`: TokenStream of the annotated item (must be a `struct` or `enum`).
306///
307/// # Behavior
308/// 1. Parses `item` into a `syn::Item`.
309/// 2. Parses `attr` into our `CommandArgs` helper (extracting `program` and `name`).
310/// 3. If `item` is a `struct` or `enum`, obtains its identifier (`ident`), builds/upgrades
311///    the program metadata via `create_program_metadata(&args.program)`, and calls
312///    `process_program_command(...)` to emit the expanded FFI binding and metadata embedding.
313/// 4. If used on any other item, emits a compile error pointing at the original span,
314///    instructing that `#[command]` only applies to types implementing `CommandHandler`.
315#[proc_macro_attribute]
316pub fn command(attr: TokenStream, item: TokenStream) -> TokenStream {
317    // Step 1: Parse the annotated item (must be struct or enum)
318    let item = parse_macro_input!(item as Item);
319
320    // Step 2: Parse the attribute arguments into our helper struct
321    let args = parse_macro_input!(attr as CommandArgs);
322
323    // Step 3: Dispatch on item type
324    match &item {
325        Item::Struct(ItemStruct { ident, .. }) | Item::Enum(ItemEnum { ident, .. }) => {
326            // Build or update the program metadata from the `program` argument
327            let mut program_metadata = create_program_metadata(&args.program);
328            // Generate and return the expanded code (FFI exports, exec helper, metadata)
329            process_program_command(&mut program_metadata, args.clone(), &item, ident)
330        }
331        // Unsupported item: error at original location
332        _ => Error::new(
333            item.span(),
334            "`#[command]` may only be used on a struct or enum that implements `CommandHandler`",
335        )
336        .to_compile_error()
337        .into(),
338    }
339}
340
341/// Generates the expanded code for a `#[command]`-annotated struct or enum.
342///
343/// This function embeds metadata, derives serialization, and creates both
344/// client-side execution helpers and server-side FFI exports.
345///
346/// # Parameters
347/// - `program_metadata`: Mutable reference to the program’s metadata record.  
348///   This will be updated with the new command’s name.
349/// - `args`: The parsed `program` and `name` arguments from the attribute.  
350/// - `item`: The original `syn::Item` (struct or enum) to which the macro is applied.
351/// - `ident`: The identifier of the struct or enum being processed.
352///
353/// # Returns
354/// A `TokenStream` containing:
355/// 1. A `static` byte array in the `.turbo_programs` section with updated metadata.  
356/// 2. The original type, annotated with `#[turbo::serialize]`.  
357/// 3. An inherent `impl` block on the type, adding:
358///    - `PROGRAM_ID` and `PROGRAM_OWNER` constants.  
359///    - An `exec(self) -> String` method for client-side invocation.  
360/// 4. Under `#[cfg(turbo_no_run)]`, two `extern "C"` functions:
361///    - The command handler entrypoint (returns commit/cancel codes).  
362///    - A de-input hook for logging JSON on the server.
363fn process_program_command(
364    program_metadata: &mut TurboProgramMetadata,
365    args: CommandArgs,
366    item: &Item,
367    ident: &Ident,
368) -> TokenStream {
369    // Extract basic identifiers and names
370    let program_id = program_metadata.program_id.as_str();
371    let program_name = program_metadata.name.as_str();
372    let owner_id = program_metadata.owner_id.as_str();
373    let name = args.name;
374
375    // 1) Register this command in the program’s metadata
376    program_metadata
377        .commands
378        .insert(TurboProgramCommandMetadata { name: name.clone() });
379
380    // 2) Build the symbols used for linking and exports
381    let handler_export = format!("turbo_program:command_handler/{}/{}", program_id, name);
382    let handler_extern = format_ident!("command_handler_{}_{}", program_name, name);
383    let de_input_export = format!("turbo_program:de_command_input/{}/{}", program_id, name);
384    let de_input_extern = format_ident!("de_command_input_{}_{}", program_name, name);
385
386    // 3) Serialize the updated metadata to JSON and embed it
387    let metadata_string = serde_json::to_string(program_metadata).unwrap() + "\n";
388    let metadata_bytes = metadata_string.as_bytes().iter().map(|b| quote! { #b });
389    let metadata_len = metadata_bytes.len();
390    let metadata_ident = format_ident!(
391        "turbo_os_program_metadata_command_{}_{}",
392        program_name,
393        name
394    );
395
396    // 4) Emit the final expanded code
397    quote! {
398        // a) Embed the JSON metadata as a static byte array
399        #[used]
400        #[doc(hidden)]
401        #[allow(non_upper_case_globals)]
402        #[link_section = "turbo_os_program_metadata"]
403        pub static #metadata_ident: [u8; #metadata_len] = [#(#metadata_bytes),*];
404
405        // b) Derive serialization on the user’s type
406        #[turbo::serialize]
407        #item
408
409        // c) Implement client-side helpers on the type
410        impl #ident {
411            /// The program’s unique ID constant.
412            pub const PROGRAM_ID: &'static str = #program_id;
413            /// The program’s owner UUID string.
414            pub const PROGRAM_OWNER: &'static str = #owner_id;
415
416            /// Execute this command on the client, returning the host’s response.
417            pub fn exec(self) -> String {
418                turbo::os::client::command::exec(#program_id, #name, self)
419            }
420        }
421
422        // d) Server-side FFI entrypoint for the command handler
423        #[cfg(turbo_no_run)]
424        #[unsafe(export_name = #handler_export)]
425        pub unsafe extern "C" fn #handler_extern() -> usize {
426            let user_id = turbo::os::server::command::user_id();
427            let mut cmd = turbo::os::server::command::parse_input::<#ident>();
428            match &mut cmd {
429                Ok(cmd) => match cmd.run(&user_id) {
430                    Ok(_) => turbo::os::server::command::COMMIT,
431                    Err(_) => turbo::os::server::command::CANCEL,
432                },
433                Err(_) => turbo::os::server::command::CANCEL,
434            }
435        }
436
437        // e) Server-side de-input hook for logging raw JSON
438        #[cfg(turbo_no_run)]
439        #[unsafe(export_name = #de_input_export)]
440        pub unsafe extern "C" fn #de_input_extern() {
441            crate::__turbo_os_program_utils::log_input_as_json::<#ident>()
442        }
443    }
444    .into()
445}
446
447// =============================================================================
448// Channel Macro
449// =============================================================================
450
451/// Parsed arguments for `#[channel(program = "...", name = "...")]`.
452#[derive(Debug, Clone, Default)]
453struct ChannelArgs {
454    /// A program name
455    pub program: String,
456    /// A channel name
457    pub name: String,
458}
459impl Parse for ChannelArgs {
460    fn parse(input: ParseStream) -> syn::Result<Self> {
461        let mut args = Self::default();
462        while !input.is_empty() {
463            let ident: syn::Ident = input.parse()?;
464            let _: syn::Token![=] = input.parse()?;
465            let value: LitStr = input.parse()?;
466            match ident.to_string().as_str() {
467                "program" => args.program = value.value(),
468                "name" => args.name = value.value(),
469                _ => return Err(syn::Error::new_spanned(ident, "unexpected attribute key")),
470            }
471            if input.peek(syn::Token![,]) {
472                let _: syn::Token![,] = input.parse()?;
473            }
474        }
475        if args.program.is_empty() {
476            return Err(input.error("missing `program`"));
477        }
478        if args.name.is_empty() {
479            return Err(input.error("missing `name`"));
480        }
481        Ok(args)
482    }
483}
484
485/// Attribute macro `#[channel]` for generating client and server bindings
486/// on a struct or enum that implements `ChannelHandler`.
487///
488/// # Parameters
489/// - `attr`: The attribute arguments as a TokenStream (e.g. `program = "foo", name = "bar"`).
490/// - `item`: The annotated item’s TokenStream (must be a `struct` or `enum`).
491///
492/// # Behavior
493/// 1. Parses the annotated item into a `syn::Item`.
494/// 2. Parses `attr` into our `ChannelArgs` (extracting `program` and `name`).
495/// 3. Matches on `item`:
496///    - If it’s a `struct` or `enum`, extracts its identifier (`ident`).
497///    - Calls `create_program_metadata(&args.program)` to build or update metadata.
498///    - Invokes `process_program_channel(...)` to generate the FFI bindings and
499///      subscription helper, returning that expanded TokenStream directly.
500/// 4. If used on any other item, emits a compile error pointing at the original span,
501///    instructing that `#[channel]` only applies to types implementing `ChannelHandler`.
502#[proc_macro_attribute]
503pub fn channel(attr: TokenStream, item: TokenStream) -> TokenStream {
504    // Step 1: Parse the annotated item (struct or enum)
505    let item = parse_macro_input!(item as Item);
506
507    // Step 2: Parse the attribute arguments into our helper struct
508    let args = parse_macro_input!(attr as ChannelArgs);
509
510    // Step 3: Ensure we only operate on structs or enums
511    match &item {
512        Item::Struct(ItemStruct { ident, .. }) | Item::Enum(ItemEnum { ident, .. }) => {
513            // Build or update the program metadata from the `program` argument
514            let mut program_metadata = create_program_metadata(&args.program);
515            // Generate the expanded code (FFI exports, subscription API, metadata embedding)
516            let expanded =
517                process_program_channel(&mut program_metadata, args.clone(), &item, ident);
518            // Return the generated code
519            return expanded;
520        }
521        // Anything else is unsupported: emit a clear compile-time error
522        _ => Error::new(
523            item.span(),
524            "`#[channel]` may only be used on a struct or enum that implements `ChannelHandler`",
525        )
526        .to_compile_error()
527        .into(),
528    }
529}
530
531/// Generates the expanded code for a `#[channel]`-annotated struct or enum.
532///
533/// Embeds metadata, derives serialization, and produces both client subscription
534/// helpers and server-side FFI exports.
535///
536/// # Parameters
537/// - `program_metadata`: Mutable reference to the program’s metadata record.  
538///   This will be updated with the new channel’s name.
539/// - `args`: The parsed `program` and `name` arguments from the attribute.  
540/// - `item`: The original `syn::Item` (struct or enum) to which the macro is applied.
541/// - `ident`: The identifier of the struct or enum being processed.
542///
543/// # Returns
544/// A `TokenStream` containing:
545/// 1. A `static` byte array with channel metadata.  
546/// 2. The original type, annotated with `#[turbo::serialize]`.  
547/// 3. An inherent `impl` block adding:
548///    - A `subscribe(channel_id) -> Option<Connection>` method for clients.  
549/// 4. Under `#[cfg(turbo_no_run)]`, `extern "C"` functions for server-side:
550///    - The channel handler (open/connect/data/timeout/close loop).  
551///    - De-send and de-receive hooks for logging.
552fn process_program_channel(
553    program_metadata: &mut TurboProgramMetadata,
554    args: ChannelArgs,
555    item: &Item,
556    ident: &Ident,
557) -> TokenStream {
558    // Extract identifiers
559    let program_id = program_metadata.program_id.as_str();
560    let program_name = program_metadata.name.as_str();
561    let name = args.name;
562
563    // 1) Register this channel in the program’s metadata
564    program_metadata
565        .channels
566        .insert(TurboProgramChannelMetadata { name: name.clone() });
567
568    // 2) Build linking symbols for subscribe and handlers
569    let handler_export = format!("turbo_program:channel_handler/{}/{}", program_id, name);
570    let handler_extern = format_ident!("channel_handler_{}_{}", program_name, name);
571    let de_send_export = format!("turbo_program:de_channel_send/{}/{}", program_id, name);
572    let de_send_extern = format_ident!("de_channel_send_{}_{}", program_name, name);
573    let de_recv_export = format!("turbo_program:de_channel_recv/{}/{}", program_id, name);
574    let de_recv_extern = format_ident!("de_channel_recv_{}_{}", program_name, name);
575
576    // 3) Serialize and embed updated channel metadata
577    let metadata_string = serde_json::to_string(program_metadata).unwrap() + "\n";
578    let metadata_bytes = metadata_string.as_bytes().iter().map(|b| quote! { #b });
579    let metadata_len = metadata_bytes.len();
580    let metadata_ident = format_ident!(
581        "turbo_os_program_metadata_channel_{}_{}",
582        program_name,
583        name
584    );
585
586    // 4) Emit final expanded code
587    quote! {
588        // a) Embed channel metadata
589        #[used]
590        #[doc(hidden)]
591        #[allow(non_upper_case_globals)]
592        #[link_section = "turbo_os_program_metadata"]
593        pub static #metadata_ident: [u8; #metadata_len] = [#(#metadata_bytes),*];
594
595        // b) Derive serialization on the type
596        #[turbo::serialize]
597        #item
598
599        // c) Client subscription helper
600        impl #ident {
601            /// Subscribe to this channel ID, returning a connection if available.
602            pub fn subscribe(
603                channel_id: &str
604            ) -> Option<turbo::os::client::channel::ChannelConnection<
605                <Self as turbo::os::server::channel::ChannelHandler>::Recv,
606                <Self as turbo::os::server::channel::ChannelHandler>::Send,
607            >> {
608                turbo::os::client::channel::Channel::subscribe(#program_id, #name, channel_id)
609            }
610        }
611
612        // d) Server-side channel handler loop
613        #[cfg(turbo_no_run)]
614        #[unsafe(export_name = #handler_export)]
615        pub unsafe extern "C" fn #handler_extern() {
616            use turbo::os::server::channel::{ChannelSettings, ChannelMessage, ChannelError, recv_with_timeout};
617            let handler = &mut #ident::new();
618            let settings = &mut ChannelSettings::default();
619
620            // on_open hook
621            if let Err(err) = handler.on_open(settings) {
622                turbo::log!("Error in on_open: {err:?}");
623                return;
624            }
625
626            // Main receive loop with timeout
627            let timeout = settings.interval.unwrap_or(u32::MAX).max(16);
628            loop {
629                match recv_with_timeout(timeout) {
630                    Ok(ChannelMessage::Connect(user_id, _)) => {
631                        let _ = handler.on_connect(&user_id).map_err(|e| turbo::log!("on_connect err: {e:?}"));
632                    }
633                    Ok(ChannelMessage::Disconnect(user_id, _)) => {
634                        let _ = handler.on_disconnect(&user_id).map_err(|e| turbo::log!("on_disconnect err: {e:?}"));
635                    }
636                    Ok(ChannelMessage::Data(user_id, data)) => match #ident::parse(&data) {
637                        Ok(data) => {
638                            let _ = handler.on_data(&user_id, data).map_err(|e| turbo::log!("on_data err: {e:?}")); 
639                        }
640                        Err(err) => turbo::log!("Error parsing data: {err:?}"),
641                    }
642                    Err(ChannelError::Timeout) => {
643                        let _ = handler.on_interval().map_err(|e| turbo::log!("on_interval err: {e:?}"));  
644                    }
645                    Err(_) => {
646                        let _ = handler.on_close().map_err(|e| turbo::log!("on_close err: {e:?}"));  
647                        return;
648                    }
649                }
650            }
651        }
652
653        // e) De-send hook: log input as JSON for outgoing messages
654        #[cfg(turbo_no_run)]
655        #[unsafe(export_name = #de_send_export)]
656        pub unsafe extern "C" fn #de_send_extern() {
657            crate::__turbo_os_program_utils::log_input_as_json::<<#ident as turbo::os::server::channel::ChannelHandler>::Send>()
658        }
659
660        // f) De-receive hook: log input as JSON for incoming messages
661        #[cfg(turbo_no_run)]
662        #[unsafe(export_name = #de_recv_export)]
663        pub unsafe extern "C" fn #de_recv_extern() {
664            crate::__turbo_os_program_utils::log_input_as_json::<<#ident as turbo::os::server::channel::ChannelHandler>::Recv>()
665        }
666    }.into()
667}
668
669// =============================================================================
670// Document Macro
671// =============================================================================
672
673/// Parsed arguments for `#[document(program = "...")]`.
674#[derive(Debug, Clone, Default)]
675struct DocumentArgs {
676    /// A program name
677    pub program: String,
678}
679impl Parse for DocumentArgs {
680    fn parse(input: ParseStream) -> syn::Result<Self> {
681        let mut args = Self::default();
682        while !input.is_empty() {
683            let ident: syn::Ident = input.parse()?;
684            let _: syn::Token![=] = input.parse()?;
685            let value: LitStr = input.parse()?;
686            match ident.to_string().as_str() {
687                "program" => args.program = value.value(),
688                _ => return Err(syn::Error::new_spanned(ident, "unexpected attribute key")),
689            }
690            if input.peek(syn::Token![,]) {
691                let _: syn::Token![,] = input.parse()?;
692            }
693        }
694        if args.program.is_empty() {
695            return Err(input.error("missing `program`"));
696        }
697        Ok(args)
698    }
699}
700
701/// Attribute macro `#[document]` for embedding program metadata and deriving serialization
702/// on a struct or enum representing a document type.
703///
704/// # Parameters
705/// - `attr`: TokenStream of attribute arguments (e.g. `program = "my_prog"`).
706/// - `item`: TokenStream of the annotated item (must be a `struct` or `enum`).
707///
708/// # Behavior
709/// 1. Parses `item` into a `syn::Item`.
710/// 2. Parses `attr` into `DocumentArgs`, extracting the `program` identifier.
711/// 3. If `item` is a `struct` or `enum`:
712///    - Calls `create_program_metadata(&args.program)` to compute the stable `program_id`.
713///    - Emits:
714///      - `#[turbo::serialize]` on the original type to derive Borsh/Serde traits.
715///      - An `impl HasProgramId` for the type, setting `PROGRAM_ID` to the computed ID.
716/// 4. If used on any other item, produces a compile error at the original span.
717///
718/// # Example
719/// ```ignore
720/// #[document(program = "counter")]
721/// struct Counter { /* fields */ }
722/// ```
723#[proc_macro_attribute]
724pub fn document(attr: TokenStream, item: TokenStream) -> TokenStream {
725    // Step 1: Parse the annotated item (struct or enum)
726    let item = parse_macro_input!(item as Item);
727
728    // Step 2: Parse the attribute arguments into our helper struct
729    let args = parse_macro_input!(attr as DocumentArgs);
730
731    // Step 3: Ensure only structs or enums are supported
732    match &item {
733        Item::Struct(ItemStruct { ident, .. }) | Item::Enum(ItemEnum { ident, .. }) => {
734            // Compute or update the program metadata (hashing UUID + program name)
735            let program_metadata = create_program_metadata(&args.program);
736            // Extract the base64-encoded program ID string
737            let program_id = program_metadata.program_id;
738            // Generate the expanded code:
739            //  - Derive Borsh/Serde serialization via #[turbo::serialize]
740            //  - Implement HasProgramId with the computed PROGRAM_ID constant
741            quote! {
742                #[turbo::serialize]
743                #item
744
745                impl turbo::os::HasProgramId for #ident {
746                    const PROGRAM_ID: &'static str = #program_id;
747                }
748            }
749            .into()
750        }
751        // Unsupported item types produce a clear compile-time error
752        _ => Error::new(
753            item.span(),
754            "`#[document]` may only be used on a struct or enum representing a document type",
755        )
756        .to_compile_error()
757        .into(),
758    }
759}