automorph 0.2.0

Derive macros for bidirectional Automerge-Rust struct synchronization
Documentation
//! Module for saving/loading `uuid::Uuid` as hyphenated strings.
//!
//! This module provides full support for the `#[automorph(with = "...")]` attribute,
//! including save, load, diff, update, and cursor operations.
//!
//! # Example
//!
//! ```rust
//! # #[cfg(feature = "uuid")] {
//! use automorph::Automorph;
//! use automerge::AutoCommit;
//! use uuid::Uuid;
//!
//! #[derive(Automorph)]
//! struct Entity {
//!     name: String,
//!
//!     #[automorph(with = "automorph::uuid_string")]
//!     id: Uuid,
//! }
//!
//! let mut doc = AutoCommit::new();
//! let entity = Entity { name: "test".to_string(), id: Uuid::new_v4() };
//! entity.save(&mut doc, automerge::ROOT, "entity").unwrap();
//! # }
//! ```
//!
//! The UUID is stored as a hyphenated string like `"550e8400-e29b-41d4-a716-446655440000"`.

use crate::{ChangeReport, Error, FieldCursor, Result};
use automerge::{ChangeHash, ObjId, Prop, ReadDoc, transaction::Transactable};
use uuid::Uuid;
use std::borrow::Cow;

/// Change report for `Uuid` fields.
///
/// Tracks whether a UUID value changed. This type is used by the derive macro
/// when `#[automorph(with = "automorph::uuid_string")]` is specified.
#[derive(Debug, Clone, Default)]
pub struct Changes {
    /// Whether the UUID value changed
    pub changed: bool,
}

impl ChangeReport for Changes {
    fn any(&self) -> bool {
        self.changed
    }

    fn paths(&self) -> Vec<Vec<Cow<'static, str>>> {
        if self.changed { vec![vec![]] } else { vec![] }
    }

    fn leaf_paths(&self) -> Vec<Vec<Cow<'static, str>>> {
        self.paths()
    }
}

/// Cursor for tracking `Uuid` changes efficiently.
///
/// This cursor stores the Automerge ObjId of the UUID field, enabling O(1)
/// change detection by comparing cached vs current ObjIds.
#[derive(Debug, Clone, Default)]
pub struct Cursor {
    /// Cached ObjId for detecting changes via object identity.
    pub obj_id: Option<ObjId>,
}

impl FieldCursor for Cursor {
    type Changes = Changes;

    fn diff<D: ReadDoc>(&self, _doc: &D, obj: &ObjId) -> Result<Self::Changes> {
        let changed = match &self.obj_id {
            Some(cached) => cached != obj,
            None => true,
        };
        Ok(Changes { changed })
    }

    fn refresh<D: ReadDoc>(&mut self, _doc: &D, obj: &ObjId) -> Result<()> {
        self.obj_id = Some(obj.clone());
        Ok(())
    }
}

/// Save a `Uuid` as a hyphenated string.
///
/// # Errors
///
/// Returns an error if the Automerge operation fails.
pub fn save<D: Transactable + ReadDoc>(
    value: &Uuid,
    doc: &mut D,
    obj: impl AsRef<ObjId>,
    prop: impl Into<Prop>,
) -> Result<()> {
    let uuid_string = value.to_string();
    crate::Automorph::save(&uuid_string, doc, obj, prop)
}

/// Load a `Uuid` from a hyphenated string.
///
/// # Errors
///
/// Returns an error if the value is missing or has an invalid format.
pub fn load<D: ReadDoc>(doc: &D, obj: impl AsRef<ObjId>, prop: impl Into<Prop>) -> Result<Uuid> {
    let uuid_string: String = crate::Automorph::load(doc, obj, prop)?;
    Uuid::parse_str(&uuid_string).map_err(|e| Error::invalid_value(format!("invalid UUID: {}", e)))
}

/// Load a `Uuid` from a specific version.
///
/// # Errors
///
/// Returns an error if the value is missing or has an invalid format.
pub fn load_at<D: ReadDoc>(
    doc: &D,
    obj: impl AsRef<ObjId>,
    prop: impl Into<Prop>,
    heads: &[ChangeHash],
) -> Result<Uuid> {
    let uuid_string: String = crate::Automorph::load_at(doc, obj, prop, heads)?;
    Uuid::parse_str(&uuid_string).map_err(|e| Error::invalid_value(format!("invalid UUID: {}", e)))
}

/// Compare a `Uuid` against the document.
///
/// # Errors
///
/// Returns an error only if the document read fails.
pub fn diff<D: ReadDoc>(
    value: &Uuid,
    doc: &D,
    obj: impl AsRef<ObjId>,
    prop: impl Into<Prop>,
) -> Result<Changes> {
    let stored: Result<Uuid> = load(doc, obj, prop);
    match stored {
        Ok(stored) => Ok(Changes { changed: *value != stored }),
        Err(_) => Ok(Changes { changed: true }),
    }
}

