瀏覽代碼

add root folder test

appflowy 3 年之前
父節點
當前提交
3eff006d6d

+ 373 - 71
shared-lib/flowy-collaboration/src/folder/folder_data.rs

@@ -1,64 +1,273 @@
-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 crate::{
+    entities::revision::Revision,
+    errors::{CollaborateError, CollaborateResult},
 };
+use dissimilar::*;
+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<Arc<Workspace>>,
-    trash: Vec<Trash>,
+    trash: Vec<Arc<Trash>>,
 }
 
 impl RootFolder {
-    pub fn add_workspace(&mut self, workspace: Workspace) -> Option<Delta<PlainTextAttributes>> {
+    pub fn from_revisions(revisions: Vec<Revision>) -> CollaborateResult<Self> {
+        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)?;
+        }
+
+        Self::from_delta(folder_delta)
+    }
+
+    pub fn from_delta(delta: PlainDelta) -> CollaborateResult<Self> {
+        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<Option<PlainDelta>> {
         let workspace = Arc::new(workspace);
         if self.workspaces.contains(&workspace) {
-            tracing::warn!("Duplicate workspace");
-            return None;
+            tracing::warn!("[RootFolder]: Duplicate workspace");
+            return Ok(None);
         }
 
-        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.modify_workspaces(move |workspaces, _| {
+            workspaces.push(workspace);
+            Ok(Some(()))
+        })
     }
 
-    pub fn update_workspace(&mut self, workspace_id: &str, name: Option<String>, desc: Option<String>) {
-        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 update_workspace(
+        &mut self,
+        workspace_id: &str,
+        name: Option<String>,
+        desc: Option<String>,
+    ) -> CollaborateResult<Option<PlainDelta>> {
+        self.modify_workspace(workspace_id, |workspace, _| {
+            if let Some(name) = name {
+                workspace.name = name;
+            }
+
+            if let Some(desc) = desc {
+                workspace.desc = desc;
+            }
+            Ok(Some(()))
+        })
+    }
+
+    pub fn delete_workspace(&mut self, workspace_id: &str) -> CollaborateResult<Option<PlainDelta>> {
+        self.modify_workspaces(|workspaces, _| {
+            workspaces.retain(|w| w.id != workspace_id);
+            Ok(Some(()))
+        })
+    }
+
+    pub fn add_app(&mut self, app: App) -> CollaborateResult<Option<PlainDelta>> {
+        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<String>,
+        desc: Option<String>,
+    ) -> CollaborateResult<Option<PlainDelta>> {
+        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<Option<PlainDelta>> {
+        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<Option<PlainDelta>> {
+        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<String>,
+        desc: Option<String>,
+        modified_time: i64,
+    ) -> CollaborateResult<Option<PlainDelta>> {
+        self.modify_view(belong_to_id, view_id, |view, _| {
             if let Some(name) = name {
-                m_workspace.name = name;
+                view.name = name;
             }
 
             if let Some(desc) = desc {
-                m_workspace.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<Option<PlainDelta>> {
+        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<Option<PlainDelta>> {
+        self.modify_trash(|trash| {
+            trash.retain(|t| t.id != trash_id);
+            Ok(Some(()))
+        })
+    }
+
+    pub fn delete_trash(&mut self, trash_id: &str) -> CollaborateResult<Option<PlainDelta>> {
+        self.modify_trash(|trash| {
+            trash.retain(|t| t.id != trash_id);
+            Ok(Some(()))
+        })
+    }
+}
+
+impl RootFolder {
+    fn modify_workspaces<F>(&mut self, f: F) -> CollaborateResult<Option<PlainDelta>>
+    where
+        F: FnOnce(&mut Vec<Arc<Workspace>>, &mut Vec<Arc<Trash>>) -> CollaborateResult<Option<()>>,
+    {
+        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)))
+            },
+        }
+    }
+
+    fn modify_workspace<F>(&mut self, workspace_id: &str, f: F) -> CollaborateResult<Option<PlainDelta>>
+    where
+        F: FnOnce(&mut Workspace, &mut Vec<Arc<Trash>>) -> CollaborateResult<Option<()>>,
+    {
+        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<F>(&mut self, f: F) -> CollaborateResult<Option<PlainDelta>>
+    where
+        F: FnOnce(&mut Vec<Arc<Trash>>) -> CollaborateResult<Option<()>>,
+    {
+        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)))
+            },
         }
     }
 
-    pub fn delete_workspace(&mut self, workspace_id: &str) { self.workspaces.retain(|w| w.id != workspace_id) }
+    fn modify_app<F>(&mut self, app_id: &str, f: F) -> CollaborateResult<Option<PlainDelta>>
+    where
+        F: FnOnce(&mut App, &mut Vec<Arc<Trash>>) -> CollaborateResult<Option<()>>,
+    {
+        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<F>(&mut self, belong_to_id: &str, view_id: &str, f: F) -> CollaborateResult<Option<PlainDelta>>
+    where
+        F: FnOnce(&mut View, &mut Vec<Arc<Trash>>) -> CollaborateResult<Option<()>>,
+    {
+        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<String> {
+        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<PlainTextAttributes> {
-    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<PlainTextAttributes> {
     delta_builder.build()
 }
 
-#[derive(Serialize, Deserialize)]
-struct WorkspacesJson {
-    workspaces: Vec<Arc<Workspace>>,
-}
-
-impl WorkspacesJson {
-    fn new(workspaces: Vec<Arc<Workspace>>) -> Self { Self { workspaces } }
-
-    fn to_json(self) -> Result<String, String> {
-        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() {
-        let mut folder = RootFolder {
-            workspaces: vec![],
-            trash: vec![],
-        };
+    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 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 mut workspace_2 = Workspace::default();
+        workspace_2.name = "My second workspace".to_owned();
+        let delta_2 = folder.add_workspace(workspace_2).unwrap().unwrap();
 
-        let workspace_2 = user_default::create_default_workspace(time);
-        let delta_2 = folder.add_workspace(workspace_2).unwrap();
-        println!("{}", delta_2);
+        let folder_from_delta = make_folder_from_delta(initial_delta, vec![delta_1, delta_2]);
+        assert_eq!(folder, folder_from_delta);
     }
 
     #[test]
-    fn serial_folder_test() {
-        let time = Utc::now();
-        let workspace = user_default::create_default_workspace(time);
-        let id = workspace.id.clone();
+    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![Arc::new(workspace)],
+            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 mut workspace = Workspace::default();
+        workspace.id = "1".to_owned();
+
+        delta = delta
+            .compose(&folder.add_workspace(workspace.clone()).unwrap().unwrap())
+            .unwrap();
+
+        (folder, delta, workspace)
+    }
+
+    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<PlainDelta>) -> RootFolder {
+        for delta in deltas {
+            initial_delta = initial_delta.compose(&delta).unwrap();
+        }
+        RootFolder::from_delta(initial_delta).unwrap()
     }
 }

+ 0 - 2
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<>
 }

+ 2 - 2
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)]

+ 2 - 2
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<App> 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,

+ 3 - 3
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<View> 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,

+ 1 - 1
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,

+ 3 - 0
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() }
         }
     };

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

@@ -13,6 +13,8 @@ use std::{
     str::FromStr,
 };
 
+pub type PlainDelta = Delta<PlainTextAttributes>;
+
 // TODO: optimize the memory usage with Arc::make_mut or Cow
 #[derive(Clone, Debug, PartialEq, Eq)]
 pub struct Delta<T: Attributes> {

+ 3 - 4
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") }