Skip to main content

spatial_narrative/io/
json_format.rs

1//! Custom JSON format for narratives.
2//!
3//! This format is optimized for storing and exchanging complete narratives
4//! with all metadata preserved.
5
6use super::format::Format;
7use crate::core::{
8    Event, Location, Narrative, NarrativeMetadata, SourceRef, SourceType, Timestamp,
9};
10use crate::{Error, Result};
11use serde::{Deserialize, Serialize};
12use std::io::{Read, Write};
13
14/// Custom JSON format handler.
15///
16/// This format preserves all narrative structure and metadata, making it
17/// ideal for storing narratives in databases or exchanging between systems.
18///
19/// # Example
20///
21/// ```rust
22/// use spatial_narrative::io::{JsonFormat, Format};
23/// use spatial_narrative::prelude::*;
24///
25/// let format = JsonFormat::new();
26///
27/// let event = Event::builder()
28///     .location(Location::new(40.7128, -74.006))
29///     .timestamp(Timestamp::now())
30///     .text("Something happened")
31///     .build();
32///
33/// let narrative = Narrative::builder()
34///     .title("My Story")
35///     .event(event)
36///     .build();
37///
38/// let json = format.export_str(&narrative).unwrap();
39/// let restored = format.import_str(&json).unwrap();
40///
41/// assert_eq!(restored.events().len(), 1);
42/// ```
43#[derive(Debug, Clone, Default)]
44pub struct JsonFormat {
45    /// Whether to pretty-print the JSON output
46    pub pretty: bool,
47}
48
49impl JsonFormat {
50    /// Create a new JSON format handler.
51    pub fn new() -> Self {
52        Self::default()
53    }
54
55    /// Create a new JSON format handler with pretty printing enabled.
56    pub fn pretty() -> Self {
57        Self { pretty: true }
58    }
59}
60
61/// JSON representation of a narrative with version info.
62#[derive(Debug, Serialize, Deserialize)]
63struct NarrativeJson {
64    /// Format version for future compatibility
65    version: String,
66
67    /// Narrative metadata
68    metadata: NarrativeMetadataJson,
69
70    /// Events in the narrative
71    events: Vec<EventJson>,
72}
73
74#[derive(Debug, Serialize, Deserialize)]
75struct NarrativeMetadataJson {
76    created: Option<String>,
77    modified: Option<String>,
78    author: Option<String>,
79    description: Option<String>,
80    category: Option<String>,
81}
82
83#[derive(Debug, Serialize, Deserialize)]
84struct EventJson {
85    id: String,
86    location: LocationJson,
87    timestamp: String,
88    text: String,
89    tags: Vec<String>,
90    #[serde(default)]
91    sources: Vec<SourceRefJson>,
92    metadata: serde_json::Value,
93}
94
95#[derive(Debug, Serialize, Deserialize)]
96struct LocationJson {
97    lat: f64,
98    lon: f64,
99    #[serde(skip_serializing_if = "Option::is_none")]
100    elevation: Option<f64>,
101    #[serde(skip_serializing_if = "Option::is_none")]
102    uncertainty_meters: Option<f64>,
103    #[serde(skip_serializing_if = "Option::is_none")]
104    name: Option<String>,
105}
106
107#[derive(Debug, Serialize, Deserialize)]
108struct SourceRefJson {
109    source_type: String,
110    #[serde(skip_serializing_if = "Option::is_none")]
111    title: Option<String>,
112    #[serde(skip_serializing_if = "Option::is_none")]
113    author: Option<String>,
114    #[serde(skip_serializing_if = "Option::is_none")]
115    url: Option<String>,
116    #[serde(skip_serializing_if = "Option::is_none")]
117    date: Option<String>,
118}
119
120impl Format for JsonFormat {
121    fn import<R: Read>(&self, reader: R) -> Result<Narrative> {
122        let json: NarrativeJson = serde_json::from_reader(reader)?;
123
124        // Check version compatibility (for now, we only support 1.0)
125        if !json.version.starts_with("1.") {
126            return Err(Error::InvalidFormat(format!(
127                "unsupported format version: {}",
128                json.version
129            )));
130        }
131
132        // Convert from JSON representation to internal types
133        let metadata = NarrativeMetadata {
134            created: json
135                .metadata
136                .created
137                .as_ref()
138                .map(|s| Timestamp::parse(s))
139                .transpose()?,
140            modified: json
141                .metadata
142                .modified
143                .as_ref()
144                .map(|s| Timestamp::parse(s))
145                .transpose()?,
146            author: json.metadata.author,
147            description: json.metadata.description,
148            category: json.metadata.category,
149            extra: std::collections::HashMap::new(),
150        };
151
152        let mut events = Vec::new();
153
154        for event_json in json.events {
155            let location = Location {
156                lat: event_json.location.lat,
157                lon: event_json.location.lon,
158                elevation: event_json.location.elevation,
159                uncertainty_meters: event_json.location.uncertainty_meters,
160                name: event_json.location.name,
161            };
162
163            // Validate location
164            location.validate()?;
165
166            let timestamp = Timestamp::parse(&event_json.timestamp)?;
167
168            let sources: Vec<SourceRef> = event_json
169                .sources
170                .into_iter()
171                .map(|s| {
172                    let source_type = match s.source_type.as_str() {
173                        "article" => SourceType::Article,
174                        "report" => SourceType::Report,
175                        "witness" => SourceType::Witness,
176                        "sensor" => SourceType::Sensor,
177                        _ => SourceType::Other,
178                    };
179
180                    SourceRef {
181                        source_type,
182                        url: s.url,
183                        title: s.title,
184                        author: s.author,
185                        date: s.date.and_then(|d| Timestamp::parse(&d).ok()),
186                        notes: None,
187                    }
188                })
189                .collect();
190
191            let event = Event {
192                id: crate::core::EventId::parse(&event_json.id)?,
193                location,
194                timestamp,
195                text: event_json.text,
196                tags: event_json.tags,
197                sources,
198                metadata: serde_json::from_value(event_json.metadata).unwrap_or_default(),
199            };
200            events.push(event);
201        }
202
203        Ok(Narrative {
204            id: crate::core::NarrativeId::new(),
205            title: "Imported Narrative".to_string(),
206            events,
207            metadata,
208            tags: Vec::new(),
209        })
210    }
211
212    fn export<W: Write>(&self, narrative: &Narrative, writer: W) -> Result<()> {
213        let metadata = NarrativeMetadataJson {
214            created: narrative.metadata.created.as_ref().map(|t| t.to_rfc3339()),
215            modified: narrative.metadata.modified.as_ref().map(|t| t.to_rfc3339()),
216            author: narrative.metadata.author.clone(),
217            description: narrative.metadata.description.clone(),
218            category: narrative.metadata.category.clone(),
219        };
220
221        let events: Vec<EventJson> = narrative
222            .events
223            .iter()
224            .map(|event| {
225                let location = LocationJson {
226                    lat: event.location.lat,
227                    lon: event.location.lon,
228                    elevation: event.location.elevation,
229                    uncertainty_meters: event.location.uncertainty_meters,
230                    name: event.location.name.clone(),
231                };
232
233                EventJson {
234                    id: event.id.to_string(),
235                    location,
236                    timestamp: event.timestamp.to_rfc3339(),
237                    text: event.text.clone(),
238                    tags: event.tags.clone(),
239                    sources: event
240                        .sources
241                        .iter()
242                        .map(|s| SourceRefJson {
243                            source_type: s.source_type.to_string(),
244                            title: s.title.clone(),
245                            author: s.author.clone(),
246                            url: s.url.clone(),
247                            date: s.date.as_ref().map(|ts| ts.to_rfc3339()),
248                        })
249                        .collect(),
250                    metadata: serde_json::to_value(&event.metadata)
251                        .unwrap_or(serde_json::Value::Object(serde_json::Map::new())),
252                }
253            })
254            .collect();
255
256        let json = NarrativeJson {
257            version: "1.0".to_string(),
258            metadata,
259            events,
260        };
261
262        if self.pretty {
263            serde_json::to_writer_pretty(writer, &json)?;
264        } else {
265            serde_json::to_writer(writer, &json)?;
266        }
267
268        Ok(())
269    }
270}
271
272#[cfg(test)]
273mod tests {
274    use super::*;
275
276    #[test]
277    fn test_json_roundtrip() {
278        let event = Event::builder()
279            .location(Location::new(40.7128, -74.006))
280            .timestamp(Timestamp::parse("2024-01-15T14:30:00Z").unwrap())
281            .text("Test event")
282            .tag("tag1")
283            .build();
284
285        let narrative = Narrative::builder()
286            .title("Test Narrative")
287            .description("A test narrative")
288            .event(event)
289            .build();
290
291        let format = JsonFormat::pretty();
292        let json = format.export_str(&narrative).unwrap();
293        let restored = format.import_str(&json).unwrap();
294
295        assert_eq!(restored.events().len(), 1);
296        assert_eq!(restored.events()[0].text, "Test event");
297        assert_eq!(restored.events()[0].tags, vec!["tag1"]);
298    }
299
300    #[test]
301    fn test_json_version_check() {
302        let json = r#"{
303            "version": "2.0",
304            "metadata": {
305                "id": "00000000-0000-0000-0000-000000000000",
306                "title": null,
307                "description": null,
308                "created_at": "2024-01-15T14:30:00Z",
309                "updated_at": "2024-01-15T14:30:00Z",
310                "tags": []
311            },
312            "events": []
313        }"#;
314
315        let format = JsonFormat::new();
316        let result = format.import_str(json);
317
318        assert!(result.is_err());
319    }
320
321    #[test]
322    fn test_json_with_source() {
323        let mut source = SourceRef::new(SourceType::Article);
324        source.title = Some("Test Source".to_string());
325        source.url = Some("https://example.com".to_string());
326
327        let event = Event::builder()
328            .location(Location::new(40.7128, -74.006))
329            .timestamp(Timestamp::parse("2024-01-15T14:30:00Z").unwrap())
330            .source(source)
331            .build();
332
333        let narrative = Narrative::builder().event(event).build();
334
335        let format = JsonFormat::new();
336        let json = format.export_str(&narrative).unwrap();
337        let restored = format.import_str(&json).unwrap();
338
339        let restored_event = &restored.events()[0];
340        assert!(!restored_event.sources.is_empty());
341        assert_eq!(
342            restored_event.sources[0].title,
343            Some("Test Source".to_string())
344        );
345        assert_eq!(
346            restored_event.sources[0].url,
347            Some("https://example.com".to_string())
348        );
349    }
350}