1use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5use uuid::Uuid;
6
7use crate::core::{Location, SourceRef, Timestamp};
8use crate::error::{Error, Result};
9
10#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
12#[serde(transparent)]
13pub struct EventId(pub Uuid);
14
15impl EventId {
16 pub fn new() -> Self {
18 Self(Uuid::new_v4())
19 }
20
21 pub fn from_uuid(uuid: Uuid) -> Self {
23 Self(uuid)
24 }
25
26 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 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#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
78pub struct Event {
79 pub id: EventId,
81 pub location: Location,
83 pub timestamp: Timestamp,
85 pub text: String,
87 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
89 pub metadata: HashMap<String, String>,
90 #[serde(default, skip_serializing_if = "Vec::is_empty")]
92 pub sources: Vec<SourceRef>,
93 #[serde(default, skip_serializing_if = "Vec::is_empty")]
95 pub tags: Vec<String>,
96}
97
98impl Event {
99 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 pub fn builder() -> EventBuilder {
114 EventBuilder::new()
115 }
116
117 pub fn has_tag(&self, tag: &str) -> bool {
119 self.tags.iter().any(|t| t == tag)
120 }
121
122 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 pub fn remove_tag(&mut self, tag: &str) {
132 self.tags.retain(|t| t != tag);
133 }
134
135 pub fn get_metadata(&self, key: &str) -> Option<&str> {
137 self.metadata.get(key).map(|s| s.as_str())
138 }
139
140 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 pub fn add_source(&mut self, source: SourceRef) {
147 self.sources.push(source);
148 }
149
150 pub fn to_geo_point(&self) -> geo_types::Point<f64> {
152 self.location.to_geo_point()
153 }
154}
155
156#[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 pub fn new() -> Self {
171 Self::default()
172 }
173
174 pub fn id(mut self, id: EventId) -> Self {
176 self.id = Some(id);
177 self
178 }
179
180 pub fn location(mut self, location: Location) -> Self {
182 self.location = Some(location);
183 self
184 }
185
186 pub fn coordinates(mut self, lat: f64, lon: f64) -> Self {
188 self.location = Some(Location::new(lat, lon));
189 self
190 }
191
192 pub fn timestamp(mut self, timestamp: Timestamp) -> Self {
194 self.timestamp = Some(timestamp);
195 self
196 }
197
198 pub fn timestamp_str(mut self, s: &str) -> Result<Self> {
200 self.timestamp = Some(Timestamp::parse(s)?);
201 Ok(self)
202 }
203
204 pub fn text(mut self, text: impl Into<String>) -> Self {
206 self.text = Some(text.into());
207 self
208 }
209
210 pub fn tag(mut self, tag: impl Into<String>) -> Self {
212 self.tags.push(tag.into());
213 self
214 }
215
216 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 pub fn source(mut self, source: SourceRef) -> Self {
224 self.sources.push(source);
225 self
226 }
227
228 pub fn sources(mut self, sources: impl IntoIterator<Item = SourceRef>) -> Self {
230 self.sources.extend(sources);
231 self
232 }
233
234 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 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 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"); 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}