# Automorph
[](https://crates.io/crates/automorph)
[](https://docs.rs/automorph)
[](LICENSE-APACHE)
**Bidirectional synchronization between Rust types and [Automerge](https://automerge.org/) documents.**
Automorph works like [Serde](https://serde.rs/) - 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](https://automerge.org/) 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`:
```toml
[dependencies]
automorph = "0.1"
automerge = "0.7"
```
Then derive `Automorph` on your types:
```rust
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:
```rust
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:
```rust
// 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`:
```rust
// 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`)*
```rust
#[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`)*
```rust
#[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
| 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](docs/getting-started.md)** - Quick tutorial to get up and running
- **[Cookbook](docs/cookbook.md)** - Common patterns and recipes
- **[Architecture](docs/architecture.md)** - How Automorph works internally
## Examples & Demo
Learn Automorph through hands-on examples:
- **[Examples](examples/README.md)** - Single-file examples covering individual features:
- `collaborative.rs` - Tracked<T>, versioning, change detection
- `crdt_collaboration.rs` - Counter and Text CRDT types
- `persistence.rs` - File storage patterns
- `sync_tcp.rs` - TCP sync protocol
- `notes_tutorial.rs` - Complete CLI notes app
- **[Demo Application](demo/README.md)** - 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](LICENSE-APACHE))
- MIT License ([LICENSE-MIT](LICENSE-MIT))
You may use this software under either license at your option.
## Related projects
* [Automerge](https://github.com/automerge)
* [Autosurgeon](https://github.com/automerge/autosurgeon) -- Automorph provides granular change tracking vs. Autosurgeon's serialization/deserialization functionality.
## Contributing
Contributions are welcome! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.