Skip to main content

hayson_engine/buffer/
resolve.rs

1use serde_json::{Map, Value};
2
3use super::entry::{BufferEntry, BufferOp};
4use super::merge::Conflict;
5
6#[derive(Debug, Clone, PartialEq)]
7pub enum Resolution {
8    Ours(String),
9    Theirs(String),
10    Custom(Value),
11}
12
13/// Picks one of `conflict`'s versions (`Ours`/`Theirs`, both by explicit source name — there is no
14/// "current vs incoming" side at this generic level, that distinction belongs to whatever UI drives
15/// this, see the TODO below) or builds a fresh `Put` from `Custom`. Panics if the named source is
16/// not one of `conflict`'s versions — the caller is expected to only offer resolutions built from
17/// `conflict.versions` in the first place.
18pub fn resolve(conflict: &Conflict, choice: Resolution) -> BufferEntry {
19    match choice {
20        Resolution::Ours(source) | Resolution::Theirs(source) => conflict
21            .versions
22            .iter()
23            .find(|(s, _)| *s == source)
24            .map(|(_, entry)| entry.clone())
25            .unwrap_or_else(|| panic!("no version from source \"{source}\" in this conflict")),
26        Resolution::Custom(data) => BufferEntry {
27            table: conflict.table.clone(),
28            key: conflict.key.clone(),
29            op: BufferOp::Put,
30            data: Some(as_object(data)),
31            buffered_at: chrono::Utc::now().timestamp_millis(),
32            source: "resolution".to_string(),
33        },
34    }
35}
36
37fn as_object(value: Value) -> Map<String, Value> {
38    match value {
39        Value::Object(map) => map,
40        other => {
41            let mut map = Map::new();
42            map.insert("value".into(), other);
43            map
44        }
45    }
46}
47
48#[cfg(test)]
49mod tests {
50    use serde_json::json;
51
52    use super::*;
53
54    fn conflict() -> Conflict {
55        let ours = BufferEntry {
56            table: "users".into(),
57            key: "u1".into(),
58            op: BufferOp::Put,
59            data: Some(json!({ "name": "Ania" }).as_object().unwrap().clone()),
60            buffered_at: 100,
61            source: "branch-a".into(),
62        };
63        let theirs = BufferEntry {
64            table: "users".into(),
65            key: "u1".into(),
66            op: BufferOp::Put,
67            data: Some(json!({ "name": "Zosia" }).as_object().unwrap().clone()),
68            buffered_at: 200,
69            source: "branch-b".into(),
70        };
71        Conflict {
72            table: "users".into(),
73            key: "u1".into(),
74            versions: vec![("branch-a".into(), ours), ("branch-b".into(), theirs)],
75        }
76    }
77
78    #[test]
79    fn ours_picks_the_named_version() {
80        let resolved = resolve(&conflict(), Resolution::Ours("branch-a".into()));
81        assert_eq!(resolved.source, "branch-a");
82        assert_eq!(resolved.data.unwrap().get("name"), Some(&json!("Ania")));
83    }
84
85    #[test]
86    fn theirs_picks_the_named_version() {
87        let resolved = resolve(&conflict(), Resolution::Theirs("branch-b".into()));
88        assert_eq!(resolved.source, "branch-b");
89        assert_eq!(resolved.data.unwrap().get("name"), Some(&json!("Zosia")));
90    }
91
92    #[test]
93    fn custom_builds_a_fresh_put_from_the_given_value() {
94        let resolved = resolve(&conflict(), Resolution::Custom(json!({ "name": "Kasia" })));
95        assert_eq!(resolved.op, BufferOp::Put);
96        assert_eq!(resolved.key, "u1");
97        assert_eq!(resolved.data.unwrap().get("name"), Some(&json!("Kasia")));
98    }
99}