Derive macros for bidirectional serialization between Rust types and Loro CRDT containers — the Loro equivalent of autosurgeon for Automerge.
#[derive(Hydrate, Reconcile)] generates field-level mapping between Rust types and Loro containers. Only modified fields produce CRDT operations.
use loro::LoroDoc;
use lorosurgeon::{Hydrate, Reconcile, DocSync};
#[derive(Debug, PartialEq, Hydrate, Reconcile)]
#[loro(root = "config")]
struct Config {
name: String,
version: i64,
position: Position,
}
#[derive(Debug, PartialEq, Hydrate, Reconcile)]
struct Position { x: f64, y: f64 }
let doc = LoroDoc::new();
let config = Config {
name: "hello".into(),
version: 1,
position: Position { x: 10.0, y: 20.0 },
};
config.to_doc(&doc).unwrap(); // Rust → Loro
doc.commit();
let loaded = Config::from_doc(&doc).unwrap(); // Loro → Rust
assert_eq!(loaded, config);Full API documentation on docs.rs →
The crate docs include type mapping tables, attribute reference, examples for concurrent editing, custom serialization, flatten, keyed list diffing, and more.
- Structs →
LoroMap(fields become keys) - Enums →
LoroMapwith variant discriminator, unit variants as strings Vec<T>→LoroListwith Myers LCS diffing#[loro(movable)]→LoroMovableListwith identity-preservingmov()/set()HashMap<String, V>→LoroMapwith stale-key cleanup#[loro(text)]→LoroTextwith character-level diffing (onStringfields)- No-op detection — identical values produce zero CRDT operations
- Concurrent merge — field-level granularity means independent edits compose
// Container-level
#[loro(root = "key")] // DocSync: to_doc() / from_doc()
// Field-level
#[key] // Identity key for movable list diffing
#[loro(rename = "name")] // Different key in Loro
#[loro(json)] // serde_json round-trip
#[loro(text)] // LoroText with character-level LCS
#[loro(movable)] // LoroMovableList instead of LoroList
#[loro(default)] // Default::default() when absent
#[loro(default = "fn")] // Custom default function
#[loro(flatten)] // Inline nested struct fields
#[loro(with = "module")] // Custom hydrate + reconcile
#[loro(hydrate = "fn")] // Custom hydrate only
#[loro(reconcile = "fn")] // Custom reconcile onlyMIT