1 unstable release
Uses new Rust 2024
| new 0.1.0 | Feb 7, 2026 |
|---|
#368 in Encoding
460KB
10K
SLoC
Automorph
Bidirectional synchronization between Rust types and Automerge documents.
Automorph works like Serde - 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.
All features documented below are validated by automated tests. Test names are noted in parentheses.
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
(validated by: test_derived_struct)
Add to your Cargo.toml:
[dependencies]
automorph = "0.1"
automerge = "0.7"
Then derive Automorph on your types:
use automorph::{Automorph, Result};
use automerge::{AutoCommit, ROOT};
#[derive(Automorph, Debug, PartialEq, Default, Clone)]
struct Person {
name: String,
age: u64,
}
fn main() -> Result<()> {
// Create an Automerge document
let mut doc = AutoCommit::new();
// Save a struct to the document
let person = Person {
name: "Alice".to_string(),
age: 30,
};
person.save(&mut doc, &ROOT, "person")?;
// Load it back
let restored = Person::load(&doc, &ROOT, "person")?;
assert_eq!(person, restored);
// Efficient updates - only changed fields are written
let mut updated = person.clone();
updated.age = 31;
updated.save(&mut doc, &ROOT, "person")?; // Only writes the age change
Ok(())
}
Features
Efficient Diff-Based Updates
(validated by: test_diff_detects_changes, test_diff_no_changes_when_equal)
Automorph only writes values that have changed:
person.age = 31;
person.save(&mut doc, &ROOT, "person")?;
// Only the 'age' field generates an Automerge operation
Version-Aware Operations
(validated by: test_derived_version_aware, test_diff_versions)
Access historical versions with *_at methods:
// Save state
person.save(&mut doc, &ROOT, "person")?;
let checkpoint = doc.get_heads();
// Make changes
person.age = 32;
person.save(&mut doc, &ROOT, "person")?;
// Load from checkpoint (time travel!)
let old_person = Person::load_at(&doc, &ROOT, "person", &checkpoint)?;
assert_eq!(old_person.age, 31);
Change Detection
(validated by: test_update_returns_change_report, test_hierarchical_change_tracking, test_change_report_paths)
Detect and inspect changes with update and diff:
// Update returns a ChangeReport showing what changed
let changes = person.update(&doc, &ROOT, "person")?;
if changes.any() {
println!("Changed fields: {:?}", changes.leaf_paths());
}
Comprehensive Type Support
Automorph supports all types that Serde does:
- Primitives:
bool,i8-i128,u8-u128,f32,f64,char - Strings:
String,&str,Box<str>,Cow<str> - Collections:
Vec,HashMap,BTreeMap,HashSet,BTreeSet - Options:
Option<T>,Result<T, E> - Tuples: up to 16 elements
- Smart pointers:
Box,Rc,Arc,Cell,RefCell - Time:
Duration,SystemTime - Network:
IpAddr,SocketAddr - And more: ranges, paths, NonZero types...
Derive Macro Attributes
Container Attributes
(validated by: test_rename_all, test_internally_tagged_enum)
#[derive(Automorph)]
#[automorph(rename_all = "camelCase")] // Rename all fields
struct Config {
user_name: String, // Stored as "userName"
}
#[derive(Automorph)]
#[automorph(tag = "type")] // Internal tagging for enums
enum Message {
Text { content: String },
Image { url: String },
}
// Stored as: {"type": "Text", "content": "Hello"}
Field Attributes
(validated by: test_field_rename, test_skip_field, test_default_field)
#[derive(Automorph)]
struct User {
#[automorph(rename = "id")]
user_id: u64,
#[automorph(skip)] // Don't sync this field
cache: Option<Vec<u8>>,
#[automorph(default)] // Use Default if missing
score: u32,
}
Comparison with Serde
| Feature | Serde | Automorph |
|---|---|---|
| Derive macro | #[derive(Serialize, Deserialize)] |
#[derive(Automorph)] |
| Field rename | #[serde(rename)] |
#[automorph(rename)] |
| Skip field | #[serde(skip)] |
#[automorph(skip)] |
| Default value | #[serde(default)] |
#[automorph(default)] |
| Bidirectional | Needs both traits | Single trait |
| Diff-based | No | Yes - only writes changes |
| Change tracking | No | Yes - diff() and update() |
| CRDT-aware | No | Yes - preserves Automerge semantics |
Documentation
- Getting Started - Quick tutorial to get up and running
- Cookbook - Common patterns and recipes
- Architecture - How Automorph works internally
Examples & Demo
Learn Automorph through hands-on examples:
-
Examples - Single-file examples covering individual features:
collaborative.rs- Tracked, versioning, change detectioncrdt_collaboration.rs- Counter and Text CRDT typespersistence.rs- File storage patternssync_tcp.rs- TCP sync protocolnotes_tutorial.rs- Complete CLI notes app
-
Demo Application - Full-featured Yew/WASM web application:
- Collaborative todo list and chat
- WebSocket sync between browsers
- Docker Compose deployment
- Production-ready patterns
Start with the examples to learn individual concepts, then study the demo for production patterns.
License
Automorph is dual-licensed under:
- Apache License 2.0 (LICENSE-APACHE)
- MIT License (LICENSE-MIT)
You may use this software under either license at your option.
Related projects
- Automerge
- Autosurgeon -- Automorph provides granular change tracking vs. Autosurgeon's serialization/deserialization functionality.
Contributing
Contributions are welcome! Please see CONTRIBUTING.md for guidelines.
Dependencies
~9–13MB
~183K SLoC