mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
chore: add update node body test
This commit is contained in:
parent
2a5d9d5530
commit
1d7d4092a5
@ -39,8 +39,8 @@ impl NodeAttributes {
|
||||
self.0.is_empty()
|
||||
}
|
||||
|
||||
pub fn delete(&mut self, key: &AttributeKey) {
|
||||
self.insert(key.clone(), AttributeValue(None));
|
||||
pub fn delete<K: ToString>(&mut self, key: K) {
|
||||
self.insert(key.to_string(), AttributeValue(None));
|
||||
}
|
||||
}
|
||||
|
||||
@ -139,8 +139,34 @@ impl std::convert::From<bool> for AttributeValue {
|
||||
fn from(val: bool) -> Self {
|
||||
let val = match val {
|
||||
true => Some("true".to_owned()),
|
||||
false => None,
|
||||
false => Some("false".to_owned()),
|
||||
};
|
||||
AttributeValue(val)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct NodeAttributeBuilder {
|
||||
attributes: NodeAttributes,
|
||||
}
|
||||
|
||||
impl NodeAttributeBuilder {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
attributes: NodeAttributes::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn insert<K: ToString, V: Into<AttributeValue>>(mut self, key: K, value: V) -> Self {
|
||||
self.attributes.insert(key, value);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn delete<K: ToString>(mut self, key: K) -> Self {
|
||||
self.attributes.delete(key);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn build(self) -> NodeAttributes {
|
||||
self.attributes
|
||||
}
|
||||
}
|
||||
|
@ -26,6 +26,13 @@ impl NodeTree {
|
||||
Some(self.arena.get(node_id)?.get())
|
||||
}
|
||||
|
||||
pub fn get_node_at_path(&self, path: &Path) -> Option<&Node> {
|
||||
{
|
||||
let node_id = self.node_id_at_path(path)?;
|
||||
self.get_node(node_id)
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
@ -41,7 +48,7 @@ impl NodeTree {
|
||||
/// let node_path = node_tree.path_of_node(node_id);
|
||||
/// debug_assert_eq!(node_path, root_path);
|
||||
/// ```
|
||||
pub fn node_at_path<T: Into<Path>>(&self, path: T) -> Option<NodeId> {
|
||||
pub fn node_id_at_path<T: Into<Path>>(&self, path: T) -> Option<NodeId> {
|
||||
let path = path.into();
|
||||
if path.is_empty() {
|
||||
return Some(self.root);
|
||||
@ -54,7 +61,7 @@ impl NodeTree {
|
||||
Some(iterate_node)
|
||||
}
|
||||
|
||||
pub fn path_of_node(&self, node_id: NodeId) -> Path {
|
||||
pub fn path_from_node_id(&self, node_id: NodeId) -> Path {
|
||||
let mut path = vec![];
|
||||
let mut current_node = node_id;
|
||||
// Use .skip(1) on the ancestors iterator to skip the root node.
|
||||
@ -137,7 +144,7 @@ impl NodeTree {
|
||||
}
|
||||
|
||||
pub fn apply(&mut self, transaction: Transaction) -> Result<(), OTError> {
|
||||
for op in &transaction.operations {
|
||||
for op in transaction.operations.iter() {
|
||||
self.apply_op(op)?;
|
||||
}
|
||||
Ok(())
|
||||
@ -164,7 +171,7 @@ impl NodeTree {
|
||||
let (parent_path, last_path) = path.split_at(path.0.len() - 1);
|
||||
let last_index = *last_path.first().unwrap();
|
||||
let parent_node = self
|
||||
.node_at_path(parent_path)
|
||||
.node_id_at_path(parent_path)
|
||||
.ok_or_else(|| ErrorBuilder::new(OTErrorCode::PathNotFound).build())?;
|
||||
|
||||
self.insert_nodes_at_index(parent_node, last_index, nodes)
|
||||
@ -228,7 +235,7 @@ impl NodeTree {
|
||||
|
||||
fn delete_node(&mut self, path: &Path, nodes: &[NodeData]) -> Result<(), OTError> {
|
||||
let mut update_node = self
|
||||
.node_at_path(path)
|
||||
.node_id_at_path(path)
|
||||
.ok_or_else(|| ErrorBuilder::new(OTErrorCode::PathNotFound).build())?;
|
||||
|
||||
for _ in 0..nodes.len() {
|
||||
@ -255,7 +262,7 @@ impl NodeTree {
|
||||
F: Fn(&mut Node) -> Result<(), OTError>,
|
||||
{
|
||||
let node_id = self
|
||||
.node_at_path(path)
|
||||
.node_id_at_path(path)
|
||||
.ok_or_else(|| ErrorBuilder::new(OTErrorCode::PathNotFound).build())?;
|
||||
match self.arena.get_mut(node_id) {
|
||||
None => tracing::warn!("The path: {:?} does not contain any nodes", path),
|
||||
|
@ -1,8 +1,10 @@
|
||||
use crate::core::document::operation_serde::*;
|
||||
use crate::core::document::path::Path;
|
||||
use crate::core::{NodeAttributes, NodeBodyChangeset, NodeData};
|
||||
use crate::errors::OTError;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
#[serde(tag = "op")]
|
||||
pub enum NodeOperation {
|
||||
#[serde(rename = "insert")]
|
||||
@ -99,59 +101,37 @@ impl NodeOperation {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::core::{NodeAttributes, NodeBodyChangeset, NodeBuilder, NodeData, NodeOperation, Path, TextDelta};
|
||||
#[test]
|
||||
fn test_serialize_insert_operation() {
|
||||
let insert = NodeOperation::Insert {
|
||||
path: Path(vec![0, 1]),
|
||||
nodes: vec![NodeData::new("text".to_owned())],
|
||||
};
|
||||
let result = serde_json::to_string(&insert).unwrap();
|
||||
assert_eq!(result, r#"{"op":"insert","path":[0,1],"nodes":[{"type":"text"}]}"#);
|
||||
}
|
||||
#[derive(Serialize, Deserialize, Default)]
|
||||
pub struct NodeOperationList {
|
||||
operations: Vec<NodeOperation>,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_serialize_insert_sub_trees() {
|
||||
let insert = NodeOperation::Insert {
|
||||
path: Path(vec![0, 1]),
|
||||
nodes: vec![NodeBuilder::new("text")
|
||||
.add_node(NodeData::new("text".to_owned()))
|
||||
.build()],
|
||||
};
|
||||
let result = serde_json::to_string(&insert).unwrap();
|
||||
assert_eq!(
|
||||
result,
|
||||
r#"{"op":"insert","path":[0,1],"nodes":[{"type":"text","children":[{"type":"text"}]}]}"#
|
||||
);
|
||||
}
|
||||
impl std::ops::Deref for NodeOperationList {
|
||||
type Target = Vec<NodeOperation>;
|
||||
|
||||
#[test]
|
||||
fn test_serialize_update_operation() {
|
||||
let insert = NodeOperation::UpdateAttributes {
|
||||
path: Path(vec![0, 1]),
|
||||
attributes: NodeAttributes::new(),
|
||||
old_attributes: NodeAttributes::new(),
|
||||
};
|
||||
let result = serde_json::to_string(&insert).unwrap();
|
||||
assert_eq!(
|
||||
result,
|
||||
r#"{"op":"update","path":[0,1],"attributes":{},"oldAttributes":{}}"#
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_serialize_text_edit_operation() {
|
||||
let changeset = NodeBodyChangeset::Delta {
|
||||
delta: TextDelta::new(),
|
||||
inverted: TextDelta::new(),
|
||||
};
|
||||
let insert = NodeOperation::UpdateBody {
|
||||
path: Path(vec![0, 1]),
|
||||
changeset,
|
||||
};
|
||||
let result = serde_json::to_string(&insert).unwrap();
|
||||
assert_eq!(result, r#"{"op":"edit-body","path":[0,1],"delta":[],"inverted":[]}"#);
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.operations
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::DerefMut for NodeOperationList {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.operations
|
||||
}
|
||||
}
|
||||
|
||||
impl NodeOperationList {
|
||||
pub fn new(operations: Vec<NodeOperation>) -> Self {
|
||||
Self { operations }
|
||||
}
|
||||
|
||||
pub fn from_bytes(bytes: Vec<u8>) -> Result<Self, OTError> {
|
||||
let operation_list = serde_json::from_slice(&bytes).map_err(|err| OTError::serde().context(err))?;
|
||||
Ok(operation_list)
|
||||
}
|
||||
|
||||
pub fn to_bytes(&self) -> Result<Vec<u8>, OTError> {
|
||||
let bytes = serde_json::to_vec(self).map_err(|err| OTError::serde().context(err))?;
|
||||
Ok(bytes)
|
||||
}
|
||||
}
|
||||
|
@ -2,26 +2,28 @@ use crate::core::document::path::Path;
|
||||
use crate::core::{NodeAttributes, NodeData, NodeOperation, NodeTree};
|
||||
use indextree::NodeId;
|
||||
|
||||
use super::{NodeBodyChangeset, NodeOperationList};
|
||||
|
||||
pub struct Transaction {
|
||||
pub operations: Vec<NodeOperation>,
|
||||
pub operations: NodeOperationList,
|
||||
}
|
||||
|
||||
impl Transaction {
|
||||
fn new(operations: Vec<NodeOperation>) -> Transaction {
|
||||
fn new(operations: NodeOperationList) -> Transaction {
|
||||
Transaction { operations }
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TransactionBuilder<'a> {
|
||||
node_tree: &'a NodeTree,
|
||||
operations: Vec<NodeOperation>,
|
||||
operations: NodeOperationList,
|
||||
}
|
||||
|
||||
impl<'a> TransactionBuilder<'a> {
|
||||
pub fn new(node_tree: &'a NodeTree) -> TransactionBuilder {
|
||||
TransactionBuilder {
|
||||
node_tree,
|
||||
operations: Vec::new(),
|
||||
operations: NodeOperationList::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -80,23 +82,39 @@ impl<'a> TransactionBuilder<'a> {
|
||||
self.insert_nodes_at_path(path, vec![node])
|
||||
}
|
||||
|
||||
pub fn update_attributes_at_path(self, path: &Path, attributes: NodeAttributes) -> Self {
|
||||
let mut old_attributes = NodeAttributes::new();
|
||||
let node = self.node_tree.node_at_path(path).unwrap();
|
||||
let node_data = self.node_tree.get_node(node).unwrap();
|
||||
pub fn update_attributes_at_path(mut self, path: &Path, attributes: NodeAttributes) -> Self {
|
||||
match self.node_tree.get_node_at_path(path) {
|
||||
Some(node) => {
|
||||
let mut old_attributes = NodeAttributes::new();
|
||||
for key in attributes.keys() {
|
||||
let old_attrs = &node.attributes;
|
||||
if let Some(value) = old_attrs.get(key.as_str()) {
|
||||
old_attributes.insert(key.clone(), value.clone());
|
||||
}
|
||||
}
|
||||
|
||||
for key in attributes.keys() {
|
||||
let old_attrs = &node_data.attributes;
|
||||
if let Some(value) = old_attrs.get(key.as_str()) {
|
||||
old_attributes.insert(key.clone(), value.clone());
|
||||
self.operations.push(NodeOperation::UpdateAttributes {
|
||||
path: path.clone(),
|
||||
attributes,
|
||||
old_attributes,
|
||||
});
|
||||
}
|
||||
None => tracing::warn!("Update attributes at path: {:?} failed. Node is not exist", path),
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
self.push(NodeOperation::UpdateAttributes {
|
||||
path: path.clone(),
|
||||
attributes,
|
||||
old_attributes,
|
||||
})
|
||||
pub fn update_body_at_path(mut self, path: &Path, changeset: NodeBodyChangeset) -> Self {
|
||||
match self.node_tree.node_id_at_path(path) {
|
||||
Some(_) => {
|
||||
self.operations.push(NodeOperation::UpdateBody {
|
||||
path: path.clone(),
|
||||
changeset,
|
||||
});
|
||||
}
|
||||
None => tracing::warn!("Update attributes at path: {:?} failed. Node is not exist", path),
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn delete_node_at_path(self, path: &Path) -> Self {
|
||||
@ -104,7 +122,7 @@ impl<'a> TransactionBuilder<'a> {
|
||||
}
|
||||
|
||||
pub fn delete_nodes_at_path(mut self, path: &Path, length: usize) -> Self {
|
||||
let mut node = self.node_tree.node_at_path(path).unwrap();
|
||||
let mut node = self.node_tree.node_id_at_path(path).unwrap();
|
||||
let mut deleted_nodes = vec![];
|
||||
for _ in 0..length {
|
||||
deleted_nodes.push(self.get_deleted_nodes(node));
|
||||
|
@ -37,6 +37,7 @@ impl OTError {
|
||||
static_ot_error!(duplicate_revision, OTErrorCode::DuplicatedRevision);
|
||||
static_ot_error!(revision_id_conflict, OTErrorCode::RevisionIDConflict);
|
||||
static_ot_error!(internal, OTErrorCode::Internal);
|
||||
static_ot_error!(serde, OTErrorCode::SerdeError);
|
||||
}
|
||||
|
||||
impl fmt::Display for OTError {
|
||||
|
@ -1,2 +1,3 @@
|
||||
mod operation_test;
|
||||
mod script;
|
||||
mod test;
|
||||
mod tree_test;
|
||||
|
61
shared-lib/lib-ot/tests/node/operation_test.rs
Normal file
61
shared-lib/lib-ot/tests/node/operation_test.rs
Normal file
@ -0,0 +1,61 @@
|
||||
use lib_ot::core::{
|
||||
NodeAttributeBuilder, NodeBodyChangeset, NodeBuilder, NodeData, NodeOperation, Path, TextDeltaBuilder,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn operation_insert_node_serde_test() {
|
||||
let insert = NodeOperation::Insert {
|
||||
path: Path(vec![0, 1]),
|
||||
nodes: vec![NodeData::new("text".to_owned())],
|
||||
};
|
||||
let result = serde_json::to_string(&insert).unwrap();
|
||||
assert_eq!(result, r#"{"op":"insert","path":[0,1],"nodes":[{"type":"text"}]}"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn operation_insert_node_with_children_serde_test() {
|
||||
let node = NodeBuilder::new("text")
|
||||
.add_node(NodeData::new("sub_text".to_owned()))
|
||||
.build();
|
||||
|
||||
let insert = NodeOperation::Insert {
|
||||
path: Path(vec![0, 1]),
|
||||
nodes: vec![node],
|
||||
};
|
||||
assert_eq!(
|
||||
serde_json::to_string(&insert).unwrap(),
|
||||
r#"{"op":"insert","path":[0,1],"nodes":[{"type":"text","children":[{"type":"sub_text"}]}]}"#
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn operation_update_node_attributes_serde_test() {
|
||||
let operation = NodeOperation::UpdateAttributes {
|
||||
path: Path(vec![0, 1]),
|
||||
attributes: NodeAttributeBuilder::new().insert("bold", true).build(),
|
||||
old_attributes: NodeAttributeBuilder::new().insert("bold", false).build(),
|
||||
};
|
||||
|
||||
let result = serde_json::to_string(&operation).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
result,
|
||||
r#"{"op":"update","path":[0,1],"attributes":{"bold":"true"},"oldAttributes":{"bold":"false"}}"#
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn operation_update_node_body_serde_test() {
|
||||
let delta = TextDeltaBuilder::new().insert("AppFlowy...").build();
|
||||
let inverted = delta.invert_str("");
|
||||
let changeset = NodeBodyChangeset::Delta { delta, inverted };
|
||||
let insert = NodeOperation::UpdateBody {
|
||||
path: Path(vec![0, 1]),
|
||||
changeset,
|
||||
};
|
||||
let result = serde_json::to_string(&insert).unwrap();
|
||||
assert_eq!(
|
||||
result,
|
||||
r#"{"op":"edit-body","path":[0,1],"delta":[{"insert":"AppFlowy..."}],"inverted":[{"delete":11}]}"#
|
||||
);
|
||||
//
|
||||
}
|
@ -1,11 +1,15 @@
|
||||
use lib_ot::core::{NodeAttributes, NodeData, NodeTree, Path, TransactionBuilder};
|
||||
use lib_ot::core::{
|
||||
NodeAttributes, NodeBody, NodeBodyChangeset, NodeData, NodeTree, Path, TextDelta, TransactionBuilder,
|
||||
};
|
||||
|
||||
pub enum NodeScript {
|
||||
InsertNode { path: Path, node: NodeData },
|
||||
InsertAttributes { path: Path, attributes: NodeAttributes },
|
||||
UpdateAttributes { path: Path, attributes: NodeAttributes },
|
||||
UpdateBody { path: Path, changeset: NodeBodyChangeset },
|
||||
DeleteNode { path: Path },
|
||||
AssertNumberOfChildrenAtPath { path: Option<Path>, len: usize },
|
||||
AssertNode { path: Path, expected: Option<NodeData> },
|
||||
AssertNodeDelta { path: Path, expected: TextDelta },
|
||||
}
|
||||
|
||||
pub struct NodeTest {
|
||||
@ -34,12 +38,19 @@ impl NodeTest {
|
||||
|
||||
self.node_tree.apply(transaction).unwrap();
|
||||
}
|
||||
NodeScript::InsertAttributes { path, attributes } => {
|
||||
NodeScript::UpdateAttributes { path, attributes } => {
|
||||
let transaction = TransactionBuilder::new(&self.node_tree)
|
||||
.update_attributes_at_path(&path, attributes)
|
||||
.finalize();
|
||||
self.node_tree.apply(transaction).unwrap();
|
||||
}
|
||||
NodeScript::UpdateBody { path, changeset } => {
|
||||
//
|
||||
let transaction = TransactionBuilder::new(&self.node_tree)
|
||||
.update_body_at_path(&path, changeset)
|
||||
.finalize();
|
||||
self.node_tree.apply(transaction).unwrap();
|
||||
}
|
||||
NodeScript::DeleteNode { path } => {
|
||||
let transaction = TransactionBuilder::new(&self.node_tree)
|
||||
.delete_node_at_path(&path)
|
||||
@ -47,7 +58,7 @@ impl NodeTest {
|
||||
self.node_tree.apply(transaction).unwrap();
|
||||
}
|
||||
NodeScript::AssertNode { path, expected } => {
|
||||
let node_id = self.node_tree.node_at_path(path);
|
||||
let node_id = self.node_tree.node_id_at_path(path);
|
||||
|
||||
match node_id {
|
||||
None => assert!(node_id.is_none()),
|
||||
@ -66,11 +77,19 @@ impl NodeTest {
|
||||
assert_eq!(len, expected_len)
|
||||
}
|
||||
Some(path) => {
|
||||
let node_id = self.node_tree.node_at_path(path).unwrap();
|
||||
let node_id = self.node_tree.node_id_at_path(path).unwrap();
|
||||
let len = self.node_tree.number_of_children(Some(node_id));
|
||||
assert_eq!(len, expected_len)
|
||||
}
|
||||
},
|
||||
NodeScript::AssertNodeDelta { path, expected } => {
|
||||
let node = self.node_tree.get_node_at_path(&path).unwrap();
|
||||
if let NodeBody::Delta(delta) = node.body.clone() {
|
||||
debug_assert_eq!(delta, expected);
|
||||
} else {
|
||||
panic!("Node body type not match, expect Delta");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,9 @@
|
||||
use crate::node::script::NodeScript::*;
|
||||
use crate::node::script::NodeTest;
|
||||
use lib_ot::core::NodeBody;
|
||||
use lib_ot::core::NodeBodyChangeset;
|
||||
use lib_ot::core::OperationTransform;
|
||||
use lib_ot::core::TextDeltaBuilder;
|
||||
use lib_ot::core::{NodeBuilder, NodeData, Path};
|
||||
|
||||
#[test]
|
||||
@ -147,7 +151,7 @@ fn node_insert_with_attributes_test() {
|
||||
path: path.clone(),
|
||||
node: inserted_node.clone(),
|
||||
},
|
||||
InsertAttributes {
|
||||
UpdateAttributes {
|
||||
path: path.clone(),
|
||||
attributes: inserted_node.attributes.clone(),
|
||||
},
|
||||
@ -175,3 +179,32 @@ fn node_delete_test() {
|
||||
];
|
||||
test.run_scripts(scripts);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn node_update_body_test() {
|
||||
let mut test = NodeTest::new();
|
||||
let path: Path = 0.into();
|
||||
|
||||
let s = "Hello".to_owned();
|
||||
let init_delta = TextDeltaBuilder::new().insert(&s).build();
|
||||
let delta = TextDeltaBuilder::new().retain(s.len()).insert(" AppFlowy").build();
|
||||
let inverted = delta.invert(&init_delta);
|
||||
let expected = init_delta.compose(&delta).unwrap();
|
||||
|
||||
let node = NodeBuilder::new("text")
|
||||
.insert_body(NodeBody::Delta(init_delta))
|
||||
.build();
|
||||
|
||||
let scripts = vec![
|
||||
InsertNode {
|
||||
path: path.clone(),
|
||||
node: node.clone(),
|
||||
},
|
||||
UpdateBody {
|
||||
path: path.clone(),
|
||||
changeset: NodeBodyChangeset::Delta { delta, inverted },
|
||||
},
|
||||
AssertNodeDelta { path, expected },
|
||||
];
|
||||
test.run_scripts(scripts);
|
||||
}
|
Loading…
Reference in New Issue
Block a user