فهرست منبع

feat: create folder node & add tests

nathan 2 سال پیش
والد
کامیت
c89277d507

+ 95 - 0
shared-lib/flowy-sync/src/client_folder/app_node.rs

@@ -0,0 +1,95 @@
+use crate::client_folder::view_node::ViewNode;
+use crate::client_folder::{get_attributes_str_value, set_attributes_str_value, AtomicNodeTree};
+use crate::errors::CollaborateResult;
+use folder_rev_model::{AppRevision, ViewRevision};
+use lib_ot::core::{NodeData, NodeDataBuilder, NodeOperation, Path, Transaction};
+use std::sync::Arc;
+
+#[derive(Debug, Clone)]
+pub struct AppNode {
+    pub id: String,
+    tree: Arc<AtomicNodeTree>,
+    pub(crate) path: Path,
+    views: Vec<Arc<ViewNode>>,
+}
+
+impl AppNode {
+    pub(crate) fn from_app_revision(
+        transaction: &mut Transaction,
+        revision: AppRevision,
+        tree: Arc<AtomicNodeTree>,
+        path: Path,
+    ) -> CollaborateResult<Self> {
+        let app_id = revision.id.clone();
+        let app_node = NodeDataBuilder::new("app")
+            .insert_attribute("id", revision.id)
+            .insert_attribute("name", revision.name)
+            .insert_attribute("workspace_id", revision.workspace_id)
+            .build();
+
+        transaction.push_operation(NodeOperation::Insert {
+            path: path.clone(),
+            nodes: vec![app_node],
+        });
+
+        let views = revision
+            .belongings
+            .into_iter()
+            .enumerate()
+            .map(|(index, app)| (path.clone_with(index), app))
+            .flat_map(
+                |(path, app)| match ViewNode::from_view_revision(transaction, app, tree.clone(), path) {
+                    Ok(view_node) => Some(Arc::new(view_node)),
+                    Err(err) => {
+                        tracing::error!("create view node failed: {:?}", err);
+                        None
+                    }
+                },
+            )
+            .collect::<Vec<Arc<ViewNode>>>();
+
+        Ok(Self {
+            id: app_id,
+            tree,
+            path,
+            views,
+        })
+    }
+
+    pub fn get_name(&self) -> Option<String> {
+        get_attributes_str_value(self.tree.clone(), &self.path, "name")
+    }
+
+    pub fn set_name(&self, name: &str) -> CollaborateResult<()> {
+        set_attributes_str_value(self.tree.clone(), &self.path, "name", name.to_string())
+    }
+
+    fn get_workspace_id(&self) -> Option<String> {
+        get_attributes_str_value(self.tree.clone(), &self.path, "workspace_id")
+    }
+
+    fn set_workspace_id(&self, workspace_id: String) -> CollaborateResult<()> {
+        set_attributes_str_value(self.tree.clone(), &self.path, "workspace_id", workspace_id)
+    }
+
+    fn get_view(&self, view_id: &str) -> Option<&Arc<ViewNode>> {
+        todo!()
+    }
+
+    fn get_mut_view(&mut self, view_id: &str) -> Option<&mut Arc<ViewNode>> {
+        todo!()
+    }
+
+    fn add_view(&mut self, revision: ViewRevision) -> CollaborateResult<()> {
+        let mut transaction = Transaction::new();
+        let path = self.path.clone_with(self.views.len());
+        let view_node = ViewNode::from_view_revision(&mut transaction, revision, self.tree.clone(), path)?;
+        let _ = self.tree.write().apply_transaction(transaction)?;
+        self.views.push(Arc::new(view_node));
+        todo!()
+    }
+
+    fn remove_view(&mut self, view_id: &str) {
+        todo!()
+    }
+}

+ 155 - 0
shared-lib/flowy-sync/src/client_folder/folder_node.rs

