Expand description
§Automorph
Bidirectional synchronization between Rust types and Automerge documents.
Automorph works similarly to Serde - you derive a trait on your structs, and the library handles synchronization automatically. Unlike serialization, Automorph performs efficient diff-based updates, only writing changes to the Automerge document.
§What is Automerge?
Automerge is a Conflict-free Replicated Data Type (CRDT) library that enables automatic merging of concurrent changes without coordination. It’s ideal for:
- Local-first software: Apps that work offline and sync when connected
- Real-time collaboration: Multiple users editing the same document
- Version control for data: Full history with time-travel debugging
§Quick Start
use automorph::{Automorph, Result};
use automerge::{AutoCommit, ROOT};
fn main() -> Result<()> {
// Create an Automerge document
let mut doc = AutoCommit::new();
// Save primitive types to the document
"Alice".to_string().save(&mut doc, &ROOT, "name")?;
30i64.save(&mut doc, &ROOT, "age")?;
// Load them back
let name = String::load(&doc, &ROOT, "name")?;
let age = i64::load(&doc, &ROOT, "age")?;
assert_eq!(name, "Alice");
assert_eq!(age, 30);
Ok(())
}With custom structs (using derive macro):
use automorph::Automorph;
use automerge::{AutoCommit, ROOT};
#[derive(Automorph, Debug, PartialEq, Default, Clone)]
struct Person {
name: String,
age: u64,
}
let mut doc = AutoCommit::new();
let person = Person { name: "Alice".to_string(), age: 30 };
person.save(&mut doc, &ROOT, "person").unwrap();
let restored = Person::load(&doc, &ROOT, "person").unwrap();
assert_eq!(person, restored);§Why Sync Instead of Serialize?
Traditional serialization writes the entire value every time. Automorph:
- Diffs before writing: Only changed values generate Automerge operations
- Preserves history: Every change is tracked, enabling undo/redo
- Enables collaboration: Changes merge automatically across devices
- Supports time-travel: Read values from any point in history
§Features
- Efficient diff-based sync: Only changed values generate Automerge operations
(validated by:
test_diff_detects_changes,test_diff_no_changes_when_equal) - Bidirectional: Sync Rust -> Automerge and Automerge -> Rust
(validated by:
test_derived_struct) - Hierarchical change tracking: Know exactly which fields changed during sync
(validated by:
test_hierarchical_change_tracking,test_change_report_paths) - Version-aware: Read from historical document states with
*_atmethods (validated by:test_derived_version_aware) - Change detection: Use
Tracked<T>to detect document modifications (validated by:test_tracked_load,test_tracked_has_structural_changes,test_tracked_update) - Serde-like attributes:
#[automorph(rename = "...")],#[automorph(skip)],#[automorph(default)](validated by:test_field_rename,test_skip_field,test_default_field) - Comprehensive type support: All primitives, collections, enums, and more
- All enum representations: Externally, internally, adjacently tagged, and untagged
(validated by:
test_enum_unit_variant,test_internally_tagged_enum,test_adjacently_tagged_enum,test_untagged_enum)
§Supported Types
| Category | Types |
|---|---|
| Primitives | bool, i8-i128, u8-u128, f32, f64, char, () |
| Strings | String, Box<str>, Cow<str>, Rc<str>, Arc<str> |
| Bytes | Vec<u8>, Box<[u8]>, [u8; N] |
| Options | Option<T>, Result<T, E> |
| Collections | Vec<T>, VecDeque<T>, LinkedList<T>, HashMap<K, V>, BTreeMap<K, V>, HashSet<T>, BTreeSet<T> |
| Arrays | [T; N] for N=0..32 |
| Smart Pointers | Box<T>, Rc<T>, Arc<T>, Cell<T>, RefCell<T>, Mutex<T>, RwLock<T> |
| Tuples | (T1,) through (T1, ..., T16) |
| Time | Duration, SystemTime |
| Net | IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6 |
| Paths | PathBuf, OsString |
| Wrappers | Wrapping<T>, Saturating<T>, NonZero* |
| Ranges | Range<T>, RangeInclusive<T>, RangeTo<T>, RangeFrom<T> |
| Marker | PhantomData<T> |
§Container Attributes
#[derive(Automorph)]
#[automorph(rename_all = "camelCase")] // Rename all fields (camelCase, snake_case, etc.)
struct MyStruct { ... }(validated by: test_rename_all)
§Field Attributes
#[derive(Automorph)]
struct MyStruct {
#[automorph(skip)] // Skip this field entirely (uses Default on load)
#[automorph(rename = "name")] // Use different key in the document
#[automorph(default)] // Use Default::default() if field is missing
field: Type,
}(validated by: test_skip_field, test_field_rename, test_default_field)
§Enum Representations
// Externally tagged (default): {"Variant": content}
// (validated by: test_enum_unit_variant, test_enum_newtype_variant, test_enum_struct_variant)
#[derive(Automorph)]
enum MyEnum { A(i32), B { x: i32 } }
// Internally tagged: {"type": "Variant", ...fields}
// (validated by: test_internally_tagged_enum, test_internally_tagged_unit)
#[derive(Automorph)]
#[automorph(tag = "type")]
enum MyEnum { A { x: i32 }, B { y: i32 } }
// Adjacently tagged: {"t": "Variant", "c": content}
// (validated by: test_adjacently_tagged_enum, test_adjacently_tagged_int)
#[derive(Automorph)]
#[automorph(tag = "t", content = "c")]
enum MyEnum { A(i32), B(String) }
// Untagged: tries each variant in order
// (validated by: test_untagged_enum)
#[derive(Automorph)]
#[automorph(untagged)]
enum MyEnum { Int(i32), Str(String) }§Change Tracking with Tracked<T>
The Tracked<T> wrapper caches document state to detect when values have been
modified. This is useful for efficiently checking if you need to re-process data.
use automorph::{Tracked, Automorph, Result, ChangeReport};
use automerge::{AutoCommit, ROOT};
#[derive(Automorph, Default, Clone)]
struct Workspace {
name: String,
items: Vec<String>,
}
fn sync_if_changed(doc: &AutoCommit) -> Result<()> {
// Load with change tracking (validated by: test_tracked_load)
let mut workspace = Tracked::load(doc, &ROOT, "workspace")?;
// Later, check if the document has structural changes (validated by: test_tracked_has_structural_changes)
if workspace.has_structural_changes(doc)? {
// Update our local copy and get a change report (validated by: test_tracked_update, test_update_returns_change_report)
let changes = workspace.update(doc)?;
if ChangeReport::any(&changes) {
println!("Workspace was modified!");
}
}
Ok(())
}Note: Tracked<T> detects changes by comparing Automerge object IDs.
It detects when objects are replaced or modified, but for primitive fields
within a struct, it tracks changes at the struct level, not individual fields.
(validated by: test_invariant_tracked_has_changes_detects_document_modification)
§Version-Aware Operations
use automorph::{Automorph, Result};
use automerge::{AutoCommit, ROOT};
fn version_aware_operations(doc: &mut AutoCommit, person: &mut Person) -> Result<()> {
// Save current version
let old_heads = doc.get_heads();
// Make changes
person.name = "Bob".to_string();
person.save(doc, &ROOT, "person")?;
// Read old value (validated by: test_derived_version_aware)
let old_person = Person::load_at(doc, &ROOT, "person", &old_heads)?;
assert_eq!(old_person.name, "Alice");
// Diff between versions (validated by: test_diff_versions)
let changes = Person::diff_versions(doc, &ROOT, "person", &old_heads, &doc.get_heads())?;
Ok(())
}§Learn More
Re-exports§
pub use automerge;
Modules§
- config
- Configuration for Automorph behavior. Configuration for Automorph behavior.
- crdt
- CRDT primitive types that expose Automerge’s collaborative semantics.
- debug
- Debug utilities for inspecting Automerge documents.
Structs§
- Error
- Error type for Automorph operations.
- MapChanges
- Change report for
HashMap<K, V>fields. - MapCursor
- Cursor for
HashMap<K, V>fields. - Primitive
Changed - Change report for primitive types that have no nested fields.
- Scalar
Cursor - Cursor for scalar/primitive types that don’t have nested structure.
- Tracked
- A value loaded from an Automerge document with change tracking.
- VecChanges
- Change report for
Vec<T>fields. - VecCursor
- Cursor for
Vec<T>fields.
Enums§
- Error
Kind - The specific kind of error that occurred.
- Option
Changes - Change report for
Option<T>fields. - Result
Changes - Change report for
Result<T, E>fields.
Traits§
- Automorph
- Primary trait for bidirectional synchronization with Automerge documents.
- Change
Report - Trait for hierarchical change reports.
- Field
Cursor - Trait for cursor types that cache ObjIds for O(1) change detection.
Type Aliases§
- Result
- Result type alias for Automorph operations.
Derive Macros§
- Automorph
- Derive the
Automorphtrait for a struct or enum.