coralstack_cmd_ipc/event.rs
1//! The [`Event`] trait and the [`DynEvent`] helper.
2//!
3//! An `Event` is a fire-and-forget broadcast identified by a string
4//! id. Unlike [`Command`](crate::command::Command), events have no
5//! handler — consumers subscribe via
6//! [`CommandRegistry::on`](crate::registry::CommandRegistry::on) and
7//! receive the deserialized payload.
8//!
9//! For compile-time events, the `#[event]` attribute macro generates
10//! the trait impl from a payload struct. For runtime-constructed
11//! events (plugin runtimes, FFI, scripting hosts), [`DynEvent`] lets
12//! you build an `Event` whose id is owned at runtime.
13
14use serde::Serialize;
15use serde_json::Value;
16
17/// A typed event payload.
18///
19/// Implementations pair a compile-time string id with a `Serialize`
20/// payload. The struct itself *is* the payload — `serde_json::to_value`
21/// on an instance produces what goes on the wire.
22///
23/// # Compile-time vs runtime events
24///
25/// - **Compile-time**: `const ID` / `const DESCRIPTION` and the
26/// `#[event]` macro. The defaults for [`id`](Self::id) and
27/// [`description`](Self::description) read these constants.
28/// - **Runtime**: use [`DynEvent`] to supply an owned `String` id,
29/// description, and payload. `DynEvent` implements `Event` by
30/// overriding the instance-level methods.
31///
32/// Both paths emit through the same
33/// [`emit`](crate::registry::CommandRegistry::emit) entry point.
34pub trait Event: Serialize + Send + Sync + 'static {
35 /// Compile-time event identifier. Ignored when a value overrides
36 /// [`id`](Self::id) to return a runtime string (as [`DynEvent`]
37 /// does).
38 ///
39 /// Identifiers prefixed with `_` are treated as private: they
40 /// fire only to local listeners and are never broadcast to
41 /// connected channels.
42 const ID: &'static str;
43
44 /// Instance-level id. Defaults to [`ID`](Self::ID);
45 /// [`DynEvent`] overrides this to return a runtime-owned id.
46 fn id(&self) -> &str {
47 Self::ID
48 }
49
50 /// Wire-level JSON Schema for the payload, if one is available.
51 /// The `#[event]` macro overrides this with a schema derived
52 /// from `schemars`.
53 fn schema(&self) -> Option<Value> {
54 None
55 }
56}
57
58/// A runtime-constructed [`Event`]. Use this when the event id or
59/// payload shape is only known at runtime (plugin runtimes, FFI,
60/// scripting hosts).
61///
62/// ```ignore
63/// use coralstack_cmd_ipc::prelude::*;
64/// use serde_json::json;
65///
66/// registry.emit(DynEvent::new(
67/// "plugin.say_hi",
68/// json!({ "greeting": "hello" }),
69/// ))?;
70/// ```
71pub struct DynEvent {
72 id: String,
73 schema: Option<Value>,
74 payload: Value,
75}
76
77impl DynEvent {
78 /// Build a new dynamic event with a runtime id and a JSON payload.
79 pub fn new(id: impl Into<String>, payload: Value) -> Self {
80 Self {
81 id: id.into(),
82 schema: None,
83 payload,
84 }
85 }
86
87 /// Attach a JSON Schema advertising the payload shape.
88 pub fn schema(mut self, schema: Value) -> Self {
89 self.schema = Some(schema);
90 self
91 }
92}
93
94impl Serialize for DynEvent {
95 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
96 // The wire payload is the inner `Value` — `DynEvent`'s own
97 // fields (id, description, schema) are metadata consumed by
98 // the registry, not serialized into the event's payload.
99 self.payload.serialize(serializer)
100 }
101}
102
103impl Event for DynEvent {
104 // Sentinel — registry always uses `id(&self)` for DynEvent.
105 const ID: &'static str = "";
106
107 fn id(&self) -> &str {
108 &self.id
109 }
110
111 fn schema(&self) -> Option<Value> {
112 self.schema.clone()
113 }
114}