@@ -0,0 +1,155 @@
+use crate::client_folder::workspace_node::WorkspaceNode;
+use crate::errors::{CollaborateError, CollaborateResult};
+use folder_rev_model::{AppRevision, ViewRevision, WorkspaceRevision};
+use lib_ot::core::{
+    AttributeEntry, AttributeHashMap, AttributeValue, Changeset, Node, NodeDataBuilder, NodeOperation, NodeTree, Path,
+    Transaction,
+};
+use parking_lot::RwLock;
+use std::string::ToString;
+use std::sync::Arc;
+
+pub type AtomicNodeTree = RwLock<NodeTree>;
+
+pub struct FolderNodePad {
+    tree: Arc<AtomicNodeTree>,
+    workspaces: Vec<Arc<WorkspaceNode>>,
+    trash: Vec<Arc<TrashNode>>,
+}
+
+impl FolderNodePad {
+    pub fn new() -> Self {
+        Self::default()
+    }
+
+    pub fn get_workspace(&self, workspace_id: &str) -> Option<&Arc<WorkspaceNode>> {
+        self.workspaces.iter().find(|workspace| workspace.id == workspace_id)
+    }
+
+    pub fn get_mut_workspace(&mut self, workspace_id: &str) -> Option<&mut Arc<WorkspaceNode>> {
+        self.workspaces
+            .iter_mut()
+            .find(|workspace| workspace.id == workspace_id)
+    }
+
+    pub fn remove_workspace(&mut self, workspace_id: &str) {
+        if let Some(workspace) = self.workspaces.iter().find(|workspace| workspace.id == workspace_id) {
+            let mut nodes = vec![];
+            let workspace_node = self.tree.read().get_node_data_at_path(&workspace.path);
+            debug_assert!(workspace_node.is_some());
+
+            if let Some(node_data) = workspace_node {
+                nodes.push(node_data);
+            }
+            let delete_operation = NodeOperation::Delete {
+                path: workspace.path.clone(),
+                nodes,
+            };
+            let _ = self.tree.write().apply_op(delete_operation);
+        }
+    }
+
+    pub fn add_workspace(&mut self, revision: WorkspaceRevision) -> CollaborateResult<()> {
+        let mut transaction = Transaction::new();
+        let workspace_node = WorkspaceNode::from_workspace_revision(
+            &mut transaction,
+            revision,
+            self.tree.clone(),
+            workspaces_path().clone_with(self.workspaces.len()),
+        )?;
+        let _ = self.tree.write().apply_transaction(transaction)?;
+        self.workspaces.push(Arc::new(workspace_node));
+        Ok(())
+    }
+
+    pub fn to_json(&self, pretty: bool) -> CollaborateResult<String> {
+        self.tree
+            .read()
+            .to_json(pretty)
+            .map_err(|e| CollaborateError::serde().context(e))
+    }
+}
+
+fn folder_path() -> Path {
+    vec![0].into()
+}
+
+fn workspaces_path() -> Path {
+    folder_path().clone_with(0)
+}
+
+fn trash_path() -> Path {
+    folder_path().clone_with(1)
+}
+
+pub fn get_attributes(tree: Arc<AtomicNodeTree>, path: &Path) -> Option<AttributeHashMap> {
+    tree.read()
+        .get_node_at_path(&path)
+        .and_then(|node| Some(node.attributes.clone()))
+}
+
+pub fn get_attributes_value(tree: Arc<AtomicNodeTree>, path: &Path, key: &str) -> Option<AttributeValue> {
+    tree.read()
+        .get_node_at_path(&path)
+        .and_then(|node| node.attributes.get(key).cloned())
+}
+
+pub fn get_attributes_str_value(tree: Arc<AtomicNodeTree>, path: &Path, key: &str) -> Option<String> {
+    tree.read()
+        .get_node_at_path(&path)
+        .and_then(|node| node.attributes.get(key).cloned())
+        .and_then(|value| value.str_value())
+}
+
+pub fn set_attributes_str_value(
+    tree: Arc<AtomicNodeTree>,
+    path: &Path,
+    key: &str,
+    value: String,
+) -> CollaborateResult<()> {
+    let old_attributes = match get_attributes(tree.clone(), path) {
+        None => AttributeHashMap::new(),
+        Some(attributes) => attributes,
+    };
+    let mut new_attributes = old_attributes.clone();
+    new_attributes.insert(key, value);
+
+    let update_operation = NodeOperation::Update {
+        path: path.clone(),
+        changeset: Changeset::Attributes {
+            new: new_attributes,
+            old: old_attributes,
+        },
+    };
+    let _ = tree.write().apply_op(update_operation)?;
+    Ok(())
+}
+
+impl std::default::Default for FolderNodePad {
+    fn default() -> Self {
+        let workspace_node = NodeDataBuilder::new("workspaces").build();
+        let trash_node = NodeDataBuilder::new("trash").build();
+        let folder_node = NodeDataBuilder::new("folder")
+            .add_node_data(workspace_node)
+            .add_node_data(trash_node)
+            .build();
+
+        let operation = NodeOperation::Insert {
+            path: folder_path(),
+            nodes: vec![folder_node],
+        };
+        let mut tree = NodeTree::default();
+        let _ = tree.apply_op(operation).unwrap();
+
+        Self {
+            tree: Arc::new(RwLock::new(tree)),
+            workspaces: vec![],
+            trash: vec![],
+        }
+    }
+}
+
+pub struct TrashNode {
+    tree: Arc<AtomicNodeTree>,
+    parent_path: Path,
+}

