Skip to main content

spatial_narrative/core/
event.rs

1//! Event representation - something that happened at a place and time.
2
3use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5use uuid::Uuid;
6
7use crate::core::{Location, SourceRef, Timestamp};
8use crate::error::{Error, Result};
9
10/// Unique identifier for an event.
11#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
12#[serde(transparent)]
13pub struct EventId(pub Uuid);
14
15impl EventId {
16    /// Creates a new random EventId.
17    pub fn new() -> Self {
18        Self(Uuid::new_v4())
19    }
20
21    /// Creates an EventId from a UUID.
22    pub fn from_uuid(uuid: Uuid) -> Self {
23        Self(uuid)
24    }
25
26    /// Parses an EventId from a string.
27    pub fn parse(s: &str) -> Result<Self> {
28        Uuid::parse_str(s)
29            .map(Self)
30            .map_err(|_| Error::ParseError(format!("invalid event ID: {}", s)))
31    }
32
33    /// Returns the inner UUID.
34    pub fn as_uuid(&self) -> &Uuid {
35        &self.0
36    }
37}
38
39impl Default for EventId {
40    fn default() -> Self {
41        Self::new()
42    }
43}
44
45impl std::fmt::Display for EventId {
46    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
47        write!(f, "{}", self.0)
48    }
49}
50
51impl From<Uuid> for EventId {
52    fn from(uuid: Uuid) -> Self {
53        Self(uuid)
54    }
55}
56
57/// An event in a spatial narrative.
58///
59/// Events are the fundamental unit of spatial narratives. Each event
60/// represents something that happened at a specific place and time.
61///
62/// # Examples
63///
64/// ```
65/// use spatial_narrative::core::{Event, Location, Timestamp, SourceRef};
66///
67/// let event = Event::builder()
68///     .location(Location::new(40.7128, -74.0060))
69///     .timestamp(Timestamp::now())
70///     .text("Protest began at City Hall")
71///     .tag("protest")
72///     .tag("politics")
73///     .source(SourceRef::article("https://news.example.com/protest"))
74///     .metadata("participants", "5000")
75///     .build();
76/// ```
77#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
78pub struct Event {
79    /// Unique identifier.
80    pub id: EventId,
81    /// Geographic location.
82    pub location: Location,
83    /// When the event occurred.
84    pub timestamp: Timestamp,
85    /// Description of the event.
86    pub text: String,
87    /// Key-value metadata.
88    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
89    pub metadata: HashMap<String, String>,
90    /// References to source material.
91    #[serde(default, skip_serializing_if = "Vec::is_empty")]
92    pub sources: Vec<SourceRef>,
93    /// Categorical tags.
94    #[serde(default, skip_serializing_if = "Vec::is_empty")]
95    pub tags: Vec<String>,
96}
97
98impl Event {
99    /// Creates a new event with required fields.
100    pub fn new(location: Location, timestamp: Timestamp, text: impl Into<String>) -> Self {
101        Self {
102            id: EventId::new(),
103            location,
104            timestamp,
105            text: text.into(),
106            metadata: HashMap::new(),
107            sources: Vec::new(),
108            tags: Vec::new(),
109        }
110    }
111
112    /// Creates a builder for constructing an Event.
113    pub fn builder() -> EventBuilder {
114        EventBuilder::new()
115    }
116
117    /// Returns true if the event has the given tag.
118    pub fn has_tag(&self, tag: &str) -> bool {
119        self.tags.iter().any(|t| t == tag)
120    }
121
122    /// Adds a tag to the event.
123    pub fn add_tag(&mut self, tag: impl Into<String>) {
124        let tag = tag.into();
125        if !self.has_tag(&tag) {
126            self.tags.push(tag);
127        }
128    }
129
130    /// Removes a tag from the event.
131    pub fn remove_tag(&mut self, tag: &str) {
132        self.tags.retain(|t| t != tag);
133    }
134
135    /// Gets a metadata value.
136    pub fn get_metadata(&self, key: &str) -> Option<&str> {
137        self.metadata.get(key).map(|s| s.as_str())
138    }
139
140    /// Sets a metadata value.
141    pub fn set_metadata(&mut self, key: impl Into<String>, value: impl Into<String>) {
142        self.metadata.insert(key.into(), value.into());
143    }
144
145    /// Adds a source reference.
146    pub fn add_source(&mut self, source: SourceRef) {
147        self.sources.push(source);
148    }
149
150    /// Returns the location as a geo-types Point.
151    pub fn to_geo_point(&self) -> geo_types::Point<f64> {
152        self.location.to_geo_point()
153    }
154}
155
156/// Builder for constructing [`Event`] instances.
157#[derive(Debug, Default)]
158pub struct EventBuilder {
159    id: Option<EventId>,
160    location: Option<Location>,
161    timestamp: Option<Timestamp>,
162    text: Option<String>,
163    metadata: HashMap<String, String>,
164    sources: Vec<SourceRef>,
165    tags: Vec<String>,
166}
167
168impl EventBuilder {
169    /// Creates a new EventBuilder.
170    pub fn new() -> Self {
171        Self::default()
172    }
173
174    /// Sets the event ID.
175    pub fn id(mut self, id: EventId) -> Self {
176        self.id = Some(id);
177        self
178    }
179
180    /// Sets the location.
181    pub fn location(mut self, location: Location) -> Self {
182        self.location = Some(location);
183        self
184    }
185
186    /// Sets the location from coordinates.
187    pub fn coordinates(mut self, lat: f64, lon: f64) -> Self {
188        self.location = Some(Location::new(lat, lon));
189        self
190    }
191
192    /// Sets the timestamp.
193    pub fn timestamp(mut self, timestamp: Timestamp) -> Self {
194        self.timestamp = Some(timestamp);
195        self
196    }
197
198    /// Sets the timestamp from a parseable string.
199    pub fn timestamp_str(mut self, s: &str) -> Result<Self> {
200        self.timestamp = Some(Timestamp::parse(s)?);
201        Ok(self)
202    }
203
204    /// Sets the event text.
205    pub fn text(mut self, text: impl Into<String>) -> Self {
206        self.text = Some(text.into());
207        self
208    }
209
210    /// Adds a tag.
211    pub fn tag(mut self, tag: impl Into<String>) -> Self {
212        self.tags.push(tag.into());
213        self
214    }
215
216    /// Adds multiple tags.
217    pub fn tags(mut self, tags: impl IntoIterator<Item = impl Into<String>>) -> Self {
218        self.tags.extend(tags.into_iter().map(Into::into));
219        self
220    }
221
222    /// Adds a source reference.
223    pub fn source(mut self, source: SourceRef) -> Self {
224        self.sources.push(source);
225        self
226    }
227
228    /// Adds multiple sources.
229    pub fn sources(mut self, sources: impl IntoIterator<Item = SourceRef>) -> Self {
230        self.sources.extend(sources);
231        self
232    }
233
234    /// Sets a metadata value.
235    pub fn metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
236        self.metadata.insert(key.into(), value.into());
237        self
238    }
239
240    /// Builds the Event.
241    ///
242    /// Uses current time if timestamp is not set.
243    /// Uses empty string if text is not set.
244    pub fn build(self) -> Event {
245        Event {
246            id: self.id.unwrap_or_default(),
247            location: self.location.unwrap_or_default(),
248            timestamp: self.timestamp.unwrap_or_else(Timestamp::now),
249            text: self.text.unwrap_or_default(),
250            metadata: self.metadata,
251            sources: self.sources,
252            tags: self.tags,
253        }
254    }
255
256    /// Builds the Event, returning an error if required fields are missing.
257    pub fn try_build(self) -> Result<Event> {
258        let location = self.location.ok_or(Error::MissingField("location"))?;
259        let timestamp = self.timestamp.ok_or(Error::MissingField("timestamp"))?;
260        let text = self.text.ok_or(Error::MissingField("text"))?;
261
262        Ok(Event {
263            id: self.id.unwrap_or_default(),
264            location,
265            timestamp,
266            text,
267            metadata: self.metadata,
268            sources: self.sources,
269            tags: self.tags,
270        })
271    }
272}
273
274#[cfg(test)]
275mod tests {
276    use super::*;
277
278    #[test]
279    fn test_event_id_new() {
280        let id1 = EventId::new();
281        let id2 = EventId::new();
282        assert_ne!(id1, id2);
283    }
284
285    #[test]
286    fn test_event_id_parse() {
287        let id = EventId::new();
288        let parsed = EventId::parse(&id.to_string()).unwrap();
289        assert_eq!(id, parsed);
290    }
291
292    #[test]
293    fn test_event_new() {
294        let event = Event::new(
295            Location::new(40.7128, -74.0060),
296            Timestamp::now(),
297            "Test event",
298        );
299        assert_eq!(event.text, "Test event");
300        assert!(event.tags.is_empty());
301    }
302
303    #[test]
304    fn test_event_builder() {
305        let event = Event::builder()
306            .location(Location::new(40.7128, -74.0060))
307            .timestamp(Timestamp::now())
308            .text("Protest at City Hall")
309            .tag("protest")
310            .tag("politics")
311            .metadata("participants", "1000")
312            .source(SourceRef::article("https://example.com"))
313            .build();
314
315        assert_eq!(event.text, "Protest at City Hall");
316        assert!(event.has_tag("protest"));
317        assert!(event.has_tag("politics"));
318        assert_eq!(event.get_metadata("participants"), Some("1000"));
319        assert_eq!(event.sources.len(), 1);
320    }
321
322    #[test]
323    fn test_event_try_build_missing_fields() {
324        let result = Event::builder().try_build();
325        assert!(result.is_err());
326    }
327
328    #[test]
329    fn test_event_tags() {
330        let mut event = Event::new(Location::new(0.0, 0.0), Timestamp::now(), "Test");
331
332        event.add_tag("tag1");
333        event.add_tag("tag2");
334        event.add_tag("tag1"); // Duplicate should be ignored
335
336        assert_eq!(event.tags.len(), 2);
337        assert!(event.has_tag("tag1"));
338        assert!(event.has_tag("tag2"));
339
340        event.remove_tag("tag1");
341        assert!(!event.has_tag("tag1"));
342    }
343
344    #[test]
345    fn test_event_metadata() {
346        let mut event = Event::builder()
347            .location(Location::new(0.0, 0.0))
348            .timestamp(Timestamp::now())
349            .text("Test")
350            .metadata("key1", "value1")
351            .build();
352
353        assert_eq!(event.get_metadata("key1"), Some("value1"));
354        assert_eq!(event.get_metadata("key2"), None);
355
356        event.set_metadata("key2", "value2");
357        assert_eq!(event.get_metadata("key2"), Some("value2"));
358    }
359
360    #[test]
361    fn test_event_serialization() {
362        let event = Event::builder()
363            .location(Location::new(40.7128, -74.0060))
364            .timestamp(Timestamp::parse("2024-03-15T14:30:00Z").unwrap())
365            .text("Test event")
366            .tag("test")
367            .build();
368
369        let json = serde_json::to_string(&event).unwrap();
370        let parsed: Event = serde_json::from_str(&json).unwrap();
371
372        assert_eq!(event.text, parsed.text);
373        assert_eq!(event.location.lat, parsed.location.lat);
374    }
375}