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