+ 5 - 0
shared-lib/flowy-sync/src/client_folder/mod.rs

@@ -1,4 +1,9 @@
+mod app_node;
 mod builder;
+mod folder_node;
 mod folder_pad;
+mod view_node;
+mod workspace_node;
 
+pub use folder_node::*;
 pub use folder_pad::*;

+ 44 - 0
shared-lib/flowy-sync/src/client_folder/view_node.rs

@@ -0,0 +1,44 @@
+use crate::client_folder::AtomicNodeTree;
+use crate::errors::CollaborateResult;
+use folder_rev_model::ViewRevision;
+use lib_ot::core::{NodeDataBuilder, NodeOperation, Path, Transaction};
+use std::sync::Arc;
+
+#[derive(Debug, Clone)]
+pub struct ViewNode {
+    tree: Arc<AtomicNodeTree>,
+    path: Path,
+}
+
+impl ViewNode {
+    pub(crate) fn from_view_revision(
+        transaction: &mut Transaction,
+        revision: ViewRevision,
+        tree: Arc<AtomicNodeTree>,
+        path: Path,
+    ) -> CollaborateResult<Self> {
+        let view_node = NodeDataBuilder::new("view")
+            .insert_attribute("id", revision.id)
+            .insert_attribute("name", revision.name)
+            .build();
+
+        transaction.push_operation(NodeOperation::Insert {
+            path: path.clone(),
+            nodes: vec![view_node],
+        });
+
+        Ok(Self { tree, path })
+    }
+
+    fn get_id(&self) -> &str {
+        todo!()
+    }
+
+    fn get_app_id(&self) -> &str {
+        todo!()
+    }
+
+    fn set_app_id(&self, workspace_id: String) {
+        todo!()
+    }
+}

+ 104 - 0
shared-lib/flowy-sync/src/client_folder/workspace_node.rs

@@ -0,0 +1,104 @@
+use crate::client_folder::app_node::AppNode;
+use crate::client_folder::view_node::ViewNode;
+use crate::client_folder::{get_attributes_str_value, get_attributes_value, set_attributes_str_value, AtomicNodeTree};
+use crate::errors::CollaborateResult;
+use folder_rev_model::{AppRevision, WorkspaceRevision};
+use lib_ot::core::{AttributeValue, NodeDataBuilder, NodeOperation, Path, Transaction};
+use std::sync::Arc;
+
+#[derive(Debug, Clone)]
+pub struct WorkspaceNode {
+    pub(crate) id: String,
+    tree: Arc<AtomicNodeTree>,
+    pub(crate) path: Path,
+    apps: Vec<Arc<AppNode>>,
+}
+
+impl WorkspaceNode {
+    pub(crate) fn from_workspace_revision(
+        transaction: &mut Transaction,
+        revision: WorkspaceRevision,
+        tree: Arc<AtomicNodeTree>,
+        path: Path,
+    ) -> CollaborateResult<Self> {
+        let workspace_id = revision.id.clone();
+        let workspace_node = NodeDataBuilder::new("workspace")
+            .insert_attribute("id", revision.id)
+            .insert_attribute("name", revision.name)
+            .build();
+
+        transaction.push_operation(NodeOperation::Insert {
+            path: path.clone(),
+            nodes: vec![workspace_node],
+        });
+
+        let apps = revision
+            .apps
+            .into_iter()
+            .enumerate()
+            .map(|(index, app)| (path.clone_with(index), app))
+            .flat_map(
+                |(path, app)| match AppNode::from_app_revision(transaction, app, tree.clone(), path) {
+                    Ok(app_node) => Some(Arc::new(app_node)),
+                    Err(err) => {
+                        tracing::warn!("Create app node failed: {:?}", err);
+                        None
+                    }
+                },
+            )
+            .collect::<Vec<Arc<AppNode>>>();
+
+        Ok(Self {
+            id: workspace_id,
+            tree,
+            path,
+            apps,
+        })
+    }
+
+    pub fn get_name(&self) -> Option<String> {
+        get_attributes_str_value(self.tree.clone(), &self.path, "name")
+    }
+
+    pub fn set_name(&self, name: &str) -> CollaborateResult<()> {
+        set_attributes_str_value(self.tree.clone(), &self.path, "name", name.to_string())
+    }
+
+    pub fn get_app(&self, app_id: &str) -> Option<&Arc<AppNode>> {
+        self.apps.iter().find(|app| app.id == app_id)
+    }
+
+    pub fn get_mut_app(&mut self, app_id: &str) -> Option<&mut Arc<AppNode>> {
+        self.apps.iter_mut().find(|app| app.id == app_id)
+    }
+
+    pub fn add_app(&mut self, app: AppRevision) -> CollaborateResult<()> {
+        let mut transaction = Transaction::new();
+        let path = self.path.clone_with(self.apps.len());
+        let app_node = AppNode::from_app_revision(&mut transaction, app, self.tree.clone(), path.clone())?;
+        let _ = self.tree.write().apply_transaction(transaction);
+        self.apps.push(Arc::new(app_node));
+        Ok(())
+    }
+
+    pub fn remove_app(&mut self, app_id: &str) {
+        if let Some(index) = self.apps.iter().position(|app| app.id == app_id) {
+            let app = self.apps.remove(index);
+            let mut nodes = vec![];
+            let app_node = self.tree.read().get_node_data_at_path(&app.path);
+            debug_assert!(app_node.is_some());
+            if let Some(node_data) = app_node {
+                nodes.push(node_data);
+            }
+            let delete_operation = NodeOperation::Delete {
+                path: app.path.clone(),
+                nodes,
+            };
+            let _ = self.tree.write().apply_op(delete_operation);
+        }
+    }
+
+    pub fn get_all_apps(&self) -> Vec<Arc<AppNode>> {
+        self.apps.clone()
+    }
+}

