use std::collections::BTreeMap;
use std::fmt::Display;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::provenance::Exactness;
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct Annotations {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub streams: Vec<String>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub provenance: BTreeMap<String, Provenance>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub exactness: BTreeMap<String, ExactnessNote>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct Provenance {
pub stream: u32,
pub offset: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tag: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct ExactnessNote {
pub entity: Exactness,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub fields: BTreeMap<String, Exactness>,
}
impl Default for ExactnessNote {
fn default() -> Self {
Self {
entity: Exactness::ByteExact,
fields: BTreeMap::new(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct StreamHandle(u32);
#[derive(Debug, Default)]
pub struct AnnotationBuilder {
annotations: Annotations,
}
impl AnnotationBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn stream(&mut self, stream: impl Into<String>) -> StreamHandle {
let stream = stream.into();
if let Some(index) = self
.annotations
.streams
.iter()
.position(|existing| existing == &stream)
{
return StreamHandle(index as u32);
}
let index = u32::try_from(self.annotations.streams.len())
.expect("annotation stream count exceeds u32::MAX");
self.annotations.streams.push(stream);
StreamHandle(index)
}
pub fn note(
&mut self,
id: impl Display,
stream: StreamHandle,
offset: u64,
) -> ProvenanceNote<'_> {
let id = id.to_string();
self.annotations.provenance.insert(
id.clone(),
Provenance {
stream: stream.0,
offset,
tag: None,
},
);
ProvenanceNote {
provenance: self
.annotations
.provenance
.get_mut(&id)
.expect("provenance was just inserted"),
}
}
pub fn exactness(&mut self, id: impl Display, exactness: Exactness) -> &mut Self {
let id = id.to_string();
let note = self.annotations.exactness.entry(id.clone()).or_default();
note.entity = exactness;
note.fields.retain(|_, value| *value != exactness);
if exactness == Exactness::ByteExact && note.fields.is_empty() {
self.annotations.exactness.remove(&id);
}
self
}
pub fn derived(&mut self, id: impl Display, field: impl Into<String>) -> &mut Self {
self.field_exactness(id, field, Exactness::Derived)
}
pub fn field_exactness(
&mut self,
id: impl Display,
field: impl Into<String>,
exactness: Exactness,
) -> &mut Self {
let id = id.to_string();
let field = field.into();
if exactness == Exactness::ByteExact {
if let Some(note) = self.annotations.exactness.get_mut(&id) {
if note.entity == Exactness::ByteExact {
note.fields.remove(&field);
if note.fields.is_empty() {
self.annotations.exactness.remove(&id);
}
} else {
note.fields.insert(field, Exactness::ByteExact);
}
}
} else {
self.annotations
.exactness
.entry(id)
.or_default()
.fields
.insert(field, exactness);
}
self
}
pub fn annotations(&self) -> &Annotations {
&self.annotations
}
pub fn build(self) -> Annotations {
self.annotations
}
}
pub struct ProvenanceNote<'a> {
provenance: &'a mut Provenance,
}
impl ProvenanceNote<'_> {
pub fn tag(self, tag: impl Into<String>) {
self.provenance.tag = Some(tag.into());
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn builder_interns_streams_and_records_provenance() {
let mut builder = AnnotationBuilder::new();
let first = builder.stream("f3d:Breps.BlobParts/body.smbh");
let second = builder.stream("f3d:Breps.BlobParts/body.smbh");
assert_eq!(first, second);
builder.note("f3d:body#0", first, 42).tag("body");
let annotations = builder.build();
assert_eq!(annotations.streams.len(), 1);
assert_eq!(
annotations.provenance["f3d:body#0"],
Provenance {
stream: 0,
offset: 42,
tag: Some("body".to_string()),
}
);
}
#[test]
fn exactness_table_stays_sparse() {
let mut builder = AnnotationBuilder::new();
builder
.derived("f3d:edge#0", "param_range")
.exactness("f3d:edge#0", Exactness::Inferred);
builder.field_exactness("f3d:edge#0", "param_range", Exactness::ByteExact);
let expected_fields = BTreeMap::from([("param_range".to_string(), Exactness::ByteExact)]);
assert_eq!(
builder.annotations().exactness["f3d:edge#0"],
ExactnessNote {
entity: Exactness::Inferred,
fields: expected_fields,
}
);
builder.exactness("f3d:edge#0", Exactness::ByteExact);
assert!(builder.annotations().exactness.is_empty());
}
}