loro 1.12.0

Loro is a high-performance CRDTs framework. Make your app collaborative efforlessly.
Documentation
#![cfg(feature = "jsonpath")]

use loro::{
    ContainerTrait, ExportMode, Index, LoroDoc, LoroList, LoroMap, LoroMovableList, LoroText,
    ToJson, TreeParentId, ValueOrContainer,
};
use pretty_assertions::assert_eq;
use serde_json::{json, Value};

fn deep_json(doc: &LoroDoc) -> Value {
    doc.get_deep_value().to_json_value()
}

fn results_json(results: &[ValueOrContainer]) -> Value {
    Value::Array(
        results
            .iter()
            .map(|value| value.get_deep_value().to_json_value())
            .collect(),
    )
}

fn build_doc() -> anyhow::Result<LoroDoc> {
    let doc = LoroDoc::new();
    doc.set_peer_id(11)?;

    let workspace = doc.get_map("workspace");
    workspace.insert("title", "Spec")?;

    let body = workspace.insert_container("body", LoroText::new())?;
    body.insert(0, "Hello world")?;

    let tasks = workspace.insert_container("tasks", LoroList::new())?;
    let first_task = tasks.insert_container(0, LoroMap::new())?;
    first_task.insert("title", "draft")?;
    first_task.insert("done", false)?;
    tasks.insert(1, "loose note")?;

    let order = workspace.insert_container("order", LoroMovableList::new())?;
    order.push("todo")?;
    order.push("doing")?;
    order.push("done")?;
    order.mov(2, 1)?;

    let outline = doc.get_tree("outline");
    outline.enable_fractional_index(0);
    let root = outline.create(TreeParentId::Root)?;
    let child = outline.create_at(root, 0)?;
    outline.get_meta(root)?.insert("title", "Root")?;
    outline.get_meta(child)?.insert("title", "Child")?;

    doc.commit();
    Ok(doc)
}

#[test]
fn jsonpath_returns_container_and_value_nodes_for_nested_state() -> anyhow::Result<()> {
    let doc = build_doc()?;
    let expected = deep_json(&doc);

    let body = doc.jsonpath("$.workspace.body")?;
    assert_eq!(body.len(), 1);
    let body_container = body[0]
        .as_container()
        .expect("text container should be returned as a container");
    assert_eq!(
        body[0].get_deep_value().to_json_value(),
        json!("Hello world")
    );
    assert_eq!(
        doc.get_path_to_container(&body_container.id())
            .expect("body container should be attached")
            .last()
            .map(|(_, index)| index),
        Some(&Index::Key("body".into()))
    );

    let task = doc.jsonpath("$.workspace.tasks[0]")?;
    assert_eq!(task.len(), 1);
    let task_container = task[0]
        .as_container()
        .expect("nested map should be returned as a container");
    assert_eq!(
        task[0].get_deep_value().to_json_value(),
        json!({"done": false, "title": "draft"})
    );
    assert_eq!(
        doc.get_path_to_container(&task_container.id())
            .expect("task container should be attached")
            .last()
            .map(|(_, index)| index),
        Some(&Index::Seq(0))
    );

    let task_title = doc.jsonpath("$.workspace.tasks[0].title")?;
    assert_eq!(results_json(&task_title), json!(["draft"]));
    assert!(task_title[0].as_value().is_some());

    let movable = doc.jsonpath("$.workspace.order[1]")?;
    assert_eq!(results_json(&movable), json!(["done"]));
    assert!(movable[0].as_value().is_some());

    assert_eq!(
        doc.get_by_path(&[Index::Key("workspace".into()), Index::Key("body".into()),])
            .expect("body should resolve by path")
            .get_deep_value()
            .to_json_value(),
        json!("Hello world")
    );
    assert_eq!(
        doc.get_by_path(&[
            Index::Key("workspace".into()),
            Index::Key("tasks".into()),
            Index::Seq(0),
            Index::Key("title".into()),
        ])
        .expect("nested task title should resolve by path")
        .get_deep_value()
        .to_json_value(),
        json!("draft")
    );
    assert_eq!(
        doc.get_by_path(&[
            Index::Key("outline".into()),
            Index::Seq(0),
            Index::Key("title".into()),
        ])
        .expect("tree root title should resolve by path")
        .get_deep_value()
        .to_json_value(),
        json!("Root")
    );
    assert_eq!(
        doc.get_by_path(&[
            Index::Key("outline".into()),
            Index::Seq(0),
            Index::Seq(0),
            Index::Key("title".into()),
        ])
        .expect("tree child title should resolve by path")
        .get_deep_value()
        .to_json_value(),
        json!("Child")
    );

    let snapshot = LoroDoc::from_snapshot(&doc.export(ExportMode::Snapshot)?)?;
    assert_eq!(deep_json(&snapshot), expected);
    assert_eq!(
        results_json(&snapshot.jsonpath("$.workspace.tasks[0].title")?),
        json!(["draft"])
    );
    assert_eq!(
        results_json(&snapshot.jsonpath("$.workspace.order[1]")?),
        json!(["done"])
    );
    assert!(snapshot
        .jsonpath("$.workspace.body")?
        .first()
        .and_then(|x| x.as_container())
        .is_some());

    let replica = LoroDoc::new();
    replica.import(&doc.export(ExportMode::all_updates())?)?;
    assert_eq!(deep_json(&replica), expected);
    assert_eq!(
        results_json(&replica.jsonpath("$.workspace.body")?),
        json!(["Hello world"])
    );
    assert_eq!(
        results_json(&replica.jsonpath("$.workspace.tasks[0].title")?),
        json!(["draft"])
    );
    assert_eq!(
        replica
            .get_by_path(&[
                Index::Key("workspace".into()),
                Index::Key("tasks".into()),
                Index::Seq(0),
                Index::Key("done".into()),
            ])
            .expect("task completion should survive import")
            .get_deep_value()
            .to_json_value(),
        json!(false)
    );

    Ok(())
}