+ 4 - 2
shared-lib/flowy-sync/src/errors.rs

@@ -34,6 +34,7 @@ impl CollaborateError {
         self
     }
 
+    static_error!(serde, ErrorCode::SerdeError);
     static_error!(internal, ErrorCode::InternalError);
     static_error!(undo, ErrorCode::UndoFail);
     static_error!(redo, ErrorCode::RedoFail);
@@ -51,14 +52,15 @@ impl fmt::Display for CollaborateError {
 
 #[derive(Debug, Clone, Display, PartialEq, Eq)]
 pub enum ErrorCode {
-    DocIdInvalid = 0,
-    DocNotfound = 1,
+    DocumentIdInvalid = 0,
+    DocumentNotfound = 1,
     UndoFail = 200,
     RedoFail = 201,
     OutOfBound = 202,
     RevisionConflict = 203,
     RecordNotFound = 300,
     CannotDeleteThePrimaryField = 301,
+    SerdeError = 999,
     InternalError = 1000,
 }
 

+ 79 - 0
shared-lib/flowy-sync/tests/client_folder/folder_test.rs

@@ -0,0 +1,79 @@
+use flowy_sync::client_folder::FolderNodePad;
+use folder_rev_model::WorkspaceRevision;
+
+#[test]
+fn client_folder_create_default_folder_test() {
+    let folder_pad = FolderNodePad::default();
+    let json = folder_pad.to_json(false).unwrap();
+    assert_eq!(
+        json,
+        r#"{"type":"folder","children":[{"type":"workspaces"},{"type":"trash"}]}"#
+    );
+}
+
+#[test]
+fn client_folder_create_default_folder_with_workspace_test() {
+    let mut folder_pad = FolderNodePad::default();
+    let workspace = WorkspaceRevision {
+        id: "1".to_string(),
+        name: "workspace name".to_string(),
+        desc: "".to_string(),
+        apps: vec![],
+        modified_time: 0,
+        create_time: 0,
+    };
+    folder_pad.add_workspace(workspace).unwrap();
+    let json = folder_pad.to_json(false).unwrap();
+    assert_eq!(
+        json,
+        r#"{"type":"folder","children":[{"type":"workspaces","children":[{"type":"workspace","attributes":{"id":"1","name":"workspace name"}}]},{"type":"trash"}]}"#
+    );
+
+    assert_eq!(
+        folder_pad.get_workspace("1").unwrap().get_name().unwrap(),
+        "workspace name"
+    );
+}
+
+#[test]
+fn client_folder_delete_workspace_test() {
+    let mut folder_pad = FolderNodePad::default();
+    let workspace = WorkspaceRevision {
+        id: "1".to_string(),
+        name: "workspace name".to_string(),
+        desc: "".to_string(),
+        apps: vec![],
+        modified_time: 0,
+        create_time: 0,
+    };
+    folder_pad.add_workspace(workspace).unwrap();
+    folder_pad.remove_workspace("1");
+    let json = folder_pad.to_json(false).unwrap();
+    assert_eq!(
+        json,
+        r#"{"type":"folder","children":[{"type":"workspaces"},{"type":"trash"}]}"#
+    );
+}
+
+#[test]
+fn client_folder_update_workspace_name_test() {
+    let mut folder_pad = FolderNodePad::default();
+    let workspace = WorkspaceRevision {
+        id: "1".to_string(),
+        name: "workspace name".to_string(),
+        desc: "".to_string(),
+        apps: vec![],
+        modified_time: 0,
+        create_time: 0,
+    };
+    folder_pad.add_workspace(workspace).unwrap();
+    folder_pad
+        .get_workspace("1")
+        .unwrap()
+        .set_name("My first workspace")
+        .unwrap();
+    assert_eq!(
+        folder_pad.get_workspace("1").unwrap().get_name().unwrap(),
+        "My first workspace"
+    );
+}

+ 3 - 0
shared-lib/flowy-sync/tests/client_folder/mod.rs

@@ -0,0 +1,3 @@
+mod folder_test;
+mod script;
+mod workspace_test;

+ 85 - 0
shared-lib/flowy-sync/tests/client_folder/script.rs

@@ -0,0 +1,85 @@
+use flowy_sync::client_folder::FolderNodePad;
+use folder_rev_model::{AppRevision, WorkspaceRevision};
+use std::sync::Arc;
+
+pub enum FolderNodePadScript {
+    CreateApp { id: String, name: String },
+    DeleteApp { id: String },
+    AssertApp { id: String, expected: Option<AppRevision> },
+    AssertAppContent { id: String, name: String },
+    AssertNumberOfApps { expected: usize },
+}
+
+pub struct FolderNodePadTest {
+    folder_pad: FolderNodePad,
+}
+
+impl FolderNodePadTest {
+    pub fn new() -> FolderNodePadTest {
+        let mut folder_pad = FolderNodePad::default();
+        let workspace = WorkspaceRevision {
+            id: "1".to_string(),
+            name: "workspace name".to_string(),
+            desc: "".to_string(),
+            apps: vec![],
+            modified_time: 0,
+            create_time: 0,
+        };
+        let _ = folder_pad.add_workspace(workspace).unwrap();
+        Self { folder_pad }
+    }
+
+    pub fn run_scripts(&mut self, scripts: Vec<FolderNodePadScript>) {
+        for script in scripts {
+            self.run_script(script);
+        }
+    }
+
+    pub fn run_script(&mut self, script: FolderNodePadScript) {
+        match script {
+            FolderNodePadScript::CreateApp { id, name } => {
+                let revision = AppRevision {
+                    id,
+                    workspace_id: "1".to_string(),
+                    name,
+                    desc: "".to_string(),
+                    belongings: vec![],
+                    version: 0,
+                    modified_time: 0,
+                    create_time: 0,
+                };
+
+                let workspace_node = self.folder_pad.get_mut_workspace("1").unwrap();
+                let workspace_node = Arc::make_mut(workspace_node);
+                let _ = workspace_node.add_app(revision).unwrap();
+            }
+            FolderNodePadScript::DeleteApp { id } => {
+                let workspace_node = self.folder_pad.get_mut_workspace("1").unwrap();
+                let workspace_node = Arc::make_mut(workspace_node);
+                workspace_node.remove_app(&id);
+            }
+
+            FolderNodePadScript::AssertApp { id, expected } => {
+                let workspace_node = self.folder_pad.get_workspace("1").unwrap();
+                let app = workspace_node.get_app(&id);
+                match expected {
+                    None => assert!(app.is_none()),
+                    Some(expected_app) => {
+                        let app_node = app.unwrap();
+                        assert_eq!(expected_app.name, app_node.get_name().unwrap());
+                        assert_eq!(expected_app.id, app_node.id);
+                    }
+                }
+            }
+            FolderNodePadScript::AssertAppContent { id, name } => {
+                let workspace_node = self.folder_pad.get_workspace("1").unwrap();
+                let app = workspace_node.get_app(&id).unwrap();
+                assert_eq!(app.get_name().unwrap(), name)
+            }
+            FolderNodePadScript::AssertNumberOfApps { expected } => {
+                let workspace_node = self.folder_pad.get_workspace("1").unwrap();
+                assert_eq!(workspace_node.get_all_apps().len(), expected);
+            }
+        }
+    }
+}

+ 34 - 0
shared-lib/flowy-sync/tests/client_folder/workspace_test.rs

@@ -0,0 +1,34 @@
+use crate::client_folder::script::FolderNodePadScript::*;
+use crate::client_folder::script::FolderNodePadTest;
+use flowy_sync::client_folder::FolderNodePad;
+
+#[test]
+fn client_folder_create_app_test() {
+    let mut test = FolderNodePadTest::new();
+    test.run_scripts(vec![
+        CreateApp {
+            id: "1".to_string(),
+            name: "my first app".to_string(),
+        },
+        AssertAppContent {
+            id: "1".to_string(),
+            name: "my first app".to_string(),
+        },
+    ]);
+}
+
+#[test]
+fn client_folder_delete_app_test() {
+    let mut test = FolderNodePadTest::new();
+    test.run_scripts(vec![
+        CreateApp {
+            id: "1".to_string(),
+            name: "my first app".to_string(),
+        },
+        DeleteApp { id: "1".to_string() },
+        AssertApp {
+            id: "1".to_string(),
+            expected: None,
+        },
+    ]);
+}

+ 1 - 0
shared-lib/flowy-sync/tests/main.rs

@@ -0,0 +1 @@
+mod client_folder;

+ 6 - 0
shared-lib/lib-ot/src/core/node_tree/operation.rs

@@ -258,3 +258,9 @@ impl std::convert::From<Vec<NodeOperation>> for NodeOperations {
         Self::from_operations(operations)
     }
 }
