automerge 0.11.0

A JSON-like data structure (a CRDT) that can be modified concurrently by different users, and merged again automatically
Documentation
use proptest::prelude::*;

use super::{
    shape::{assert_private_data_changed, ShapeSignature},
    Anonymization,
};
use crate::marks::{ExpandMark, Mark};
use crate::transaction::{CommitOptions, Transactable};
use crate::{ActorId, AutoCommit, Automerge, ObjId, ObjType, ReadDoc, ScalarValue, ROOT};

const REPLICA_COUNT: usize = 3;
const ROOT_KEYS: [&str; 4] = ["alpha", "beta", "gamma", "delta"];

proptest! {
    #![proptest_config(ProptestConfig::with_cases(64))]

    #[test]
    fn randomized_documents_retain_their_shape(
        program in proptest::collection::vec(any::<u8>(), 0..192),
        seed in any::<[u8; 32]>(),
    ) {
        let source = document_from_program(&program);
        let source_shape = ShapeSignature::new(&source);
        let anonymized = Anonymization::from_seed(seed).anonymize(&source).unwrap();

        prop_assert_eq!(source_shape, ShapeSignature::new(&anonymized));
        assert_private_data_changed(&source, &anonymized);

        let reloaded = Automerge::load(&anonymized.save()).unwrap();
        prop_assert_eq!(ShapeSignature::new(&anonymized), ShapeSignature::new(&reloaded));
    }
}

fn document_from_program(program: &[u8]) -> Automerge {
    let mut base = AutoCommit::new();
    base.set_actor(actor(0));
    let list = base.put_object(ROOT, "items", ObjType::List).unwrap();
    let text = base.put_object(ROOT, "notes", ObjType::Text).unwrap();
    base.put(ROOT, "counter", ScalarValue::counter(0)).unwrap();
    base.commit_with(
        CommitOptions::default()
            .with_message("initial private data")
            .with_time(1_700_000_000),
    );

    let mut replicas = (0..REPLICA_COUNT)
        .map(|index| {
            let mut replica = base.fork();
            replica.set_actor(actor(index as u8 + 1));
            replica
        })
        .collect::<Vec<_>>();

    for instruction in program.chunks(6) {
        apply_instruction(&mut replicas, &list, &text, instruction);
    }

    for index in 1..replicas.len() {
        let mut incoming = replicas[index].fork();
        replicas[0].merge(&mut incoming).unwrap();
    }

    Automerge::load(&replicas[0].save()).unwrap()
}

fn apply_instruction(replicas: &mut [AutoCommit], list: &ObjId, text: &ObjId, instruction: &[u8]) {
    let byte = |index| instruction.get(index).copied().unwrap_or_default();
    let opcode = byte(0) % 11;
    let replica_index = byte(1) as usize % replicas.len();
    let a = byte(2);
    let b = byte(3);
    let c = byte(4);
    let d = byte(5);

    if opcode == 9 {
        let source_index = a as usize % replicas.len();
        if source_index != replica_index {
            let mut incoming = replicas[source_index].fork();
            replicas[replica_index].merge(&mut incoming).unwrap();
        }
        return;
    }

    let document = &mut replicas[replica_index];
    match opcode {
        0 => {
            document
                .put(
                    ROOT,
                    ROOT_KEYS[a as usize % ROOT_KEYS.len()],
                    scalar(b, c, d),
                )
                .unwrap();
        }
        1 => {
            document
                .delete(ROOT, ROOT_KEYS[a as usize % ROOT_KEYS.len()])
                .unwrap();
        }
        2 => {
            let length = document.length(list);
            let index = a as usize % (length + 1);
            document.insert(list, index, scalar(b, c, d)).unwrap();
        }
        3 => {
            let length = document.length(list);
            if length > 0 {
                document
                    .put(list, a as usize % length, scalar(b, c, d))
                    .unwrap();
            }
        }
        4 => {
            let length = document.length(list);
            if length > 0 {
                document.delete(list, a as usize % length).unwrap();
            }
        }
        5 => {
            let length = document.length(text);
            let index = a as usize % (length + 1);
            let value = ["a", "Z", " ", "\n"][b as usize % 4];
            document.splice_text(text, index, 0, value).unwrap();
        }
        6 => {
            let length = document.length(text);
            if length > 0 {
                document
                    .splice_text(text, a as usize % length, 1, "")
                    .unwrap();
            }
        }
        7 => {
            let increment = i64::from(i8::from_ne_bytes([a]));
            let _ = document.increment(ROOT, "counter", increment);
        }
        8 => {
            let length = document.length(text);
            if length > 0 {
                let start = a as usize % length;
                let end = start + 1 + b as usize % (length - start);
                let expand = match c % 4 {
                    0 => ExpandMark::Before,
                    1 => ExpandMark::After,
                    2 => ExpandMark::Both,
                    _ => ExpandMark::None,
                };
                document
                    .mark(
                        text,
                        Mark::new(format!("mark-{}", d % 4), scalar(b, c, d), start, end),
                        expand,
                    )
                    .unwrap();
            }
        }
        10 => {
            let object_type = match b % 4 {
                0 => ObjType::Map,
                1 => ObjType::Table,
                2 => ObjType::List,
                _ => ObjType::Text,
            };
            document
                .put_object(ROOT, format!("object-{}", a % 4), object_type)
                .unwrap();
        }
        _ => unreachable!(),
    }

    if document.pending_ops() > 0 {
        let mut options = CommitOptions::default().with_time(i64::from(i16::from_le_bytes([c, d])));
        if a.is_multiple_of(2) {
            options = options.with_message(format!("private-message-{a}-{b}"));
        }
        document.commit_with(options);
    }
}

fn scalar(kind: u8, a: u8, b: u8) -> ScalarValue {
    match kind % 10 {
        0 => ScalarValue::from(format!("private-{a}-{b}")),
        1 => ScalarValue::Bytes(vec![a, b, kind]),
        2 => ScalarValue::Int(i64::from(i16::from_le_bytes([a, b]))),
        3 => ScalarValue::Uint(u64::from(u16::from_le_bytes([a, b]))),
        4 => ScalarValue::F64(f64::from(i16::from_le_bytes([a, b])) / 10.0),
        5 => ScalarValue::counter(i64::from(i16::from_le_bytes([a, b]))),
        6 => ScalarValue::Timestamp(i64::from(i16::from_le_bytes([a, b]))),
        7 => ScalarValue::Boolean(a.is_multiple_of(2)),
        8 => ScalarValue::Unknown {
            type_code: 10 + a % 6,
            bytes: vec![a, b],
        },
        _ => ScalarValue::Null,
    }
}

fn actor(index: u8) -> ActorId {
    ActorId::from(vec![index])
}