loro 1.12.0

Loro is a high-performance CRDTs framework. Make your app collaborative efforlessly.
Documentation
use std::{borrow::Cow, collections::HashMap, iter::FromIterator};

use loro::{
    event::{Diff, ListDiffItem, MapDelta},
    ApplyDiff, ContainerTrait, ContainerType, Index, JsonSchema, LoroDoc, LoroList, LoroMap,
    LoroText, LoroValue, TextDelta, ToJson, ValueOrContainer, VersionVector,
};
use pretty_assertions::assert_eq;
use rustc_hash::FxHashMap;
use serde_json::json;

fn text_insert(insert: &str) -> TextDelta {
    TextDelta::Insert {
        insert: insert.to_string(),
        attributes: None,
    }
}

fn text_retain(retain: usize) -> TextDelta {
    TextDelta::Retain {
        retain,
        attributes: None,
    }
}

fn assert_to_json_roundtrip(value: &LoroValue) {
    let compact = value.to_json();
    let pretty = value.to_json_pretty();

    assert_eq!(LoroValue::from_json(&compact), value.clone());
    assert_eq!(LoroValue::from_json(&pretty), value.clone());
    assert_eq!(
        serde_json::from_str::<serde_json::Value>(&compact).unwrap(),
        value.to_json_value()
    );
    assert_eq!(
        serde_json::from_str::<serde_json::Value>(&pretty).unwrap(),
        value.to_json_value()
    );
}

#[test]
fn loro_value_defaults_and_to_json_roundtrip_cover_contracts() -> anyhow::Result<()> {
    assert_eq!(LoroValue::default(), LoroValue::Null);
    assert_eq!(
        ContainerType::Map.default_value(),
        LoroValue::from(HashMap::<String, LoroValue>::new())
    );
    assert_eq!(
        ContainerType::List.default_value(),
        LoroValue::from(Vec::<LoroValue>::new())
    );
    assert_eq!(
        ContainerType::Text.default_value(),
        LoroValue::from(String::new())
    );
    assert_eq!(
        ContainerType::Tree.default_value(),
        LoroValue::from(Vec::<LoroValue>::new())
    );
    assert_eq!(
        ContainerType::MovableList.default_value(),
        LoroValue::from(Vec::<LoroValue>::new())
    );
    #[cfg(feature = "counter")]
    assert_eq!(ContainerType::Counter.default_value(), LoroValue::from(0.0));

    let doc = LoroDoc::new();
    let container = doc
        .get_map("root")
        .insert_container("child", LoroText::new())?;

    let binary = LoroValue::from(vec![1_u8, 2, 3, 255]);
    assert_eq!(binary.to_json_value(), json!([1, 2, 3, 255]));
    assert_eq!(
        serde_json::from_str::<serde_json::Value>(&binary.to_json())?,
        json!([1, 2, 3, 255])
    );
    assert_eq!(
        serde_json::from_str::<serde_json::Value>(&binary.to_json_pretty())?,
        json!([1, 2, 3, 255])
    );

    let values = vec![
        LoroValue::Null,
        false.into(),
        true.into(),
        123_i64.into(),
        (-12.5_f64).into(),
        "hello".into(),
        LoroValue::from(vec![
            LoroValue::Null,
            true.into(),
            7_i64.into(),
            3.25_f64.into(),
            "text".into(),
            LoroValue::from(vec![4_i64, 5_i64]),
            LoroValue::from(HashMap::from([
                ("nested".to_string(), LoroValue::from(vec![1_i64, 2_i64])),
                ("flag".to_string(), true.into()),
            ])),
        ]),
        LoroValue::from(HashMap::from([
            ("null".to_string(), LoroValue::Null),
            ("count".to_string(), 9_i64.into()),
            ("ratio".to_string(), 1.25_f64.into()),
            ("label".to_string(), "map".into()),
            ("items".to_string(), LoroValue::from(vec![1_i64, 2_i64])),
        ])),
        LoroValue::from(container.id()),
    ];

    for value in values {
        assert_to_json_roundtrip(&value);
    }

    Ok(())
}

