automorph 0.2.0

Derive macros for bidirectional Automerge-Rust struct synchronization
Documentation
//! Implementations for time types.
//!
//! This module provides implementations for:
//! - `Duration` - Time duration
//! - `SystemTime` - System clock time

use std::time::{Duration, SystemTime, UNIX_EPOCH};

use automerge::{ChangeHash, ObjId, ObjType, Prop, ReadDoc, Value, transaction::Transactable};

use crate::{Automorph, ChangeReport, Error, PrimitiveChanged, Result, ScalarCursor};

// Duration stored as a map with secs and nanos fields
impl Automorph for Duration {
    type Changes = PrimitiveChanged;
    type Cursor = ScalarCursor;

    fn save<D: Transactable + ReadDoc>(
        &self,
        doc: &mut D,
        obj: impl AsRef<ObjId>,
        prop: impl Into<Prop>,
    ) -> Result<()> {
        let prop: Prop = prop.into();
        let obj = obj.as_ref();

        let map_id = match doc.get(obj, prop.clone())? {
            Some((Value::Object(ObjType::Map), id)) => id,
            _ => doc.put_object(obj, prop, ObjType::Map)?,
        };

        self.as_secs().save(doc, &map_id, "secs")?;
        self.subsec_nanos().save(doc, &map_id, "nanos")?;

        Ok(())
    }

    fn load<D: ReadDoc>(doc: &D, obj: impl AsRef<ObjId>, prop: impl Into<Prop>) -> Result<Self> {
        let prop: Prop = prop.into();
        let obj = obj.as_ref();

        match doc.get(obj, prop)? {
            Some((Value::Object(ObjType::Map), map_id)) => {
                let secs = u64::load(doc, &map_id, "secs")?;
                let nanos = u32::load(doc, &map_id, "nanos")?;
                Ok(Duration::new(secs, nanos))
            }
            Some((v, _)) => Err(Error::type_mismatch(
                "Duration (Map)",
                Some(format!("{:?}", v)),
            )),
            None => Err(Error::missing_value()),
        }
    }

    fn load_at<D: ReadDoc>(
        doc: &D,
        obj: impl AsRef<ObjId>,
        prop: impl Into<Prop>,
        heads: &[ChangeHash],
    ) -> Result<Self> {
        let prop: Prop = prop.into();
        let obj = obj.as_ref();

        match doc.get_at(obj, prop, heads)? {
            Some((Value::Object(ObjType::Map), map_id)) => {
                let secs = u64::load_at(doc, &map_id, "secs", heads)?;
                let nanos = u32::load_at(doc, &map_id, "nanos", heads)?;
                Ok(Duration::new(secs, nanos))
            }
            Some((v, _)) => Err(Error::type_mismatch(
                "Duration (Map)",
                Some(format!("{:?}", v)),
            )),
            None => Err(Error::missing_value()),
        }
    }

    fn diff<D: ReadDoc>(
        &self,
        doc: &D,
        obj: impl AsRef<ObjId>,
        prop: impl Into<Prop>,
    ) -> Result<Self::Changes> {
        let prop: Prop = prop.into();
        let obj = obj.as_ref();

        match doc.get(obj, prop)? {
            Some((Value::Object(ObjType::Map), map_id)) => {
                let secs_changes = self.as_secs().diff(doc, &map_id, "secs")?;
                if secs_changes.any() {
                    return Ok(PrimitiveChanged::new(true));
                }
                let nanos_changes = self.subsec_nanos().diff(doc, &map_id, "nanos")?;
                Ok(PrimitiveChanged::new(nanos_changes.any()))
            }
            Some((v, _)) => Err(Error::type_mismatch(
                "Duration (Map)",
                Some(format!("{:?}", v)),
            )),
            None => Err(Error::missing_value()),
        }
    }

    fn diff_at<D: ReadDoc>(
        &self,
        doc: &D,
        obj: impl AsRef<ObjId>,
        prop: impl Into<Prop>,
        heads: &[ChangeHash],
    ) -> Result<Self::Changes> {
        let prop: Prop = prop.into();
        let obj = obj.as_ref();

        match doc.get_at(obj, prop, heads)? {
            Some((Value::Object(ObjType::Map), map_id)) => {
                let secs_changes = self.as_secs().diff_at(doc, &map_id, "secs", heads)?;
                if secs_changes.any() {
                    return Ok(PrimitiveChanged::new(true));
                }
                let nanos_changes = self.subsec_nanos().diff_at(doc, &map_id, "nanos", heads)?;
                Ok(PrimitiveChanged::new(nanos_changes.any()))
            }
            Some((v, _)) => Err(Error::type_mismatch(
                "Duration (Map)",
                Some(format!("{:?}", v)),
            )),
            None => Err(Error::missing_value()),
        }
    }

    fn update<D: ReadDoc>(
        &mut self,
        doc: &D,
        obj: impl AsRef<ObjId>,
        prop: impl Into<Prop>,
    ) -> Result<Self::Changes> {
        let prop: Prop = prop.into();
        let obj = obj.as_ref();

        match doc.get(obj, prop)? {
            Some((Value::Object(ObjType::Map), map_id)) => {
                let secs = u64::load(doc, &map_id, "secs")?;
                let nanos = u32::load(doc, &map_id, "nanos")?;
                let new_value = Duration::new(secs, nanos);
                let changed = *self != new_value;
                if changed {
                    *self = new_value;
                }
                Ok(PrimitiveChanged::new(changed))
            }
            Some((v, _)) => Err(Error::type_mismatch(
                "Duration (Map)",
                Some(format!("{:?}", v)),
            )),
            None => Err(Error::missing_value()),
        }
    }

