From 783fd40f63bbb95af008befc4283918cd9c53319 Mon Sep 17 00:00:00 2001 From: "Nathan.fooo" <86001920+appflowy@users.noreply.github.com> Date: Sat, 29 Oct 2022 20:54:11 +0800 Subject: [PATCH] Feat/op compose (#1392) --- .../flowy-document/src/editor/document.rs | 6 +- .../src/editor/document_serde.rs | 39 +- .../flowy-document/tests/editor/serde_test.rs | 2 +- .../tests/new_document/script.rs | 4 +- .../src/services/workspace/event_handler.rs | 1 - .../lib-ot/src/core/attributes/attribute.rs | 9 +- shared-lib/lib-ot/src/core/node_tree/mod.rs | 1 + shared-lib/lib-ot/src/core/node_tree/node.rs | 63 ++- .../lib-ot/src/core/node_tree/operation.rs | 193 ++++++-- .../src/core/node_tree/operation_serde.rs | 215 ++------ shared-lib/lib-ot/src/core/node_tree/path.rs | 2 +- .../lib-ot/src/core/node_tree/transaction.rs | 158 +++--- shared-lib/lib-ot/src/core/node_tree/tree.rs | 120 ++++- shared-lib/lib-ot/src/errors.rs | 5 +- shared-lib/lib-ot/tests/node/mod.rs | 5 +- .../tests/node/operation_delete_test.rs | 178 +++++++ .../lib-ot/tests/node/operation_delta_test.rs | 41 ++ .../tests/node/operation_insert_test.rs | 460 ++++++++++++++++++ .../lib-ot/tests/node/operation_test.rs | 170 ------- shared-lib/lib-ot/tests/node/script.rs | 66 ++- shared-lib/lib-ot/tests/node/serde_test.rs | 58 ++- .../tests/node/transaction_compose_test.rs | 104 ++++ shared-lib/lib-ot/tests/node/tree_test.rs | 130 +---- 23 files changed, 1340 insertions(+), 690 deletions(-) create mode 100644 shared-lib/lib-ot/tests/node/operation_delete_test.rs create mode 100644 shared-lib/lib-ot/tests/node/operation_delta_test.rs create mode 100644 shared-lib/lib-ot/tests/node/operation_insert_test.rs delete mode 100644 shared-lib/lib-ot/tests/node/operation_test.rs create mode 100644 shared-lib/lib-ot/tests/node/transaction_compose_test.rs diff --git a/frontend/rust-lib/flowy-document/src/editor/document.rs b/frontend/rust-lib/flowy-document/src/editor/document.rs index 21894c3a66..bf3ff469c8 100644 --- a/frontend/rust-lib/flowy-document/src/editor/document.rs +++ b/frontend/rust-lib/flowy-document/src/editor/document.rs @@ -2,9 +2,7 @@ use bytes::Bytes; use flowy_error::{FlowyError, FlowyResult}; use flowy_revision::{RevisionCompress, RevisionObjectDeserializer, RevisionObjectSerializer}; use flowy_sync::entities::revision::Revision; -use lib_ot::core::{ - Body, Extension, NodeDataBuilder, NodeOperation, NodeTree, NodeTreeContext, Selection, Transaction, -}; +use lib_ot::core::{Extension, NodeDataBuilder, NodeOperation, NodeTree, NodeTreeContext, Selection, Transaction}; use lib_ot::text_delta::DeltaTextOperationBuilder; #[derive(Debug)] @@ -46,7 +44,7 @@ pub(crate) fn make_tree_context() -> NodeTreeContext { pub fn initial_document_content() -> String { let delta = DeltaTextOperationBuilder::new().insert("").build(); - let node_data = NodeDataBuilder::new("text").insert_body(Body::Delta(delta)).build(); + let node_data = NodeDataBuilder::new("text").insert_delta(delta).build(); let editor_node = NodeDataBuilder::new("editor").add_node_data(node_data).build(); let node_operation = NodeOperation::Insert { path: vec![0].into(), diff --git a/frontend/rust-lib/flowy-document/src/editor/document_serde.rs b/frontend/rust-lib/flowy-document/src/editor/document_serde.rs index a3c9257a32..460c6dd29f 100644 --- a/frontend/rust-lib/flowy-document/src/editor/document_serde.rs +++ b/frontend/rust-lib/flowy-document/src/editor/document_serde.rs @@ -117,7 +117,8 @@ impl DocumentTransaction { impl std::convert::From for DocumentTransaction { fn from(transaction: Transaction) -> Self { - let (before_selection, after_selection) = match transaction.extension { + let (operations, extension) = transaction.split(); + let (before_selection, after_selection) = match extension { Extension::Empty => (Selection::default(), Selection::default()), Extension::TextSelection { before_selection, @@ -126,9 +127,7 @@ impl std::convert::From for DocumentTransaction { }; DocumentTransaction { - operations: transaction - .operations - .into_inner() + operations: operations .into_iter() .map(|operation| operation.as_ref().into()) .collect(), @@ -139,19 +138,16 @@ impl std::convert::From for DocumentTransaction { } impl std::convert::From for Transaction { - fn from(transaction: DocumentTransaction) -> Self { - Transaction { - operations: transaction - .operations - .into_iter() - .map(|operation| operation.into()) - .collect::>() - .into(), - extension: Extension::TextSelection { - before_selection: transaction.before_selection, - after_selection: transaction.after_selection, - }, + fn from(document_transaction: DocumentTransaction) -> Self { + let mut transaction = Transaction::new(); + for document_operation in document_transaction.operations { + transaction.push_operation(document_operation); } + transaction.extension = Extension::TextSelection { + before_selection: document_transaction.before_selection, + after_selection: document_transaction.after_selection, + }; + transaction } } @@ -374,6 +370,17 @@ mod tests { let _ = serde_json::to_string_pretty(&document).unwrap(); } + // #[test] + // fn document_operation_compose_test() { + // let json = include_str!("./test.json"); + // let transaction: Transaction = Transaction::from_json(json).unwrap(); + // let json = transaction.to_json().unwrap(); + // // let transaction: Transaction = Transaction::from_json(&json).unwrap(); + // let document = Document::from_transaction(transaction).unwrap(); + // let content = document.get_content(false).unwrap(); + // println!("{}", json); + // } + const EXAMPLE_DOCUMENT: &str = r#"{ "document": { "type": "editor", diff --git a/frontend/rust-lib/flowy-document/tests/editor/serde_test.rs b/frontend/rust-lib/flowy-document/tests/editor/serde_test.rs index 4e34d8ca91..9941424dc7 100644 --- a/frontend/rust-lib/flowy-document/tests/editor/serde_test.rs +++ b/frontend/rust-lib/flowy-document/tests/editor/serde_test.rs @@ -89,7 +89,7 @@ fn delta_deserialize_null_test() { let delta1 = DeltaTextOperations::from_json(json).unwrap(); let mut attribute = BuildInTextAttribute::Bold(true); - attribute.remove_value(); + attribute.clear(); let delta2 = DeltaOperationBuilder::new() .retain_with_attributes(7, attribute.into()) diff --git a/frontend/rust-lib/flowy-document/tests/new_document/script.rs b/frontend/rust-lib/flowy-document/tests/new_document/script.rs index 8694915f85..90a48e1e5e 100644 --- a/frontend/rust-lib/flowy-document/tests/new_document/script.rs +++ b/frontend/rust-lib/flowy-document/tests/new_document/script.rs @@ -3,7 +3,7 @@ use flowy_document::editor::{AppFlowyDocumentEditor, Document, DocumentTransacti use flowy_document::entities::DocumentVersionPB; use flowy_test::helper::ViewTest; use flowy_test::FlowySDKTest; -use lib_ot::core::{Body, Changeset, NodeDataBuilder, NodeOperation, Path, Transaction}; +use lib_ot::core::{Changeset, NodeDataBuilder, NodeOperation, Path, Transaction}; use lib_ot::text_delta::DeltaTextOperations; use std::sync::Arc; @@ -64,7 +64,7 @@ impl DocumentEditorTest { async fn run_script(&self, script: EditScript) { match script { EditScript::InsertText { path, delta } => { - let node_data = NodeDataBuilder::new("text").insert_body(Body::Delta(delta)).build(); + let node_data = NodeDataBuilder::new("text").insert_delta(delta).build(); let operation = NodeOperation::Insert { path, nodes: vec![node_data], diff --git a/frontend/rust-lib/flowy-folder/src/services/workspace/event_handler.rs b/frontend/rust-lib/flowy-folder/src/services/workspace/event_handler.rs index 3b576ae5dd..bce1f508b5 100644 --- a/frontend/rust-lib/flowy-folder/src/services/workspace/event_handler.rs +++ b/frontend/rust-lib/flowy-folder/src/services/workspace/event_handler.rs @@ -119,7 +119,6 @@ fn read_workspaces_on_server( let workspace_revs = server.read_workspace(&token, params).await?; let _ = persistence .begin_transaction(|transaction| { - tracing::trace!("Save {} workspace", workspace_revs.len()); for workspace_rev in &workspace_revs { let m_workspace = workspace_rev.clone(); let app_revs = m_workspace.apps.clone(); diff --git a/shared-lib/lib-ot/src/core/attributes/attribute.rs b/shared-lib/lib-ot/src/core/attributes/attribute.rs index 0acabc1209..60830684f8 100644 --- a/shared-lib/lib-ot/src/core/attributes/attribute.rs +++ b/shared-lib/lib-ot/src/core/attributes/attribute.rs @@ -12,7 +12,14 @@ pub struct AttributeEntry { } impl AttributeEntry { - pub fn remove_value(&mut self) { + pub fn new, V: Into>(key: K, value: V) -> Self { + Self { + key: key.into(), + value: value.into(), + } + } + + pub fn clear(&mut self) { self.value.ty = None; self.value.value = None; } diff --git a/shared-lib/lib-ot/src/core/node_tree/mod.rs b/shared-lib/lib-ot/src/core/node_tree/mod.rs index a05819bbe3..417c4af03f 100644 --- a/shared-lib/lib-ot/src/core/node_tree/mod.rs +++ b/shared-lib/lib-ot/src/core/node_tree/mod.rs @@ -3,6 +3,7 @@ mod node; mod node_serde; mod operation; +mod operation_serde; mod path; mod transaction; mod transaction_serde; diff --git a/shared-lib/lib-ot/src/core/node_tree/node.rs b/shared-lib/lib-ot/src/core/node_tree/node.rs index 556d3617bb..f0ce53e6d8 100644 --- a/shared-lib/lib-ot/src/core/node_tree/node.rs +++ b/shared-lib/lib-ot/src/core/node_tree/node.rs @@ -1,7 +1,7 @@ use super::node_serde::*; use crate::core::attributes::{AttributeHashMap, AttributeKey, AttributeValue}; use crate::core::Body::Delta; -use crate::core::OperationTransform; +use crate::core::{AttributeEntry, OperationTransform}; use crate::errors::OTError; use crate::text_delta::DeltaTextOperations; use serde::{Deserialize, Serialize}; @@ -69,14 +69,18 @@ impl NodeDataBuilder { /// Inserts attributes to the builder's node. /// /// The attributes will be replace if they shared the same key - pub fn insert_attribute(mut self, key: AttributeKey, value: AttributeValue) -> Self { - self.node.attributes.insert(key, value); + pub fn insert_attribute, V: Into>(mut self, key: K, value: V) -> Self { + self.node.attributes.insert(key.into(), value); self } - /// Inserts a body to the builder's node - pub fn insert_body(mut self, body: Body) -> Self { - self.node.body = body; + pub fn insert_attribute_entry(mut self, entry: AttributeEntry) -> Self { + self.node.attributes.insert_entry(entry); + self + } + + pub fn insert_delta(mut self, delta: DeltaTextOperations) -> Self { + self.node.body = Body::Delta(delta); self } @@ -174,6 +178,18 @@ pub enum Changeset { } impl Changeset { + pub fn is_delta(&self) -> bool { + match self { + Changeset::Delta { .. } => true, + Changeset::Attributes { .. } => false, + } + } + pub fn is_attribute(&self) -> bool { + match self { + Changeset::Delta { .. } => false, + Changeset::Attributes { .. } => true, + } + } pub fn inverted(&self) -> Changeset { match self { Changeset::Delta { delta, inverted } => Changeset::Delta { @@ -186,6 +202,41 @@ impl Changeset { }, } } + + pub fn compose(&mut self, other: &Changeset) -> Result<(), OTError> { + match (self, other) { + ( + Changeset::Delta { delta, inverted }, + Changeset::Delta { + delta: other_delta, + inverted: _, + }, + ) => { + let original = delta.invert(inverted); + let new_delta = delta.compose(other_delta)?; + let new_inverted = new_delta.invert(&original); + + *delta = new_delta; + *inverted = new_inverted; + Ok(()) + } + ( + Changeset::Attributes { new, old }, + Changeset::Attributes { + new: other_new, + old: other_old, + }, + ) => { + *new = other_new.clone(); + *old = other_old.clone(); + Ok(()) + } + (left, right) => { + let err = format!("Compose changeset failed. {:?} can't compose {:?}", left, right); + Err(OTError::compose().context(err)) + } + } + } } /// [`Node`] represents as a leaf in the [`NodeTree`]. diff --git a/shared-lib/lib-ot/src/core/node_tree/operation.rs b/shared-lib/lib-ot/src/core/node_tree/operation.rs index 8698d26d28..d432b8456a 100644 --- a/shared-lib/lib-ot/src/core/node_tree/operation.rs +++ b/shared-lib/lib-ot/src/core/node_tree/operation.rs @@ -1,5 +1,6 @@ -use crate::core::{Changeset, NodeData, Path}; +use crate::core::{Body, Changeset, NodeData, OperationTransform, Path}; use crate::errors::OTError; + use serde::{Deserialize, Serialize}; use std::sync::Arc; @@ -33,7 +34,80 @@ impl NodeOperation { } } - pub fn invert(&self) -> NodeOperation { + pub fn is_update_delta(&self) -> bool { + match self { + NodeOperation::Insert { .. } => false, + NodeOperation::Update { path: _, changeset } => changeset.is_delta(), + NodeOperation::Delete { .. } => false, + } + } + + pub fn is_update_attribute(&self) -> bool { + match self { + NodeOperation::Insert { .. } => false, + NodeOperation::Update { path: _, changeset } => changeset.is_attribute(), + NodeOperation::Delete { .. } => false, + } + } + pub fn is_insert(&self) -> bool { + match self { + NodeOperation::Insert { .. } => true, + NodeOperation::Update { .. } => false, + NodeOperation::Delete { .. } => false, + } + } + pub fn can_compose(&self, other: &NodeOperation) -> bool { + if self.get_path() != other.get_path() { + return false; + } + if self.is_update_delta() && other.is_update_delta() { + return true; + } + + if self.is_update_attribute() && other.is_update_attribute() { + return true; + } + + if self.is_insert() && other.is_update_delta() { + return true; + } + false + } + + pub fn compose(&mut self, other: &NodeOperation) -> Result<(), OTError> { + match (self, other) { + ( + NodeOperation::Insert { path: _, nodes }, + NodeOperation::Update { + path: _other_path, + changeset, + }, + ) => { + match changeset { + Changeset::Delta { delta, inverted: _ } => { + if let Body::Delta(old_delta) = &mut nodes.last_mut().unwrap().body { + let new_delta = old_delta.compose(delta)?; + *old_delta = new_delta; + } + } + Changeset::Attributes { new: _, old: _ } => { + return Err(OTError::compose().context("Can't compose the attributes changeset")); + } + } + Ok(()) + } + ( + NodeOperation::Update { path: _, changeset }, + NodeOperation::Update { + path: _, + changeset: other_changeset, + }, + ) => changeset.compose(other_changeset), + (_left, _right) => Err(OTError::compose().context("Can't compose the operation")), + } + } + + pub fn inverted(&self) -> NodeOperation { match self { NodeOperation::Insert { path, nodes } => NodeOperation::Delete { path: path.clone(), @@ -99,52 +173,24 @@ impl NodeOperation { } } -#[derive(Debug, Clone, Serialize, Deserialize, Default)] +type OperationIndexMap = Vec>; + +#[derive(Debug, Clone, Default)] pub struct NodeOperations { - operations: Vec>, + inner: OperationIndexMap, } impl NodeOperations { - pub fn into_inner(self) -> Vec> { - self.operations + pub fn new() -> Self { + Self::default() } - pub fn push_op(&mut self, operation: NodeOperation) { - self.operations.push(Arc::new(operation)); - } - - pub fn extend(&mut self, other: NodeOperations) { - for operation in other.operations { - self.operations.push(operation); - } - } -} - -impl std::ops::Deref for NodeOperations { - type Target = Vec>; - - fn deref(&self) -> &Self::Target { - &self.operations - } -} - -impl std::ops::DerefMut for NodeOperations { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.operations - } -} - -impl std::convert::From> for NodeOperations { - fn from(operations: Vec) -> Self { - Self::new(operations) - } -} - -impl NodeOperations { - pub fn new(operations: Vec) -> Self { - Self { - operations: operations.into_iter().map(Arc::new).collect(), + pub fn from_operations(operations: Vec) -> Self { + let mut ops = Self::new(); + for op in operations { + ops.push_op(op) } + ops } pub fn from_bytes(bytes: Vec) -> Result { @@ -156,4 +202,69 @@ impl NodeOperations { let bytes = serde_json::to_vec(self).map_err(|err| OTError::serde().context(err))?; Ok(bytes) } + + pub fn values(&self) -> &Vec> { + &self.inner + } + + pub fn values_mut(&mut self) -> &mut Vec> { + &mut self.inner + } + + pub fn len(&self) -> usize { + self.values().len() + } + + pub fn is_empty(&self) -> bool { + self.inner.is_empty() + } + + pub fn into_inner(self) -> Vec> { + self.inner + } + + pub fn push_op>>(&mut self, other: T) { + let other = other.into(); + if let Some(last_operation) = self.inner.last_mut() { + if last_operation.can_compose(&other) { + let mut_operation = Arc::make_mut(last_operation); + if mut_operation.compose(&other).is_ok() { + return; + } + } + } + + // if let Some(operations) = self.inner.get_mut(other.get_path()) { + // if let Some(last_operation) = operations.last_mut() { + // if last_operation.can_compose(&other) { + // let mut_operation = Arc::make_mut(last_operation); + // if mut_operation.compose(&other).is_ok() { + // return; + // } + // } + // } + // } + // If the passed-in operation can't be composed, then append it to the end. + self.inner.push(other); + } + + pub fn compose(&mut self, other: NodeOperations) { + for operation in other.values() { + self.push_op(operation.clone()); + } + } + + pub fn inverted(&self) -> Self { + let mut operations = Self::new(); + for operation in self.values() { + operations.push_op(operation.inverted()); + } + operations + } +} + +impl std::convert::From> for NodeOperations { + fn from(operations: Vec) -> Self { + Self::from_operations(operations) + } } diff --git a/shared-lib/lib-ot/src/core/node_tree/operation_serde.rs b/shared-lib/lib-ot/src/core/node_tree/operation_serde.rs index 5135ca5d38..5bb5c48e29 100644 --- a/shared-lib/lib-ot/src/core/node_tree/operation_serde.rs +++ b/shared-lib/lib-ot/src/core/node_tree/operation_serde.rs @@ -1,196 +1,49 @@ -use crate::core::{AttributeHashMap, Changeset, Path}; -use crate::text_delta::TextOperations; -use serde::de::{self, MapAccess, Visitor}; -use serde::ser::SerializeMap; -use serde::{Deserializer, Serializer}; -use std::convert::TryInto; +use crate::core::{NodeOperation, NodeOperations}; +use serde::de::{SeqAccess, Visitor}; +use serde::ser::SerializeSeq; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; use std::fmt; -use std::marker::PhantomData; -#[allow(dead_code)] -pub fn serialize_changeset(path: &Path, changeset: &Changeset, serializer: S) -> Result -where - S: Serializer, -{ - let mut map = serializer.serialize_map(Some(3))?; - map.serialize_key("path")?; - map.serialize_value(path)?; - - match changeset { - Changeset::Delta { delta, inverted } => { - map.serialize_key("delta")?; - map.serialize_value(delta)?; - map.serialize_key("inverted")?; - map.serialize_value(inverted)?; - map.end() - } - Changeset::Attributes { new, old } => { - map.serialize_key("new")?; - map.serialize_value(new)?; - map.serialize_key("old")?; - map.serialize_value(old)?; - map.end() +impl Serialize for NodeOperations { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let operations = self.values(); + let mut seq = serializer.serialize_seq(Some(operations.len()))?; + for operation in operations { + let _ = seq.serialize_element(&operation)?; } + seq.end() } } -#[allow(dead_code)] -pub fn deserialize_changeset<'de, D>(deserializer: D) -> Result<(Path, Changeset), D::Error> -where - D: Deserializer<'de>, -{ - struct ChangesetVisitor(); +impl<'de> Deserialize<'de> for NodeOperations { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct NodeOperationsVisitor(); - impl<'de> Visitor<'de> for ChangesetVisitor { - type Value = (Path, Changeset); + impl<'de> Visitor<'de> for NodeOperationsVisitor { + type Value = NodeOperations; - fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - formatter.write_str("Expect Path and Changeset") - } + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("Expected node operation") + } - #[inline] - fn visit_map(self, mut map: V) -> Result - where - V: MapAccess<'de>, - { - let mut path: Option = None; - let mut delta_changeset = DeltaChangeset::::new(); - let mut attribute_changeset = AttributeChangeset::new(); - while let Some(key) = map.next_key()? { - match key { - "delta" => { - if delta_changeset.delta.is_some() { - return Err(de::Error::duplicate_field("delta")); - } - delta_changeset.delta = Some(map.next_value()?); - } - "inverted" => { - if delta_changeset.inverted.is_some() { - return Err(de::Error::duplicate_field("inverted")); - } - delta_changeset.inverted = Some(map.next_value()?); - } - "path" => { - if path.is_some() { - return Err(de::Error::duplicate_field("path")); - } - path = Some(map.next_value::()?) - } - "new" => { - if attribute_changeset.new.is_some() { - return Err(de::Error::duplicate_field("new")); - } - attribute_changeset.new = Some(map.next_value()?); - } - "old" => { - if attribute_changeset.old.is_some() { - return Err(de::Error::duplicate_field("old")); - } - attribute_changeset.old = Some(map.next_value()?); - } - other => { - tracing::warn!("Unexpected key: {}", other); - panic!() - } + fn visit_seq(self, mut seq: A) -> Result + where + A: SeqAccess<'de>, + { + let mut operations = NodeOperations::new(); + while let Some(operation) = seq.next_element::()? { + operations.push_op(operation); } + Ok(operations) } - if path.is_none() { - return Err(de::Error::missing_field("path")); - } - - let mut changeset: Changeset; - if !delta_changeset.is_empty() { - changeset = delta_changeset.try_into()? - } else { - changeset = attribute_changeset.try_into()?; - } - - Ok((path.unwrap(), changeset)) - } - } - deserializer.deserialize_any(ChangesetVisitor()) -} - -struct DeltaChangeset { - delta: Option, - inverted: Option, - error: PhantomData, -} - -impl DeltaChangeset { - fn new() -> Self { - Self { - delta: None, - inverted: None, - error: PhantomData, - } - } - - fn is_empty(&self) -> bool { - self.delta.is_none() && self.inverted.is_none() - } -} - -impl std::convert::TryInto for DeltaChangeset -where - E: de::Error, -{ - type Error = E; - - fn try_into(self) -> Result { - if self.delta.is_none() { - return Err(de::Error::missing_field("delta")); } - if self.inverted.is_none() { - return Err(de::Error::missing_field("inverted")); - } - let changeset = Changeset::Delta { - delta: self.delta.unwrap(), - inverted: self.inverted.unwrap(), - }; - - Ok(changeset) - } -} -struct AttributeChangeset { - new: Option, - old: Option, - error: PhantomData, -} - -impl AttributeChangeset { - fn new() -> Self { - Self { - new: Default::default(), - old: Default::default(), - error: PhantomData, - } - } - - fn is_empty(&self) -> bool { - self.new.is_none() && self.old.is_none() - } -} - -impl std::convert::TryInto for AttributeChangeset -where - E: de::Error, -{ - type Error = E; - - fn try_into(self) -> Result { - if self.new.is_none() { - return Err(de::Error::missing_field("new")); - } - - if self.old.is_none() { - return Err(de::Error::missing_field("old")); - } - - Ok(Changeset::Attributes { - new: self.new.unwrap(), - old: self.old.unwrap(), - }) + deserializer.deserialize_any(NodeOperationsVisitor()) } } diff --git a/shared-lib/lib-ot/src/core/node_tree/path.rs b/shared-lib/lib-ot/src/core/node_tree/path.rs index 6963a661f3..b754baa70f 100644 --- a/shared-lib/lib-ot/src/core/node_tree/path.rs +++ b/shared-lib/lib-ot/src/core/node_tree/path.rs @@ -23,7 +23,7 @@ use serde::{Deserialize, Serialize}; /// The path of Node A-1 will be [0,0] /// The path of Node A-2 will be [0,1] /// The path of Node B-2 will be [1,1] -#[derive(Clone, Serialize, Deserialize, Eq, PartialEq, Debug, Default)] +#[derive(Clone, Serialize, Deserialize, Eq, PartialEq, Debug, Default, Hash)] pub struct Path(pub Vec); impl Path { diff --git a/shared-lib/lib-ot/src/core/node_tree/transaction.rs b/shared-lib/lib-ot/src/core/node_tree/transaction.rs index 6c0e8bd0b5..70c017c218 100644 --- a/shared-lib/lib-ot/src/core/node_tree/transaction.rs +++ b/shared-lib/lib-ot/src/core/node_tree/transaction.rs @@ -1,13 +1,12 @@ use super::{Changeset, NodeOperations}; -use crate::core::attributes::AttributeHashMap; use crate::core::{NodeData, NodeOperation, NodeTree, Path}; use crate::errors::OTError; use indextree::NodeId; use serde::{Deserialize, Serialize}; use std::sync::Arc; + #[derive(Debug, Clone, Default, Serialize, Deserialize)] pub struct Transaction { - #[serde(flatten)] pub operations: NodeOperations, #[serde(default)] @@ -37,6 +36,16 @@ impl Transaction { Ok(bytes) } + pub fn from_json(s: &str) -> Result { + let serde_transaction: Transaction = serde_json::from_str(s).map_err(|err| OTError::serde().context(err))?; + let mut transaction = Self::new(); + transaction.extension = serde_transaction.extension; + for operation in serde_transaction.operations.into_inner() { + transaction.operations.push_op(operation); + } + Ok(transaction) + } + pub fn to_json(&self) -> Result { serde_json::to_string(&self).map_err(|err| OTError::serde().context(err)) } @@ -45,6 +54,10 @@ impl Transaction { self.operations.into_inner() } + pub fn split(self) -> (Vec>, Extension) { + (self.operations.into_inner(), self.extension) + } + pub fn push_operation>(&mut self, operation: T) { let operation = operation.into(); self.operations.push_op(operation); @@ -57,38 +70,26 @@ impl Transaction { pub fn transform(&self, other: &Transaction) -> Result { let mut other = other.clone(); other.extension = self.extension.clone(); - for other_operation in other.iter_mut() { + + for other_operation in other.operations.values_mut() { let other_operation = Arc::make_mut(other_operation); - for operation in self.operations.iter() { + for operation in self.operations.values() { operation.transform(other_operation); } } + Ok(other) } pub fn compose(&mut self, other: Transaction) -> Result<(), OTError> { // For the moment, just append `other` operations to the end of `self`. let Transaction { operations, extension } = other; - self.operations.extend(operations); + self.operations.compose(operations); self.extension = extension; Ok(()) } } -impl std::ops::Deref for Transaction { - type Target = Vec>; - - fn deref(&self) -> &Self::Target { - &self.operations - } -} - -impl std::ops::DerefMut for Transaction { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.operations - } -} - #[derive(Debug, Clone, Serialize, Deserialize)] pub enum Extension { Empty, @@ -121,18 +122,14 @@ pub struct Position { path: Path, offset: usize, } - -pub struct TransactionBuilder<'a> { - node_tree: &'a NodeTree, +#[derive(Default)] +pub struct TransactionBuilder { operations: NodeOperations, } -impl<'a> TransactionBuilder<'a> { - pub fn new(node_tree: &'a NodeTree) -> TransactionBuilder { - TransactionBuilder { - node_tree, - operations: NodeOperations::default(), - } +impl TransactionBuilder { + pub fn new() -> TransactionBuilder { + Self::default() } /// @@ -148,9 +145,9 @@ impl<'a> TransactionBuilder<'a> { /// // 0 -- text_1 /// use lib_ot::core::{NodeTree, NodeData, TransactionBuilder}; /// let mut node_tree = NodeTree::default(); - /// let transaction = TransactionBuilder::new(&node_tree) + /// let transaction = TransactionBuilder::new() /// .insert_nodes_at_path(0,vec![ NodeData::new("text_1")]) - /// .finalize(); + /// .build(); /// node_tree.apply_transaction(transaction).unwrap(); /// /// node_tree.node_id_at_path(vec![0]).unwrap(); @@ -178,9 +175,9 @@ impl<'a> TransactionBuilder<'a> { /// // |-- text /// use lib_ot::core::{NodeTree, NodeData, TransactionBuilder}; /// let mut node_tree = NodeTree::default(); - /// let transaction = TransactionBuilder::new(&node_tree) + /// let transaction = TransactionBuilder::new() /// .insert_node_at_path(0, NodeData::new("text")) - /// .finalize(); + /// .build(); /// node_tree.apply_transaction(transaction).unwrap(); /// ``` /// @@ -188,49 +185,50 @@ impl<'a> TransactionBuilder<'a> { self.insert_nodes_at_path(path, vec![node]) } - pub fn update_attributes_at_path(mut self, path: &Path, attributes: AttributeHashMap) -> Self { - match self.node_tree.get_node_at_path(path) { - Some(node) => { - let mut old_attributes = AttributeHashMap::new(); - for key in attributes.keys() { - let old_attrs = &node.attributes; - if let Some(value) = old_attrs.get(key.as_str()) { - old_attributes.insert(key.clone(), value.clone()); - } - } - - self.operations.push_op(NodeOperation::Update { - path: path.clone(), - changeset: Changeset::Attributes { - new: attributes, - old: old_attributes, - }, - }); - } - None => tracing::warn!("Update attributes at path: {:?} failed. Node is not exist", path), - } + pub fn update_node_at_path>(mut self, path: T, changeset: Changeset) -> Self { + self.operations.push_op(NodeOperation::Update { + path: path.into(), + changeset, + }); self } + // + // pub fn update_delta_at_path>( + // mut self, + // path: T, + // new_delta: DeltaTextOperations, + // ) -> Result { + // let path = path.into(); + // let operation: NodeOperation = self + // .operations + // .get(&path) + // .ok_or(Err(OTError::record_not_found().context("Can't found the node")))?; + // + // match operation { + // NodeOperation::Insert { path, nodes } => {} + // NodeOperation::Update { path, changeset } => {} + // NodeOperation::Delete { .. } => {} + // } + // + // match node.body { + // Body::Empty => Ok(self), + // Body::Delta(delta) => { + // let inverted = new_delta.invert(&delta); + // let changeset = Changeset::Delta { + // delta: new_delta, + // inverted, + // }; + // Ok(self.update_node_at_path(path, changeset)) + // } + // } + // } - pub fn update_body_at_path(mut self, path: &Path, changeset: Changeset) -> Self { - match self.node_tree.node_id_at_path(path) { - Some(_) => { - self.operations.push_op(NodeOperation::Update { - path: path.clone(), - changeset, - }); - } - None => tracing::warn!("Update attributes at path: {:?} failed. Node is not exist", path), - } - self + pub fn delete_node_at_path(self, node_tree: &NodeTree, path: &Path) -> Self { + self.delete_nodes_at_path(node_tree, path, 1) } - pub fn delete_node_at_path(self, path: &Path) -> Self { - self.delete_nodes_at_path(path, 1) - } - - pub fn delete_nodes_at_path(mut self, path: &Path, length: usize) -> Self { - let node_id = self.node_tree.node_id_at_path(path); + pub fn delete_nodes_at_path(mut self, node_tree: &NodeTree, path: &Path, length: usize) -> Self { + let node_id = node_tree.node_id_at_path(path); if node_id.is_none() { tracing::warn!("Path: {:?} doesn't contains any nodes", path); return self; @@ -239,8 +237,8 @@ impl<'a> TransactionBuilder<'a> { let mut node_id = node_id.unwrap(); let mut deleted_nodes = vec![]; for _ in 0..length { - deleted_nodes.push(self.get_deleted_node_data(node_id)); - node_id = self.node_tree.following_siblings(node_id).next().unwrap(); + deleted_nodes.push(self.get_deleted_node_data(node_tree, node_id)); + node_id = node_tree.following_siblings(node_id).next().unwrap(); } self.operations.push_op(NodeOperation::Delete { @@ -250,16 +248,12 @@ impl<'a> TransactionBuilder<'a> { self } - fn get_deleted_node_data(&self, node_id: NodeId) -> NodeData { - let node_data = self.node_tree.get_node(node_id).unwrap(); - + fn get_deleted_node_data(&self, node_tree: &NodeTree, node_id: NodeId) -> NodeData { + let node_data = node_tree.get_node(node_id).unwrap(); let mut children = vec![]; - self.node_tree - .get_children_ids(node_id) - .into_iter() - .for_each(|child_id| { - children.push(self.get_deleted_node_data(child_id)); - }); + node_tree.get_children_ids(node_id).into_iter().for_each(|child_id| { + children.push(self.get_deleted_node_data(node_tree, child_id)); + }); NodeData { node_type: node_data.node_type.clone(), @@ -274,7 +268,7 @@ impl<'a> TransactionBuilder<'a> { self } - pub fn finalize(self) -> Transaction { + pub fn build(self) -> Transaction { Transaction::from_operations(self.operations) } } diff --git a/shared-lib/lib-ot/src/core/node_tree/tree.rs b/shared-lib/lib-ot/src/core/node_tree/tree.rs index 2028451ad7..dfd0a5f77f 100644 --- a/shared-lib/lib-ot/src/core/node_tree/tree.rs +++ b/shared-lib/lib-ot/src/core/node_tree/tree.rs @@ -1,6 +1,6 @@ use super::NodeOperations; use crate::core::{Changeset, Node, NodeData, NodeOperation, Path, Transaction}; -use crate::errors::{ErrorBuilder, OTError, OTErrorCode}; +use crate::errors::{OTError, OTErrorCode}; use indextree::{Arena, FollowingSiblings, NodeId}; use std::sync::Arc; @@ -20,6 +20,8 @@ impl Default for NodeTree { } } +pub const PLACEHOLDER_NODE_TYPE: &str = ""; + impl NodeTree { pub fn new(context: NodeTreeContext) -> NodeTree { let mut arena = Arena::new(); @@ -41,7 +43,7 @@ impl NodeTree { pub fn from_operations>(operations: T, context: NodeTreeContext) -> Result { let operations = operations.into(); let mut node_tree = NodeTree::new(context); - for operation in operations.into_inner().into_iter() { + for (_, operation) in operations.into_inner().into_iter().enumerate() { let _ = node_tree.apply_op(operation)?; } Ok(node_tree) @@ -149,15 +151,15 @@ impl NodeTree { return None; } - let mut iterate_node = self.root; + let mut node_id = self.root; for id in path.iter() { - iterate_node = self.child_from_node_at_index(iterate_node, *id)?; + node_id = self.node_id_from_parent_at_index(node_id, *id)?; } - if iterate_node.is_removed(&self.arena) { + if node_id.is_removed(&self.arena) { return None; } - Some(iterate_node) + Some(node_id) } pub fn path_from_node_id(&self, node_id: NodeId) -> Path { @@ -210,7 +212,7 @@ impl NodeTree { /// let node_2 = node_tree.get_node_at_path(&inserted_path).unwrap(); /// assert_eq!(node_2.node_type, node_1.node_type); /// ``` - pub fn child_from_node_at_index(&self, node_id: NodeId, index: usize) -> Option { + pub fn node_id_from_parent_at_index(&self, node_id: NodeId, index: usize) -> Option { let children = node_id.children(&self.arena); for (counter, child) in children.enumerate() { if counter == index { @@ -240,10 +242,11 @@ impl NodeTree { } pub fn apply_transaction(&mut self, transaction: Transaction) -> Result<(), OTError> { - let operations = transaction.into_operations(); + let operations = transaction.split().0; for operation in operations { self.apply_op(operation)?; } + Ok(()) } @@ -294,25 +297,68 @@ impl NodeTree { if parent_path.is_empty() { self.insert_nodes_at_index(self.root, last_index, nodes) } else { - let parent_node = self - .node_id_at_path(parent_path) - .ok_or_else(|| ErrorBuilder::new(OTErrorCode::PathNotFound).build())?; + let parent_node = match self.node_id_at_path(parent_path) { + None => self.create_adjacent_nodes_for_path(parent_path), + Some(parent_node) => parent_node, + }; self.insert_nodes_at_index(parent_node, last_index, nodes) } } + /// Create the adjacent nodes for the path + /// + /// It will create a corresponding node for each node on the path if it's not existing. + /// If the path is not start from zero, it will create its siblings. + /// + /// Check out the operation_insert_test.rs for more examples. + /// * operation_insert_node_when_its_parent_is_not_exist + /// * operation_insert_node_when_multiple_parent_is_not_exist_test + /// + /// # Arguments + /// + /// * `path`: creates nodes for this path + /// + /// returns: NodeId + /// + fn create_adjacent_nodes_for_path>(&mut self, path: T) -> NodeId { + let path = path.into(); + let mut node_id = self.root; + for id in path.iter() { + match self.node_id_from_parent_at_index(node_id, *id) { + None => { + let num_of_children = node_id.children(&self.arena).count(); + if *id > num_of_children { + for _ in 0..(*id - num_of_children) { + let node: Node = placeholder_node().into(); + let sibling_node = self.arena.new_node(node); + node_id.append(sibling_node, &mut self.arena); + } + } + + let node: Node = placeholder_node().into(); + let new_node_id = self.arena.new_node(node); + node_id.append(new_node_id, &mut self.arena); + node_id = new_node_id; + } + Some(next_node_id) => { + node_id = next_node_id; + } + } + } + node_id + } + /// Inserts nodes before the node with node_id /// fn insert_nodes_before(&mut self, node_id: &NodeId, nodes: Vec) { + if node_id.is_removed(&self.arena) { + tracing::warn!("Node:{:?} is remove before insert", node_id); + return; + } for node in nodes { let (node, children) = node.split(); let new_node_id = self.arena.new_node(node); - if node_id.is_removed(&self.arena) { - tracing::warn!("Node:{:?} is remove before insert", node_id); - return; - } - node_id.insert_before(new_node_id, &mut self.arena); self.append_nodes(&new_node_id, children); } @@ -326,14 +372,21 @@ impl NodeTree { // Append the node to the end of the children list if index greater or equal to the // length of the children. - if index >= parent.children(&self.arena).count() { + let num_of_children = parent.children(&self.arena).count(); + if index >= num_of_children { + let mut num_of_nodes_to_insert = index - num_of_children; + while num_of_nodes_to_insert > 0 { + self.append_nodes(&parent, vec![placeholder_node()]); + num_of_nodes_to_insert -= 1; + } + self.append_nodes(&parent, nodes); return Ok(()); } let node_to_insert = self - .child_from_node_at_index(parent, index) - .ok_or_else(|| ErrorBuilder::new(OTErrorCode::PathNotFound).build())?; + .node_id_from_parent_at_index(parent, index) + .ok_or_else(|| OTError::internal().context(format!("Can't find the node at {}", index)))?; self.insert_nodes_before(&node_to_insert, nodes); Ok(()) @@ -364,11 +417,22 @@ impl NodeTree { Ok(()) } + /// Update the node at path with the `changeset` + /// + /// Do nothing if there is no node at the path. + /// + /// # Arguments + /// + /// * `path`: references to the node that will be applied with the changeset + /// * `changeset`: the change that will be applied to the node + /// + /// returns: Result<(), OTError> fn update(&mut self, path: &Path, changeset: Changeset) -> Result<(), OTError> { - self.mut_node_at_path(path, |node| { - let _ = node.apply_changeset(changeset)?; - Ok(()) - }) + match self.mut_node_at_path(path, |node| node.apply_changeset(changeset)) { + Ok(_) => {} + Err(err) => tracing::error!("{}", err), + } + Ok(()) } fn mut_node_at_path(&mut self, path: &Path, f: F) -> Result<(), OTError> @@ -378,9 +442,9 @@ impl NodeTree { if !path.is_valid() { return Err(OTErrorCode::InvalidPath.into()); } - let node_id = self - .node_id_at_path(path) - .ok_or_else(|| ErrorBuilder::new(OTErrorCode::PathNotFound).build())?; + let node_id = self.node_id_at_path(path).ok_or_else(|| { + OTError::path_not_found().context(format!("Can't find the mutated node at path: {:?}", path)) + })?; match self.arena.get_mut(node_id) { None => tracing::warn!("The path: {:?} does not contain any nodes", path), Some(node) => { @@ -391,3 +455,7 @@ impl NodeTree { Ok(()) } } + +pub fn placeholder_node() -> NodeData { + NodeData::new(PLACEHOLDER_NODE_TYPE) +} diff --git a/shared-lib/lib-ot/src/errors.rs b/shared-lib/lib-ot/src/errors.rs index 92dc116fb0..86b01bec44 100644 --- a/shared-lib/lib-ot/src/errors.rs +++ b/shared-lib/lib-ot/src/errors.rs @@ -38,6 +38,9 @@ impl OTError { static_ot_error!(revision_id_conflict, OTErrorCode::RevisionIDConflict); static_ot_error!(internal, OTErrorCode::Internal); static_ot_error!(serde, OTErrorCode::SerdeError); + static_ot_error!(path_not_found, OTErrorCode::PathNotFound); + static_ot_error!(compose, OTErrorCode::ComposeOperationFail); + static_ot_error!(record_not_found, OTErrorCode::RecordNotFound); } impl fmt::Display for OTError { @@ -75,7 +78,7 @@ pub enum OTErrorCode { PathNotFound, PathIsEmpty, InvalidPath, - UnexpectedEmpty, + RecordNotFound, } pub struct ErrorBuilder { diff --git a/shared-lib/lib-ot/tests/node/mod.rs b/shared-lib/lib-ot/tests/node/mod.rs index 58fbcdc8ea..42fd074dce 100644 --- a/shared-lib/lib-ot/tests/node/mod.rs +++ b/shared-lib/lib-ot/tests/node/mod.rs @@ -1,4 +1,7 @@ -mod operation_test; +mod operation_delete_test; +mod operation_delta_test; +mod operation_insert_test; mod script; mod serde_test; +mod transaction_compose_test; mod tree_test; diff --git a/shared-lib/lib-ot/tests/node/operation_delete_test.rs b/shared-lib/lib-ot/tests/node/operation_delete_test.rs new file mode 100644 index 0000000000..145699c9b2 --- /dev/null +++ b/shared-lib/lib-ot/tests/node/operation_delete_test.rs @@ -0,0 +1,178 @@ +use crate::node::script::NodeScript::*; +use crate::node::script::NodeTest; + +use lib_ot::core::{Changeset, NodeData, NodeDataBuilder}; + +#[test] +fn operation_delete_nested_node_test() { + let mut test = NodeTest::new(); + let image_a = NodeData::new("image_a"); + let image_b = NodeData::new("image_b"); + + let video_a = NodeData::new("video_a"); + let video_b = NodeData::new("video_b"); + + let image_1 = NodeDataBuilder::new("image_1") + .add_node_data(image_a.clone()) + .add_node_data(image_b.clone()) + .build(); + let video_1 = NodeDataBuilder::new("video_1") + .add_node_data(video_a.clone()) + .add_node_data(video_b) + .build(); + + let text_node_1 = NodeDataBuilder::new("text_1") + .add_node_data(image_1) + .add_node_data(video_1.clone()) + .build(); + + let image_2 = NodeDataBuilder::new("image_2") + .add_node_data(image_a) + .add_node_data(image_b.clone()) + .build(); + let text_node_2 = NodeDataBuilder::new("text_2").add_node_data(image_2).build(); + + let scripts = vec![ + InsertNode { + path: 0.into(), + node_data: text_node_1, + rev_id: 1, + }, + InsertNode { + path: 1.into(), + node_data: text_node_2, + rev_id: 2, + }, + // 0:text_1 + // 0:image_1 + // 0:image_a + // 1:image_b + // 1:video_1 + // 0:video_a + // 1:video_b + // 1:text_2 + // 0:image_2 + // 0:image_a + // 1:image_b + DeleteNode { + path: vec![0, 0, 0].into(), + rev_id: 3, + }, + AssertNode { + path: vec![0, 0, 0].into(), + expected: Some(image_b), + }, + AssertNode { + path: vec![0, 1].into(), + expected: Some(video_1), + }, + DeleteNode { + path: vec![0, 1, 1].into(), + rev_id: 4, + }, + AssertNode { + path: vec![0, 1, 0].into(), + expected: Some(video_a), + }, + ]; + test.run_scripts(scripts); +} + +#[test] +fn operation_delete_node_with_revision_conflict_test() { + let mut test = NodeTest::new(); + let text_1 = NodeDataBuilder::new("text_1").build(); + let text_2 = NodeDataBuilder::new("text_2").build(); + let text_3 = NodeDataBuilder::new("text_3").build(); + + let scripts = vec![ + InsertNode { + path: 0.into(), + node_data: text_1.clone(), + rev_id: 1, + }, + InsertNode { + path: 1.into(), + node_data: text_2, + rev_id: 2, + }, + // The node's in the tree will be: + // 0: text_1 + // 2: text_2 + // + // The insert action is happened concurrently with the delete action, because they + // share the same rev_id. aka, 3. The delete action is want to delete the node at index 1, + // but it was moved to index 2. + InsertNode { + path: 1.into(), + node_data: text_3.clone(), + rev_id: 3, + }, + // 0: text_1 + // 1: text_3 + // 2: text_2 + // + // The path of the delete action will be transformed to a new path that point to the text_2. + // 1 -> 2 + DeleteNode { + path: 1.into(), + rev_id: 3, + }, + // After perform the delete action, the tree will be: + // 0: text_1 + // 1: text_3 + AssertNumberOfChildrenAtPath { + path: None, + expected: 2, + }, + AssertNode { + path: 0.into(), + expected: Some(text_1), + }, + AssertNode { + path: 1.into(), + expected: Some(text_3), + }, + AssertNode { + path: 2.into(), + expected: None, + }, + ]; + test.run_scripts(scripts); +} + +#[test] +fn operation_update_node_after_delete_test() { + let mut test = NodeTest::new(); + let text_1 = NodeDataBuilder::new("text_1").build(); + let text_2 = NodeDataBuilder::new("text_2").build(); + let scripts = vec![ + InsertNode { + path: 0.into(), + node_data: text_1, + rev_id: 1, + }, + InsertNode { + path: 1.into(), + node_data: text_2, + rev_id: 2, + }, + DeleteNode { + path: 0.into(), + rev_id: 3, + }, + // The node at path 1 is not exist. The following UpdateBody script will do nothing + AssertNode { + path: 1.into(), + expected: None, + }, + UpdateBody { + path: 1.into(), + changeset: Changeset::Delta { + delta: Default::default(), + inverted: Default::default(), + }, + }, + ]; + test.run_scripts(scripts); +} diff --git a/shared-lib/lib-ot/tests/node/operation_delta_test.rs b/shared-lib/lib-ot/tests/node/operation_delta_test.rs new file mode 100644 index 0000000000..bdc2812158 --- /dev/null +++ b/shared-lib/lib-ot/tests/node/operation_delta_test.rs @@ -0,0 +1,41 @@ +use crate::node::script::NodeScript::{AssertNodeDelta, InsertNode, UpdateBody}; +use crate::node::script::{edit_node_delta, NodeTest}; +use lib_ot::core::NodeDataBuilder; +use lib_ot::text_delta::DeltaTextOperationBuilder; + +#[test] +fn operation_update_delta_test() { + let mut test = NodeTest::new(); + let initial_delta = DeltaTextOperationBuilder::new().build(); + let new_delta = DeltaTextOperationBuilder::new() + .retain(initial_delta.utf16_base_len) + .insert("Hello, world") + .build(); + let (changeset, expected) = edit_node_delta(&initial_delta, new_delta); + let node = NodeDataBuilder::new("text").insert_delta(initial_delta.clone()).build(); + + let scripts = vec![ + InsertNode { + path: 0.into(), + node_data: node, + rev_id: 1, + }, + UpdateBody { + path: 0.into(), + changeset: changeset.clone(), + }, + AssertNodeDelta { + path: 0.into(), + expected, + }, + UpdateBody { + path: 0.into(), + changeset: changeset.inverted(), + }, + AssertNodeDelta { + path: 0.into(), + expected: initial_delta, + }, + ]; + test.run_scripts(scripts); +} diff --git a/shared-lib/lib-ot/tests/node/operation_insert_test.rs b/shared-lib/lib-ot/tests/node/operation_insert_test.rs new file mode 100644 index 0000000000..50cc57d130 --- /dev/null +++ b/shared-lib/lib-ot/tests/node/operation_insert_test.rs @@ -0,0 +1,460 @@ +use crate::node::script::NodeScript::*; +use crate::node::script::NodeTest; + +use lib_ot::core::{placeholder_node, NodeData, NodeDataBuilder, NodeOperation, Path}; + +#[test] +fn operation_insert_op_transform_test() { + let node_1 = NodeDataBuilder::new("text_1").build(); + let node_2 = NodeDataBuilder::new("text_2").build(); + let op_1 = NodeOperation::Insert { + path: Path(vec![0, 1]), + nodes: vec![node_1], + }; + + let mut insert_2 = NodeOperation::Insert { + path: Path(vec![0, 1]), + nodes: vec![node_2], + }; + + // let mut node_tree = NodeTree::new("root"); + // node_tree.apply_op(insert_1.clone()).unwrap(); + + op_1.transform(&mut insert_2); + let json = serde_json::to_string(&insert_2).unwrap(); + assert_eq!(json, r#"{"op":"insert","path":[0,2],"nodes":[{"type":"text_2"}]}"#); +} + +#[test] +fn operation_insert_one_level_path_test() { + let node_data_1 = NodeDataBuilder::new("text_1").build(); + let node_data_2 = NodeDataBuilder::new("text_2").build(); + let node_data_3 = NodeDataBuilder::new("text_3").build(); + let node_3 = node_data_3.clone(); + // 0: text_1 + // 1: text_2 + // + // Insert a new operation with rev_id 2 to index 1,but the index was already taken, so + // it needs to be transformed. + // + // 0: text_1 + // 1: text_2 + // 2: text_3 + let scripts = vec![ + InsertNode { + path: 0.into(), + node_data: node_data_1.clone(), + rev_id: 1, + }, + InsertNode { + path: 1.into(), + node_data: node_data_2.clone(), + rev_id: 2, + }, + InsertNode { + path: 1.into(), + node_data: node_data_3.clone(), + rev_id: 2, + }, + AssertNode { + path: 2.into(), + expected: Some(node_3.clone()), + }, + ]; + NodeTest::new().run_scripts(scripts); + + // If the rev_id of the node_data_3 is 3. then the tree will be: + // 0: text_1 + // 1: text_3 + // 2: text_2 + let scripts = vec![ + InsertNode { + path: 0.into(), + node_data: node_data_1, + rev_id: 1, + }, + InsertNode { + path: 1.into(), + node_data: node_data_2, + rev_id: 2, + }, + InsertNode { + path: 1.into(), + node_data: node_data_3, + rev_id: 3, + }, + AssertNode { + path: 1.into(), + expected: Some(node_3), + }, + ]; + NodeTest::new().run_scripts(scripts); +} + +#[test] +fn operation_insert_with_multiple_level_path_test() { + let mut test = NodeTest::new(); + let node_data_1 = NodeDataBuilder::new("text_1") + .add_node_data(NodeDataBuilder::new("text_1_1").build()) + .add_node_data(NodeDataBuilder::new("text_1_2").build()) + .build(); + + let node_data_2 = NodeDataBuilder::new("text_2") + .add_node_data(NodeDataBuilder::new("text_2_1").build()) + .add_node_data(NodeDataBuilder::new("text_2_2").build()) + .build(); + + let node_data_3 = NodeDataBuilder::new("text_3").build(); + let scripts = vec![ + InsertNode { + path: 0.into(), + node_data: node_data_1, + rev_id: 1, + }, + InsertNode { + path: 1.into(), + node_data: node_data_2, + rev_id: 2, + }, + InsertNode { + path: 1.into(), + node_data: node_data_3.clone(), + rev_id: 2, + }, + AssertNode { + path: 2.into(), + expected: Some(node_data_3), + }, + ]; + test.run_scripts(scripts); +} + +#[test] +fn operation_insert_node_out_of_bound_test() { + let mut test = NodeTest::new(); + let image_a = NodeData::new("image_a"); + let image_b = NodeData::new("image_b"); + let image = NodeDataBuilder::new("image_1") + .add_node_data(image_a) + .add_node_data(image_b) + .build(); + let text_node = NodeDataBuilder::new("text_1").add_node_data(image).build(); + let image_c = NodeData::new("image_c"); + + let scripts = vec![ + InsertNode { + path: 0.into(), + node_data: text_node, + rev_id: 1, + }, + // 0:text_1 + // 0:image_1 + // 0:image_a + // 1:image_b + InsertNode { + path: vec![0, 0, 3].into(), + node_data: image_c.clone(), + rev_id: 2, + }, + // 0:text_1 + // 0:image_1 + // 0:image_a + // 1:image_b + // 2:placeholder node + // 3:image_c + AssertNode { + path: vec![0, 0, 2].into(), + expected: Some(placeholder_node()), + }, + AssertNode { + path: vec![0, 0, 3].into(), + expected: Some(image_c), + }, + AssertNode { + path: vec![0, 0, 10].into(), + expected: None, + }, + ]; + test.run_scripts(scripts); +} +#[test] +fn operation_insert_node_when_parent_is_not_exist_test1() { + let mut test = NodeTest::new(); + let text_1 = NodeDataBuilder::new("text_1").build(); + let text_2 = NodeDataBuilder::new("text_2").build(); + let scripts = vec![ + InsertNode { + path: 0.into(), + node_data: text_1, + rev_id: 1, + }, + // The node at path 1 is not existing when inserting the text_2 to path 2. + InsertNode { + path: 2.into(), + node_data: text_2.clone(), + rev_id: 2, + }, + AssertNode { + path: 1.into(), + expected: Some(placeholder_node()), + }, + AssertNode { + path: 2.into(), + expected: Some(text_2), + }, + ]; + test.run_scripts(scripts); +} + +#[test] +fn operation_insert_node_when_parent_is_not_exist_test2() { + let mut test = NodeTest::new(); + let text_1 = NodeDataBuilder::new("text_1").build(); + let text_2 = NodeDataBuilder::new("text_2").build(); + let scripts = vec![ + InsertNode { + path: 0.into(), + node_data: text_1, + rev_id: 1, + }, + // The node at path 1 is not existing when inserting the text_2 to path 2. + InsertNode { + path: 3.into(), + node_data: text_2.clone(), + rev_id: 2, + }, + AssertNode { + path: 1.into(), + expected: Some(placeholder_node()), + }, + AssertNode { + path: 2.into(), + expected: Some(placeholder_node()), + }, + AssertNode { + path: 3.into(), + expected: Some(text_2), + }, + ]; + test.run_scripts(scripts); +} + +#[test] +fn operation_insert_node_when_its_parent_is_not_exist_test3() { + let mut test = NodeTest::new(); + let text_1 = NodeDataBuilder::new("text_1").build(); + let text_2 = NodeDataBuilder::new("text_2").build(); + + let mut placeholder_node = placeholder_node(); + placeholder_node.children.push(text_2.clone()); + + let scripts = vec![ + InsertNode { + path: 0.into(), + node_data: text_1, + rev_id: 1, + }, + // The node at path 1 is not existing when inserting the text_2 to path 2. + InsertNode { + path: vec![1, 0].into(), + node_data: text_2.clone(), + rev_id: 2, + }, + AssertNode { + path: 1.into(), + expected: Some(placeholder_node), + }, + AssertNode { + path: vec![1, 0].into(), + expected: Some(text_2), + }, + ]; + test.run_scripts(scripts); +} + +#[test] +fn operation_insert_node_to_the_end_when_parent_is_not_exist_test() { + let mut test = NodeTest::new(); + let node_0 = NodeData::new("0"); + let node_1 = NodeData::new("1"); + let node_1_1 = NodeData::new("1_1"); + let text_node = NodeData::new("text"); + let mut ghost = placeholder_node(); + ghost.children.push(text_node.clone()); + // 0:0 + // 1:1 + // 0:1_1 + // 1:ghost + // 0:text + let scripts = vec![ + InsertNode { + path: 0.into(), + node_data: node_0, + rev_id: 1, + }, + InsertNode { + path: 1.into(), + node_data: node_1, + rev_id: 2, + }, + InsertNode { + path: vec![1, 0].into(), + node_data: node_1_1.clone(), + rev_id: 3, + }, + InsertNode { + path: vec![1, 1, 0].into(), + node_data: text_node.clone(), + rev_id: 4, + }, + AssertNode { + path: vec![1, 0].into(), + expected: Some(node_1_1), + }, + AssertNode { + path: vec![1, 1].into(), + expected: Some(ghost), + }, + AssertNode { + path: vec![1, 1, 0].into(), + expected: Some(text_node), + }, + ]; + test.run_scripts(scripts); +} +#[test] +fn operation_insert_node_when_multiple_parent_is_not_exist_test() { + let mut test = NodeTest::new(); + let text_1 = NodeDataBuilder::new("text_1").build(); + let text_2 = NodeDataBuilder::new("text_2").build(); + + let path = vec![1, 0, 0, 0, 0, 0]; + let mut auto_fill_node = placeholder_node(); + let mut iter_node: &mut NodeData = &mut auto_fill_node; + let insert_path = path.split_at(1).1; + for (index, _) in insert_path.iter().enumerate() { + if index == insert_path.len() - 1 { + iter_node.children.push(text_2.clone()); + } else { + iter_node.children.push(placeholder_node()); + iter_node = iter_node.children.last_mut().unwrap(); + } + } + + let scripts = vec![ + InsertNode { + path: 0.into(), + node_data: text_1, + rev_id: 1, + }, + InsertNode { + path: path.clone().into(), + node_data: text_2.clone(), + rev_id: 2, + }, + AssertNode { + path: vec![1].into(), + expected: Some(auto_fill_node), + }, + AssertNode { + path: path.into(), + expected: Some(text_2), + }, + ]; + test.run_scripts(scripts); +} + +#[test] +fn operation_insert_node_when_multiple_parent_is_not_exist_test2() { + let mut test = NodeTest::new(); + // 0:ghost + // 0:ghost + // 1:ghost + // 0:text + let mut text_node_parent = placeholder_node(); + let text_node = NodeDataBuilder::new("text").build(); + text_node_parent.children.push(text_node.clone()); + + let mut ghost = placeholder_node(); + ghost.children.push(placeholder_node()); + ghost.children.push(text_node_parent.clone()); + + let path = vec![1, 1, 0]; + let scripts = vec![ + InsertNode { + path: path.into(), + node_data: text_node.clone(), + rev_id: 1, + }, + // 0:ghost + // 1:ghost + // 0:ghost + // 1:ghost + // 0:text + AssertNode { + path: 0.into(), + expected: Some(placeholder_node()), + }, + AssertNode { + path: 1.into(), + expected: Some(ghost), + }, + AssertNumberOfChildrenAtPath { + path: Some(1.into()), + expected: 2, + }, + AssertNode { + path: vec![1, 1].into(), + expected: Some(text_node_parent), + }, + AssertNode { + path: vec![1, 1, 0].into(), + expected: Some(text_node), + }, + ]; + test.run_scripts(scripts); +} + +#[test] +fn operation_insert_node_when_multiple_parent_is_not_exist_test3() { + let mut test = NodeTest::new(); + let text_node = NodeDataBuilder::new("text").build(); + let path = vec![3, 3, 0]; + let scripts = vec![ + InsertNode { + path: path.clone().into(), + node_data: text_node.clone(), + rev_id: 1, + }, + // 0:ghost + // 1:ghost + // 2:ghost + // 3:ghost + // 0:ghost + // 1:ghost + // 2:ghost + // 3:ghost + // 0:text + AssertNode { + path: 0.into(), + expected: Some(placeholder_node()), + }, + AssertNode { + path: 1.into(), + expected: Some(placeholder_node()), + }, + AssertNode { + path: 2.into(), + expected: Some(placeholder_node()), + }, + AssertNumberOfChildrenAtPath { + path: Some(3.into()), + expected: 4, + }, + AssertNode { + path: path.into(), + expected: Some(text_node), + }, + ]; + test.run_scripts(scripts); +} diff --git a/shared-lib/lib-ot/tests/node/operation_test.rs b/shared-lib/lib-ot/tests/node/operation_test.rs deleted file mode 100644 index bed011d918..0000000000 --- a/shared-lib/lib-ot/tests/node/operation_test.rs +++ /dev/null @@ -1,170 +0,0 @@ -use crate::node::script::NodeScript::*; -use crate::node::script::NodeTest; - -use lib_ot::core::{NodeDataBuilder, NodeOperation, Path}; - -#[test] -fn operation_insert_op_transform_test() { - let node_1 = NodeDataBuilder::new("text_1").build(); - let node_2 = NodeDataBuilder::new("text_2").build(); - let op_1 = NodeOperation::Insert { - path: Path(vec![0, 1]), - nodes: vec![node_1], - }; - - let mut insert_2 = NodeOperation::Insert { - path: Path(vec![0, 1]), - nodes: vec![node_2], - }; - - // let mut node_tree = NodeTree::new("root"); - // node_tree.apply_op(insert_1.clone()).unwrap(); - - op_1.transform(&mut insert_2); - let json = serde_json::to_string(&insert_2).unwrap(); - assert_eq!(json, r#"{"op":"insert","path":[0,2],"nodes":[{"type":"text_2"}]}"#); -} - -#[test] -fn operation_insert_one_level_path_test() { - let mut test = NodeTest::new(); - let node_data_1 = NodeDataBuilder::new("text_1").build(); - let node_data_2 = NodeDataBuilder::new("text_2").build(); - let node_data_3 = NodeDataBuilder::new("text_3").build(); - let node_3 = node_data_3.clone(); - // 0: text_1 - // 1: text_2 - // - // Insert a new operation with rev_id 1,but the rev_id:1 is already exist, so - // it needs to be transformed. - // 1:text_3 => 2:text_3 - // - // 0: text_1 - // 1: text_2 - // 2: text_3 - // - // If the rev_id of the insert operation is 3. then the tree will be: - // 0: text_1 - // 1: text_3 - // 2: text_2 - let scripts = vec![ - InsertNode { - path: 0.into(), - node_data: node_data_1, - rev_id: 1, - }, - InsertNode { - path: 1.into(), - node_data: node_data_2, - rev_id: 2, - }, - InsertNode { - path: 1.into(), - node_data: node_data_3, - rev_id: 1, - }, - AssertNode { - path: 2.into(), - expected: Some(node_3), - }, - ]; - test.run_scripts(scripts); -} - -#[test] -fn operation_insert_with_multiple_level_path_test() { - let mut test = NodeTest::new(); - let node_data_1 = NodeDataBuilder::new("text_1") - .add_node_data(NodeDataBuilder::new("text_1_1").build()) - .add_node_data(NodeDataBuilder::new("text_1_2").build()) - .build(); - - let node_data_2 = NodeDataBuilder::new("text_2") - .add_node_data(NodeDataBuilder::new("text_2_1").build()) - .add_node_data(NodeDataBuilder::new("text_2_2").build()) - .build(); - - let node_data_3 = NodeDataBuilder::new("text_3").build(); - let scripts = vec![ - InsertNode { - path: 0.into(), - node_data: node_data_1, - rev_id: 1, - }, - InsertNode { - path: 1.into(), - node_data: node_data_2, - rev_id: 2, - }, - InsertNode { - path: 1.into(), - node_data: node_data_3.clone(), - rev_id: 1, - }, - AssertNode { - path: 2.into(), - expected: Some(node_data_3), - }, - ]; - test.run_scripts(scripts); -} - -#[test] -fn operation_delete_test() { - let mut test = NodeTest::new(); - let node_data_1 = NodeDataBuilder::new("text_1").build(); - let node_data_2 = NodeDataBuilder::new("text_2").build(); - let node_data_3 = NodeDataBuilder::new("text_3").build(); - let node_3 = node_data_3.clone(); - - let scripts = vec![ - InsertNode { - path: 0.into(), - node_data: node_data_1, - rev_id: 1, - }, - InsertNode { - path: 1.into(), - node_data: node_data_2, - rev_id: 2, - }, - // The node's in the tree will be: - // 0: text_1 - // 2: text_2 - // - // The insert action is happened concurrently with the delete action, because they - // share the same rev_id. aka, 3. The delete action is want to delete the node at index 1, - // but it was moved to index 2. - InsertNode { - path: 1.into(), - node_data: node_data_3, - rev_id: 3, - }, - // 0: text_1 - // 1: text_3 - // 2: text_2 - // - // The path of the delete action will be transformed to a new path that point to the text_2. - // 1 -> 2 - DeleteNode { - path: 1.into(), - rev_id: 3, - }, - // After perform the delete action, the tree will be: - // 0: text_1 - // 1: text_3 - AssertNumberOfChildrenAtPath { - path: None, - expected: 2, - }, - AssertNode { - path: 1.into(), - expected: Some(node_3), - }, - AssertNode { - path: 2.into(), - expected: None, - }, - ]; - test.run_scripts(scripts); -} diff --git a/shared-lib/lib-ot/tests/node/script.rs b/shared-lib/lib-ot/tests/node/script.rs index ec8f101293..14c2a09881 100644 --- a/shared-lib/lib-ot/tests/node/script.rs +++ b/shared-lib/lib-ot/tests/node/script.rs @@ -1,5 +1,6 @@ #![allow(clippy::all)] -use lib_ot::core::{NodeTreeContext, Transaction}; +use lib_ot::core::{NodeTreeContext, OperationTransform, Transaction}; +use lib_ot::text_delta::DeltaTextOperationBuilder; use lib_ot::{ core::attributes::AttributeHashMap, core::{Body, Changeset, NodeData, NodeTree, Path, TransactionBuilder}, @@ -84,9 +85,7 @@ impl NodeTest { node_data: node, rev_id, } => { - let mut transaction = TransactionBuilder::new(&self.node_tree) - .insert_node_at_path(path, node) - .finalize(); + let mut transaction = TransactionBuilder::new().insert_node_at_path(path, node).build(); self.transform_transaction_if_need(&mut transaction, rev_id); self.apply_transaction(transaction); } @@ -95,29 +94,34 @@ impl NodeTest { node_data_list, rev_id, } => { - let mut transaction = TransactionBuilder::new(&self.node_tree) + let mut transaction = TransactionBuilder::new() .insert_nodes_at_path(path, node_data_list) - .finalize(); + .build(); self.transform_transaction_if_need(&mut transaction, rev_id); self.apply_transaction(transaction); } NodeScript::UpdateAttributes { path, attributes } => { - let transaction = TransactionBuilder::new(&self.node_tree) - .update_attributes_at_path(&path, attributes) - .finalize(); + let node = self.node_tree.get_node_data_at_path(&path).unwrap(); + let transaction = TransactionBuilder::new() + .update_node_at_path( + &path, + Changeset::Attributes { + new: attributes, + old: node.attributes, + }, + ) + .build(); self.apply_transaction(transaction); } NodeScript::UpdateBody { path, changeset } => { // - let transaction = TransactionBuilder::new(&self.node_tree) - .update_body_at_path(&path, changeset) - .finalize(); + let transaction = TransactionBuilder::new().update_node_at_path(&path, changeset).build(); self.apply_transaction(transaction); } NodeScript::DeleteNode { path, rev_id } => { - let mut transaction = TransactionBuilder::new(&self.node_tree) - .delete_node_at_path(&path) - .finalize(); + let mut transaction = TransactionBuilder::new() + .delete_node_at_path(&self.node_tree, &path) + .build(); self.transform_transaction_if_need(&mut transaction, rev_id); self.apply_transaction(transaction); } @@ -175,3 +179,35 @@ impl NodeTest { } } } + +pub fn edit_node_delta( + delta: &DeltaTextOperations, + new_delta: DeltaTextOperations, +) -> (Changeset, DeltaTextOperations) { + let inverted = new_delta.invert(&delta); + let expected = delta.compose(&new_delta).unwrap(); + let changeset = Changeset::Delta { + delta: new_delta.clone(), + inverted: inverted.clone(), + }; + (changeset, expected) +} + +pub fn make_node_delta_changeset( + initial_content: &str, + insert_str: &str, +) -> (DeltaTextOperations, Changeset, DeltaTextOperations) { + let initial_content = initial_content.to_owned(); + let initial_delta = DeltaTextOperationBuilder::new().insert(&initial_content).build(); + let delta = DeltaTextOperationBuilder::new() + .retain(initial_content.len()) + .insert(insert_str) + .build(); + let inverted = delta.invert(&initial_delta); + let expected = initial_delta.compose(&delta).unwrap(); + let changeset = Changeset::Delta { + delta: delta.clone(), + inverted: inverted.clone(), + }; + (initial_delta, changeset, expected) +} diff --git a/shared-lib/lib-ot/tests/node/serde_test.rs b/shared-lib/lib-ot/tests/node/serde_test.rs index 9d4c9a8d70..25e086c160 100644 --- a/shared-lib/lib-ot/tests/node/serde_test.rs +++ b/shared-lib/lib-ot/tests/node/serde_test.rs @@ -1,6 +1,4 @@ -use lib_ot::core::{ - AttributeBuilder, Changeset, NodeData, NodeDataBuilder, NodeOperation, NodeTree, Path, Transaction, -}; +use lib_ot::core::{AttributeBuilder, Changeset, NodeData, NodeDataBuilder, NodeOperation, NodeTree, Path}; use lib_ot::text_delta::DeltaTextOperationBuilder; #[test] @@ -69,33 +67,33 @@ fn operation_update_node_body_deserialize_test() { assert_eq!(json_1, json_2); } -#[test] -fn transaction_serialize_test() { - let insert = NodeOperation::Insert { - path: Path(vec![0, 1]), - nodes: vec![NodeData::new("text".to_owned())], - }; - let transaction = Transaction::from_operations(vec![insert]); - let json = serde_json::to_string(&transaction).unwrap(); - assert_eq!( - json, - r#"{"operations":[{"op":"insert","path":[0,1],"nodes":[{"type":"text"}]}]}"# - ); -} - -#[test] -fn transaction_deserialize_test() { - let json = r#"{"operations":[{"op":"insert","path":[0,1],"nodes":[{"type":"text"}]}],"TextSelection":{"before_selection":{"start":{"path":[],"offset":0},"end":{"path":[],"offset":0}},"after_selection":{"start":{"path":[],"offset":0},"end":{"path":[],"offset":0}}}}"#; - - let transaction: Transaction = serde_json::from_str(json).unwrap(); - assert_eq!(transaction.operations.len(), 1); -} - -#[test] -fn node_tree_deserialize_test() { - let tree: NodeTree = serde_json::from_str(TREE_JSON).unwrap(); - assert_eq!(tree.number_of_children(None), 1); -} +// #[test] +// fn transaction_serialize_test() { +// let insert = NodeOperation::Insert { +// path: Path(vec![0, 1]), +// nodes: vec![NodeData::new("text".to_owned())], +// }; +// let transaction = Transaction::from_operations(vec![insert]); +// let json = serde_json::to_string(&transaction).unwrap(); +// assert_eq!( +// json, +// r#"{"operations":[{"op":"insert","path":[0,1],"nodes":[{"type":"text"}]}]}"# +// ); +// } +// +// #[test] +// fn transaction_deserialize_test() { +// let json = r#"{"operations":[{"op":"insert","path":[0,1],"nodes":[{"type":"text"}]}],"TextSelection":{"before_selection":{"start":{"path":[],"offset":0},"end":{"path":[],"offset":0}},"after_selection":{"start":{"path":[],"offset":0},"end":{"path":[],"offset":0}}}}"#; +// +// let transaction: Transaction = serde_json::from_str(json).unwrap(); +// assert_eq!(transaction.operations.len(), 1); +// } +// +// #[test] +// fn node_tree_deserialize_test() { +// let tree: NodeTree = serde_json::from_str(TREE_JSON).unwrap(); +// assert_eq!(tree.number_of_children(None), 1); +// } #[test] fn node_tree_serialize_test() { diff --git a/shared-lib/lib-ot/tests/node/transaction_compose_test.rs b/shared-lib/lib-ot/tests/node/transaction_compose_test.rs new file mode 100644 index 0000000000..4b5dd11b76 --- /dev/null +++ b/shared-lib/lib-ot/tests/node/transaction_compose_test.rs @@ -0,0 +1,104 @@ +use crate::node::script::{edit_node_delta, make_node_delta_changeset}; +use lib_ot::core::{AttributeEntry, Changeset, NodeDataBuilder, NodeOperation, Transaction, TransactionBuilder}; +use lib_ot::text_delta::DeltaTextOperationBuilder; + +#[test] +fn transaction_compose_update_after_insert_test() { + let (initial_delta, changeset, _) = make_node_delta_changeset("Hello", " world"); + let node_data = NodeDataBuilder::new("text").insert_delta(initial_delta).build(); + + // Modify the same path, the operations will be merged after composing if possible. + let mut transaction_a = TransactionBuilder::new().insert_node_at_path(0, node_data).build(); + let transaction_b = TransactionBuilder::new().update_node_at_path(0, changeset).build(); + let _ = transaction_a.compose(transaction_b).unwrap(); + + // The operations are merged into one operation + assert_eq!(transaction_a.operations.len(), 1); + assert_eq!( + transaction_a.to_json().unwrap(), + r#"{"operations":[{"op":"insert","path":[0],"nodes":[{"type":"text","body":{"delta":[{"insert":"Hello world"}]}}]}]}"# + ); +} + +#[test] +fn transaction_compose_multiple_update_test() { + let (initial_delta, changeset_1, final_delta) = make_node_delta_changeset("Hello", " world"); + let mut transaction = TransactionBuilder::new() + .insert_node_at_path(0, NodeDataBuilder::new("text").insert_delta(initial_delta).build()) + .build(); + let (changeset_2, _) = edit_node_delta( + &final_delta, + DeltaTextOperationBuilder::new() + .retain(final_delta.utf16_target_len) + .insert("๐Ÿ˜") + .build(), + ); + + let mut other_transaction = Transaction::new(); + + // the following two update operations will be merged into one + let update_1 = TransactionBuilder::new().update_node_at_path(0, changeset_1).build(); + other_transaction.compose(update_1).unwrap(); + + let update_2 = TransactionBuilder::new().update_node_at_path(0, changeset_2).build(); + other_transaction.compose(update_2).unwrap(); + + let inverted = Transaction::from_operations(other_transaction.operations.inverted()); + + // the update operation will be merged into insert operation + let _ = transaction.compose(other_transaction).unwrap(); + assert_eq!(transaction.operations.len(), 1); + assert_eq!( + transaction.to_json().unwrap(), + r#"{"operations":[{"op":"insert","path":[0],"nodes":[{"type":"text","body":{"delta":[{"insert":"Hello world๐Ÿ˜"}]}}]}]}"# + ); + + let _ = transaction.compose(inverted).unwrap(); + assert_eq!( + transaction.to_json().unwrap(), + r#"{"operations":[{"op":"insert","path":[0],"nodes":[{"type":"text","body":{"delta":[{"insert":"Hello"}]}}]}]}"# + ); +} + +#[test] +fn transaction_compose_multiple_attribute_test() { + let delta = DeltaTextOperationBuilder::new().insert("Hello").build(); + let node = NodeDataBuilder::new("text").insert_delta(delta).build(); + + let insert_operation = NodeOperation::Insert { + path: 0.into(), + nodes: vec![node], + }; + + let mut transaction = Transaction::new(); + transaction.push_operation(insert_operation); + + let new_attribute = AttributeEntry::new("subtype", "bulleted-list"); + let update_operation = NodeOperation::Update { + path: 0.into(), + changeset: Changeset::Attributes { + new: new_attribute.clone().into(), + old: Default::default(), + }, + }; + transaction.push_operation(update_operation); + assert_eq!( + transaction.to_json().unwrap(), + r#"{"operations":[{"op":"insert","path":[0],"nodes":[{"type":"text","body":{"delta":[{"insert":"Hello"}]}}]},{"op":"update","path":[0],"changeset":{"attributes":{"new":{"subtype":"bulleted-list"},"old":{}}}}]}"# + ); + + let old_attribute = new_attribute; + let new_attribute = AttributeEntry::new("subtype", "number-list"); + transaction.push_operation(NodeOperation::Update { + path: 0.into(), + changeset: Changeset::Attributes { + new: new_attribute.into(), + old: old_attribute.into(), + }, + }); + + assert_eq!( + transaction.to_json().unwrap(), + r#"{"operations":[{"op":"insert","path":[0],"nodes":[{"type":"text","body":{"delta":[{"insert":"Hello"}]}}]},{"op":"update","path":[0],"changeset":{"attributes":{"new":{"subtype":"number-list"},"old":{"subtype":"bulleted-list"}}}}]}"# + ); +} diff --git a/shared-lib/lib-ot/tests/node/tree_test.rs b/shared-lib/lib-ot/tests/node/tree_test.rs index b508e194f6..0606b88cc9 100644 --- a/shared-lib/lib-ot/tests/node/tree_test.rs +++ b/shared-lib/lib-ot/tests/node/tree_test.rs @@ -1,10 +1,7 @@ use crate::node::script::NodeScript::*; -use crate::node::script::NodeTest; -use lib_ot::core::Body; -use lib_ot::core::Changeset; -use lib_ot::core::OperationTransform; +use crate::node::script::{make_node_delta_changeset, NodeTest}; + use lib_ot::core::{NodeData, NodeDataBuilder, Path}; -use lib_ot::text_delta::{DeltaTextOperationBuilder, DeltaTextOperations}; #[test] fn node_insert_test() { @@ -37,71 +34,6 @@ fn node_insert_with_empty_path_test() { test.run_scripts(scripts); } -#[test] -#[should_panic] -fn node_insert_with_not_exist_path_test() { - let mut test = NodeTest::new(); - let node_data = NodeData::new("text"); - let path: Path = vec![0, 0, 9].into(); - let scripts = vec![ - InsertNode { - path: path.clone(), - node_data: node_data.clone(), - rev_id: 1, - }, - AssertNode { - path, - expected: Some(node_data), - }, - ]; - test.run_scripts(scripts); -} - -#[test] -// Append the node to the end of the list if the insert path is out of bounds. -fn node_insert_out_of_bound_test() { - let mut test = NodeTest::new(); - let image_a = NodeData::new("image_a"); - let image_b = NodeData::new("image_b"); - let image = NodeDataBuilder::new("image_1") - .add_node_data(image_a) - .add_node_data(image_b) - .build(); - let text_node = NodeDataBuilder::new("text_1").add_node_data(image).build(); - let image_c = NodeData::new("image_c"); - - let scripts = vec![ - InsertNode { - path: 0.into(), - node_data: text_node, - rev_id: 1, - }, - // 0:text_1 - // 0:image_1 - // 0:image_a - // 1:image_b - InsertNode { - path: vec![0, 0, 10].into(), - node_data: image_c.clone(), - rev_id: 2, - }, - // 0:text_1 - // 0:image_1 - // 0:image_a - // 1:image_b - // 2:image_b - AssertNode { - path: vec![0, 0, 2].into(), - expected: Some(image_c), - }, - AssertNode { - path: vec![0, 0, 10].into(), - expected: None, - }, - ]; - test.run_scripts(scripts); -} - #[test] fn tree_insert_multiple_nodes_at_root_path_test() { let mut test = NodeTest::new(); @@ -438,11 +370,11 @@ fn node_delete_node_from_list_test() { InsertNode { path: 1.into(), node_data: text_node_2.clone(), - rev_id: 1, + rev_id: 2, }, DeleteNode { path: 0.into(), - rev_id: 2, + rev_id: 3, }, AssertNode { path: 1.into(), @@ -487,7 +419,7 @@ fn node_delete_nested_node_test() { InsertNode { path: 1.into(), node_data: text_node_2, - rev_id: 1, + rev_id: 2, }, // 0:text_1 // 0:image_1 @@ -499,7 +431,7 @@ fn node_delete_nested_node_test() { // 1:image_b DeleteNode { path: vec![0, 0, 0].into(), - rev_id: 2, + rev_id: 3, }, // 0:text_1 // 0:image_1 @@ -514,7 +446,7 @@ fn node_delete_nested_node_test() { }, DeleteNode { path: vec![0, 0].into(), - rev_id: 3, + rev_id: 4, }, // 0:text_1 // 1:text_2 @@ -676,12 +608,16 @@ fn node_reorder_nodes_test() { // 1:image_b DeleteNode { path: vec![0].into(), - rev_id: 2, + rev_id: 3, + }, + AssertNode { + path: vec![0].into(), + expected: Some(text_node_2.clone()), }, InsertNode { path: vec![1].into(), node_data: text_node_1.clone(), - rev_id: 3, + rev_id: 4, }, // 0:text_2 // 0:image_2 @@ -722,10 +658,8 @@ fn node_reorder_nodes_test() { #[test] fn node_update_body_test() { let mut test = NodeTest::new(); - let (initial_delta, changeset, _, expected) = make_node_delta_changeset("Hello", "AppFlowy"); - let node = NodeDataBuilder::new("text") - .insert_body(Body::Delta(initial_delta)) - .build(); + let (initial_delta, changeset, expected) = make_node_delta_changeset("Hello", "AppFlowy"); + let node = NodeDataBuilder::new("text").insert_delta(initial_delta).build(); let scripts = vec![ InsertNode { @@ -748,10 +682,8 @@ fn node_update_body_test() { #[test] fn node_inverted_body_changeset_test() { let mut test = NodeTest::new(); - let (initial_delta, changeset, inverted_changeset, _expected) = make_node_delta_changeset("Hello", "AppFlowy"); - let node = NodeDataBuilder::new("text") - .insert_body(Body::Delta(initial_delta.clone())) - .build(); + let (initial_delta, changeset, _expected) = make_node_delta_changeset("Hello", "AppFlowy"); + let node = NodeDataBuilder::new("text").insert_delta(initial_delta.clone()).build(); let scripts = vec![ InsertNode { @@ -761,11 +693,11 @@ fn node_inverted_body_changeset_test() { }, UpdateBody { path: 0.into(), - changeset, + changeset: changeset.clone(), }, UpdateBody { path: 0.into(), - changeset: inverted_changeset, + changeset: changeset.inverted(), }, AssertNodeDelta { path: 0.into(), @@ -774,27 +706,3 @@ fn node_inverted_body_changeset_test() { ]; test.run_scripts(scripts); } - -fn make_node_delta_changeset( - initial_content: &str, - insert_str: &str, -) -> (DeltaTextOperations, Changeset, Changeset, DeltaTextOperations) { - let initial_content = initial_content.to_owned(); - let initial_delta = DeltaTextOperationBuilder::new().insert(&initial_content).build(); - let delta = DeltaTextOperationBuilder::new() - .retain(initial_content.len()) - .insert(insert_str) - .build(); - let inverted = delta.invert(&initial_delta); - let expected = initial_delta.compose(&delta).unwrap(); - - let changeset = Changeset::Delta { - delta: delta.clone(), - inverted: inverted.clone(), - }; - let inverted_changeset = Changeset::Delta { - delta: inverted, - inverted: delta, - }; - (initial_delta, changeset, inverted_changeset, expected) -}