1use 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#[derive(Debug, Clone, Default)]
44pub struct JsonFormat {
45 pub pretty: bool,
47}
48
49impl JsonFormat {
50 pub fn new() -> Self {
52 Self::default()
53 }
54
55 pub fn pretty() -> Self {
57 Self { pretty: true }
58 }
59}
60
61#[derive(Debug, Serialize, Deserialize)]
63struct NarrativeJson {
64 version: String,
66
67 metadata: NarrativeMetadataJson,
69
70 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 if !json.version.starts_with("1.") {
126 return Err(Error::InvalidFormat(format!(
127 "unsupported format version: {}",
128 json.version
129 )));
130 }
131
132 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 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}