automorph 0.2.0

Derive macros for bidirectional Automerge-Rust struct synchronization
Documentation
#![forbid(unsafe_code)]

//! # Automorph
//!
//! Bidirectional synchronization between Rust types and
//! [Automerge](https://automerge.org/) documents.
//!
//! Automorph works similarly to [Serde](https://serde.rs/) - 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
//!
//! ```rust
//! 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):
//!
//! ```rust
//! 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
//!
//! | 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
//!
//! ```rust
//! use automorph::Automorph;
//! use automerge::{AutoCommit, ROOT};
//!
//! #[derive(Automorph, Debug, PartialEq, Default, Clone)]
//! #[automorph(rename_all = "camelCase")]
//! struct MyStruct {
//!     user_name: String,
//!     email_address: String,
//! }
//!
//! let mut doc = AutoCommit::new();
//! let s = MyStruct {
//!     user_name: "Alice".to_string(),
//!     email_address: "alice@example.com".to_string(),
//! };
//! s.save(&mut doc, &ROOT, "s").unwrap();
//!
//! let loaded = MyStruct::load(&doc, &ROOT, "s").unwrap();
//! assert_eq!(s, loaded);
//! ```
//!
//! (validated by: `test_rename_all`)
//!
//! ## Field Attributes
//!
//! ```rust
//! use automorph::Automorph;
//! use automerge::{AutoCommit, ROOT};
//!
//! #[derive(Automorph, Debug, PartialEq, Default, Clone)]
//! struct MyStruct {
//!     #[automorph(rename = "user_name")]
//!     name: String,
//!     #[automorph(skip)]
//!     cached: u64,
//!     #[automorph(default)]
//!     optional: String,
//! }
//!
//! let mut doc = AutoCommit::new();
//! let s = MyStruct {
//!     name: "Alice".to_string(),
//!     cached: 42,
//!     optional: "default".to_string(),
//! };
//! s.save(&mut doc, &ROOT, "s").unwrap();
//!
//! let loaded = MyStruct::load(&doc, &ROOT, "s").unwrap();
//! assert_eq!(loaded.name, "Alice");
//! ```
//!
//! (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`)
//!
//! ```rust
//! use automorph::Automorph;
//! use automerge::{AutoCommit, ROOT};
//!
//! #[derive(Automorph, Debug, PartialEq, Clone)]
//! enum MyEnum {
//!     A(i32),
//!     B { x: i32 },
//! }
//!
//! let mut doc = AutoCommit::new();
//! let val = MyEnum::A(42);
//! val.save(&mut doc, &ROOT, "val").unwrap();
//!
//! let loaded = MyEnum::load(&doc, &ROOT, "val").unwrap();
//! assert_eq!(val, loaded);
//! ```
//!
//! Internally tagged: `{"type": "Variant", ...fields}`
//! (validated by: `test_internally_tagged_enum`, `test_internally_tagged_unit`)
//!
//! ```rust
//! use automorph::Automorph;
//! use automerge::{AutoCommit, ROOT};
//!
//! #[derive(Automorph, Debug, PartialEq, Clone)]
//! #[automorph(tag = "type")]
//! enum TaggedEnum {
//!     A { x: i32 },
//!     B { y: i32 },
//! }
//!
//! let mut doc = AutoCommit::new();
//! let val = TaggedEnum::A { x: 42 };
//! val.save(&mut doc, &ROOT, "val").unwrap();
//!
//! let loaded = TaggedEnum::load(&doc, &ROOT, "val").unwrap();
//! assert_eq!(val, loaded);
//! ```
//!
//! Adjacently tagged: `{"t": "Variant", "c": content}`
//! (validated by: `test_adjacently_tagged_enum`, `test_adjacently_tagged_int`)
//!
//! ```rust
//! use automorph::Automorph;
//! use automerge::{AutoCommit, ROOT};
//!
//! #[derive(Automorph, Debug, PartialEq, Clone)]
//! #[automorph(tag = "t", content = "c")]
//! enum AdjacentEnum {
//!     A(i32),
//!     B(String),
//! }
//!
//! let mut doc = AutoCommit::new();
//! let val = AdjacentEnum::A(42);
//! val.save(&mut doc, &ROOT, "val").unwrap();
//!
//! let loaded = AdjacentEnum::load(&doc, &ROOT, "val").unwrap();
//! assert_eq!(val, loaded);
//! ```
//!
//! Untagged: tries each variant in order
//! (validated by: `test_untagged_enum`)
//!
//! ```rust
//! use automorph::Automorph;
//! use automerge::{AutoCommit, ROOT};
//!
//! #[derive(Automorph, Debug, PartialEq, Clone)]
//! #[automorph(untagged)]
//! enum UntaggedEnum {
//!     Int(i32),
//!     Str(String),
//! }
//!
//! let mut doc = AutoCommit::new();
//! let val = UntaggedEnum::Int(42);
//! val.save(&mut doc, &ROOT, "val").unwrap();
//!
//! let loaded = UntaggedEnum::load(&doc, &ROOT, "val").unwrap();
//! assert_eq!(val, loaded);
//! ```
//!
//! ## 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.
//!
//! ```rust
//! use automorph::{Tracked, Automorph, ChangeReport};
//! use automerge::{AutoCommit, ROOT};
//!
//! #[derive(Automorph, Default, Clone, Debug)]
//! struct Workspace {
//!     name: String,
//!     items: Vec<String>,
//! }
//!
//! let mut doc = AutoCommit::new();
//! let workspace = Workspace {
//!     name: "test".to_string(),
//!     items: vec!["a".to_string()],
//! };
//! workspace.save(&mut doc, &ROOT, "workspace").unwrap();
//!
//! // Load with change tracking (validated by: test_tracked_load)
//! let mut tracked: Tracked<Workspace> = Tracked::load(&doc, &ROOT, "workspace").unwrap();
//!
//! // Modify the document
//! let workspace2 = Workspace {
//!     name: "modified".to_string(),
//!     items: vec!["a".to_string(), "b".to_string()],
//! };
//! workspace2.save(&mut doc, &ROOT, "workspace").unwrap();
//!
//! // Check if the document has structural changes (validated by: test_tracked_has_structural_changes)
//! if tracked.has_structural_changes(&doc).unwrap() {
//!     // Update our local copy and get a change report (validated by: test_tracked_update, test_update_returns_change_report)
//!     let changes = tracked.update(&doc).unwrap();
//!     if changes.any() {
//!         println!("Workspace was modified!");
//!     }
//! }
//! ```
//!
//! **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
//!
//! ```rust
//! use automorph::Automorph;
//! use automerge::{AutoCommit, ROOT};
//!
//! #[derive(Automorph, Debug, PartialEq, Default, Clone)]
//! struct Person {
//!     name: String,
//! }
//!
//! let mut doc = AutoCommit::new();
//! let mut person = Person { name: "Alice".to_string() };
//! person.save(&mut doc, &ROOT, "person").unwrap();
//!
//! // Save current version
//! let old_heads = doc.get_heads();
//!
//! // Make changes
//! person.name = "Bob".to_string();
//! person.save(&mut doc, &ROOT, "person").unwrap();
//!
//! // Read old value (validated by: test_derived_version_aware)
//! let old_person = Person::load_at(&doc, &ROOT, "person", &old_heads).unwrap();
//! assert_eq!(old_person.name, "Alice");
//!
//! // Diff between versions (validated by: test_diff_versions)
//! let changes = person.diff_at(&doc, &ROOT, "person", &old_heads).unwrap();
//! assert!(changes.name.map_or(false, |n| n.changed));
//! ```
//!
//! ## Learn More
//!
//! - [Automerge Documentation](https://docs.rs/automerge)
//! - [What is Automerge?](https://automerge.org/)
//! - [CRDTs Explained](https://crdt.tech/)

