#![allow(
clippy::bool_assert_comparison, // assert_eq!(x, true) is clearer in tests
clippy::useless_conversion, // .into() on same type in loro import_batch
clippy::box_collection, // Box<String> intentionally tested
clippy::owned_cow, // Cow<'_, String> intentionally tested
)]
use std::borrow::Cow;
use std::collections::HashMap;
use loro::LoroDoc;
use lorosurgeon::{
ByteArray, DocSync, Hydrate, HydrateResultExt, MapReconciler, Reconcile, RootReconciler,
};
#[test]
fn test_scalar_roundtrip_bool() {
let doc = LoroDoc::new();
let map = doc.get_map("test");
map.insert("v", true).unwrap();
doc.commit();
let result: bool = lorosurgeon::hydrate_prop(&map, "v").unwrap();
assert_eq!(result, true);
}
#[test]
fn test_scalar_roundtrip_i64() {
let doc = LoroDoc::new();
let map = doc.get_map("test");
map.insert("v", 42i64).unwrap();
doc.commit();
let result: i64 = lorosurgeon::hydrate_prop(&map, "v").unwrap();
assert_eq!(result, 42);
}
#[test]
fn test_scalar_roundtrip_f64() {
let doc = LoroDoc::new();
let map = doc.get_map("test");
map.insert("v", 2.72f64).unwrap();
doc.commit();
let result: f64 = lorosurgeon::hydrate_prop(&map, "v").unwrap();
assert!((result - 2.72).abs() < f64::EPSILON);
}
#[test]
fn test_scalar_roundtrip_string() {
let doc = LoroDoc::new();
let map = doc.get_map("test");
map.insert("v", "hello").unwrap();
doc.commit();
let result: String = lorosurgeon::hydrate_prop(&map, "v").unwrap();
assert_eq!(result, "hello");
}
#[test]
fn test_scalar_roundtrip_option_none() {
let doc = LoroDoc::new();
let map = doc.get_map("test");
doc.commit();
let result: Option<String> = lorosurgeon::hydrate_prop_or_default(&map, "v").unwrap();
assert_eq!(result, None);
}
#[test]
fn test_scalar_roundtrip_option_some() {
let doc = LoroDoc::new();
let map = doc.get_map("test");
map.insert("v", "world").unwrap();
doc.commit();
let result: Option<String> = lorosurgeon::hydrate_prop(&map, "v").unwrap();
assert_eq!(result, Some("world".to_string()));
}
#[test]
fn test_integer_overflow() {
let doc = LoroDoc::new();
let map = doc.get_map("test");
map.insert("v", 300i64).unwrap();
doc.commit();
let result = lorosurgeon::hydrate_prop::<u8>(&map, "v");
assert!(result.is_err());
}
#[test]
fn test_i64_as_f64() {
let doc = LoroDoc::new();
let map = doc.get_map("test");
map.insert("v", 42i64).unwrap();
doc.commit();
let result: f64 = lorosurgeon::hydrate_prop(&map, "v").unwrap();
assert_eq!(result, 42.0);
}
#[test]
fn test_reconcile_scalars_into_map() {
let doc = LoroDoc::new();
let map = doc.get_map("test");
let mut mr = MapReconciler { map: map.clone() };
mr.entry("name", &"Alice".to_string()).unwrap();
mr.entry("age", &30i64).unwrap();
mr.entry("active", &true).unwrap();
mr.entry("score", &95.5f64).unwrap();
doc.commit();
assert_eq!(
lorosurgeon::hydrate_prop::<String>(&map, "name").unwrap(),
"Alice"
);
assert_eq!(lorosurgeon::hydrate_prop::<i64>(&map, "age").unwrap(), 30);
assert_eq!(
lorosurgeon::hydrate_prop::<bool>(&map, "active").unwrap(),
true
);
assert_eq!(
lorosurgeon::hydrate_prop::<f64>(&map, "score").unwrap(),
95.5
);
}
#[derive(Debug, Clone, PartialEq, Hydrate, Reconcile)]
struct Position {
x: f64,
y: f64,
}
#[test]
fn test_derive_simple_struct() {
let doc = LoroDoc::new();
let map = doc.get_map("root");
let pos = Position { x: 10.0, y: 20.0 };
let reconciler = RootReconciler::new(map.clone());
pos.reconcile(reconciler).unwrap();
doc.commit();
let hydrated = Position::hydrate_map(&map).unwrap();
assert_eq!(hydrated, pos);
}
#[derive(Debug, Clone, PartialEq, Hydrate, Reconcile)]
struct ResourceMeta {
#[loro(default)]
id: String,
name: String,
description: Option<String>,
#[loro(default)]
forkable: bool,
}
#[test]
fn test_derive_struct_with_options_and_missing() {
let doc = LoroDoc::new();
let map = doc.get_map("root");
let meta = ResourceMeta {
id: "abc".to_string(),
name: "Test".to_string(),
description: Some("A test resource".to_string()),
forkable: true,
};
let reconciler = RootReconciler::new(map.clone());
meta.reconcile(reconciler).unwrap();
doc.commit();
let hydrated = ResourceMeta::hydrate_map(&map).unwrap();
assert_eq!(hydrated, meta);
}
#[test]
fn test_derive_struct_missing_fields_use_defaults() {
let doc = LoroDoc::new();
let map = doc.get_map("root");
map.insert("name", "Test").unwrap();
doc.commit();
let hydrated = ResourceMeta::hydrate_map(&map).unwrap();
assert_eq!(hydrated.id, ""); assert_eq!(hydrated.name, "Test");
assert_eq!(hydrated.description, None); assert_eq!(hydrated.forkable, false); }
#[derive(Debug, Clone, PartialEq, Hydrate, Reconcile)]
struct Nested {
pos: Position,
label: String,
}
#[test]
fn test_derive_nested_struct() {
let doc = LoroDoc::new();
let map = doc.get_map("root");
let nested = Nested {
pos: Position { x: 1.0, y: 2.0 },
label: "origin".to_string(),
};
let reconciler = RootReconciler::new(map.clone());
nested.reconcile(reconciler).unwrap();
doc.commit();
let hydrated = Nested::hydrate_map(&map).unwrap();
assert_eq!(hydrated, nested);
}
#[derive(Debug, Clone, PartialEq, Hydrate, Reconcile)]
struct Wrapper(String);
#[test]
fn test_derive_newtype_struct() {
let doc = LoroDoc::new();
let map = doc.get_map("root");
let val = Wrapper("hello".to_string());
map.insert("w", "hello").unwrap();
doc.commit();
let hydrated: Wrapper = lorosurgeon::hydrate_prop(&map, "w").unwrap();
assert_eq!(hydrated, val);
}
#[derive(Debug, Clone, PartialEq, Hydrate, Reconcile)]
#[loro(root = "config")]
struct Config {
version: i64,
name: String,
}
#[test]
fn test_derive_doc_sync() {
let doc = LoroDoc::new();
let config = Config {
version: 1,
name: "test-doc".to_string(),
};
config.to_doc(&doc).unwrap();
doc.commit();
let hydrated = Config::from_doc(&doc).unwrap();
assert_eq!(hydrated, config);
}
#[derive(Debug, Clone, PartialEq, Hydrate, Reconcile)]
struct WithRename {
#[loro(rename = "pos_x")]
x: f64,
#[loro(rename = "pos_y")]
y: f64,
}
#[test]
fn test_derive_rename() {
let doc = LoroDoc::new();
let map = doc.get_map("root");
let val = WithRename { x: 1.0, y: 2.0 };
let reconciler = RootReconciler::new(map.clone());
val.reconcile(reconciler).unwrap();
doc.commit();
let x: f64 = lorosurgeon::hydrate_prop(&map, "pos_x").unwrap();
let y: f64 = lorosurgeon::hydrate_prop(&map, "pos_y").unwrap();
assert_eq!(x, 1.0);
assert_eq!(y, 2.0);
let hydrated = WithRename::hydrate_map(&map).unwrap();
assert_eq!(hydrated, val);
}
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, Default)]
struct Theme {
primary: String,
font_size: i32,
}
#[derive(Debug, Clone, PartialEq, Hydrate, Reconcile)]
struct WithJson {
name: String,
#[loro(json, default)]
theme: Theme,
}
#[test]
fn test_derive_json_attribute() {
let doc = LoroDoc::new();
let map = doc.get_map("root");
let val = WithJson {
name: "test".to_string(),
theme: Theme {
primary: "blue".to_string(),
font_size: 14,
},
};
let reconciler = RootReconciler::new(map.clone());
val.reconcile(reconciler).unwrap();
doc.commit();
let hydrated = WithJson::hydrate_map(&map).unwrap();
assert_eq!(hydrated, val);
}
#[derive(Debug, Clone, PartialEq, Hydrate, Reconcile)]
enum Color {
Red,
Green,
Blue,
}
#[test]
fn test_derive_unit_enum() {
let doc = LoroDoc::new();
let map = doc.get_map("root");
map.insert("color", "Green").unwrap();
doc.commit();
let hydrated: Color = lorosurgeon::hydrate_prop(&map, "color").unwrap();
assert_eq!(hydrated, Color::Green);
let mut mr = MapReconciler { map: map.clone() };
mr.entry("color2", &Color::Blue).unwrap();
doc.commit();
let hydrated2: Color = lorosurgeon::hydrate_prop(&map, "color2").unwrap();
assert_eq!(hydrated2, Color::Blue);
}
#[derive(Debug, Clone, PartialEq, Hydrate, Reconcile)]
enum Shape {
Circle { radius: f64 },
Rectangle { width: f64, height: f64 },
Point,
}
#[test]
fn test_derive_mixed_enum_named_variant() {
let doc = LoroDoc::new();
let map = doc.get_map("root");
let shape = Shape::Circle { radius: 5.0 };
let reconciler = RootReconciler::new(map.clone());
shape.reconcile(reconciler).unwrap();
doc.commit();
let hydrated = Shape::hydrate_map(&map).unwrap();
assert_eq!(hydrated, shape);
}
#[test]
fn test_derive_mixed_enum_unit_variant() {
let doc = LoroDoc::new();
let map = doc.get_map("root");
let shape = Shape::Point;
let reconciler = RootReconciler::new(map.clone());
shape.reconcile(reconciler).unwrap();
doc.commit();
let hydrated = Shape::hydrate_map(&map).unwrap();
assert_eq!(hydrated, shape);
}
#[test]
fn test_derive_mixed_enum_switch_variant() {
let doc = LoroDoc::new();
let map = doc.get_map("root");
let shape1 = Shape::Circle { radius: 5.0 };
let reconciler = RootReconciler::new(map.clone());
shape1.reconcile(reconciler).unwrap();
doc.commit();
let shape2 = Shape::Rectangle {
width: 10.0,
height: 20.0,
};
let reconciler = RootReconciler::new(map.clone());
shape2.reconcile(reconciler).unwrap();
doc.commit();
let hydrated = Shape::hydrate_map(&map).unwrap();
assert_eq!(hydrated, shape2);
assert!(map.get("Circle").is_none());
}
#[derive(Debug, Clone, PartialEq, Hydrate, Reconcile)]
enum Value {
Int(i64),
Text(String),
Pair(i64, String),
}
#[test]
fn test_derive_tuple_enum_single() {
let doc = LoroDoc::new();
let map = doc.get_map("root");
let val = Value::Int(42);
let reconciler = RootReconciler::new(map.clone());
val.reconcile(reconciler).unwrap();
doc.commit();
let hydrated = Value::hydrate_map(&map).unwrap();
assert_eq!(hydrated, val);
}
#[test]
fn test_derive_tuple_enum_multi() {
let doc = LoroDoc::new();
let map = doc.get_map("root");
let val = Value::Pair(7, "lucky".to_string());
let reconciler = RootReconciler::new(map.clone());
val.reconcile(reconciler).unwrap();
doc.commit();
let hydrated = Value::hydrate_map(&map).unwrap();
assert_eq!(hydrated, val);
}
#[test]
fn test_hashmap_roundtrip() {
let doc = LoroDoc::new();
let map = doc.get_map("root");
let mut data: HashMap<String, i64> = HashMap::new();
data.insert("a".to_string(), 1);
data.insert("b".to_string(), 2);
data.insert("c".to_string(), 3);
let mut mr = MapReconciler { map: map.clone() };
mr.entry("data", &data).unwrap();
doc.commit();
let hydrated: HashMap<String, i64> = lorosurgeon::hydrate_prop(&map, "data").unwrap();
assert_eq!(hydrated, data);
}
#[test]
fn test_hashmap_update_removes_old_keys() {
let doc = LoroDoc::new();
let map = doc.get_map("root");
let mut data1: HashMap<String, i64> = HashMap::new();
data1.insert("a".to_string(), 1);
data1.insert("b".to_string(), 2);
let mut mr = MapReconciler { map: map.clone() };
mr.entry("data", &data1).unwrap();
doc.commit();
let mut data2: HashMap<String, i64> = HashMap::new();
data2.insert("a".to_string(), 10);
data2.insert("c".to_string(), 3);
let mut mr = MapReconciler { map: map.clone() };
mr.entry("data", &data2).unwrap();
doc.commit();
let hydrated: HashMap<String, i64> = lorosurgeon::hydrate_prop(&map, "data").unwrap();
assert_eq!(hydrated, data2);
}
#[derive(Debug, PartialEq, Hydrate, Reconcile)]
#[loro(root = "textdoc")]
struct TextDoc {
#[loro(text)]
content: String,
}
#[test]
fn test_text_attribute_roundtrip() {
let doc = LoroDoc::new();
let state = TextDoc {
content: "Hello, world!".to_string(),
};
state.to_doc(&doc).unwrap();
doc.commit();
let loaded = TextDoc::from_doc(&doc).unwrap();
assert_eq!(loaded, state);
}
#[test]
fn test_text_attribute_update() {
let doc = LoroDoc::new();
let state = TextDoc {
content: "Hello".to_string(),
};
state.to_doc(&doc).unwrap();
doc.commit();
let updated = TextDoc {
content: "Hello, world!".to_string(),
};
updated.to_doc(&doc).unwrap();
doc.commit();
let loaded = TextDoc::from_doc(&doc).unwrap();
assert_eq!(loaded, updated);
}
#[test]
fn test_maybe_missing_absent() {
let doc = LoroDoc::new();
let map = doc.get_map("root");
let result: lorosurgeon::MaybeMissing<String> =
lorosurgeon::hydrate_prop_or_default(&map, "nonexistent").unwrap();
assert!(result.is_missing());
}
#[test]
fn test_maybe_missing_present() {
let doc = LoroDoc::new();
let map = doc.get_map("root");
map.insert("name", "Alice").unwrap();
doc.commit();
let result: lorosurgeon::MaybeMissing<String> =
lorosurgeon::hydrate_prop(&map, "name").unwrap();
assert_eq!(
result,
lorosurgeon::MaybeMissing::Present("Alice".to_string())
);
}
#[derive(Debug, Clone, PartialEq, Hydrate, Reconcile)]
#[loro(root = "data")]
struct CrdtStruct {
name: String,
value: i64,
active: bool,
}
#[test]
fn test_concurrent_field_edits_merge() {
let doc1 = LoroDoc::new();
let initial = CrdtStruct {
name: "initial".to_string(),
value: 0,
active: false,
};
initial.to_doc(&doc1).unwrap();
doc1.commit();
let bytes = doc1.export(loro::ExportMode::snapshot()).unwrap();
let doc2 = LoroDoc::new();
doc2.import_batch(&[bytes.into()]).unwrap();
{
let updated = CrdtStruct {
name: "updated-by-1".to_string(),
value: 0,
active: false,
};
updated.to_doc(&doc1).unwrap();
doc1.commit();
}
{
let updated = CrdtStruct {
name: "initial".to_string(),
value: 42,
active: true,
};
updated.to_doc(&doc2).unwrap();
doc2.commit();
}
let update1 = doc1.export(loro::ExportMode::all_updates()).unwrap();
let update2 = doc2.export(loro::ExportMode::all_updates()).unwrap();
let merged = LoroDoc::new();
merged
.import_batch(&[update1.into(), update2.into()])
.unwrap();
let result = CrdtStruct::from_doc(&merged).unwrap();
assert_eq!(result.value, 42);
assert_eq!(result.active, true);
assert!(
result.name == "updated-by-1" || result.name == "initial",
"name should be one of the two values"
);
}
#[derive(Debug, Clone, PartialEq, Hydrate, Reconcile)]
struct CanvasPosition {
x: f64,
y: f64,
width: f64,
height: f64,
}
#[derive(Debug, Clone, PartialEq, Hydrate, Reconcile)]
struct Element {
id: String,
#[loro(flatten)]
canvas: CanvasPosition,
}
#[test]
fn test_derive_flatten() {
let doc = LoroDoc::new();
let map = doc.get_map("root");
let elem = Element {
id: "e1".to_string(),
canvas: CanvasPosition {
x: 10.0,
y: 20.0,
width: 100.0,
height: 50.0,
},
};
let reconciler = RootReconciler::new(map.clone());
elem.reconcile(reconciler).unwrap();
doc.commit();
let x: f64 = lorosurgeon::hydrate_prop(&map, "x").unwrap();
assert_eq!(x, 10.0);
let hydrated = Element::hydrate_map(&map).unwrap();
assert_eq!(hydrated, elem);
}
#[derive(Debug, Clone, PartialEq, Hydrate, Reconcile)]
struct KeyedItem {
#[key]
id: String,
value: i64,
}
#[derive(Debug, Clone, PartialEq, Hydrate, Reconcile)]
struct PositionalMovable {
#[loro(movable)]
items: Vec<i64>,
}
#[test]
fn test_movable_list_positional_basic() {
let doc = LoroDoc::new();
let map = doc.get_map("root");
let v1 = PositionalMovable {
items: vec![10, 20, 30],
};
let reconciler = RootReconciler::new(map.clone());
v1.reconcile(reconciler).unwrap();
doc.commit();
let hydrated = PositionalMovable::hydrate_map(&map).unwrap();
assert_eq!(hydrated, v1);
let v2 = PositionalMovable {
items: vec![10, 25, 30, 40],
};
let reconciler = RootReconciler::new(map.clone());
v2.reconcile(reconciler).unwrap();
doc.commit();
let hydrated = PositionalMovable::hydrate_map(&map).unwrap();
assert_eq!(hydrated, v2);
}
#[test]
fn test_movable_list_positional_shrink() {
let doc = LoroDoc::new();
let map = doc.get_map("root");
let v1 = PositionalMovable {
items: vec![1, 2, 3, 4, 5],
};
let reconciler = RootReconciler::new(map.clone());
v1.reconcile(reconciler).unwrap();
doc.commit();
let v2 = PositionalMovable { items: vec![1, 2] };
let reconciler = RootReconciler::new(map.clone());
v2.reconcile(reconciler).unwrap();
doc.commit();
let hydrated = PositionalMovable::hydrate_map(&map).unwrap();
assert_eq!(hydrated, v2);
}
#[derive(Debug, Clone, PartialEq, Hydrate, Reconcile)]
struct MovableContainer {
#[loro(movable)]
items: Vec<KeyedItem>,
}
#[test]
fn test_movable_list_keyed_insert_delete() {
let doc = LoroDoc::new();
let map = doc.get_map("root");
let v1 = MovableContainer {
items: vec![
KeyedItem {
id: "a".into(),
value: 1,
},
KeyedItem {
id: "b".into(),
value: 2,
},
KeyedItem {
id: "c".into(),
value: 3,
},
],
};
let reconciler = RootReconciler::new(map.clone());
v1.reconcile(reconciler).unwrap();
doc.commit();
let hydrated = MovableContainer::hydrate_map(&map).unwrap();
assert_eq!(hydrated, v1);
let v2 = MovableContainer {
items: vec![
KeyedItem {
id: "a".into(),
value: 1,
},
KeyedItem {
id: "c".into(),
value: 3,
},
KeyedItem {
id: "d".into(),
value: 4,
},
],
};
let reconciler = RootReconciler::new(map.clone());
v2.reconcile(reconciler).unwrap();
doc.commit();
let hydrated = MovableContainer::hydrate_map(&map).unwrap();
assert_eq!(hydrated, v2);
}
#[test]
fn test_movable_list_keyed_reorder() {
let doc = LoroDoc::new();
let map = doc.get_map("root");
let v1 = MovableContainer {
items: vec![
KeyedItem {
id: "a".into(),
value: 1,
},
KeyedItem {
id: "b".into(),
value: 2,
},
KeyedItem {
id: "c".into(),
value: 3,
},
],
};
let reconciler = RootReconciler::new(map.clone());
v1.reconcile(reconciler).unwrap();
doc.commit();
let v2 = MovableContainer {
items: vec![
KeyedItem {
id: "c".into(),
value: 3,
},
KeyedItem {
id: "b".into(),
value: 2,
},
KeyedItem {
id: "a".into(),
value: 1,
},
],
};
let reconciler = RootReconciler::new(map.clone());
v2.reconcile(reconciler).unwrap();
doc.commit();
let hydrated = MovableContainer::hydrate_map(&map).unwrap();
assert_eq!(hydrated, v2);
}
#[test]
fn test_movable_list_keyed_update_in_place() {
let doc = LoroDoc::new();
let map = doc.get_map("root");
let v1 = MovableContainer {
items: vec![
KeyedItem {
id: "a".into(),
value: 1,
},
KeyedItem {
id: "b".into(),
value: 2,
},
],
};
let reconciler = RootReconciler::new(map.clone());
v1.reconcile(reconciler).unwrap();
doc.commit();
let v2 = MovableContainer {
items: vec![
KeyedItem {
id: "a".into(),
value: 10,
},
KeyedItem {
id: "b".into(),
value: 20,
},
],
};
let reconciler = RootReconciler::new(map.clone());
v2.reconcile(reconciler).unwrap();
doc.commit();
let hydrated = MovableContainer::hydrate_map(&map).unwrap();
assert_eq!(hydrated, v2);
}
#[test]
fn test_movable_list_keyed_concurrent_reorder_merge() {
let doc1 = LoroDoc::new();
let map1 = doc1.get_map("root");
let initial = MovableContainer {
items: vec![
KeyedItem {
id: "a".into(),
value: 1,
},
KeyedItem {
id: "b".into(),
value: 2,
},
KeyedItem {
id: "c".into(),
value: 3,
},
],
};
let reconciler = RootReconciler::new(map1.clone());
initial.reconcile(reconciler).unwrap();
doc1.commit();
let bytes = doc1.export(loro::ExportMode::snapshot()).unwrap();
let doc2 = LoroDoc::new();
doc2.import_batch(&[bytes.into()]).unwrap();
let map2 = doc2.get_map("root");
let v1 = MovableContainer {
items: vec![
KeyedItem {
id: "a".into(),
value: 100,
},
KeyedItem {
id: "b".into(),
value: 2,
},
KeyedItem {
id: "c".into(),
value: 3,
},
],
};
let reconciler = RootReconciler::new(map1.clone());
v1.reconcile(reconciler).unwrap();
doc1.commit();
let v2 = MovableContainer {
items: vec![
KeyedItem {
id: "a".into(),
value: 1,
},
KeyedItem {
id: "b".into(),
value: 2,
},
KeyedItem {
id: "c".into(),
value: 300,
},
],
};
let reconciler = RootReconciler::new(map2.clone());
v2.reconcile(reconciler).unwrap();
doc2.commit();
let update1 = doc1.export(loro::ExportMode::all_updates()).unwrap();
let update2 = doc2.export(loro::ExportMode::all_updates()).unwrap();
let merged = LoroDoc::new();
merged
.import_batch(&[update1.into(), update2.into()])
.unwrap();
let result = MovableContainer::hydrate_map(&merged.get_map("root")).unwrap();
assert_eq!(result.items.len(), 3);
let a = result.items.iter().find(|i| i.id == "a").unwrap();
let c = result.items.iter().find(|i| i.id == "c").unwrap();
assert_eq!(a.value, 100);
assert_eq!(c.value, 300);
}
#[test]
fn test_movable_list_empty_to_nonempty() {
let doc = LoroDoc::new();
let map = doc.get_map("root");
let v1 = MovableContainer { items: vec![] };
let reconciler = RootReconciler::new(map.clone());
v1.reconcile(reconciler).unwrap();
doc.commit();
let hydrated = MovableContainer::hydrate_map(&map).unwrap();
assert_eq!(hydrated.items.len(), 0);
let v2 = MovableContainer {
items: vec![KeyedItem {
id: "x".into(),
value: 42,
}],
};
let reconciler = RootReconciler::new(map.clone());
v2.reconcile(reconciler).unwrap();
doc.commit();
let hydrated = MovableContainer::hydrate_map(&map).unwrap();
assert_eq!(hydrated, v2);
}
#[test]
fn test_movable_list_nonempty_to_empty() {
let doc = LoroDoc::new();
let map = doc.get_map("root");
let v1 = MovableContainer {
items: vec![
KeyedItem {
id: "a".into(),
value: 1,
},
KeyedItem {
id: "b".into(),
value: 2,
},
],
};
let reconciler = RootReconciler::new(map.clone());
v1.reconcile(reconciler).unwrap();
doc.commit();
let v2 = MovableContainer { items: vec![] };
let reconciler = RootReconciler::new(map.clone());
v2.reconcile(reconciler).unwrap();
doc.commit();
let hydrated = MovableContainer::hydrate_map(&map).unwrap();
assert_eq!(hydrated.items.len(), 0);
}
#[test]
fn test_noop_detection_no_history_bloat() {
let doc = LoroDoc::new();
let map = doc.get_map("root");
let state = Position { x: 10.0, y: 20.0 };
let reconciler = RootReconciler::new(map.clone());
state.reconcile(reconciler).unwrap();
doc.commit();
let version_after_first = doc.oplog_vv();
let reconciler = RootReconciler::new(map.clone());
state.reconcile(reconciler).unwrap();
doc.commit();
let version_after_second = doc.oplog_vv();
assert_eq!(
version_after_first, version_after_second,
"Reconciling identical values should not create new CRDT operations"
);
}
#[test]
fn test_noop_detection_partial_change() {
let doc = LoroDoc::new();
let map = doc.get_map("root");
let state1 = Position { x: 10.0, y: 20.0 };
let reconciler = RootReconciler::new(map.clone());
state1.reconcile(reconciler).unwrap();
doc.commit();
let version_after_first = doc.oplog_vv();
let state2 = Position { x: 10.0, y: 30.0 };
let reconciler = RootReconciler::new(map.clone());
state2.reconcile(reconciler).unwrap();
doc.commit();
let version_after_second = doc.oplog_vv();
assert_ne!(
version_after_first, version_after_second,
"Changed values should create new CRDT operations"
);
let hydrated = Position::hydrate_map(&map).unwrap();
assert_eq!(hydrated, state2);
}
#[test]
fn test_version_guard_no_change() {
let doc = LoroDoc::new();
let guard = lorosurgeon::VersionGuard::capture(&doc);
assert!(guard.check(&doc).is_ok());
}
#[test]
fn test_version_guard_detects_concurrent_commit() {
let doc = LoroDoc::new();
let config = Config {
version: 1,
name: "initial".to_string(),
};
config.to_doc(&doc).unwrap();
doc.commit();
let guard = lorosurgeon::VersionGuard::capture(&doc);
let map = doc.get_map("config");
map.insert("name", "concurrent-change").unwrap();
doc.commit();
let result = guard.check(&doc);
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
lorosurgeon::ReconcileError::StaleHeads
));
}
#[test]
fn test_version_guard_workflow() {
let doc = LoroDoc::new();
let initial = Config {
version: 1,
name: "test".to_string(),
};
initial.to_doc(&doc).unwrap();
doc.commit();
let guard = lorosurgeon::VersionGuard::capture(&doc);
let mut state = Config::from_doc(&doc).unwrap();
state.version = 2;
state.name = "updated".to_string();
guard.check(&doc).unwrap();
state.to_doc(&doc).unwrap();
doc.commit();
let result = Config::from_doc(&doc).unwrap();
assert_eq!(result.version, 2);
assert_eq!(result.name, "updated");
}
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, Default)]
struct StyleConfig {
color: String,
opacity: f64,
}
#[derive(Debug, Clone, PartialEq, Hydrate, Reconcile)]
#[loro(root = "whiteboard")]
struct WhiteboardState {
#[loro(default)]
id: String,
name: String,
#[loro(rename = "desc")]
description: Option<String>,
#[loro(json, default)]
style: StyleConfig,
#[loro(flatten)]
position: Position,
}
#[test]
fn test_full_featured_struct() {
let doc = LoroDoc::new();
let state = WhiteboardState {
id: "wb-1".to_string(),
name: "My Whiteboard".to_string(),
description: Some("A test whiteboard".to_string()),
style: StyleConfig {
color: "red".to_string(),
opacity: 0.8,
},
position: Position { x: 100.0, y: 200.0 },
};
state.to_doc(&doc).unwrap();
doc.commit();
let hydrated = WhiteboardState::from_doc(&doc).unwrap();
assert_eq!(hydrated, state);
}
#[derive(Debug, Clone, PartialEq, Hydrate, Reconcile)]
struct ListContainer {
items: Vec<i64>,
}
#[test]
fn test_list_lcs_basic_roundtrip() {
let doc = LoroDoc::new();
let map = doc.get_map("root");
let v1 = ListContainer {
items: vec![1, 2, 3],
};
let reconciler = RootReconciler::new(map.clone());
v1.reconcile(reconciler).unwrap();
doc.commit();
let hydrated = ListContainer::hydrate_map(&map).unwrap();
assert_eq!(hydrated, v1);
}
#[test]
fn test_list_lcs_append() {
let doc = LoroDoc::new();
let map = doc.get_map("root");
let v1 = ListContainer {
items: vec![1, 2, 3],
};
let reconciler = RootReconciler::new(map.clone());
v1.reconcile(reconciler).unwrap();
doc.commit();
let v2 = ListContainer {
items: vec![1, 2, 3, 4, 5],
};
let reconciler = RootReconciler::new(map.clone());
v2.reconcile(reconciler).unwrap();
doc.commit();
let hydrated = ListContainer::hydrate_map(&map).unwrap();
assert_eq!(hydrated, v2);
}
#[test]
fn test_list_lcs_prepend() {
let doc = LoroDoc::new();
let map = doc.get_map("root");
let v1 = ListContainer {
items: vec![3, 4, 5],
};
let reconciler = RootReconciler::new(map.clone());
v1.reconcile(reconciler).unwrap();
doc.commit();
let v2 = ListContainer {
items: vec![1, 2, 3, 4, 5],
};
let reconciler = RootReconciler::new(map.clone());
v2.reconcile(reconciler).unwrap();
doc.commit();
let hydrated = ListContainer::hydrate_map(&map).unwrap();
assert_eq!(hydrated, v2);
}
#[test]
fn test_list_lcs_middle_insert() {
let doc = LoroDoc::new();
let map = doc.get_map("root");
let v1 = ListContainer {
items: vec![1, 2, 5, 6],
};
let reconciler = RootReconciler::new(map.clone());
v1.reconcile(reconciler).unwrap();
doc.commit();
let v2 = ListContainer {
items: vec![1, 2, 3, 4, 5, 6],
};
let reconciler = RootReconciler::new(map.clone());
v2.reconcile(reconciler).unwrap();
doc.commit();
let hydrated = ListContainer::hydrate_map(&map).unwrap();
assert_eq!(hydrated, v2);
}
#[test]
fn test_list_lcs_delete() {
let doc = LoroDoc::new();
let map = doc.get_map("root");
let v1 = ListContainer {
items: vec![1, 2, 3, 4, 5],
};
let reconciler = RootReconciler::new(map.clone());
v1.reconcile(reconciler).unwrap();
doc.commit();
let v2 = ListContainer {
items: vec![1, 2, 4, 5],
};
let reconciler = RootReconciler::new(map.clone());
v2.reconcile(reconciler).unwrap();
doc.commit();
let hydrated = ListContainer::hydrate_map(&map).unwrap();
assert_eq!(hydrated, v2);
}
#[test]
fn test_list_lcs_no_change_no_ops() {
let doc = LoroDoc::new();
let map = doc.get_map("root");
let v1 = ListContainer {
items: vec![1, 2, 3],
};
let reconciler = RootReconciler::new(map.clone());
v1.reconcile(reconciler).unwrap();
doc.commit();
let version_before = doc.oplog_vv();
let reconciler = RootReconciler::new(map.clone());
v1.reconcile(reconciler).unwrap();
doc.commit();
let version_after = doc.oplog_vv();
assert_eq!(
version_before, version_after,
"Identical list should produce zero ops"
);
}
#[test]
fn test_list_lcs_empty_to_nonempty() {
let doc = LoroDoc::new();
let map = doc.get_map("root");
let v1 = ListContainer { items: vec![] };
let reconciler = RootReconciler::new(map.clone());
v1.reconcile(reconciler).unwrap();
doc.commit();
let v2 = ListContainer {
items: vec![1, 2, 3],
};
let reconciler = RootReconciler::new(map.clone());
v2.reconcile(reconciler).unwrap();
doc.commit();
let hydrated = ListContainer::hydrate_map(&map).unwrap();
assert_eq!(hydrated, v2);
}
#[test]
fn test_list_lcs_nonempty_to_empty() {
let doc = LoroDoc::new();
let map = doc.get_map("root");
let v1 = ListContainer {
items: vec![1, 2, 3],
};
let reconciler = RootReconciler::new(map.clone());
v1.reconcile(reconciler).unwrap();
doc.commit();
let v2 = ListContainer { items: vec![] };
let reconciler = RootReconciler::new(map.clone());
v2.reconcile(reconciler).unwrap();
doc.commit();
let hydrated = ListContainer::hydrate_map(&map).unwrap();
assert_eq!(hydrated, v2);
}
#[derive(Debug, Clone, PartialEq, Hydrate, Reconcile)]
struct NestedListContainer {
items: Vec<Position>,
}
#[test]
fn test_list_lcs_nested_structs() {
let doc = LoroDoc::new();
let map = doc.get_map("root");
let v1 = NestedListContainer {
items: vec![
Position { x: 1.0, y: 2.0 },
Position { x: 3.0, y: 4.0 },
Position { x: 5.0, y: 6.0 },
],
};
let reconciler = RootReconciler::new(map.clone());
v1.reconcile(reconciler).unwrap();
doc.commit();
let v2 = NestedListContainer {
items: vec![
Position { x: 3.0, y: 4.0 },
Position { x: 5.0, y: 6.0 },
Position { x: 7.0, y: 8.0 },
],
};
let reconciler = RootReconciler::new(map.clone());
v2.reconcile(reconciler).unwrap();
doc.commit();
let hydrated = NestedListContainer::hydrate_map(&map).unwrap();
assert_eq!(hydrated, v2);
}
#[test]
fn test_box_roundtrip() {
let doc = LoroDoc::new();
let map = doc.get_map("test");
let boxed = Box::new(Position { x: 1.0, y: 2.0 });
let reconciler = lorosurgeon::PropReconciler::map_put(map.clone(), "pos".into());
boxed.reconcile(reconciler).unwrap();
doc.commit();
let hydrated: Box<Position> = lorosurgeon::hydrate_prop(&map, "pos").unwrap();
assert_eq!(*hydrated, Position { x: 1.0, y: 2.0 });
}
#[test]
fn test_cow_roundtrip() {
let doc = LoroDoc::new();
let map = doc.get_map("test");
let cow: Cow<'_, String> = Cow::Owned("hello".to_string());
let reconciler = lorosurgeon::PropReconciler::map_put(map.clone(), "s".into());
cow.reconcile(reconciler).unwrap();
doc.commit();
let hydrated: Cow<'_, String> = lorosurgeon::hydrate_prop(&map, "s").unwrap();
assert_eq!(&*hydrated, "hello");
}
#[test]
fn test_slice_reconcile() {
let doc = LoroDoc::new();
let map = doc.get_map("test");
let items: &[i64] = &[1, 2, 3];
let reconciler = lorosurgeon::PropReconciler::map_put(map.clone(), "list".into());
items.reconcile(reconciler).unwrap();
doc.commit();
let list = match map.get("list").unwrap() {
loro::ValueOrContainer::Container(loro::Container::List(l)) => l,
_ => panic!("expected list"),
};
let result: Vec<i64> = lorosurgeon::hydrate::impls::hydrate_vec_from_list(&list).unwrap();
assert_eq!(result, vec![1, 2, 3]);
}
#[test]
fn test_map_reconciler_entries() {
let doc = LoroDoc::new();
let map = doc.get_map("test");
map.insert("a", 1i64).unwrap();
map.insert("b", 2i64).unwrap();
doc.commit();
let mr = MapReconciler { map: map.clone() };
let entries: HashMap<String, i64> = mr
.entries()
.map(|(k, voc)| {
let v: i64 = lorosurgeon::Hydrate::hydrate(&voc).unwrap();
(k, v)
})
.collect();
assert_eq!(entries.len(), 2);
assert_eq!(entries["a"], 1);
assert_eq!(entries["b"], 2);
}
#[test]
fn test_strip_unexpected_ok() {
let result: Result<i64, lorosurgeon::HydrateError> = Ok(42);
assert_eq!(result.strip_unexpected().unwrap(), Some(42));
}
#[test]
fn test_strip_unexpected_converts_unexpected() {
let result: Result<i64, lorosurgeon::HydrateError> =
Err(lorosurgeon::HydrateError::unexpected("int", "string"));
assert_eq!(result.strip_unexpected().unwrap(), None);
}
#[test]
fn test_strip_unexpected_propagates_other_errors() {
let result: Result<i64, lorosurgeon::HydrateError> =
Err(lorosurgeon::HydrateError::missing("field"));
assert!(result.strip_unexpected().is_err());
}
#[test]
fn test_byte_array_roundtrip() {
let doc = LoroDoc::new();
let map = doc.get_map("test");
let arr = ByteArray::new([0xDE, 0xAD, 0xBE, 0xEF]);
let reconciler = lorosurgeon::PropReconciler::map_put(map.clone(), "hash".into());
arr.reconcile(reconciler).unwrap();
doc.commit();
let hydrated: ByteArray<4> = lorosurgeon::hydrate_prop(&map, "hash").unwrap();
assert_eq!(hydrated, arr);
}
#[test]
fn test_byte_array_wrong_length() {
let doc = LoroDoc::new();
let map = doc.get_map("test");
let arr = ByteArray::new([1, 2, 3, 4]);
let reconciler = lorosurgeon::PropReconciler::map_put(map.clone(), "data".into());
arr.reconcile(reconciler).unwrap();
doc.commit();
let result: Result<ByteArray<8>, _> = lorosurgeon::hydrate_prop(&map, "data");
assert!(result.is_err());
}
#[test]
fn test_str_ref_reconcile() {
let doc = LoroDoc::new();
let map = doc.get_map("test");
let s: &str = "hello world";
let reconciler = lorosurgeon::PropReconciler::map_put(map.clone(), "msg".into());
s.reconcile(reconciler).unwrap();
doc.commit();
let hydrated: String = lorosurgeon::hydrate_prop(&map, "msg").unwrap();
assert_eq!(hydrated, "hello world");
}
#[derive(Debug, Clone, PartialEq, Hydrate, Reconcile)]
enum KeyedShape {
Circle {
#[key]
id: String,
radius: f64,
},
Rectangle {
#[key]
id: String,
width: f64,
height: f64,
},
Point,
}
#[derive(Debug, Clone, PartialEq, Hydrate, Reconcile)]
struct KeyedShapeContainer {
#[loro(movable)]
shapes: Vec<KeyedShape>,
}
#[test]
fn test_enum_key_basic_roundtrip() {
let doc = LoroDoc::new();
let map = doc.get_map("root");
let v1 = KeyedShapeContainer {
shapes: vec![
KeyedShape::Circle {
id: "c1".into(),
radius: 5.0,
},
KeyedShape::Rectangle {
id: "r1".into(),
width: 10.0,
height: 20.0,
},
KeyedShape::Point,
],
};
let reconciler = RootReconciler::new(map.clone());
v1.reconcile(reconciler).unwrap();
doc.commit();
let hydrated = KeyedShapeContainer::hydrate_map(&map).unwrap();
assert_eq!(hydrated, v1);
}
#[test]
fn test_enum_key_reorder_preserves_identity() {
let doc = LoroDoc::new();
let map = doc.get_map("root");
let v1 = KeyedShapeContainer {
shapes: vec![
KeyedShape::Circle {
id: "c1".into(),
radius: 5.0,
},
KeyedShape::Rectangle {
id: "r1".into(),
width: 10.0,
height: 20.0,
},
],
};
let reconciler = RootReconciler::new(map.clone());
v1.reconcile(reconciler).unwrap();
doc.commit();
let v2 = KeyedShapeContainer {
shapes: vec![
KeyedShape::Rectangle {
id: "r1".into(),
width: 30.0,
height: 40.0,
},
KeyedShape::Circle {
id: "c1".into(),
radius: 15.0,
},
],
};
let reconciler = RootReconciler::new(map.clone());
v2.reconcile(reconciler).unwrap();
doc.commit();
let hydrated = KeyedShapeContainer::hydrate_map(&map).unwrap();
assert_eq!(hydrated, v2);
}
#[test]
fn test_enum_key_insert_delete() {
let doc = LoroDoc::new();
let map = doc.get_map("root");
let v1 = KeyedShapeContainer {
shapes: vec![
KeyedShape::Circle {
id: "c1".into(),
radius: 5.0,
},
KeyedShape::Circle {
id: "c2".into(),
radius: 10.0,
},
],
};
let reconciler = RootReconciler::new(map.clone());
v1.reconcile(reconciler).unwrap();
doc.commit();
let v2 = KeyedShapeContainer {
shapes: vec![
KeyedShape::Circle {
id: "c2".into(),
radius: 10.0,
},
KeyedShape::Rectangle {
id: "r1".into(),
width: 5.0,
height: 5.0,
},
],
};
let reconciler = RootReconciler::new(map.clone());
v2.reconcile(reconciler).unwrap();
doc.commit();
let hydrated = KeyedShapeContainer::hydrate_map(&map).unwrap();
assert_eq!(hydrated, v2);
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct MyId(String);
impl From<String> for MyId {
fn from(s: String) -> Self {
MyId(s)
}
}
impl std::fmt::Display for MyId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
#[test]
fn test_flexible_hashmap_keys() {
let doc = LoroDoc::new();
let map = doc.get_map("test");
let inner = map
.get_or_create_container("data", loro::LoroMap::new())
.unwrap();
inner.insert("key1", "value1").unwrap();
inner.insert("key2", "value2").unwrap();
doc.commit();
let result: HashMap<MyId, String> = lorosurgeon::hydrate_keyed_map(&inner).unwrap();
assert_eq!(result.len(), 2);
assert_eq!(result[&MyId("key1".into())], "value1");
assert_eq!(result[&MyId("key2".into())], "value2");
let new_map: HashMap<MyId, String> = [(MyId("key1".into()), "updated".into())].into();
let reconciler = lorosurgeon::PropReconciler::map_put(map.clone(), "data2".into());
lorosurgeon::reconcile_keyed_map(&new_map, reconciler).unwrap();
doc.commit();
let inner2 = match map.get("data2").unwrap() {
loro::ValueOrContainer::Container(loro::Container::Map(m)) => m,
_ => panic!("expected map"),
};
let result2: HashMap<MyId, String> = lorosurgeon::hydrate_keyed_map(&inner2).unwrap();
assert_eq!(result2.len(), 1);
assert_eq!(result2[&MyId("key1".into())], "updated");
}
#[allow(clippy::ptr_arg)] mod custom_fns {
use loro::{LoroMap, ValueOrContainer};
use lorosurgeon::{HydrateError, MapReconciler};
pub fn hydrate_upper(map: &LoroMap, key: &str) -> Result<String, HydrateError> {
match map.get(key) {
Some(ValueOrContainer::Value(loro::LoroValue::String(s))) => Ok(s.to_uppercase()),
Some(_) => Err(HydrateError::unexpected("string", "other")),
None => Err(HydrateError::missing(key)),
}
}
pub fn reconcile_lower(
value: &String,
m: &mut MapReconciler,
key: &str,
) -> Result<(), lorosurgeon::ReconcileError> {
m.entry(key, &value.to_lowercase())
}
}
#[allow(clippy::ptr_arg)]
mod custom_module {
use loro::LoroMap;
use lorosurgeon::{HydrateError, MapReconciler};
pub fn hydrate(map: &LoroMap, key: &str) -> Result<String, HydrateError> {
match map.get(key) {
Some(loro::ValueOrContainer::Value(loro::LoroValue::String(s))) => {
Ok(s.chars().rev().collect())
}
Some(_) => Err(HydrateError::unexpected("string", "other")),
None => Err(HydrateError::missing(key)),
}
}
pub fn reconcile(
value: &String,
m: &mut MapReconciler,
key: &str,
) -> Result<(), lorosurgeon::ReconcileError> {
let reversed: String = value.chars().rev().collect();
m.entry(key, &reversed)
}
}
#[derive(Debug, Clone, PartialEq, Hydrate, Reconcile)]
struct CustomHydrateOnly {
#[loro(hydrate = "custom_fns::hydrate_upper")]
name: String,
}
#[derive(Debug, Clone, PartialEq, Hydrate, Reconcile)]
struct CustomReconcileOnly {
#[loro(reconcile = "custom_fns::reconcile_lower")]
name: String,
}
#[derive(Debug, Clone, PartialEq, Hydrate, Reconcile)]
struct CustomBoth {
#[loro(
hydrate = "custom_fns::hydrate_upper",
reconcile = "custom_fns::reconcile_lower"
)]
name: String,
}
#[derive(Debug, Clone, PartialEq, Hydrate, Reconcile)]
struct CustomWithModule {
#[loro(with = "custom_module")]
label: String,
}
#[test]
fn test_custom_hydrate_fn() {
let doc = LoroDoc::new();
let map = doc.get_map("root");
map.insert("name", "hello").unwrap();
doc.commit();
let result = CustomHydrateOnly::hydrate_map(&map).unwrap();
assert_eq!(result.name, "HELLO");
}
#[test]
fn test_custom_reconcile_fn() {
let doc = LoroDoc::new();
let map = doc.get_map("root");
let val = CustomReconcileOnly {
name: "HELLO".to_string(),
};
let reconciler = RootReconciler::new(map.clone());
val.reconcile(reconciler).unwrap();
doc.commit();
let raw: String = lorosurgeon::hydrate_prop(&map, "name").unwrap();
assert_eq!(raw, "hello");
}
#[test]
fn test_custom_hydrate_and_reconcile() {
let doc = LoroDoc::new();
let map = doc.get_map("root");
let val = CustomBoth {
name: "Hello".to_string(),
};
let reconciler = RootReconciler::new(map.clone());
val.reconcile(reconciler).unwrap();
doc.commit();
let raw: String = lorosurgeon::hydrate_prop(&map, "name").unwrap();
assert_eq!(raw, "hello");
let hydrated = CustomBoth::hydrate_map(&map).unwrap();
assert_eq!(hydrated.name, "HELLO");
}
#[test]
fn test_custom_with_module() {
let doc = LoroDoc::new();
let map = doc.get_map("root");
let val = CustomWithModule {
label: "abc".to_string(),
};
let reconciler = RootReconciler::new(map.clone());
val.reconcile(reconciler).unwrap();
doc.commit();
let raw: String = lorosurgeon::hydrate_prop(&map, "label").unwrap();
assert_eq!(raw, "cba");
let hydrated = CustomWithModule::hydrate_map(&map).unwrap();
assert_eq!(hydrated.label, "abc");
}
#[derive(Debug, Clone, PartialEq, Hydrate, Reconcile)]
struct Dimensions {
width: f64,
height: f64,
}
#[derive(Debug, Clone, PartialEq, Hydrate, Reconcile)]
struct Origin {
x: f64,
y: f64,
}
#[derive(Debug, Clone, PartialEq, Hydrate, Reconcile)]
struct MultiFlattened {
name: String,
#[loro(flatten)]
origin: Origin,
#[loro(flatten)]
size: Dimensions,
}
#[test]
fn test_flatten_multiple() {
let doc = LoroDoc::new();
let map = doc.get_map("root");
let val = MultiFlattened {
name: "rect".to_string(),
origin: Origin { x: 5.0, y: 10.0 },
size: Dimensions {
width: 100.0,
height: 50.0,
},
};
let reconciler = RootReconciler::new(map.clone());
val.reconcile(reconciler).unwrap();
doc.commit();
let x: f64 = lorosurgeon::hydrate_prop(&map, "x").unwrap();
let y: f64 = lorosurgeon::hydrate_prop(&map, "y").unwrap();
let w: f64 = lorosurgeon::hydrate_prop(&map, "width").unwrap();
let h: f64 = lorosurgeon::hydrate_prop(&map, "height").unwrap();
assert_eq!(x, 5.0);
assert_eq!(y, 10.0);
assert_eq!(w, 100.0);
assert_eq!(h, 50.0);
let hydrated = MultiFlattened::hydrate_map(&map).unwrap();
assert_eq!(hydrated, val);
}
#[derive(Debug, Clone, PartialEq, Hydrate, Reconcile)]
struct FlattenWithMissing {
name: String,
#[loro(flatten)]
pos: PositionWithDefaults,
}
#[derive(Debug, Clone, PartialEq, Hydrate, Reconcile)]
struct PositionWithDefaults {
#[loro(default)]
x: f64,
#[loro(default)]
y: f64,
#[loro(default)]
rotation: f64,
}
#[test]
fn test_flatten_with_missing_defaults() {
let doc = LoroDoc::new();
let map = doc.get_map("root");
map.insert("name", "test").unwrap();
doc.commit();
let hydrated = FlattenWithMissing::hydrate_map(&map).unwrap();
assert_eq!(hydrated.name, "test");
assert_eq!(hydrated.pos.x, 0.0);
assert_eq!(hydrated.pos.y, 0.0);
assert_eq!(hydrated.pos.rotation, 0.0);
}
#[test]
fn test_flatten_roundtrip_update() {
let doc = LoroDoc::new();
let map = doc.get_map("root");
let v1 = MultiFlattened {
name: "rect".to_string(),
origin: Origin { x: 0.0, y: 0.0 },
size: Dimensions {
width: 50.0,
height: 25.0,
},
};
let reconciler = RootReconciler::new(map.clone());
v1.reconcile(reconciler).unwrap();
doc.commit();
let v2 = MultiFlattened {
name: "rect".to_string(),
origin: Origin { x: 10.0, y: 20.0 },
size: Dimensions {
width: 50.0,
height: 25.0,
},
};
let reconciler = RootReconciler::new(map.clone());
v2.reconcile(reconciler).unwrap();
doc.commit();
let hydrated = MultiFlattened::hydrate_map(&map).unwrap();
assert_eq!(hydrated, v2);
}
#[derive(Debug, Clone, PartialEq, Hydrate, Reconcile)]
struct GenericWrapper<
T: lorosurgeon::Hydrate + lorosurgeon::Reconcile + std::fmt::Debug + Clone + PartialEq,
> {
inner: T,
label: String,
}
#[test]
fn test_generic_struct_with_scalar() {
let doc = LoroDoc::new();
let map = doc.get_map("root");
let val = GenericWrapper {
inner: 42i64,
label: "test".to_string(),
};
let reconciler = RootReconciler::new(map.clone());
val.reconcile(reconciler).unwrap();
doc.commit();
let hydrated = GenericWrapper::<i64>::hydrate_map(&map).unwrap();
assert_eq!(hydrated, val);
}
#[test]
fn test_generic_struct_nested() {
let doc = LoroDoc::new();
let map = doc.get_map("root");
let val = GenericWrapper {
inner: Position { x: 1.0, y: 2.0 },
label: "pos".to_string(),
};
let reconciler = RootReconciler::new(map.clone());
val.reconcile(reconciler).unwrap();
doc.commit();
let hydrated = GenericWrapper::<Position>::hydrate_map(&map).unwrap();
assert_eq!(hydrated, val);
}
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
enum JsonTheme {
Light,
Dark,
Custom { primary: String, secondary: String },
}
#[derive(Debug, Clone, PartialEq, Hydrate, Reconcile)]
struct DocWithJsonEnum {
name: String,
#[loro(json)]
theme: JsonTheme,
}
#[test]
fn test_enum_json_unit_variant() {
let doc = LoroDoc::new();
let map = doc.get_map("root");
let val = DocWithJsonEnum {
name: "my-doc".to_string(),
theme: JsonTheme::Light,
};
let reconciler = RootReconciler::new(map.clone());
val.reconcile(reconciler).unwrap();
doc.commit();
let hydrated = DocWithJsonEnum::hydrate_map(&map).unwrap();
assert_eq!(hydrated, val);
}
#[test]
fn test_enum_json_data_variant() {
let doc = LoroDoc::new();
let map = doc.get_map("root");
let val = DocWithJsonEnum {
name: "my-doc".to_string(),
theme: JsonTheme::Custom {
primary: "#ff0000".to_string(),
secondary: "#00ff00".to_string(),
},
};
let reconciler = RootReconciler::new(map.clone());
val.reconcile(reconciler).unwrap();
doc.commit();
let hydrated = DocWithJsonEnum::hydrate_map(&map).unwrap();
assert_eq!(hydrated, val);
}
#[derive(Debug, Clone, PartialEq, Hydrate, Reconcile)]
enum Content {
Text { body: String },
Number { value: i64 },
Empty,
}
#[test]
fn test_enum_data_variant_roundtrip() {
let doc = LoroDoc::new();
let map = doc.get_map("root");
let val = Content::Text {
body: "hello".to_string(),
};
let reconciler = lorosurgeon::PropReconciler::map_put(map.clone(), "v".to_string());
val.reconcile(reconciler).unwrap();
doc.commit();
let hydrated: Content = lorosurgeon::Hydrate::hydrate(&map.get("v").unwrap()).unwrap();
assert_eq!(hydrated, val);
}
#[test]
fn test_enum_unit_variant_roundtrip() {
let doc = LoroDoc::new();
let map = doc.get_map("root");
let val = Content::Empty;
let reconciler = lorosurgeon::PropReconciler::map_put(map.clone(), "v".to_string());
val.reconcile(reconciler).unwrap();
doc.commit();
let hydrated: Content = lorosurgeon::Hydrate::hydrate(&map.get("v").unwrap()).unwrap();
assert_eq!(hydrated, val);
}
#[test]
fn test_enum_switch_variant() {
let doc = LoroDoc::new();
let map = doc.get_map("root");
let val1 = Content::Text {
body: "hello".to_string(),
};
let reconciler = lorosurgeon::PropReconciler::map_put(map.clone(), "v".to_string());
val1.reconcile(reconciler).unwrap();
doc.commit();
let val2 = Content::Number { value: 42 };
let reconciler = lorosurgeon::PropReconciler::map_put(map.clone(), "v".to_string());
val2.reconcile(reconciler).unwrap();
doc.commit();
let hydrated: Content = lorosurgeon::Hydrate::hydrate(&map.get("v").unwrap()).unwrap();
assert_eq!(hydrated, val2);
}
#[derive(Debug, Clone, PartialEq, Hydrate, Reconcile)]
struct BoxedFields {
#[loro(default)]
name: Box<String>,
value: i64,
}
#[test]
fn test_boxed_field_roundtrip() {
let doc = LoroDoc::new();
let map = doc.get_map("root");
let val = BoxedFields {
name: Box::new("boxed".to_string()),
value: 99,
};
let reconciler = RootReconciler::new(map.clone());
val.reconcile(reconciler).unwrap();
doc.commit();
let hydrated = BoxedFields::hydrate_map(&map).unwrap();
assert_eq!(hydrated, val);
}
#[derive(Debug, Clone, PartialEq, Hydrate, Reconcile)]
enum ListContent {
Text { body: String },
Items { items: Vec<String> },
Empty,
}
#[test]
fn test_enum_with_vec_field() {
let doc = LoroDoc::new();
let map = doc.get_map("root");
let val = ListContent::Items {
items: vec!["a".to_string(), "b".to_string(), "c".to_string()],
};
let reconciler = lorosurgeon::PropReconciler::map_put(map.clone(), "v".to_string());
val.reconcile(reconciler).unwrap();
doc.commit();
let hydrated: ListContent = lorosurgeon::Hydrate::hydrate(&map.get("v").unwrap()).unwrap();
assert_eq!(hydrated, val);
}
#[test]
fn test_enum_with_vec_field_switch() {
let doc = LoroDoc::new();
let map = doc.get_map("root");
let val1 = ListContent::Items {
items: vec!["x".to_string()],
};
let reconciler = lorosurgeon::PropReconciler::map_put(map.clone(), "v".to_string());
val1.reconcile(reconciler).unwrap();
doc.commit();
let val2 = ListContent::Text {
body: "hello".to_string(),
};
let reconciler = lorosurgeon::PropReconciler::map_put(map.clone(), "v".to_string());
val2.reconcile(reconciler).unwrap();
doc.commit();
let hydrated: ListContent = lorosurgeon::Hydrate::hydrate(&map.get("v").unwrap()).unwrap();
assert_eq!(hydrated, val2);
}
#[derive(Debug, Clone, PartialEq, Hydrate, Reconcile)]
struct IdList(Vec<String>);
#[test]
fn test_newtype_over_vec() {
let doc = LoroDoc::new();
let map = doc.get_map("root");
let val = IdList(vec!["a".to_string(), "b".to_string(), "c".to_string()]);
let reconciler = lorosurgeon::PropReconciler::map_put(map.clone(), "ids".to_string());
val.reconcile(reconciler).unwrap();
doc.commit();
let hydrated: IdList = lorosurgeon::Hydrate::hydrate(&map.get("ids").unwrap()).unwrap();
assert_eq!(hydrated, val);
}
#[test]
fn test_newtype_over_vec_empty() {
let doc = LoroDoc::new();
let map = doc.get_map("root");
let val = IdList(vec![]);
let reconciler = lorosurgeon::PropReconciler::map_put(map.clone(), "ids".to_string());
val.reconcile(reconciler).unwrap();
doc.commit();
let hydrated: IdList = lorosurgeon::Hydrate::hydrate(&map.get("ids").unwrap()).unwrap();
assert_eq!(hydrated, val);
}
#[derive(Debug, Clone, PartialEq, Hydrate, Reconcile)]
struct DocWithIdList {
name: String,
tags: IdList,
}
#[test]
fn test_newtype_vec_as_field() {
let doc = LoroDoc::new();
let map = doc.get_map("root");
let val = DocWithIdList {
name: "test".to_string(),
tags: IdList(vec!["rust".to_string(), "loro".to_string()]),
};
let reconciler = RootReconciler::new(map.clone());
val.reconcile(reconciler).unwrap();
doc.commit();
let hydrated = DocWithIdList::hydrate_map(&map).unwrap();
assert_eq!(hydrated, val);
}