diff --git a/shared-lib/lib-ot/src/core/document/attributes.rs b/shared-lib/lib-ot/src/core/document/attributes.rs index d9242c67cc..ca5878fb4b 100644 --- a/shared-lib/lib-ot/src/core/document/attributes.rs +++ b/shared-lib/lib-ot/src/core/document/attributes.rs @@ -1,7 +1,10 @@ +use serde::{Deserialize, Serialize}; use std::collections::HashMap; -#[derive(Clone, serde::Serialize, serde::Deserialize)] -pub struct NodeAttributes(pub HashMap>); +pub type AttributeMap = HashMap>; + +#[derive(Clone, Serialize, Deserialize, Eq, PartialEq, Debug)] +pub struct NodeAttributes(pub AttributeMap); impl Default for NodeAttributes { fn default() -> Self { @@ -9,13 +12,31 @@ impl Default for NodeAttributes { } } +impl std::ops::Deref for NodeAttributes { + type Target = AttributeMap; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl std::ops::DerefMut for NodeAttributes { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + impl NodeAttributes { pub fn new() -> NodeAttributes { NodeAttributes(HashMap::new()) } + pub fn to_inner(&self) -> AttributeMap { + self.0.clone() + } + pub fn compose(a: &NodeAttributes, b: &NodeAttributes) -> NodeAttributes { - let mut new_map: HashMap> = b.0.clone(); + let mut new_map: AttributeMap = b.0.clone(); for (key, value) in &a.0 { if b.0.contains_key(key.as_str()) { diff --git a/shared-lib/lib-ot/src/core/document/document.rs b/shared-lib/lib-ot/src/core/document/document.rs index 4165ff446c..59758c7ca9 100644 --- a/shared-lib/lib-ot/src/core/document/document.rs +++ b/shared-lib/lib-ot/src/core/document/document.rs @@ -126,8 +126,18 @@ impl DocumentTree { Some(self.arena.get(node_id)?.get()) } - pub fn number_of_children(&self) -> usize { - self.root.children(&self.arena).fold(0, |count, _| count + 1) + /// + /// # Arguments + /// + /// * `node_id`: if the node_is is None, then will use root node_id. + /// + /// returns number of the children of the root node + /// + pub fn number_of_children(&self, node_id: Option) -> usize { + match node_id { + None => self.root.children(&self.arena).count(), + Some(node_id) => node_id.children(&self.arena).count(), + } } pub fn following_siblings(&self, node_id: NodeId) -> FollowingSiblings<'_, NodeData> { @@ -176,9 +186,7 @@ impl DocumentTree { return Ok(()); } - let children_length = parent.children(&self.arena).fold(0, |counter, _| counter + 1); - - if index == children_length { + if index == parent.children(&self.arena).count() { self.append_subtree(&parent, insert_children); return Ok(()); } diff --git a/shared-lib/lib-ot/src/core/document/node.rs b/shared-lib/lib-ot/src/core/document/node.rs index ea62f3f192..9115f656f4 100644 --- a/shared-lib/lib-ot/src/core/document/node.rs +++ b/shared-lib/lib-ot/src/core/document/node.rs @@ -1,6 +1,7 @@ use crate::core::{NodeAttributes, TextDelta}; +use serde::{Deserialize, Serialize}; -#[derive(Clone)] +#[derive(Clone, Eq, PartialEq, Debug)] pub struct NodeData { pub node_type: String, pub attributes: NodeAttributes, @@ -17,7 +18,7 @@ impl NodeData { } } -#[derive(Clone, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Serialize, Deserialize, Eq, PartialEq)] pub struct NodeSubTree { #[serde(rename = "type")] pub note_type: String, diff --git a/shared-lib/lib-ot/src/core/document/position.rs b/shared-lib/lib-ot/src/core/document/position.rs index 383581db27..c098096569 100644 --- a/shared-lib/lib-ot/src/core/document/position.rs +++ b/shared-lib/lib-ot/src/core/document/position.rs @@ -1,4 +1,4 @@ -use serde::{Serialize, Deserialize}; +use serde::{Deserialize, Serialize}; #[derive(Clone, Serialize, Deserialize, Eq, PartialEq, Debug)] pub struct Path(pub Vec); @@ -25,7 +25,7 @@ impl std::convert::Into for &usize { impl std::convert::Into for &Path { fn into(self) -> Path { - self.clone() + self.clone() } } @@ -47,7 +47,6 @@ impl From<&[usize]> for Path { } } - impl Path { // delta is default to be 1 pub fn transform(pre_insert_path: &Path, b: &Path, offset: i64) -> Path { diff --git a/shared-lib/lib-ot/src/core/document/transaction.rs b/shared-lib/lib-ot/src/core/document/transaction.rs index a4e4a8f759..a99ab66dac 100644 --- a/shared-lib/lib-ot/src/core/document/transaction.rs +++ b/shared-lib/lib-ot/src/core/document/transaction.rs @@ -1,7 +1,6 @@ use crate::core::document::position::Path; -use crate::core::{DocumentOperation, DocumentTree, NodeAttributes, NodeSubTree}; +use crate::core::{AttributeMap, DocumentOperation, DocumentTree, NodeAttributes, NodeSubTree}; use indextree::NodeId; -use std::collections::HashMap; pub struct Transaction { pub operations: Vec, @@ -81,8 +80,8 @@ impl<'a> TransactionBuilder<'a> { self.insert_nodes_at_path(path, vec![node]) } - pub fn update_attributes_at_path(self, path: &Path, attributes: HashMap>) -> Self { - let mut old_attributes: HashMap> = HashMap::new(); + pub fn update_attributes_at_path(self, path: &Path, attributes: AttributeMap) -> Self { + let mut old_attributes: AttributeMap = AttributeMap::new(); let node = self.document.node_at_path(path).unwrap(); let node_data = self.document.get_node_data(node).unwrap(); diff --git a/shared-lib/lib-ot/tests/main.rs b/shared-lib/lib-ot/tests/main.rs index 070fba3598..274142d345 100644 --- a/shared-lib/lib-ot/tests/main.rs +++ b/shared-lib/lib-ot/tests/main.rs @@ -1,126 +1 @@ -use lib_ot::core::{DocumentTree, NodeAttributes, NodeSubTree, Path, TransactionBuilder}; -use lib_ot::errors::OTErrorCode; -use std::collections::HashMap; - -#[test] -fn main() { - // Create a new arena - let _document = DocumentTree::new(); -} - -#[test] -fn test_documents() { - let mut document = DocumentTree::new(); - let transaction = TransactionBuilder::new(&document) - .insert_node_at_path(0, NodeSubTree::new("text")) - .finalize(); - - document.apply(transaction).unwrap(); - - assert!(document.node_at_path(0).is_some()); - let node = document.node_at_path(0).unwrap(); - let node_data = document.get_node_data(node).unwrap(); - assert_eq!(node_data.node_type, "text"); - - let transaction = TransactionBuilder::new(&document) - .update_attributes_at_path( - &vec![0].into(), - HashMap::from([("subtype".into(), Some("bullet-list".into()))]), - ) - .finalize(); - document.apply(transaction).unwrap(); - - let transaction = TransactionBuilder::new(&document) - .delete_node_at_path(&vec![0].into()) - .finalize(); - document.apply(transaction).unwrap(); - assert!(document.node_at_path(0).is_none()); -} - -#[test] -fn test_inserts_nodes() { - let mut document = DocumentTree::new(); - let transaction = TransactionBuilder::new(&document) - .insert_node_at_path(0, NodeSubTree::new("text")) - .insert_node_at_path(1, NodeSubTree::new("text")) - .insert_node_at_path(2, NodeSubTree::new("text")) - .finalize(); - document.apply(transaction).unwrap(); - - let transaction = TransactionBuilder::new(&document) - .insert_node_at_path(1, NodeSubTree::new("text")) - .finalize(); - document.apply(transaction).unwrap(); -} - -#[test] -fn test_inserts_subtrees() { - let mut document = DocumentTree::new(); - let transaction = TransactionBuilder::new(&document) - .insert_node_at_path( - 0, - NodeSubTree { - note_type: "text".into(), - attributes: NodeAttributes::new(), - delta: None, - children: vec![NodeSubTree::new("image")], - }, - ) - .finalize(); - document.apply(transaction).unwrap(); - - let node = document.node_at_path(&Path(vec![0, 0])).unwrap(); - let data = document.get_node_data(node).unwrap(); - assert_eq!(data.node_type, "image"); -} - -#[test] -fn test_update_nodes() { - let mut document = DocumentTree::new(); - let transaction = TransactionBuilder::new(&document) - .insert_node_at_path(&vec![0], NodeSubTree::new("text")) - .insert_node_at_path(&vec![1], NodeSubTree::new("text")) - .insert_node_at_path(vec![2], NodeSubTree::new("text")) - .finalize(); - document.apply(transaction).unwrap(); - - let transaction = TransactionBuilder::new(&document) - .update_attributes_at_path(&vec![1].into(), HashMap::from([("bolded".into(), Some("true".into()))])) - .finalize(); - document.apply(transaction).unwrap(); - - let node = document.node_at_path(&Path(vec![1])).unwrap(); - let node_data = document.get_node_data(node).unwrap(); - let is_bold = node_data.attributes.0.get("bolded").unwrap().clone(); - assert_eq!(is_bold.unwrap(), "true"); -} - -#[test] -fn test_delete_nodes() { - let mut document = DocumentTree::new(); - let transaction = TransactionBuilder::new(&document) - .insert_node_at_path(0, NodeSubTree::new("text")) - .insert_node_at_path(1, NodeSubTree::new("text")) - .insert_node_at_path(2, NodeSubTree::new("text")) - .finalize(); - document.apply(transaction).unwrap(); - - let transaction = TransactionBuilder::new(&document) - .delete_node_at_path(&Path(vec![1])) - .finalize(); - document.apply(transaction).unwrap(); - - let len = document.number_of_children(); - assert_eq!(len, 2); -} - -#[test] -fn test_errors() { - let mut document = DocumentTree::new(); - let transaction = TransactionBuilder::new(&document) - .insert_node_at_path(0, NodeSubTree::new("text")) - .insert_node_at_path(100, NodeSubTree::new("text")) - .finalize(); - let result = document.apply(transaction); - assert_eq!(result.err().unwrap().code, OTErrorCode::PathNotFound); -} +mod node; diff --git a/shared-lib/lib-ot/tests/node/mod.rs b/shared-lib/lib-ot/tests/node/mod.rs new file mode 100644 index 0000000000..63d424afaf --- /dev/null +++ b/shared-lib/lib-ot/tests/node/mod.rs @@ -0,0 +1,2 @@ +mod script; +mod test; diff --git a/shared-lib/lib-ot/tests/node/script.rs b/shared-lib/lib-ot/tests/node/script.rs new file mode 100644 index 0000000000..392a746e0b --- /dev/null +++ b/shared-lib/lib-ot/tests/node/script.rs @@ -0,0 +1,76 @@ +use lib_ot::core::{DocumentTree, NodeAttributes, NodeData, NodeSubTree, Path, TransactionBuilder}; + +pub enum NodeScript { + InsertNode { path: Path, node: NodeSubTree }, + InsertAttributes { path: Path, attributes: NodeAttributes }, + DeleteNode { path: Path }, + AssertNumberOfChildrenAtPath { path: Option, len: usize }, + AssertNode { path: Path, expected: Option }, +} + +pub struct NodeTest { + node_tree: DocumentTree, +} + +impl NodeTest { + pub fn new() -> Self { + Self { + node_tree: DocumentTree::new(), + } + } + + pub fn run_scripts(&mut self, scripts: Vec) { + for script in scripts { + self.run_script(script); + } + } + + pub fn run_script(&mut self, script: NodeScript) { + match script { + NodeScript::InsertNode { path, node } => { + let transaction = TransactionBuilder::new(&self.node_tree) + .insert_node_at_path(path, node) + .finalize(); + + self.node_tree.apply(transaction).unwrap(); + } + NodeScript::InsertAttributes { path, attributes } => { + let transaction = TransactionBuilder::new(&self.node_tree) + .update_attributes_at_path(&path, attributes.to_inner()) + .finalize(); + self.node_tree.apply(transaction).unwrap(); + } + NodeScript::DeleteNode { path } => { + let transaction = TransactionBuilder::new(&self.node_tree) + .delete_node_at_path(&path) + .finalize(); + self.node_tree.apply(transaction).unwrap(); + } + NodeScript::AssertNode { path, expected } => { + let node_id = self.node_tree.node_at_path(path); + + match node_id { + None => assert!(node_id.is_none()), + Some(node_id) => { + let node_data = self.node_tree.get_node_data(node_id).cloned(); + assert_eq!(node_data, expected.and_then(|e| Some(e.to_node_data()))); + } + } + } + NodeScript::AssertNumberOfChildrenAtPath { + path, + len: expected_len, + } => match path { + None => { + let len = self.node_tree.number_of_children(None); + assert_eq!(len, expected_len) + } + Some(path) => { + let node_id = self.node_tree.node_at_path(path).unwrap(); + let len = self.node_tree.number_of_children(Some(node_id)); + assert_eq!(len, expected_len) + } + }, + } + } +} diff --git a/shared-lib/lib-ot/tests/node/test.rs b/shared-lib/lib-ot/tests/node/test.rs new file mode 100644 index 0000000000..d299b08714 --- /dev/null +++ b/shared-lib/lib-ot/tests/node/test.rs @@ -0,0 +1,184 @@ +use crate::node::script::NodeScript::*; +use crate::node::script::NodeTest; +use lib_ot::core::{NodeAttributes, NodeSubTree, Path}; + +#[test] +fn node_insert_test() { + let mut test = NodeTest::new(); + let inserted_node = NodeSubTree::new("text"); + let path: Path = 0.into(); + let scripts = vec![ + InsertNode { + path: path.clone(), + node: inserted_node.clone(), + }, + AssertNode { + path, + expected: Some(inserted_node), + }, + ]; + test.run_scripts(scripts); +} + +#[test] +fn node_insert_node_with_children_test() { + let mut test = NodeTest::new(); + let inserted_node = NodeSubTree { + note_type: "text".into(), + attributes: NodeAttributes::new(), + delta: None, + children: vec![NodeSubTree::new("image")], + }; + let path: Path = 0.into(); + let scripts = vec![ + InsertNode { + path: path.clone(), + node: inserted_node.clone(), + }, + AssertNode { + path, + expected: Some(inserted_node), + }, + ]; + test.run_scripts(scripts); +} + +#[test] +fn node_insert_multi_nodes_test() { + let mut test = NodeTest::new(); + let path_1: Path = 0.into(); + let node_1 = NodeSubTree::new("text_1"); + + let path_2: Path = 1.into(); + let node_2 = NodeSubTree::new("text_2"); + + let path_3: Path = 2.into(); + let node_3 = NodeSubTree::new("text_3"); + + let scripts = vec![ + InsertNode { + path: path_1.clone(), + node: node_1.clone(), + }, + InsertNode { + path: path_2.clone(), + node: node_2.clone(), + }, + InsertNode { + path: path_3.clone(), + node: node_3.clone(), + }, + AssertNode { + path: path_1, + expected: Some(node_1), + }, + AssertNode { + path: path_2, + expected: Some(node_2), + }, + AssertNode { + path: path_3, + expected: Some(node_3), + }, + ]; + test.run_scripts(scripts); +} + +#[test] +fn node_insert_node_in_ordered_nodes_test() { + let mut test = NodeTest::new(); + let path_1: Path = 0.into(); + let node_1 = NodeSubTree::new("text_1"); + + let path_2: Path = 1.into(); + let node_2_1 = NodeSubTree::new("text_2_1"); + let node_2_2 = NodeSubTree::new("text_2_2"); + + let path_3: Path = 2.into(); + let node_3 = NodeSubTree::new("text_3"); + + let path_4: Path = 3.into(); + + let scripts = vec![ + InsertNode { + path: path_1.clone(), + node: node_1.clone(), + }, + InsertNode { + path: path_2.clone(), + node: node_2_1.clone(), + }, + InsertNode { + path: path_3.clone(), + node: node_3.clone(), + }, + // 0:note_1 , 1: note_2_1, 2: note_3 + InsertNode { + path: path_2.clone(), + node: node_2_2.clone(), + }, + // 0:note_1 , 1:note_2_2, 2: note_2_1, 3: note_3 + AssertNode { + path: path_1, + expected: Some(node_1), + }, + AssertNode { + path: path_2, + expected: Some(node_2_2), + }, + AssertNode { + path: path_3, + expected: Some(node_2_1), + }, + AssertNode { + path: path_4, + expected: Some(node_3), + }, + AssertNumberOfChildrenAtPath { path: None, len: 4 }, + ]; + test.run_scripts(scripts); +} + +#[test] +fn node_insert_with_attributes_test() { + let mut test = NodeTest::new(); + let path: Path = 0.into(); + let mut inserted_node = NodeSubTree::new("text"); + inserted_node.attributes.insert("bold".to_string(), Some("true".into())); + inserted_node + .attributes + .insert("underline".to_string(), Some("true".into())); + + let scripts = vec![ + InsertNode { + path: path.clone(), + node: inserted_node.clone(), + }, + InsertAttributes { + path: path.clone(), + attributes: inserted_node.attributes.clone(), + }, + AssertNode { + path, + expected: Some(inserted_node), + }, + ]; + test.run_scripts(scripts); +} + +#[test] +fn node_delete_test() { + let mut test = NodeTest::new(); + let inserted_node = NodeSubTree::new("text"); + + let path: Path = 0.into(); + let scripts = vec![ + InsertNode { + path: path.clone(), + node: inserted_node.clone(), + }, + DeleteNode { path: path.clone() }, + AssertNode { path, expected: None }, + ]; + test.run_scripts(scripts); +}