autosurgeon 0.11.0

A library for working with data in automerge documents
Documentation
use std::collections::{BTreeMap, HashMap, HashSet};

use crate::Reconcile;

use super::{LoadKey, MapReconciler};

impl<K, V> Reconcile for HashMap<K, V>
where
    K: AsRef<str>,
    V: Reconcile,
{
    type Key<'a> = super::NoKey;

    fn reconcile<R: crate::Reconciler>(&self, reconciler: R) -> Result<(), R::Error> {
        reconcile_map_impl(self.iter(), reconciler)
    }
}

impl<K, V> Reconcile for BTreeMap<K, V>
where
    K: AsRef<str>,
    V: Reconcile,
{
    type Key<'a> = super::NoKey;

    fn reconcile<R: crate::Reconciler>(&self, reconciler: R) -> Result<(), R::Error> {
        reconcile_map_impl(self.iter(), reconciler)
    }
}

pub(crate) fn reconcile_map_impl<
    'a,
    K: AsRef<str> + 'a,
    V: Reconcile + 'a,
    I: Iterator<Item = (K, &'a V)>,
    R: crate::Reconciler,
>(
    items: I,
    mut reconciler: R,
) -> Result<(), R::Error> {
    let mut m = reconciler.map()?;
    let old_keys = m
        .entries()
        .map(|(k, _)| k.to_string())
        .collect::<HashSet<_>>();
    let mut incoming_keys = HashSet::new();
    for (k, val) in items {
        incoming_keys.insert(k.as_ref().to_string());
        if let LoadKey::Found(new_key) = val.key() {
            if let LoadKey::Found(existing_key) = m.hydrate_entry_key::<V, _>(&k)? {
                if existing_key != new_key {
                    m.replace(k, val)?;
                    continue;
                }
            }
        }
        m.put(k.as_ref(), val)?;
    }
    let delenda = old_keys.difference(&incoming_keys);
    for k in delenda {
        m.delete(k)?;
    }
    Ok(())
}

#[cfg(test)]
mod tests {
    use std::collections::HashMap;

    use automerge::ActorId;
    use automerge_test::{assert_doc, list, map};

    use crate::{
        reconcile,
        reconcile::{hydrate_key, LoadKey, MapReconciler},
        ReadDoc, Reconcile,
    };

    #[test]
    fn reconcile_map() {
        let mut map = HashMap::new();
        map.insert("key1", vec!["one", "two"]);
        map.insert("key2", vec!["three"]);
        let mut doc = automerge::AutoCommit::new();
        reconcile(&mut doc, &map).unwrap();
        assert_doc!(
            doc.document(),
            map! {
                "key1" => { list! { {"one"}, {"two"} }},
                "key2" => { list! { {"three"} }},
            }
        );

        // Added the following:
        map.remove("key1");
        reconcile(&mut doc, &map).unwrap();
        assert_doc!(
            doc.document(),
            map! {
                "key2" => { list! { {"three"} }},
            }
        );
    }

    #[derive(Clone)]
    struct User {
        id: u64,
        name: &'static str,
    }

    impl Reconcile for User {
        type Key<'a> = u64;

        fn reconcile<R: crate::Reconciler>(&self, mut reconciler: R) -> Result<(), R::Error> {
            let mut m = reconciler.map()?;
            m.put("id", self.id)?;
            m.put("name", self.name)?;
            Ok(())
        }

        fn hydrate_key<'a, D: ReadDoc>(
            doc: &D,
            obj: &automerge::ObjId,
            prop: crate::Prop<'_>,
        ) -> Result<reconcile::LoadKey<Self::Key<'a>>, crate::ReconcileError> {
            hydrate_key::<_, u64>(doc, obj, prop, "id".into())
        }

        fn key(&self) -> LoadKey<Self::Key<'_>> {
            LoadKey::Found(self.id)
        }
    }

    #[test]
    fn reconcile_map_with_key() {
        let mut map = HashMap::new();
        map.insert("user", User { id: 1, name: "one" });
        let mut doc = automerge::AutoCommit::new();
        reconcile(&mut doc, &map).unwrap();

        let mut doc2 = doc.fork().with_actor(ActorId::random());
        let mut map2 = map.clone();
        map2.insert("user", User { id: 2, name: "two" });
        reconcile(&mut doc2, &map2).unwrap();

        map.insert(
            "user",
            User {
                id: 3,
                name: "three",
            },
        );
        reconcile(&mut doc, &map).unwrap();

        doc.merge(&mut doc2).unwrap();

        assert_doc!(
            doc.document(),
            map! {
                "user" => {
                    map! {
                        "id" => { 2_u64 },
                        "name" => { "two" },
                    },
                    map! {
                        "id" => { 3_u64 },
                        "name" => { "three" },
                    }
                }
            }
        );
    }
}