diff --git a/shared-lib/Cargo.lock b/shared-lib/Cargo.lock index 596f1fe694..79357da09b 100644 --- a/shared-lib/Cargo.lock +++ b/shared-lib/Cargo.lock @@ -821,6 +821,7 @@ dependencies = [ "md5", "serde", "serde_json", + "serde_repr", "strum", "strum_macros", "thiserror", diff --git a/shared-lib/lib-ot/Cargo.toml b/shared-lib/lib-ot/Cargo.toml index d47968dcfe..3d0bd5aadd 100644 --- a/shared-lib/lib-ot/Cargo.toml +++ b/shared-lib/lib-ot/Cargo.toml @@ -10,14 +10,15 @@ bytecount = "0.6.0" serde = { version = "1.0", features = ["derive"] } #protobuf = {version = "2.18.0"} #flowy-derive = { path = "../flowy-derive" } -tokio = {version = "1", features = ["sync"]} +tokio = { version = "1", features = ["sync"] } dashmap = "5" md5 = "0.7.0" anyhow = "1.0" thiserror = "1.0" -serde_json = {version = "1.0"} -derive_more = {version = "0.99", features = ["display"]} +serde_json = { version = "1.0" } +serde_repr = { version = "0.1" } +derive_more = { version = "0.99", features = ["display"] } log = "0.4" tracing = { version = "0.1", features = ["log"] } lazy_static = "1.4.0" @@ -29,5 +30,3 @@ indextree = "4.4.0" [features] flowy_unit_test = [] - - diff --git a/shared-lib/lib-ot/src/core/document/attributes.rs b/shared-lib/lib-ot/src/core/document/attributes.rs index 0c79bbc307..cb102b6f9e 100644 --- a/shared-lib/lib-ot/src/core/document/attributes.rs +++ b/shared-lib/lib-ot/src/core/document/attributes.rs @@ -1,8 +1,8 @@ use crate::core::OperationTransform; use crate::errors::OTError; use serde::{Deserialize, Serialize}; +use serde_repr::*; use std::collections::HashMap; - pub type AttributeMap = HashMap; #[derive(Default, Clone, Serialize, Deserialize, Eq, PartialEq, Debug)] @@ -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(&mut self, key: K) { + self.insert(key.to_string(), AttributeValue::empty()); } } @@ -94,53 +94,104 @@ impl OperationTransform for NodeAttributes { pub type AttributeKey = String; -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] -pub struct AttributeValue(pub Option); - -impl std::convert::From<&usize> for AttributeValue { - fn from(val: &usize) -> Self { - AttributeValue::from(*val) - } +#[derive(Eq, PartialEq, Hash, Debug, Clone, Serialize_repr, Deserialize_repr)] +#[repr(u8)] +pub enum ValueType { + IntType = 0, + FloatType = 1, + StrType = 2, + BoolType = 3, } -impl std::convert::From for AttributeValue { - fn from(val: usize) -> Self { - if val > 0_usize { - AttributeValue(Some(format!("{}", val))) - } else { - AttributeValue(None) +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct AttributeValue { + pub ty: ValueType, + pub value: Option, +} + +impl AttributeValue { + pub fn empty() -> Self { + Self { + ty: ValueType::StrType, + value: None, } } -} - -impl std::convert::From<&str> for AttributeValue { - fn from(val: &str) -> Self { - val.to_owned().into() - } -} - -impl std::convert::From for AttributeValue { - fn from(val: String) -> Self { - if val.is_empty() { - AttributeValue(None) - } else { - AttributeValue(Some(val)) + pub fn from_int(val: usize) -> Self { + Self { + ty: ValueType::IntType, + value: Some(val.to_string()), } } -} -impl std::convert::From<&bool> for AttributeValue { - fn from(val: &bool) -> Self { - AttributeValue::from(*val) + pub fn from_float(val: f64) -> Self { + Self { + ty: ValueType::FloatType, + value: Some(val.to_string()), + } + } + + pub fn from_bool(val: bool) -> Self { + Self { + ty: ValueType::BoolType, + value: Some(val.to_string()), + } + } + pub fn from_str(s: &str) -> Self { + let value = if s.is_empty() { None } else { Some(s.to_string()) }; + Self { + ty: ValueType::StrType, + value, + } + } + + pub fn int_value(&self) -> Option { + let value = self.value.as_ref()?; + Some(value.parse::().unwrap_or(0)) + } + + pub fn bool_value(&self) -> Option { + let value = self.value.as_ref()?; + Some(value.parse::().unwrap_or(false)) + } + + pub fn str_value(&self) -> Option { + self.value.clone() + } + + pub fn float_value(&self) -> Option { + let value = self.value.as_ref()?; + Some(value.parse::().unwrap_or(0.0)) } } impl std::convert::From for AttributeValue { - fn from(val: bool) -> Self { - let val = match val { - true => Some("true".to_owned()), - false => None, - }; - AttributeValue(val) + fn from(value: bool) -> Self { + AttributeValue::from_bool(value) + } +} + +pub struct NodeAttributeBuilder { + attributes: NodeAttributes, +} + +impl NodeAttributeBuilder { + pub fn new() -> Self { + Self { + attributes: NodeAttributes::default(), + } + } + + pub fn insert>(mut self, key: K, value: V) -> Self { + self.attributes.insert(key, value); + self + } + + pub fn delete(mut self, key: K) -> Self { + self.attributes.delete(key); + self + } + + pub fn build(self) -> NodeAttributes { + self.attributes } } diff --git a/shared-lib/lib-ot/src/core/document/attributes_serde.rs b/shared-lib/lib-ot/src/core/document/attributes_serde.rs new file mode 100644 index 0000000000..60e30d07df --- /dev/null +++ b/shared-lib/lib-ot/src/core/document/attributes_serde.rs @@ -0,0 +1,159 @@ +use std::fmt; + +use serde::{ + de::{self, MapAccess, Visitor}, + Deserialize, Deserializer, Serialize, Serializer, +}; + +use super::AttributeValue; + +impl Serialize for AttributeValue { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + match self.ty { + super::ValueType::IntType => { + // + if let Some(value) = self.int_value() { + serializer.serialize_i64(value) + } else { + serializer.serialize_none() + } + } + super::ValueType::FloatType => { + if let Some(value) = self.float_value() { + serializer.serialize_f64(value) + } else { + serializer.serialize_none() + } + } + super::ValueType::StrType => { + if let Some(value) = self.str_value() { + serializer.serialize_str(&value) + } else { + serializer.serialize_none() + } + } + super::ValueType::BoolType => { + if let Some(value) = self.bool_value() { + serializer.serialize_bool(value) + } else { + serializer.serialize_none() + } + } + } + } +} + +impl<'de> Deserialize<'de> for AttributeValue { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct AttributeValueVisitor; + impl<'de> Visitor<'de> for AttributeValueVisitor { + type Value = AttributeValue; + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("bool, usize or string") + } + + fn visit_bool(self, value: bool) -> Result + where + E: de::Error, + { + Ok(AttributeValue::from_bool(value)) + } + + fn visit_i8(self, value: i8) -> Result + where + E: de::Error, + { + Ok(AttributeValue::from_int(value as usize)) + } + + fn visit_i16(self, value: i16) -> Result + where + E: de::Error, + { + Ok(AttributeValue::from_int(value as usize)) + } + + fn visit_i32(self, value: i32) -> Result + where + E: de::Error, + { + Ok(AttributeValue::from_int(value as usize)) + } + + fn visit_i64(self, value: i64) -> Result + where + E: de::Error, + { + Ok(AttributeValue::from_int(value as usize)) + } + + fn visit_u8(self, value: u8) -> Result + where + E: de::Error, + { + Ok(AttributeValue::from_int(value as usize)) + } + + fn visit_u16(self, value: u16) -> Result + where + E: de::Error, + { + Ok(AttributeValue::from_int(value as usize)) + } + + fn visit_u32(self, value: u32) -> Result + where + E: de::Error, + { + Ok(AttributeValue::from_int(value as usize)) + } + + fn visit_u64(self, value: u64) -> Result + where + E: de::Error, + { + Ok(AttributeValue::from_int(value as usize)) + } + + fn visit_str(self, s: &str) -> Result + where + E: de::Error, + { + Ok(AttributeValue::from_str(s)) + } + + fn visit_none(self) -> Result + where + E: de::Error, + { + Ok(AttributeValue::empty()) + } + + fn visit_unit(self) -> Result + where + E: de::Error, + { + // the value that contains null will be processed here. + Ok(AttributeValue::empty()) + } + + fn visit_map(self, map: A) -> Result + where + A: MapAccess<'de>, + { + // https://github.com/serde-rs/json/issues/505 + let mut map = map; + let value = map.next_value::()?; + Ok(value) + } + } + + deserializer.deserialize_any(AttributeValueVisitor) + } +} diff --git a/shared-lib/lib-ot/src/core/document/mod.rs b/shared-lib/lib-ot/src/core/document/mod.rs index d01bb9502c..023cb80d14 100644 --- a/shared-lib/lib-ot/src/core/document/mod.rs +++ b/shared-lib/lib-ot/src/core/document/mod.rs @@ -1,6 +1,8 @@ #![allow(clippy::module_inception)] mod attributes; +mod attributes_serde; mod node; +mod node_serde; mod node_tree; mod operation; mod operation_serde; diff --git a/shared-lib/lib-ot/src/core/document/node.rs b/shared-lib/lib-ot/src/core/document/node.rs index 0fc712c96c..0a7c214703 100644 --- a/shared-lib/lib-ot/src/core/document/node.rs +++ b/shared-lib/lib-ot/src/core/document/node.rs @@ -1,6 +1,8 @@ +use super::node_serde::*; use crate::core::NodeBody::Delta; -use crate::core::{AttributeKey, AttributeValue, NodeAttributes, OperationTransform, TextDelta}; +use crate::core::{AttributeKey, AttributeValue, NodeAttributes, OperationTransform}; use crate::errors::OTError; +use crate::rich_text::RichTextDelta; use serde::{Deserialize, Serialize}; #[derive(Default, Clone, Serialize, Deserialize, Eq, PartialEq)] @@ -9,12 +11,17 @@ pub struct NodeData { pub node_type: String, #[serde(skip_serializing_if = "NodeAttributes::is_empty")] + #[serde(default)] pub attributes: NodeAttributes, + #[serde(serialize_with = "serialize_body")] + #[serde(deserialize_with = "deserialize_body")] #[serde(skip_serializing_if = "NodeBody::is_empty")] + #[serde(default)] pub body: NodeBody, #[serde(skip_serializing_if = "Vec::is_empty")] + #[serde(default)] pub children: Vec, } @@ -25,14 +32,24 @@ impl NodeData { ..Default::default() } } + + pub fn split(self) -> (Node, Vec) { + let node = Node { + node_type: self.node_type, + body: self.body, + attributes: self.attributes, + }; + + (node, self.children) + } } /// Builder for [`NodeData`] -pub struct NodeBuilder { +pub struct NodeDataBuilder { node: NodeData, } -impl NodeBuilder { +impl NodeDataBuilder { pub fn new(node_type: T) -> Self { Self { node: NodeData::new(node_type.to_string()), @@ -73,10 +90,10 @@ impl NodeBuilder { /// The NodeBody implements the [`OperationTransform`] trait which means it can perform /// compose, transform and invert. /// -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq)] pub enum NodeBody { Empty, - Delta(TextDelta), + Delta(RichTextDelta), } impl std::default::Default for NodeBody { @@ -142,8 +159,12 @@ impl OperationTransform for NodeBody { /// /// Each NodeBody except the Empty should have its corresponding changeset variant. #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] pub enum NodeBodyChangeset { - Delta { delta: TextDelta, inverted: TextDelta }, + Delta { + delta: RichTextDelta, + inverted: RichTextDelta, + }, } impl NodeBodyChangeset { @@ -175,7 +196,7 @@ impl Node { } } - pub fn apply_body_changeset(&mut self, changeset: &NodeBodyChangeset) { + pub fn apply_body_changeset(&mut self, changeset: NodeBodyChangeset) { match changeset { NodeBodyChangeset::Delta { delta, inverted: _ } => match self.body.compose(&Delta(delta.clone())) { Ok(new_body) => self.body = new_body, diff --git a/shared-lib/lib-ot/src/core/document/node_serde.rs b/shared-lib/lib-ot/src/core/document/node_serde.rs new file mode 100644 index 0000000000..32e54d0324 --- /dev/null +++ b/shared-lib/lib-ot/src/core/document/node_serde.rs @@ -0,0 +1,75 @@ +use super::NodeBody; +use crate::rich_text::RichTextDelta; +use serde::de::{self, MapAccess, Visitor}; +use serde::ser::SerializeMap; +use serde::{Deserializer, Serializer}; +use std::fmt; + +pub fn serialize_body(body: &NodeBody, serializer: S) -> Result +where + S: Serializer, +{ + let mut map = serializer.serialize_map(Some(3))?; + match body { + NodeBody::Empty => {} + NodeBody::Delta(delta) => { + map.serialize_key("delta")?; + map.serialize_value(delta)?; + } + } + map.end() +} + +pub fn deserialize_body<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + struct NodeBodyVisitor(); + + impl<'de> Visitor<'de> for NodeBodyVisitor { + type Value = NodeBody; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("Expect NodeBody") + } + + fn visit_seq(self, mut seq: A) -> Result + where + A: de::SeqAccess<'de>, + { + let mut delta = RichTextDelta::default(); + while let Some(op) = seq.next_element()? { + delta.add(op); + } + Ok(NodeBody::Delta(delta)) + } + + #[inline] + fn visit_map(self, mut map: V) -> Result + where + V: MapAccess<'de>, + { + let mut delta: Option = None; + while let Some(key) = map.next_key()? { + match key { + "delta" => { + if delta.is_some() { + return Err(de::Error::duplicate_field("delta")); + } + delta = Some(map.next_value()?); + } + other => { + panic!("Unexpected key: {}", other); + } + } + } + + if delta.is_some() { + return Ok(NodeBody::Delta(delta.unwrap())); + } + + Err(de::Error::missing_field("delta")) + } + } + deserializer.deserialize_any(NodeBodyVisitor()) +} diff --git a/shared-lib/lib-ot/src/core/document/node_tree.rs b/shared-lib/lib-ot/src/core/document/node_tree.rs index 15e167ace9..7eb360ace5 100644 --- a/shared-lib/lib-ot/src/core/document/node_tree.rs +++ b/shared-lib/lib-ot/src/core/document/node_tree.rs @@ -3,6 +3,8 @@ use crate::core::{Node, NodeAttributes, NodeBodyChangeset, NodeData, NodeOperati use crate::errors::{ErrorBuilder, OTError, OTErrorCode}; use indextree::{Arena, Children, FollowingSiblings, NodeId}; +use super::NodeOperationList; + /// pub struct NodeTree { arena: Arena, @@ -11,21 +13,42 @@ pub struct NodeTree { impl Default for NodeTree { fn default() -> Self { - Self::new() + Self::new("root") } } impl NodeTree { - pub fn new() -> NodeTree { + pub fn new(root_name: &str) -> NodeTree { let mut arena = Arena::new(); - let root = arena.new_node(Node::new("root")); + let root = arena.new_node(Node::new(root_name)); NodeTree { arena, root } } + pub fn from_bytes(root_name: &str, bytes: Vec) -> Result { + let operations = NodeOperationList::from_bytes(bytes)?.into_inner(); + Self::from_operations(root_name, operations) + } + + pub fn from_operations(root_name: &str, operations: Vec) -> Result { + let mut node_tree = NodeTree::new(root_name); + + for operation in operations { + let _ = node_tree.apply_op(operation)?; + } + Ok(node_tree) + } + pub fn get_node(&self, node_id: NodeId) -> Option<&Node> { 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 /// @@ -35,13 +58,13 @@ impl NodeTree { /// let root_path: Path = vec![0].into(); /// let op = NodeOperation::Insert {path: root_path.clone(),nodes }; /// - /// let mut node_tree = NodeTree::new(); - /// node_tree.apply_op(&op).unwrap(); - /// let node_id = node_tree.node_at_path(&root_path).unwrap(); - /// let node_path = node_tree.path_of_node(node_id); + /// let mut node_tree = NodeTree::new("root"); + /// node_tree.apply_op(op).unwrap(); + /// let node_id = node_tree.node_id_at_path(&root_path).unwrap(); + /// let node_path = node_tree.path_from_node_id(node_id); /// debug_assert_eq!(node_path, root_path); /// ``` - pub fn node_at_path>(&self, path: T) -> Option { + pub fn node_id_at_path>(&self, path: T) -> Option { let path = path.into(); if path.is_empty() { return Some(self.root); @@ -54,7 +77,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. @@ -93,15 +116,14 @@ impl NodeTree { /// /// ``` /// use lib_ot::core::{NodeOperation, NodeTree, NodeData, Path}; - /// let node = NodeData::new("text".to_string()); + /// let node_1 = NodeData::new("text".to_string()); /// let inserted_path: Path = vec![0].into(); /// - /// let mut node_tree = NodeTree::new(); - /// node_tree.apply_op(&NodeOperation::Insert {path: inserted_path.clone(),nodes: vec![node.clone()] }).unwrap(); + /// let mut node_tree = NodeTree::new("root"); + /// node_tree.apply_op(NodeOperation::Insert {path: inserted_path.clone(),nodes: vec![node_1.clone()] }).unwrap(); /// - /// let inserted_note = node_tree.node_at_path(&inserted_path).unwrap(); - /// let inserted_data = node_tree.get_node(inserted_note).unwrap(); - /// assert_eq!(inserted_data.node_type, node.node_type); + /// let node_2 = node_tree.get_node_at_path(&inserted_path).unwrap(); + /// assert_eq!(node_2.node_type, node_1.node_type); /// ``` pub fn child_from_node_at_index(&self, node_id: NodeId, index: usize) -> Option { let children = node_id.children(&self.arena); @@ -137,25 +159,26 @@ impl NodeTree { } pub fn apply(&mut self, transaction: Transaction) -> Result<(), OTError> { - for op in &transaction.operations { - self.apply_op(op)?; + let operations = transaction.into_operations(); + for operation in operations { + self.apply_op(operation)?; } Ok(()) } - pub fn apply_op(&mut self, op: &NodeOperation) -> Result<(), OTError> { + pub fn apply_op(&mut self, op: NodeOperation) -> Result<(), OTError> { match op { - NodeOperation::Insert { path, nodes } => self.insert_nodes(path, nodes), - NodeOperation::UpdateAttributes { path, attributes, .. } => self.update_attributes(path, attributes), - NodeOperation::UpdateBody { path, changeset } => self.update_body(path, changeset), - NodeOperation::Delete { path, nodes } => self.delete_node(path, nodes), + NodeOperation::Insert { path, nodes } => self.insert_nodes(&path, nodes), + NodeOperation::UpdateAttributes { path, attributes, .. } => self.update_attributes(&path, attributes), + NodeOperation::UpdateBody { path, changeset } => self.update_body(&path, changeset), + NodeOperation::Delete { path, nodes } => self.delete_node(&path, nodes), } } /// Inserts nodes at given path /// /// returns error if the path is empty /// - fn insert_nodes(&mut self, path: &Path, nodes: &[NodeData]) -> Result<(), OTError> { + fn insert_nodes(&mut self, path: &Path, nodes: Vec) -> Result<(), OTError> { debug_assert!(!path.is_empty()); if path.is_empty() { return Err(OTErrorCode::PathIsEmpty.into()); @@ -164,7 +187,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) @@ -172,8 +195,9 @@ impl NodeTree { /// Inserts nodes before the node with node_id /// - fn insert_nodes_before(&mut self, node_id: &NodeId, nodes: &[NodeData]) { + fn insert_nodes_before(&mut self, node_id: &NodeId, nodes: Vec) { for node in nodes { + let (node, children) = node.split(); let new_node_id = self.arena.new_node(node.into()); if node_id.is_removed(&self.arena) { tracing::warn!("Node:{:?} is remove before insert", node_id); @@ -181,23 +205,18 @@ impl NodeTree { } node_id.insert_before(new_node_id, &mut self.arena); - self.append_nodes(&new_node_id, &node.children); + self.append_nodes(&new_node_id, children); } } - fn insert_nodes_at_index( - &mut self, - parent: NodeId, - index: usize, - insert_children: &[NodeData], - ) -> Result<(), OTError> { + fn insert_nodes_at_index(&mut self, parent: NodeId, index: usize, nodes: Vec) -> Result<(), OTError> { if index == 0 && parent.children(&self.arena).next().is_none() { - self.append_nodes(&parent, insert_children); + self.append_nodes(&parent, nodes); return Ok(()); } if index == parent.children(&self.arena).count() { - self.append_nodes(&parent, insert_children); + self.append_nodes(&parent, nodes); return Ok(()); } @@ -205,30 +224,31 @@ impl NodeTree { .child_from_node_at_index(parent, index) .ok_or_else(|| ErrorBuilder::new(OTErrorCode::PathNotFound).build())?; - self.insert_nodes_before(&node_to_insert, insert_children); + self.insert_nodes_before(&node_to_insert, nodes); Ok(()) } - fn append_nodes(&mut self, parent: &NodeId, nodes: &[NodeData]) { + fn append_nodes(&mut self, parent: &NodeId, nodes: Vec) { for node in nodes { + let (node, children) = node.split(); let new_node_id = self.arena.new_node(node.into()); parent.append(new_node_id, &mut self.arena); - self.append_nodes(&new_node_id, &node.children); + self.append_nodes(&new_node_id, children); } } - fn update_attributes(&mut self, path: &Path, attributes: &NodeAttributes) -> Result<(), OTError> { + fn update_attributes(&mut self, path: &Path, attributes: NodeAttributes) -> Result<(), OTError> { self.mut_node_at_path(path, |node| { - let new_attributes = NodeAttributes::compose(&node.attributes, attributes)?; + let new_attributes = NodeAttributes::compose(&node.attributes, &attributes)?; node.attributes = new_attributes; Ok(()) }) } - fn delete_node(&mut self, path: &Path, nodes: &[NodeData]) -> Result<(), OTError> { + fn delete_node(&mut self, path: &Path, nodes: Vec) -> 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() { @@ -243,7 +263,7 @@ impl NodeTree { Ok(()) } - fn update_body(&mut self, path: &Path, changeset: &NodeBodyChangeset) -> Result<(), OTError> { + fn update_body(&mut self, path: &Path, changeset: NodeBodyChangeset) -> Result<(), OTError> { self.mut_node_at_path(path, |node| { node.apply_body_changeset(changeset); Ok(()) @@ -252,10 +272,10 @@ impl NodeTree { fn mut_node_at_path(&mut self, path: &Path, f: F) -> Result<(), OTError> where - F: Fn(&mut Node) -> Result<(), OTError>, + F: FnOnce(&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), diff --git a/shared-lib/lib-ot/src/core/document/operation.rs b/shared-lib/lib-ot/src/core/document/operation.rs index c22fd6ca54..9d95a40678 100644 --- a/shared-lib/lib-ot/src/core/document/operation.rs +++ b/shared-lib/lib-ot/src/core/document/operation.rs @@ -1,8 +1,9 @@ -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")] @@ -16,9 +17,9 @@ pub enum NodeOperation { old_attributes: NodeAttributes, }, - #[serde(rename = "edit-body")] - #[serde(serialize_with = "serialize_edit_body")] - // #[serde(deserialize_with = "operation_serde::deserialize_edit_body")] + #[serde(rename = "update-body")] + // #[serde(serialize_with = "serialize_edit_body")] + // #[serde(deserialize_with = "deserialize_edit_body")] UpdateBody { path: Path, changeset: NodeBodyChangeset }, #[serde(rename = "delete")] @@ -99,59 +100,43 @@ 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, +} - #[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"}]}]}"# - ); - } - - #[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":[]}"#); +impl NodeOperationList { + pub fn into_inner(self) -> Vec { + self.operations + } +} + +impl std::ops::Deref for NodeOperationList { + type Target = Vec; + + 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) -> Self { + Self { operations } + } + + pub fn from_bytes(bytes: Vec) -> Result { + let operation_list = serde_json::from_slice(&bytes).map_err(|err| OTError::serde().context(err))?; + Ok(operation_list) + } + + pub fn to_bytes(&self) -> Result, OTError> { + let bytes = serde_json::to_vec(self).map_err(|err| OTError::serde().context(err))?; + Ok(bytes) } } diff --git a/shared-lib/lib-ot/src/core/document/operation_serde.rs b/shared-lib/lib-ot/src/core/document/operation_serde.rs index fddc7ecd14..2234216eae 100644 --- a/shared-lib/lib-ot/src/core/document/operation_serde.rs +++ b/shared-lib/lib-ot/src/core/document/operation_serde.rs @@ -1,7 +1,13 @@ use crate::core::{NodeBodyChangeset, Path}; +use crate::rich_text::RichTextDelta; +use serde::de::{self, MapAccess, Visitor}; use serde::ser::SerializeMap; -use serde::Serializer; +use serde::{Deserializer, Serializer}; +use std::convert::TryInto; +use std::fmt; +use std::marker::PhantomData; +#[allow(dead_code)] pub fn serialize_edit_body(path: &Path, changeset: &NodeBodyChangeset, serializer: S) -> Result where S: Serializer, @@ -21,9 +27,101 @@ where } } -// pub fn deserialize_edit_body<'de, D>(deserializer: D) -> Result -// where -// D: Deserializer<'de>, -// { -// todo!() -// } +#[allow(dead_code)] +pub fn deserialize_edit_body<'de, D>(deserializer: D) -> Result<(Path, NodeBodyChangeset), D::Error> +where + D: Deserializer<'de>, +{ + struct NodeBodyChangesetVisitor(); + + impl<'de> Visitor<'de> for NodeBodyChangesetVisitor { + type Value = (Path, NodeBodyChangeset); + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("Expect Path and NodeBodyChangeset") + } + + #[inline] + fn visit_map(self, mut map: V) -> Result + where + V: MapAccess<'de>, + { + let mut path: Option = None; + let mut delta_changeset = DeltaBodyChangeset::::new(); + while let Some(key) = map.next_key()? { + match key { + "delta" => { + if delta_changeset.delta.is_some() { + return Err(de::Error::duplicate_field("delta")); + } + delta_changeset.delta = Some(map.next_value()?); + } + "inverted" => { + if delta_changeset.inverted.is_some() { + return Err(de::Error::duplicate_field("inverted")); + } + delta_changeset.inverted = Some(map.next_value()?); + } + "path" => { + if path.is_some() { + return Err(de::Error::duplicate_field("path")); + } + + path = Some(map.next_value::()?) + } + other => { + panic!("Unexpected key: {}", other); + } + } + } + if path.is_none() { + return Err(de::Error::missing_field("path")); + } + + let changeset = delta_changeset.try_into()?; + + Ok((path.unwrap(), changeset)) + } + } + deserializer.deserialize_any(NodeBodyChangesetVisitor()) +} + +#[allow(dead_code)] +struct DeltaBodyChangeset { + delta: Option, + inverted: Option, + error: PhantomData, +} + +impl DeltaBodyChangeset { + fn new() -> Self { + Self { + delta: None, + inverted: None, + error: PhantomData, + } + } +} + +impl std::convert::TryInto for DeltaBodyChangeset +where + E: de::Error, +{ + type Error = E; + + fn try_into(self) -> Result { + if self.delta.is_none() { + return Err(de::Error::missing_field("delta")); + } + + if self.inverted.is_none() { + return Err(de::Error::missing_field("inverted")); + } + let changeset = NodeBodyChangeset::Delta { + delta: self.delta.unwrap(), + inverted: self.inverted.unwrap(), + }; + + Ok(changeset) + } +} diff --git a/shared-lib/lib-ot/src/core/document/transaction.rs b/shared-lib/lib-ot/src/core/document/transaction.rs index ddd6168d38..14b1595d3d 100644 --- a/shared-lib/lib-ot/src/core/document/transaction.rs +++ b/shared-lib/lib-ot/src/core/document/transaction.rs @@ -2,26 +2,46 @@ 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, + operations: NodeOperationList, } impl Transaction { - fn new(operations: Vec) -> Transaction { + pub fn new(operations: NodeOperationList) -> Transaction { Transaction { operations } } + + pub fn into_operations(self) -> Vec { + self.operations.into_inner() + } +} + +impl std::ops::Deref for Transaction { + type Target = NodeOperationList; + + fn deref(&self) -> &Self::Target { + &self.operations + } +} + +impl std::ops::DerefMut for Transaction { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.operations + } } pub struct TransactionBuilder<'a> { node_tree: &'a NodeTree, - operations: Vec, + operations: NodeOperationList, } impl<'a> TransactionBuilder<'a> { pub fn new(node_tree: &'a NodeTree) -> TransactionBuilder { TransactionBuilder { node_tree, - operations: Vec::new(), + operations: NodeOperationList::default(), } } @@ -39,13 +59,13 @@ impl<'a> TransactionBuilder<'a> { /// // 0 -- text_1 /// // 1 -- text_2 /// use lib_ot::core::{NodeTree, NodeData, TransactionBuilder}; - /// let mut node_tree = NodeTree::new(); + /// let mut node_tree = NodeTree::new("root"); /// let transaction = TransactionBuilder::new(&node_tree) /// .insert_nodes_at_path(0,vec![ NodeData::new("text_1"), NodeData::new("text_2")]) /// .finalize(); /// node_tree.apply(transaction).unwrap(); /// - /// node_tree.node_at_path(vec![0, 0]); + /// node_tree.node_id_at_path(vec![0, 0]); /// ``` /// pub fn insert_nodes_at_path>(self, path: T, nodes: Vec) -> Self { @@ -69,7 +89,7 @@ impl<'a> TransactionBuilder<'a> { /// // -- 0 /// // |-- text /// use lib_ot::core::{NodeTree, NodeData, TransactionBuilder}; - /// let mut node_tree = NodeTree::new(); + /// let mut node_tree = NodeTree::new("root"); /// let transaction = TransactionBuilder::new(&node_tree) /// .insert_node_at_path(0, NodeData::new("text")) /// .finalize(); @@ -80,23 +100,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 +140,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)); diff --git a/shared-lib/lib-ot/src/errors.rs b/shared-lib/lib-ot/src/errors.rs index fe2a03b206..9878c74947 100644 --- a/shared-lib/lib-ot/src/errors.rs +++ b/shared-lib/lib-ot/src/errors.rs @@ -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 { diff --git a/shared-lib/lib-ot/src/rich_text/attributes.rs b/shared-lib/lib-ot/src/rich_text/attributes.rs index f862dab4f3..d4d1d652f7 100644 --- a/shared-lib/lib-ot/src/rich_text/attributes.rs +++ b/shared-lib/lib-ot/src/rich_text/attributes.rs @@ -82,17 +82,6 @@ impl TextAttributes { self.inner.retain(|k, _| k != &key); } - // pub fn block_attributes_except_header(attributes: &Attributes) -> Attributes - // { let mut new_attributes = Attributes::new(); - // attributes.iter().for_each(|(k, v)| { - // if k != &AttributeKey::Header { - // new_attributes.insert(k.clone(), v.clone()); - // } - // }); - // - // new_attributes - // } - // Update inner by constructing new attributes from the other if it's // not None and replace the key/value with self key/value. pub fn merge(&mut self, other: Option) { @@ -257,7 +246,6 @@ impl std::convert::From for TextAttributes { } #[derive(Clone, Debug, Display, Hash, Eq, PartialEq, serde::Serialize, serde::Deserialize)] -// serde.rs/variant-attrs.html // #[serde(rename_all = "snake_case")] pub enum TextAttributeKey { #[serde(rename = "bold")] @@ -298,7 +286,6 @@ pub enum TextAttributeKey { Header, } -// pub trait AttributeValueData<'a>: Serialize + Deserialize<'a> {} #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct TextAttributeValue(pub Option); diff --git a/shared-lib/lib-ot/tests/node/editor_test.rs b/shared-lib/lib-ot/tests/node/editor_test.rs new file mode 100644 index 0000000000..9cb4b5d98f --- /dev/null +++ b/shared-lib/lib-ot/tests/node/editor_test.rs @@ -0,0 +1,162 @@ +use super::script::{NodeScript::*, *}; +use lib_ot::{ + core::{NodeData, Path}, + rich_text::{AttributeBuilder, RichTextDeltaBuilder, TextAttribute, TextAttributes}, +}; + +#[test] +fn appflowy_editor_deserialize_node_test() { + let mut test = NodeTest::new(); + let node: NodeData = serde_json::from_str(EXAMPLE_JSON).unwrap(); + let path: Path = 0.into(); + + let expected_delta = RichTextDeltaBuilder::new() + .insert("👋 ") + .insert_with_attributes( + "Welcome to ", + AttributeBuilder::new().add_attr(TextAttribute::Bold(true)).build(), + ) + .insert_with_attributes( + "AppFlowy Editor", + AttributeBuilder::new().add_attr(TextAttribute::Italic(true)).build(), + ) + .build(); + + test.run_scripts(vec![ + InsertNode { + path: path.clone(), + node: node.clone(), + }, + AssertNumberOfNodesAtPath { path: None, len: 1 }, + AssertNumberOfNodesAtPath { + path: Some(0.into()), + len: 14, + }, + AssertNumberOfNodesAtPath { + path: Some(0.into()), + len: 14, + }, + AssertNodeDelta { + path: vec![0, 1].into(), + expected: expected_delta, + }, + AssertNode { + path: vec![0, 0].into(), + expected: Some(node.children[0].clone()), + }, + AssertNode { + path: vec![0, 3].into(), + expected: Some(node.children[3].clone()), + }, + ]); +} + +#[allow(dead_code)] +const EXAMPLE_JSON: &str = r#" +{ + "type": "editor", + "children": [ + { + "type": "image", + "attributes": { + "image_src": "https://s1.ax1x.com/2022/08/26/v2sSbR.jpg", + "align": "center" + } + }, + { + "type": "text", + "attributes": { + "subtype": "heading", + "heading": "h1" + }, + "body": { + "delta": [ + { + "insert": "👋 " + }, + { + "insert": "Welcome to ", + "attributes": { + "bold": true + } + }, + { + "insert": "AppFlowy Editor", + "attributes": { + "italic": true + } + } + ] + } + }, + { "type": "text", "delta": [] }, + { + "type": "text", + "body": { + "delta": [ + { "insert": "AppFlowy Editor is a " }, + { "insert": "highly customizable", "attributes": { "bold": true } }, + { "insert": " " }, + { "insert": "rich-text editor", "attributes": { "italic": true } }, + { "insert": " for " }, + { "insert": "Flutter", "attributes": { "underline": true } } + ] + } + }, + { + "type": "text", + "attributes": { "checkbox": true, "subtype": "checkbox" }, + "body": { + "delta": [{ "insert": "Customizable" }] + } + }, + { + "type": "text", + "attributes": { "checkbox": true, "subtype": "checkbox" }, + "delta": [{ "insert": "Test-covered" }] + }, + { + "type": "text", + "attributes": { "checkbox": false, "subtype": "checkbox" }, + "delta": [{ "insert": "more to come!" }] + }, + { "type": "text", "delta": [] }, + { + "type": "text", + "attributes": { "subtype": "quote" }, + "delta": [{ "insert": "Here is an exmaple you can give it a try" }] + }, + { "type": "text", "delta": [] }, + { + "type": "text", + "delta": [ + { "insert": "You can also use " }, + { + "insert": "AppFlowy Editor", + "attributes": { + "italic": true, + "bold": true, + "backgroundColor": "0x6000BCF0" + } + }, + { "insert": " as a component to build your own app." } + ] + }, + { "type": "text", "delta": [] }, + { + "type": "text", + "attributes": { "subtype": "bulleted-list" }, + "delta": [{ "insert": "Use / to insert blocks" }] + }, + { + "type": "text", + "attributes": { "subtype": "bulleted-list" }, + "delta": [ + { + "insert": "Select text to trigger to the toolbar to format your notes." + } + ] + } + ] +} +"#; diff --git a/shared-lib/lib-ot/tests/node/mod.rs b/shared-lib/lib-ot/tests/node/mod.rs index 63d424afaf..dddad56eb5 100644 --- a/shared-lib/lib-ot/tests/node/mod.rs +++ b/shared-lib/lib-ot/tests/node/mod.rs @@ -1,2 +1,4 @@ +mod editor_test; +mod operation_test; mod script; -mod test; +mod tree_test; diff --git a/shared-lib/lib-ot/tests/node/operation_test.rs b/shared-lib/lib-ot/tests/node/operation_test.rs new file mode 100644 index 0000000000..87a760b904 --- /dev/null +++ b/shared-lib/lib-ot/tests/node/operation_test.rs @@ -0,0 +1,70 @@ +use lib_ot::{ + core::{NodeAttributeBuilder, NodeBodyChangeset, NodeData, NodeDataBuilder, NodeOperation, Path}, + rich_text::RichTextDeltaBuilder, +}; + +#[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 = NodeDataBuilder::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_serialize_test() { + let delta = RichTextDeltaBuilder::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":"update-body","path":[0,1],"changeset":{"delta":{"delta":[{"insert":"AppFlowy..."}],"inverted":[{"delete":11}]}}}"# + ); + // +} + +#[test] +fn operation_update_node_body_deserialize_test() { + let json_1 = r#"{"op":"update-body","path":[0,1],"changeset":{"delta":{"delta":[{"insert":"AppFlowy..."}],"inverted":[{"delete":11}]}}}"#; + let operation: NodeOperation = serde_json::from_str(json_1).unwrap(); + let json_2 = serde_json::to_string(&operation).unwrap(); + assert_eq!(json_1, json_2); +} diff --git a/shared-lib/lib-ot/tests/node/script.rs b/shared-lib/lib-ot/tests/node/script.rs index 91f36d0e8b..0879b22c1d 100644 --- a/shared-lib/lib-ot/tests/node/script.rs +++ b/shared-lib/lib-ot/tests/node/script.rs @@ -1,11 +1,16 @@ -use lib_ot::core::{NodeAttributes, NodeData, NodeTree, Path, TransactionBuilder}; +use lib_ot::{ + core::{NodeAttributes, NodeBody, NodeBodyChangeset, NodeData, NodeTree, Path, TransactionBuilder}, + rich_text::RichTextDelta, +}; 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, len: usize }, + AssertNumberOfNodesAtPath { path: Option, len: usize }, AssertNode { path: Path, expected: Option }, + AssertNodeDelta { path: Path, expected: RichTextDelta }, } pub struct NodeTest { @@ -15,7 +20,7 @@ pub struct NodeTest { impl NodeTest { pub fn new() -> Self { Self { - node_tree: NodeTree::new(), + node_tree: NodeTree::new("root"), } } @@ -34,12 +39,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 +59,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()), @@ -57,7 +69,7 @@ impl NodeTest { } } } - NodeScript::AssertNumberOfChildrenAtPath { + NodeScript::AssertNumberOfNodesAtPath { path, len: expected_len, } => match path { @@ -66,11 +78,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"); + } + } } } } diff --git a/shared-lib/lib-ot/tests/node/test.rs b/shared-lib/lib-ot/tests/node/tree_test.rs similarity index 77% rename from shared-lib/lib-ot/tests/node/test.rs rename to shared-lib/lib-ot/tests/node/tree_test.rs index 6def19a025..b1a954055f 100644 --- a/shared-lib/lib-ot/tests/node/test.rs +++ b/shared-lib/lib-ot/tests/node/tree_test.rs @@ -1,6 +1,10 @@ use crate::node::script::NodeScript::*; use crate::node::script::NodeTest; -use lib_ot::core::{NodeBuilder, NodeData, Path}; +use lib_ot::core::NodeBody; +use lib_ot::core::NodeBodyChangeset; +use lib_ot::core::OperationTransform; +use lib_ot::core::{NodeData, NodeDataBuilder, Path}; +use lib_ot::rich_text::RichTextDeltaBuilder; #[test] fn node_insert_test() { @@ -23,7 +27,7 @@ fn node_insert_test() { #[test] fn node_insert_node_with_children_test() { let mut test = NodeTest::new(); - let inserted_node = NodeBuilder::new("text").add_node(NodeData::new("image")).build(); + let inserted_node = NodeDataBuilder::new("text").add_node(NodeData::new("image")).build(); let path: Path = 0.into(); let scripts = vec![ InsertNode { @@ -129,7 +133,7 @@ fn node_insert_node_in_ordered_nodes_test() { path: path_4, expected: Some(node_3), }, - AssertNumberOfChildrenAtPath { path: None, len: 4 }, + AssertNumberOfNodesAtPath { path: None, len: 4 }, ]; test.run_scripts(scripts); } @@ -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 = RichTextDeltaBuilder::new().insert(&s).build(); + let delta = RichTextDeltaBuilder::new().retain(s.len()).insert(" AppFlowy").build(); + let inverted = delta.invert(&init_delta); + let expected = init_delta.compose(&delta).unwrap(); + + let node = NodeDataBuilder::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); +}