hayson-engine 0.9.0

Storage engine for hayson document databases — the embeddable core behind hayson-server, hayson-cli and hayson-browser
Documentation
use serde_json::{Map, Value};

use super::entry::{BufferEntry, BufferOp};
use super::merge::Conflict;

#[derive(Debug, Clone, PartialEq)]
pub enum Resolution {
    Ours(String),
    Theirs(String),
    Custom(Value),
}

/// Picks one of `conflict`'s versions (`Ours`/`Theirs`, both by explicit source name — there is no
/// "current vs incoming" side at this generic level, that distinction belongs to whatever UI drives
/// this, see the TODO below) or builds a fresh `Put` from `Custom`. Panics if the named source is
/// not one of `conflict`'s versions — the caller is expected to only offer resolutions built from
/// `conflict.versions` in the first place.
pub fn resolve(conflict: &Conflict, choice: Resolution) -> BufferEntry {
    match choice {
        Resolution::Ours(source) | Resolution::Theirs(source) => conflict
            .versions
            .iter()
            .find(|(s, _)| *s == source)
            .map(|(_, entry)| entry.clone())
            .unwrap_or_else(|| panic!("no version from source \"{source}\" in this conflict")),
        Resolution::Custom(data) => BufferEntry {
            table: conflict.table.clone(),
            key: conflict.key.clone(),
            op: BufferOp::Put,
            data: Some(as_object(data)),
            buffered_at: chrono::Utc::now().timestamp_millis(),
            source: "resolution".to_string(),
        },
    }
}

fn as_object(value: Value) -> Map<String, Value> {
    match value {
        Value::Object(map) => map,
        other => {
            let mut map = Map::new();
            map.insert("value".into(), other);
            map
        }
    }
}

#[cfg(test)]
mod tests {
    use serde_json::json;

    use super::*;

    fn conflict() -> Conflict {
        let ours = BufferEntry {
            table: "users".into(),
            key: "u1".into(),
            op: BufferOp::Put,
            data: Some(json!({ "name": "Ania" }).as_object().unwrap().clone()),
            buffered_at: 100,
            source: "branch-a".into(),
        };
        let theirs = BufferEntry {
            table: "users".into(),
            key: "u1".into(),
            op: BufferOp::Put,
            data: Some(json!({ "name": "Zosia" }).as_object().unwrap().clone()),
            buffered_at: 200,
            source: "branch-b".into(),
        };
        Conflict {
            table: "users".into(),
            key: "u1".into(),
            versions: vec![("branch-a".into(), ours), ("branch-b".into(), theirs)],
        }
    }

    #[test]
    fn ours_picks_the_named_version() {
        let resolved = resolve(&conflict(), Resolution::Ours("branch-a".into()));
        assert_eq!(resolved.source, "branch-a");
        assert_eq!(resolved.data.unwrap().get("name"), Some(&json!("Ania")));
    }

    #[test]
    fn theirs_picks_the_named_version() {
        let resolved = resolve(&conflict(), Resolution::Theirs("branch-b".into()));
        assert_eq!(resolved.source, "branch-b");
        assert_eq!(resolved.data.unwrap().get("name"), Some(&json!("Zosia")));
    }

    #[test]
    fn custom_builds_a_fresh_put_from_the_given_value() {
        let resolved = resolve(&conflict(), Resolution::Custom(json!({ "name": "Kasia" })));
        assert_eq!(resolved.op, BufferOp::Put);
        assert_eq!(resolved.key, "u1");
        assert_eq!(resolved.data.unwrap().get("name"), Some(&json!("Kasia")));
    }
}