use crate::{ChangeReport, Error, FieldCursor, Result};
use automerge::{ChangeHash, ObjId, Prop, ReadDoc, transaction::Transactable};
use chrono::{DateTime, Utc};
use std::borrow::Cow;
#[derive(Debug, Clone, Default)]
pub struct Changes {
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()
}
}
#[derive(Debug, Clone, Default)]
pub struct Cursor {
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(())
}
}
pub fn save<D: Transactable + ReadDoc>(
value: &DateTime<Utc>,
doc: &mut D,
obj: impl AsRef<ObjId>,
prop: impl Into<Prop>,
) -> Result<()> {
let iso_string = value.to_rfc3339();
crate::Automorph::save(&iso_string, doc, obj, prop)
}
pub fn load<D: ReadDoc>(
doc: &D,
obj: impl AsRef<ObjId>,
prop: impl Into<Prop>,
) -> Result<DateTime<Utc>> {
let iso_string: String = crate::Automorph::load(doc, obj, prop)?;
DateTime::parse_from_rfc3339(&iso_string)
.map(|dt| dt.with_timezone(&Utc))
.map_err(|e| Error::invalid_value(format!("invalid ISO 8601 datetime: {}", e)))
}
pub fn load_at<D: ReadDoc>(
doc: &D,
obj: impl AsRef<ObjId>,
prop: impl Into<Prop>,
heads: &[ChangeHash],
) -> Result<DateTime<Utc>> {
let iso_string: String = crate::Automorph::load_at(doc, obj, prop, heads)?;
DateTime::parse_from_rfc3339(&iso_string)
.map(|dt| dt.with_timezone(&Utc))
.map_err(|e| Error::invalid_value(format!("invalid ISO 8601 datetime: {}", e)))
}
pub fn diff<D: ReadDoc>(
value: &DateTime<Utc>,
doc: &D,
obj: impl AsRef<ObjId>,
prop: impl Into<Prop>,
) -> Result<Changes> {
let stored: Result<DateTime<Utc>> = load(doc, obj, prop);
match stored {
Ok(stored) => Ok(Changes { changed: *value != stored }),
Err(_) => Ok(Changes { changed: true }),
}
}
pub fn diff_at<D: ReadDoc>(
value: &DateTime<Utc>,
doc: &D,
obj: impl AsRef<ObjId>,
prop: impl Into<Prop>,
heads: &[ChangeHash],
) -> Result<Changes> {
let stored: Result<DateTime<Utc>> = load_at(doc, obj, prop, heads);
match stored {
Ok(stored) => Ok(Changes { changed: *value != stored }),
Err(_) => Ok(Changes { changed: true }),
}
}
pub fn update<D: ReadDoc>(
value: &mut DateTime<Utc>,
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 })
}
pub fn update_at<D: ReadDoc>(
value: &mut DateTime<Utc>,
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};
use chrono::TimeZone;
#[test]
fn test_datetime_roundtrip() {
let mut doc = AutoCommit::new();
let original = Utc.with_ymd_and_hms(2024, 1, 15, 10, 30, 0).unwrap();
save(&original, &mut doc, &ROOT, "timestamp").unwrap();
let loaded = load(&doc, &ROOT, "timestamp").unwrap();
assert_eq!(original, loaded);
}
#[test]
fn test_datetime_format() {
let mut doc = AutoCommit::new();
let dt = Utc.with_ymd_and_hms(2024, 1, 15, 10, 30, 0).unwrap();
save(&dt, &mut doc, &ROOT, "timestamp").unwrap();
let stored: String = crate::Automorph::load(&doc, &ROOT, "timestamp").unwrap();
assert!(stored.contains("2024-01-15"));
assert!(stored.contains("10:30:00"));
}
#[test]
fn test_datetime_diff_no_change() {
let mut doc = AutoCommit::new();
let dt = Utc.with_ymd_and_hms(2024, 1, 15, 10, 30, 0).unwrap();
save(&dt, &mut doc, &ROOT, "timestamp").unwrap();
let changes = diff(&dt, &doc, &ROOT, "timestamp").unwrap();
assert!(!changes.any());
}
#[test]
fn test_datetime_diff_with_change() {
let mut doc = AutoCommit::new();
let dt1 = Utc.with_ymd_and_hms(2024, 1, 15, 10, 30, 0).unwrap();
let dt2 = Utc.with_ymd_and_hms(2024, 1, 16, 14, 0, 0).unwrap();
save(&dt1, &mut doc, &ROOT, "timestamp").unwrap();
let changes = diff(&dt2, &doc, &ROOT, "timestamp").unwrap();
assert!(changes.any());
}
#[test]
fn test_datetime_update() {
let mut doc = AutoCommit::new();
let dt1 = Utc.with_ymd_and_hms(2024, 1, 15, 10, 30, 0).unwrap();
let dt2 = Utc.with_ymd_and_hms(2024, 1, 16, 14, 0, 0).unwrap();
save(&dt1, &mut doc, &ROOT, "timestamp").unwrap();
let mut current = dt1;
let changes = update(&mut current, &doc, &ROOT, "timestamp").unwrap();
assert!(!changes.any());
assert_eq!(current, dt1);
save(&dt2, &mut doc, &ROOT, "timestamp").unwrap();
let changes = update(&mut current, &doc, &ROOT, "timestamp").unwrap();
assert!(changes.any());
assert_eq!(current, dt2);
}
#[test]
fn test_datetime_cursor() {
let mut doc = AutoCommit::new();
let dt = Utc.with_ymd_and_hms(2024, 1, 15, 10, 30, 0).unwrap();
save(&dt, &mut doc, &ROOT, "timestamp").unwrap();
let mut cursor = Cursor::default();
let (_, obj_id) = doc.get(&ROOT, "timestamp").unwrap().unwrap();
cursor.refresh(&doc, &obj_id).unwrap();
let changes = cursor.diff(&doc, &obj_id).unwrap();
assert!(!changes.any());
let dt2 = Utc.with_ymd_and_hms(2024, 1, 16, 14, 0, 0).unwrap();
save(&dt2, &mut doc, &ROOT, "timestamp").unwrap();
let (_, new_obj_id) = doc.get(&ROOT, "timestamp").unwrap().unwrap();
let changes = cursor.diff(&doc, &new_obj_id).unwrap();
assert!(changes.any());
}
}