/// Compare a `Uuid` against a specific document version.
///
/// # Errors
///
/// Returns an error only if the document read fails.
pub fn diff_at<D: ReadDoc>(
    value: &Uuid,
    doc: &D,
    obj: impl AsRef<ObjId>,
    prop: impl Into<Prop>,
    heads: &[ChangeHash],
) -> Result<Changes> {
    let stored: Result<Uuid> = load_at(doc, obj, prop, heads);
    match stored {
        Ok(stored) => Ok(Changes { changed: *value != stored }),
        Err(_) => Ok(Changes { changed: true }),
    }
}

/// Update a `Uuid` value from the document.
///
/// # Errors
///
/// Returns an error if the value cannot be loaded from the document.
pub fn update<D: ReadDoc>(
    value: &mut Uuid,
    doc: &D,
    obj: impl AsRef<ObjId>,
    prop: impl Into<Prop>,
) -> Result<Changes> {
    let loaded = load(doc, obj, prop)?;
    let changed = *value != loaded;
    *value = loaded;
    Ok(Changes { changed })
}

/// Update a `Uuid` value from a specific document version.
///
/// # Errors
///
/// Returns an error if the value cannot be loaded from the document.
pub fn update_at<D: ReadDoc>(
    value: &mut Uuid,
    doc: &D,
    obj: impl AsRef<ObjId>,
    prop: impl Into<Prop>,
    heads: &[ChangeHash],
) -> Result<Changes> {
    let loaded = load_at(doc, obj, prop, heads)?;
    let changed = *value != loaded;
    *value = loaded;
    Ok(Changes { changed })
}

#[cfg(test)]
mod tests {
    use super::*;
    use automerge::{AutoCommit, ROOT};

    #[test]
    fn test_uuid_roundtrip() {
        let mut doc = AutoCommit::new();
        let original = Uuid::new_v4();

        save(&original, &mut doc, &ROOT, "id").unwrap();
        let loaded = load(&doc, &ROOT, "id").unwrap();

        assert_eq!(original, loaded);
    }

    #[test]
    fn test_uuid_format() {
        let mut doc = AutoCommit::new();
        let id = Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap();

        save(&id, &mut doc, &ROOT, "id").unwrap();

        // Verify it's stored as a string
        let stored: String = crate::Automorph::load(&doc, &ROOT, "id").unwrap();
        assert_eq!(stored, "550e8400-e29b-41d4-a716-446655440000");
    }

    #[test]
    fn test_uuid_nil() {
        let mut doc = AutoCommit::new();
        let nil = Uuid::nil();

        save(&nil, &mut doc, &ROOT, "id").unwrap();
        let loaded = load(&doc, &ROOT, "id").unwrap();

        assert_eq!(nil, loaded);
        assert!(loaded.is_nil());
    }

    #[test]
    fn test_uuid_diff_no_change() {
        let mut doc = AutoCommit::new();
        let id = Uuid::new_v4();

        save(&id, &mut doc, &ROOT, "id").unwrap();
        let changes = diff(&id, &doc, &ROOT, "id").unwrap();

        assert!(!changes.any());
    }

    #[test]
    fn test_uuid_diff_with_change() {
        let mut doc = AutoCommit::new();
        let id1 = Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap();
        let id2 = Uuid::parse_str("660e8400-e29b-41d4-a716-446655440001").unwrap();

        save(&id1, &mut doc, &ROOT, "id").unwrap();
        let changes = diff(&id2, &doc, &ROOT, "id").unwrap();

        assert!(changes.any());
    }

    #[test]
    fn test_uuid_update() {
        let mut doc = AutoCommit::new();
        let id1 = Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap();
        let id2 = Uuid::parse_str("660e8400-e29b-41d4-a716-446655440001").unwrap();

        save(&id1, &mut doc, &ROOT, "id").unwrap();

        let mut current = id1;
        let changes = update(&mut current, &doc, &ROOT, "id").unwrap();
        assert!(!changes.any());
        assert_eq!(current, id1);

        // Now change the document
        save(&id2, &mut doc, &ROOT, "id").unwrap();

        let changes = update(&mut current, &doc, &ROOT, "id").unwrap();
        assert!(changes.any());
        assert_eq!(current, id2);
    }

    #[test]
    fn test_uuid_cursor() {
        let mut doc = AutoCommit::new();
        let id = Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap();

        save(&id, &mut doc, &ROOT, "id").unwrap();

        let mut cursor = Cursor::default();
        let (_, obj_id) = doc.get(&ROOT, "id").unwrap().unwrap();
        cursor.refresh(&doc, &obj_id).unwrap();

        // No change yet
        let changes = cursor.diff(&doc, &obj_id).unwrap();
        assert!(!changes.any());

        // Change the value
        let id2 = Uuid::parse_str("660e8400-e29b-41d4-a716-446655440001").unwrap();
        save(&id2, &mut doc, &ROOT, "id").unwrap();

        let (_, new_obj_id) = doc.get(&ROOT, "id").unwrap().unwrap();
        let changes = cursor.diff(&doc, &new_obj_id).unwrap();
        assert!(changes.any());
    }
}