#[test]
fn apply_diff_handles_nested_paths_existing_list_indexes_and_missing_tree_nodes(
) -> anyhow::Result<()> {
    let mut root = LoroValue::from(HashMap::<String, LoroValue>::new());

    root.apply(
        &vec![Index::Key("doc".into()), Index::Key("title".into())].into(),
        &[Diff::Text(vec![text_insert("Hello")]).into()],
    );

    root.apply(
        &vec![Index::Key("doc".into()), Index::Key("meta".into())].into(),
        &[Diff::Map(MapDelta {
            updated: FxHashMap::from_iter([
                (
                    Cow::Borrowed("author"),
                    Some(ValueOrContainer::Value("Ada".into())),
                ),
                (
                    Cow::Borrowed("pages"),
                    Some(ValueOrContainer::Value(3_i64.into())),
                ),
                (
                    Cow::Borrowed("tags"),
                    Some(ValueOrContainer::Value(LoroValue::from(vec![
                        LoroValue::from("rust"),
                        LoroValue::from("crdt"),
                    ]))),
                ),
            ]),
        })
        .into()],
    );

    root.apply(
        &vec![Index::Key("doc".into()), Index::Key("items".into())].into(),
        &[Diff::List(vec![ListDiffItem::Insert {
            insert: vec![
                ValueOrContainer::Value("alpha".into()),
                ValueOrContainer::Value("beta".into()),
                ValueOrContainer::Value(LoroValue::from(vec![1_i64, 2_i64])),
            ],
            is_move: false,
        }])
        .into()],
    );

    root.apply(
        &vec![
            Index::Key("doc".into()),
            Index::Key("items".into()),
            Index::Seq(1),
        ]
        .into(),
        &[Diff::Text(vec![
            text_retain(2),
            TextDelta::Delete { delete: 2 },
            text_insert("TA"),
        ])
        .into()],
    );

    assert_eq!(
        root.to_json_value(),
        json!({
            "doc": {
                "title": "Hello",
                "meta": {
                    "author": "Ada",
                    "pages": 3,
                    "tags": ["rust", "crdt"],
                },
                "items": ["alpha", "beTA", [1, 2]],
            }
        })
    );

    let doc = LoroDoc::new();
    let tree = doc.get_tree("tree");
    tree.enable_fractional_index(0);
    let node = tree.create(None)?;
    tree.get_meta(node)?.insert("kind", "live")?;
    doc.commit();

    tree.delete(node)?;
    doc.commit();

    let mut tree_value = tree.get_value_with_meta();
    let before = tree_value.clone();
    tree_value.apply(
        &vec![Index::Node(node)].into(),
        &[Diff::Map(MapDelta {
            updated: FxHashMap::from_iter([(
                Cow::Borrowed("ghost"),
                Some(ValueOrContainer::Value(true.into())),
            )]),
        })
        .into()],
    );
    assert_eq!(tree_value, before);

    Ok(())
}

