feat: Phase 9 — developer experience, extensibility, and community growth
New crates: - quicproquo-bot: Bot SDK with polling API + JSON pipe mode - quicproquo-kt: Key Transparency Merkle log (RFC 9162 subset) - quicproquo-plugin-api: no_std C-compatible plugin vtable API - quicproquo-gen: scaffolding tool (qpq-gen plugin/bot/rpc/hook) Server features: - ServerHooks trait wired into all RPC handlers (enqueue, fetch, auth, channel, registration) with plugin rejection support - Dynamic plugin loader (libloading) with --plugin-dir config - Delivery proof canary tokens (Ed25519 server signatures on enqueue) - Key Transparency Merkle log with inclusion proofs on resolveUser Core library: - Safety numbers (60-digit HMAC-SHA256 key verification codes) - Verifiable transcript archive (CBOR + ChaCha20-Poly1305 + hash chain) - Delivery proof verification utility - Criterion benchmarks (hybrid KEM, MLS, identity, sealed sender, padding) Client: - /verify REPL command for out-of-band key verification - Full-screen TUI via Ratatui (feature-gated --features tui) - qpq export / qpq export-verify CLI subcommands - KT inclusion proof verification on user resolution Also: ROADMAP Phase 9 added, bot SDK docs, server hooks docs, crate-responsibilities updated, example plugins (rate_limit, logging).
This commit is contained in:
134
crates/quicproquo-gen/src/generators/hook.rs
Normal file
134
crates/quicproquo-gen/src/generators/hook.rs
Normal file
@@ -0,0 +1,134 @@
|
||||
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<u8>,
|
||||
// pub user_key: Vec<u8>,
|
||||
}}
|
||||
"#,
|
||||
);
|
||||
|
||||
// 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<extern "C" fn(*mut c_void, *const C{pascal}Event)>,
|
||||
"#,
|
||||
);
|
||||
|
||||
// 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/<handler>.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()
|
||||
}
|
||||
Reference in New Issue
Block a user