loro 1.12.0

Loro is a high-performance CRDTs framework. Make your app collaborative efforlessly.
Documentation
use loro::json::redact;
use loro::{LoroDoc, LoroList, LoroMovableList, LoroTree, LoroValue};
use loro_internal::version::VersionRange;

#[test]
fn redact_text_doc() {
    let doc = LoroDoc::new();
    doc.set_peer_id(1).unwrap();
    let text = doc.get_text("text");
    //              |-----------------------| <- 24 ops
    text.insert(0, "Hello, world! This is a secret message.")
        .unwrap();

    let mut json = doc.export_json_updates(&Default::default(), &doc.oplog_vv());
    let mut range = VersionRange::new();
    range.insert(1, 24, 30);
    redact(&mut json, range).unwrap();
    let redacted_doc = LoroDoc::new();
    redacted_doc.import_json_updates(json).unwrap();
    let redacted_text = redacted_doc.get_text("text");
    assert_eq!(
        redacted_text.to_string(),
        "Hello, world! This is a ������ message."
    );
    assert_ne!(text.to_string(), redacted_text.to_string());
}

#[test]
fn redact_map_list_insertions() {
    let doc = LoroDoc::new();
    doc.set_peer_id(1).unwrap();
    let map = doc.get_map("map");
    let list = doc.get_list("list");

    // Insert into map
    map.insert("key1", "sensitive data").unwrap();
    map.insert("key2", 42).unwrap();

    // Insert into list
    list.insert(0, "secret info").unwrap();
    list.insert(1, true).unwrap();

    let mut json = doc.export_json_updates(&Default::default(), &doc.oplog_vv());
    let mut range = VersionRange::new();
    range.insert(1, 0, 5); // Redact all operations
    redact(&mut json, range).unwrap();

    let s = serde_json::to_string_pretty(&json).unwrap();
    let expected = r#"{
  "schema_version": 1,
  "start_version": {},
  "peers": [
    "1"
  ],
  "changes": [
    {
      "id": "0@0",
      "timestamp": 0,
      "deps": [],
      "lamport": 0,
      "msg": null,
      "ops": [
        {
          "container": "cid:root-map:Map",
          "content": {
            "type": "insert",
            "key": "key1",
            "value": null
          },
          "counter": 0
        },
        {
          "container": "cid:root-map:Map",
          "content": {
            "type": "insert",
            "key": "key2",
            "value": null
          },
          "counter": 1
        },
        {
          "container": "cid:root-list:List",
          "content": {
            "type": "insert",
            "pos": 0,
            "value": [
              null,
              null
            ]
          },
          "counter": 2
        }
      ]
    }
  ]
}"#;
    assert_eq!(s, expected);
    let redacted_doc = LoroDoc::new();
    redacted_doc.import_json_updates(json).unwrap();

    let redacted_map = redacted_doc.get_map("map");
    let redacted_list = redacted_doc.get_list("list");

    // Check map values
    assert_eq!(
        redacted_map.get("key1").unwrap().into_value().unwrap(),
        LoroValue::Null
    );
    assert_eq!(
        redacted_map.get("key2").unwrap().into_value().unwrap(),
        LoroValue::Null
    );

    // Check list values
    assert_eq!(
        redacted_list.get(0).unwrap().into_value().unwrap(),
        LoroValue::Null
    );
    assert_eq!(
        redacted_list.get(1).unwrap().into_value().unwrap(),
        LoroValue::Null
    );
}

