diff --git a/shared-lib/lib-ot/src/core/document/attributes.rs b/shared-lib/lib-ot/src/core/document/attributes.rs index 0c79bbc307..f36654fa54 100644 --- a/shared-lib/lib-ot/src/core/document/attributes.rs +++ b/shared-lib/lib-ot/src/core/document/attributes.rs @@ -39,8 +39,8 @@ impl NodeAttributes { self.0.is_empty() } - pub fn delete(&mut self, key: &AttributeKey) { - self.insert(key.clone(), AttributeValue(None)); + pub fn delete(&mut self, key: K) { + self.insert(key.to_string(), AttributeValue(None)); } } @@ -139,8 +139,34 @@ impl std::convert::From for AttributeValue { fn from(val: bool) -> Self { let val = match val { true => Some("true".to_owned()), - false => None, + false => Some("false".to_owned()), }; AttributeValue(val) } } + +pub struct NodeAttributeBuilder { + attributes: NodeAttributes, +} + +impl NodeAttributeBuilder { + pub fn new() -> Self { + Self { + attributes: NodeAttributes::default(), + } + } + + pub fn insert>(mut self, key: K, value: V) -> Self { + self.attributes.insert(key, value); + self + } + + pub fn delete(mut self, key: K) -> Self { + self.attributes.delete(key); + self + } + + pub fn build(self) -> NodeAttributes { + self.attributes + } +} diff --git a/shared-lib/lib-ot/src/core/document/node_tree.rs b/shared-lib/lib-ot/src/core/document/node_tree.rs index 15e167ace9..41720c8ddd 100644 --- a/shared-lib/lib-ot/src/core/document/node_tree.rs +++ b/shared-lib/lib-ot/src/core/document/node_tree.rs @@ -26,6 +26,13 @@ impl NodeTree { Some(self.arena.get(node_id)?.get()) } + pub fn get_node_at_path(&self, path: &Path) -> Option<&Node> { + { + let node_id = self.node_id_at_path(path)?; + self.get_node(node_id) + } + } + /// /// # Examples /// @@ -41,7 +48,7 @@ impl NodeTree { /// let node_path = node_tree.path_of_node(node_id); /// debug_assert_eq!(node_path, root_path); /// ``` - pub fn node_at_path>(&self, path: T) -> Option { + pub fn node_id_at_path>(&self, path: T) -> Option { let path = path.into(); if path.is_empty() { return Some(self.root); @@ -54,7 +61,7 @@ impl NodeTree { Some(iterate_node) } - pub fn path_of_node(&self, node_id: NodeId) -> Path { + pub fn path_from_node_id(&self, node_id: NodeId) -> Path { let mut path = vec![]; let mut current_node = node_id; // Use .skip(1) on the ancestors iterator to skip the root node. @@ -137,7 +144,7 @@ impl NodeTree { } pub fn apply(&mut self, transaction: Transaction) -> Result<(), OTError> { - for op in &transaction.operations { + for op in transaction.operations.iter() { self.apply_op(op)?; } Ok(()) @@ -164,7 +171,7 @@ impl NodeTree { let (parent_path, last_path) = path.split_at(path.0.len() - 1); let last_index = *last_path.first().unwrap(); let parent_node = self - .node_at_path(parent_path) + .node_id_at_path(parent_path) .ok_or_else(|| ErrorBuilder::new(OTErrorCode::PathNotFound).build())?; self.insert_nodes_at_index(parent_node, last_index, nodes) @@ -228,7 +235,7 @@ impl NodeTree { fn delete_node(&mut self, path: &Path, nodes: &[NodeData]) -> Result<(), OTError> { let mut update_node = self - .node_at_path(path) + .node_id_at_path(path) .ok_or_else(|| ErrorBuilder::new(OTErrorCode::PathNotFound).build())?; for _ in 0..nodes.len() { @@ -255,7 +262,7 @@ impl NodeTree { F: Fn(&mut Node) -> Result<(), OTError>, { let node_id = self - .node_at_path(path) + .node_id_at_path(path) .ok_or_else(|| ErrorBuilder::new(OTErrorCode::PathNotFound).build())?; match self.arena.get_mut(node_id) { None => tracing::warn!("The path: {:?} does not contain any nodes", path), diff --git a/shared-lib/lib-ot/src/core/document/operation.rs b/shared-lib/lib-ot/src/core/document/operation.rs index c22fd6ca54..a8b759f694 100644 --- a/shared-lib/lib-ot/src/core/document/operation.rs +++ b/shared-lib/lib-ot/src/core/document/operation.rs @@ -1,8 +1,10 @@ use crate::core::document::operation_serde::*; use crate::core::document::path::Path; use crate::core::{NodeAttributes, NodeBodyChangeset, NodeData}; +use crate::errors::OTError; +use serde::{Deserialize, Serialize}; -#[derive(Clone, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Serialize, Deserialize)] #[serde(tag = "op")] pub enum NodeOperation { #[serde(rename = "insert")] @@ -99,59 +101,37 @@ impl NodeOperation { } } -#[cfg(test)] -mod tests { - use crate::core::{NodeAttributes, NodeBodyChangeset, NodeBuilder, NodeData, NodeOperation, Path, TextDelta}; - #[test] - fn test_serialize_insert_operation() { - let insert = NodeOperation::Insert { - path: Path(vec![0, 1]), - nodes: vec![NodeData::new("text".to_owned())], - }; - let result = serde_json::to_string(&insert).unwrap(); - assert_eq!(result, r#"{"op":"insert","path":[0,1],"nodes":[{"type":"text"}]}"#); - } +#[derive(Serialize, Deserialize, Default)] +pub struct NodeOperationList { + operations: Vec, +} - #[test] - fn test_serialize_insert_sub_trees() { - let insert = NodeOperation::Insert { - path: Path(vec![0, 1]), - nodes: vec![NodeBuilder::new("text") - .add_node(NodeData::new("text".to_owned())) - .build()], - }; - let result = serde_json::to_string(&insert).unwrap(); - assert_eq!( - result, - r#"{"op":"insert","path":[0,1],"nodes":[{"type":"text","children":[{"type":"text"}]}]}"# - ); - } +impl std::ops::Deref for NodeOperationList { + type Target = Vec; - #[test] - fn test_serialize_update_operation() { - let insert = NodeOperation::UpdateAttributes { - path: Path(vec![0, 1]), - attributes: NodeAttributes::new(), - old_attributes: NodeAttributes::new(), - }; - let result = serde_json::to_string(&insert).unwrap(); - assert_eq!( - result, - r#"{"op":"update","path":[0,1],"attributes":{},"oldAttributes":{}}"# - ); - } - - #[test] - fn test_serialize_text_edit_operation() { - let changeset = NodeBodyChangeset::Delta { - delta: TextDelta::new(), - inverted: TextDelta::new(), - }; - let insert = NodeOperation::UpdateBody { - path: Path(vec![0, 1]), - changeset, - }; - let result = serde_json::to_string(&insert).unwrap(); - assert_eq!(result, r#"{"op":"edit-body","path":[0,1],"delta":[],"inverted":[]}"#); + fn deref(&self) -> &Self::Target { + &self.operations + } +} + +impl std::ops::DerefMut for NodeOperationList { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.operations + } +} + +impl NodeOperationList { + pub fn new(operations: Vec) -> Self { + Self { operations } + } + + pub fn from_bytes(bytes: Vec) -> Result { + let operation_list = serde_json::from_slice(&bytes).map_err(|err| OTError::serde().context(err))?; + Ok(operation_list) + } + + pub fn to_bytes(&self) -> Result, OTError> { + let bytes = serde_json::to_vec(self).map_err(|err| OTError::serde().context(err))?; + Ok(bytes) } } diff --git a/shared-lib/lib-ot/src/core/document/transaction.rs b/shared-lib/lib-ot/src/core/document/transaction.rs index ddd6168d38..4f577e017a 100644 --- a/shared-lib/lib-ot/src/core/document/transaction.rs +++ b/shared-lib/lib-ot/src/core/document/transaction.rs @@ -2,26 +2,28 @@ use crate::core::document::path::Path; use crate::core::{NodeAttributes, NodeData, NodeOperation, NodeTree}; use indextree::NodeId; +use super::{NodeBodyChangeset, NodeOperationList}; + pub struct Transaction { - pub operations: Vec, + pub operations: NodeOperationList, } impl Transaction { - fn new(operations: Vec) -> Transaction { + fn new(operations: NodeOperationList) -> Transaction { Transaction { operations } } } pub struct TransactionBuilder<'a> { node_tree: &'a NodeTree, - operations: Vec, + operations: NodeOperationList, } impl<'a> TransactionBuilder<'a> { pub fn new(node_tree: &'a NodeTree) -> TransactionBuilder { TransactionBuilder { node_tree, - operations: Vec::new(), + operations: NodeOperationList::default(), } } @@ -80,23 +82,39 @@ impl<'a> TransactionBuilder<'a> { self.insert_nodes_at_path(path, vec![node]) } - pub fn update_attributes_at_path(self, path: &Path, attributes: NodeAttributes) -> Self { - let mut old_attributes = NodeAttributes::new(); - let node = self.node_tree.node_at_path(path).unwrap(); - let node_data = self.node_tree.get_node(node).unwrap(); + pub fn update_attributes_at_path(mut self, path: &Path, attributes: NodeAttributes) -> Self { + match self.node_tree.get_node_at_path(path) { + Some(node) => { + let mut old_attributes = NodeAttributes::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()); + } + } - for key in attributes.keys() { - let old_attrs = &node_data.attributes; - if let Some(value) = old_attrs.get(key.as_str()) { - old_attributes.insert(key.clone(), value.clone()); + self.operations.push(NodeOperation::UpdateAttributes { + path: path.clone(), + attributes, + old_attributes, + }); } + None => tracing::warn!("Update attributes at path: {:?} failed. Node is not exist", path), } + self + } - self.push(NodeOperation::UpdateAttributes { - path: path.clone(), - attributes, - old_attributes, - }) + pub fn update_body_at_path(mut self, path: &Path, changeset: NodeBodyChangeset) -> Self { + match self.node_tree.node_id_at_path(path) { + Some(_) => { + self.operations.push(NodeOperation::UpdateBody { + 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, path: &Path) -> Self { @@ -104,7 +122,7 @@ impl<'a> TransactionBuilder<'a> { } pub fn delete_nodes_at_path(mut self, path: &Path, length: usize) -> Self { - let mut node = self.node_tree.node_at_path(path).unwrap(); + let mut node = self.node_tree.node_id_at_path(path).unwrap(); let mut deleted_nodes = vec![]; for _ in 0..length { deleted_nodes.push(self.get_deleted_nodes(node)); diff --git a/shared-lib/lib-ot/src/errors.rs b/shared-lib/lib-ot/src/errors.rs index fe2a03b206..9878c74947 100644 --- a/shared-lib/lib-ot/src/errors.rs +++ b/shared-lib/lib-ot/src/errors.rs @@ -37,6 +37,7 @@ impl OTError { static_ot_error!(duplicate_revision, OTErrorCode::DuplicatedRevision); static_ot_error!(revision_id_conflict, OTErrorCode::RevisionIDConflict); static_ot_error!(internal, OTErrorCode::Internal); + static_ot_error!(serde, OTErrorCode::SerdeError); } impl fmt::Display for OTError { diff --git a/shared-lib/lib-ot/tests/node/mod.rs b/shared-lib/lib-ot/tests/node/mod.rs index 63d424afaf..95f6886069 100644 --- a/shared-lib/lib-ot/tests/node/mod.rs +++ b/shared-lib/lib-ot/tests/node/mod.rs @@ -1,2 +1,3 @@ +mod operation_test; mod script; -mod test; +mod tree_test; diff --git a/shared-lib/lib-ot/tests/node/operation_test.rs b/shared-lib/lib-ot/tests/node/operation_test.rs new file mode 100644 index 0000000000..d84d96d7d6 --- /dev/null +++ b/shared-lib/lib-ot/tests/node/operation_test.rs @@ -0,0 +1,61 @@ +use lib_ot::core::{ + NodeAttributeBuilder, NodeBodyChangeset, NodeBuilder, NodeData, NodeOperation, Path, TextDeltaBuilder, +}; + +#[test] +fn operation_insert_node_serde_test() { + let insert = NodeOperation::Insert { + path: Path(vec![0, 1]), + nodes: vec![NodeData::new("text".to_owned())], + }; + let result = serde_json::to_string(&insert).unwrap(); + assert_eq!(result, r#"{"op":"insert","path":[0,1],"nodes":[{"type":"text"}]}"#); +} + +#[test] +fn operation_insert_node_with_children_serde_test() { + let node = NodeBuilder::new("text") + .add_node(NodeData::new("sub_text".to_owned())) + .build(); + + let insert = NodeOperation::Insert { + path: Path(vec![0, 1]), + nodes: vec![node], + }; + assert_eq!( + serde_json::to_string(&insert).unwrap(), + r#"{"op":"insert","path":[0,1],"nodes":[{"type":"text","children":[{"type":"sub_text"}]}]}"# + ); +} +#[test] +fn operation_update_node_attributes_serde_test() { + let operation = NodeOperation::UpdateAttributes { + path: Path(vec![0, 1]), + attributes: NodeAttributeBuilder::new().insert("bold", true).build(), + old_attributes: NodeAttributeBuilder::new().insert("bold", false).build(), + }; + + let result = serde_json::to_string(&operation).unwrap(); + + assert_eq!( + result, + r#"{"op":"update","path":[0,1],"attributes":{"bold":"true"},"oldAttributes":{"bold":"false"}}"# + ); +} + +#[test] +fn operation_update_node_body_serde_test() { + let delta = TextDeltaBuilder::new().insert("AppFlowy...").build(); + let inverted = delta.invert_str(""); + let changeset = NodeBodyChangeset::Delta { delta, inverted }; + let insert = NodeOperation::UpdateBody { + path: Path(vec![0, 1]), + changeset, + }; + let result = serde_json::to_string(&insert).unwrap(); + assert_eq!( + result, + r#"{"op":"edit-body","path":[0,1],"delta":[{"insert":"AppFlowy..."}],"inverted":[{"delete":11}]}"# + ); + // +} diff --git a/shared-lib/lib-ot/tests/node/script.rs b/shared-lib/lib-ot/tests/node/script.rs index 91f36d0e8b..4068c0f704 100644 --- a/shared-lib/lib-ot/tests/node/script.rs +++ b/shared-lib/lib-ot/tests/node/script.rs @@ -1,11 +1,15 @@ -use lib_ot::core::{NodeAttributes, NodeData, NodeTree, Path, TransactionBuilder}; +use lib_ot::core::{ + NodeAttributes, NodeBody, NodeBodyChangeset, NodeData, NodeTree, Path, TextDelta, TransactionBuilder, +}; pub enum NodeScript { InsertNode { path: Path, node: NodeData }, - InsertAttributes { path: Path, attributes: NodeAttributes }, + UpdateAttributes { path: Path, attributes: NodeAttributes }, + UpdateBody { path: Path, changeset: NodeBodyChangeset }, DeleteNode { path: Path }, AssertNumberOfChildrenAtPath { path: Option, len: usize }, AssertNode { path: Path, expected: Option }, + AssertNodeDelta { path: Path, expected: TextDelta }, } pub struct NodeTest { @@ -34,12 +38,19 @@ impl NodeTest { self.node_tree.apply(transaction).unwrap(); } - NodeScript::InsertAttributes { path, attributes } => { + NodeScript::UpdateAttributes { path, attributes } => { let transaction = TransactionBuilder::new(&self.node_tree) .update_attributes_at_path(&path, attributes) .finalize(); self.node_tree.apply(transaction).unwrap(); } + NodeScript::UpdateBody { path, changeset } => { + // + let transaction = TransactionBuilder::new(&self.node_tree) + .update_body_at_path(&path, changeset) + .finalize(); + self.node_tree.apply(transaction).unwrap(); + } NodeScript::DeleteNode { path } => { let transaction = TransactionBuilder::new(&self.node_tree) .delete_node_at_path(&path) @@ -47,7 +58,7 @@ impl NodeTest { self.node_tree.apply(transaction).unwrap(); } NodeScript::AssertNode { path, expected } => { - let node_id = self.node_tree.node_at_path(path); + let node_id = self.node_tree.node_id_at_path(path); match node_id { None => assert!(node_id.is_none()), @@ -66,11 +77,19 @@ impl NodeTest { assert_eq!(len, expected_len) } Some(path) => { - let node_id = self.node_tree.node_at_path(path).unwrap(); + let node_id = self.node_tree.node_id_at_path(path).unwrap(); let len = self.node_tree.number_of_children(Some(node_id)); assert_eq!(len, expected_len) } }, + NodeScript::AssertNodeDelta { path, expected } => { + let node = self.node_tree.get_node_at_path(&path).unwrap(); + if let NodeBody::Delta(delta) = node.body.clone() { + debug_assert_eq!(delta, expected); + } else { + panic!("Node body type not match, expect Delta"); + } + } } } } diff --git a/shared-lib/lib-ot/tests/node/test.rs b/shared-lib/lib-ot/tests/node/tree_test.rs similarity index 81% rename from shared-lib/lib-ot/tests/node/test.rs rename to shared-lib/lib-ot/tests/node/tree_test.rs index 6def19a025..8ec1d3b8dc 100644 --- a/shared-lib/lib-ot/tests/node/test.rs +++ b/shared-lib/lib-ot/tests/node/tree_test.rs @@ -1,5 +1,9 @@ use crate::node::script::NodeScript::*; use crate::node::script::NodeTest; +use lib_ot::core::NodeBody; +use lib_ot::core::NodeBodyChangeset; +use lib_ot::core::OperationTransform; +use lib_ot::core::TextDeltaBuilder; use lib_ot::core::{NodeBuilder, NodeData, Path}; #[test] @@ -147,7 +151,7 @@ fn node_insert_with_attributes_test() { path: path.clone(), node: inserted_node.clone(), }, - InsertAttributes { + UpdateAttributes { path: path.clone(), attributes: inserted_node.attributes.clone(), }, @@ -175,3 +179,32 @@ fn node_delete_test() { ]; test.run_scripts(scripts); } + +#[test] +fn node_update_body_test() { + let mut test = NodeTest::new(); + let path: Path = 0.into(); + + let s = "Hello".to_owned(); + let init_delta = TextDeltaBuilder::new().insert(&s).build(); + let delta = TextDeltaBuilder::new().retain(s.len()).insert(" AppFlowy").build(); + let inverted = delta.invert(&init_delta); + let expected = init_delta.compose(&delta).unwrap(); + + let node = NodeBuilder::new("text") + .insert_body(NodeBody::Delta(init_delta)) + .build(); + + let scripts = vec![ + InsertNode { + path: path.clone(), + node: node.clone(), + }, + UpdateBody { + path: path.clone(), + changeset: NodeBodyChangeset::Delta { delta, inverted }, + }, + AssertNodeDelta { path, expected }, + ]; + test.run_scripts(scripts); +}