use std::collections::BTreeSet;
use serde_json::{json, Value};
use crate::ingest::RawQuad;
pub const MANIFEST_VERSION: u64 = 1;
pub const MANIFEST_SUFFIX: &str = ".rete-manifest.json";
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SegmentRef {
pub url: String,
pub size: u64,
pub blake3_16: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LogEntry {
pub adds: Option<SegmentRef>,
pub dels: Option<SegmentRef>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Manifest {
pub name: String,
pub generation: u64,
pub log: Vec<LogEntry>,
}
#[derive(Debug)]
pub struct ManifestError( pub String);
impl std::fmt::Display for ManifestError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "manifest: {}", self.0)
}
}
impl std::error::Error for ManifestError {}
fn err(msg: impl Into<String>) -> ManifestError {
ManifestError(msg.into())
}
fn segment_from_json(v: &Value, at: &str) -> Result<SegmentRef, ManifestError> {
let url = v["url"]
.as_str()
.ok_or_else(|| err(format!("{at}: missing string `url`")))?;
let size = v["size"]
.as_u64()
.ok_or_else(|| err(format!("{at}: missing integer `size`")))?;
let blake3_16 = v["blake3_16"]
.as_str()
.ok_or_else(|| err(format!("{at}: missing string `blake3_16`")))?;
if blake3_16.len() != 32 || !blake3_16.bytes().all(|b| b.is_ascii_hexdigit()) {
return Err(err(format!(
"{at}: `blake3_16` must be 32 hex chars, got {blake3_16:?}"
)));
}
Ok(SegmentRef {
url: url.to_string(),
size,
blake3_16: blake3_16.to_ascii_lowercase(),
})
}
fn segment_to_json(s: &SegmentRef) -> Value {
json!({ "url": s.url, "size": s.size, "blake3_16": s.blake3_16 })
}
impl Manifest {
pub fn parse(text: &str) -> Result<Manifest, ManifestError> {
let v: Value = serde_json::from_str(text).map_err(|e| err(format!("invalid JSON: {e}")))?;
let version = v["rete_manifest"]
.as_u64()
.ok_or_else(|| err("missing integer `rete_manifest` version field"))?;
if version != MANIFEST_VERSION {
return Err(err(format!(
"version {version} not supported (this build reads {MANIFEST_VERSION})"
)));
}
let name = v["name"]
.as_str()
.ok_or_else(|| err("missing string `name`"))?
.to_string();
let generation = v["generation"]
.as_u64()
.ok_or_else(|| err("missing integer `generation`"))?;
let log_json = v["log"]
.as_array()
.ok_or_else(|| err("missing array `log`"))?;
let mut log = Vec::with_capacity(log_json.len());
for (i, entry) in log_json.iter().enumerate() {
let adds = match &entry["adds"] {
Value::Null => None,
other => Some(segment_from_json(other, &format!("log[{i}].adds"))?),
};
let dels = match &entry["dels"] {
Value::Null => None,
other => Some(segment_from_json(other, &format!("log[{i}].dels"))?),
};
if adds.is_none() && dels.is_none() {
return Err(err(format!("log[{i}]: needs `adds` and/or `dels`")));
}
log.push(LogEntry { adds, dels });
}
Ok(Manifest {
name,
generation,
log,
})
}
pub fn to_json_pretty(&self) -> String {
let log: Vec<Value> = self
.log
.iter()
.map(|e| {
let mut entry = serde_json::Map::new();
if let Some(a) = &e.adds {
entry.insert("adds".into(), segment_to_json(a));
}
if let Some(d) = &e.dels {
entry.insert("dels".into(), segment_to_json(d));
}
Value::Object(entry)
})
.collect();
let doc = json!({
"rete_manifest": MANIFEST_VERSION,
"name": self.name,
"generation": self.generation,
"log": log,
});
serde_json::to_string_pretty(&doc).expect("static shape") + "\n"
}
pub fn segments(&self) -> impl Iterator<Item = (&SegmentRef, /* is_dels: */ bool)> {
self.log.iter().flat_map(|e| {
e.adds
.iter()
.map(|s| (s, false))
.chain(e.dels.iter().map(|s| (s, true)))
})
}
}
pub fn fold_entry(
visible: &mut BTreeSet<RawQuad>,
dels: impl IntoIterator<Item = RawQuad>,
adds: impl IntoIterator<Item = RawQuad>,
) {
for q in dels {
visible.remove(&q);
}
for q in adds {
visible.insert(q);
}
}
#[cfg(test)]
mod tests {
use super::*;
fn q(s: &str) -> RawQuad {
(
format!("<http://ex/{s}>"),
"<http://ex/p>".into(),
"<http://ex/o>".into(),
None,
)
}
fn seg(url: &str) -> SegmentRef {
SegmentRef {
url: url.into(),
size: 123,
blake3_16: "00112233445566778899aabbccddeeff".into(),
}
}
#[test]
fn roundtrip() {
let m = Manifest {
name: "demo".into(),
generation: 7,
log: vec![
LogEntry {
adds: Some(seg("base.rete")),
dels: None,
},
LogEntry {
adds: Some(seg("delta-g2.rete")),
dels: Some(seg("delta-g2.tomb.rete")),
},
],
};
let parsed = Manifest::parse(&m.to_json_pretty()).unwrap();
assert_eq!(parsed, m);
}
#[test]
fn rejects_bad_documents() {
assert!(Manifest::parse("{}").is_err());
assert!(Manifest::parse(
r#"{"rete_manifest": 99, "name": "x", "generation": 0, "log": []}"#
)
.is_err());
let empty_entry = r#"{"rete_manifest": 1, "name": "x", "generation": 0, "log": [{}]}"#;
assert!(Manifest::parse(empty_entry).is_err());
let bad_hash = r#"{"rete_manifest": 1, "name": "x", "generation": 0,
"log": [{"adds": {"url": "a.rete", "size": 1, "blake3_16": "zz"}}]}"#;
assert!(Manifest::parse(bad_hash).is_err());
}
#[test]
fn fold_is_ordered_last_writer_wins() {
let mut visible = BTreeSet::new();
fold_entry(&mut visible, [], [q("a"), q("b")]);
fold_entry(&mut visible, [q("a")], []);
assert!(!visible.contains(&q("a")) && visible.contains(&q("b")));
fold_entry(&mut visible, [], [q("a")]);
assert!(visible.contains(&q("a")));
fold_entry(&mut visible, [q("ghost")], []);
assert_eq!(visible.len(), 2);
}
#[test]
fn update_is_delete_plus_insert() {
let old = (
"<http://ex/a>".to_string(),
"<http://ex/name>".to_string(),
"\"Old\"".to_string(),
None,
);
let new = (
"<http://ex/a>".to_string(),
"<http://ex/name>".to_string(),
"\"New\"".to_string(),
None,
);
let mut visible = BTreeSet::from([old.clone()]);
fold_entry(&mut visible, [old], [new.clone()]);
assert_eq!(visible, BTreeSet::from([new]));
}
}