Skip to main content

Crate automorph

Crate automorph 

Source
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:

  1. Diffs before writing: Only changed values generate Automerge operations
  2. Preserves history: Every change is tracked, enabling undo/redo
  3. Enables collaboration: Changes merge automatically across devices
  4. 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 *_at methods (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

CategoryTypes
Primitivesbool, i8-i128, u8-u128, f32, f64, char, ()
StringsString, Box<str>, Cow<str>, Rc<str>, Arc<str>
BytesVec<u8>, Box<[u8]>, [u8; N]
OptionsOption<T>, Result<T, E>
CollectionsVec<T>, VecDeque<T>, LinkedList<T>, HashMap<K, V>, BTreeMap<K, V>, HashSet<T>, BTreeSet<T>
Arrays[T; N] for N=0..32
Smart PointersBox<T>, Rc<T>, Arc<T>, Cell<T>, RefCell<T>, Mutex<T>, RwLock<T>
Tuples(T1,) through (T1, ..., T16)
TimeDuration, SystemTime
NetIpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6
PathsPathBuf, OsString
WrappersWrapping<T>, Saturating<T>, NonZero*
RangesRange<T>, RangeInclusive<T>, RangeTo<T>, RangeFrom<T>
MarkerPhantomData<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.
PrimitiveChanged
Change report for primitive types that have no nested fields.
ScalarCursor
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§

ErrorKind
The specific kind of error that occurred.
OptionChanges
Change report for Option<T> fields.
ResultChanges
Change report for Result<T, E> fields.

Traits§

Automorph
Primary trait for bidirectional synchronization with Automerge documents.
ChangeReport
Trait for hierarchical change reports.
FieldCursor
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 Automorph trait for a struct or enum.