diff --git a/shared-lib/flowy-sync/src/client_folder/app_node.rs b/shared-lib/flowy-sync/src/client_folder/app_node.rs new file mode 100644 index 0000000000..bbff863c62 --- /dev/null +++ b/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, + pub(crate) path: Path, + views: Vec>, +} + +impl AppNode { + pub(crate) fn from_app_revision( + transaction: &mut Transaction, + revision: AppRevision, + tree: Arc, + path: Path, + ) -> CollaborateResult { + 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::>>(); + + Ok(Self { + id: app_id, + tree, + path, + views, + }) + } + + pub fn get_name(&self) -> Option { + 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 { + 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> { + todo!() + } + + fn get_mut_view(&mut self, view_id: &str) -> Option<&mut Arc> { + 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!() + } +} diff --git a/shared-lib/flowy-sync/src/client_folder/folder_node.rs b/shared-lib/flowy-sync/src/client_folder/folder_node.rs new file mode 100644 index 0000000000..34099b1c8d --- /dev/null +++ b/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; + +pub struct FolderNodePad { + tree: Arc, + workspaces: Vec>, + trash: Vec>, +} + +impl FolderNodePad { + pub fn new() -> Self { + Self::default() + } + + pub fn get_workspace(&self, workspace_id: &str) -> Option<&Arc> { + self.workspaces.iter().find(|workspace| workspace.id == workspace_id) + } + + pub fn get_mut_workspace(&mut self, workspace_id: &str) -> Option<&mut Arc> { + 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 { + 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, path: &Path) -> Option { + tree.read() + .get_node_at_path(&path) + .and_then(|node| Some(node.attributes.clone())) +} + +pub fn get_attributes_value(tree: Arc, path: &Path, key: &str) -> Option { + tree.read() + .get_node_at_path(&path) + .and_then(|node| node.attributes.get(key).cloned()) +} + +pub fn get_attributes_str_value(tree: Arc, path: &Path, key: &str) -> Option { + 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, + 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, + parent_path: Path, +} diff --git a/shared-lib/flowy-sync/src/client_folder/mod.rs b/shared-lib/flowy-sync/src/client_folder/mod.rs index 5f08d39be2..42269c2b61 100644 --- a/shared-lib/flowy-sync/src/client_folder/mod.rs +++ b/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::*; diff --git a/shared-lib/flowy-sync/src/client_folder/view_node.rs b/shared-lib/flowy-sync/src/client_folder/view_node.rs new file mode 100644 index 0000000000..a6af4a9d6d --- /dev/null +++ b/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, + path: Path, +} + +impl ViewNode { + pub(crate) fn from_view_revision( + transaction: &mut Transaction, + revision: ViewRevision, + tree: Arc, + path: Path, + ) -> CollaborateResult { + 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!() + } +} diff --git a/shared-lib/flowy-sync/src/client_folder/workspace_node.rs b/shared-lib/flowy-sync/src/client_folder/workspace_node.rs new file mode 100644 index 0000000000..3f601f2ac1 --- /dev/null +++ b/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, + pub(crate) path: Path, + apps: Vec>, +} + +impl WorkspaceNode { + pub(crate) fn from_workspace_revision( + transaction: &mut Transaction, + revision: WorkspaceRevision, + tree: Arc, + path: Path, + ) -> CollaborateResult { + 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::>>(); + + Ok(Self { + id: workspace_id, + tree, + path, + apps, + }) + } + + pub fn get_name(&self) -> Option { + 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> { + self.apps.iter().find(|app| app.id == app_id) + } + + pub fn get_mut_app(&mut self, app_id: &str) -> Option<&mut Arc> { + 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> { + self.apps.clone() + } +} diff --git a/shared-lib/flowy-sync/src/errors.rs b/shared-lib/flowy-sync/src/errors.rs index 62f9519e63..241f07d867 100644 --- a/shared-lib/flowy-sync/src/errors.rs +++ b/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, } diff --git a/shared-lib/flowy-sync/tests/client_folder/folder_test.rs b/shared-lib/flowy-sync/tests/client_folder/folder_test.rs new file mode 100644 index 0000000000..e8f5410071 --- /dev/null +++ b/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" + ); +} diff --git a/shared-lib/flowy-sync/tests/client_folder/mod.rs b/shared-lib/flowy-sync/tests/client_folder/mod.rs new file mode 100644 index 0000000000..727fee521b --- /dev/null +++ b/shared-lib/flowy-sync/tests/client_folder/mod.rs @@ -0,0 +1,3 @@ +mod folder_test; +mod script; +mod workspace_test; diff --git a/shared-lib/flowy-sync/tests/client_folder/script.rs b/shared-lib/flowy-sync/tests/client_folder/script.rs new file mode 100644 index 0000000000..d330831c08 --- /dev/null +++ b/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 }, + 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) { + 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); + } + } + } +} diff --git a/shared-lib/flowy-sync/tests/client_folder/workspace_test.rs b/shared-lib/flowy-sync/tests/client_folder/workspace_test.rs new file mode 100644 index 0000000000..281a251a71 --- /dev/null +++ b/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, + }, + ]); +} diff --git a/shared-lib/flowy-sync/tests/main.rs b/shared-lib/flowy-sync/tests/main.rs new file mode 100644 index 0000000000..6eb34d1302 --- /dev/null +++ b/shared-lib/flowy-sync/tests/main.rs @@ -0,0 +1 @@ +mod client_folder; 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 7314584b89..145e7f1428 100644 --- a/shared-lib/lib-ot/src/core/node_tree/operation.rs +++ b/shared-lib/lib-ot/src/core/node_tree/operation.rs @@ -258,3 +258,9 @@ impl std::convert::From> for NodeOperations { Self::from_operations(operations) } } + +impl std::convert::From for NodeOperations { + fn from(operation: NodeOperation) -> Self { + Self::from_operations(vec![operation]) + } +} 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 b754baa70f..ebe52c96fb 100644 --- a/shared-lib/lib-ot/src/core/node_tree/path.rs +++ b/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 for Path { fn from(val: usize) -> Self { Path(vec![val]) 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 16399211ea..0cd90e4b55 100644 --- a/shared-lib/lib-ot/src/core/node_tree/tree.rs +++ b/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, root: NodeId, @@ -50,6 +50,20 @@ impl NodeTree { } } + pub fn to_json(&self, pretty: bool) -> Result { + 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>(operations: T, context: NodeTreeContext) -> Result { 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) -> Result<(), OTError> { - let op = match Arc::try_unwrap(op) { + pub fn apply_op>>(&mut self, op: T) -> Result<(), OTError> { + let op = match Arc::try_unwrap(op.into()) { Ok(op) => op, Err(op) => op.as_ref().clone(), };