bc_envelope/extension/expressions/
event.rs

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