#[test]
fn redact_movable_list() {
    let doc = LoroDoc::new();
    doc.set_peer_id(1).unwrap();
    let list = doc.get_movable_list("movable_list");
    list.insert(0, "sensitive data 1").unwrap();
    list.insert(1, "sensitive data 2").unwrap();
    list.set(0, "updated sensitive data").unwrap();

    let mut json = doc.export_json_updates(&Default::default(), &doc.oplog_vv());
    let mut range = VersionRange::new();
    range.insert(1, 0, 3);
    redact(&mut json, range).unwrap();
    let redacted_json = serde_json::to_string_pretty(&json).unwrap();
    assert_eq!(
        redacted_json,
        r#"{
  "schema_version": 1,
  "start_version": {},
  "peers": [
    "1"
  ],
  "changes": [
    {
      "id": "0@0",
      "timestamp": 0,
      "deps": [],
      "lamport": 0,
      "msg": null,
      "ops": [
        {
          "container": "cid:root-movable_list:MovableList",
          "content": {
            "type": "insert",
            "pos": 0,
            "value": [
              null,
              null
            ]
          },
          "counter": 0
        },
        {
          "container": "cid:root-movable_list:MovableList",
          "content": {
            "type": "set",
            "elem_id": "L0@0",
            "value": null
          },
          "counter": 2
        }
      ]
    }
  ]
}"#
    );

    // Create a new document from the redacted JSON
    let redacted_doc = LoroDoc::new();
    redacted_doc.import_json_updates(&redacted_json).unwrap();

    let redacted_list = redacted_doc.get_movable_list("movable_list");

    // Check that the insert operations were redacted
    assert_eq!(
        redacted_list.get(0).unwrap().into_value().unwrap(),
        LoroValue::Null
    );
    assert_eq!(
        redacted_list.get(1).unwrap().into_value().unwrap(),
        LoroValue::Null
    );

    // Check that the set operation was redacted
    // The set operation should have replaced the first null value with another null
    assert_eq!(
        redacted_list.get(0).unwrap().into_value().unwrap(),
        LoroValue::Null
    );

    // Verify the list length is correct
    assert_eq!(redacted_list.len(), 2);

    // Optionally, you can print the redacted JSON to inspect it
    // println!("{}", redacted_json);
}

#[test]
fn redact_should_keep_parent_child_relationship() {
    let doc = LoroDoc::new();
    doc.set_peer_id(1).unwrap();
    let map = doc.get_map("map");
    let list = map.insert_container("list", LoroList::new()).unwrap();
    let tree = list.insert_container(0, LoroTree::new()).unwrap();
    let node = tree.create(None).unwrap();
    let sub_map = tree.get_meta(node).unwrap();
    let _m = sub_map
        .insert_container("ll", LoroMovableList::new())
        .unwrap();
    let mut json = doc.export_json_updates(&Default::default(), &doc.oplog_vv());
    let mut range = VersionRange::new();
    range.insert(1, 0, 100);
    redact(&mut json, range).unwrap();
    let redacted_json = serde_json::to_string_pretty(&json).unwrap();
    pretty_assertions::assert_eq!(
        redacted_json,
        r#"{
  "schema_version": 1,
  "start_version": {},
  "peers": [
    "1"
  ],
  "changes": [
    {
      "id": "0@0",
      "timestamp": 0,
      "deps": [],
      "lamport": 0,
      "msg": null,
      "ops": [
        {
          "container": "cid:root-map:Map",
          "content": {
            "type": "insert",
            "key": "list",
            "value": "🦜:cid:0@0:List"
          },
          "counter": 0
        },
        {
          "container": "cid:0@0:List",
          "content": {
            "type": "insert",
            "pos": 0,
            "value": [
              "🦜:cid:1@0:Tree"
            ]
          },
          "counter": 1
        },
        {
          "container": "cid:1@0:Tree",
          "content": {
            "type": "create",
            "target": "2@0",
            "parent": null,
            "fractional_index": "80"
          },
          "counter": 2
        },
        {
          "container": "cid:2@0:Map",
          "content": {
            "type": "insert",
            "key": "ll",
            "value": "🦜:cid:3@0:MovableList"
          },
          "counter": 3
        }
      ]
    }
  ]
}"#
    );
    let new_doc = LoroDoc::new();
    new_doc.import_json_updates(&redacted_json).unwrap();
    assert_eq!(new_doc.get_deep_value(), doc.get_deep_value());
}