mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
Feat/op compose (#1392)
This commit is contained in:
parent
95fdfd7da2
commit
783fd40f63
@ -2,9 +2,7 @@ use bytes::Bytes;
|
||||
use flowy_error::{FlowyError, FlowyResult};
|
||||
use flowy_revision::{RevisionCompress, RevisionObjectDeserializer, RevisionObjectSerializer};
|
||||
use flowy_sync::entities::revision::Revision;
|
||||
use lib_ot::core::{
|
||||
Body, Extension, NodeDataBuilder, NodeOperation, NodeTree, NodeTreeContext, Selection, Transaction,
|
||||
};
|
||||
use lib_ot::core::{Extension, NodeDataBuilder, NodeOperation, NodeTree, NodeTreeContext, Selection, Transaction};
|
||||
use lib_ot::text_delta::DeltaTextOperationBuilder;
|
||||
|
||||
#[derive(Debug)]
|
||||
@ -46,7 +44,7 @@ pub(crate) fn make_tree_context() -> NodeTreeContext {
|
||||
|
||||
pub fn initial_document_content() -> String {
|
||||
let delta = DeltaTextOperationBuilder::new().insert("").build();
|
||||
let node_data = NodeDataBuilder::new("text").insert_body(Body::Delta(delta)).build();
|
||||
let node_data = NodeDataBuilder::new("text").insert_delta(delta).build();
|
||||
let editor_node = NodeDataBuilder::new("editor").add_node_data(node_data).build();
|
||||
let node_operation = NodeOperation::Insert {
|
||||
path: vec![0].into(),
|
||||
|
@ -117,7 +117,8 @@ impl DocumentTransaction {
|
||||
|
||||
impl std::convert::From<Transaction> for DocumentTransaction {
|
||||
fn from(transaction: Transaction) -> Self {
|
||||
let (before_selection, after_selection) = match transaction.extension {
|
||||
let (operations, extension) = transaction.split();
|
||||
let (before_selection, after_selection) = match extension {
|
||||
Extension::Empty => (Selection::default(), Selection::default()),
|
||||
Extension::TextSelection {
|
||||
before_selection,
|
||||
@ -126,9 +127,7 @@ impl std::convert::From<Transaction> for DocumentTransaction {
|
||||
};
|
||||
|
||||
DocumentTransaction {
|
||||
operations: transaction
|
||||
.operations
|
||||
.into_inner()
|
||||
operations: operations
|
||||
.into_iter()
|
||||
.map(|operation| operation.as_ref().into())
|
||||
.collect(),
|
||||
@ -139,19 +138,16 @@ impl std::convert::From<Transaction> for DocumentTransaction {
|
||||
}
|
||||
|
||||
impl std::convert::From<DocumentTransaction> for Transaction {
|
||||
fn from(transaction: DocumentTransaction) -> Self {
|
||||
Transaction {
|
||||
operations: transaction
|
||||
.operations
|
||||
.into_iter()
|
||||
.map(|operation| operation.into())
|
||||
.collect::<Vec<NodeOperation>>()
|
||||
.into(),
|
||||
extension: Extension::TextSelection {
|
||||
before_selection: transaction.before_selection,
|
||||
after_selection: transaction.after_selection,
|
||||
},
|
||||
fn from(document_transaction: DocumentTransaction) -> Self {
|
||||
let mut transaction = Transaction::new();
|
||||
for document_operation in document_transaction.operations {
|
||||
transaction.push_operation(document_operation);
|
||||
}
|
||||
transaction.extension = Extension::TextSelection {
|
||||
before_selection: document_transaction.before_selection,
|
||||
after_selection: document_transaction.after_selection,
|
||||
};
|
||||
transaction
|
||||
}
|
||||
}
|
||||
|
||||
@ -374,6 +370,17 @@ mod tests {
|
||||
let _ = serde_json::to_string_pretty(&document).unwrap();
|
||||
}
|
||||
|
||||
// #[test]
|
||||
// fn document_operation_compose_test() {
|
||||
// let json = include_str!("./test.json");
|
||||
// let transaction: Transaction = Transaction::from_json(json).unwrap();
|
||||
// let json = transaction.to_json().unwrap();
|
||||
// // let transaction: Transaction = Transaction::from_json(&json).unwrap();
|
||||
// let document = Document::from_transaction(transaction).unwrap();
|
||||
// let content = document.get_content(false).unwrap();
|
||||
// println!("{}", json);
|
||||
// }
|
||||
|
||||
const EXAMPLE_DOCUMENT: &str = r#"{
|
||||
"document": {
|
||||
"type": "editor",
|
||||
|
@ -89,7 +89,7 @@ fn delta_deserialize_null_test() {
|
||||
let delta1 = DeltaTextOperations::from_json(json).unwrap();
|
||||
|
||||
let mut attribute = BuildInTextAttribute::Bold(true);
|
||||
attribute.remove_value();
|
||||
attribute.clear();
|
||||
|
||||
let delta2 = DeltaOperationBuilder::new()
|
||||
.retain_with_attributes(7, attribute.into())
|
||||
|
@ -3,7 +3,7 @@ use flowy_document::editor::{AppFlowyDocumentEditor, Document, DocumentTransacti
|
||||
use flowy_document::entities::DocumentVersionPB;
|
||||
use flowy_test::helper::ViewTest;
|
||||
use flowy_test::FlowySDKTest;
|
||||
use lib_ot::core::{Body, Changeset, NodeDataBuilder, NodeOperation, Path, Transaction};
|
||||
use lib_ot::core::{Changeset, NodeDataBuilder, NodeOperation, Path, Transaction};
|
||||
use lib_ot::text_delta::DeltaTextOperations;
|
||||
use std::sync::Arc;
|
||||
|
||||
@ -64,7 +64,7 @@ impl DocumentEditorTest {
|
||||
async fn run_script(&self, script: EditScript) {
|
||||
match script {
|
||||
EditScript::InsertText { path, delta } => {
|
||||
let node_data = NodeDataBuilder::new("text").insert_body(Body::Delta(delta)).build();
|
||||
let node_data = NodeDataBuilder::new("text").insert_delta(delta).build();
|
||||
let operation = NodeOperation::Insert {
|
||||
path,
|
||||
nodes: vec![node_data],
|
||||
|
@ -119,7 +119,6 @@ fn read_workspaces_on_server(
|
||||
let workspace_revs = server.read_workspace(&token, params).await?;
|
||||
let _ = persistence
|
||||
.begin_transaction(|transaction| {
|
||||
tracing::trace!("Save {} workspace", workspace_revs.len());
|
||||
for workspace_rev in &workspace_revs {
|
||||
let m_workspace = workspace_rev.clone();
|
||||
let app_revs = m_workspace.apps.clone();
|
||||
|
@ -12,7 +12,14 @@ pub struct AttributeEntry {
|
||||
}
|
||||
|
||||
impl AttributeEntry {
|
||||
pub fn remove_value(&mut self) {
|
||||
pub fn new<K: Into<AttributeKey>, V: Into<AttributeValue>>(key: K, value: V) -> Self {
|
||||
Self {
|
||||
key: key.into(),
|
||||
value: value.into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clear(&mut self) {
|
||||
self.value.ty = None;
|
||||
self.value.value = None;
|
||||
}
|
||||
|
@ -3,6 +3,7 @@
|
||||
mod node;
|
||||
mod node_serde;
|
||||
mod operation;
|
||||
mod operation_serde;
|
||||
mod path;
|
||||
mod transaction;
|
||||
mod transaction_serde;
|
||||
|
@ -1,7 +1,7 @@
|
||||
use super::node_serde::*;
|
||||
use crate::core::attributes::{AttributeHashMap, AttributeKey, AttributeValue};
|
||||
use crate::core::Body::Delta;
|
||||
use crate::core::OperationTransform;
|
||||
use crate::core::{AttributeEntry, OperationTransform};
|
||||
use crate::errors::OTError;
|
||||
use crate::text_delta::DeltaTextOperations;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@ -69,14 +69,18 @@ impl NodeDataBuilder {
|
||||
/// Inserts attributes to the builder's node.
|
||||
///
|
||||
/// The attributes will be replace if they shared the same key
|
||||
pub fn insert_attribute(mut self, key: AttributeKey, value: AttributeValue) -> Self {
|
||||
self.node.attributes.insert(key, value);
|
||||
pub fn insert_attribute<K: Into<AttributeKey>, V: Into<AttributeValue>>(mut self, key: K, value: V) -> Self {
|
||||
self.node.attributes.insert(key.into(), value);
|
||||
self
|
||||
}
|
||||
|
||||
/// Inserts a body to the builder's node
|
||||
pub fn insert_body(mut self, body: Body) -> Self {
|
||||
self.node.body = body;
|
||||
pub fn insert_attribute_entry(mut self, entry: AttributeEntry) -> Self {
|
||||
self.node.attributes.insert_entry(entry);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn insert_delta(mut self, delta: DeltaTextOperations) -> Self {
|
||||
self.node.body = Body::Delta(delta);
|
||||
self
|
||||
}
|
||||
|
||||
@ -174,6 +178,18 @@ pub enum Changeset {
|
||||
}
|
||||
|
||||
impl Changeset {
|
||||
pub fn is_delta(&self) -> bool {
|
||||
match self {
|
||||
Changeset::Delta { .. } => true,
|
||||
Changeset::Attributes { .. } => false,
|
||||
}
|
||||
}
|
||||
pub fn is_attribute(&self) -> bool {
|
||||
match self {
|
||||
Changeset::Delta { .. } => false,
|
||||
Changeset::Attributes { .. } => true,
|
||||
}
|
||||
}
|
||||
pub fn inverted(&self) -> Changeset {
|
||||
match self {
|
||||
Changeset::Delta { delta, inverted } => Changeset::Delta {
|
||||
@ -186,6 +202,41 @@ impl Changeset {
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn compose(&mut self, other: &Changeset) -> Result<(), OTError> {
|
||||
match (self, other) {
|
||||
(
|
||||
Changeset::Delta { delta, inverted },
|
||||
Changeset::Delta {
|
||||
delta: other_delta,
|
||||
inverted: _,
|
||||
},
|
||||
) => {
|
||||
let original = delta.invert(inverted);
|
||||
let new_delta = delta.compose(other_delta)?;
|
||||
let new_inverted = new_delta.invert(&original);
|
||||
|
||||
*delta = new_delta;
|
||||
*inverted = new_inverted;
|
||||
Ok(())
|
||||
}
|
||||
(
|
||||
Changeset::Attributes { new, old },
|
||||
Changeset::Attributes {
|
||||
new: other_new,
|
||||
old: other_old,
|
||||
},
|
||||
) => {
|
||||
*new = other_new.clone();
|
||||
*old = other_old.clone();
|
||||
Ok(())
|
||||
}
|
||||
(left, right) => {
|
||||
let err = format!("Compose changeset failed. {:?} can't compose {:?}", left, right);
|
||||
Err(OTError::compose().context(err))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// [`Node`] represents as a leaf in the [`NodeTree`].
|
||||
|
@ -1,5 +1,6 @@
|
||||
use crate::core::{Changeset, NodeData, Path};
|
||||
use crate::core::{Body, Changeset, NodeData, OperationTransform, Path};
|
||||
use crate::errors::OTError;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
|
||||
@ -33,7 +34,80 @@ impl NodeOperation {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn invert(&self) -> NodeOperation {
|
||||
pub fn is_update_delta(&self) -> bool {
|
||||
match self {
|
||||
NodeOperation::Insert { .. } => false,
|
||||
NodeOperation::Update { path: _, changeset } => changeset.is_delta(),
|
||||
NodeOperation::Delete { .. } => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_update_attribute(&self) -> bool {
|
||||
match self {
|
||||
NodeOperation::Insert { .. } => false,
|
||||
NodeOperation::Update { path: _, changeset } => changeset.is_attribute(),
|
||||
NodeOperation::Delete { .. } => false,
|
||||
}
|
||||
}
|
||||
pub fn is_insert(&self) -> bool {
|
||||
match self {
|
||||
NodeOperation::Insert { .. } => true,
|
||||
NodeOperation::Update { .. } => false,
|
||||
NodeOperation::Delete { .. } => false,
|
||||
}
|
||||
}
|
||||
pub fn can_compose(&self, other: &NodeOperation) -> bool {
|
||||
if self.get_path() != other.get_path() {
|
||||
return false;
|
||||
}
|
||||
if self.is_update_delta() && other.is_update_delta() {
|
||||
return true;
|
||||
}
|
||||
|
||||
if self.is_update_attribute() && other.is_update_attribute() {
|
||||
return true;
|
||||
}
|
||||
|
||||
if self.is_insert() && other.is_update_delta() {
|
||||
return true;
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
pub fn compose(&mut self, other: &NodeOperation) -> Result<(), OTError> {
|
||||
match (self, other) {
|
||||
(
|
||||
NodeOperation::Insert { path: _, nodes },
|
||||
NodeOperation::Update {
|
||||
path: _other_path,
|
||||
changeset,
|
||||
},
|
||||
) => {
|
||||
match changeset {
|
||||
Changeset::Delta { delta, inverted: _ } => {
|
||||
if let Body::Delta(old_delta) = &mut nodes.last_mut().unwrap().body {
|
||||
let new_delta = old_delta.compose(delta)?;
|
||||
*old_delta = new_delta;
|
||||
}
|
||||
}
|
||||
Changeset::Attributes { new: _, old: _ } => {
|
||||
return Err(OTError::compose().context("Can't compose the attributes changeset"));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
(
|
||||
NodeOperation::Update { path: _, changeset },
|
||||
NodeOperation::Update {
|
||||
path: _,
|
||||
changeset: other_changeset,
|
||||
},
|
||||
) => changeset.compose(other_changeset),
|
||||
(_left, _right) => Err(OTError::compose().context("Can't compose the operation")),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn inverted(&self) -> NodeOperation {
|
||||
match self {
|
||||
NodeOperation::Insert { path, nodes } => NodeOperation::Delete {
|
||||
path: path.clone(),
|
||||
@ -99,52 +173,24 @@ impl NodeOperation {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
type OperationIndexMap = Vec<Arc<NodeOperation>>;
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct NodeOperations {
|
||||
operations: Vec<Arc<NodeOperation>>,
|
||||
inner: OperationIndexMap,
|
||||
}
|
||||
|
||||
impl NodeOperations {
|
||||
pub fn into_inner(self) -> Vec<Arc<NodeOperation>> {
|
||||
self.operations
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub fn push_op(&mut self, operation: NodeOperation) {
|
||||
self.operations.push(Arc::new(operation));
|
||||
}
|
||||
|
||||
pub fn extend(&mut self, other: NodeOperations) {
|
||||
for operation in other.operations {
|
||||
self.operations.push(operation);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Deref for NodeOperations {
|
||||
type Target = Vec<Arc<NodeOperation>>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.operations
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::DerefMut for NodeOperations {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.operations
|
||||
}
|
||||
}
|
||||
|
||||
impl std::convert::From<Vec<NodeOperation>> for NodeOperations {
|
||||
fn from(operations: Vec<NodeOperation>) -> Self {
|
||||
Self::new(operations)
|
||||
}
|
||||
}
|
||||
|
||||
impl NodeOperations {
|
||||
pub fn new(operations: Vec<NodeOperation>) -> Self {
|
||||
Self {
|
||||
operations: operations.into_iter().map(Arc::new).collect(),
|
||||
pub fn from_operations(operations: Vec<NodeOperation>) -> Self {
|
||||
let mut ops = Self::new();
|
||||
for op in operations {
|
||||
ops.push_op(op)
|
||||
}
|
||||
ops
|
||||
}
|
||||
|
||||
pub fn from_bytes(bytes: Vec<u8>) -> Result<Self, OTError> {
|
||||
@ -156,4 +202,69 @@ impl NodeOperations {
|
||||
let bytes = serde_json::to_vec(self).map_err(|err| OTError::serde().context(err))?;
|
||||
Ok(bytes)
|
||||
}
|
||||
|
||||
pub fn values(&self) -> &Vec<Arc<NodeOperation>> {
|
||||
&self.inner
|
||||
}
|
||||
|
||||
pub fn values_mut(&mut self) -> &mut Vec<Arc<NodeOperation>> {
|
||||
&mut self.inner
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.values().len()
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.inner.is_empty()
|
||||
}
|
||||
|
||||
pub fn into_inner(self) -> Vec<Arc<NodeOperation>> {
|
||||
self.inner
|
||||
}
|
||||
|
||||
pub fn push_op<T: Into<Arc<NodeOperation>>>(&mut self, other: T) {
|
||||
let other = other.into();
|
||||
if let Some(last_operation) = self.inner.last_mut() {
|
||||
if last_operation.can_compose(&other) {
|
||||
let mut_operation = Arc::make_mut(last_operation);
|
||||
if mut_operation.compose(&other).is_ok() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// if let Some(operations) = self.inner.get_mut(other.get_path()) {
|
||||
// if let Some(last_operation) = operations.last_mut() {
|
||||
// if last_operation.can_compose(&other) {
|
||||
// let mut_operation = Arc::make_mut(last_operation);
|
||||
// if mut_operation.compose(&other).is_ok() {
|
||||
// return;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// If the passed-in operation can't be composed, then append it to the end.
|
||||
self.inner.push(other);
|
||||
}
|
||||
|
||||
pub fn compose(&mut self, other: NodeOperations) {
|
||||
for operation in other.values() {
|
||||
self.push_op(operation.clone());
|
||||
}
|
||||
}
|
||||
|
||||
pub fn inverted(&self) -> Self {
|
||||
let mut operations = Self::new();
|
||||
for operation in self.values() {
|
||||
operations.push_op(operation.inverted());
|
||||
}
|
||||
operations
|
||||
}
|
||||
}
|
||||
|
||||
impl std::convert::From<Vec<NodeOperation>> for NodeOperations {
|
||||
fn from(operations: Vec<NodeOperation>) -> Self {
|
||||
Self::from_operations(operations)
|
||||
}
|
||||
}
|
||||
|
@ -1,196 +1,49 @@
|
||||
use crate::core::{AttributeHashMap, Changeset, Path};
|
||||
use crate::text_delta::TextOperations;
|
||||
use serde::de::{self, MapAccess, Visitor};
|
||||
use serde::ser::SerializeMap;
|
||||
use serde::{Deserializer, Serializer};
|
||||
use std::convert::TryInto;
|
||||
use crate::core::{NodeOperation, NodeOperations};
|
||||
use serde::de::{SeqAccess, Visitor};
|
||||
use serde::ser::SerializeSeq;
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
use std::fmt;
|
||||
use std::marker::PhantomData;
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn serialize_changeset<S>(path: &Path, changeset: &Changeset, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
let mut map = serializer.serialize_map(Some(3))?;
|
||||
map.serialize_key("path")?;
|
||||
map.serialize_value(path)?;
|
||||
|
||||
match changeset {
|
||||
Changeset::Delta { delta, inverted } => {
|
||||
map.serialize_key("delta")?;
|
||||
map.serialize_value(delta)?;
|
||||
map.serialize_key("inverted")?;
|
||||
map.serialize_value(inverted)?;
|
||||
map.end()
|
||||
}
|
||||
Changeset::Attributes { new, old } => {
|
||||
map.serialize_key("new")?;
|
||||
map.serialize_value(new)?;
|
||||
map.serialize_key("old")?;
|
||||
map.serialize_value(old)?;
|
||||
map.end()
|
||||
impl Serialize for NodeOperations {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
let operations = self.values();
|
||||
let mut seq = serializer.serialize_seq(Some(operations.len()))?;
|
||||
for operation in operations {
|
||||
let _ = seq.serialize_element(&operation)?;
|
||||
}
|
||||
seq.end()
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn deserialize_changeset<'de, D>(deserializer: D) -> Result<(Path, Changeset), D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
struct ChangesetVisitor();
|
||||
impl<'de> Deserialize<'de> for NodeOperations {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
struct NodeOperationsVisitor();
|
||||
|
||||
impl<'de> Visitor<'de> for ChangesetVisitor {
|
||||
type Value = (Path, Changeset);
|
||||
impl<'de> Visitor<'de> for NodeOperationsVisitor {
|
||||
type Value = NodeOperations;
|
||||
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||
formatter.write_str("Expect Path and Changeset")
|
||||
}
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||
formatter.write_str("Expected node operation")
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn visit_map<V>(self, mut map: V) -> Result<Self::Value, V::Error>
|
||||
where
|
||||
V: MapAccess<'de>,
|
||||
{
|
||||
let mut path: Option<Path> = None;
|
||||
let mut delta_changeset = DeltaChangeset::<V::Error>::new();
|
||||
let mut attribute_changeset = AttributeChangeset::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::<Path>()?)
|
||||
}
|
||||
"new" => {
|
||||
if attribute_changeset.new.is_some() {
|
||||
return Err(de::Error::duplicate_field("new"));
|
||||
}
|
||||
attribute_changeset.new = Some(map.next_value()?);
|
||||
}
|
||||
"old" => {
|
||||
if attribute_changeset.old.is_some() {
|
||||
return Err(de::Error::duplicate_field("old"));
|
||||
}
|
||||
attribute_changeset.old = Some(map.next_value()?);
|
||||
}
|
||||
other => {
|
||||
tracing::warn!("Unexpected key: {}", other);
|
||||
panic!()
|
||||
}
|
||||
fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
|
||||
where
|
||||
A: SeqAccess<'de>,
|
||||
{
|
||||
let mut operations = NodeOperations::new();
|
||||
while let Some(operation) = seq.next_element::<NodeOperation>()? {
|
||||
operations.push_op(operation);
|
||||
}
|
||||
Ok(operations)
|
||||
}
|
||||
if path.is_none() {
|
||||
return Err(de::Error::missing_field("path"));
|
||||
}
|
||||
|
||||
let mut changeset: Changeset;
|
||||
if !delta_changeset.is_empty() {
|
||||
changeset = delta_changeset.try_into()?
|
||||
} else {
|
||||
changeset = attribute_changeset.try_into()?;
|
||||
}
|
||||
|
||||
Ok((path.unwrap(), changeset))
|
||||
}
|
||||
}
|
||||
deserializer.deserialize_any(ChangesetVisitor())
|
||||
}
|
||||
|
||||
struct DeltaChangeset<E> {
|
||||
delta: Option<TextOperations>,
|
||||
inverted: Option<TextOperations>,
|
||||
error: PhantomData<E>,
|
||||
}
|
||||
|
||||
impl<E> DeltaChangeset<E> {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
delta: None,
|
||||
inverted: None,
|
||||
error: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
fn is_empty(&self) -> bool {
|
||||
self.delta.is_none() && self.inverted.is_none()
|
||||
}
|
||||
}
|
||||
|
||||
impl<E> std::convert::TryInto<Changeset> for DeltaChangeset<E>
|
||||
where
|
||||
E: de::Error,
|
||||
{
|
||||
type Error = E;
|
||||
|
||||
fn try_into(self) -> Result<Changeset, Self::Error> {
|
||||
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 = Changeset::Delta {
|
||||
delta: self.delta.unwrap(),
|
||||
inverted: self.inverted.unwrap(),
|
||||
};
|
||||
|
||||
Ok(changeset)
|
||||
}
|
||||
}
|
||||
struct AttributeChangeset<E> {
|
||||
new: Option<AttributeHashMap>,
|
||||
old: Option<AttributeHashMap>,
|
||||
error: PhantomData<E>,
|
||||
}
|
||||
|
||||
impl<E> AttributeChangeset<E> {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
new: Default::default(),
|
||||
old: Default::default(),
|
||||
error: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
fn is_empty(&self) -> bool {
|
||||
self.new.is_none() && self.old.is_none()
|
||||
}
|
||||
}
|
||||
|
||||
impl<E> std::convert::TryInto<Changeset> for AttributeChangeset<E>
|
||||
where
|
||||
E: de::Error,
|
||||
{
|
||||
type Error = E;
|
||||
|
||||
fn try_into(self) -> Result<Changeset, Self::Error> {
|
||||
if self.new.is_none() {
|
||||
return Err(de::Error::missing_field("new"));
|
||||
}
|
||||
|
||||
if self.old.is_none() {
|
||||
return Err(de::Error::missing_field("old"));
|
||||
}
|
||||
|
||||
Ok(Changeset::Attributes {
|
||||
new: self.new.unwrap(),
|
||||
old: self.old.unwrap(),
|
||||
})
|
||||
deserializer.deserialize_any(NodeOperationsVisitor())
|
||||
}
|
||||
}
|
||||
|
@ -23,7 +23,7 @@ use serde::{Deserialize, Serialize};
|
||||
/// The path of Node A-1 will be [0,0]
|
||||
/// The path of Node A-2 will be [0,1]
|
||||
/// The path of Node B-2 will be [1,1]
|
||||
#[derive(Clone, Serialize, Deserialize, Eq, PartialEq, Debug, Default)]
|
||||
#[derive(Clone, Serialize, Deserialize, Eq, PartialEq, Debug, Default, Hash)]
|
||||
pub struct Path(pub Vec<usize>);
|
||||
|
||||
impl Path {
|
||||
|
@ -1,13 +1,12 @@
|
||||
use super::{Changeset, NodeOperations};
|
||||
use crate::core::attributes::AttributeHashMap;
|
||||
use crate::core::{NodeData, NodeOperation, NodeTree, Path};
|
||||
use crate::errors::OTError;
|
||||
use indextree::NodeId;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
pub struct Transaction {
|
||||
#[serde(flatten)]
|
||||
pub operations: NodeOperations,
|
||||
|
||||
#[serde(default)]
|
||||
@ -37,6 +36,16 @@ impl Transaction {
|
||||
Ok(bytes)
|
||||
}
|
||||
|
||||
pub fn from_json(s: &str) -> Result<Self, OTError> {
|
||||
let serde_transaction: Transaction = serde_json::from_str(s).map_err(|err| OTError::serde().context(err))?;
|
||||
let mut transaction = Self::new();
|
||||
transaction.extension = serde_transaction.extension;
|
||||
for operation in serde_transaction.operations.into_inner() {
|
||||
transaction.operations.push_op(operation);
|
||||
}
|
||||
Ok(transaction)
|
||||
}
|
||||
|
||||
pub fn to_json(&self) -> Result<String, OTError> {
|
||||
serde_json::to_string(&self).map_err(|err| OTError::serde().context(err))
|
||||
}
|
||||
@ -45,6 +54,10 @@ impl Transaction {
|
||||
self.operations.into_inner()
|
||||
}
|
||||
|
||||
pub fn split(self) -> (Vec<Arc<NodeOperation>>, Extension) {
|
||||
(self.operations.into_inner(), self.extension)
|
||||
}
|
||||
|
||||
pub fn push_operation<T: Into<NodeOperation>>(&mut self, operation: T) {
|
||||
let operation = operation.into();
|
||||
self.operations.push_op(operation);
|
||||
@ -57,38 +70,26 @@ impl Transaction {
|
||||
pub fn transform(&self, other: &Transaction) -> Result<Transaction, OTError> {
|
||||
let mut other = other.clone();
|
||||
other.extension = self.extension.clone();
|
||||
for other_operation in other.iter_mut() {
|
||||
|
||||
for other_operation in other.operations.values_mut() {
|
||||
let other_operation = Arc::make_mut(other_operation);
|
||||
for operation in self.operations.iter() {
|
||||
for operation in self.operations.values() {
|
||||
operation.transform(other_operation);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(other)
|
||||
}
|
||||
|
||||
pub fn compose(&mut self, other: Transaction) -> Result<(), OTError> {
|
||||
// For the moment, just append `other` operations to the end of `self`.
|
||||
let Transaction { operations, extension } = other;
|
||||
self.operations.extend(operations);
|
||||
self.operations.compose(operations);
|
||||
self.extension = extension;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Deref for Transaction {
|
||||
type Target = Vec<Arc<NodeOperation>>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.operations
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::DerefMut for Transaction {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.operations
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum Extension {
|
||||
Empty,
|
||||
@ -121,18 +122,14 @@ pub struct Position {
|
||||
path: Path,
|
||||
offset: usize,
|
||||
}
|
||||
|
||||
pub struct TransactionBuilder<'a> {
|
||||
node_tree: &'a NodeTree,
|
||||
#[derive(Default)]
|
||||
pub struct TransactionBuilder {
|
||||
operations: NodeOperations,
|
||||
}
|
||||
|
||||
impl<'a> TransactionBuilder<'a> {
|
||||
pub fn new(node_tree: &'a NodeTree) -> TransactionBuilder {
|
||||
TransactionBuilder {
|
||||
node_tree,
|
||||
operations: NodeOperations::default(),
|
||||
}
|
||||
impl TransactionBuilder {
|
||||
pub fn new() -> TransactionBuilder {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
///
|
||||
@ -148,9 +145,9 @@ impl<'a> TransactionBuilder<'a> {
|
||||
/// // 0 -- text_1
|
||||
/// use lib_ot::core::{NodeTree, NodeData, TransactionBuilder};
|
||||
/// let mut node_tree = NodeTree::default();
|
||||
/// let transaction = TransactionBuilder::new(&node_tree)
|
||||
/// let transaction = TransactionBuilder::new()
|
||||
/// .insert_nodes_at_path(0,vec![ NodeData::new("text_1")])
|
||||
/// .finalize();
|
||||
/// .build();
|
||||
/// node_tree.apply_transaction(transaction).unwrap();
|
||||
///
|
||||
/// node_tree.node_id_at_path(vec![0]).unwrap();
|
||||
@ -178,9 +175,9 @@ impl<'a> TransactionBuilder<'a> {
|
||||
/// // |-- text
|
||||
/// use lib_ot::core::{NodeTree, NodeData, TransactionBuilder};
|
||||
/// let mut node_tree = NodeTree::default();
|
||||
/// let transaction = TransactionBuilder::new(&node_tree)
|
||||
/// let transaction = TransactionBuilder::new()
|
||||
/// .insert_node_at_path(0, NodeData::new("text"))
|
||||
/// .finalize();
|
||||
/// .build();
|
||||
/// node_tree.apply_transaction(transaction).unwrap();
|
||||
/// ```
|
||||
///
|
||||
@ -188,49 +185,50 @@ impl<'a> TransactionBuilder<'a> {
|
||||
self.insert_nodes_at_path(path, vec![node])
|
||||
}
|
||||
|
||||
pub fn update_attributes_at_path(mut self, path: &Path, attributes: AttributeHashMap) -> Self {
|
||||
match self.node_tree.get_node_at_path(path) {
|
||||
Some(node) => {
|
||||
let mut old_attributes = AttributeHashMap::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());
|
||||
}
|
||||
}
|
||||
|
||||
self.operations.push_op(NodeOperation::Update {
|
||||
path: path.clone(),
|
||||
changeset: Changeset::Attributes {
|
||||
new: attributes,
|
||||
old: old_attributes,
|
||||
},
|
||||
});
|
||||
}
|
||||
None => tracing::warn!("Update attributes at path: {:?} failed. Node is not exist", path),
|
||||
}
|
||||
pub fn update_node_at_path<T: Into<Path>>(mut self, path: T, changeset: Changeset) -> Self {
|
||||
self.operations.push_op(NodeOperation::Update {
|
||||
path: path.into(),
|
||||
changeset,
|
||||
});
|
||||
self
|
||||
}
|
||||
//
|
||||
// pub fn update_delta_at_path<T: Into<Path>>(
|
||||
// mut self,
|
||||
// path: T,
|
||||
// new_delta: DeltaTextOperations,
|
||||
// ) -> Result<Self, OTError> {
|
||||
// let path = path.into();
|
||||
// let operation: NodeOperation = self
|
||||
// .operations
|
||||
// .get(&path)
|
||||
// .ok_or(Err(OTError::record_not_found().context("Can't found the node")))?;
|
||||
//
|
||||
// match operation {
|
||||
// NodeOperation::Insert { path, nodes } => {}
|
||||
// NodeOperation::Update { path, changeset } => {}
|
||||
// NodeOperation::Delete { .. } => {}
|
||||
// }
|
||||
//
|
||||
// match node.body {
|
||||
// Body::Empty => Ok(self),
|
||||
// Body::Delta(delta) => {
|
||||
// let inverted = new_delta.invert(&delta);
|
||||
// let changeset = Changeset::Delta {
|
||||
// delta: new_delta,
|
||||
// inverted,
|
||||
// };
|
||||
// Ok(self.update_node_at_path(path, changeset))
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
pub fn update_body_at_path(mut self, path: &Path, changeset: Changeset) -> Self {
|
||||
match self.node_tree.node_id_at_path(path) {
|
||||
Some(_) => {
|
||||
self.operations.push_op(NodeOperation::Update {
|
||||
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, node_tree: &NodeTree, path: &Path) -> Self {
|
||||
self.delete_nodes_at_path(node_tree, path, 1)
|
||||
}
|
||||
|
||||
pub fn delete_node_at_path(self, path: &Path) -> Self {
|
||||
self.delete_nodes_at_path(path, 1)
|
||||
}
|
||||
|
||||
pub fn delete_nodes_at_path(mut self, path: &Path, length: usize) -> Self {
|
||||
let node_id = self.node_tree.node_id_at_path(path);
|
||||
pub fn delete_nodes_at_path(mut self, node_tree: &NodeTree, path: &Path, length: usize) -> Self {
|
||||
let node_id = node_tree.node_id_at_path(path);
|
||||
if node_id.is_none() {
|
||||
tracing::warn!("Path: {:?} doesn't contains any nodes", path);
|
||||
return self;
|
||||
@ -239,8 +237,8 @@ impl<'a> TransactionBuilder<'a> {
|
||||
let mut node_id = node_id.unwrap();
|
||||
let mut deleted_nodes = vec![];
|
||||
for _ in 0..length {
|
||||
deleted_nodes.push(self.get_deleted_node_data(node_id));
|
||||
node_id = self.node_tree.following_siblings(node_id).next().unwrap();
|
||||
deleted_nodes.push(self.get_deleted_node_data(node_tree, node_id));
|
||||
node_id = node_tree.following_siblings(node_id).next().unwrap();
|
||||
}
|
||||
|
||||
self.operations.push_op(NodeOperation::Delete {
|
||||
@ -250,16 +248,12 @@ impl<'a> TransactionBuilder<'a> {
|
||||
self
|
||||
}
|
||||
|
||||
fn get_deleted_node_data(&self, node_id: NodeId) -> NodeData {
|
||||
let node_data = self.node_tree.get_node(node_id).unwrap();
|
||||
|
||||
fn get_deleted_node_data(&self, node_tree: &NodeTree, node_id: NodeId) -> NodeData {
|
||||
let node_data = node_tree.get_node(node_id).unwrap();
|
||||
let mut children = vec![];
|
||||
self.node_tree
|
||||
.get_children_ids(node_id)
|
||||
.into_iter()
|
||||
.for_each(|child_id| {
|
||||
children.push(self.get_deleted_node_data(child_id));
|
||||
});
|
||||
node_tree.get_children_ids(node_id).into_iter().for_each(|child_id| {
|
||||
children.push(self.get_deleted_node_data(node_tree, child_id));
|
||||
});
|
||||
|
||||
NodeData {
|
||||
node_type: node_data.node_type.clone(),
|
||||
@ -274,7 +268,7 @@ impl<'a> TransactionBuilder<'a> {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn finalize(self) -> Transaction {
|
||||
pub fn build(self) -> Transaction {
|
||||
Transaction::from_operations(self.operations)
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
use super::NodeOperations;
|
||||
use crate::core::{Changeset, Node, NodeData, NodeOperation, Path, Transaction};
|
||||
use crate::errors::{ErrorBuilder, OTError, OTErrorCode};
|
||||
use crate::errors::{OTError, OTErrorCode};
|
||||
use indextree::{Arena, FollowingSiblings, NodeId};
|
||||
use std::sync::Arc;
|
||||
|
||||
@ -20,6 +20,8 @@ impl Default for NodeTree {
|
||||
}
|
||||
}
|
||||
|
||||
pub const PLACEHOLDER_NODE_TYPE: &str = "";
|
||||
|
||||
impl NodeTree {
|
||||
pub fn new(context: NodeTreeContext) -> NodeTree {
|
||||
let mut arena = Arena::new();
|
||||
@ -41,7 +43,7 @@ impl NodeTree {
|
||||
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);
|
||||
for operation in operations.into_inner().into_iter() {
|
||||
for (_, operation) in operations.into_inner().into_iter().enumerate() {
|
||||
let _ = node_tree.apply_op(operation)?;
|
||||
}
|
||||
Ok(node_tree)
|
||||
@ -149,15 +151,15 @@ impl NodeTree {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut iterate_node = self.root;
|
||||
let mut node_id = self.root;
|
||||
for id in path.iter() {
|
||||
iterate_node = self.child_from_node_at_index(iterate_node, *id)?;
|
||||
node_id = self.node_id_from_parent_at_index(node_id, *id)?;
|
||||
}
|
||||
|
||||
if iterate_node.is_removed(&self.arena) {
|
||||
if node_id.is_removed(&self.arena) {
|
||||
return None;
|
||||
}
|
||||
Some(iterate_node)
|
||||
Some(node_id)
|
||||
}
|
||||
|
||||
pub fn path_from_node_id(&self, node_id: NodeId) -> Path {
|
||||
@ -210,7 +212,7 @@ impl NodeTree {
|
||||
/// 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<NodeId> {
|
||||
pub fn node_id_from_parent_at_index(&self, node_id: NodeId, index: usize) -> Option<NodeId> {
|
||||
let children = node_id.children(&self.arena);
|
||||
for (counter, child) in children.enumerate() {
|
||||
if counter == index {
|
||||
@ -240,10 +242,11 @@ impl NodeTree {
|
||||
}
|
||||
|
||||
pub fn apply_transaction(&mut self, transaction: Transaction) -> Result<(), OTError> {
|
||||
let operations = transaction.into_operations();
|
||||
let operations = transaction.split().0;
|
||||
for operation in operations {
|
||||
self.apply_op(operation)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -294,25 +297,68 @@ impl NodeTree {
|
||||
if parent_path.is_empty() {
|
||||
self.insert_nodes_at_index(self.root, last_index, nodes)
|
||||
} else {
|
||||
let parent_node = self
|
||||
.node_id_at_path(parent_path)
|
||||
.ok_or_else(|| ErrorBuilder::new(OTErrorCode::PathNotFound).build())?;
|
||||
let parent_node = match self.node_id_at_path(parent_path) {
|
||||
None => self.create_adjacent_nodes_for_path(parent_path),
|
||||
Some(parent_node) => parent_node,
|
||||
};
|
||||
|
||||
self.insert_nodes_at_index(parent_node, last_index, nodes)
|
||||
}
|
||||
}
|
||||
|
||||
/// Create the adjacent nodes for the path
|
||||
///
|
||||
/// It will create a corresponding node for each node on the path if it's not existing.
|
||||
/// If the path is not start from zero, it will create its siblings.
|
||||
///
|
||||
/// Check out the operation_insert_test.rs for more examples.
|
||||
/// * operation_insert_node_when_its_parent_is_not_exist
|
||||
/// * operation_insert_node_when_multiple_parent_is_not_exist_test
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `path`: creates nodes for this path
|
||||
///
|
||||
/// returns: NodeId
|
||||
///
|
||||
fn create_adjacent_nodes_for_path<T: Into<Path>>(&mut self, path: T) -> NodeId {
|
||||
let path = path.into();
|
||||
let mut node_id = self.root;
|
||||
for id in path.iter() {
|
||||
match self.node_id_from_parent_at_index(node_id, *id) {
|
||||
None => {
|
||||
let num_of_children = node_id.children(&self.arena).count();
|
||||
if *id > num_of_children {
|
||||
for _ in 0..(*id - num_of_children) {
|
||||
let node: Node = placeholder_node().into();
|
||||
let sibling_node = self.arena.new_node(node);
|
||||
node_id.append(sibling_node, &mut self.arena);
|
||||
}
|
||||
}
|
||||
|
||||
let node: Node = placeholder_node().into();
|
||||
let new_node_id = self.arena.new_node(node);
|
||||
node_id.append(new_node_id, &mut self.arena);
|
||||
node_id = new_node_id;
|
||||
}
|
||||
Some(next_node_id) => {
|
||||
node_id = next_node_id;
|
||||
}
|
||||
}
|
||||
}
|
||||
node_id
|
||||
}
|
||||
|
||||
/// Inserts nodes before the node with node_id
|
||||
///
|
||||
fn insert_nodes_before(&mut self, node_id: &NodeId, nodes: Vec<NodeData>) {
|
||||
if node_id.is_removed(&self.arena) {
|
||||
tracing::warn!("Node:{:?} is remove before insert", node_id);
|
||||
return;
|
||||
}
|
||||
for node in nodes {
|
||||
let (node, children) = node.split();
|
||||
let new_node_id = self.arena.new_node(node);
|
||||
if node_id.is_removed(&self.arena) {
|
||||
tracing::warn!("Node:{:?} is remove before insert", node_id);
|
||||
return;
|
||||
}
|
||||
|
||||
node_id.insert_before(new_node_id, &mut self.arena);
|
||||
self.append_nodes(&new_node_id, children);
|
||||
}
|
||||
@ -326,14 +372,21 @@ impl NodeTree {
|
||||
|
||||
// Append the node to the end of the children list if index greater or equal to the
|
||||
// length of the children.
|
||||
if index >= parent.children(&self.arena).count() {
|
||||
let num_of_children = parent.children(&self.arena).count();
|
||||
if index >= num_of_children {
|
||||
let mut num_of_nodes_to_insert = index - num_of_children;
|
||||
while num_of_nodes_to_insert > 0 {
|
||||
self.append_nodes(&parent, vec![placeholder_node()]);
|
||||
num_of_nodes_to_insert -= 1;
|
||||
}
|
||||
|
||||
self.append_nodes(&parent, nodes);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let node_to_insert = self
|
||||
.child_from_node_at_index(parent, index)
|
||||
.ok_or_else(|| ErrorBuilder::new(OTErrorCode::PathNotFound).build())?;
|
||||
.node_id_from_parent_at_index(parent, index)
|
||||
.ok_or_else(|| OTError::internal().context(format!("Can't find the node at {}", index)))?;
|
||||
|
||||
self.insert_nodes_before(&node_to_insert, nodes);
|
||||
Ok(())
|
||||
@ -364,11 +417,22 @@ impl NodeTree {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Update the node at path with the `changeset`
|
||||
///
|
||||
/// Do nothing if there is no node at the path.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `path`: references to the node that will be applied with the changeset
|
||||
/// * `changeset`: the change that will be applied to the node
|
||||
///
|
||||
/// returns: Result<(), OTError>
|
||||
fn update(&mut self, path: &Path, changeset: Changeset) -> Result<(), OTError> {
|
||||
self.mut_node_at_path(path, |node| {
|
||||
let _ = node.apply_changeset(changeset)?;
|
||||
Ok(())
|
||||
})
|
||||
match self.mut_node_at_path(path, |node| node.apply_changeset(changeset)) {
|
||||
Ok(_) => {}
|
||||
Err(err) => tracing::error!("{}", err),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn mut_node_at_path<F>(&mut self, path: &Path, f: F) -> Result<(), OTError>
|
||||
@ -378,9 +442,9 @@ impl NodeTree {
|
||||
if !path.is_valid() {
|
||||
return Err(OTErrorCode::InvalidPath.into());
|
||||
}
|
||||
let node_id = self
|
||||
.node_id_at_path(path)
|
||||
.ok_or_else(|| ErrorBuilder::new(OTErrorCode::PathNotFound).build())?;
|
||||
let node_id = self.node_id_at_path(path).ok_or_else(|| {
|
||||
OTError::path_not_found().context(format!("Can't find the mutated node at path: {:?}", path))
|
||||
})?;
|
||||
match self.arena.get_mut(node_id) {
|
||||
None => tracing::warn!("The path: {:?} does not contain any nodes", path),
|
||||
Some(node) => {
|
||||
@ -391,3 +455,7 @@ impl NodeTree {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn placeholder_node() -> NodeData {
|
||||
NodeData::new(PLACEHOLDER_NODE_TYPE)
|
||||
}
|
||||
|
@ -38,6 +38,9 @@ impl OTError {
|
||||
static_ot_error!(revision_id_conflict, OTErrorCode::RevisionIDConflict);
|
||||
static_ot_error!(internal, OTErrorCode::Internal);
|
||||
static_ot_error!(serde, OTErrorCode::SerdeError);
|
||||
static_ot_error!(path_not_found, OTErrorCode::PathNotFound);
|
||||
static_ot_error!(compose, OTErrorCode::ComposeOperationFail);
|
||||
static_ot_error!(record_not_found, OTErrorCode::RecordNotFound);
|
||||
}
|
||||
|
||||
impl fmt::Display for OTError {
|
||||
@ -75,7 +78,7 @@ pub enum OTErrorCode {
|
||||
PathNotFound,
|
||||
PathIsEmpty,
|
||||
InvalidPath,
|
||||
UnexpectedEmpty,
|
||||
RecordNotFound,
|
||||
}
|
||||
|
||||
pub struct ErrorBuilder {
|
||||
|
@ -1,4 +1,7 @@
|
||||
mod operation_test;
|
||||
mod operation_delete_test;
|
||||
mod operation_delta_test;
|
||||
mod operation_insert_test;
|
||||
mod script;
|
||||
mod serde_test;
|
||||
mod transaction_compose_test;
|
||||
mod tree_test;
|
||||
|
178
shared-lib/lib-ot/tests/node/operation_delete_test.rs
Normal file
178
shared-lib/lib-ot/tests/node/operation_delete_test.rs
Normal file
@ -0,0 +1,178 @@
|
||||
use crate::node::script::NodeScript::*;
|
||||
use crate::node::script::NodeTest;
|
||||
|
||||
use lib_ot::core::{Changeset, NodeData, NodeDataBuilder};
|
||||
|
||||
#[test]
|
||||
fn operation_delete_nested_node_test() {
|
||||
let mut test = NodeTest::new();
|
||||
let image_a = NodeData::new("image_a");
|
||||
let image_b = NodeData::new("image_b");
|
||||
|
||||
let video_a = NodeData::new("video_a");
|
||||
let video_b = NodeData::new("video_b");
|
||||
|
||||
let image_1 = NodeDataBuilder::new("image_1")
|
||||
.add_node_data(image_a.clone())
|
||||
.add_node_data(image_b.clone())
|
||||
.build();
|
||||
let video_1 = NodeDataBuilder::new("video_1")
|
||||
.add_node_data(video_a.clone())
|
||||
.add_node_data(video_b)
|
||||
.build();
|
||||
|
||||
let text_node_1 = NodeDataBuilder::new("text_1")
|
||||
.add_node_data(image_1)
|
||||
.add_node_data(video_1.clone())
|
||||
.build();
|
||||
|
||||
let image_2 = NodeDataBuilder::new("image_2")
|
||||
.add_node_data(image_a)
|
||||
.add_node_data(image_b.clone())
|
||||
.build();
|
||||
let text_node_2 = NodeDataBuilder::new("text_2").add_node_data(image_2).build();
|
||||
|
||||
let scripts = vec![
|
||||
InsertNode {
|
||||
path: 0.into(),
|
||||
node_data: text_node_1,
|
||||
rev_id: 1,
|
||||
},
|
||||
InsertNode {
|
||||
path: 1.into(),
|
||||
node_data: text_node_2,
|
||||
rev_id: 2,
|
||||
},
|
||||
// 0:text_1
|
||||
// 0:image_1
|
||||
// 0:image_a
|
||||
// 1:image_b
|
||||
// 1:video_1
|
||||
// 0:video_a
|
||||
// 1:video_b
|
||||
// 1:text_2
|
||||
// 0:image_2
|
||||
// 0:image_a
|
||||
// 1:image_b
|
||||
DeleteNode {
|
||||
path: vec![0, 0, 0].into(),
|
||||
rev_id: 3,
|
||||
},
|
||||
AssertNode {
|
||||
path: vec![0, 0, 0].into(),
|
||||
expected: Some(image_b),
|
||||
},
|
||||
AssertNode {
|
||||
path: vec![0, 1].into(),
|
||||
expected: Some(video_1),
|
||||
},
|
||||
DeleteNode {
|
||||
path: vec![0, 1, 1].into(),
|
||||
rev_id: 4,
|
||||
},
|
||||
AssertNode {
|
||||
path: vec![0, 1, 0].into(),
|
||||
expected: Some(video_a),
|
||||
},
|
||||
];
|
||||
test.run_scripts(scripts);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn operation_delete_node_with_revision_conflict_test() {
|
||||
let mut test = NodeTest::new();
|
||||
let text_1 = NodeDataBuilder::new("text_1").build();
|
||||
let text_2 = NodeDataBuilder::new("text_2").build();
|
||||
let text_3 = NodeDataBuilder::new("text_3").build();
|
||||
|
||||
let scripts = vec![
|
||||
InsertNode {
|
||||
path: 0.into(),
|
||||
node_data: text_1.clone(),
|
||||
rev_id: 1,
|
||||
},
|
||||
InsertNode {
|
||||
path: 1.into(),
|
||||
node_data: text_2,
|
||||
rev_id: 2,
|
||||
},
|
||||
// The node's in the tree will be:
|
||||
// 0: text_1
|
||||
// 2: text_2
|
||||
//
|
||||
// The insert action is happened concurrently with the delete action, because they
|
||||
// share the same rev_id. aka, 3. The delete action is want to delete the node at index 1,
|
||||
// but it was moved to index 2.
|
||||
InsertNode {
|
||||
path: 1.into(),
|
||||
node_data: text_3.clone(),
|
||||
rev_id: 3,
|
||||
},
|
||||
// 0: text_1
|
||||
// 1: text_3
|
||||
// 2: text_2
|
||||
//
|
||||
// The path of the delete action will be transformed to a new path that point to the text_2.
|
||||
// 1 -> 2
|
||||
DeleteNode {
|
||||
path: 1.into(),
|
||||
rev_id: 3,
|
||||
},
|
||||
// After perform the delete action, the tree will be:
|
||||
// 0: text_1
|
||||
// 1: text_3
|
||||
AssertNumberOfChildrenAtPath {
|
||||
path: None,
|
||||
expected: 2,
|
||||
},
|
||||
AssertNode {
|
||||
path: 0.into(),
|
||||
expected: Some(text_1),
|
||||
},
|
||||
AssertNode {
|
||||
path: 1.into(),
|
||||
expected: Some(text_3),
|
||||
},
|
||||
AssertNode {
|
||||
path: 2.into(),
|
||||
expected: None,
|
||||
},
|
||||
];
|
||||
test.run_scripts(scripts);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn operation_update_node_after_delete_test() {
|
||||
let mut test = NodeTest::new();
|
||||
let text_1 = NodeDataBuilder::new("text_1").build();
|
||||
let text_2 = NodeDataBuilder::new("text_2").build();
|
||||
let scripts = vec![
|
||||
InsertNode {
|
||||
path: 0.into(),
|
||||
node_data: text_1,
|
||||
rev_id: 1,
|
||||
},
|
||||
InsertNode {
|
||||
path: 1.into(),
|
||||
node_data: text_2,
|
||||
rev_id: 2,
|
||||
},
|
||||
DeleteNode {
|
||||
path: 0.into(),
|
||||
rev_id: 3,
|
||||
},
|
||||
// The node at path 1 is not exist. The following UpdateBody script will do nothing
|
||||
AssertNode {
|
||||
path: 1.into(),
|
||||
expected: None,
|
||||
},
|
||||
UpdateBody {
|
||||
path: 1.into(),
|
||||
changeset: Changeset::Delta {
|
||||
delta: Default::default(),
|
||||
inverted: Default::default(),
|
||||
},
|
||||
},
|
||||
];
|
||||
test.run_scripts(scripts);
|
||||
}
|
41
shared-lib/lib-ot/tests/node/operation_delta_test.rs
Normal file
41
shared-lib/lib-ot/tests/node/operation_delta_test.rs
Normal file
@ -0,0 +1,41 @@
|
||||
use crate::node::script::NodeScript::{AssertNodeDelta, InsertNode, UpdateBody};
|
||||
use crate::node::script::{edit_node_delta, NodeTest};
|
||||
use lib_ot::core::NodeDataBuilder;
|
||||
use lib_ot::text_delta::DeltaTextOperationBuilder;
|
||||
|
||||
#[test]
|
||||
fn operation_update_delta_test() {
|
||||
let mut test = NodeTest::new();
|
||||
let initial_delta = DeltaTextOperationBuilder::new().build();
|
||||
let new_delta = DeltaTextOperationBuilder::new()
|
||||
.retain(initial_delta.utf16_base_len)
|
||||
.insert("Hello, world")
|
||||
.build();
|
||||
let (changeset, expected) = edit_node_delta(&initial_delta, new_delta);
|
||||
let node = NodeDataBuilder::new("text").insert_delta(initial_delta.clone()).build();
|
||||
|
||||
let scripts = vec![
|
||||
InsertNode {
|
||||
path: 0.into(),
|
||||
node_data: node,
|
||||
rev_id: 1,
|
||||
},
|
||||
UpdateBody {
|
||||
path: 0.into(),
|
||||
changeset: changeset.clone(),
|
||||
},
|
||||
AssertNodeDelta {
|
||||
path: 0.into(),
|
||||
expected,
|
||||
},
|
||||
UpdateBody {
|
||||
path: 0.into(),
|
||||
changeset: changeset.inverted(),
|
||||
},
|
||||
AssertNodeDelta {
|
||||
path: 0.into(),
|
||||
expected: initial_delta,
|
||||
},
|
||||
];
|
||||
test.run_scripts(scripts);
|
||||
}
|
460
shared-lib/lib-ot/tests/node/operation_insert_test.rs
Normal file
460
shared-lib/lib-ot/tests/node/operation_insert_test.rs
Normal file
@ -0,0 +1,460 @@
|
||||
use crate::node::script::NodeScript::*;
|
||||
use crate::node::script::NodeTest;
|
||||
|
||||
use lib_ot::core::{placeholder_node, NodeData, NodeDataBuilder, NodeOperation, Path};
|
||||
|
||||
#[test]
|
||||
fn operation_insert_op_transform_test() {
|
||||
let node_1 = NodeDataBuilder::new("text_1").build();
|
||||
let node_2 = NodeDataBuilder::new("text_2").build();
|
||||
let op_1 = NodeOperation::Insert {
|
||||
path: Path(vec![0, 1]),
|
||||
nodes: vec![node_1],
|
||||
};
|
||||
|
||||
let mut insert_2 = NodeOperation::Insert {
|
||||
path: Path(vec![0, 1]),
|
||||
nodes: vec![node_2],
|
||||
};
|
||||
|
||||
// let mut node_tree = NodeTree::new("root");
|
||||
// node_tree.apply_op(insert_1.clone()).unwrap();
|
||||
|
||||
op_1.transform(&mut insert_2);
|
||||
let json = serde_json::to_string(&insert_2).unwrap();
|
||||
assert_eq!(json, r#"{"op":"insert","path":[0,2],"nodes":[{"type":"text_2"}]}"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn operation_insert_one_level_path_test() {
|
||||
let node_data_1 = NodeDataBuilder::new("text_1").build();
|
||||
let node_data_2 = NodeDataBuilder::new("text_2").build();
|
||||
let node_data_3 = NodeDataBuilder::new("text_3").build();
|
||||
let node_3 = node_data_3.clone();
|
||||
// 0: text_1
|
||||
// 1: text_2
|
||||
//
|
||||
// Insert a new operation with rev_id 2 to index 1,but the index was already taken, so
|
||||
// it needs to be transformed.
|
||||
//
|
||||
// 0: text_1
|
||||
// 1: text_2
|
||||
// 2: text_3
|
||||
let scripts = vec![
|
||||
InsertNode {
|
||||
path: 0.into(),
|
||||
node_data: node_data_1.clone(),
|
||||
rev_id: 1,
|
||||
},
|
||||
InsertNode {
|
||||
path: 1.into(),
|
||||
node_data: node_data_2.clone(),
|
||||
rev_id: 2,
|
||||
},
|
||||
InsertNode {
|
||||
path: 1.into(),
|
||||
node_data: node_data_3.clone(),
|
||||
rev_id: 2,
|
||||
},
|
||||
AssertNode {
|
||||
path: 2.into(),
|
||||
expected: Some(node_3.clone()),
|
||||
},
|
||||
];
|
||||
NodeTest::new().run_scripts(scripts);
|
||||
|
||||
// If the rev_id of the node_data_3 is 3. then the tree will be:
|
||||
// 0: text_1
|
||||
// 1: text_3
|
||||
// 2: text_2
|
||||
let scripts = vec![
|
||||
InsertNode {
|
||||
path: 0.into(),
|
||||
node_data: node_data_1,
|
||||
rev_id: 1,
|
||||
},
|
||||
InsertNode {
|
||||
path: 1.into(),
|
||||
node_data: node_data_2,
|
||||
rev_id: 2,
|
||||
},
|
||||
InsertNode {
|
||||
path: 1.into(),
|
||||
node_data: node_data_3,
|
||||
rev_id: 3,
|
||||
},
|
||||
AssertNode {
|
||||
path: 1.into(),
|
||||
expected: Some(node_3),
|
||||
},
|
||||
];
|
||||
NodeTest::new().run_scripts(scripts);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn operation_insert_with_multiple_level_path_test() {
|
||||
let mut test = NodeTest::new();
|
||||
let node_data_1 = NodeDataBuilder::new("text_1")
|
||||
.add_node_data(NodeDataBuilder::new("text_1_1").build())
|
||||
.add_node_data(NodeDataBuilder::new("text_1_2").build())
|
||||
.build();
|
||||
|
||||
let node_data_2 = NodeDataBuilder::new("text_2")
|
||||
.add_node_data(NodeDataBuilder::new("text_2_1").build())
|
||||
.add_node_data(NodeDataBuilder::new("text_2_2").build())
|
||||
.build();
|
||||
|
||||
let node_data_3 = NodeDataBuilder::new("text_3").build();
|
||||
let scripts = vec![
|
||||
InsertNode {
|
||||
path: 0.into(),
|
||||
node_data: node_data_1,
|
||||
rev_id: 1,
|
||||
},
|
||||
InsertNode {
|
||||
path: 1.into(),
|
||||
node_data: node_data_2,
|
||||
rev_id: 2,
|
||||
},
|
||||
InsertNode {
|
||||
path: 1.into(),
|
||||
node_data: node_data_3.clone(),
|
||||
rev_id: 2,
|
||||
},
|
||||
AssertNode {
|
||||
path: 2.into(),
|
||||
expected: Some(node_data_3),
|
||||
},
|
||||
];
|
||||
test.run_scripts(scripts);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn operation_insert_node_out_of_bound_test() {
|
||||
let mut test = NodeTest::new();
|
||||
let image_a = NodeData::new("image_a");
|
||||
let image_b = NodeData::new("image_b");
|
||||
let image = NodeDataBuilder::new("image_1")
|
||||
.add_node_data(image_a)
|
||||
.add_node_data(image_b)
|
||||
.build();
|
||||
let text_node = NodeDataBuilder::new("text_1").add_node_data(image).build();
|
||||
let image_c = NodeData::new("image_c");
|
||||
|
||||
let scripts = vec![
|
||||
InsertNode {
|
||||
path: 0.into(),
|
||||
node_data: text_node,
|
||||
rev_id: 1,
|
||||
},
|
||||
// 0:text_1
|
||||
// 0:image_1
|
||||
// 0:image_a
|
||||
// 1:image_b
|
||||
InsertNode {
|
||||
path: vec![0, 0, 3].into(),
|
||||
node_data: image_c.clone(),
|
||||
rev_id: 2,
|
||||
},
|
||||
// 0:text_1
|
||||
// 0:image_1
|
||||
// 0:image_a
|
||||
// 1:image_b
|
||||
// 2:placeholder node
|
||||
// 3:image_c
|
||||
AssertNode {
|
||||
path: vec![0, 0, 2].into(),
|
||||
expected: Some(placeholder_node()),
|
||||
},
|
||||
AssertNode {
|
||||
path: vec![0, 0, 3].into(),
|
||||
expected: Some(image_c),
|
||||
},
|
||||
AssertNode {
|
||||
path: vec![0, 0, 10].into(),
|
||||
expected: None,
|
||||
},
|
||||
];
|
||||
test.run_scripts(scripts);
|
||||
}
|
||||
#[test]
|
||||
fn operation_insert_node_when_parent_is_not_exist_test1() {
|
||||
let mut test = NodeTest::new();
|
||||
let text_1 = NodeDataBuilder::new("text_1").build();
|
||||
let text_2 = NodeDataBuilder::new("text_2").build();
|
||||
let scripts = vec![
|
||||
InsertNode {
|
||||
path: 0.into(),
|
||||
node_data: text_1,
|
||||
rev_id: 1,
|
||||
},
|
||||
// The node at path 1 is not existing when inserting the text_2 to path 2.
|
||||
InsertNode {
|
||||
path: 2.into(),
|
||||
node_data: text_2.clone(),
|
||||
rev_id: 2,
|
||||
},
|
||||
AssertNode {
|
||||
path: 1.into(),
|
||||
expected: Some(placeholder_node()),
|
||||
},
|
||||
AssertNode {
|
||||
path: 2.into(),
|
||||
expected: Some(text_2),
|
||||
},
|
||||
];
|
||||
test.run_scripts(scripts);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn operation_insert_node_when_parent_is_not_exist_test2() {
|
||||
let mut test = NodeTest::new();
|
||||
let text_1 = NodeDataBuilder::new("text_1").build();
|
||||
let text_2 = NodeDataBuilder::new("text_2").build();
|
||||
let scripts = vec![
|
||||
InsertNode {
|
||||
path: 0.into(),
|
||||
node_data: text_1,
|
||||
rev_id: 1,
|
||||
},
|
||||
// The node at path 1 is not existing when inserting the text_2 to path 2.
|
||||
InsertNode {
|
||||
path: 3.into(),
|
||||
node_data: text_2.clone(),
|
||||
rev_id: 2,
|
||||
},
|
||||
AssertNode {
|
||||
path: 1.into(),
|
||||
expected: Some(placeholder_node()),
|
||||
},
|
||||
AssertNode {
|
||||
path: 2.into(),
|
||||
expected: Some(placeholder_node()),
|
||||
},
|
||||
AssertNode {
|
||||
path: 3.into(),
|
||||
expected: Some(text_2),
|
||||
},
|
||||
];
|
||||
test.run_scripts(scripts);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn operation_insert_node_when_its_parent_is_not_exist_test3() {
|
||||
let mut test = NodeTest::new();
|
||||
let text_1 = NodeDataBuilder::new("text_1").build();
|
||||
let text_2 = NodeDataBuilder::new("text_2").build();
|
||||
|
||||
let mut placeholder_node = placeholder_node();
|
||||
placeholder_node.children.push(text_2.clone());
|
||||
|
||||
let scripts = vec![
|
||||
InsertNode {
|
||||
path: 0.into(),
|
||||
node_data: text_1,
|
||||
rev_id: 1,
|
||||
},
|
||||
// The node at path 1 is not existing when inserting the text_2 to path 2.
|
||||
InsertNode {
|
||||
path: vec![1, 0].into(),
|
||||
node_data: text_2.clone(),
|
||||
rev_id: 2,
|
||||
},
|
||||
AssertNode {
|
||||
path: 1.into(),
|
||||
expected: Some(placeholder_node),
|
||||
},
|
||||
AssertNode {
|
||||
path: vec![1, 0].into(),
|
||||
expected: Some(text_2),
|
||||
},
|
||||
];
|
||||
test.run_scripts(scripts);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn operation_insert_node_to_the_end_when_parent_is_not_exist_test() {
|
||||
let mut test = NodeTest::new();
|
||||
let node_0 = NodeData::new("0");
|
||||
let node_1 = NodeData::new("1");
|
||||
let node_1_1 = NodeData::new("1_1");
|
||||
let text_node = NodeData::new("text");
|
||||
let mut ghost = placeholder_node();
|
||||
ghost.children.push(text_node.clone());
|
||||
// 0:0
|
||||
// 1:1
|
||||
// 0:1_1
|
||||
// 1:ghost
|
||||
// 0:text
|
||||
let scripts = vec![
|
||||
InsertNode {
|
||||
path: 0.into(),
|
||||
node_data: node_0,
|
||||
rev_id: 1,
|
||||
},
|
||||
InsertNode {
|
||||
path: 1.into(),
|
||||
node_data: node_1,
|
||||
rev_id: 2,
|
||||
},
|
||||
InsertNode {
|
||||
path: vec![1, 0].into(),
|
||||
node_data: node_1_1.clone(),
|
||||
rev_id: 3,
|
||||
},
|
||||
InsertNode {
|
||||
path: vec![1, 1, 0].into(),
|
||||
node_data: text_node.clone(),
|
||||
rev_id: 4,
|
||||
},
|
||||
AssertNode {
|
||||
path: vec![1, 0].into(),
|
||||
expected: Some(node_1_1),
|
||||
},
|
||||
AssertNode {
|
||||
path: vec![1, 1].into(),
|
||||
expected: Some(ghost),
|
||||
},
|
||||
AssertNode {
|
||||
path: vec![1, 1, 0].into(),
|
||||
expected: Some(text_node),
|
||||
},
|
||||
];
|
||||
test.run_scripts(scripts);
|
||||
}
|
||||
#[test]
|
||||
fn operation_insert_node_when_multiple_parent_is_not_exist_test() {
|
||||
let mut test = NodeTest::new();
|
||||
let text_1 = NodeDataBuilder::new("text_1").build();
|
||||
let text_2 = NodeDataBuilder::new("text_2").build();
|
||||
|
||||
let path = vec![1, 0, 0, 0, 0, 0];
|
||||
let mut auto_fill_node = placeholder_node();
|
||||
let mut iter_node: &mut NodeData = &mut auto_fill_node;
|
||||
let insert_path = path.split_at(1).1;
|
||||
for (index, _) in insert_path.iter().enumerate() {
|
||||
if index == insert_path.len() - 1 {
|
||||
iter_node.children.push(text_2.clone());
|
||||
} else {
|
||||
iter_node.children.push(placeholder_node());
|
||||
iter_node = iter_node.children.last_mut().unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
let scripts = vec![
|
||||
InsertNode {
|
||||
path: 0.into(),
|
||||
node_data: text_1,
|
||||
rev_id: 1,
|
||||
},
|
||||
InsertNode {
|
||||
path: path.clone().into(),
|
||||
node_data: text_2.clone(),
|
||||
rev_id: 2,
|
||||
},
|
||||
AssertNode {
|
||||
path: vec![1].into(),
|
||||
expected: Some(auto_fill_node),
|
||||
},
|
||||
AssertNode {
|
||||
path: path.into(),
|
||||
expected: Some(text_2),
|
||||
},
|
||||
];
|
||||
test.run_scripts(scripts);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn operation_insert_node_when_multiple_parent_is_not_exist_test2() {
|
||||
let mut test = NodeTest::new();
|
||||
// 0:ghost
|
||||
// 0:ghost
|
||||
// 1:ghost
|
||||
// 0:text
|
||||
let mut text_node_parent = placeholder_node();
|
||||
let text_node = NodeDataBuilder::new("text").build();
|
||||
text_node_parent.children.push(text_node.clone());
|
||||
|
||||
let mut ghost = placeholder_node();
|
||||
ghost.children.push(placeholder_node());
|
||||
ghost.children.push(text_node_parent.clone());
|
||||
|
||||
let path = vec![1, 1, 0];
|
||||
let scripts = vec![
|
||||
InsertNode {
|
||||
path: path.into(),
|
||||
node_data: text_node.clone(),
|
||||
rev_id: 1,
|
||||
},
|
||||
// 0:ghost
|
||||
// 1:ghost
|
||||
// 0:ghost
|
||||
// 1:ghost
|
||||
// 0:text
|
||||
AssertNode {
|
||||
path: 0.into(),
|
||||
expected: Some(placeholder_node()),
|
||||
},
|
||||
AssertNode {
|
||||
path: 1.into(),
|
||||
expected: Some(ghost),
|
||||
},
|
||||
AssertNumberOfChildrenAtPath {
|
||||
path: Some(1.into()),
|
||||
expected: 2,
|
||||
},
|
||||
AssertNode {
|
||||
path: vec![1, 1].into(),
|
||||
expected: Some(text_node_parent),
|
||||
},
|
||||
AssertNode {
|
||||
path: vec![1, 1, 0].into(),
|
||||
expected: Some(text_node),
|
||||
},
|
||||
];
|
||||
test.run_scripts(scripts);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn operation_insert_node_when_multiple_parent_is_not_exist_test3() {
|
||||
let mut test = NodeTest::new();
|
||||
let text_node = NodeDataBuilder::new("text").build();
|
||||
let path = vec![3, 3, 0];
|
||||
let scripts = vec![
|
||||
InsertNode {
|
||||
path: path.clone().into(),
|
||||
node_data: text_node.clone(),
|
||||
rev_id: 1,
|
||||
},
|
||||
// 0:ghost
|
||||
// 1:ghost
|
||||
// 2:ghost
|
||||
// 3:ghost
|
||||
// 0:ghost
|
||||
// 1:ghost
|
||||
// 2:ghost
|
||||
// 3:ghost
|
||||
// 0:text
|
||||
AssertNode {
|
||||
path: 0.into(),
|
||||
expected: Some(placeholder_node()),
|
||||
},
|
||||
AssertNode {
|
||||
path: 1.into(),
|
||||
expected: Some(placeholder_node()),
|
||||
},
|
||||
AssertNode {
|
||||
path: 2.into(),
|
||||
expected: Some(placeholder_node()),
|
||||
},
|
||||
AssertNumberOfChildrenAtPath {
|
||||
path: Some(3.into()),
|
||||
expected: 4,
|
||||
},
|
||||
AssertNode {
|
||||
path: path.into(),
|
||||
expected: Some(text_node),
|
||||
},
|
||||
];
|
||||
test.run_scripts(scripts);
|
||||
}
|
@ -1,170 +0,0 @@
|
||||
use crate::node::script::NodeScript::*;
|
||||
use crate::node::script::NodeTest;
|
||||
|
||||
use lib_ot::core::{NodeDataBuilder, NodeOperation, Path};
|
||||
|
||||
#[test]
|
||||
fn operation_insert_op_transform_test() {
|
||||
let node_1 = NodeDataBuilder::new("text_1").build();
|
||||
let node_2 = NodeDataBuilder::new("text_2").build();
|
||||
let op_1 = NodeOperation::Insert {
|
||||
path: Path(vec![0, 1]),
|
||||
nodes: vec![node_1],
|
||||
};
|
||||
|
||||
let mut insert_2 = NodeOperation::Insert {
|
||||
path: Path(vec![0, 1]),
|
||||
nodes: vec![node_2],
|
||||
};
|
||||
|
||||
// let mut node_tree = NodeTree::new("root");
|
||||
// node_tree.apply_op(insert_1.clone()).unwrap();
|
||||
|
||||
op_1.transform(&mut insert_2);
|
||||
let json = serde_json::to_string(&insert_2).unwrap();
|
||||
assert_eq!(json, r#"{"op":"insert","path":[0,2],"nodes":[{"type":"text_2"}]}"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn operation_insert_one_level_path_test() {
|
||||
let mut test = NodeTest::new();
|
||||
let node_data_1 = NodeDataBuilder::new("text_1").build();
|
||||
let node_data_2 = NodeDataBuilder::new("text_2").build();
|
||||
let node_data_3 = NodeDataBuilder::new("text_3").build();
|
||||
let node_3 = node_data_3.clone();
|
||||
// 0: text_1
|
||||
// 1: text_2
|
||||
//
|
||||
// Insert a new operation with rev_id 1,but the rev_id:1 is already exist, so
|
||||
// it needs to be transformed.
|
||||
// 1:text_3 => 2:text_3
|
||||
//
|
||||
// 0: text_1
|
||||
// 1: text_2
|
||||
// 2: text_3
|
||||
//
|
||||
// If the rev_id of the insert operation is 3. then the tree will be:
|
||||
// 0: text_1
|
||||
// 1: text_3
|
||||
// 2: text_2
|
||||
let scripts = vec![
|
||||
InsertNode {
|
||||
path: 0.into(),
|
||||
node_data: node_data_1,
|
||||
rev_id: 1,
|
||||
},
|
||||
InsertNode {
|
||||
path: 1.into(),
|
||||
node_data: node_data_2,
|
||||
rev_id: 2,
|
||||
},
|
||||
InsertNode {
|
||||
path: 1.into(),
|
||||
node_data: node_data_3,
|
||||
rev_id: 1,
|
||||
},
|
||||
AssertNode {
|
||||
path: 2.into(),
|
||||
expected: Some(node_3),
|
||||
},
|
||||
];
|
||||
test.run_scripts(scripts);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn operation_insert_with_multiple_level_path_test() {
|
||||
let mut test = NodeTest::new();
|
||||
let node_data_1 = NodeDataBuilder::new("text_1")
|
||||
.add_node_data(NodeDataBuilder::new("text_1_1").build())
|
||||
.add_node_data(NodeDataBuilder::new("text_1_2").build())
|
||||
.build();
|
||||
|
||||
let node_data_2 = NodeDataBuilder::new("text_2")
|
||||
.add_node_data(NodeDataBuilder::new("text_2_1").build())
|
||||
.add_node_data(NodeDataBuilder::new("text_2_2").build())
|
||||
.build();
|
||||
|
||||
let node_data_3 = NodeDataBuilder::new("text_3").build();
|
||||
let scripts = vec![
|
||||
InsertNode {
|
||||
path: 0.into(),
|
||||
node_data: node_data_1,
|
||||
rev_id: 1,
|
||||
},
|
||||
InsertNode {
|
||||
path: 1.into(),
|
||||
node_data: node_data_2,
|
||||
rev_id: 2,
|
||||
},
|
||||
InsertNode {
|
||||
path: 1.into(),
|
||||
node_data: node_data_3.clone(),
|
||||
rev_id: 1,
|
||||
},
|
||||
AssertNode {
|
||||
path: 2.into(),
|
||||
expected: Some(node_data_3),
|
||||
},
|
||||
];
|
||||
test.run_scripts(scripts);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn operation_delete_test() {
|
||||
let mut test = NodeTest::new();
|
||||
let node_data_1 = NodeDataBuilder::new("text_1").build();
|
||||
let node_data_2 = NodeDataBuilder::new("text_2").build();
|
||||
let node_data_3 = NodeDataBuilder::new("text_3").build();
|
||||
let node_3 = node_data_3.clone();
|
||||
|
||||
let scripts = vec![
|
||||
InsertNode {
|
||||
path: 0.into(),
|
||||
node_data: node_data_1,
|
||||
rev_id: 1,
|
||||
},
|
||||
InsertNode {
|
||||
path: 1.into(),
|
||||
node_data: node_data_2,
|
||||
rev_id: 2,
|
||||
},
|
||||
// The node's in the tree will be:
|
||||
// 0: text_1
|
||||
// 2: text_2
|
||||
//
|
||||
// The insert action is happened concurrently with the delete action, because they
|
||||
// share the same rev_id. aka, 3. The delete action is want to delete the node at index 1,
|
||||
// but it was moved to index 2.
|
||||
InsertNode {
|
||||
path: 1.into(),
|
||||
node_data: node_data_3,
|
||||
rev_id: 3,
|
||||
},
|
||||
// 0: text_1
|
||||
// 1: text_3
|
||||
// 2: text_2
|
||||
//
|
||||
// The path of the delete action will be transformed to a new path that point to the text_2.
|
||||
// 1 -> 2
|
||||
DeleteNode {
|
||||
path: 1.into(),
|
||||
rev_id: 3,
|
||||
},
|
||||
// After perform the delete action, the tree will be:
|
||||
// 0: text_1
|
||||
// 1: text_3
|
||||
AssertNumberOfChildrenAtPath {
|
||||
path: None,
|
||||
expected: 2,
|
||||
},
|
||||
AssertNode {
|
||||
path: 1.into(),
|
||||
expected: Some(node_3),
|
||||
},
|
||||
AssertNode {
|
||||
path: 2.into(),
|
||||
expected: None,
|
||||
},
|
||||
];
|
||||
test.run_scripts(scripts);
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
#![allow(clippy::all)]
|
||||
use lib_ot::core::{NodeTreeContext, Transaction};
|
||||
use lib_ot::core::{NodeTreeContext, OperationTransform, Transaction};
|
||||
use lib_ot::text_delta::DeltaTextOperationBuilder;
|
||||
use lib_ot::{
|
||||
core::attributes::AttributeHashMap,
|
||||
core::{Body, Changeset, NodeData, NodeTree, Path, TransactionBuilder},
|
||||
@ -84,9 +85,7 @@ impl NodeTest {
|
||||
node_data: node,
|
||||
rev_id,
|
||||
} => {
|
||||
let mut transaction = TransactionBuilder::new(&self.node_tree)
|
||||
.insert_node_at_path(path, node)
|
||||
.finalize();
|
||||
let mut transaction = TransactionBuilder::new().insert_node_at_path(path, node).build();
|
||||
self.transform_transaction_if_need(&mut transaction, rev_id);
|
||||
self.apply_transaction(transaction);
|
||||
}
|
||||
@ -95,29 +94,34 @@ impl NodeTest {
|
||||
node_data_list,
|
||||
rev_id,
|
||||
} => {
|
||||
let mut transaction = TransactionBuilder::new(&self.node_tree)
|
||||
let mut transaction = TransactionBuilder::new()
|
||||
.insert_nodes_at_path(path, node_data_list)
|
||||
.finalize();
|
||||
.build();
|
||||
self.transform_transaction_if_need(&mut transaction, rev_id);
|
||||
self.apply_transaction(transaction);
|
||||
}
|
||||
NodeScript::UpdateAttributes { path, attributes } => {
|
||||
let transaction = TransactionBuilder::new(&self.node_tree)
|
||||
.update_attributes_at_path(&path, attributes)
|
||||
.finalize();
|
||||
let node = self.node_tree.get_node_data_at_path(&path).unwrap();
|
||||
let transaction = TransactionBuilder::new()
|
||||
.update_node_at_path(
|
||||
&path,
|
||||
Changeset::Attributes {
|
||||
new: attributes,
|
||||
old: node.attributes,
|
||||
},
|
||||
)
|
||||
.build();
|
||||
self.apply_transaction(transaction);
|
||||
}
|
||||
NodeScript::UpdateBody { path, changeset } => {
|
||||
//
|
||||
let transaction = TransactionBuilder::new(&self.node_tree)
|
||||
.update_body_at_path(&path, changeset)
|
||||
.finalize();
|
||||
let transaction = TransactionBuilder::new().update_node_at_path(&path, changeset).build();
|
||||
self.apply_transaction(transaction);
|
||||
}
|
||||
NodeScript::DeleteNode { path, rev_id } => {
|
||||
let mut transaction = TransactionBuilder::new(&self.node_tree)
|
||||
.delete_node_at_path(&path)
|
||||
.finalize();
|
||||
let mut transaction = TransactionBuilder::new()
|
||||
.delete_node_at_path(&self.node_tree, &path)
|
||||
.build();
|
||||
self.transform_transaction_if_need(&mut transaction, rev_id);
|
||||
self.apply_transaction(transaction);
|
||||
}
|
||||
@ -175,3 +179,35 @@ impl NodeTest {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn edit_node_delta(
|
||||
delta: &DeltaTextOperations,
|
||||
new_delta: DeltaTextOperations,
|
||||
) -> (Changeset, DeltaTextOperations) {
|
||||
let inverted = new_delta.invert(&delta);
|
||||
let expected = delta.compose(&new_delta).unwrap();
|
||||
let changeset = Changeset::Delta {
|
||||
delta: new_delta.clone(),
|
||||
inverted: inverted.clone(),
|
||||
};
|
||||
(changeset, expected)
|
||||
}
|
||||
|
||||
pub fn make_node_delta_changeset(
|
||||
initial_content: &str,
|
||||
insert_str: &str,
|
||||
) -> (DeltaTextOperations, Changeset, DeltaTextOperations) {
|
||||
let initial_content = initial_content.to_owned();
|
||||
let initial_delta = DeltaTextOperationBuilder::new().insert(&initial_content).build();
|
||||
let delta = DeltaTextOperationBuilder::new()
|
||||
.retain(initial_content.len())
|
||||
.insert(insert_str)
|
||||
.build();
|
||||
let inverted = delta.invert(&initial_delta);
|
||||
let expected = initial_delta.compose(&delta).unwrap();
|
||||
let changeset = Changeset::Delta {
|
||||
delta: delta.clone(),
|
||||
inverted: inverted.clone(),
|
||||
};
|
||||
(initial_delta, changeset, expected)
|
||||
}
|
||||
|
@ -1,6 +1,4 @@
|
||||
use lib_ot::core::{
|
||||
AttributeBuilder, Changeset, NodeData, NodeDataBuilder, NodeOperation, NodeTree, Path, Transaction,
|
||||
};
|
||||
use lib_ot::core::{AttributeBuilder, Changeset, NodeData, NodeDataBuilder, NodeOperation, NodeTree, Path};
|
||||
use lib_ot::text_delta::DeltaTextOperationBuilder;
|
||||
|
||||
#[test]
|
||||
@ -69,33 +67,33 @@ fn operation_update_node_body_deserialize_test() {
|
||||
assert_eq!(json_1, json_2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transaction_serialize_test() {
|
||||
let insert = NodeOperation::Insert {
|
||||
path: Path(vec![0, 1]),
|
||||
nodes: vec![NodeData::new("text".to_owned())],
|
||||
};
|
||||
let transaction = Transaction::from_operations(vec![insert]);
|
||||
let json = serde_json::to_string(&transaction).unwrap();
|
||||
assert_eq!(
|
||||
json,
|
||||
r#"{"operations":[{"op":"insert","path":[0,1],"nodes":[{"type":"text"}]}]}"#
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transaction_deserialize_test() {
|
||||
let json = r#"{"operations":[{"op":"insert","path":[0,1],"nodes":[{"type":"text"}]}],"TextSelection":{"before_selection":{"start":{"path":[],"offset":0},"end":{"path":[],"offset":0}},"after_selection":{"start":{"path":[],"offset":0},"end":{"path":[],"offset":0}}}}"#;
|
||||
|
||||
let transaction: Transaction = serde_json::from_str(json).unwrap();
|
||||
assert_eq!(transaction.operations.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn node_tree_deserialize_test() {
|
||||
let tree: NodeTree = serde_json::from_str(TREE_JSON).unwrap();
|
||||
assert_eq!(tree.number_of_children(None), 1);
|
||||
}
|
||||
// #[test]
|
||||
// fn transaction_serialize_test() {
|
||||
// let insert = NodeOperation::Insert {
|
||||
// path: Path(vec![0, 1]),
|
||||
// nodes: vec![NodeData::new("text".to_owned())],
|
||||
// };
|
||||
// let transaction = Transaction::from_operations(vec![insert]);
|
||||
// let json = serde_json::to_string(&transaction).unwrap();
|
||||
// assert_eq!(
|
||||
// json,
|
||||
// r#"{"operations":[{"op":"insert","path":[0,1],"nodes":[{"type":"text"}]}]}"#
|
||||
// );
|
||||
// }
|
||||
//
|
||||
// #[test]
|
||||
// fn transaction_deserialize_test() {
|
||||
// let json = r#"{"operations":[{"op":"insert","path":[0,1],"nodes":[{"type":"text"}]}],"TextSelection":{"before_selection":{"start":{"path":[],"offset":0},"end":{"path":[],"offset":0}},"after_selection":{"start":{"path":[],"offset":0},"end":{"path":[],"offset":0}}}}"#;
|
||||
//
|
||||
// let transaction: Transaction = serde_json::from_str(json).unwrap();
|
||||
// assert_eq!(transaction.operations.len(), 1);
|
||||
// }
|
||||
//
|
||||
// #[test]
|
||||
// fn node_tree_deserialize_test() {
|
||||
// let tree: NodeTree = serde_json::from_str(TREE_JSON).unwrap();
|
||||
// assert_eq!(tree.number_of_children(None), 1);
|
||||
// }
|
||||
|
||||
#[test]
|
||||
fn node_tree_serialize_test() {
|
||||
|
104
shared-lib/lib-ot/tests/node/transaction_compose_test.rs
Normal file
104
shared-lib/lib-ot/tests/node/transaction_compose_test.rs
Normal file
@ -0,0 +1,104 @@
|
||||
use crate::node::script::{edit_node_delta, make_node_delta_changeset};
|
||||
use lib_ot::core::{AttributeEntry, Changeset, NodeDataBuilder, NodeOperation, Transaction, TransactionBuilder};
|
||||
use lib_ot::text_delta::DeltaTextOperationBuilder;
|
||||
|
||||
#[test]
|
||||
fn transaction_compose_update_after_insert_test() {
|
||||
let (initial_delta, changeset, _) = make_node_delta_changeset("Hello", " world");
|
||||
let node_data = NodeDataBuilder::new("text").insert_delta(initial_delta).build();
|
||||
|
||||
// Modify the same path, the operations will be merged after composing if possible.
|
||||
let mut transaction_a = TransactionBuilder::new().insert_node_at_path(0, node_data).build();
|
||||
let transaction_b = TransactionBuilder::new().update_node_at_path(0, changeset).build();
|
||||
let _ = transaction_a.compose(transaction_b).unwrap();
|
||||
|
||||
// The operations are merged into one operation
|
||||
assert_eq!(transaction_a.operations.len(), 1);
|
||||
assert_eq!(
|
||||
transaction_a.to_json().unwrap(),
|
||||
r#"{"operations":[{"op":"insert","path":[0],"nodes":[{"type":"text","body":{"delta":[{"insert":"Hello world"}]}}]}]}"#
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transaction_compose_multiple_update_test() {
|
||||
let (initial_delta, changeset_1, final_delta) = make_node_delta_changeset("Hello", " world");
|
||||
let mut transaction = TransactionBuilder::new()
|
||||
.insert_node_at_path(0, NodeDataBuilder::new("text").insert_delta(initial_delta).build())
|
||||
.build();
|
||||
let (changeset_2, _) = edit_node_delta(
|
||||
&final_delta,
|
||||
DeltaTextOperationBuilder::new()
|
||||
.retain(final_delta.utf16_target_len)
|
||||
.insert("😁")
|
||||
.build(),
|
||||
);
|
||||
|
||||
let mut other_transaction = Transaction::new();
|
||||
|
||||
// the following two update operations will be merged into one
|
||||
let update_1 = TransactionBuilder::new().update_node_at_path(0, changeset_1).build();
|
||||
other_transaction.compose(update_1).unwrap();
|
||||
|
||||
let update_2 = TransactionBuilder::new().update_node_at_path(0, changeset_2).build();
|
||||
other_transaction.compose(update_2).unwrap();
|
||||
|
||||
let inverted = Transaction::from_operations(other_transaction.operations.inverted());
|
||||
|
||||
// the update operation will be merged into insert operation
|
||||
let _ = transaction.compose(other_transaction).unwrap();
|
||||
assert_eq!(transaction.operations.len(), 1);
|
||||
assert_eq!(
|
||||
transaction.to_json().unwrap(),
|
||||
r#"{"operations":[{"op":"insert","path":[0],"nodes":[{"type":"text","body":{"delta":[{"insert":"Hello world😁"}]}}]}]}"#
|
||||
);
|
||||
|
||||
let _ = transaction.compose(inverted).unwrap();
|
||||
assert_eq!(
|
||||
transaction.to_json().unwrap(),
|
||||
r#"{"operations":[{"op":"insert","path":[0],"nodes":[{"type":"text","body":{"delta":[{"insert":"Hello"}]}}]}]}"#
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transaction_compose_multiple_attribute_test() {
|
||||
let delta = DeltaTextOperationBuilder::new().insert("Hello").build();
|
||||
let node = NodeDataBuilder::new("text").insert_delta(delta).build();
|
||||
|
||||
let insert_operation = NodeOperation::Insert {
|
||||
path: 0.into(),
|
||||
nodes: vec![node],
|
||||
};
|
||||
|
||||
let mut transaction = Transaction::new();
|
||||
transaction.push_operation(insert_operation);
|
||||
|
||||
let new_attribute = AttributeEntry::new("subtype", "bulleted-list");
|
||||
let update_operation = NodeOperation::Update {
|
||||
path: 0.into(),
|
||||
changeset: Changeset::Attributes {
|
||||
new: new_attribute.clone().into(),
|
||||
old: Default::default(),
|
||||
},
|
||||
};
|
||||
transaction.push_operation(update_operation);
|
||||
assert_eq!(
|
||||
transaction.to_json().unwrap(),
|
||||
r#"{"operations":[{"op":"insert","path":[0],"nodes":[{"type":"text","body":{"delta":[{"insert":"Hello"}]}}]},{"op":"update","path":[0],"changeset":{"attributes":{"new":{"subtype":"bulleted-list"},"old":{}}}}]}"#
|
||||
);
|
||||
|
||||
let old_attribute = new_attribute;
|
||||
let new_attribute = AttributeEntry::new("subtype", "number-list");
|
||||
transaction.push_operation(NodeOperation::Update {
|
||||
path: 0.into(),
|
||||
changeset: Changeset::Attributes {
|
||||
new: new_attribute.into(),
|
||||
old: old_attribute.into(),
|
||||
},
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
transaction.to_json().unwrap(),
|
||||
r#"{"operations":[{"op":"insert","path":[0],"nodes":[{"type":"text","body":{"delta":[{"insert":"Hello"}]}}]},{"op":"update","path":[0],"changeset":{"attributes":{"new":{"subtype":"number-list"},"old":{"subtype":"bulleted-list"}}}}]}"#
|
||||
);
|
||||
}
|
@ -1,10 +1,7 @@
|
||||
use crate::node::script::NodeScript::*;
|
||||
use crate::node::script::NodeTest;
|
||||
use lib_ot::core::Body;
|
||||
use lib_ot::core::Changeset;
|
||||
use lib_ot::core::OperationTransform;
|
||||
use crate::node::script::{make_node_delta_changeset, NodeTest};
|
||||
|
||||
use lib_ot::core::{NodeData, NodeDataBuilder, Path};
|
||||
use lib_ot::text_delta::{DeltaTextOperationBuilder, DeltaTextOperations};
|
||||
|
||||
#[test]
|
||||
fn node_insert_test() {
|
||||
@ -37,71 +34,6 @@ fn node_insert_with_empty_path_test() {
|
||||
test.run_scripts(scripts);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn node_insert_with_not_exist_path_test() {
|
||||
let mut test = NodeTest::new();
|
||||
let node_data = NodeData::new("text");
|
||||
let path: Path = vec![0, 0, 9].into();
|
||||
let scripts = vec![
|
||||
InsertNode {
|
||||
path: path.clone(),
|
||||
node_data: node_data.clone(),
|
||||
rev_id: 1,
|
||||
},
|
||||
AssertNode {
|
||||
path,
|
||||
expected: Some(node_data),
|
||||
},
|
||||
];
|
||||
test.run_scripts(scripts);
|
||||
}
|
||||
|
||||
#[test]
|
||||
// Append the node to the end of the list if the insert path is out of bounds.
|
||||
fn node_insert_out_of_bound_test() {
|
||||
let mut test = NodeTest::new();
|
||||
let image_a = NodeData::new("image_a");
|
||||
let image_b = NodeData::new("image_b");
|
||||
let image = NodeDataBuilder::new("image_1")
|
||||
.add_node_data(image_a)
|
||||
.add_node_data(image_b)
|
||||
.build();
|
||||
let text_node = NodeDataBuilder::new("text_1").add_node_data(image).build();
|
||||
let image_c = NodeData::new("image_c");
|
||||
|
||||
let scripts = vec![
|
||||
InsertNode {
|
||||
path: 0.into(),
|
||||
node_data: text_node,
|
||||
rev_id: 1,
|
||||
},
|
||||
// 0:text_1
|
||||
// 0:image_1
|
||||
// 0:image_a
|
||||
// 1:image_b
|
||||
InsertNode {
|
||||
path: vec![0, 0, 10].into(),
|
||||
node_data: image_c.clone(),
|
||||
rev_id: 2,
|
||||
},
|
||||
// 0:text_1
|
||||
// 0:image_1
|
||||
// 0:image_a
|
||||
// 1:image_b
|
||||
// 2:image_b
|
||||
AssertNode {
|
||||
path: vec![0, 0, 2].into(),
|
||||
expected: Some(image_c),
|
||||
},
|
||||
AssertNode {
|
||||
path: vec![0, 0, 10].into(),
|
||||
expected: None,
|
||||
},
|
||||
];
|
||||
test.run_scripts(scripts);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tree_insert_multiple_nodes_at_root_path_test() {
|
||||
let mut test = NodeTest::new();
|
||||
@ -438,11 +370,11 @@ fn node_delete_node_from_list_test() {
|
||||
InsertNode {
|
||||
path: 1.into(),
|
||||
node_data: text_node_2.clone(),
|
||||
rev_id: 1,
|
||||
rev_id: 2,
|
||||
},
|
||||
DeleteNode {
|
||||
path: 0.into(),
|
||||
rev_id: 2,
|
||||
rev_id: 3,
|
||||
},
|
||||
AssertNode {
|
||||
path: 1.into(),
|
||||
@ -487,7 +419,7 @@ fn node_delete_nested_node_test() {
|
||||
InsertNode {
|
||||
path: 1.into(),
|
||||
node_data: text_node_2,
|
||||
rev_id: 1,
|
||||
rev_id: 2,
|
||||
},
|
||||
// 0:text_1
|
||||
// 0:image_1
|
||||
@ -499,7 +431,7 @@ fn node_delete_nested_node_test() {
|
||||
// 1:image_b
|
||||
DeleteNode {
|
||||
path: vec![0, 0, 0].into(),
|
||||
rev_id: 2,
|
||||
rev_id: 3,
|
||||
},
|
||||
// 0:text_1
|
||||
// 0:image_1
|
||||
@ -514,7 +446,7 @@ fn node_delete_nested_node_test() {
|
||||
},
|
||||
DeleteNode {
|
||||
path: vec![0, 0].into(),
|
||||
rev_id: 3,
|
||||
rev_id: 4,
|
||||
},
|
||||
// 0:text_1
|
||||
// 1:text_2
|
||||
@ -676,12 +608,16 @@ fn node_reorder_nodes_test() {
|
||||
// 1:image_b
|
||||
DeleteNode {
|
||||
path: vec![0].into(),
|
||||
rev_id: 2,
|
||||
rev_id: 3,
|
||||
},
|
||||
AssertNode {
|
||||
path: vec![0].into(),
|
||||
expected: Some(text_node_2.clone()),
|
||||
},
|
||||
InsertNode {
|
||||
path: vec![1].into(),
|
||||
node_data: text_node_1.clone(),
|
||||
rev_id: 3,
|
||||
rev_id: 4,
|
||||
},
|
||||
// 0:text_2
|
||||
// 0:image_2
|
||||
@ -722,10 +658,8 @@ fn node_reorder_nodes_test() {
|
||||
#[test]
|
||||
fn node_update_body_test() {
|
||||
let mut test = NodeTest::new();
|
||||
let (initial_delta, changeset, _, expected) = make_node_delta_changeset("Hello", "AppFlowy");
|
||||
let node = NodeDataBuilder::new("text")
|
||||
.insert_body(Body::Delta(initial_delta))
|
||||
.build();
|
||||
let (initial_delta, changeset, expected) = make_node_delta_changeset("Hello", "AppFlowy");
|
||||
let node = NodeDataBuilder::new("text").insert_delta(initial_delta).build();
|
||||
|
||||
let scripts = vec![
|
||||
InsertNode {
|
||||
@ -748,10 +682,8 @@ fn node_update_body_test() {
|
||||
#[test]
|
||||
fn node_inverted_body_changeset_test() {
|
||||
let mut test = NodeTest::new();
|
||||
let (initial_delta, changeset, inverted_changeset, _expected) = make_node_delta_changeset("Hello", "AppFlowy");
|
||||
let node = NodeDataBuilder::new("text")
|
||||
.insert_body(Body::Delta(initial_delta.clone()))
|
||||
.build();
|
||||
let (initial_delta, changeset, _expected) = make_node_delta_changeset("Hello", "AppFlowy");
|
||||
let node = NodeDataBuilder::new("text").insert_delta(initial_delta.clone()).build();
|
||||
|
||||
let scripts = vec![
|
||||
InsertNode {
|
||||
@ -761,11 +693,11 @@ fn node_inverted_body_changeset_test() {
|
||||
},
|
||||
UpdateBody {
|
||||
path: 0.into(),
|
||||
changeset,
|
||||
changeset: changeset.clone(),
|
||||
},
|
||||
UpdateBody {
|
||||
path: 0.into(),
|
||||
changeset: inverted_changeset,
|
||||
changeset: changeset.inverted(),
|
||||
},
|
||||
AssertNodeDelta {
|
||||
path: 0.into(),
|
||||
@ -774,27 +706,3 @@ fn node_inverted_body_changeset_test() {
|
||||
];
|
||||
test.run_scripts(scripts);
|
||||
}
|
||||
|
||||
fn make_node_delta_changeset(
|
||||
initial_content: &str,
|
||||
insert_str: &str,
|
||||
) -> (DeltaTextOperations, Changeset, Changeset, DeltaTextOperations) {
|
||||
let initial_content = initial_content.to_owned();
|
||||
let initial_delta = DeltaTextOperationBuilder::new().insert(&initial_content).build();
|
||||
let delta = DeltaTextOperationBuilder::new()
|
||||
.retain(initial_content.len())
|
||||
.insert(insert_str)
|
||||
.build();
|
||||
let inverted = delta.invert(&initial_delta);
|
||||
let expected = initial_delta.compose(&delta).unwrap();
|
||||
|
||||
let changeset = Changeset::Delta {
|
||||
delta: delta.clone(),
|
||||
inverted: inverted.clone(),
|
||||
};
|
||||
let inverted_changeset = Changeset::Delta {
|
||||
delta: inverted,
|
||||
inverted: delta,
|
||||
};
|
||||
(initial_delta, changeset, inverted_changeset, expected)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user