#![warn(missing_docs)]
#![warn(clippy::all)]

mod error;
mod impls;
mod traits;
mod with_modules;

/// Configuration for Automorph behavior.
pub mod config;

/// CRDT primitive types that expose Automerge's collaborative semantics.
///
/// This module provides types that leverage Automerge's CRDT capabilities directly:
/// - `Counter` - For concurrent increment/decrement operations
/// - `Text` - For collaborative text editing with character-level merging
///
/// Unlike regular types which use last-writer-wins semantics, these types
/// merge concurrent operations correctly.
///
/// See the module documentation for usage examples.
pub mod crdt;

/// Debug utilities for inspecting Automerge documents.
///
/// This module provides tools for debugging and inspecting document contents.
/// See `dump_json` for quick document inspection.
pub mod debug;

// Re-export core types from automerge for convenience
pub use automerge;

// Export our primary trait
pub use traits::Automorph;

// Export supporting traits and types for change tracking
pub use traits::Tracked;
pub use traits::{ChangeReport, FieldCursor, PrimitiveChanged, ScalarCursor};
pub use traits::{MapChanges, MapCursor, VecChanges, VecCursor};
pub use traits::{OptionChanges, ResultChanges};

// Export OptionCursor and ResultCursor for cursor-based change detection
pub use impls::option::OptionCursor;
pub use impls::option::ResultCursor;

// Re-export with modules for custom save/load
#[cfg(feature = "chrono")]
pub use with_modules::chrono_iso8601;

#[cfg(feature = "uuid")]
pub use with_modules::uuid_string;

// Re-export the derive macro with the same name as the trait
// This allows `#[derive(Automorph)]` to work just like serde's `#[derive(Serialize)]`
pub use automorph_derive::Automorph;

// Export error types
pub use error::{Error, ErrorKind, Result};