Skip to main content

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}