bc_envelope/extension/expressions/
event.rs

1use bc_components::{ARID, tags};
2use dcbor::{Date, prelude::*};
3
4use crate::{Envelope, EnvelopeEncodable, Error, Result, known_values};
5
6/// An `Event` represents a notification or message that doesn't expect a
7/// response.
8///
9/// Unlike `Request` and `Response` which form a pair, an `Event` is a
10/// standalone message that can be used for broadcasting information, logging,
11/// or publishing notifications. Events are used when the sender does not expect
12/// or require a response from the recipients.
13///
14/// Each event contains:
15/// - Content of a generic type `T` that holds the event payload
16/// - A unique identifier (ARID) for tracking and correlation
17/// - Optional metadata like a note and timestamp
18///
19/// When serialized to an envelope, events are tagged with `#6.40012`
20/// (TAG_EVENT).
21///
22/// # Type Parameters
23///
24/// * `T` - The type of content this event carries. Must implement
25///   `EnvelopeEncodable` and be convertible from an `Envelope`.
26///
27/// # Examples
28///
29/// ```
30/// use bc_components::ARID;
31/// use bc_envelope::prelude::*;
32///
33/// // Create a status update event
34/// let event_id = ARID::new();
35/// let timestamp = Date::try_from("2024-08-15T13:45:30Z").unwrap();
36///
37/// let status_event = Event::<String>::new("System online", event_id)
38///     .with_note("Regular status update")
39///     .with_date(timestamp);
40///
41/// // Convert to an envelope for transmission
42/// let envelope = status_event.into_envelope();
43/// ```
44#[derive(Debug, Clone, PartialEq)]
45pub struct Event<T>
46where
47    T: EnvelopeEncodable
48        + TryFrom<Envelope>
49        + std::fmt::Debug
50        + Clone
51        + PartialEq,
52{
53    content: T,
54    id: ARID,
55    note: String,
56    date: Option<Date>,
57}
58
59impl<T> std::fmt::Display for Event<T>
60where
61    T: EnvelopeEncodable
62        + TryFrom<Envelope>
63        + std::fmt::Debug
64        + Clone
65        + PartialEq,
66{
67    /// Formats the event for display, showing its ID and content.
68    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
69        write!(f, "Event({})", self.summary())
70    }
71}
72
73impl<T> Event<T>
74where
75    T: EnvelopeEncodable
76        + TryFrom<Envelope>
77        + std::fmt::Debug
78        + Clone
79        + PartialEq,
80{
81    /// Returns a human-readable summary of the event.
82    pub fn summary(&self) -> String {
83        format!(
84            "id: {}, content: {}",
85            self.id.short_description(),
86            self.content.to_envelope().format_flat()
87        )
88    }
89}
90
91impl<T> Event<T>
92where
93    T: EnvelopeEncodable
94        + TryFrom<Envelope>
95        + std::fmt::Debug
96        + Clone
97        + PartialEq,
98{
99    /// Creates a new event with the specified content and ID.
100    ///
101    /// # Arguments
102    ///
103    /// * `content` - The payload for this event
104    /// * `id` - Unique identifier for the event
105    ///
106    /// # Examples
107    ///
108    /// ```
109    /// use bc_components::ARID;
110    /// use bc_envelope::prelude::*;
111    ///
112    /// let event_id = ARID::new();
113    /// let event = Event::<String>::new("Temperature alert: 90°F", event_id);
114    /// ```
115    pub fn new(content: impl Into<T>, id: ARID) -> Self {
116        Self {
117            content: content.into(),
118            id,
119            note: String::new(),
120            date: None,
121        }
122    }
123}
124
125/// Trait that defines the behavior of an event.
126///
127/// This trait provides methods for composing events with metadata
128/// and for extracting information from events.
129pub trait EventBehavior<T>
130where
131    T: EnvelopeEncodable + TryFrom<Envelope>,
132{
133    //
134    // Composition
135    //
136
137    /// Adds a note to the event.
138    ///
139    /// This provides human-readable context about the event's purpose.
140    fn with_note(self, note: impl Into<String>) -> Self;
141
142    /// Adds a date to the event.
143    ///
144    /// This timestamp typically represents when the event occurred.
145    fn with_date(self, date: Date) -> Self;
146
147    //
148    // Parsing
149    //
150
151    /// Returns the content of the event.
152    fn content(&self) -> &T;
153
154    /// Returns the unique identifier (ARID) of the event.
155    fn id(&self) -> ARID;
156
157    /// Returns the note attached to the event, or an empty string if none
158    /// exists.
159    fn note(&self) -> &str;
160
161    /// Returns the date attached to the event, if any.
162    fn date(&self) -> Option<Date>;
163}
164
165impl<T> EventBehavior<T> for Event<T>
166where
167    T: EnvelopeEncodable
168        + TryFrom<Envelope>
169        + std::fmt::Debug
170        + Clone
171        + PartialEq,
172{
173    /// Adds a note to the event.
174    fn with_note(mut self, note: impl Into<String>) -> Self {
175        self.note = note.into();
176        self
177    }
178
179    /// Adds a date to the event.
180    fn with_date(mut self, date: Date) -> Self {
181        self.date = Some(date);
182        self
183    }
184
185    /// Returns the content of the event.
186    fn content(&self) -> &T { &self.content }
187
188    /// Returns the ID of the event.
189    fn id(&self) -> ARID { self.id }
190
191    /// Returns the note of the event.
192    fn note(&self) -> &str { &self.note }
193
194    /// Returns the date of the event.
195    fn date(&self) -> Option<Date> { self.date }
196}
197
198/// Converts an `Event<T>` to an `Envelope`.
199///
200/// The envelope's subject is the event's ID tagged with TAG_EVENT,
201/// and assertions include the event's content, note (if not empty), and date
202/// (if present).
203impl<T> From<Event<T>> for Envelope
204where
205    T: EnvelopeEncodable
206        + TryFrom<Envelope>
207        + std::fmt::Debug
208        + Clone
209        + PartialEq,
210{
211    fn from(event: Event<T>) -> Self {
212        Envelope::new(CBOR::to_tagged_value(tags::TAG_EVENT, event.id))
213            .add_assertion(known_values::CONTENT, event.content.to_envelope())
214            .add_assertion_if(
215                !event.note.is_empty(),
216                known_values::NOTE,
217                event.note,
218            )
219            .add_optional_assertion(known_values::DATE, event.date)
220    }
221}
222
223/// Converts an `Envelope` to an `Event<T>`.
224///
225/// This constructor extracts the event ID, content, note, and date from an
226/// envelope. The content is converted to the generic type `T`.
227impl<T> TryFrom<Envelope> for Event<T>
228where
229    T: EnvelopeEncodable
230        + TryFrom<Envelope>
231        + std::fmt::Debug
232        + Clone
233        + PartialEq,
234{
235    type Error = Error;
236
237    fn try_from(envelope: Envelope) -> Result<Self> {
238        let content_envelope =
239            envelope.object_for_predicate(known_values::CONTENT)?;
240        let content = T::try_from(content_envelope)
241            .map_err(|_| Error::msg("Failed to parse content"))?;
242        Ok(Self {
243            content,
244            id: envelope
245                .subject()
246                .try_leaf()?
247                .try_into_expected_tagged_value(tags::TAG_EVENT)?
248                .try_into()?,
249            note: envelope
250                .extract_optional_object_for_predicate(known_values::NOTE)?
251                .unwrap_or_default(),
252            date: envelope
253                .extract_optional_object_for_predicate(known_values::DATE)?,
254        })
255    }
256}
257
258#[cfg(test)]
259mod tests {
260    use hex_literal::hex;
261    use indoc::indoc;
262
263    use super::*;
264
265    fn request_id() -> ARID {
266        ARID::from_data(hex!(
267            "c66be27dbad7cd095ca77647406d07976dc0f35f0d4d654bb0e96dd227a1e9fc"
268        ))
269    }
270
271    #[test]
272    fn test_event() {
273        crate::register_tags();
274
275        let event_date = Date::try_from("2024-07-04T11:11:11Z").unwrap();
276        let event = Event::<String>::new("test", request_id())
277            .with_note("This is a test")
278            .with_date(event_date);
279
280        let envelope: Envelope = event.clone().into();
281        #[rustfmt::skip]
282        let expected = indoc!{r#"
283            event(ARID(c66be27d)) [
284                'content': "test"
285                'date': 2024-07-04T11:11:11Z
286                'note': "This is a test"
287            ]
288        "#}.trim();
289        assert_eq!(envelope.format(), expected);
290
291        let parsed_request = Event::<String>::try_from(envelope).unwrap();
292        assert_eq!(parsed_request.content(), "test");
293        assert_eq!(parsed_request.note(), "This is a test");
294        assert_eq!(parsed_request.date(), Some(event_date));
295    }
296}