1use crate::{Body, ContentHash, CoreError, RecordKey, Result};
14use serde::{Deserialize, Serialize};
15use std::collections::BTreeMap;
16
17const MANIFEST_COLLECTION: &str = "graph-manifest";
19
20#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
26pub struct Manifest {
27 pub entries: BTreeMap<String, ContentHash>,
28}
29
30impl Manifest {
31 pub fn new() -> Self {
33 Self::default()
34 }
35
36 pub fn key(repo: impl Into<String>, view_id: impl Into<String>) -> RecordKey {
39 RecordKey::new(repo, MANIFEST_COLLECTION, view_id)
40 }
41
42 pub fn collection() -> &'static str {
47 MANIFEST_COLLECTION
48 }
49
50 pub fn insert(&mut self, path: impl Into<String>, hash: ContentHash) {
52 self.entries.insert(path.into(), hash);
53 }
54
55 pub fn get(&self, path: &str) -> Option<&ContentHash> {
57 self.entries.get(path)
58 }
59
60 pub fn to_body(&self) -> Body {
62 Body::Inline(
63 serde_json::to_vec(&self.entries).expect("BTreeMap<String, ContentHash> serializes"),
64 )
65 }
66
67 pub fn from_body(body: &Body) -> Result<Self> {
70 let entries =
71 serde_json::from_slice(body.bytes()).map_err(|e| CoreError::Serde(e.to_string()))?;
72 Ok(Self { entries })
73 }
74
75 pub fn reconcile(&self, desired: &BTreeMap<String, ContentHash>) -> Reconciliation {
85 let mut added = Vec::new();
86 let mut modified = Vec::new();
87 for (path, hash) in desired {
88 match self.entries.get(path) {
89 None => added.push(path.clone()),
90 Some(current) if current != hash => modified.push(path.clone()),
91 Some(_) => {} }
93 }
94 let deleted = self
95 .entries
96 .keys()
97 .filter(|path| !desired.contains_key(*path))
98 .cloned()
99 .collect();
100 Reconciliation {
103 added,
104 modified,
105 deleted,
106 manifest: Manifest {
107 entries: desired.clone(),
108 },
109 }
110 }
111}
112
113pub fn desired_set<P, C>(entries: impl IntoIterator<Item = (P, C)>) -> BTreeMap<String, ContentHash>
116where
117 P: Into<String>,
118 C: AsRef<[u8]>,
119{
120 entries
121 .into_iter()
122 .map(|(path, content)| (path.into(), ContentHash::of(content.as_ref())))
123 .collect()
124}
125
126#[derive(Clone, Debug, Default, PartialEq, Eq)]
129pub struct Reconciliation {
130 pub added: Vec<String>,
132 pub modified: Vec<String>,
134 pub deleted: Vec<String>,
136 pub manifest: Manifest,
138}
139
140#[cfg(test)]
141mod tests {
142 use super::*;
143 use crate::RecordKind;
144
145 fn hash(s: &str) -> ContentHash {
146 ContentHash::of(s.as_bytes())
147 }
148
149 #[test]
150 fn key_addresses_repo_and_view_as_namespace_and_id() {
151 let k = Manifest::key("acme/widgets", "main");
152 assert_eq!(k.namespace, "acme/widgets");
153 assert_eq!(k.collection, MANIFEST_COLLECTION);
154 assert_eq!(k.id, "main");
155 }
156
157 #[test]
158 fn key_is_stable_for_the_same_repo_and_view() {
159 assert_eq!(Manifest::key("r", "v"), Manifest::key("r", "v"));
160 assert_ne!(Manifest::key("r", "v"), Manifest::key("r", "w"));
161 assert_ne!(Manifest::key("r", "v"), Manifest::key("s", "v"));
162 }
163
164 #[test]
165 fn insert_and_get_resolve_path_to_slice_hash() {
166 let mut m = Manifest::new();
167 m.insert("src/lib.rs", hash("slice-a"));
168 assert_eq!(m.get("src/lib.rs"), Some(&hash("slice-a")));
169 assert_eq!(m.get("src/absent.rs"), None);
170 }
171
172 #[test]
173 fn body_round_trips() {
174 let mut m = Manifest::new();
175 m.insert("src/main.rs", hash("s1"));
176 m.insert("src/lib.rs", hash("s2"));
177
178 let restored = Manifest::from_body(&m.to_body()).unwrap();
179 assert_eq!(restored, m);
180 }
181
182 #[test]
183 fn body_bytes_are_deterministic_regardless_of_insert_order() {
184 let mut a = Manifest::new();
188 a.insert("b.rs", hash("x"));
189 a.insert("a.rs", hash("y"));
190
191 let mut b = Manifest::new();
192 b.insert("a.rs", hash("y"));
193 b.insert("b.rs", hash("x"));
194
195 assert_eq!(a.to_body().bytes(), b.to_body().bytes());
196 }
197
198 #[test]
199 fn from_body_rejects_non_manifest_bytes() {
200 let garbage = Body::Inline(b"not json at all".to_vec());
201 assert!(matches!(
202 Manifest::from_body(&garbage),
203 Err(CoreError::Serde(_))
204 ));
205 }
206
207 #[test]
208 fn manifest_kind_is_derived() {
209 assert_eq!(
210 RecordKind::GraphManifest.merge_class(),
211 crate::MergeClass::Derived
212 );
213 }
214
215 #[test]
216 fn desired_set_hashes_each_tree_entry() {
217 let desired = desired_set([("a.rs", "one"), ("b.rs", "two")]);
218 assert_eq!(desired.get("a.rs"), Some(&hash("one")));
219 assert_eq!(desired.get("b.rs"), Some(&hash("two")));
220 assert_eq!(desired.len(), 2);
221 }
222
223 #[test]
224 fn reconcile_classifies_added_modified_deleted() {
225 let mut current = Manifest::new();
226 current.insert("keep.rs", hash("same")); current.insert("edit.rs", hash("old")); current.insert("gone.rs", hash("bye")); let desired = desired_set([("keep.rs", "same"), ("edit.rs", "new"), ("add.rs", "fresh")]);
231
232 let r = current.reconcile(&desired);
233 assert_eq!(r.added, vec!["add.rs".to_string()]);
234 assert_eq!(r.modified, vec!["edit.rs".to_string()]);
235 assert_eq!(r.deleted, vec!["gone.rs".to_string()]);
236 }
237
238 #[test]
239 fn reconciled_manifest_equals_the_desired_tree() {
240 let mut current = Manifest::new();
241 current.insert("gone.rs", hash("bye"));
242 let desired = desired_set([("add.rs", "fresh")]);
243
244 let r = current.reconcile(&desired);
245 assert_eq!(r.manifest.entries, desired);
246 }
247
248 #[test]
249 fn reconcile_reports_nothing_when_tree_matches_manifest() {
250 let mut current = Manifest::new();
251 current.insert("a.rs", hash("x"));
252 current.insert("b.rs", hash("y"));
253 let desired = desired_set([("a.rs", "x"), ("b.rs", "y")]);
254
255 let r = current.reconcile(&desired);
256 assert!(r.added.is_empty());
257 assert!(r.modified.is_empty());
258 assert!(r.deleted.is_empty());
259 assert_eq!(r.manifest.entries, current.entries);
260 }
261
262 #[test]
263 fn reconcile_from_empty_marks_all_added() {
264 let desired = desired_set([("b.rs", "2"), ("a.rs", "1")]);
265 let r = Manifest::new().reconcile(&desired);
266 assert_eq!(r.added, vec!["a.rs".to_string(), "b.rs".to_string()]);
268 assert!(r.modified.is_empty());
269 assert!(r.deleted.is_empty());
270 }
271
272 #[test]
273 fn reconcile_to_empty_marks_all_deleted() {
274 let mut current = Manifest::new();
275 current.insert("b.rs", hash("2"));
276 current.insert("a.rs", hash("1"));
277
278 let r = current.reconcile(&BTreeMap::new());
279 assert_eq!(r.deleted, vec!["a.rs".to_string(), "b.rs".to_string()]);
280 assert!(r.added.is_empty());
281 assert!(r.modified.is_empty());
282 assert!(r.manifest.entries.is_empty());
283 }
284}