use std::borrow::Cow;
use automerge::ScalarValue;
use crate::{Doc, Prop, ReadDoc};
mod impls;
pub(crate) mod map;
#[cfg(test)]
mod reconcile_key_matching_tests;
pub(crate) mod reconcile_to_am_hydrate;
mod seq;
pub(crate) use reconcile_to_am_hydrate::reconcile_to_am_hydrate;
pub trait Reconciler {
type Error: std::error::Error + From<StaleHeads>;
type Map<'a>: MapReconciler<Error = Self::Error>
where
Self: 'a;
type Seq<'a>: SeqReconciler<Error = Self::Error>
where
Self: 'a;
type Text<'a>: TextReconciler<Error = Self::Error>
where
Self: 'a;
type Counter<'a>: CounterReconciler<Error = Self::Error>
where
Self: 'a;
fn none(&mut self) -> Result<(), Self::Error>;
fn bytes<B: AsRef<[u8]>>(&mut self, value: B) -> Result<(), Self::Error>;
fn timestamp(&mut self, value: i64) -> Result<(), Self::Error>;
fn boolean(&mut self, value: bool) -> Result<(), Self::Error>;
fn str<S: AsRef<str>>(&mut self, value: S) -> Result<(), Self::Error>;
fn u64(&mut self, value: u64) -> Result<(), Self::Error>;
fn i64(&mut self, value: i64) -> Result<(), Self::Error>;
fn f64(&mut self, value: f64) -> Result<(), Self::Error>;
fn map(&mut self) -> Result<Self::Map<'_>, Self::Error>;
fn seq(&mut self) -> Result<Self::Seq<'_>, Self::Error>;
fn text(&mut self) -> Result<Self::Text<'_>, Self::Error>;
fn counter(&mut self) -> Result<Self::Counter<'_>, Self::Error>;
fn heads(&self) -> &[automerge::ChangeHash];
}
pub trait MapReconciler {
type Error: std::error::Error + From<StaleHeads>;
type EntriesIter<'a>: Iterator<Item = (Cow<'a, str>, automerge::ValueRef<'a>)>
where
Self: 'a;
fn entries(&self) -> Self::EntriesIter<'_>;
fn entry<P: AsRef<str>>(&self, prop: P) -> Option<automerge::Value<'_>>;
fn put<R: Reconcile, P: AsRef<str>>(&mut self, prop: P, value: R) -> Result<(), Self::Error>;
fn delete<P: AsRef<str>>(&mut self, prop: P) -> Result<(), Self::Error>;
fn hydrate_entry_key<'a, R: Reconcile, P: AsRef<str>>(
&self,
prop: P,
) -> Result<LoadKey<R::Key<'a>>, Self::Error>;
fn replace<R: Reconcile, P: AsRef<str>>(
&mut self,
prop: P,
value: R,
) -> Result<(), Self::Error> {
self.delete(&prop)?;
self.put(prop, value)?;
Ok(())
}
fn retain<F: FnMut(&str, automerge::ValueRef<'_>) -> bool>(
&mut self,
mut pred: F,
) -> Result<(), Self::Error> {
let mut delenda = Vec::new();
for (k, v) in self.entries() {
if !pred(k.as_ref(), v) {
delenda.push(k.to_string());
}
}
for k in &delenda {
self.delete(k)?;
}
Ok(())
}
}
pub trait SeqReconciler {
type Error: std::error::Error + From<StaleHeads>;
type ItemIter<'a>: Iterator<Item = automerge::ValueRef<'a>>
where
Self: 'a;
fn items(&self) -> Self::ItemIter<'_>;
fn get(&self, index: usize) -> Result<Option<automerge::Value<'_>>, Self::Error>;
fn hydrate_item_key<'a, R: Reconcile>(
&self,
index: usize,
) -> Result<LoadKey<R::Key<'a>>, Self::Error>;
fn insert<R: Reconcile>(&mut self, index: usize, value: R) -> Result<(), Self::Error>;
fn set<R: Reconcile>(&mut self, index: usize, value: R) -> Result<(), Self::Error>;
fn delete(&mut self, index: usize) -> Result<(), Self::Error>;
fn len(&self) -> Result<usize, Self::Error>;
fn is_empty(&self) -> Result<bool, Self::Error> {
Ok(self.len()? == 0)
}
}
pub trait CounterReconciler {
type Error: std::error::Error + From<StaleHeads>;
fn increment(&mut self, by: i64) -> Result<(), Self::Error>;
fn set(&mut self, value: i64) -> Result<(), Self::Error>;
}
pub trait TextReconciler {
type Error: std::error::Error + From<StaleHeads>;
fn splice<S: AsRef<str>>(
&mut self,
pos: usize,
delete: isize,
insert: S,
) -> Result<(), Self::Error>;
fn heads(&self) -> &[automerge::ChangeHash];
fn update<S: AsRef<str>>(&mut self, new_text: S) -> Result<(), Self::Error>;
}
#[derive(Clone, PartialEq, Eq)]
pub struct NoKey;
#[derive(Debug)]
pub enum LoadKey<K> {
NoKey,
KeyNotFound,
Found(K),
}
impl<K> LoadKey<K> {
pub fn map<L, F: FnOnce(K) -> L>(self, f: F) -> LoadKey<L> {
match self {
Self::NoKey => LoadKey::NoKey,
Self::KeyNotFound => LoadKey::KeyNotFound,
Self::Found(k) => LoadKey::Found(f(k)),
}
}
}
pub trait Reconcile {
type Key<'a>: PartialEq;
fn reconcile<R: Reconciler>(&self, reconciler: R) -> Result<(), R::Error>;
fn hydrate_key<'a, D: ReadDoc>(
#[allow(unused_variables)] doc: &D,
#[allow(unused_variables)] obj: &automerge::ObjId,
#[allow(unused_variables)] prop: Prop<'_>,
) -> Result<LoadKey<Self::Key<'a>>, ReconcileError> {
Ok(LoadKey::NoKey)
}
fn key(&self) -> LoadKey<Self::Key<'_>> {
LoadKey::NoKey
}
}
#[derive(Debug, thiserror::Error)]
pub enum ReconcileError {
#[error(transparent)]
Automerge(#[from] automerge::AutomergeError),
#[error("the top level object must reconcile to a map")]
TopLevelNotMap,
#[error(transparent)]
StaleHeads(#[from] StaleHeads),
}
#[derive(Debug, thiserror::Error)]
#[error("the data to be reconciled is stale, expected heads were {expected:?} but found {found:?}")]
pub struct StaleHeads {
pub expected: Vec<automerge::ChangeHash>,
pub found: Vec<automerge::ChangeHash>,
}
struct RootReconciler<'a, D> {
heads: Vec<automerge::ChangeHash>,
doc: &'a mut D,
}
impl<'a, D: Doc> Reconciler for RootReconciler<'a, D> {
type Error = ReconcileError;
type Map<'b>
= InMap<'b, D>
where
Self: 'b;
type Seq<'b>
= InSeq<'b, D>
where
Self: 'b;
type Text<'b>
= InText<'b, D>
where
Self: 'b;
type Counter<'b>
= AtCounter<'b, D>
where
Self: 'b;
fn none(&mut self) -> Result<(), Self::Error> {
Err(ReconcileError::TopLevelNotMap)
}
fn bytes<B: AsRef<[u8]>>(&mut self, _value: B) -> Result<(), Self::Error> {
Err(ReconcileError::TopLevelNotMap)
}
fn boolean(&mut self, _value: bool) -> Result<(), Self::Error> {
Err(ReconcileError::TopLevelNotMap)
}
fn timestamp(&mut self, _value: i64) -> Result<(), Self::Error> {
Err(ReconcileError::TopLevelNotMap)
}
fn str<S: AsRef<str>>(&mut self, _value: S) -> Result<(), Self::Error> {
Err(ReconcileError::TopLevelNotMap)
}
fn u64(&mut self, _value: u64) -> Result<(), Self::Error> {
Err(ReconcileError::TopLevelNotMap)
}
fn i64(&mut self, _value: i64) -> Result<(), Self::Error> {
Err(ReconcileError::TopLevelNotMap)
}
fn f64(&mut self, _value: f64) -> Result<(), Self::Error> {
Err(ReconcileError::TopLevelNotMap)
}
fn map(&mut self) -> Result<InMap<'_, D>, Self::Error> {
Ok(InMap {
heads: &self.heads,
current_obj: automerge::ROOT,
doc: self.doc,
})
}
fn seq(&mut self) -> Result<InSeq<'_, D>, Self::Error> {
Err(ReconcileError::TopLevelNotMap)
}
fn text(&mut self) -> Result<Self::Text<'_>, Self::Error> {
Err(ReconcileError::TopLevelNotMap)
}
fn counter(&mut self) -> Result<Self::Counter<'a>, Self::Error> {
Err(ReconcileError::TopLevelNotMap)
}
fn heads(&self) -> &[automerge::ChangeHash] {
&self.heads
}
}
enum PropAction<'a> {
Put { prop: Prop<'a>, create: bool },
Insert(u32),
}
impl PropAction<'_> {
fn get_target<'b, D: Doc>(
&self,
doc: &'b D,
obj: &automerge::ObjId,
) -> Result<Option<(automerge::Value<'b>, automerge::ObjId)>, automerge::AutomergeError> {
match self {
Self::Put {
prop,
create: false,
} => doc.get(obj, prop),
Self::Put {
prop: _,
create: true,
} => Ok(None),
Self::Insert(_idx) => Ok(None),
}
}
fn create_target_obj<D: Doc>(
&self,
doc: &mut D,
obj: &automerge::ObjId,
objtype: automerge::ObjType,
) -> Result<automerge::ObjId, automerge::AutomergeError> {
match self {
Self::Put { prop, create: _ } => doc.put_object(obj, prop, objtype),
Self::Insert(idx) => doc.insert_object(obj, (*idx) as usize, objtype),
}
}
fn create_primitive<V: Into<automerge::ScalarValue>, D: Doc>(
&self,
doc: &mut D,
obj: &automerge::ObjId,
value: V,
) -> Result<(), automerge::AutomergeError> {
match self {
Self::Put { prop: p, create: _ } => doc.put(obj, p, value),
Self::Insert(idx) => doc.insert(obj, (*idx) as usize, value),
}
}
}
struct PropReconciler<'a, D> {
heads: &'a [automerge::ChangeHash],
doc: &'a mut D,
current_obj: automerge::ObjId,
action: PropAction<'a>,
}
impl<D: Doc> Reconciler for PropReconciler<'_, D> {
type Error = ReconcileError;
type Map<'b>
= InMap<'b, D>
where
Self: 'b;
type Seq<'b>
= InSeq<'b, D>
where
Self: 'b;
type Text<'b>
= InText<'b, D>
where
Self: 'b;
type Counter<'b>
= AtCounter<'b, D>
where
Self: 'b;
fn none(&mut self) -> Result<(), Self::Error> {
self.action
.create_primitive(self.doc, &self.current_obj, ScalarValue::Null)
.map_err(ReconcileError::from)
}
fn bytes<B: AsRef<[u8]>>(&mut self, value: B) -> Result<(), Self::Error> {
self.action
.create_primitive(self.doc, &self.current_obj, value.as_ref().to_vec())
.map_err(ReconcileError::from)
}
fn boolean(&mut self, value: bool) -> Result<(), Self::Error> {
self.action
.create_primitive(self.doc, &self.current_obj, value)
.map_err(ReconcileError::from)
}
fn timestamp(&mut self, value: i64) -> Result<(), Self::Error> {
self.action
.create_primitive(self.doc, &self.current_obj, ScalarValue::Timestamp(value))
.map_err(ReconcileError::from)
}
fn str<S: AsRef<str>>(&mut self, value: S) -> Result<(), Self::Error> {
self.action
.create_primitive(self.doc, &self.current_obj, value.as_ref())
.map_err(ReconcileError::from)
}
fn u64(&mut self, value: u64) -> Result<(), Self::Error> {
self.action
.create_primitive(self.doc, &self.current_obj, value)
.map_err(ReconcileError::from)
}
fn i64(&mut self, value: i64) -> Result<(), Self::Error> {
self.action
.create_primitive(self.doc, &self.current_obj, value)
.map_err(ReconcileError::from)
}
fn f64(&mut self, value: f64) -> Result<(), Self::Error> {
self.action
.create_primitive(self.doc, &self.current_obj, value)
.map_err(ReconcileError::from)
}
fn map(&mut self) -> Result<InMap<'_, D>, Self::Error> {
use automerge::{ObjType, Value};
let map_id = if let Some((Value::Object(ObjType::Map), id)) =
self.action.get_target(self.doc, &self.current_obj)?
{
id
} else {
self.action
.create_target_obj(self.doc, &self.current_obj, ObjType::Map)?
};
Ok(InMap {
heads: self.heads,
current_obj: map_id,
doc: self.doc,
})
}
fn seq(&mut self) -> Result<InSeq<'_, D>, Self::Error> {
use automerge::{ObjType, Value};
let seq_id = if let Some((Value::Object(ObjType::List), id)) =
self.action.get_target(self.doc, &self.current_obj)?
{
id
} else {
self.action
.create_target_obj(self.doc, &self.current_obj, ObjType::List)?
};
Ok(InSeq {
heads: self.heads,
obj: seq_id,
doc: self.doc,
})
}
fn text(&mut self) -> Result<Self::Text<'_>, Self::Error> {
use automerge::{ObjType, Value};
let text_id = if let Some((Value::Object(ObjType::Text), id)) =
self.action.get_target(self.doc, &self.current_obj)?
{
id
} else {
self.action
.create_target_obj(self.doc, &self.current_obj, ObjType::Text)?
};
Ok(InText {
heads: self.heads,
obj: text_id,
doc: self.doc,
})
}
fn counter(&mut self) -> Result<Self::Counter<'_>, Self::Error> {
Ok(AtCounter {
doc: self.doc,
current_obj: &self.current_obj,
action: &self.action,
})
}
fn heads(&self) -> &[automerge::ChangeHash] {
self.heads
}
}
struct AtCounter<'a, D> {
doc: &'a mut D,
current_obj: &'a automerge::ObjId,
action: &'a PropAction<'a>,
}
impl<D: Doc> CounterReconciler for AtCounter<'_, D> {
type Error = ReconcileError;
fn increment(&mut self, by: i64) -> Result<(), Self::Error> {
use automerge::Value;
match &self.action {
PropAction::Put { prop, create: _ } => {
if let Some((Value::Scalar(s), _)) = self.doc.get(self.current_obj, prop)? {
if let ScalarValue::Counter(_) = s.as_ref() {
self.doc.increment(self.current_obj, prop, by)?;
return Ok(());
}
}
self.doc
.put(self.current_obj, prop, ScalarValue::Counter(by.into()))?;
Ok(())
}
PropAction::Insert(idx) => {
self.doc.insert(
self.current_obj,
(*idx) as usize,
ScalarValue::Counter(by.into()),
)?;
Ok(())
}
}
}
fn set(&mut self, value: i64) -> Result<(), Self::Error> {
self.action.create_primitive(
self.doc,
self.current_obj,
automerge::ScalarValue::Counter(value.into()),
)?;
Ok(())
}
}
struct InMap<'a, D> {
heads: &'a [automerge::ChangeHash],
doc: &'a mut D,
current_obj: automerge::ObjId,
}
impl<D: Doc> MapReconciler for InMap<'_, D> {
type Error = ReconcileError;
type EntriesIter<'b>
= InMapEntries<'b>
where
Self: 'b;
fn entries(&self) -> Self::EntriesIter<'_> {
InMapEntries {
map_range: self.doc.map_range(&self.current_obj, ..),
}
}
fn entry<P: AsRef<str>>(&self, prop: P) -> Option<automerge::Value<'_>> {
self.doc
.get(&self.current_obj, prop.as_ref())
.ok()
.flatten()
.map(|v| v.0)
}
fn put<R: Reconcile, P: AsRef<str>>(&mut self, prop: P, value: R) -> Result<(), Self::Error> {
let existing_key = self.hydrate_entry_key::<R, _>(prop.as_ref())?;
let new_key = value.key();
let create_new_object = match (existing_key, new_key) {
(LoadKey::Found(before), LoadKey::Found(after)) if before == after => false,
(LoadKey::Found(_), _) | (_, LoadKey::Found(_)) => true,
_ => false,
};
if create_new_object {
if let Some(hydrate_val) = reconcile_to_am_hydrate(value) {
self.doc
.batch_create(&self.current_obj, prop.as_ref(), hydrate_val, false)?;
}
return Ok(());
}
let reconciler = PropReconciler {
heads: self.heads,
current_obj: self.current_obj.clone(),
doc: self.doc,
action: PropAction::Put {
prop: prop.as_ref().into(),
create: create_new_object,
},
};
value.reconcile(reconciler)?;
Ok(())
}
fn delete<P: AsRef<str>>(&mut self, prop: P) -> Result<(), Self::Error> {
self.doc
.delete(&self.current_obj, prop.as_ref())
.map_err(ReconcileError::from)
}
fn hydrate_entry_key<'b, R: Reconcile, P: AsRef<str>>(
&self,
prop: P,
) -> Result<LoadKey<R::Key<'b>>, Self::Error> {
R::hydrate_key(self.doc, &self.current_obj, prop.as_ref().into())
}
}
struct InMapEntries<'a> {
map_range: automerge::iter::MapRange<'a>,
}
impl<'a> Iterator for InMapEntries<'a> {
type Item = (Cow<'a, str>, automerge::ValueRef<'a>);
fn next(&mut self) -> Option<Self::Item> {
self.map_range
.next()
.map(|automerge::iter::MapRangeItem { key, value, .. }| (key, value))
}
}
struct InSeq<'a, D> {
heads: &'a [automerge::ChangeHash],
doc: &'a mut D,
obj: automerge::ObjId,
}
struct ItemsInSeq<'a> {
list_range: automerge::iter::ListRange<'a>,
}
impl<'a> Iterator for ItemsInSeq<'a> {
type Item = automerge::ValueRef<'a>;
fn next(&mut self) -> Option<Self::Item> {
self.list_range.next().map(|i| i.value)
}
}
impl<D: Doc> SeqReconciler for InSeq<'_, D> {
type Error = ReconcileError;
type ItemIter<'b>
= ItemsInSeq<'b>
where
Self: 'b;
fn items<'b>(&'_ self) -> Self::ItemIter<'_> {
ItemsInSeq {
list_range: self.doc.list_range(&self.obj, ..),
}
}
fn get(&'_ self, index: usize) -> Result<Option<automerge::Value<'_>>, Self::Error> {
Ok(self.doc.get(&self.obj, index)?.map(|(v, _)| v))
}
fn insert<R: Reconcile>(&mut self, index: usize, value: R) -> Result<(), Self::Error> {
if let Some(hydrate_val) = reconcile_to_am_hydrate(value) {
self.doc.batch_create(&self.obj, index, hydrate_val, true)?;
}
Ok(())
}
fn set<R: Reconcile>(&mut self, index: usize, value: R) -> Result<(), Self::Error> {
let existing_key = self.hydrate_item_key::<R>(index)?;
let new_key = value.key();
let create_new_object = match (existing_key, new_key) {
(LoadKey::Found(before), LoadKey::Found(after)) if before == after => false,
(LoadKey::Found(_), _) | (_, LoadKey::Found(_)) => true,
_ => false,
};
if create_new_object {
if let Some(hydrate_val) = reconcile_to_am_hydrate(value) {
self.doc
.batch_create(&self.obj, index, hydrate_val, false)?;
}
return Ok(());
}
let reconciler = PropReconciler {
heads: self.heads,
doc: self.doc,
current_obj: self.obj.clone(),
action: PropAction::Put {
prop: index.into(),
create: create_new_object,
},
};
value.reconcile(reconciler)?;
Ok(())
}
fn delete<'b>(&mut self, index: usize) -> Result<(), Self::Error> {
self.doc
.delete(&self.obj, index)
.map_err(ReconcileError::from)
}
fn len(&self) -> Result<usize, Self::Error> {
Ok(self.doc.length(&self.obj))
}
fn hydrate_item_key<'b, R: Reconcile>(
&self,
index: usize,
) -> Result<LoadKey<R::Key<'b>>, Self::Error> {
if self.doc.get(&self.obj, index)?.is_some() {
R::hydrate_key(self.doc, &self.obj, index.into())
} else {
Ok(LoadKey::KeyNotFound)
}
}
}
struct InText<'a, D> {
heads: &'a [automerge::ChangeHash],
doc: &'a mut D,
obj: automerge::ObjId,
}
impl<D: Doc> TextReconciler for InText<'_, D> {
type Error = ReconcileError;
fn splice<S: AsRef<str>>(
&mut self,
pos: usize,
delete: isize,
text: S,
) -> Result<(), Self::Error> {
self.doc
.splice_text(&self.obj, pos, delete, text.as_ref())?;
Ok(())
}
fn heads(&self) -> &[automerge::ChangeHash] {
self.heads
}
fn update<S: AsRef<str>>(&mut self, new_text: S) -> Result<(), Self::Error> {
self.doc.update_text(&self.obj, new_text.as_ref())?;
Ok(())
}
}
pub fn reconcile<R: Reconcile, D: Doc>(doc: &mut D, value: R) -> Result<(), ReconcileError> {
if doc.is_empty() {
if let Some(value) = reconcile_to_am_hydrate(value) {
let automerge::hydrate::Value::Map(map) = value else {
return Err(ReconcileError::TopLevelNotMap);
};
doc.init_root_from_hydrate(&map)?;
}
return Ok(());
}
let reconciler = RootReconciler {
heads: doc.get_heads(),
doc,
};
value.reconcile(reconciler)?;
Ok(())
}
pub fn reconcile_prop<'a, D: Doc, R: Reconcile, O: AsRef<automerge::ObjId>, P: Into<Prop<'a>>>(
doc: &mut D,
obj: O,
prop: P,
value: R,
) -> Result<(), ReconcileError> {
let prop = prop.into();
let heads = doc.get_heads();
let existing_key = R::hydrate_key(doc, obj.as_ref(), prop.clone())?;
let new_key = value.key();
let create_new_object = match (existing_key, new_key) {
(LoadKey::Found(before), LoadKey::Found(after)) if before == after => false,
(LoadKey::Found(_), _) | (_, LoadKey::Found(_)) => true,
_ => false,
};
let reconciler = PropReconciler {
heads: &heads,
doc,
action: PropAction::Put {
prop: prop.clone(),
create: create_new_object,
},
current_obj: obj.as_ref().clone(),
};
value.reconcile(reconciler)?;
Ok(())
}
pub fn reconcile_insert<D: Doc, R: Reconcile>(
doc: &mut D,
obj: automerge::ObjId,
idx: usize,
value: R,
) -> Result<(), ReconcileError> {
let heads = doc.get_heads();
let reconciler = PropReconciler {
heads: &heads,
doc,
action: PropAction::Insert(idx as u32),
current_obj: obj,
};
value.reconcile(reconciler)?;
Ok(())
}
pub fn hydrate_key<'a, D: ReadDoc, H: crate::Hydrate + Clone>(
doc: &D,
obj: &automerge::ObjId,
outer: Prop<'a>,
inner: Prop<'a>,
) -> Result<LoadKey<H>, ReconcileError> {
use crate::hydrate::HydrateResultExt;
Ok(
crate::hydrate::hydrate_path(doc, obj, vec![outer, inner].into_iter())
.strip_unexpected()?
.map(LoadKey::Found)
.unwrap_or(LoadKey::KeyNotFound),
)
}
#[cfg(test)]
mod tests {
use super::*;
use automerge_test::{assert_doc, list, map};
struct Contact {
name: String,
addresses: Vec<Address>,
id: u64,
}
struct Address {
line_one: String,
line_two: String,
}
impl Reconcile for Address {
type Key<'a> = NoKey;
fn reconcile<R: Reconciler>(&self, mut reconciler: R) -> Result<(), R::Error> {
let mut map = reconciler.map()?;
map.put("line_one", &self.line_one)?;
map.put("line_two", &self.line_two)?;
Ok(())
}
}
impl Reconcile for Contact {
type Key<'a> = NoKey;
fn reconcile<R: Reconciler>(&self, mut reconciler: R) -> Result<(), R::Error> {
let mut map = reconciler.map()?;
map.put("addresses", &self.addresses)?;
map.put("name", &self.name)?;
map.put("id", self.id)?;
Ok(())
}
}
#[test]
fn basic_reconciliation() {
let mut bob = Contact {
name: "bob".to_string(),
id: 1,
addresses: vec![Address {
line_one: "line one".to_string(),
line_two: "line two".to_string(),
}],
};
let mut doc = automerge::AutoCommit::new();
reconcile(&mut doc, &bob).unwrap();
assert_doc!(
&doc,
map! {
"name" => { "bob" },
"id" => { 1_u64 },
"addresses" => { list!{
{ map! {
"line_one" => { "line one" },
"line_two" => { "line two" },
}}
}
}}
);
let mut doc2 = doc.clone().with_actor("actor2".as_bytes().into());
bob.name = "Bobsson".to_string();
reconcile(&mut doc2, &bob).unwrap();
let mut doc3 = doc.clone().with_actor("actor3".as_bytes().into());
bob.addresses[0].line_one = "Line one the premier".to_string();
reconcile(&mut doc3, &bob).unwrap();
doc.merge(&mut doc2).unwrap();
doc.merge(&mut doc3).unwrap();
assert_doc!(
doc.document(),
map! {
"name" => { "Bobsson" },
"id" => { 1_u64 },
"addresses" => { list!{
{ map! {
"line_one" => { "Line one the premier" },
"line_two" => { "line two" },
}}
}
}}
);
}
#[test]
fn test_with_transaction() {
let mut doc = automerge::Automerge::new();
let mut tx = doc.transaction();
let bob = Contact {
name: "bob".to_string(),
id: 1,
addresses: vec![Address {
line_one: "line one".to_string(),
line_two: "line two".to_string(),
}],
};
reconcile(&mut tx, &bob).unwrap();
tx.commit();
assert_doc!(
&doc,
map! {
"name" => { "bob" },
"id" => { 1_u64 },
"addresses" => { list!{
{ map! {
"line_one" => { "line one" },
"line_two" => { "line two" },
}}
}
}}
);
}
struct Foo {
bar: crate::hydrate::MaybeMissing<String>,
}
impl crate::Reconcile for Foo {
type Key<'a> = NoKey;
fn reconcile<R: Reconciler>(&self, mut reconciler: R) -> Result<(), R::Error> {
let mut map = reconciler.map()?;
map.put("bar", &self.bar)?;
Ok(())
}
}
#[test]
fn reconcile_maybe_missing_present() {
let mut doc = automerge::AutoCommit::new();
reconcile(
&mut doc,
&Foo {
bar: crate::MaybeMissing::Present("baz".to_string()),
},
)
.unwrap();
let val = doc.get(&automerge::ROOT, "bar").unwrap().unwrap();
assert_eq!(
val.0,
automerge::Value::Scalar(std::borrow::Cow::Owned(ScalarValue::Str("baz".into())))
);
}
#[test]
fn reconcile_maybe_missing_not_present() {
let mut doc = automerge::AutoCommit::new();
reconcile(
&mut doc,
&Foo {
bar: crate::MaybeMissing::Missing,
},
)
.unwrap();
let val = doc.get(&automerge::ROOT, "bar").unwrap();
assert!(val.is_none());
}
}