瀏覽代碼

Merge pull request #859 from AppFlowy-IO/feat/document-model-in-rust

Feat: document model in rust
Nathan.fooo 2 年之前
父節點
當前提交
cebee48248

+ 11 - 11
frontend/app_flowy/packages/appflowy_editor/lib/src/operation/operation.dart

@@ -2,14 +2,14 @@ import 'package:appflowy_editor/appflowy_editor.dart';
 
 abstract class Operation {
   factory Operation.fromJson(Map<String, dynamic> map) {
-    String t = map["type"] as String;
-    if (t == "insert-operation") {
+    String t = map["op"] as String;
+    if (t == "insert") {
       return InsertOperation.fromJson(map);
-    } else if (t == "update-operation") {
+    } else if (t == "update") {
       return UpdateOperation.fromJson(map);
-    } else if (t == "delete-operation") {
+    } else if (t == "delete") {
       return DeleteOperation.fromJson(map);
-    } else if (t == "text-edit-operation") {
+    } else if (t == "text-edit") {
       return TextEditOperation.fromJson(map);
     }
 
@@ -51,7 +51,7 @@ class InsertOperation extends Operation {
   @override
   Map<String, dynamic> toJson() {
     return {
-      "type": "insert-operation",
+      "op": "insert",
       "path": path.toList(),
       "nodes": nodes.map((n) => n.toJson()),
     };
@@ -95,7 +95,7 @@ class UpdateOperation extends Operation {
   @override
   Map<String, dynamic> toJson() {
     return {
-      "type": "update-operation",
+      "op": "update",
       "path": path.toList(),
       "attributes": {...attributes},
       "oldAttributes": {...oldAttributes},
@@ -132,7 +132,7 @@ class DeleteOperation extends Operation {
   @override
   Map<String, dynamic> toJson() {
     return {
-      "type": "delete-operation",
+      "op": "delete",
       "path": path.toList(),
       "nodes": nodes.map((n) => n.toJson()),
     };
@@ -171,7 +171,7 @@ class TextEditOperation extends Operation {
   @override
   Map<String, dynamic> toJson() {
     return {
-      "type": "text-edit-operation",
+      "op": "text-edit",
       "path": path.toList(),
       "delta": delta.toJson(),
       "invert": inverted.toJson(),
@@ -207,10 +207,10 @@ Path transformPath(Path preInsertPath, Path b, [int delta = 1]) {
 
 Operation transformOperation(Operation a, Operation b) {
   if (a is InsertOperation) {
-    final newPath = transformPath(a.path, b.path);
+    final newPath = transformPath(a.path, b.path, a.nodes.length);
     return b.copyWithPath(newPath);
   } else if (a is DeleteOperation) {
-    final newPath = transformPath(a.path, b.path, -1);
+    final newPath = transformPath(a.path, b.path, -1 * a.nodes.length);
     return b.copyWithPath(newPath);
   }
   // TODO: transform update and textedit

+ 2 - 2
frontend/app_flowy/packages/appflowy_editor/test/legacy/operation_test.dart

@@ -84,7 +84,7 @@ void main() {
       expect(transaction.toJson(), {
         "operations": [
           {
-            "type": "insert-operation",
+            "op": "insert",
             "path": [0],
             "nodes": [item1.toJson()],
           }
@@ -107,7 +107,7 @@ void main() {
       expect(transaction.toJson(), {
         "operations": [
           {
-            "type": "delete-operation",
+            "op": "delete",
             "path": [0],
             "nodes": [item1.toJson()],
           }

+ 7 - 0
frontend/rust-lib/Cargo.lock

@@ -1618,6 +1618,12 @@ dependencies = [
  "serde",
 ]
 
+[[package]]
+name = "indextree"
+version = "4.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42b4b46b3311ebd8e5cd44f6b03b36e0f48a70552cf6b036afcebc5626794066"
+
 [[package]]
 name = "instant"
 version = "0.1.12"
@@ -1766,6 +1772,7 @@ dependencies = [
  "bytes",
  "dashmap",
  "derive_more",
+ "indextree",
  "lazy_static",
  "log",
  "md5",

+ 7 - 0
shared-lib/Cargo.lock

@@ -741,6 +741,12 @@ dependencies = [
  "serde",
 ]
 
+[[package]]
+name = "indextree"
+version = "4.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42b4b46b3311ebd8e5cd44f6b03b36e0f48a70552cf6b036afcebc5626794066"
+
 [[package]]
 name = "instant"
 version = "0.1.12"
@@ -809,6 +815,7 @@ dependencies = [
  "bytes",
  "dashmap",
  "derive_more",
+ "indextree",
  "lazy_static",
  "log",
  "md5",

+ 1 - 0
shared-lib/lib-ot/Cargo.toml

@@ -24,6 +24,7 @@ lazy_static = "1.4.0"
 strum = "0.21"
 strum_macros = "0.21"
 bytes = "1.0"
+indextree = "4.4.0"
 
 
 [features]

+ 22 - 0
shared-lib/lib-ot/src/core/document/attributes.rs

@@ -0,0 +1,22 @@
+use std::collections::HashMap;
+
+#[derive(Clone, serde::Serialize, serde::Deserialize)]
+pub struct NodeAttributes(pub HashMap<String, Option<String>>);
+
+impl NodeAttributes {
+    pub fn new() -> NodeAttributes {
+        NodeAttributes(HashMap::new())
+    }
+
+    pub fn compose(a: &NodeAttributes, b: &NodeAttributes) -> NodeAttributes {
+        let mut new_map: HashMap<String, Option<String>> = b.0.clone();
+
+        for (key, value) in &a.0 {
+            if b.0.contains_key(key.as_str()) {
+                new_map.insert(key.into(), value.clone());
+            }
+        }
+
+        NodeAttributes(new_map)
+    }
+}

+ 211 - 0
shared-lib/lib-ot/src/core/document/document.rs

@@ -0,0 +1,211 @@
+use crate::core::document::position::Position;
+use crate::core::{
+    DocumentOperation, NodeAttributes, NodeData, NodeSubTree, OperationTransform, TextDelta, Transaction,
+};
+use crate::errors::{ErrorBuilder, OTError, OTErrorCode};
+use indextree::{Arena, NodeId};
+
+pub struct DocumentTree {
+    pub arena: Arena<NodeData>,
+    pub root: NodeId,
+}
+
+impl DocumentTree {
+    pub fn new() -> DocumentTree {
+        let mut arena = Arena::new();
+        let root = arena.new_node(NodeData::new("root".into()));
+        DocumentTree { arena, root }
+    }
+
+    pub fn node_at_path(&self, position: &Position) -> Option<NodeId> {
+        if position.is_empty() {
+            return Some(self.root);
+        }
+
+        let mut iterate_node = self.root;
+
+        for id in &position.0 {
+            let child = self.child_at_index_of_path(iterate_node, id.clone());
+            iterate_node = match child {
+                Some(node) => node,
+                None => return None,
+            };
+        }
+
+        Some(iterate_node)
+    }
+
+    pub fn path_of_node(&self, node_id: NodeId) -> Position {
+        let mut path: Vec<usize> = Vec::new();
+
+        let mut ancestors = node_id.ancestors(&self.arena);
+        let mut current_node = node_id;
+        let mut parent = ancestors.next();
+
+        while parent.is_some() {
+            let parent_node = parent.unwrap();
+            let counter = self.index_of_node(parent_node, current_node);
+            path.push(counter);
+            current_node = parent_node;
+            parent = ancestors.next();
+        }
+
+        Position(path)
+    }
+
+    fn index_of_node(&self, parent_node: NodeId, child_node: NodeId) -> usize {
+        let mut counter: usize = 0;
+
+        let mut children_iterator = parent_node.children(&self.arena);
+        let mut node = children_iterator.next();
+
+        while node.is_some() {
+            if node.unwrap() == child_node {
+                return counter;
+            }
+
+            node = children_iterator.next();
+            counter += 1;
+        }
+
+        counter
+    }
+
+    fn child_at_index_of_path(&self, at_node: NodeId, index: usize) -> Option<NodeId> {
+        let children = at_node.children(&self.arena);
+
+        let mut counter = 0;
+        for child in children {
+            if counter == index {
+                return Some(child);
+            }
+
+            counter += 1;
+        }
+
+        None
+    }
+
+    pub fn apply(&mut self, transaction: Transaction) -> Result<(), OTError> {
+        for op in &transaction.operations {
+            self.apply_op(op)?;
+        }
+        Ok(())
+    }
+
+    fn apply_op(&mut self, op: &DocumentOperation) -> Result<(), OTError> {
+        match op {
+            DocumentOperation::Insert { path, nodes } => self.apply_insert(path, nodes),
+            DocumentOperation::Update { path, attributes, .. } => self.apply_update(path, attributes),
+            DocumentOperation::Delete { path, nodes } => self.apply_delete(path, nodes.len()),
+            DocumentOperation::TextEdit { path, delta, .. } => self.apply_text_edit(path, delta),
+        }
+    }
+
+    fn apply_insert(&mut self, path: &Position, nodes: &[Box<NodeSubTree>]) -> Result<(), OTError> {
+        let parent_path = &path.0[0..(path.0.len() - 1)];
+        let last_index = path.0[path.0.len() - 1];
+        let parent_node = self
+            .node_at_path(&Position(parent_path.to_vec()))
+            .ok_or(ErrorBuilder::new(OTErrorCode::PathNotFound).build())?;
+
+        self.insert_child_at_index(parent_node, last_index, nodes.as_ref())
+    }
+
+    fn insert_child_at_index(
+        &mut self,
+        parent: NodeId,
+        index: usize,
+        insert_children: &[Box<NodeSubTree>],
+    ) -> Result<(), OTError> {
+        if index == 0 && parent.children(&self.arena).next().is_none() {
+            self.append_subtree(&parent, insert_children);
+            return Ok(());
+        }
+
+        let children_length = parent.children(&self.arena).fold(0, |counter, _| counter + 1);
+
+        if index == children_length {
+            self.append_subtree(&parent, insert_children);
+            return Ok(());
+        }
+
+        let node_to_insert = self
+            .child_at_index_of_path(parent, index)
+            .ok_or(ErrorBuilder::new(OTErrorCode::PathNotFound).build())?;
+
+        self.insert_subtree_before(&node_to_insert, insert_children);
+        Ok(())
+    }
+
+    // recursive append the subtrees to the node
+    fn append_subtree(&mut self, parent: &NodeId, insert_children: &[Box<NodeSubTree>]) {
+        for child in insert_children {
+            let child_id = self.arena.new_node(child.to_node_data());
+            parent.append(child_id, &mut self.arena);
+
+            self.append_subtree(&child_id, child.children.as_ref());
+        }
+    }
+
+    fn insert_subtree_before(&mut self, before: &NodeId, insert_children: &[Box<NodeSubTree>]) {
+        for child in insert_children {
+            let child_id = self.arena.new_node(child.to_node_data());
+            before.insert_before(child_id, &mut self.arena);
+
+            self.append_subtree(&child_id, child.children.as_ref());
+        }
+    }
+
+    fn apply_update(&mut self, path: &Position, attributes: &NodeAttributes) -> Result<(), OTError> {
+        let update_node = self
+            .node_at_path(path)
+            .ok_or(ErrorBuilder::new(OTErrorCode::PathNotFound).build())?;
+        let node_data = self.arena.get_mut(update_node).unwrap();
+        let new_node = {
+            let old_attributes = &node_data.get().attributes;
+            let new_attributes = NodeAttributes::compose(&old_attributes, attributes);
+            NodeData {
+                attributes: new_attributes,
+                ..node_data.get().clone()
+            }
+        };
+        *node_data.get_mut() = new_node;
+        Ok(())
+    }
+
+    fn apply_delete(&mut self, path: &Position, len: usize) -> Result<(), OTError> {
+        let mut update_node = self
+            .node_at_path(path)
+            .ok_or(ErrorBuilder::new(OTErrorCode::PathNotFound).build())?;
+        for _ in 0..len {
+            let next = update_node.following_siblings(&self.arena).next();
+            update_node.remove_subtree(&mut self.arena);
+            if let Some(next_id) = next {
+                update_node = next_id;
+            } else {
+                break;
+            }
+        }
+        Ok(())
+    }
+
+    fn apply_text_edit(&mut self, path: &Position, delta: &TextDelta) -> Result<(), OTError> {
+        let edit_node = self
+            .node_at_path(path)
+            .ok_or(ErrorBuilder::new(OTErrorCode::PathNotFound).build())?;
+        let node_data = self.arena.get_mut(edit_node).unwrap();
+        let new_delta = if let Some(old_delta) = &node_data.get().delta {
+            Some(old_delta.compose(delta)?)
+        } else {
+            None
+        };
+        if let Some(new_delta) = new_delta {
+            *node_data.get_mut() = NodeData {
+                delta: Some(new_delta),
+                ..node_data.get().clone()
+            };
+        };
+        Ok(())
+    }
+}

+ 215 - 0
shared-lib/lib-ot/src/core/document/document_operation.rs

@@ -0,0 +1,215 @@
+use crate::core::document::position::Position;
+use crate::core::{NodeAttributes, NodeSubTree, TextDelta};
+
+#[derive(Clone, serde::Serialize, serde::Deserialize)]
+#[serde(tag = "op")]
+pub enum DocumentOperation {
+    #[serde(rename = "insert")]
+    Insert {
+        path: Position,
+        nodes: Vec<Box<NodeSubTree>>,
+    },
+    #[serde(rename = "update")]
+    Update {
+        path: Position,
+        attributes: NodeAttributes,
+        #[serde(rename = "oldAttributes")]
+        old_attributes: NodeAttributes,
+    },
+    #[serde(rename = "delete")]
+    Delete {
+        path: Position,
+        nodes: Vec<Box<NodeSubTree>>,
+    },
+    #[serde(rename = "text-edit")]
+    TextEdit {
+        path: Position,
+        delta: TextDelta,
+        inverted: TextDelta,
+    },
+}
+
+impl DocumentOperation {
+    pub fn path(&self) -> &Position {
+        match self {
+            DocumentOperation::Insert { path, .. } => path,
+            DocumentOperation::Update { path, .. } => path,
+            DocumentOperation::Delete { path, .. } => path,
+            DocumentOperation::TextEdit { path, .. } => path,
+        }
+    }
+    pub fn invert(&self) -> DocumentOperation {
+        match self {
+            DocumentOperation::Insert { path, nodes } => DocumentOperation::Delete {
+                path: path.clone(),
+                nodes: nodes.clone(),
+            },
+            DocumentOperation::Update {
+                path,
+                attributes,
+                old_attributes,
+            } => DocumentOperation::Update {
+                path: path.clone(),
+                attributes: old_attributes.clone(),
+                old_attributes: attributes.clone(),
+            },
+            DocumentOperation::Delete { path, nodes } => DocumentOperation::Insert {
+                path: path.clone(),
+                nodes: nodes.clone(),
+            },
+            DocumentOperation::TextEdit { path, delta, inverted } => DocumentOperation::TextEdit {
+                path: path.clone(),
+                delta: inverted.clone(),
+                inverted: delta.clone(),
+            },
+        }
+    }
+    pub fn clone_with_new_path(&self, path: Position) -> DocumentOperation {
+        match self {
+            DocumentOperation::Insert { nodes, .. } => DocumentOperation::Insert {
+                path,
+                nodes: nodes.clone(),
+            },
+            DocumentOperation::Update {
+                attributes,
+                old_attributes,
+                ..
+            } => DocumentOperation::Update {
+                path,
+                attributes: attributes.clone(),
+                old_attributes: old_attributes.clone(),
+            },
+            DocumentOperation::Delete { nodes, .. } => DocumentOperation::Delete {
+                path,
+                nodes: nodes.clone(),
+            },
+            DocumentOperation::TextEdit { delta, inverted, .. } => DocumentOperation::TextEdit {
+                path,
+                delta: delta.clone(),
+                inverted: inverted.clone(),
+            },
+        }
+    }
+    pub fn transform(a: &DocumentOperation, b: &DocumentOperation) -> DocumentOperation {
+        match a {
+            DocumentOperation::Insert { path: a_path, nodes } => {
+                let new_path = Position::transform(a_path, b.path(), nodes.len() as i64);
+                b.clone_with_new_path(new_path)
+            }
+            DocumentOperation::Delete { path: a_path, nodes } => {
+                let new_path = Position::transform(a_path, b.path(), nodes.len() as i64);
+                b.clone_with_new_path(new_path)
+            }
+            _ => b.clone(),
+        }
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use crate::core::{Delta, DocumentOperation, NodeAttributes, NodeSubTree, Position};
+
+    #[test]
+    fn test_transform_path_1() {
+        assert_eq!(
+            { Position::transform(&Position(vec![0, 1]), &Position(vec![0, 1]), 1) }.0,
+            vec![0, 2]
+        );
+    }
+
+    #[test]
+    fn test_transform_path_2() {
+        assert_eq!(
+            { Position::transform(&Position(vec![0, 1]), &Position(vec![0, 2]), 1) }.0,
+            vec![0, 3]
+        );
+    }
+
+    #[test]
+    fn test_transform_path_3() {
+        assert_eq!(
+            { Position::transform(&Position(vec![0, 1]), &Position(vec![0, 2, 7, 8, 9]), 1) }.0,
+            vec![0, 3, 7, 8, 9]
+        );
+    }
+
+    #[test]
+    fn test_transform_path_not_changed() {
+        assert_eq!(
+            { Position::transform(&Position(vec![0, 1, 2]), &Position(vec![0, 0, 7, 8, 9]), 1) }.0,
+            vec![0, 0, 7, 8, 9]
+        );
+        assert_eq!(
+            { Position::transform(&Position(vec![0, 1, 2]), &Position(vec![0, 1]), 1) }.0,
+            vec![0, 1]
+        );
+        assert_eq!(
+            { Position::transform(&Position(vec![1, 1]), &Position(vec![1, 0]), 1) }.0,
+            vec![1, 0]
+        );
+    }
+
+    #[test]
+    fn test_transform_delta() {
+        assert_eq!(
+            { Position::transform(&Position(vec![0, 1]), &Position(vec![0, 1]), 5) }.0,
+            vec![0, 6]
+        );
+    }
+
+    #[test]
+    fn test_serialize_insert_operation() {
+        let insert = DocumentOperation::Insert {
+            path: Position(vec![0, 1]),
+            nodes: vec![Box::new(NodeSubTree::new("text"))],
+        };
+        let result = serde_json::to_string(&insert).unwrap();
+        assert_eq!(
+            result,
+            r#"{"op":"insert","path":[0,1],"nodes":[{"type":"text","attributes":{}}]}"#
+        );
+    }
+
+    #[test]
+    fn test_serialize_insert_sub_trees() {
+        let insert = DocumentOperation::Insert {
+            path: Position(vec![0, 1]),
+            nodes: vec![Box::new(NodeSubTree {
+                node_type: "text".into(),
+                attributes: NodeAttributes::new(),
+                delta: None,
+                children: vec![Box::new(NodeSubTree::new("text".into()))],
+            })],
+        };
+        let result = serde_json::to_string(&insert).unwrap();
+        assert_eq!(
+            result,
+            r#"{"op":"insert","path":[0,1],"nodes":[{"type":"text","attributes":{},"children":[{"type":"text","attributes":{}}]}]}"#
+        );
+    }
+
+    #[test]
+    fn test_serialize_update_operation() {
+        let insert = DocumentOperation::Update {
+            path: Position(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 insert = DocumentOperation::TextEdit {
+            path: Position(vec![0, 1]),
+            delta: Delta::new(),
+            inverted: Delta::new(),
+        };
+        let result = serde_json::to_string(&insert).unwrap();
+        assert_eq!(result, r#"{"op":"text-edit","path":[0,1],"delta":[],"inverted":[]}"#);
+    }
+}

+ 13 - 0
shared-lib/lib-ot/src/core/document/mod.rs

@@ -0,0 +1,13 @@
+mod attributes;
+mod document;
+mod document_operation;
+mod node;
+mod position;
+mod transaction;
+
+pub use attributes::*;
+pub use document::*;
+pub use document_operation::*;
+pub use node::*;
+pub use position::*;
+pub use transaction::*;

+ 48 - 0
shared-lib/lib-ot/src/core/document/node.rs

@@ -0,0 +1,48 @@
+use crate::core::{NodeAttributes, TextDelta};
+
+#[derive(Clone)]
+pub struct NodeData {
+    pub node_type: String,
+    pub attributes: NodeAttributes,
+    pub delta: Option<TextDelta>,
+}
+
+impl NodeData {
+    pub fn new(node_type: &str) -> NodeData {
+        NodeData {
+            node_type: node_type.into(),
+            attributes: NodeAttributes::new(),
+            delta: None,
+        }
+    }
+}
+
+#[derive(Clone, serde::Serialize, serde::Deserialize)]
+pub struct NodeSubTree {
+    #[serde(rename = "type")]
+    pub node_type: String,
+    pub attributes: NodeAttributes,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub delta: Option<TextDelta>,
+    #[serde(skip_serializing_if = "Vec::is_empty")]
+    pub children: Vec<Box<NodeSubTree>>,
+}
+
+impl NodeSubTree {
+    pub fn new(node_type: &str) -> NodeSubTree {
+        NodeSubTree {
+            node_type: node_type.into(),
+            attributes: NodeAttributes::new(),
+            delta: None,
+            children: Vec::new(),
+        }
+    }
+
+    pub fn to_node_data(&self) -> NodeData {
+        NodeData {
+            node_type: self.node_type.clone(),
+            attributes: self.attributes.clone(),
+            delta: self.delta.clone(),
+        }
+    }
+}

+ 46 - 0
shared-lib/lib-ot/src/core/document/position.rs

@@ -0,0 +1,46 @@
+#[derive(Clone, serde::Serialize, serde::Deserialize)]
+pub struct Position(pub Vec<usize>);
+
+impl Position {
+    pub fn is_empty(&self) -> bool {
+        self.0.is_empty()
+    }
+    pub fn len(&self) -> usize {
+        self.0.len()
+    }
+}
+
+impl Position {
+    // delta is default to be 1
+    pub fn transform(pre_insert_path: &Position, b: &Position, offset: i64) -> Position {
+        if pre_insert_path.len() > b.len() {
+            return b.clone();
+        }
+        if pre_insert_path.is_empty() || b.is_empty() {
+            return b.clone();
+        }
+        // check the prefix
+        for i in 0..(pre_insert_path.len() - 1) {
+            if pre_insert_path.0[i] != b.0[i] {
+                return b.clone();
+            }
+        }
+        let mut prefix: Vec<usize> = pre_insert_path.0[0..(pre_insert_path.len() - 1)].into();
+        let mut suffix: Vec<usize> = b.0[pre_insert_path.0.len()..].into();
+        let prev_insert_last: usize = *pre_insert_path.0.last().unwrap();
+        let b_at_index = b.0[pre_insert_path.0.len() - 1];
+        if prev_insert_last <= b_at_index {
+            prefix.push(((b_at_index as i64) + offset) as usize);
+        } else {
+            prefix.push(b_at_index);
+        }
+        prefix.append(&mut suffix);
+        return Position(prefix);
+    }
+}
+
+impl From<Vec<usize>> for Position {
+    fn from(v: Vec<usize>) -> Self {
+        Position(v)
+    }
+}

+ 106 - 0
shared-lib/lib-ot/src/core/document/transaction.rs

@@ -0,0 +1,106 @@
+use crate::core::document::position::Position;
+use crate::core::{DocumentOperation, DocumentTree, NodeAttributes, NodeSubTree};
+use indextree::NodeId;
+use std::collections::HashMap;
+
+pub struct Transaction {
+    pub operations: Vec<DocumentOperation>,
+}
+
+impl Transaction {
+    fn new(operations: Vec<DocumentOperation>) -> Transaction {
+        Transaction { operations }
+    }
+}
+
+pub struct TransactionBuilder<'a> {
+    document: &'a DocumentTree,
+    operations: Vec<DocumentOperation>,
+}
+
+impl<'a> TransactionBuilder<'a> {
+    pub fn new(document: &'a DocumentTree) -> TransactionBuilder {
+        TransactionBuilder {
+            document,
+            operations: Vec::new(),
+        }
+    }
+
+    pub fn insert_nodes_at_path(&mut self, path: &Position, nodes: &[Box<NodeSubTree>]) {
+        self.push(DocumentOperation::Insert {
+            path: path.clone(),
+            nodes: nodes.to_vec(),
+        });
+    }
+
+    pub fn update_attributes_at_path(&mut self, path: &Position, attributes: HashMap<String, Option<String>>) {
+        let mut old_attributes: HashMap<String, Option<String>> = HashMap::new();
+        let node = self.document.node_at_path(path).unwrap();
+        let node_data = self.document.arena.get(node).unwrap().get();
+
+        for key in attributes.keys() {
+            let old_attrs = &node_data.attributes;
+            let old_value = match old_attrs.0.get(key.as_str()) {
+                Some(value) => value.clone(),
+                None => None,
+            };
+            old_attributes.insert(key.clone(), old_value);
+        }
+
+        self.push(DocumentOperation::Update {
+            path: path.clone(),
+            attributes: NodeAttributes(attributes),
+            old_attributes: NodeAttributes(old_attributes),
+        })
+    }
+
+    pub fn delete_node_at_path(&mut self, path: &Position) {
+        self.delete_nodes_at_path(path, 1);
+    }
+
+    pub fn delete_nodes_at_path(&mut self, path: &Position, length: usize) {
+        let mut node = self.document.node_at_path(path).unwrap();
+        let mut deleted_nodes: Vec<Box<NodeSubTree>> = Vec::new();
+
+        for _ in 0..length {
+            deleted_nodes.push(self.get_deleted_nodes(node.clone()));
+            node = node.following_siblings(&self.document.arena).next().unwrap();
+        }
+
+        self.operations.push(DocumentOperation::Delete {
+            path: path.clone(),
+            nodes: deleted_nodes,
+        })
+    }
+
+    fn get_deleted_nodes(&self, node_id: NodeId) -> Box<NodeSubTree> {
+        let node = self.document.arena.get(node_id.clone()).unwrap();
+        let node_data = node.get();
+        let mut children: Vec<Box<NodeSubTree>> = vec![];
+
+        let mut children_iterators = node_id.children(&self.document.arena);
+        loop {
+            let next_child = children_iterators.next();
+            if let Some(child_id) = next_child {
+                children.push(self.get_deleted_nodes(child_id));
+            } else {
+                break;
+            }
+        }
+
+        Box::new(NodeSubTree {
+            node_type: node_data.node_type.clone(),
+            attributes: node_data.attributes.clone(),
+            delta: node_data.delta.clone(),
+            children,
+        })
+    }
+
+    pub fn push(&mut self, op: DocumentOperation) {
+        self.operations.push(op);
+    }
+
+    pub fn finalize(self) -> Transaction {
+        Transaction::new(self.operations)
+    }
+}

+ 2 - 0
shared-lib/lib-ot/src/core/mod.rs

@@ -1,9 +1,11 @@
 mod delta;
+mod document;
 mod interval;
 mod operation;
 mod ot_str;
 
 pub use delta::*;
+pub use document::*;
 pub use interval::*;
 pub use operation::*;
 pub use ot_str::*;

+ 2 - 1
shared-lib/lib-ot/src/errors.rs

@@ -60,7 +60,7 @@ impl std::convert::From<Utf8Error> for OTError {
     }
 }
 
-#[derive(Debug, Clone)]
+#[derive(Debug, Clone, Eq, PartialEq)]
 pub enum OTErrorCode {
     IncompatibleLength,
     ApplyInsertFail,
@@ -74,6 +74,7 @@ pub enum OTErrorCode {
     DuplicatedRevision,
     RevisionIDConflict,
     Internal,
+    PathNotFound,
 }
 
 pub struct ErrorBuilder {

+ 146 - 0
shared-lib/lib-ot/tests/main.rs

@@ -1 +1,147 @@
+use lib_ot::core::{DocumentTree, NodeAttributes, NodeSubTree, Position, 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 = {
+        let mut tb = TransactionBuilder::new(&document);
+        tb.insert_nodes_at_path(&vec![0].into(), &vec![Box::new(NodeSubTree::new("text"))]);
+        tb.finalize()
+    };
+    document.apply(transaction).unwrap();
+
+    assert!(document.node_at_path(&vec![0].into()).is_some());
+    let node = document.node_at_path(&vec![0].into()).unwrap();
+    let node_data = document.arena.get(node).unwrap().get();
+    assert_eq!(node_data.node_type, "text");
+
+    let transaction = {
+        let mut tb = TransactionBuilder::new(&document);
+        tb.update_attributes_at_path(
+            &vec![0].into(),
+            HashMap::from([("subtype".into(), Some("bullet-list".into()))]),
+        );
+        tb.finalize()
+    };
+    document.apply(transaction).unwrap();
+
+    let transaction = {
+        let mut tb = TransactionBuilder::new(&document);
+        tb.delete_node_at_path(&vec![0].into());
+        tb.finalize()
+    };
+    document.apply(transaction).unwrap();
+    assert!(document.node_at_path(&vec![0].into()).is_none());
+}
+
+#[test]
+fn test_inserts_nodes() {
+    let mut document = DocumentTree::new();
+    let transaction = {
+        let mut tb = TransactionBuilder::new(&document);
+        tb.insert_nodes_at_path(&vec![0].into(), &vec![Box::new(NodeSubTree::new("text"))]);
+        tb.insert_nodes_at_path(&vec![1].into(), &vec![Box::new(NodeSubTree::new("text"))]);
+        tb.insert_nodes_at_path(&vec![2].into(), &vec![Box::new(NodeSubTree::new("text"))]);
+        tb.finalize()
+    };
+    document.apply(transaction).unwrap();
+
+    let transaction = {
+        let mut tb = TransactionBuilder::new(&document);
+        tb.insert_nodes_at_path(&vec![1].into(), &vec![Box::new(NodeSubTree::new("text"))]);
+        tb.finalize()
+    };
+    document.apply(transaction).unwrap();
+}
+
+#[test]
+fn test_inserts_subtrees() {
+    let mut document = DocumentTree::new();
+    let transaction = {
+        let mut tb = TransactionBuilder::new(&document);
+        tb.insert_nodes_at_path(
+            &vec![0].into(),
+            &vec![Box::new(NodeSubTree {
+                node_type: "text".into(),
+                attributes: NodeAttributes::new(),
+                delta: None,
+                children: vec![Box::new(NodeSubTree::new("image".into()))],
+            })],
+        );
+        tb.finalize()
+    };
+    document.apply(transaction).unwrap();
+
+    let node = document.node_at_path(&Position(vec![0, 0])).unwrap();
+    let data = document.arena.get(node).unwrap().get();
+    assert_eq!(data.node_type, "image");
+}
+
+#[test]
+fn test_update_nodes() {
+    let mut document = DocumentTree::new();
+    let transaction = {
+        let mut tb = TransactionBuilder::new(&document);
+        tb.insert_nodes_at_path(&vec![0].into(), &vec![Box::new(NodeSubTree::new("text"))]);
+        tb.insert_nodes_at_path(&vec![1].into(), &vec![Box::new(NodeSubTree::new("text"))]);
+        tb.insert_nodes_at_path(&vec![2].into(), &vec![Box::new(NodeSubTree::new("text"))]);
+        tb.finalize()
+    };
+    document.apply(transaction).unwrap();
+
+    let transaction = {
+        let mut tb = TransactionBuilder::new(&document);
+        tb.update_attributes_at_path(&vec![1].into(), HashMap::from([("bolded".into(), Some("true".into()))]));
+        tb.finalize()
+    };
+    document.apply(transaction).unwrap();
+
+    let node = document.node_at_path(&Position(vec![1])).unwrap();
+    let node_data = document.arena.get(node).unwrap().get();
+    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 = {
+        let mut tb = TransactionBuilder::new(&document);
+        tb.insert_nodes_at_path(&vec![0].into(), &vec![Box::new(NodeSubTree::new("text"))]);
+        tb.insert_nodes_at_path(&vec![1].into(), &vec![Box::new(NodeSubTree::new("text"))]);
+        tb.insert_nodes_at_path(&vec![2].into(), &vec![Box::new(NodeSubTree::new("text"))]);
+        tb.finalize()
+    };
+    document.apply(transaction).unwrap();
+
+    let transaction = {
+        let mut tb = TransactionBuilder::new(&document);
+        tb.delete_node_at_path(&Position(vec![1]));
+        tb.finalize()
+    };
+    document.apply(transaction).unwrap();
+
+    let len = document.root.children(&document.arena).fold(0, |count, _| count + 1);
+    assert_eq!(len, 2);
+}
+
+#[test]
+fn test_errors() {
+    let mut document = DocumentTree::new();
+    let transaction = {
+        let mut tb = TransactionBuilder::new(&document);
+        tb.insert_nodes_at_path(&vec![0].into(), &vec![Box::new(NodeSubTree::new("text"))]);
+        tb.insert_nodes_at_path(&vec![100].into(), &vec![Box::new(NodeSubTree::new("text"))]);
+        tb.finalize()
+    };
+    let result = document.apply(transaction);
+    assert_eq!(result.err().unwrap().code, OTErrorCode::PathNotFound);
+}