#[test]
fn path_queries_keep_tree_and_nested_container_contracts_after_roundtrip() -> anyhow::Result<()> {
    let doc = build_doc()?;
    let snapshot = doc.export(ExportMode::Snapshot)?;
    let restored = LoroDoc::from_snapshot(&snapshot)?;

    let restored_body = restored.jsonpath("$.workspace.body")?;
    let restored_task = restored.jsonpath("$.workspace.tasks[0]")?;

    assert_eq!(
        restored_body[0].get_deep_value().to_json_value(),
        json!("Hello world")
    );
    assert_eq!(
        restored_task[0].get_deep_value().to_json_value(),
        json!({"done": false, "title": "draft"})
    );

    let restored_body_path = restored
        .get_path_to_container(&restored_body[0].as_container().unwrap().id())
        .expect("restored body should still be attached");
    assert_eq!(
        restored_body_path.last().map(|(_, index)| index),
        Some(&Index::Key("body".into()))
    );

    let restored_task_path = restored
        .get_path_to_container(&restored_task[0].as_container().unwrap().id())
        .expect("restored task should still be attached");
    assert_eq!(
        restored_task_path.last().map(|(_, index)| index),
        Some(&Index::Seq(0))
    );

    let tree_root_title = restored
        .get_by_path(&[
            Index::Key("outline".into()),
            Index::Seq(0),
            Index::Key("title".into()),
        ])
        .expect("tree root title should resolve after snapshot");
    assert_eq!(
        tree_root_title.get_deep_value().to_json_value(),
        json!("Root")
    );

    let tree_child_title = restored
        .get_by_path(&[
            Index::Key("outline".into()),
            Index::Seq(0),
            Index::Seq(0),
            Index::Key("title".into()),
        ])
        .expect("tree child title should resolve after snapshot");
    assert_eq!(
        tree_child_title.get_deep_value().to_json_value(),
        json!("Child")
    );

    Ok(())
}