use std::collections::BTreeMap;
use std::ops::Range;
use serde::{Deserialize, Serialize};
use crate::attribute::AttributeValue;
use crate::memory::MemoryId;
use crate::partition::PartitionPath;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct SummaryContent {
pub prose: String,
#[serde(default)]
pub blocks: Vec<SummaryBlock>,
}
impl SummaryContent {
#[must_use]
pub fn prose_only(prose: impl Into<String>) -> Self {
SummaryContent {
prose: prose.into(),
blocks: Vec::new(),
}
}
#[must_use]
pub fn with_block(mut self, block: SummaryBlock) -> Self {
self.blocks.push(block);
self
}
pub fn data_point_refs(&self) -> impl Iterator<Item = &DataPointRef> {
self.blocks.iter().flat_map(|b| match b {
SummaryBlock::DataPointLinks(refs) => refs.as_slice(),
SummaryBlock::Citation { refs, .. } => refs.as_slice(),
_ => &[],
})
}
pub fn partition_refs(&self) -> impl Iterator<Item = &PartitionRef> {
self.blocks.iter().flat_map(|b| match b {
SummaryBlock::PartitionLinks(refs) => refs.as_slice(),
_ => &[],
})
}
}
impl From<String> for SummaryContent {
fn from(s: String) -> Self {
SummaryContent::prose_only(s)
}
}
impl From<&str> for SummaryContent {
fn from(s: &str) -> Self {
SummaryContent::prose_only(s.to_string())
}
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", content = "value", rename_all = "snake_case")]
pub enum SummaryBlock {
DataPointLinks(Vec<DataPointRef>),
PartitionLinks(Vec<PartitionRef>),
Citation {
sentence: String,
refs: Vec<DataPointRef>,
},
Attributes(BTreeMap<String, AttributeValue>),
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DataPointRef {
pub memory_id: MemoryId,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub byte_range: Option<Range<u32>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub line_range: Option<Range<u32>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub time_range_ms: Option<Range<u32>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub note: Option<String>,
}
impl DataPointRef {
#[must_use]
pub fn whole_memory(memory_id: MemoryId) -> Self {
DataPointRef {
memory_id,
byte_range: None,
line_range: None,
time_range_ms: None,
note: None,
}
}
#[must_use]
pub fn as_input_range(&self) -> SummaryInputRange {
SummaryInputRange {
byte_start: self.byte_range.as_ref().map(|r| r.start),
byte_end: self.byte_range.as_ref().map(|r| r.end),
line_start: self.line_range.as_ref().map(|r| r.start),
line_end: self.line_range.as_ref().map(|r| r.end),
time_start_ms: self.time_range_ms.as_ref().map(|r| r.start),
time_end_ms: self.time_range_ms.as_ref().map(|r| r.end),
note: self.note.clone(),
}
}
#[must_use]
pub fn from_input_range(memory_id: MemoryId, range: &SummaryInputRange) -> Self {
DataPointRef {
memory_id,
byte_range: range.byte_start.zip(range.byte_end).map(|(s, e)| s..e),
line_range: range.line_start.zip(range.line_end).map(|(s, e)| s..e),
time_range_ms: range
.time_start_ms
.zip(range.time_end_ms)
.map(|(s, e)| s..e),
note: range.note.clone(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PartitionRef {
pub path: PartitionPath,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub note: Option<String>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct SummaryInputRange {
pub byte_start: Option<u32>,
pub byte_end: Option<u32>,
pub line_start: Option<u32>,
pub line_end: Option<u32>,
pub time_start_ms: Option<u32>,
pub time_end_ms: Option<u32>,
pub note: Option<String>,
}
impl SummaryInputRange {
#[must_use]
pub fn is_empty(&self) -> bool {
self.byte_start.is_none()
&& self.byte_end.is_none()
&& self.line_start.is_none()
&& self.line_end.is_none()
&& self.time_start_ms.is_none()
&& self.time_end_ms.is_none()
&& self.note.is_none()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn content_round_trips_through_json() {
let mid = MemoryId::generate();
let c = SummaryContent::prose_only("hello").with_block(SummaryBlock::Citation {
sentence: "Alex spoke first.".into(),
refs: vec![DataPointRef {
memory_id: mid,
byte_range: None,
line_range: Some(42..44),
time_range_ms: Some(127_000..130_000),
note: Some("intro".into()),
}],
});
let s = serde_json::to_string(&c).unwrap();
let back: SummaryContent = serde_json::from_str(&s).unwrap();
assert_eq!(c, back);
}
#[test]
fn prose_only_constructor_has_no_blocks() {
let c = SummaryContent::prose_only("hi");
assert_eq!(c.prose, "hi");
assert!(c.blocks.is_empty());
}
#[test]
fn data_point_ref_skips_none_optionals_in_json() {
let r = DataPointRef::whole_memory(MemoryId::generate());
let s = serde_json::to_string(&r).unwrap();
assert!(!s.contains("byte_range"));
assert!(!s.contains("line_range"));
assert!(!s.contains("time_range_ms"));
assert!(!s.contains("note"));
}
#[test]
fn input_range_is_empty_iff_all_none() {
let r = SummaryInputRange::default();
assert!(r.is_empty());
let r = SummaryInputRange {
line_start: Some(1),
..Default::default()
};
assert!(!r.is_empty());
}
#[test]
fn data_point_ref_round_trips_through_input_range() {
let mid = MemoryId::generate();
let original = DataPointRef {
memory_id: mid,
byte_range: Some(0..10),
line_range: Some(2..5),
time_range_ms: Some(1000..2000),
note: Some("note".into()),
};
let r = original.as_input_range();
let back = DataPointRef::from_input_range(mid, &r);
assert_eq!(original, back);
}
#[test]
fn from_string_compiles() {
let _: SummaryContent = String::from("hello").into();
let _: SummaryContent = "hi".into();
}
#[test]
fn data_point_refs_iter_visits_links_and_citations() {
let mid = MemoryId::generate();
let dp = DataPointRef::whole_memory(mid);
let c = SummaryContent::prose_only("p")
.with_block(SummaryBlock::DataPointLinks(vec![dp.clone()]))
.with_block(SummaryBlock::Citation {
sentence: "x".into(),
refs: vec![dp.clone()],
})
.with_block(SummaryBlock::Attributes(BTreeMap::new()));
assert_eq!(c.data_point_refs().count(), 2);
}
}