colla 0.2.0

Operational Transformation library for nested documents with text and rich-text
Documentation
use colla::{
    apply, compose, invert, transform_pair, Change, ChangeKind, InputLimits, IntChange, ListChange,
    ListOp, MapChange, MapEntryChange, RichTextChange, TextChange, TextOp, TieBreak, Value,
};

#[test]
fn package_functions_and_type_entrypoints_form_one_facade() {
    let base = Value::text("ab");
    let first = Change::from(
        TextChange::from_ops(vec![TextOp::Retain(1), TextOp::Insert("x".into())]).unwrap(),
    );
    let second = Change::from(
        TextChange::from_ops(vec![TextOp::Retain(2), TextOp::Insert("y".into())]).unwrap(),
    );

    let after_first = apply(&base, &first).unwrap();
    let combined = compose(&first, &second).unwrap();
    assert_eq!(apply(&base, &combined).unwrap(), Value::text("axyb"));
    assert_eq!(apply(&after_first, &second).unwrap(), Value::text("axyb"));

    let inverse = invert(&combined, &base).unwrap();
    assert_eq!(apply(&Value::text("axyb"), &inverse).unwrap(), base);

    let concurrent = Change::from(
        TextChange::from_ops(vec![TextOp::Retain(1), TextOp::Insert("z".into())]).unwrap(),
    );
    let (first_prime, concurrent_prime) =
        transform_pair(&first, &concurrent, TieBreak::LeftFirst).unwrap();
    assert_eq!(
        apply(&apply(&base, &first).unwrap(), &concurrent_prime).unwrap(),
        apply(&apply(&base, &concurrent).unwrap(), &first_prime).unwrap()
    );

    let bytes = combined.encode();
    assert_eq!(Change::decode(&bytes).unwrap(), combined);
    assert_eq!(
        Change::decode_with_limits(&bytes, &InputLimits::default()).unwrap(),
        combined
    );

    let value_bytes = after_first.encode();
    assert_eq!(Value::decode(&value_bytes).unwrap(), after_first);
    assert_eq!(
        Value::decode_with_limits(&value_bytes, &InputLimits::default()).unwrap(),
        after_first
    );
}

#[test]
fn typed_constructors_accept_iterators_and_convert_into_change() {
    let text: Change = TextChange::from_ops([TextOp::Retain(1), TextOp::Insert("x".into())])
        .unwrap()
        .into();
    assert!(matches!(text.kind(), ChangeKind::Text(_)));

    let list: Change = ListChange::from_ops(vec![ListOp::Insert(vec![Value::int(1)])])
        .unwrap()
        .into();
    assert!(matches!(list.kind(), ChangeKind::List(_)));

    let map: Change = MapChange::from_entries(
        [("key", Value::string("value"))]
            .into_iter()
            .map(|(key, value)| (key, MapEntryChange::Insert(value))),
    )
    .unwrap()
    .into();
    assert!(matches!(map.kind(), ChangeKind::Map(_)));

    let rich: Change = RichTextChange::from_ops(std::iter::empty()).unwrap().into();
    assert!(rich.is_noop());
    assert!(Change::from(TextChange::from_ops([]).unwrap()).is_noop());
    assert!(Change::from(ListChange::from_ops([]).unwrap()).is_noop());
    assert!(Change::from(MapChange::from_entries::<_, String>([]).unwrap()).is_noop());
    assert!(Change::from(IntChange::Add(0)).is_noop());
    assert!(matches!(
        Change::from(IntChange::Add(4)).kind(),
        ChangeKind::Int(_)
    ));
}

#[test]
fn operation_results_are_independent_of_receiver_input_limits() {
    let base = Value::text("a");
    let change = Change::from(
        TextChange::from_ops(vec![TextOp::Retain(1), TextOp::Insert("bc".into())]).unwrap(),
    );
    let result = apply(&base, &change).unwrap();
    let bytes = result.encode();
    let limits = InputLimits {
        max_string_bytes: 1,
        ..InputLimits::default()
    };

    assert!(Value::decode_with_limits(&bytes, &limits).is_err());
    assert_eq!(Value::decode(&bytes).unwrap(), result);
}

#[test]
fn huge_logical_changes_transform_without_an_operation_budget() {
    let huge = Change::from(TextChange::from_ops(vec![TextOp::Delete(usize::MAX)]).unwrap());
    assert_eq!(
        transform_pair(&huge, &Change::noop(), TieBreak::LeftFirst).unwrap(),
        (huge, Change::noop())
    );
}