use super::format::Format;
use crate::core::{
Event, Location, Narrative, NarrativeMetadata, SourceRef, SourceType, Timestamp,
};
use crate::{Error, Result};
use serde::{Deserialize, Serialize};
use std::io::{Read, Write};
#[derive(Debug, Clone, Default)]
pub struct JsonFormat {
pub pretty: bool,
}
impl JsonFormat {
pub fn new() -> Self {
Self::default()
}
pub fn pretty() -> Self {
Self { pretty: true }
}
}
#[derive(Debug, Serialize, Deserialize)]
struct NarrativeJson {
version: String,
metadata: NarrativeMetadataJson,
events: Vec<EventJson>,
}
#[derive(Debug, Serialize, Deserialize)]
struct NarrativeMetadataJson {
created: Option<String>,
modified: Option<String>,
author: Option<String>,
description: Option<String>,
category: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
struct EventJson {
id: String,
location: LocationJson,
timestamp: String,
text: String,
tags: Vec<String>,
#[serde(default)]
sources: Vec<SourceRefJson>,
metadata: serde_json::Value,
}
#[derive(Debug, Serialize, Deserialize)]
struct LocationJson {
lat: f64,
lon: f64,
#[serde(skip_serializing_if = "Option::is_none")]
elevation: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
uncertainty_meters: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
name: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
struct SourceRefJson {
source_type: String,
#[serde(skip_serializing_if = "Option::is_none")]
title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
author: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
date: Option<String>,
}
impl Format for JsonFormat {
fn import<R: Read>(&self, reader: R) -> Result<Narrative> {
let json: NarrativeJson = serde_json::from_reader(reader)?;
if !json.version.starts_with("1.") {
return Err(Error::InvalidFormat(format!(
"unsupported format version: {}",
json.version
)));
}
let metadata = NarrativeMetadata {
created: json
.metadata
.created
.as_ref()
.map(|s| Timestamp::parse(s))
.transpose()?,
modified: json
.metadata
.modified
.as_ref()
.map(|s| Timestamp::parse(s))
.transpose()?,
author: json.metadata.author,
description: json.metadata.description,
category: json.metadata.category,
extra: std::collections::HashMap::new(),
};
let mut events = Vec::new();
for event_json in json.events {
let location = Location {
lat: event_json.location.lat,
lon: event_json.location.lon,
elevation: event_json.location.elevation,
uncertainty_meters: event_json.location.uncertainty_meters,
name: event_json.location.name,
};
location.validate()?;
let timestamp = Timestamp::parse(&event_json.timestamp)?;
let sources: Vec<SourceRef> = event_json
.sources
.into_iter()
.map(|s| {
let source_type = match s.source_type.as_str() {
"article" => SourceType::Article,
"report" => SourceType::Report,
"witness" => SourceType::Witness,
"sensor" => SourceType::Sensor,
_ => SourceType::Other,
};
SourceRef {
source_type,
url: s.url,
title: s.title,
author: s.author,
date: s.date.and_then(|d| Timestamp::parse(&d).ok()),
notes: None,
}
})
.collect();
let event = Event {
id: crate::core::EventId::parse(&event_json.id)?,
location,
timestamp,
text: event_json.text,
tags: event_json.tags,
sources,
metadata: serde_json::from_value(event_json.metadata).unwrap_or_default(),
};
events.push(event);
}
Ok(Narrative {
id: crate::core::NarrativeId::new(),
title: "Imported Narrative".to_string(),
events,
metadata,
tags: Vec::new(),
})
}
fn export<W: Write>(&self, narrative: &Narrative, writer: W) -> Result<()> {
let metadata = NarrativeMetadataJson {
created: narrative.metadata.created.as_ref().map(|t| t.to_rfc3339()),
modified: narrative.metadata.modified.as_ref().map(|t| t.to_rfc3339()),
author: narrative.metadata.author.clone(),
description: narrative.metadata.description.clone(),
category: narrative.metadata.category.clone(),
};
let events: Vec<EventJson> = narrative
.events
.iter()
.map(|event| {
let location = LocationJson {
lat: event.location.lat,
lon: event.location.lon,
elevation: event.location.elevation,
uncertainty_meters: event.location.uncertainty_meters,
name: event.location.name.clone(),
};
EventJson {
id: event.id.to_string(),
location,
timestamp: event.timestamp.to_rfc3339(),
text: event.text.clone(),
tags: event.tags.clone(),
sources: event
.sources
.iter()
.map(|s| SourceRefJson {
source_type: s.source_type.to_string(),
title: s.title.clone(),
author: s.author.clone(),
url: s.url.clone(),
date: s.date.as_ref().map(|ts| ts.to_rfc3339()),
})
.collect(),
metadata: serde_json::to_value(&event.metadata)
.unwrap_or(serde_json::Value::Object(serde_json::Map::new())),
}
})
.collect();
let json = NarrativeJson {
version: "1.0".to_string(),
metadata,
events,
};
if self.pretty {
serde_json::to_writer_pretty(writer, &json)?;
} else {
serde_json::to_writer(writer, &json)?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_json_roundtrip() {
let event = Event::builder()
.location(Location::new(40.7128, -74.006))
.timestamp(Timestamp::parse("2024-01-15T14:30:00Z").unwrap())
.text("Test event")
.tag("tag1")
.build();
let narrative = Narrative::builder()
.title("Test Narrative")
.description("A test narrative")
.event(event)
.build();
let format = JsonFormat::pretty();
let json = format.export_str(&narrative).unwrap();
let restored = format.import_str(&json).unwrap();
assert_eq!(restored.events().len(), 1);
assert_eq!(restored.events()[0].text, "Test event");
assert_eq!(restored.events()[0].tags, vec!["tag1"]);
}
#[test]
fn test_json_version_check() {
let json = r#"{
"version": "2.0",
"metadata": {
"id": "00000000-0000-0000-0000-000000000000",
"title": null,
"description": null,
"created_at": "2024-01-15T14:30:00Z",
"updated_at": "2024-01-15T14:30:00Z",
"tags": []
},
"events": []
}"#;
let format = JsonFormat::new();
let result = format.import_str(json);
assert!(result.is_err());
}
#[test]
fn test_json_with_source() {
let mut source = SourceRef::new(SourceType::Article);
source.title = Some("Test Source".to_string());
source.url = Some("https://example.com".to_string());
let event = Event::builder()
.location(Location::new(40.7128, -74.006))
.timestamp(Timestamp::parse("2024-01-15T14:30:00Z").unwrap())
.source(source)
.build();
let narrative = Narrative::builder().event(event).build();
let format = JsonFormat::new();
let json = format.export_str(&narrative).unwrap();
let restored = format.import_str(&json).unwrap();
let restored_event = &restored.events()[0];
assert!(!restored_event.sources.is_empty());
assert_eq!(
restored_event.sources[0].title,
Some("Test Source".to_string())
);
assert_eq!(
restored_event.sources[0].url,
Some("https://example.com".to_string())
);
}
}