feat: create folder node & add tests

This commit is contained in:
nathan 2022-12-02 22:31:03 +08:00
parent fa01dbb572
commit c89277d507
14 changed files with 645 additions and 6 deletions

View File

@ -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!()
}
}

View File

@ -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,
}

View File

@ -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::*;

View File

@ -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!()
}
}

View File

@ -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()
}
}

View File

@ -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,
}

View File

@ -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"
);
}

View File

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

View File

@ -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);
}
}
}
}

View File

@ -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,
},
]);
}

View File

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

View File

@ -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])
}
}

View File

@ -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])

View File

@ -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(),
};