pub fn generate(name: &str) -> Result<(), String> { let snake = name.to_lowercase().replace(['-', ' '], "_"); let pascal = to_pascal_case(&snake); println!("=== Adding hook event: on_{snake} ==="); println!(); println!("Follow these steps to add a new `on_{snake}` hook event."); println!(); // Step 1: Event struct println!("--- Step 1: Event struct ---"); println!("File: crates/quicproquo-server/src/hooks.rs"); println!(); println!( r#"/// Event data for {snake} operations. #[derive(Clone, Debug)] pub struct {pascal}Event {{ // TODO: add your event fields here // Example: // pub channel_id: Vec, // pub user_key: Vec, }} "#, ); // Step 2: Trait method println!("--- Step 2: Trait method ---"); println!("File: crates/quicproquo-server/src/hooks.rs"); println!(); println!("Add to the `ServerHooks` trait:"); println!(); println!( r#" /// Called when {snake} occurs. fn on_{snake}(&self, _event: &{pascal}Event) {{ // Default: no-op }} "#, ); // Step 3: TracingHooks implementation println!("--- Step 3: TracingHooks implementation ---"); println!("File: crates/quicproquo-server/src/hooks.rs"); println!(); println!("Add to `impl ServerHooks for TracingHooks`:"); println!(); println!( r#" fn on_{snake}(&self, _event: &{pascal}Event) {{ tracing::info!("hook: {snake}"); }} "#, ); // Step 4: Plugin API (C-compatible struct) println!("--- Step 4: Plugin API ---"); println!("File: crates/quicproquo-plugin-api/src/lib.rs"); println!(); println!("Add a C-compatible event struct:"); println!(); println!( r#"#[repr(C)] pub struct C{pascal}Event {{ // TODO: mirror the fields from {pascal}Event using C-compatible types // Use *const u8 + len for byte slices, *const c_char for strings }} "#, ); println!("Add to `HookVTable`:"); println!(); println!( r#" pub on_{snake}: Option, "#, ); // Step 5: Wire into PluginHooks println!("--- Step 5: PluginHooks dispatch ---"); println!("File: crates/quicproquo-server/src/plugin_loader.rs"); println!(); println!("Add to `impl ServerHooks for PluginHooks`:"); println!(); println!( r#" fn on_{snake}(&self, event: &{pascal}Event) {{ if let Some(hook_fn) = self.vtable.on_{snake} {{ let c_event = C{pascal}Event {{ // TODO: convert fields }}; hook_fn(self.vtable.user_data, &c_event); }} }} "#, ); // Step 6: Call the hook println!("--- Step 6: Call the hook in the RPC handler ---"); println!("In the relevant handler file under crates/quicproquo-server/src/node_service/:"); println!(); println!( r#" use crate::hooks::{pascal}Event; // At the appropriate point in the handler: self.hooks.on_{snake}(&{pascal}Event {{ // fill in fields }}); "#, ); // Step 7: Verify println!("--- Step 7: Verify ---"); println!(" cargo build -p quicproquo-plugin-api"); println!(" cargo build -p quicproquo-server"); println!(" cargo test -p quicproquo-server"); println!(); // Summary println!("=== Files to modify ==="); println!(" [modify] crates/quicproquo-server/src/hooks.rs"); println!(" [modify] crates/quicproquo-plugin-api/src/lib.rs"); println!(" [modify] crates/quicproquo-server/src/plugin_loader.rs"); println!(" [modify] crates/quicproquo-server/src/node_service/.rs"); Ok(()) } fn to_pascal_case(snake: &str) -> String { snake .split('_') .map(|word| { let mut chars = word.chars(); match chars.next() { None => String::new(), Some(c) => c.to_uppercase().to_string() + chars.as_str(), } }) .collect() }