+
+impl std::convert::From<NodeOperation> for NodeOperations {
+    fn from(operation: NodeOperation) -> Self {
+        Self::from_operations(vec![operation])
+    }
+}

+ 12 - 0
shared-lib/lib-ot/src/core/node_tree/path.rs

@@ -34,6 +34,12 @@ impl Path {
         true
     }
 
+    pub fn clone_with(&self, element: usize) -> Self {
+        let mut cloned_self = self.clone();
+        cloned_self.push(element);
+        cloned_self
+    }
+
     pub fn is_root(&self) -> bool {
         self.0.len() == 1 && self.0[0] == 0
     }
@@ -47,6 +53,12 @@ impl std::ops::Deref for Path {
     }
 }
 
+impl std::ops::DerefMut for Path {
+    fn deref_mut(&mut self) -> &mut Self::Target {
+        &mut self.0
+    }
+}
+
 impl std::convert::From<usize> for Path {
     fn from(val: usize) -> Self {
         Path(vec![val])

+ 18 - 4
shared-lib/lib-ot/src/core/node_tree/tree.rs

@@ -4,10 +4,10 @@ use crate::errors::{OTError, OTErrorCode};
 use indextree::{Arena, FollowingSiblings, NodeId};
 use std::sync::Arc;
 
-#[derive(Default, Debug)]
+#[derive(Default, Debug, Clone)]
 pub struct NodeTreeContext {}
 
-#[derive(Debug)]
+#[derive(Debug, Clone)]
 pub struct NodeTree {
     arena: Arena<Node>,
     root: NodeId,
@@ -50,6 +50,20 @@ impl NodeTree {
         }
     }
 
+    pub fn to_json(&self, pretty: bool) -> Result<String, OTError> {
+        if pretty {
+            match serde_json::to_string_pretty(self) {
+                Ok(json) => Ok(json),
+                Err(err) => Err(OTError::serde().context(err)),
+            }
+        } else {
+            match serde_json::to_string(self) {
+                Ok(json) => Ok(json),
+                Err(err) => Err(OTError::serde().context(err)),
+            }
+        }
+    }
+
     pub fn from_operations<T: Into<NodeOperations>>(operations: T, context: NodeTreeContext) -> Result<Self, OTError> {
         let operations = operations.into();
         let mut node_tree = NodeTree::new(context);
@@ -260,8 +274,8 @@ impl NodeTree {
         Ok(())
     }
 
-    pub fn apply_op(&mut self, op: Arc<NodeOperation>) -> Result<(), OTError> {
-        let op = match Arc::try_unwrap(op) {
+    pub fn apply_op<T: Into<Arc<NodeOperation>>>(&mut self, op: T) -> Result<(), OTError> {
+        let op = match Arc::try_unwrap(op.into()) {
             Ok(op) => op,
             Err(op) => op.as_ref().clone(),
         };