    fn update_at<D: ReadDoc>(
        &mut self,
        doc: &D,
        obj: impl AsRef<ObjId>,
        prop: impl Into<Prop>,
        heads: &[ChangeHash],
    ) -> Result<Self::Changes> {
        let prop: Prop = prop.into();
        let obj = obj.as_ref();

        match doc.get_at(obj, prop, heads)? {
            Some((Value::Object(ObjType::Map), map_id)) => {
                let secs = u64::load_at(doc, &map_id, "secs", heads)?;
                let nanos = u32::load_at(doc, &map_id, "nanos", heads)?;
                let new_value = Duration::new(secs, nanos);
                let changed = *self != new_value;
                if changed {
                    *self = new_value;
                }
                Ok(PrimitiveChanged::new(changed))
            }
            Some((v, _)) => Err(Error::type_mismatch(
                "Duration (Map)",
                Some(format!("{:?}", v)),
            )),
            None => Err(Error::missing_value()),
        }
    }
}

// SystemTime stored as duration since UNIX_EPOCH
impl Automorph for SystemTime {
    type Changes = PrimitiveChanged;
    type Cursor = ScalarCursor;

    fn save<D: Transactable + ReadDoc>(
        &self,
        doc: &mut D,
        obj: impl AsRef<ObjId>,
        prop: impl Into<Prop>,
    ) -> Result<()> {
        let duration = self
            .duration_since(UNIX_EPOCH)
            .map_err(|e| Error::invalid_value(format!("SystemTime before UNIX_EPOCH: {}", e)))?;
        duration.save(doc, obj, prop)
    }

    fn load<D: ReadDoc>(doc: &D, obj: impl AsRef<ObjId>, prop: impl Into<Prop>) -> Result<Self> {
        let duration = Duration::load(doc, obj, prop)?;
        Ok(UNIX_EPOCH + duration)
    }

    fn load_at<D: ReadDoc>(
        doc: &D,
        obj: impl AsRef<ObjId>,
        prop: impl Into<Prop>,
        heads: &[ChangeHash],
    ) -> Result<Self> {
        let duration = Duration::load_at(doc, obj, prop, heads)?;
        Ok(UNIX_EPOCH + duration)
    }

    fn diff<D: ReadDoc>(
        &self,
        doc: &D,
        obj: impl AsRef<ObjId>,
        prop: impl Into<Prop>,
    ) -> Result<Self::Changes> {
        let duration = self
            .duration_since(UNIX_EPOCH)
            .map_err(|e| Error::invalid_value(format!("SystemTime before UNIX_EPOCH: {}", e)))?;
        duration.diff(doc, obj, prop)
    }

    fn diff_at<D: ReadDoc>(
        &self,
        doc: &D,
        obj: impl AsRef<ObjId>,
        prop: impl Into<Prop>,
        heads: &[ChangeHash],
    ) -> Result<Self::Changes> {
        let duration = self
            .duration_since(UNIX_EPOCH)
            .map_err(|e| Error::invalid_value(format!("SystemTime before UNIX_EPOCH: {}", e)))?;
        duration.diff_at(doc, obj, prop, heads)
    }

    fn update<D: ReadDoc>(
        &mut self,
        doc: &D,
        obj: impl AsRef<ObjId>,
        prop: impl Into<Prop>,
    ) -> Result<Self::Changes> {
        let duration = Duration::load(doc, obj, prop)?;
        let new_value = UNIX_EPOCH + duration;
        let changed = *self != new_value;
        if changed {
            *self = new_value;
        }
        Ok(PrimitiveChanged::new(changed))
    }

    fn update_at<D: ReadDoc>(
        &mut self,
        doc: &D,
        obj: impl AsRef<ObjId>,
        prop: impl Into<Prop>,
        heads: &[ChangeHash],
    ) -> Result<Self::Changes> {
        let duration = Duration::load_at(doc, obj, prop, heads)?;
        let new_value = UNIX_EPOCH + duration;
        let changed = *self != new_value;
        if changed {
            *self = new_value;
        }
        Ok(PrimitiveChanged::new(changed))
    }
}

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

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

        let dur = Duration::new(123, 456789);
        dur.save(&mut doc, &ROOT, "dur").unwrap();

        let restored = Duration::load(&doc, &ROOT, "dur").unwrap();
        assert_eq!(restored, dur);
    }

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

        let time = SystemTime::now();
        time.save(&mut doc, &ROOT, "time").unwrap();

        let restored = SystemTime::load(&doc, &ROOT, "time").unwrap();
        // Compare with some tolerance due to potential precision loss
        let diff = time.duration_since(UNIX_EPOCH).unwrap().as_nanos() as i128
            - restored.duration_since(UNIX_EPOCH).unwrap().as_nanos() as i128;
        assert!(diff.abs() < 1000); // Within 1 microsecond
    }
}