automerge/rust/automerge/src/parents.rs
alexjg 9b44a75f69
fix: don't panic when generating parents for hidden objects (#500)
Problem: the `OpSet::export_key` method uses `query::ElemIdPos` to
determine the index of sequence elements when exporting a key. This
query returned `None` for invisible elements. The `Parents` iterator
which is used to generate paths to objects in patches in
`automerge-wasm` used `export_key`. The end result is that applying a
remote change which deletes an object in a sequence would panic as it
tries to generate a path for an invisible object.

Solution: modify `query::ElemIdPos` to include invisible objects. This
does mean that the path generated will refer to the previous visible
object in the sequence as it's index, but this is probably fine as for
an invisible object the path shouldn't be used anyway.

While we're here also change the return value of `OpSet::export_key` to
an `Option` and make `query::Index::ops` private as obeisance to the
Lady of the Golden Blade.
2023-01-19 21:11:36 +00:00

106 lines
2.9 KiB
Rust

use crate::op_set;
use crate::op_set::OpSet;
use crate::types::{ListEncoding, ObjId};
use crate::{exid::ExId, Prop};
#[derive(Debug)]
pub struct Parents<'a> {
pub(crate) obj: ObjId,
pub(crate) ops: &'a OpSet,
}
impl<'a> Parents<'a> {
// returns the path to the object
// works even if the object or a parent has been deleted
pub fn path(&mut self) -> Vec<(ExId, Prop)> {
let mut path = self
.map(|Parent { obj, prop, .. }| (obj, prop))
.collect::<Vec<_>>();
path.reverse();
path
}
// returns the path to the object
// if the object or one of its parents has been deleted or conflicted out
// returns none
pub fn visible_path(&mut self) -> Option<Vec<(ExId, Prop)>> {
let mut path = Vec::new();
for Parent { obj, prop, visible } in self {
if !visible {
return None;
}
path.push((obj, prop))
}
path.reverse();
Some(path)
}
}
impl<'a> Iterator for Parents<'a> {
type Item = Parent;
fn next(&mut self) -> Option<Self::Item> {
if self.obj.is_root() {
None
} else if let Some(op_set::Parent { obj, key, visible }) = self.ops.parent_object(&self.obj)
{
self.obj = obj;
Some(Parent {
obj: self.ops.id_to_exid(self.obj.0),
prop: self
.ops
.export_key(self.obj, key, ListEncoding::List)
.unwrap(),
visible,
})
} else {
None
}
}
}
#[derive(Debug, PartialEq, Eq)]
pub struct Parent {
pub obj: ExId,
pub prop: Prop,
pub visible: bool,
}
#[cfg(test)]
mod tests {
use super::Parent;
use crate::{transaction::Transactable, Prop};
#[test]
fn test_invisible_parents() {
// Create a document with a list of objects, then delete one of the objects, then generate
// a path to the deleted object.
let mut doc = crate::AutoCommit::new();
let list = doc
.put_object(crate::ROOT, "list", crate::ObjType::List)
.unwrap();
let obj1 = doc.insert_object(&list, 0, crate::ObjType::Map).unwrap();
let _obj2 = doc.insert_object(&list, 1, crate::ObjType::Map).unwrap();
doc.put(&obj1, "key", "value").unwrap();
doc.delete(&list, 0).unwrap();
let mut parents = doc.parents(&obj1).unwrap().collect::<Vec<_>>();
parents.reverse();
assert_eq!(
parents,
vec![
Parent {
obj: crate::ROOT,
prop: Prop::Map("list".to_string()),
visible: true,
},
Parent {
obj: list,
prop: Prop::Seq(0),
visible: false,
},
]
);
}
}