1use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5use uuid::Uuid;
6
7use crate::core::{Event, EventId, GeoBounds, TimeRange, Timestamp};
8use crate::error::{Error, Result};
9
10#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
12#[serde(transparent)]
13pub struct NarrativeId(pub Uuid);
14
15impl NarrativeId {
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 narrative ID: {}", s)))
31 }
32
33 pub fn as_uuid(&self) -> &Uuid {
35 &self.0
36 }
37}
38
39impl Default for NarrativeId {
40 fn default() -> Self {
41 Self::new()
42 }
43}
44
45impl std::fmt::Display for NarrativeId {
46 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
47 write!(f, "{}", self.0)
48 }
49}
50
51#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
53pub struct NarrativeMetadata {
54 #[serde(skip_serializing_if = "Option::is_none")]
56 pub created: Option<Timestamp>,
57 #[serde(skip_serializing_if = "Option::is_none")]
59 pub modified: Option<Timestamp>,
60 #[serde(skip_serializing_if = "Option::is_none")]
62 pub author: Option<String>,
63 #[serde(skip_serializing_if = "Option::is_none")]
65 pub description: Option<String>,
66 #[serde(skip_serializing_if = "Option::is_none")]
68 pub category: Option<String>,
69 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
71 pub extra: HashMap<String, String>,
72}
73
74impl NarrativeMetadata {
75 pub fn new() -> Self {
77 Self::default()
78 }
79
80 pub fn with_created_now() -> Self {
82 Self {
83 created: Some(Timestamp::now()),
84 ..Default::default()
85 }
86 }
87}
88
89#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
113pub struct Narrative {
114 pub id: NarrativeId,
116 pub title: String,
118 #[serde(default)]
120 pub events: Vec<Event>,
121 #[serde(default)]
123 pub metadata: NarrativeMetadata,
124 #[serde(default, skip_serializing_if = "Vec::is_empty")]
126 pub tags: Vec<String>,
127}
128
129impl Narrative {
130 pub fn new(title: impl Into<String>) -> Self {
132 Self {
133 id: NarrativeId::new(),
134 title: title.into(),
135 events: Vec::new(),
136 metadata: NarrativeMetadata::with_created_now(),
137 tags: Vec::new(),
138 }
139 }
140
141 pub fn builder() -> NarrativeBuilder {
143 NarrativeBuilder::new()
144 }
145
146 pub fn events(&self) -> &[Event] {
148 &self.events
149 }
150
151 pub fn events_mut(&mut self) -> &mut Vec<Event> {
153 &mut self.events
154 }
155
156 pub fn len(&self) -> usize {
158 self.events.len()
159 }
160
161 pub fn is_empty(&self) -> bool {
163 self.events.is_empty()
164 }
165
166 pub fn add_event(&mut self, event: Event) {
168 self.events.push(event);
169 self.metadata.modified = Some(Timestamp::now());
170 }
171
172 pub fn remove_event(&mut self, id: &EventId) -> Option<Event> {
174 if let Some(pos) = self.events.iter().position(|e| &e.id == id) {
175 self.metadata.modified = Some(Timestamp::now());
176 Some(self.events.remove(pos))
177 } else {
178 None
179 }
180 }
181
182 pub fn get_event(&self, id: &EventId) -> Option<&Event> {
184 self.events.iter().find(|e| &e.id == id)
185 }
186
187 pub fn get_event_mut(&mut self, id: &EventId) -> Option<&mut Event> {
189 self.events.iter_mut().find(|e| &e.id == id)
190 }
191
192 pub fn events_chronological(&self) -> Vec<&Event> {
194 let mut events: Vec<_> = self.events.iter().collect();
195 events.sort_by_key(|e| &e.timestamp);
196 events
197 }
198
199 pub fn filter_spatial(&self, bounds: &GeoBounds) -> Vec<&Event> {
201 self.events
202 .iter()
203 .filter(|e| bounds.contains(&e.location))
204 .collect()
205 }
206
207 pub fn filter_temporal(&self, range: &TimeRange) -> Vec<&Event> {
209 self.events
210 .iter()
211 .filter(|e| range.contains(&e.timestamp))
212 .collect()
213 }
214
215 pub fn filter_by_tag(&self, tag: &str) -> Vec<&Event> {
217 self.events.iter().filter(|e| e.has_tag(tag)).collect()
218 }
219
220 pub fn bounds(&self) -> Option<GeoBounds> {
222 let locations: Vec<_> = self.events.iter().map(|e| &e.location).collect();
223 GeoBounds::from_locations(locations)
224 }
225
226 pub fn time_range(&self) -> Option<TimeRange> {
228 if self.events.is_empty() {
229 return None;
230 }
231
232 let mut min_ts = &self.events[0].timestamp;
233 let mut max_ts = &self.events[0].timestamp;
234
235 for event in &self.events {
236 if event.timestamp < *min_ts {
237 min_ts = &event.timestamp;
238 }
239 if event.timestamp > *max_ts {
240 max_ts = &event.timestamp;
241 }
242 }
243
244 Some(TimeRange::new(min_ts.clone(), max_ts.clone()))
245 }
246
247 pub fn all_tags(&self) -> Vec<&str> {
249 let mut tags: Vec<_> = self
250 .events
251 .iter()
252 .flat_map(|e| e.tags.iter().map(|s| s.as_str()))
253 .collect();
254 tags.sort();
255 tags.dedup();
256 tags
257 }
258
259 pub fn add_tag(&mut self, tag: impl Into<String>) {
261 let tag = tag.into();
262 if !self.tags.contains(&tag) {
263 self.tags.push(tag);
264 }
265 }
266
267 pub fn filter<F>(&self, predicate: F) -> Narrative
269 where
270 F: Fn(&Event) -> bool,
271 {
272 let events = self
273 .events
274 .iter()
275 .filter(|e| predicate(e))
276 .cloned()
277 .collect();
278 Narrative {
279 id: NarrativeId::new(),
280 title: format!("{} (filtered)", self.title),
281 events,
282 metadata: NarrativeMetadata::with_created_now(),
283 tags: self.tags.clone(),
284 }
285 }
286
287 pub fn merge(&mut self, other: Narrative) {
289 self.events.extend(other.events);
290 self.metadata.modified = Some(Timestamp::now());
291 }
292}
293
294impl Default for Narrative {
295 fn default() -> Self {
296 Self::new("Untitled")
297 }
298}
299
300#[derive(Debug, Default)]
302pub struct NarrativeBuilder {
303 id: Option<NarrativeId>,
304 title: Option<String>,
305 events: Vec<Event>,
306 metadata: NarrativeMetadata,
307 tags: Vec<String>,
308}
309
310impl NarrativeBuilder {
311 pub fn new() -> Self {
313 Self {
314 metadata: NarrativeMetadata::with_created_now(),
315 ..Default::default()
316 }
317 }
318
319 pub fn id(mut self, id: NarrativeId) -> Self {
321 self.id = Some(id);
322 self
323 }
324
325 pub fn title(mut self, title: impl Into<String>) -> Self {
327 self.title = Some(title.into());
328 self
329 }
330
331 pub fn event(mut self, event: Event) -> Self {
333 self.events.push(event);
334 self
335 }
336
337 pub fn events(mut self, events: impl IntoIterator<Item = Event>) -> Self {
339 self.events.extend(events);
340 self
341 }
342
343 pub fn author(mut self, author: impl Into<String>) -> Self {
345 self.metadata.author = Some(author.into());
346 self
347 }
348
349 pub fn description(mut self, description: impl Into<String>) -> Self {
351 self.metadata.description = Some(description.into());
352 self
353 }
354
355 pub fn category(mut self, category: impl Into<String>) -> Self {
357 self.metadata.category = Some(category.into());
358 self
359 }
360
361 pub fn metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
363 self.metadata.extra.insert(key.into(), value.into());
364 self
365 }
366
367 pub fn tag(mut self, tag: impl Into<String>) -> Self {
369 self.tags.push(tag.into());
370 self
371 }
372
373 pub fn tags(mut self, tags: impl IntoIterator<Item = impl Into<String>>) -> Self {
375 self.tags.extend(tags.into_iter().map(Into::into));
376 self
377 }
378
379 pub fn build(self) -> Narrative {
381 Narrative {
382 id: self.id.unwrap_or_default(),
383 title: self.title.unwrap_or_else(|| "Untitled".to_string()),
384 events: self.events,
385 metadata: self.metadata,
386 tags: self.tags,
387 }
388 }
389}
390
391#[cfg(test)]
392mod tests {
393 use super::*;
394 use crate::core::Location;
395
396 fn make_event(lat: f64, lon: f64, time: &str, text: &str) -> Event {
397 Event::builder()
398 .location(Location::new(lat, lon))
399 .timestamp(Timestamp::parse(time).unwrap())
400 .text(text)
401 .build()
402 }
403
404 #[test]
405 fn test_narrative_new() {
406 let narrative = Narrative::new("Test Narrative");
407 assert_eq!(narrative.title, "Test Narrative");
408 assert!(narrative.is_empty());
409 }
410
411 #[test]
412 fn test_narrative_builder() {
413 let narrative = Narrative::builder()
414 .title("Hurricane Timeline")
415 .description("Tracking the storm")
416 .author("Weather Service")
417 .category("disaster")
418 .tag("weather")
419 .build();
420
421 assert_eq!(narrative.title, "Hurricane Timeline");
422 assert_eq!(
423 narrative.metadata.description,
424 Some("Tracking the storm".to_string())
425 );
426 assert!(narrative.tags.contains(&"weather".to_string()));
427 }
428
429 #[test]
430 fn test_narrative_add_events() {
431 let mut narrative = Narrative::new("Test");
432
433 narrative.add_event(make_event(40.0, -74.0, "2024-03-15T10:00:00Z", "Event 1"));
434 narrative.add_event(make_event(41.0, -73.0, "2024-03-15T11:00:00Z", "Event 2"));
435
436 assert_eq!(narrative.len(), 2);
437 }
438
439 #[test]
440 fn test_narrative_filter_spatial() {
441 let mut narrative = Narrative::new("Test");
442 narrative.add_event(make_event(40.0, -74.0, "2024-03-15T10:00:00Z", "NYC"));
443 narrative.add_event(make_event(34.0, -118.0, "2024-03-15T11:00:00Z", "LA"));
444
445 let nyc_bounds = GeoBounds::new(39.0, -75.0, 41.0, -73.0);
446 let filtered = narrative.filter_spatial(&nyc_bounds);
447
448 assert_eq!(filtered.len(), 1);
449 assert_eq!(filtered[0].text, "NYC");
450 }
451
452 #[test]
453 fn test_narrative_filter_temporal() {
454 let mut narrative = Narrative::new("Test");
455 narrative.add_event(make_event(40.0, -74.0, "2024-03-15T10:00:00Z", "March"));
456 narrative.add_event(make_event(40.0, -74.0, "2024-04-15T10:00:00Z", "April"));
457
458 let march = TimeRange::month(2024, 3);
459 let filtered = narrative.filter_temporal(&march);
460
461 assert_eq!(filtered.len(), 1);
462 assert_eq!(filtered[0].text, "March");
463 }
464
465 #[test]
466 fn test_narrative_bounds() {
467 let mut narrative = Narrative::new("Test");
468 narrative.add_event(make_event(40.0, -74.0, "2024-03-15T10:00:00Z", "NYC"));
469 narrative.add_event(make_event(34.0, -118.0, "2024-03-15T11:00:00Z", "LA"));
470
471 let bounds = narrative.bounds().unwrap();
472 assert_eq!(bounds.min_lat, 34.0);
473 assert_eq!(bounds.max_lat, 40.0);
474 assert_eq!(bounds.min_lon, -118.0);
475 assert_eq!(bounds.max_lon, -74.0);
476 }
477
478 #[test]
479 fn test_narrative_time_range() {
480 let mut narrative = Narrative::new("Test");
481 narrative.add_event(make_event(40.0, -74.0, "2024-03-15T10:00:00Z", "First"));
482 narrative.add_event(make_event(40.0, -74.0, "2024-03-20T10:00:00Z", "Last"));
483
484 let range = narrative.time_range().unwrap();
485 let duration = range.duration();
486 assert_eq!(duration.num_days(), 5);
487 }
488
489 #[test]
490 fn test_narrative_events_chronological() {
491 let mut narrative = Narrative::new("Test");
492 narrative.add_event(make_event(40.0, -74.0, "2024-03-20T10:00:00Z", "Third"));
493 narrative.add_event(make_event(40.0, -74.0, "2024-03-10T10:00:00Z", "First"));
494 narrative.add_event(make_event(40.0, -74.0, "2024-03-15T10:00:00Z", "Second"));
495
496 let sorted = narrative.events_chronological();
497 assert_eq!(sorted[0].text, "First");
498 assert_eq!(sorted[1].text, "Second");
499 assert_eq!(sorted[2].text, "Third");
500 }
501
502 #[test]
503 fn test_narrative_serialization() {
504 let narrative = Narrative::builder()
505 .title("Test")
506 .event(make_event(40.0, -74.0, "2024-03-15T10:00:00Z", "Event"))
507 .build();
508
509 let json = serde_json::to_string(&narrative).unwrap();
510 let parsed: Narrative = serde_json::from_str(&json).unwrap();
511
512 assert_eq!(narrative.title, parsed.title);
513 assert_eq!(narrative.events.len(), parsed.events.len());
514 }
515}