1use std::collections::BTreeSet;
28
29use serde_json::{json, Value};
30
31use crate::ingest::RawQuad;
32
33pub const MANIFEST_VERSION: u64 = 1;
35
36pub const MANIFEST_SUFFIX: &str = ".rete-manifest.json";
38
39#[derive(Debug, Clone, PartialEq, Eq)]
44pub struct SegmentRef {
45 pub url: String,
47 pub size: u64,
49 pub blake3_16: String,
51}
52
53#[derive(Debug, Clone, PartialEq, Eq)]
57pub struct LogEntry {
58 pub adds: Option<SegmentRef>,
60 pub dels: Option<SegmentRef>,
62}
63
64#[derive(Debug, Clone, PartialEq, Eq)]
67pub struct Manifest {
68 pub name: String,
70 pub generation: u64,
72 pub log: Vec<LogEntry>,
74}
75
76#[derive(Debug)]
78pub struct ManifestError(pub String);
79
80impl std::fmt::Display for ManifestError {
81 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
82 write!(f, "manifest: {}", self.0)
83 }
84}
85
86impl std::error::Error for ManifestError {}
87
88fn err(msg: impl Into<String>) -> ManifestError {
89 ManifestError(msg.into())
90}
91
92fn segment_from_json(v: &Value, at: &str) -> Result<SegmentRef, ManifestError> {
93 let url = v["url"]
94 .as_str()
95 .ok_or_else(|| err(format!("{at}: missing string `url`")))?;
96 let size = v["size"]
97 .as_u64()
98 .ok_or_else(|| err(format!("{at}: missing integer `size`")))?;
99 let blake3_16 = v["blake3_16"]
100 .as_str()
101 .ok_or_else(|| err(format!("{at}: missing string `blake3_16`")))?;
102 if blake3_16.len() != 32 || !blake3_16.bytes().all(|b| b.is_ascii_hexdigit()) {
103 return Err(err(format!(
104 "{at}: `blake3_16` must be 32 hex chars, got {blake3_16:?}"
105 )));
106 }
107 Ok(SegmentRef {
108 url: url.to_string(),
109 size,
110 blake3_16: blake3_16.to_ascii_lowercase(),
111 })
112}
113
114fn segment_to_json(s: &SegmentRef) -> Value {
115 json!({ "url": s.url, "size": s.size, "blake3_16": s.blake3_16 })
116}
117
118impl Manifest {
119 pub fn parse(text: &str) -> Result<Manifest, ManifestError> {
121 let v: Value = serde_json::from_str(text).map_err(|e| err(format!("invalid JSON: {e}")))?;
122 let version = v["rete_manifest"]
123 .as_u64()
124 .ok_or_else(|| err("missing integer `rete_manifest` version field"))?;
125 if version != MANIFEST_VERSION {
126 return Err(err(format!(
127 "version {version} not supported (this build reads {MANIFEST_VERSION})"
128 )));
129 }
130 let name = v["name"]
131 .as_str()
132 .ok_or_else(|| err("missing string `name`"))?
133 .to_string();
134 let generation = v["generation"]
135 .as_u64()
136 .ok_or_else(|| err("missing integer `generation`"))?;
137 let log_json = v["log"]
138 .as_array()
139 .ok_or_else(|| err("missing array `log`"))?;
140 let mut log = Vec::with_capacity(log_json.len());
141 for (i, entry) in log_json.iter().enumerate() {
142 let adds = match &entry["adds"] {
143 Value::Null => None,
144 other => Some(segment_from_json(other, &format!("log[{i}].adds"))?),
145 };
146 let dels = match &entry["dels"] {
147 Value::Null => None,
148 other => Some(segment_from_json(other, &format!("log[{i}].dels"))?),
149 };
150 if adds.is_none() && dels.is_none() {
151 return Err(err(format!("log[{i}]: needs `adds` and/or `dels`")));
152 }
153 log.push(LogEntry { adds, dels });
154 }
155 Ok(Manifest {
156 name,
157 generation,
158 log,
159 })
160 }
161
162 pub fn to_json_pretty(&self) -> String {
164 let log: Vec<Value> = self
165 .log
166 .iter()
167 .map(|e| {
168 let mut entry = serde_json::Map::new();
169 if let Some(a) = &e.adds {
170 entry.insert("adds".into(), segment_to_json(a));
171 }
172 if let Some(d) = &e.dels {
173 entry.insert("dels".into(), segment_to_json(d));
174 }
175 Value::Object(entry)
176 })
177 .collect();
178 let doc = json!({
179 "rete_manifest": MANIFEST_VERSION,
180 "name": self.name,
181 "generation": self.generation,
182 "log": log,
183 });
184 serde_json::to_string_pretty(&doc).expect("static shape") + "\n"
185 }
186
187 pub fn segments(&self) -> impl Iterator<Item = (&SegmentRef, bool)> {
190 self.log.iter().flat_map(|e| {
191 e.adds
192 .iter()
193 .map(|s| (s, false))
194 .chain(e.dels.iter().map(|s| (s, true)))
195 })
196 }
197}
198
199pub fn fold_entry(
203 visible: &mut BTreeSet<RawQuad>,
204 dels: impl IntoIterator<Item = RawQuad>,
205 adds: impl IntoIterator<Item = RawQuad>,
206) {
207 for q in dels {
208 visible.remove(&q);
209 }
210 for q in adds {
211 visible.insert(q);
212 }
213}
214
215#[cfg(test)]
216mod tests {
217 use super::*;
218
219 fn q(s: &str) -> RawQuad {
220 (
221 format!("<http://ex/{s}>"),
222 "<http://ex/p>".into(),
223 "<http://ex/o>".into(),
224 None,
225 )
226 }
227
228 fn seg(url: &str) -> SegmentRef {
229 SegmentRef {
230 url: url.into(),
231 size: 123,
232 blake3_16: "00112233445566778899aabbccddeeff".into(),
233 }
234 }
235
236 #[test]
237 fn roundtrip() {
238 let m = Manifest {
239 name: "demo".into(),
240 generation: 7,
241 log: vec![
242 LogEntry {
243 adds: Some(seg("base.rete")),
244 dels: None,
245 },
246 LogEntry {
247 adds: Some(seg("delta-g2.rete")),
248 dels: Some(seg("delta-g2.tomb.rete")),
249 },
250 ],
251 };
252 let parsed = Manifest::parse(&m.to_json_pretty()).unwrap();
253 assert_eq!(parsed, m);
254 }
255
256 #[test]
257 fn rejects_bad_documents() {
258 assert!(Manifest::parse("{}").is_err());
259 assert!(Manifest::parse(
260 r#"{"rete_manifest": 99, "name": "x", "generation": 0, "log": []}"#
261 )
262 .is_err());
263 let empty_entry = r#"{"rete_manifest": 1, "name": "x", "generation": 0, "log": [{}]}"#;
265 assert!(Manifest::parse(empty_entry).is_err());
266 let bad_hash = r#"{"rete_manifest": 1, "name": "x", "generation": 0,
268 "log": [{"adds": {"url": "a.rete", "size": 1, "blake3_16": "zz"}}]}"#;
269 assert!(Manifest::parse(bad_hash).is_err());
270 }
271
272 #[test]
273 fn fold_is_ordered_last_writer_wins() {
274 let mut visible = BTreeSet::new();
275 fold_entry(&mut visible, [], [q("a"), q("b")]);
277 fold_entry(&mut visible, [q("a")], []);
279 assert!(!visible.contains(&q("a")) && visible.contains(&q("b")));
280 fold_entry(&mut visible, [], [q("a")]);
282 assert!(visible.contains(&q("a")));
283 fold_entry(&mut visible, [q("ghost")], []);
285 assert_eq!(visible.len(), 2);
286 }
287
288 #[test]
289 fn update_is_delete_plus_insert() {
290 let old = (
291 "<http://ex/a>".to_string(),
292 "<http://ex/name>".to_string(),
293 "\"Old\"".to_string(),
294 None,
295 );
296 let new = (
297 "<http://ex/a>".to_string(),
298 "<http://ex/name>".to_string(),
299 "\"New\"".to_string(),
300 None,
301 );
302 let mut visible = BTreeSet::from([old.clone()]);
303 fold_entry(&mut visible, [old], [new.clone()]);
304 assert_eq!(visible, BTreeSet::from([new]));
305 }
306}