kimun_notes/server_client/
reconcile.rs1use std::collections::HashMap;
6
7#[derive(Debug, Default, PartialEq, Eq)]
9pub struct ReconcilePlan {
10 pub to_push: Vec<String>,
13 pub to_delete: Vec<String>,
15}
16
17impl ReconcilePlan {
18 pub fn is_empty(&self) -> bool {
19 self.to_push.is_empty() && self.to_delete.is_empty()
20 }
21}
22
23pub fn diff(local: &HashMap<String, String>, server: &HashMap<String, String>) -> ReconcilePlan {
25 let mut plan = ReconcilePlan::default();
26
27 for (path, hash) in local {
28 match server.get(path) {
29 Some(server_hash) if server_hash == hash => {} _ => plan.to_push.push(path.clone()), }
32 }
33 for path in server.keys() {
34 if !local.contains_key(path) {
35 plan.to_delete.push(path.clone());
36 }
37 }
38
39 plan.to_push.sort();
40 plan.to_delete.sort();
41 plan
42}
43
44#[cfg(test)]
45mod tests {
46 use super::*;
47
48 fn map(pairs: &[(&str, &str)]) -> HashMap<String, String> {
49 pairs
50 .iter()
51 .map(|(p, h)| (p.to_string(), h.to_string()))
52 .collect()
53 }
54
55 #[test]
56 fn identical_sets_need_nothing() {
57 let plan = diff(
58 &map(&[("a", "1"), ("b", "2")]),
59 &map(&[("a", "1"), ("b", "2")]),
60 );
61 assert!(plan.is_empty());
62 }
63
64 #[test]
65 fn new_and_changed_notes_are_pushed() {
66 let local = map(&[("a", "2"), ("b", "2"), ("c", "9")]);
68 let server = map(&[("a", "1"), ("b", "2")]);
69 let plan = diff(&local, &server);
70 assert_eq!(plan.to_push, vec!["a".to_string(), "c".to_string()]);
71 assert!(plan.to_delete.is_empty());
72 }
73
74 #[test]
75 fn notes_gone_from_the_vault_are_deleted() {
76 let local = map(&[("a", "1")]);
77 let server = map(&[("a", "1"), ("stale", "7")]);
78 let plan = diff(&local, &server);
79 assert!(plan.to_push.is_empty());
80 assert_eq!(plan.to_delete, vec!["stale".to_string()]);
81 }
82
83 #[test]
84 fn empty_server_pushes_everything() {
85 let plan = diff(&map(&[("a", "1"), ("b", "2")]), &HashMap::new());
86 assert_eq!(plan.to_push, vec!["a".to_string(), "b".to_string()]);
87 }
88}