autosurgeon 0.12.0

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

use automerge::{self as am, ObjType};

use crate::{Hydrate, HydrateError};

impl<K, V> Hydrate for HashMap<K, V>
where
    K: From<String> + Hash + Eq,
    V: Hydrate,
{
    fn hydrate_map<D: crate::ReadDoc>(
        doc: &D,
        obj: &automerge::ObjId,
    ) -> Result<Self, crate::HydrateError> {
        hydrate_map_impl(doc, obj, |k| Ok(K::from(k.to_string())))
    }
}

impl<K, V> Hydrate for BTreeMap<K, V>
where
    K: From<String> + Ord,
    V: Hydrate,
{
    fn hydrate_map<D: crate::ReadDoc>(
        doc: &D,
        obj: &automerge::ObjId,
    ) -> Result<Self, crate::HydrateError> {
        hydrate_map_impl(doc, obj, |k| Ok(K::from(k.to_string())))
    }
}

pub(crate) fn hydrate_map_impl<'a, F, D, K, V, M>(
    doc: &'a D,
    obj: &automerge::ObjId,
    extract_key: F,
) -> Result<M, crate::HydrateError>
where
    F: for<'b> Fn(&'b str) -> Result<K, crate::HydrateError>,
    D: crate::ReadDoc,
    V: Hydrate,
    M: FromIterator<(K, V)>,
{
    let Some(obj_type) = doc.object_type(obj) else {
        return Err(HydrateError::unexpected(
            "a map",
            "a scalar value".to_string(),
        ));
    };
    match obj_type {
        ObjType::Map | ObjType::Table => doc
            .map_range(obj.clone(), ..)
            .map(move |am::iter::MapRangeItem { key, .. }| {
                let key_parsed: K = extract_key(key.as_ref())?;
                let val = V::hydrate(doc, obj, key.into())?;
                Ok((key_parsed, val))
            })
            .collect(),
        ObjType::Text => Err(HydrateError::unexpected(
            "a map",
            "a text object".to_string(),
        )),
        ObjType::List => Err(HydrateError::unexpected(
            "a map",
            "a list object".to_string(),
        )),
    }
}

#[cfg(test)]
mod tests {
    use am::transaction::Transactable;
    use automerge as am;
    use std::collections::HashMap;

    use crate::{hydrate, Hydrate};

    #[derive(Debug, PartialEq)]
    struct User {
        id: u64,
        name: String,
    }
    impl Hydrate for User {
        fn hydrate_map<D: crate::ReadDoc>(
            doc: &D,
            obj: &am::ObjId,
        ) -> Result<Self, crate::HydrateError> {
            let id = u64::hydrate(doc, obj, "id".into())?;
            let name = String::hydrate(doc, obj, "name".into())?;
            Ok(User { id, name })
        }
    }

    #[derive(Debug, PartialEq, Eq, Hash)]
    struct UserName(String);
    impl From<String> for UserName {
        fn from(s: String) -> Self {
            UserName(s)
        }
    }
    impl<'a> From<&'a str> for UserName {
        fn from(s: &'a str) -> Self {
            UserName(s.to_string())
        }
    }

    #[test]
    fn basic_hydrate_map() {
        let mut doc = am::AutoCommit::new();
        let u1 = doc.put_object(am::ROOT, "user1", am::ObjType::Map).unwrap();
        doc.put(&u1, "name", "One").unwrap();
        doc.put(&u1, "id", 1_u64).unwrap();

        let u2 = doc.put_object(am::ROOT, "user2", am::ObjType::Map).unwrap();
        doc.put(&u2, "name", "Two").unwrap();
        doc.put(&u2, "id", 2_u64).unwrap();

        let mut expected: HashMap<UserName, User> = HashMap::new();
        expected.insert(
            "user1".into(),
            User {
                id: 1,
                name: "One".to_string(),
            },
        );
        expected.insert(
            "user2".into(),
            User {
                id: 2,
                name: "Two".to_string(),
            },
        );

        let result: HashMap<UserName, User> = hydrate(&doc).unwrap();
        assert_eq!(result, expected);
    }
}