From 3eff006d6dd6bb95ac6d0949db70d3db5c00d572 Mon Sep 17 00:00:00 2001 From: appflowy Date: Sun, 16 Jan 2022 13:44:14 +0800 Subject: [PATCH] add root folder test --- .../src/folder/folder_data.rs | 448 +++++++++++++++--- .../src/folder/folder_manager.rs | 2 - .../flowy-core-data-model/src/entities/app.rs | 4 +- .../src/entities/trash.rs | 4 +- .../src/entities/view.rs | 6 +- .../src/entities/workspace.rs | 2 +- .../flowy-core-data-model/src/macros.rs | 3 + shared-lib/lib-ot/src/core/delta/delta.rs | 2 + .../lib-ot/src/core/operation/operation.rs | 7 +- 9 files changed, 391 insertions(+), 87 deletions(-) diff --git a/shared-lib/flowy-collaboration/src/folder/folder_data.rs b/shared-lib/flowy-collaboration/src/folder/folder_data.rs index 189b2869da..731a615723 100644 --- a/shared-lib/flowy-collaboration/src/folder/folder_data.rs +++ b/shared-lib/flowy-collaboration/src/folder/folder_data.rs @@ -1,64 +1,273 @@ +use crate::{ + entities::revision::Revision, + errors::{CollaborateError, CollaborateResult}, +}; use dissimilar::*; -use flowy_core_data_model::entities::{ - app::{App, RepeatedApp}, - trash::{RepeatedTrash, Trash}, - view::{RepeatedView, View}, - workspace::{RepeatedWorkspace, Workspace}, -}; -use lib_ot::core::{ - Delta, - FlowyStr, - Operation, - Operation::Retain, - PlainDeltaBuilder, - PlainTextAttributes, - PlainTextOpBuilder, -}; +use flowy_core_data_model::entities::{app::App, trash::Trash, view::View, workspace::Workspace}; +use lib_ot::core::{Delta, FlowyStr, OperationTransformable, PlainDelta, PlainDeltaBuilder, PlainTextAttributes}; use serde::{Deserialize, Serialize}; use std::sync::Arc; -#[derive(Debug, Deserialize, Serialize, Clone)] +#[derive(Debug, Deserialize, Serialize, Clone, Eq, PartialEq)] pub struct RootFolder { workspaces: Vec>, - trash: Vec, + trash: Vec>, } impl RootFolder { - pub fn add_workspace(&mut self, workspace: Workspace) -> Option> { - let workspace = Arc::new(workspace); - if self.workspaces.contains(&workspace) { - tracing::warn!("Duplicate workspace"); - return None; + pub fn from_revisions(revisions: Vec) -> CollaborateResult { + let mut folder_delta = PlainDelta::new(); + for revision in revisions { + if revision.delta_data.is_empty() { + tracing::warn!("revision delta_data is empty"); + } + + let delta = PlainDelta::from_bytes(revision.delta_data)?; + folder_delta = folder_delta.compose(&delta)?; } - let old = WorkspacesJson::new(self.workspaces.clone()).to_json().unwrap(); - self.workspaces.push(workspace); - let new = WorkspacesJson::new(self.workspaces.clone()).to_json().unwrap(); - Some(cal_diff(old, new)) + Self::from_delta(folder_delta) } - pub fn update_workspace(&mut self, workspace_id: &str, name: Option, desc: Option) { - if let Some(mut workspace) = self - .workspaces - .iter_mut() - .find(|workspace| workspace.id == workspace_id) - { - let m_workspace = Arc::make_mut(&mut workspace); + pub fn from_delta(delta: PlainDelta) -> CollaborateResult { + let folder_json = delta.apply("").unwrap(); + let folder: RootFolder = serde_json::from_str(&folder_json) + .map_err(|e| CollaborateError::internal().context(format!("Deserial json to root folder failed: {}", e)))?; + Ok(folder) + } + + pub fn add_workspace(&mut self, workspace: Workspace) -> CollaborateResult> { + let workspace = Arc::new(workspace); + if self.workspaces.contains(&workspace) { + tracing::warn!("[RootFolder]: Duplicate workspace"); + return Ok(None); + } + + self.modify_workspaces(move |workspaces, _| { + workspaces.push(workspace); + Ok(Some(())) + }) + } + + pub fn update_workspace( + &mut self, + workspace_id: &str, + name: Option, + desc: Option, + ) -> CollaborateResult> { + self.modify_workspace(workspace_id, |workspace, _| { if let Some(name) = name { - m_workspace.name = name; + workspace.name = name; } if let Some(desc) = desc { - m_workspace.desc = desc; + workspace.desc = desc; } + Ok(Some(())) + }) + } + + pub fn delete_workspace(&mut self, workspace_id: &str) -> CollaborateResult> { + self.modify_workspaces(|workspaces, _| { + workspaces.retain(|w| w.id != workspace_id); + Ok(Some(())) + }) + } + + pub fn add_app(&mut self, app: App) -> CollaborateResult> { + let workspace_id = app.workspace_id.clone(); + self.modify_workspace(&workspace_id, move |workspace, _| { + if workspace.apps.contains(&app) { + tracing::warn!("[RootFolder]: Duplicate app"); + return Ok(None); + } + workspace.apps.push(app); + Ok(Some(())) + }) + } + + pub fn update_app( + &mut self, + app_id: &str, + name: Option, + desc: Option, + ) -> CollaborateResult> { + self.modify_app(app_id, move |app, _| { + if let Some(name) = name { + app.name = name; + } + + if let Some(desc) = desc { + app.desc = desc; + } + Ok(Some(())) + }) + } + + pub fn delete_app(&mut self, workspace_id: &str, app_id: &str) -> CollaborateResult> { + self.modify_workspace(workspace_id, |workspace, trash| { + for app in workspace.apps.take_items() { + if app.id == app_id { + trash.push(Arc::new(Trash::from(app))) + } else { + workspace.apps.push(app); + } + } + Ok(Some(())) + }) + } + + pub fn add_view(&mut self, view: View) -> CollaborateResult> { + let app_id = view.belong_to_id.clone(); + self.modify_app(&app_id, move |app, _| { + if app.belongings.contains(&view) { + tracing::warn!("[RootFolder]: Duplicate view"); + return Ok(None); + } + app.belongings.push(view); + Ok(Some(())) + }) + } + + pub fn update_view( + &mut self, + belong_to_id: &str, + view_id: &str, + name: Option, + desc: Option, + modified_time: i64, + ) -> CollaborateResult> { + self.modify_view(belong_to_id, view_id, |view, _| { + if let Some(name) = name { + view.name = name; + } + + if let Some(desc) = desc { + view.desc = desc; + } + + view.modified_time = modified_time; + Ok(Some(())) + }) + } + + pub fn delete_view(&mut self, belong_to_id: &str, view_id: &str) -> CollaborateResult> { + self.modify_app(belong_to_id, |app, trash| { + for view in app.belongings.take_items() { + if view.id == view_id { + trash.push(Arc::new(Trash::from(view))) + } else { + app.belongings.push(view); + } + } + Ok(Some(())) + }) + } + + pub fn putback_trash(&mut self, trash_id: &str) -> CollaborateResult> { + self.modify_trash(|trash| { + trash.retain(|t| t.id != trash_id); + Ok(Some(())) + }) + } + + pub fn delete_trash(&mut self, trash_id: &str) -> CollaborateResult> { + self.modify_trash(|trash| { + trash.retain(|t| t.id != trash_id); + Ok(Some(())) + }) + } +} + +impl RootFolder { + fn modify_workspaces(&mut self, f: F) -> CollaborateResult> + where + F: FnOnce(&mut Vec>, &mut Vec>) -> CollaborateResult>, + { + let cloned_self = self.clone(); + match f(&mut self.workspaces, &mut self.trash)? { + None => Ok(None), + Some(_) => { + let old = cloned_self.to_json()?; + let new = self.to_json()?; + Ok(Some(cal_diff(old, new))) + }, } } - pub fn delete_workspace(&mut self, workspace_id: &str) { self.workspaces.retain(|w| w.id != workspace_id) } + fn modify_workspace(&mut self, workspace_id: &str, f: F) -> CollaborateResult> + where + F: FnOnce(&mut Workspace, &mut Vec>) -> CollaborateResult>, + { + self.modify_workspaces(|workspaces, trash| { + if let Some(workspace) = workspaces.iter_mut().find(|workspace| workspace_id == workspace.id) { + f(Arc::make_mut(workspace), trash) + } else { + tracing::warn!("[RootFolder]: Can't find any workspace with id: {}", workspace_id); + Ok(None) + } + }) + } + + fn modify_trash(&mut self, f: F) -> CollaborateResult> + where + F: FnOnce(&mut Vec>) -> CollaborateResult>, + { + let cloned_self = self.clone(); + match f(&mut self.trash)? { + None => Ok(None), + Some(_) => { + let old = cloned_self.to_json()?; + let new = self.to_json()?; + Ok(Some(cal_diff(old, new))) + }, + } + } + + fn modify_app(&mut self, app_id: &str, f: F) -> CollaborateResult> + where + F: FnOnce(&mut App, &mut Vec>) -> CollaborateResult>, + { + let workspace_id = match self + .workspaces + .iter() + .find(|workspace| workspace.apps.iter().any(|app| app.id == app_id)) + { + None => { + tracing::warn!("[RootFolder]: Can't find any app with id: {}", app_id); + return Ok(None); + }, + Some(workspace) => workspace.id.clone(), + }; + + self.modify_workspace(&workspace_id, |workspace, trash| { + f(workspace.apps.iter_mut().find(|app| app_id == app.id).unwrap(), trash) + }) + } + + fn modify_view(&mut self, belong_to_id: &str, view_id: &str, f: F) -> CollaborateResult> + where + F: FnOnce(&mut View, &mut Vec>) -> CollaborateResult>, + { + self.modify_app(belong_to_id, |app, trash| { + match app.belongings.iter_mut().find(|view| view_id == view.id) { + None => { + tracing::warn!("[RootFolder]: Can't find any view with id: {}", view_id); + Ok(None) + }, + Some(view) => f(view, trash), + } + }) + } + + fn to_json(&self) -> CollaborateResult { + serde_json::to_string(self) + .map_err(|e| CollaborateError::internal().context(format!("serial trash to json failed: {}", e))) + } } fn cal_diff(old: String, new: String) -> Delta { - let mut chunks = dissimilar::diff(&old, &new); + let chunks = dissimilar::diff(&old, &new); let mut delta_builder = PlainDeltaBuilder::new(); for chunk in &chunks { match chunk { @@ -76,57 +285,150 @@ fn cal_diff(old: String, new: String) -> Delta { delta_builder.build() } -#[derive(Serialize, Deserialize)] -struct WorkspacesJson { - workspaces: Vec>, -} - -impl WorkspacesJson { - fn new(workspaces: Vec>) -> Self { Self { workspaces } } - - fn to_json(self) -> Result { - serde_json::to_string(&self).map_err(|e| format!("format workspaces failed: {}", e)) - } -} - #[cfg(test)] mod tests { + #![allow(clippy::all)] use crate::folder::folder_data::RootFolder; use chrono::Utc; - use flowy_core_data_model::{entities::prelude::Workspace, user_default}; - use std::{borrow::Cow, sync::Arc}; + use flowy_core_data_model::entities::{app::App, view::View, workspace::Workspace}; + use lib_ot::core::{OperationTransformable, PlainDelta, PlainDeltaBuilder}; #[test] - fn folder_add_workspace_serde_test() { + fn folder_add_workspace() { + let (mut folder, initial_delta, _) = test_folder(); + + let _time = Utc::now(); + let mut workspace_1 = Workspace::default(); + workspace_1.name = "My first workspace".to_owned(); + let delta_1 = folder.add_workspace(workspace_1).unwrap().unwrap(); + + let mut workspace_2 = Workspace::default(); + workspace_2.name = "My second workspace".to_owned(); + let delta_2 = folder.add_workspace(workspace_2).unwrap().unwrap(); + + let folder_from_delta = make_folder_from_delta(initial_delta, vec![delta_1, delta_2]); + assert_eq!(folder, folder_from_delta); + } + + #[test] + fn folder_update_workspace() { + let (mut folder, initial_delta, workspace) = test_folder(); + let delta = folder + .update_workspace(&workspace.id, Some("✅️".to_string()), None) + .unwrap() + .unwrap(); + + let folder_from_delta = make_folder_from_delta(initial_delta, vec![delta]); + assert_eq!(folder, folder_from_delta); + } + + #[test] + fn folder_add_app() { + let (folder, initial_delta, _app) = test_app_folder(); + let folder_from_delta = make_folder_from_delta(initial_delta, vec![]); + assert_eq!(folder, folder_from_delta); + } + + #[test] + fn folder_update_app() { + let (mut folder, initial_delta, app) = test_app_folder(); + let delta = folder + .update_app(&app.id, Some("😁😁😁".to_owned()), None) + .unwrap() + .unwrap(); + + let folder_from_delta = make_folder_from_delta(initial_delta, vec![delta]); + assert_eq!(folder, folder_from_delta); + } + + #[test] + fn folder_delete_app() { + let (mut folder, initial_delta, app) = test_app_folder(); + let delta = folder.delete_app(&app.workspace_id, &app.id).unwrap().unwrap(); + assert_eq!(folder.trash.len(), 1); + + let folder_from_delta = make_folder_from_delta(initial_delta, vec![delta]); + assert_eq!(folder, folder_from_delta); + } + + #[test] + fn folder_add_view() { + let (folder, initial_delta, _view) = test_view_folder(); + let folder_from_delta = make_folder_from_delta(initial_delta, vec![]); + assert_eq!(folder, folder_from_delta); + } + + #[test] + fn folder_update_view() { + let (mut folder, initial_delta, view) = test_view_folder(); + let delta = folder + .update_view(&view.belong_to_id, &view.id, Some("😁😁😁".to_owned()), None, 123) + .unwrap() + .unwrap(); + + let folder_from_delta = make_folder_from_delta(initial_delta, vec![delta]); + assert_eq!(folder, folder_from_delta); + } + + #[test] + fn folder_delete_view() { + let (mut folder, initial_delta, view) = test_view_folder(); + let delta = folder.delete_view(&view.belong_to_id, &view.id).unwrap().unwrap(); + + assert_eq!(folder.trash.len(), 1); + let folder_from_delta = make_folder_from_delta(initial_delta, vec![delta]); + assert_eq!(folder, folder_from_delta); + } + + fn test_folder() -> (RootFolder, PlainDelta, Workspace) { let mut folder = RootFolder { workspaces: vec![], trash: vec![], }; + let folder_json = serde_json::to_string(&folder).unwrap(); + let mut delta = PlainDeltaBuilder::new().insert(&folder_json).build(); - let time = Utc::now(); - let workspace_1 = user_default::create_default_workspace(time); - let delta_1 = folder.add_workspace(workspace_1).unwrap(); - println!("{}", delta_1); + let _time = Utc::now(); + let mut workspace = Workspace::default(); + workspace.id = "1".to_owned(); - let workspace_2 = user_default::create_default_workspace(time); - let delta_2 = folder.add_workspace(workspace_2).unwrap(); - println!("{}", delta_2); + delta = delta + .compose(&folder.add_workspace(workspace.clone()).unwrap().unwrap()) + .unwrap(); + + (folder, delta, workspace) } - #[test] - fn serial_folder_test() { - let time = Utc::now(); - let workspace = user_default::create_default_workspace(time); - let id = workspace.id.clone(); - let mut folder = RootFolder { - workspaces: vec![Arc::new(workspace)], - trash: vec![], - }; + fn test_app_folder() -> (RootFolder, PlainDelta, App) { + let (mut folder, mut initial_delta, workspace) = test_folder(); + let mut app = App::default(); + app.workspace_id = workspace.id; + app.name = "My first app".to_owned(); - let mut cloned = folder.clone(); - cloned.update_workspace(&id, Some("123".to_owned()), None); + initial_delta = initial_delta + .compose(&folder.add_app(app.clone()).unwrap().unwrap()) + .unwrap(); - println!("{}", serde_json::to_string(&folder).unwrap()); - println!("{}", serde_json::to_string(&cloned).unwrap()); + (folder, initial_delta, app) + } + + fn test_view_folder() -> (RootFolder, PlainDelta, View) { + let (mut folder, mut initial_delta, app) = test_app_folder(); + let mut view = View::default(); + view.belong_to_id = app.id.clone(); + view.name = "My first view".to_owned(); + + initial_delta = initial_delta + .compose(&folder.add_view(view.clone()).unwrap().unwrap()) + .unwrap(); + + (folder, initial_delta, view) + } + + fn make_folder_from_delta(mut initial_delta: PlainDelta, deltas: Vec) -> RootFolder { + for delta in deltas { + initial_delta = initial_delta.compose(&delta).unwrap(); + } + RootFolder::from_delta(initial_delta).unwrap() } } diff --git a/shared-lib/flowy-collaboration/src/folder/folder_manager.rs b/shared-lib/flowy-collaboration/src/folder/folder_manager.rs index 64c6bb36a1..17c33e27da 100644 --- a/shared-lib/flowy-collaboration/src/folder/folder_manager.rs +++ b/shared-lib/flowy-collaboration/src/folder/folder_manager.rs @@ -1,5 +1,3 @@ -use lib_infra::future::BoxResultFuture; - pub trait FolderCloudPersistence: Send + Sync { // fn read_folder(&self) -> BoxResultFuture<> } diff --git a/shared-lib/flowy-core-data-model/src/entities/app.rs b/shared-lib/flowy-core-data-model/src/entities/app.rs index 956cbefc78..841fb61448 100644 --- a/shared-lib/flowy-core-data-model/src/entities/app.rs +++ b/shared-lib/flowy-core-data-model/src/entities/app.rs @@ -11,7 +11,7 @@ use flowy_derive::ProtoBuf; use serde::{Deserialize, Serialize}; use std::convert::TryInto; -#[derive(PartialEq, ProtoBuf, Default, Debug, Clone, Serialize, Deserialize)] +#[derive(Eq, PartialEq, ProtoBuf, Default, Debug, Clone, Serialize, Deserialize)] pub struct App { #[pb(index = 1)] pub id: String, @@ -42,7 +42,7 @@ impl App { pub fn take_belongings(&mut self) -> RepeatedView { std::mem::take(&mut self.belongings) } } -#[derive(PartialEq, Debug, Default, ProtoBuf, Clone, Serialize, Deserialize)] +#[derive(Eq, PartialEq, Debug, Default, ProtoBuf, Clone, Serialize, Deserialize)] #[serde(transparent)] pub struct RepeatedApp { #[pb(index = 1)] diff --git a/shared-lib/flowy-core-data-model/src/entities/trash.rs b/shared-lib/flowy-core-data-model/src/entities/trash.rs index 5fe5d11c9e..487a51c020 100644 --- a/shared-lib/flowy-core-data-model/src/entities/trash.rs +++ b/shared-lib/flowy-core-data-model/src/entities/trash.rs @@ -3,7 +3,7 @@ use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; use serde::{Deserialize, Serialize}; use std::fmt::Formatter; -#[derive(PartialEq, ProtoBuf, Default, Debug, Clone, Serialize, Deserialize)] +#[derive(Eq, PartialEq, ProtoBuf, Default, Debug, Clone, Serialize, Deserialize)] pub struct Trash { #[pb(index = 1)] pub id: String, @@ -41,7 +41,7 @@ impl std::convert::From for Trash { } } -#[derive(PartialEq, Debug, ProtoBuf_Enum, Clone, Serialize, Deserialize)] +#[derive(Eq, PartialEq, Debug, ProtoBuf_Enum, Clone, Serialize, Deserialize)] pub enum TrashType { Unknown = 0, View = 1, diff --git a/shared-lib/flowy-core-data-model/src/entities/view.rs b/shared-lib/flowy-core-data-model/src/entities/view.rs index 3637d298a6..47826dfd78 100644 --- a/shared-lib/flowy-core-data-model/src/entities/view.rs +++ b/shared-lib/flowy-core-data-model/src/entities/view.rs @@ -11,7 +11,7 @@ use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; use serde::{Deserialize, Serialize}; use std::convert::TryInto; -#[derive(PartialEq, ProtoBuf, Default, Debug, Clone, Serialize, Deserialize)] +#[derive(Eq, PartialEq, ProtoBuf, Default, Debug, Clone, Serialize, Deserialize)] pub struct View { #[pb(index = 1)] pub id: String, @@ -41,7 +41,7 @@ pub struct View { pub create_time: i64, } -#[derive(PartialEq, Debug, Default, ProtoBuf, Clone, Serialize, Deserialize)] +#[derive(Eq, PartialEq, Debug, Default, ProtoBuf, Clone, Serialize, Deserialize)] #[serde(transparent)] pub struct RepeatedView { #[pb(index = 1)] @@ -62,7 +62,7 @@ impl std::convert::From for Trash { } } -#[derive(PartialEq, Debug, ProtoBuf_Enum, Clone, Serialize, Deserialize)] +#[derive(Eq, PartialEq, Debug, ProtoBuf_Enum, Clone, Serialize, Deserialize)] pub enum ViewType { Blank = 0, Doc = 1, diff --git a/shared-lib/flowy-core-data-model/src/entities/workspace.rs b/shared-lib/flowy-core-data-model/src/entities/workspace.rs index 5203f38345..ab5ed57645 100644 --- a/shared-lib/flowy-core-data-model/src/entities/workspace.rs +++ b/shared-lib/flowy-core-data-model/src/entities/workspace.rs @@ -8,7 +8,7 @@ use flowy_derive::ProtoBuf; use serde::{Deserialize, Serialize}; use std::convert::TryInto; -#[derive(PartialEq, ProtoBuf, Default, Debug, Clone, Serialize, Deserialize)] +#[derive(Eq, PartialEq, ProtoBuf, Default, Debug, Clone, Serialize, Deserialize)] pub struct Workspace { #[pb(index = 1)] pub id: String, diff --git a/shared-lib/flowy-core-data-model/src/macros.rs b/shared-lib/flowy-core-data-model/src/macros.rs index 05cfe0ac76..46f6e206cc 100644 --- a/shared-lib/flowy-core-data-model/src/macros.rs +++ b/shared-lib/flowy-core-data-model/src/macros.rs @@ -24,6 +24,9 @@ macro_rules! impl_def_and_def_mut { self.items.push(item); } + #[allow(dead_code)] + pub fn take_items(&mut self) -> Vec<$item> { std::mem::take(&mut self.items) } + pub fn first_or_crash(&self) -> &$item { self.items.first().unwrap() } } }; diff --git a/shared-lib/lib-ot/src/core/delta/delta.rs b/shared-lib/lib-ot/src/core/delta/delta.rs index ead1f23ffd..98e5d02ba5 100644 --- a/shared-lib/lib-ot/src/core/delta/delta.rs +++ b/shared-lib/lib-ot/src/core/delta/delta.rs @@ -13,6 +13,8 @@ use std::{ str::FromStr, }; +pub type PlainDelta = Delta; + // TODO: optimize the memory usage with Arc::make_mut or Cow #[derive(Clone, Debug, PartialEq, Eq)] pub struct Delta { diff --git a/shared-lib/lib-ot/src/core/operation/operation.rs b/shared-lib/lib-ot/src/core/operation/operation.rs index 8cc81784d7..0ecdd80eda 100644 --- a/shared-lib/lib-ot/src/core/operation/operation.rs +++ b/shared-lib/lib-ot/src/core/operation/operation.rs @@ -1,13 +1,12 @@ use crate::{ core::{FlowyStr, Interval, OpBuilder, OperationTransformable}, errors::OTError, - rich_text::{RichTextAttribute, RichTextAttributes}, }; -use serde::__private::Formatter; +use serde::{Deserialize, Serialize, __private::Formatter}; use std::{ cmp::min, fmt, - fmt::{Debug, Display}, + fmt::Debug, ops::{Deref, DerefMut}, }; @@ -323,7 +322,7 @@ where } } -#[derive(Debug, Clone, Eq, PartialEq, Default)] +#[derive(Debug, Clone, Eq, PartialEq, Default, Serialize, Deserialize)] pub struct PlainTextAttributes(); impl fmt::Display for PlainTextAttributes { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str("PlainTextAttributes") }