#[test]
fn json_schema_roundtrip_keeps_value_types_and_imports_back_state() -> anyhow::Result<()> {
    let doc = LoroDoc::new();
    doc.set_peer_id(17)?;

    let root = doc.get_map("root");
    root.insert("null", LoroValue::Null)?;
    root.insert("bool", true)?;
    root.insert("i64", 42_i64)?;
    root.insert("double", 3.5_f64)?;
    root.insert("string", "hello")?;
    root.insert("binary", vec![1_u8, 2, 3, 255])?;
    root.insert(
        "list_value",
        LoroValue::from(vec![
            LoroValue::Null,
            true.into(),
            7_i64.into(),
            1.25_f64.into(),
            "leaf".into(),
            vec![4_u8, 5].into(),
            LoroValue::from(HashMap::from([
                ("nested".to_string(), "value".into()),
                ("count".to_string(), 9_i64.into()),
            ])),
        ]),
    )?;
    root.insert(
        "map_value",
        LoroValue::from(HashMap::from([
            ("nested".to_string(), LoroValue::from(vec![1_i64, 2_i64])),
            ("flag".to_string(), true.into()),
        ])),
    )?;

    let child_map = root.insert_container("child_map", LoroMap::new())?;
    child_map.insert("kind", "map")?;
    child_map.insert("payload", vec![9_u8, 8, 7])?;

    let child_list = root.insert_container("child_list", LoroList::new())?;
    child_list.push("alpha")?;
    child_list.push(LoroValue::from(HashMap::from([
        ("flag".to_string(), true.into()),
        ("label".to_string(), "list-item".into()),
    ])))?;
    let nested_text = child_list.push_container(LoroText::new())?;
    nested_text.insert(0, "nested")?;
    nested_text.mark(0..6, "bold", true)?;

    let body = root.insert_container("body", LoroText::new())?;
    body.insert(0, "hello")?;
    body.mark(0..5, "italic", true)?;

    #[cfg(feature = "counter")]
    {
        let counter = root.insert_container("counter", loro::LoroCounter::new())?;
        counter.increment(5.5)?;
        counter.decrement(1.0)?;
    }

    doc.commit();

    let start = VersionVector::default();
    let end = doc.oplog_vv();
    let compressed = doc.export_json_updates(&start, &end);
    let uncompressed = doc.export_json_updates_without_peer_compression(&start, &end);

    let compressed_json = serde_json::to_string(&compressed)?;
    let uncompressed_json = serde_json::to_string(&uncompressed)?;

    let compressed_from_serde: JsonSchema = serde_json::from_str(&compressed_json)?;
    let compressed_from_try_into: JsonSchema = compressed_json.as_str().try_into()?;
    let uncompressed_from_serde: JsonSchema = serde_json::from_str(&uncompressed_json)?;
    let uncompressed_from_try_into: JsonSchema = uncompressed_json.as_str().try_into()?;

    assert_eq!(
        serde_json::to_value(&compressed_from_serde)?,
        serde_json::to_value(&compressed)?
    );
    assert_eq!(
        serde_json::to_value(&compressed_from_try_into)?,
        serde_json::to_value(&compressed)?
    );
    assert_eq!(
        serde_json::to_value(&uncompressed_from_serde)?,
        serde_json::to_value(&uncompressed)?
    );
    assert_eq!(
        serde_json::to_value(&uncompressed_from_try_into)?,
        serde_json::to_value(&uncompressed)?
    );

    let imported_from_compressed = LoroDoc::new();
    imported_from_compressed.import_json_updates(compressed_from_try_into.clone())?;
    assert_eq!(
        imported_from_compressed.get_deep_value().to_json_value(),
        doc.get_deep_value().to_json_value()
    );

    let imported_from_compressed_json = LoroDoc::new();
    imported_from_compressed_json.import_json_updates(compressed_json.clone())?;
    assert_eq!(
        imported_from_compressed_json
            .get_deep_value()
            .to_json_value(),
        doc.get_deep_value().to_json_value()
    );

    let imported_from_uncompressed = LoroDoc::new();
    imported_from_uncompressed.import_json_updates(uncompressed_from_serde.clone())?;
    assert_eq!(
        imported_from_uncompressed.get_deep_value().to_json_value(),
        doc.get_deep_value().to_json_value()
    );

    let imported_from_uncompressed_json = LoroDoc::new();
    imported_from_uncompressed_json.import_json_updates(uncompressed_json.clone())?;
    assert_eq!(
        imported_from_uncompressed_json
            .get_deep_value()
            .to_json_value(),
        doc.get_deep_value().to_json_value()
    );

    Ok(())
}