Merge pull request #1052 from AppFlowy-IO/feat/node_transform

This commit is contained in:
Nathan.fooo 2022-09-15 22:34:25 +08:00 committed by GitHub
commit b0ab4eff11
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
34 changed files with 1033 additions and 507 deletions

View File

@ -90,7 +90,7 @@ class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
final result = await service.openDocument(docId: view.id); final result = await service.openDocument(docId: view.id);
result.fold( result.fold(
(block) { (block) {
document = _decodeJsonToDocument(block.deltaStr); document = _decodeJsonToDocument(block.snapshot);
_subscription = document.changes.listen((event) { _subscription = document.changes.listen((event) {
final delta = event.item2; final delta = event.item2;
final documentDelta = document.toDelta(); final documentDelta = document.toDelta();
@ -115,16 +115,12 @@ class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
void _composeDelta(Delta composedDelta, Delta documentDelta) async { void _composeDelta(Delta composedDelta, Delta documentDelta) async {
final json = jsonEncode(composedDelta.toJson()); final json = jsonEncode(composedDelta.toJson());
Log.debug("doc_id: $view.id - Send json: $json"); Log.debug("doc_id: $view.id - Send json: $json");
final result = await service.composeDelta(docId: view.id, data: json); final result = await service.applyEdit(docId: view.id, data: json);
result.fold((rustDoc) { result.fold(
// final json = utf8.decode(doc.data); (_) {},
final rustDelta = Delta.fromJson(jsonDecode(rustDoc.deltaStr)); (r) => Log.error(r),
if (documentDelta != rustDelta) { );
Log.error("Receive : $rustDelta");
Log.error("Expected : $documentDelta");
}
}, (r) => null);
} }
Document _decodeJsonToDocument(String data) { Document _decodeJsonToDocument(String data) {

View File

@ -4,22 +4,28 @@ import 'package:flowy_sdk/dispatch/dispatch.dart';
import 'package:flowy_sdk/protobuf/flowy-folder/view.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-folder/view.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-sync/text_block.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-sync/text_block.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-text-block/entities.pb.dart';
class DocumentService { class DocumentService {
Future<Either<TextBlockDeltaPB, FlowyError>> openDocument({ Future<Either<TextBlockPB, FlowyError>> openDocument({
required String docId, required String docId,
}) async { }) async {
await FolderEventSetLatestView(ViewIdPB(value: docId)).send(); await FolderEventSetLatestView(ViewIdPB(value: docId)).send();
final payload = TextBlockIdPB(value: docId); final payload = TextBlockIdPB(value: docId);
return TextBlockEventGetBlockData(payload).send(); return TextBlockEventGetTextBlock(payload).send();
} }
Future<Either<TextBlockDeltaPB, FlowyError>> composeDelta({required String docId, required String data}) { Future<Either<Unit, FlowyError>> applyEdit({
final payload = TextBlockDeltaPB.create() required String docId,
..blockId = docId required String data,
..deltaStr = data; String operations = "",
return TextBlockEventApplyDelta(payload).send(); }) {
final payload = EditPayloadPB.create()
..textBlockId = docId
..operations = operations
..delta = data;
return TextBlockEventApplyEdit(payload).send();
} }
Future<Either<Unit, FlowyError>> closeDocument({required String docId}) { Future<Either<Unit, FlowyError>> closeDocument({required String docId}) {

View File

@ -4,7 +4,7 @@ use crate::{
}; };
use flowy_error::FlowyError; use flowy_error::FlowyError;
use flowy_sync::entities::text_block::{CreateTextBlockParams, DocumentPB, ResetTextBlockParams, TextBlockIdPB}; use flowy_sync::entities::text_block::{CreateTextBlockParams, DocumentPB, ResetTextBlockParams, TextBlockIdPB};
use flowy_text_block::BlockCloudService; use flowy_text_block::TextEditorCloudService;
use http_flowy::response::FlowyResponse; use http_flowy::response::FlowyResponse;
use lazy_static::lazy_static; use lazy_static::lazy_static;
use lib_infra::future::FutureResult; use lib_infra::future::FutureResult;
@ -20,20 +20,20 @@ impl BlockHttpCloudService {
} }
} }
impl BlockCloudService for BlockHttpCloudService { impl TextEditorCloudService for BlockHttpCloudService {
fn create_block(&self, token: &str, params: CreateTextBlockParams) -> FutureResult<(), FlowyError> { fn create_text_block(&self, token: &str, params: CreateTextBlockParams) -> FutureResult<(), FlowyError> {
let token = token.to_owned(); let token = token.to_owned();
let url = self.config.doc_url(); let url = self.config.doc_url();
FutureResult::new(async move { create_document_request(&token, params, &url).await }) FutureResult::new(async move { create_document_request(&token, params, &url).await })
} }
fn read_block(&self, token: &str, params: TextBlockIdPB) -> FutureResult<Option<DocumentPB>, FlowyError> { fn read_text_block(&self, token: &str, params: TextBlockIdPB) -> FutureResult<Option<DocumentPB>, FlowyError> {
let token = token.to_owned(); let token = token.to_owned();
let url = self.config.doc_url(); let url = self.config.doc_url();
FutureResult::new(async move { read_document_request(&token, params, &url).await }) FutureResult::new(async move { read_document_request(&token, params, &url).await })
} }
fn update_block(&self, token: &str, params: ResetTextBlockParams) -> FutureResult<(), FlowyError> { fn update_text_block(&self, token: &str, params: ResetTextBlockParams) -> FutureResult<(), FlowyError> {
let token = token.to_owned(); let token = token.to_owned();
let url = self.config.doc_url(); let url = self.config.doc_url();
FutureResult::new(async move { reset_doc_request(&token, params, &url).await }) FutureResult::new(async move { reset_doc_request(&token, params, &url).await })

View File

@ -261,7 +261,7 @@ use flowy_folder::entities::{
use flowy_folder_data_model::revision::{ use flowy_folder_data_model::revision::{
gen_app_id, gen_workspace_id, AppRevision, TrashRevision, ViewRevision, WorkspaceRevision, gen_app_id, gen_workspace_id, AppRevision, TrashRevision, ViewRevision, WorkspaceRevision,
}; };
use flowy_text_block::BlockCloudService; use flowy_text_block::TextEditorCloudService;
use flowy_user::entities::{ use flowy_user::entities::{
SignInParams, SignInResponse, SignUpParams, SignUpResponse, UpdateUserProfileParams, UserProfilePB, SignInParams, SignInResponse, SignUpParams, SignUpResponse, UpdateUserProfileParams, UserProfilePB,
}; };
@ -414,12 +414,12 @@ impl UserCloudService for LocalServer {
} }
} }
impl BlockCloudService for LocalServer { impl TextEditorCloudService for LocalServer {
fn create_block(&self, _token: &str, _params: CreateTextBlockParams) -> FutureResult<(), FlowyError> { fn create_text_block(&self, _token: &str, _params: CreateTextBlockParams) -> FutureResult<(), FlowyError> {
FutureResult::new(async { Ok(()) }) FutureResult::new(async { Ok(()) })
} }
fn read_block(&self, _token: &str, params: TextBlockIdPB) -> FutureResult<Option<DocumentPB>, FlowyError> { fn read_text_block(&self, _token: &str, params: TextBlockIdPB) -> FutureResult<Option<DocumentPB>, FlowyError> {
let doc = DocumentPB { let doc = DocumentPB {
block_id: params.value, block_id: params.value,
text: initial_quill_delta_string(), text: initial_quill_delta_string(),
@ -429,7 +429,7 @@ impl BlockCloudService for LocalServer {
FutureResult::new(async { Ok(Some(doc)) }) FutureResult::new(async { Ok(Some(doc)) })
} }
fn update_block(&self, _token: &str, _params: ResetTextBlockParams) -> FutureResult<(), FlowyError> { fn update_text_block(&self, _token: &str, _params: ResetTextBlockParams) -> FutureResult<(), FlowyError> {
FutureResult::new(async { Ok(()) }) FutureResult::new(async { Ok(()) })
} }
} }

View File

@ -18,7 +18,7 @@ use flowy_revision::{RevisionWebSocket, WSStateReceiver};
use flowy_sync::client_document::default::initial_quill_delta_string; use flowy_sync::client_document::default::initial_quill_delta_string;
use flowy_sync::entities::revision::{RepeatedRevision, Revision}; use flowy_sync::entities::revision::{RepeatedRevision, Revision};
use flowy_sync::entities::ws_data::ClientRevisionWSData; use flowy_sync::entities::ws_data::ClientRevisionWSData;
use flowy_text_block::TextBlockManager; use flowy_text_block::TextEditorManager;
use flowy_user::services::UserSession; use flowy_user::services::UserSession;
use futures_core::future::BoxFuture; use futures_core::future::BoxFuture;
use lib_infra::future::{BoxResultFuture, FutureResult}; use lib_infra::future::{BoxResultFuture, FutureResult};
@ -34,7 +34,7 @@ impl FolderDepsResolver {
user_session: Arc<UserSession>, user_session: Arc<UserSession>,
server_config: &ClientServerConfiguration, server_config: &ClientServerConfiguration,
ws_conn: &Arc<FlowyWebSocketConnect>, ws_conn: &Arc<FlowyWebSocketConnect>,
text_block_manager: &Arc<TextBlockManager>, text_block_manager: &Arc<TextEditorManager>,
grid_manager: &Arc<GridManager>, grid_manager: &Arc<GridManager>,
) -> Arc<FolderManager> { ) -> Arc<FolderManager> {
let user: Arc<dyn WorkspaceUser> = Arc::new(WorkspaceUserImpl(user_session.clone())); let user: Arc<dyn WorkspaceUser> = Arc::new(WorkspaceUserImpl(user_session.clone()));
@ -63,7 +63,7 @@ impl FolderDepsResolver {
} }
fn make_view_data_processor( fn make_view_data_processor(
text_block_manager: Arc<TextBlockManager>, text_block_manager: Arc<TextEditorManager>,
grid_manager: Arc<GridManager>, grid_manager: Arc<GridManager>,
) -> ViewDataProcessorMap { ) -> ViewDataProcessorMap {
let mut map: HashMap<ViewDataTypePB, Arc<dyn ViewDataProcessor + Send + Sync>> = HashMap::new(); let mut map: HashMap<ViewDataTypePB, Arc<dyn ViewDataProcessor + Send + Sync>> = HashMap::new();
@ -135,7 +135,7 @@ impl WSMessageReceiver for FolderWSMessageReceiverImpl {
} }
} }
struct TextBlockViewDataProcessor(Arc<TextBlockManager>); struct TextBlockViewDataProcessor(Arc<TextEditorManager>);
impl ViewDataProcessor for TextBlockViewDataProcessor { impl ViewDataProcessor for TextBlockViewDataProcessor {
fn initialize(&self) -> FutureResult<(), FlowyError> { fn initialize(&self) -> FutureResult<(), FlowyError> {
let manager = self.0.clone(); let manager = self.0.clone();
@ -147,7 +147,7 @@ impl ViewDataProcessor for TextBlockViewDataProcessor {
let view_id = view_id.to_string(); let view_id = view_id.to_string();
let manager = self.0.clone(); let manager = self.0.clone();
FutureResult::new(async move { FutureResult::new(async move {
let _ = manager.create_block(view_id, repeated_revision).await?; let _ = manager.create_text_block(view_id, repeated_revision).await?;
Ok(()) Ok(())
}) })
} }
@ -156,7 +156,7 @@ impl ViewDataProcessor for TextBlockViewDataProcessor {
let manager = self.0.clone(); let manager = self.0.clone();
let view_id = view_id.to_string(); let view_id = view_id.to_string();
FutureResult::new(async move { FutureResult::new(async move {
let _ = manager.delete_block(view_id)?; let _ = manager.close_text_editor(view_id)?;
Ok(()) Ok(())
}) })
} }
@ -165,7 +165,7 @@ impl ViewDataProcessor for TextBlockViewDataProcessor {
let manager = self.0.clone(); let manager = self.0.clone();
let view_id = view_id.to_string(); let view_id = view_id.to_string();
FutureResult::new(async move { FutureResult::new(async move {
let _ = manager.close_block(view_id)?; let _ = manager.close_text_editor(view_id)?;
Ok(()) Ok(())
}) })
} }
@ -174,7 +174,7 @@ impl ViewDataProcessor for TextBlockViewDataProcessor {
let view_id = view_id.to_string(); let view_id = view_id.to_string();
let manager = self.0.clone(); let manager = self.0.clone();
FutureResult::new(async move { FutureResult::new(async move {
let editor = manager.open_block(view_id).await?; let editor = manager.open_text_editor(view_id).await?;
let delta_bytes = Bytes::from(editor.delta_str().await?); let delta_bytes = Bytes::from(editor.delta_str().await?);
Ok(delta_bytes) Ok(delta_bytes)
}) })
@ -195,7 +195,7 @@ impl ViewDataProcessor for TextBlockViewDataProcessor {
let delta_data = Bytes::from(view_data); let delta_data = Bytes::from(view_data);
let repeated_revision: RepeatedRevision = let repeated_revision: RepeatedRevision =
Revision::initial_revision(&user_id, &view_id, delta_data.clone()).into(); Revision::initial_revision(&user_id, &view_id, delta_data.clone()).into();
let _ = manager.create_block(view_id, repeated_revision).await?; let _ = manager.create_text_block(view_id, repeated_revision).await?;
Ok(delta_data) Ok(delta_data)
}) })
} }

View File

@ -8,7 +8,7 @@ use flowy_revision::{RevisionWebSocket, WSStateReceiver};
use flowy_sync::entities::ws_data::ClientRevisionWSData; use flowy_sync::entities::ws_data::ClientRevisionWSData;
use flowy_text_block::{ use flowy_text_block::{
errors::{internal_error, FlowyError}, errors::{internal_error, FlowyError},
BlockCloudService, TextBlockManager, TextBlockUser, TextEditorCloudService, TextEditorManager, TextEditorUser,
}; };
use flowy_user::services::UserSession; use flowy_user::services::UserSession;
use futures_core::future::BoxFuture; use futures_core::future::BoxFuture;
@ -23,15 +23,15 @@ impl TextBlockDepsResolver {
ws_conn: Arc<FlowyWebSocketConnect>, ws_conn: Arc<FlowyWebSocketConnect>,
user_session: Arc<UserSession>, user_session: Arc<UserSession>,
server_config: &ClientServerConfiguration, server_config: &ClientServerConfiguration,
) -> Arc<TextBlockManager> { ) -> Arc<TextEditorManager> {
let user = Arc::new(BlockUserImpl(user_session)); let user = Arc::new(BlockUserImpl(user_session));
let rev_web_socket = Arc::new(TextBlockWebSocket(ws_conn.clone())); let rev_web_socket = Arc::new(TextBlockWebSocket(ws_conn.clone()));
let cloud_service: Arc<dyn BlockCloudService> = match local_server { let cloud_service: Arc<dyn TextEditorCloudService> = match local_server {
None => Arc::new(BlockHttpCloudService::new(server_config.clone())), None => Arc::new(BlockHttpCloudService::new(server_config.clone())),
Some(local_server) => local_server, Some(local_server) => local_server,
}; };
let manager = Arc::new(TextBlockManager::new(cloud_service, user, rev_web_socket)); let manager = Arc::new(TextEditorManager::new(cloud_service, user, rev_web_socket));
let receiver = Arc::new(DocumentWSMessageReceiverImpl(manager.clone())); let receiver = Arc::new(DocumentWSMessageReceiverImpl(manager.clone()));
ws_conn.add_ws_message_receiver(receiver).unwrap(); ws_conn.add_ws_message_receiver(receiver).unwrap();
@ -40,7 +40,7 @@ impl TextBlockDepsResolver {
} }
struct BlockUserImpl(Arc<UserSession>); struct BlockUserImpl(Arc<UserSession>);
impl TextBlockUser for BlockUserImpl { impl TextEditorUser for BlockUserImpl {
fn user_dir(&self) -> Result<String, FlowyError> { fn user_dir(&self) -> Result<String, FlowyError> {
let dir = self.0.user_dir().map_err(|e| FlowyError::unauthorized().context(e))?; let dir = self.0.user_dir().map_err(|e| FlowyError::unauthorized().context(e))?;
@ -90,7 +90,7 @@ impl RevisionWebSocket for TextBlockWebSocket {
} }
} }
struct DocumentWSMessageReceiverImpl(Arc<TextBlockManager>); struct DocumentWSMessageReceiverImpl(Arc<TextEditorManager>);
impl WSMessageReceiver for DocumentWSMessageReceiverImpl { impl WSMessageReceiver for DocumentWSMessageReceiverImpl {
fn source(&self) -> WSChannel { fn source(&self) -> WSChannel {
WSChannel::Document WSChannel::Document

View File

@ -11,7 +11,7 @@ use flowy_net::{
local_server::LocalServer, local_server::LocalServer,
ws::connection::{listen_on_websocket, FlowyWebSocketConnect}, ws::connection::{listen_on_websocket, FlowyWebSocketConnect},
}; };
use flowy_text_block::TextBlockManager; use flowy_text_block::TextEditorManager;
use flowy_user::services::{notifier::UserStatus, UserSession, UserSessionConfig}; use flowy_user::services::{notifier::UserStatus, UserSession, UserSessionConfig};
use lib_dispatch::prelude::*; use lib_dispatch::prelude::*;
use lib_dispatch::runtime::tokio_default_runtime; use lib_dispatch::runtime::tokio_default_runtime;
@ -89,7 +89,7 @@ pub struct FlowySDK {
#[allow(dead_code)] #[allow(dead_code)]
config: FlowySDKConfig, config: FlowySDKConfig,
pub user_session: Arc<UserSession>, pub user_session: Arc<UserSession>,
pub text_block_manager: Arc<TextBlockManager>, pub text_block_manager: Arc<TextEditorManager>,
pub folder_manager: Arc<FolderManager>, pub folder_manager: Arc<FolderManager>,
pub grid_manager: Arc<GridManager>, pub grid_manager: Arc<GridManager>,
pub dispatcher: Arc<EventDispatcher>, pub dispatcher: Arc<EventDispatcher>,

View File

@ -1,7 +1,7 @@
use flowy_folder::manager::FolderManager; use flowy_folder::manager::FolderManager;
use flowy_grid::manager::GridManager; use flowy_grid::manager::GridManager;
use flowy_net::ws::connection::FlowyWebSocketConnect; use flowy_net::ws::connection::FlowyWebSocketConnect;
use flowy_text_block::TextBlockManager; use flowy_text_block::TextEditorManager;
use flowy_user::services::UserSession; use flowy_user::services::UserSession;
use lib_dispatch::prelude::Module; use lib_dispatch::prelude::Module;
use std::sync::Arc; use std::sync::Arc;
@ -11,7 +11,7 @@ pub fn mk_modules(
folder_manager: &Arc<FolderManager>, folder_manager: &Arc<FolderManager>,
grid_manager: &Arc<GridManager>, grid_manager: &Arc<GridManager>,
user_session: &Arc<UserSession>, user_session: &Arc<UserSession>,
text_block_manager: &Arc<TextBlockManager>, text_block_manager: &Arc<TextEditorManager>,
) -> Vec<Module> { ) -> Vec<Module> {
let user_module = mk_user_module(user_session.clone()); let user_module = mk_user_module(user_session.clone());
let folder_module = mk_folder_module(folder_manager.clone()); let folder_module = mk_folder_module(folder_manager.clone());
@ -43,6 +43,6 @@ fn mk_grid_module(grid_manager: Arc<GridManager>) -> Module {
flowy_grid::event_map::create(grid_manager) flowy_grid::event_map::create(grid_manager)
} }
fn mk_text_block_module(text_block_manager: Arc<TextBlockManager>) -> Module { fn mk_text_block_module(text_block_manager: Arc<TextEditorManager>) -> Module {
flowy_text_block::event_map::create(text_block_manager) flowy_text_block::event_map::create(text_block_manager)
} }

View File

@ -2,7 +2,7 @@ use crate::web_socket::EditorCommandSender;
use crate::{ use crate::{
errors::FlowyError, errors::FlowyError,
queue::{EditBlockQueue, EditorCommand}, queue::{EditBlockQueue, EditorCommand},
TextBlockUser, TextEditorUser,
}; };
use bytes::Bytes; use bytes::Bytes;
use flowy_error::{internal_error, FlowyResult}; use flowy_error::{internal_error, FlowyResult};
@ -24,7 +24,6 @@ use tokio::sync::{mpsc, oneshot};
pub struct TextBlockEditor { pub struct TextBlockEditor {
pub doc_id: String, pub doc_id: String,
#[allow(dead_code)]
rev_manager: Arc<RevisionManager>, rev_manager: Arc<RevisionManager>,
#[cfg(feature = "sync")] #[cfg(feature = "sync")]
ws_manager: Arc<flowy_revision::RevisionWebSocketManager>, ws_manager: Arc<flowy_revision::RevisionWebSocketManager>,
@ -35,7 +34,7 @@ impl TextBlockEditor {
#[allow(unused_variables)] #[allow(unused_variables)]
pub(crate) async fn new( pub(crate) async fn new(
doc_id: &str, doc_id: &str,
user: Arc<dyn TextBlockUser>, user: Arc<dyn TextEditorUser>,
mut rev_manager: RevisionManager, mut rev_manager: RevisionManager,
rev_web_socket: Arc<dyn RevisionWebSocket>, rev_web_socket: Arc<dyn RevisionWebSocket>,
cloud_service: Arc<dyn RevisionCloudService>, cloud_service: Arc<dyn RevisionCloudService>,
@ -194,7 +193,7 @@ impl std::ops::Drop for TextBlockEditor {
// The edit queue will exit after the EditorCommandSender was dropped. // The edit queue will exit after the EditorCommandSender was dropped.
fn spawn_edit_queue( fn spawn_edit_queue(
user: Arc<dyn TextBlockUser>, user: Arc<dyn TextEditorUser>,
rev_manager: Arc<RevisionManager>, rev_manager: Arc<RevisionManager>,
delta: TextDelta, delta: TextDelta,
) -> EditorCommandSender { ) -> EditorCommandSender {

View File

@ -29,6 +29,52 @@ impl std::convert::From<i32> for ExportType {
} }
} }
#[derive(Default, ProtoBuf)]
pub struct EditPayloadPB {
#[pb(index = 1)]
pub text_block_id: String,
// Encode in JSON format
#[pb(index = 2)]
pub operations: String,
// Encode in JSON format
#[pb(index = 3)]
pub delta: String,
}
#[derive(Default)]
pub struct EditParams {
pub text_block_id: String,
// Encode in JSON format
pub operations: String,
// Encode in JSON format
pub delta: String,
}
impl TryInto<EditParams> for EditPayloadPB {
type Error = ErrorCode;
fn try_into(self) -> Result<EditParams, Self::Error> {
Ok(EditParams {
text_block_id: self.text_block_id,
operations: self.operations,
delta: self.delta,
})
}
}
#[derive(Default, ProtoBuf)]
pub struct TextBlockPB {
#[pb(index = 1)]
pub text_block_id: String,
/// Encode in JSON format
#[pb(index = 2)]
pub snapshot: String,
}
#[derive(Default, ProtoBuf)] #[derive(Default, ProtoBuf)]
pub struct ExportPayloadPB { pub struct ExportPayloadPB {
#[pb(index = 1)] #[pb(index = 1)]

View File

@ -1,39 +1,40 @@
use crate::entities::{ExportDataPB, ExportParams, ExportPayloadPB}; use crate::entities::{EditParams, EditPayloadPB, ExportDataPB, ExportParams, ExportPayloadPB, TextBlockPB};
use crate::TextBlockManager; use crate::TextEditorManager;
use flowy_error::FlowyError; use flowy_error::FlowyError;
use flowy_sync::entities::text_block::{TextBlockDeltaPB, TextBlockIdPB}; use flowy_sync::entities::text_block::TextBlockIdPB;
use lib_dispatch::prelude::{data_result, AppData, Data, DataResult}; use lib_dispatch::prelude::{data_result, AppData, Data, DataResult};
use std::convert::TryInto; use std::convert::TryInto;
use std::sync::Arc; use std::sync::Arc;
pub(crate) async fn get_block_data_handler( pub(crate) async fn get_text_block_handler(
data: Data<TextBlockIdPB>, data: Data<TextBlockIdPB>,
manager: AppData<Arc<TextBlockManager>>, manager: AppData<Arc<TextEditorManager>>,
) -> DataResult<TextBlockDeltaPB, FlowyError> { ) -> DataResult<TextBlockPB, FlowyError> {
let block_id: TextBlockIdPB = data.into_inner(); let text_block_id: TextBlockIdPB = data.into_inner();
let editor = manager.open_block(&block_id).await?; let editor = manager.open_text_editor(&text_block_id).await?;
let delta_str = editor.delta_str().await?; let delta_str = editor.delta_str().await?;
data_result(TextBlockDeltaPB { data_result(TextBlockPB {
block_id: block_id.into(), text_block_id: text_block_id.into(),
delta_str, snapshot: delta_str,
}) })
} }
pub(crate) async fn apply_delta_handler( pub(crate) async fn apply_edit_handler(
data: Data<TextBlockDeltaPB>, data: Data<EditPayloadPB>,
manager: AppData<Arc<TextBlockManager>>, manager: AppData<Arc<TextEditorManager>>,
) -> DataResult<TextBlockDeltaPB, FlowyError> { ) -> Result<(), FlowyError> {
let block_delta = manager.receive_local_delta(data.into_inner()).await?; let params: EditParams = data.into_inner().try_into()?;
data_result(block_delta) let _ = manager.apply_edit(params).await?;
Ok(())
} }
#[tracing::instrument(level = "debug", skip(data, manager), err)] #[tracing::instrument(level = "debug", skip(data, manager), err)]
pub(crate) async fn export_handler( pub(crate) async fn export_handler(
data: Data<ExportPayloadPB>, data: Data<ExportPayloadPB>,
manager: AppData<Arc<TextBlockManager>>, manager: AppData<Arc<TextEditorManager>>,
) -> DataResult<ExportDataPB, FlowyError> { ) -> DataResult<ExportDataPB, FlowyError> {
let params: ExportParams = data.into_inner().try_into()?; let params: ExportParams = data.into_inner().try_into()?;
let editor = manager.open_block(&params.view_id).await?; let editor = manager.open_text_editor(&params.view_id).await?;
let delta_json = editor.delta_str().await?; let delta_json = editor.delta_str().await?;
data_result(ExportDataPB { data_result(ExportDataPB {
data: delta_json, data: delta_json,

View File

@ -1,16 +1,16 @@
use crate::event_handler::*; use crate::event_handler::*;
use crate::TextBlockManager; use crate::TextEditorManager;
use flowy_derive::{Flowy_Event, ProtoBuf_Enum}; use flowy_derive::{Flowy_Event, ProtoBuf_Enum};
use lib_dispatch::prelude::Module; use lib_dispatch::prelude::Module;
use std::sync::Arc; use std::sync::Arc;
use strum_macros::Display; use strum_macros::Display;
pub fn create(block_manager: Arc<TextBlockManager>) -> Module { pub fn create(block_manager: Arc<TextEditorManager>) -> Module {
let mut module = Module::new().name(env!("CARGO_PKG_NAME")).data(block_manager); let mut module = Module::new().name(env!("CARGO_PKG_NAME")).data(block_manager);
module = module module = module
.event(TextBlockEvent::GetBlockData, get_block_data_handler) .event(TextBlockEvent::GetTextBlock, get_text_block_handler)
.event(TextBlockEvent::ApplyDelta, apply_delta_handler) .event(TextBlockEvent::ApplyEdit, apply_edit_handler)
.event(TextBlockEvent::ExportDocument, export_handler); .event(TextBlockEvent::ExportDocument, export_handler);
module module
@ -19,11 +19,11 @@ pub fn create(block_manager: Arc<TextBlockManager>) -> Module {
#[derive(Clone, Copy, PartialEq, Eq, Debug, Display, Hash, ProtoBuf_Enum, Flowy_Event)] #[derive(Clone, Copy, PartialEq, Eq, Debug, Display, Hash, ProtoBuf_Enum, Flowy_Event)]
#[event_err = "FlowyError"] #[event_err = "FlowyError"]
pub enum TextBlockEvent { pub enum TextBlockEvent {
#[event(input = "TextBlockIdPB", output = "TextBlockDeltaPB")] #[event(input = "TextBlockIdPB", output = "TextBlockPB")]
GetBlockData = 0, GetTextBlock = 0,
#[event(input = "TextBlockDeltaPB", output = "TextBlockDeltaPB")] #[event(input = "EditPayloadPB")]
ApplyDelta = 1, ApplyEdit = 1,
#[event(input = "ExportPayloadPB", output = "ExportDataPB")] #[event(input = "ExportPayloadPB", output = "ExportDataPB")]
ExportDocument = 2, ExportDocument = 2,

View File

@ -18,10 +18,10 @@ use crate::errors::FlowyError;
use flowy_sync::entities::text_block::{CreateTextBlockParams, DocumentPB, ResetTextBlockParams, TextBlockIdPB}; use flowy_sync::entities::text_block::{CreateTextBlockParams, DocumentPB, ResetTextBlockParams, TextBlockIdPB};
use lib_infra::future::FutureResult; use lib_infra::future::FutureResult;
pub trait BlockCloudService: Send + Sync { pub trait TextEditorCloudService: Send + Sync {
fn create_block(&self, token: &str, params: CreateTextBlockParams) -> FutureResult<(), FlowyError>; fn create_text_block(&self, token: &str, params: CreateTextBlockParams) -> FutureResult<(), FlowyError>;
fn read_block(&self, token: &str, params: TextBlockIdPB) -> FutureResult<Option<DocumentPB>, FlowyError>; fn read_text_block(&self, token: &str, params: TextBlockIdPB) -> FutureResult<Option<DocumentPB>, FlowyError>;
fn update_block(&self, token: &str, params: ResetTextBlockParams) -> FutureResult<(), FlowyError>; fn update_text_block(&self, token: &str, params: ResetTextBlockParams) -> FutureResult<(), FlowyError>;
} }

View File

@ -1,5 +1,6 @@
use crate::entities::EditParams;
use crate::queue::TextBlockRevisionCompactor; use crate::queue::TextBlockRevisionCompactor;
use crate::{editor::TextBlockEditor, errors::FlowyError, BlockCloudService}; use crate::{editor::TextBlockEditor, errors::FlowyError, TextEditorCloudService};
use bytes::Bytes; use bytes::Bytes;
use dashmap::DashMap; use dashmap::DashMap;
use flowy_database::ConnectionPool; use flowy_database::ConnectionPool;
@ -16,30 +17,30 @@ use flowy_sync::entities::{
use lib_infra::future::FutureResult; use lib_infra::future::FutureResult;
use std::{convert::TryInto, sync::Arc}; use std::{convert::TryInto, sync::Arc};
pub trait TextBlockUser: Send + Sync { pub trait TextEditorUser: Send + Sync {
fn user_dir(&self) -> Result<String, FlowyError>; fn user_dir(&self) -> Result<String, FlowyError>;
fn user_id(&self) -> Result<String, FlowyError>; fn user_id(&self) -> Result<String, FlowyError>;
fn token(&self) -> Result<String, FlowyError>; fn token(&self) -> Result<String, FlowyError>;
fn db_pool(&self) -> Result<Arc<ConnectionPool>, FlowyError>; fn db_pool(&self) -> Result<Arc<ConnectionPool>, FlowyError>;
} }
pub struct TextBlockManager { pub struct TextEditorManager {
cloud_service: Arc<dyn BlockCloudService>, cloud_service: Arc<dyn TextEditorCloudService>,
rev_web_socket: Arc<dyn RevisionWebSocket>, rev_web_socket: Arc<dyn RevisionWebSocket>,
editor_map: Arc<TextBlockEditorMap>, editor_map: Arc<TextEditorMap>,
user: Arc<dyn TextBlockUser>, user: Arc<dyn TextEditorUser>,
} }
impl TextBlockManager { impl TextEditorManager {
pub fn new( pub fn new(
cloud_service: Arc<dyn BlockCloudService>, cloud_service: Arc<dyn TextEditorCloudService>,
text_block_user: Arc<dyn TextBlockUser>, text_block_user: Arc<dyn TextEditorUser>,
rev_web_socket: Arc<dyn RevisionWebSocket>, rev_web_socket: Arc<dyn RevisionWebSocket>,
) -> Self { ) -> Self {
Self { Self {
cloud_service, cloud_service,
rev_web_socket, rev_web_socket,
editor_map: Arc::new(TextBlockEditorMap::new()), editor_map: Arc::new(TextEditorMap::new()),
user: text_block_user, user: text_block_user,
} }
} }
@ -50,45 +51,47 @@ impl TextBlockManager {
Ok(()) Ok(())
} }
#[tracing::instrument(level = "trace", skip(self, block_id), fields(block_id), err)] #[tracing::instrument(level = "trace", skip(self, editor_id), fields(editor_id), err)]
pub async fn open_block<T: AsRef<str>>(&self, block_id: T) -> Result<Arc<TextBlockEditor>, FlowyError> { pub async fn open_text_editor<T: AsRef<str>>(&self, editor_id: T) -> Result<Arc<TextBlockEditor>, FlowyError> {
let block_id = block_id.as_ref(); let editor_id = editor_id.as_ref();
tracing::Span::current().record("block_id", &block_id); tracing::Span::current().record("editor_id", &editor_id);
self.get_block_editor(block_id).await self.get_text_editor(editor_id).await
} }
#[tracing::instrument(level = "trace", skip(self, block_id), fields(block_id), err)] #[tracing::instrument(level = "trace", skip(self, editor_id), fields(editor_id), err)]
pub fn close_block<T: AsRef<str>>(&self, block_id: T) -> Result<(), FlowyError> { pub fn close_text_editor<T: AsRef<str>>(&self, editor_id: T) -> Result<(), FlowyError> {
let block_id = block_id.as_ref(); let editor_id = editor_id.as_ref();
tracing::Span::current().record("block_id", &block_id); tracing::Span::current().record("editor_id", &editor_id);
self.editor_map.remove(block_id); self.editor_map.remove(editor_id);
Ok(()) Ok(())
} }
#[tracing::instrument(level = "debug", skip(self, doc_id), fields(doc_id), err)] #[tracing::instrument(level = "debug", skip(self, delta), err)]
pub fn delete_block<T: AsRef<str>>(&self, doc_id: T) -> Result<(), FlowyError> {
let doc_id = doc_id.as_ref();
tracing::Span::current().record("doc_id", &doc_id);
self.editor_map.remove(doc_id);
Ok(())
}
#[tracing::instrument(level = "debug", skip(self, delta), fields(doc_id = %delta.block_id), err)]
pub async fn receive_local_delta(&self, delta: TextBlockDeltaPB) -> Result<TextBlockDeltaPB, FlowyError> { pub async fn receive_local_delta(&self, delta: TextBlockDeltaPB) -> Result<TextBlockDeltaPB, FlowyError> {
let editor = self.get_block_editor(&delta.block_id).await?; let editor = self.get_text_editor(&delta.text_block_id).await?;
let _ = editor.compose_local_delta(Bytes::from(delta.delta_str)).await?; let _ = editor.compose_local_delta(Bytes::from(delta.delta_str)).await?;
let document_json = editor.delta_str().await?; let delta_str = editor.delta_str().await?;
Ok(TextBlockDeltaPB { Ok(TextBlockDeltaPB {
block_id: delta.block_id.clone(), text_block_id: delta.text_block_id.clone(),
delta_str: document_json, delta_str,
}) })
} }
pub async fn create_block<T: AsRef<str>>(&self, doc_id: T, revisions: RepeatedRevision) -> FlowyResult<()> { pub async fn apply_edit(&self, params: EditParams) -> FlowyResult<()> {
let doc_id = doc_id.as_ref().to_owned(); let editor = self.get_text_editor(&params.text_block_id).await?;
let _ = editor.compose_local_delta(Bytes::from(params.delta)).await?;
Ok(())
}
pub async fn create_text_block<T: AsRef<str>>(
&self,
text_block_id: T,
revisions: RepeatedRevision,
) -> FlowyResult<()> {
let doc_id = text_block_id.as_ref().to_owned();
let db_pool = self.user.db_pool()?; let db_pool = self.user.db_pool()?;
// Maybe we could save the block to disk without creating the RevisionManager // Maybe we could save the block to disk without creating the RevisionManager
let rev_manager = self.make_rev_manager(&doc_id, db_pool)?; let rev_manager = self.make_text_block_rev_manager(&doc_id, db_pool)?;
let _ = rev_manager.reset_object(revisions).await?; let _ = rev_manager.reset_object(revisions).await?;
Ok(()) Ok(())
} }
@ -110,26 +113,26 @@ impl TextBlockManager {
} }
} }
impl TextBlockManager { impl TextEditorManager {
async fn get_block_editor(&self, block_id: &str) -> FlowyResult<Arc<TextBlockEditor>> { async fn get_text_editor(&self, block_id: &str) -> FlowyResult<Arc<TextBlockEditor>> {
match self.editor_map.get(block_id) { match self.editor_map.get(block_id) {
None => { None => {
let db_pool = self.user.db_pool()?; let db_pool = self.user.db_pool()?;
self.make_text_block_editor(block_id, db_pool).await self.make_text_editor(block_id, db_pool).await
} }
Some(editor) => Ok(editor), Some(editor) => Ok(editor),
} }
} }
#[tracing::instrument(level = "trace", skip(self, pool), err)] #[tracing::instrument(level = "trace", skip(self, pool), err)]
async fn make_text_block_editor( async fn make_text_editor(
&self, &self,
block_id: &str, block_id: &str,
pool: Arc<ConnectionPool>, pool: Arc<ConnectionPool>,
) -> Result<Arc<TextBlockEditor>, FlowyError> { ) -> Result<Arc<TextBlockEditor>, FlowyError> {
let user = self.user.clone(); let user = self.user.clone();
let token = self.user.token()?; let token = self.user.token()?;
let rev_manager = self.make_rev_manager(block_id, pool.clone())?; let rev_manager = self.make_text_block_rev_manager(block_id, pool.clone())?;
let cloud_service = Arc::new(TextBlockRevisionCloudService { let cloud_service = Arc::new(TextBlockRevisionCloudService {
token, token,
server: self.cloud_service.clone(), server: self.cloud_service.clone(),
@ -140,7 +143,11 @@ impl TextBlockManager {
Ok(doc_editor) Ok(doc_editor)
} }
fn make_rev_manager(&self, doc_id: &str, pool: Arc<ConnectionPool>) -> Result<RevisionManager, FlowyError> { fn make_text_block_rev_manager(
&self,
doc_id: &str,
pool: Arc<ConnectionPool>,
) -> Result<RevisionManager, FlowyError> {
let user_id = self.user.user_id()?; let user_id = self.user.user_id()?;
let disk_cache = SQLiteTextBlockRevisionPersistence::new(&user_id, pool.clone()); let disk_cache = SQLiteTextBlockRevisionPersistence::new(&user_id, pool.clone());
let rev_persistence = RevisionPersistence::new(&user_id, doc_id, disk_cache); let rev_persistence = RevisionPersistence::new(&user_id, doc_id, disk_cache);
@ -161,7 +168,7 @@ impl TextBlockManager {
struct TextBlockRevisionCloudService { struct TextBlockRevisionCloudService {
token: String, token: String,
server: Arc<dyn BlockCloudService>, server: Arc<dyn TextEditorCloudService>,
} }
impl RevisionCloudService for TextBlockRevisionCloudService { impl RevisionCloudService for TextBlockRevisionCloudService {
@ -173,7 +180,7 @@ impl RevisionCloudService for TextBlockRevisionCloudService {
let user_id = user_id.to_string(); let user_id = user_id.to_string();
FutureResult::new(async move { FutureResult::new(async move {
match server.read_block(&token, params).await? { match server.read_text_block(&token, params).await? {
None => Err(FlowyError::record_not_found().context("Remote doesn't have this document")), None => Err(FlowyError::record_not_found().context("Remote doesn't have this document")),
Some(doc) => { Some(doc) => {
let delta_data = Bytes::from(doc.text.clone()); let delta_data = Bytes::from(doc.text.clone());
@ -193,36 +200,36 @@ impl RevisionCloudService for TextBlockRevisionCloudService {
} }
} }
pub struct TextBlockEditorMap { pub struct TextEditorMap {
inner: DashMap<String, Arc<TextBlockEditor>>, inner: DashMap<String, Arc<TextBlockEditor>>,
} }
impl TextBlockEditorMap { impl TextEditorMap {
fn new() -> Self { fn new() -> Self {
Self { inner: DashMap::new() } Self { inner: DashMap::new() }
} }
pub(crate) fn insert(&self, block_id: &str, doc: &Arc<TextBlockEditor>) { pub(crate) fn insert(&self, editor_id: &str, doc: &Arc<TextBlockEditor>) {
if self.inner.contains_key(block_id) { if self.inner.contains_key(editor_id) {
log::warn!("Doc:{} already exists in cache", block_id); log::warn!("Doc:{} already exists in cache", editor_id);
} }
self.inner.insert(block_id.to_string(), doc.clone()); self.inner.insert(editor_id.to_string(), doc.clone());
} }
pub(crate) fn get(&self, block_id: &str) -> Option<Arc<TextBlockEditor>> { pub(crate) fn get(&self, editor_id: &str) -> Option<Arc<TextBlockEditor>> {
Some(self.inner.get(block_id)?.clone()) Some(self.inner.get(editor_id)?.clone())
} }
pub(crate) fn remove(&self, block_id: &str) { pub(crate) fn remove(&self, editor_id: &str) {
if let Some(editor) = self.get(block_id) { if let Some(editor) = self.get(editor_id) {
editor.stop() editor.stop()
} }
self.inner.remove(block_id); self.inner.remove(editor_id);
} }
} }
#[tracing::instrument(level = "trace", skip(web_socket, handlers))] #[tracing::instrument(level = "trace", skip(web_socket, handlers))]
fn listen_ws_state_changed(web_socket: Arc<dyn RevisionWebSocket>, handlers: Arc<TextBlockEditorMap>) { fn listen_ws_state_changed(web_socket: Arc<dyn RevisionWebSocket>, handlers: Arc<TextEditorMap>) {
tokio::spawn(async move { tokio::spawn(async move {
let mut notify = web_socket.subscribe_state_changed().await; let mut notify = web_socket.subscribe_state_changed().await;
while let Ok(state) = notify.recv().await { while let Ok(state) = notify.recv().await {

View File

@ -1,5 +1,5 @@
use crate::web_socket::EditorCommandReceiver; use crate::web_socket::EditorCommandReceiver;
use crate::TextBlockUser; use crate::TextEditorUser;
use async_stream::stream; use async_stream::stream;
use bytes::Bytes; use bytes::Bytes;
use flowy_error::{FlowyError, FlowyResult}; use flowy_error::{FlowyError, FlowyResult};
@ -23,14 +23,14 @@ use tokio::sync::{oneshot, RwLock};
// serial. // serial.
pub(crate) struct EditBlockQueue { pub(crate) struct EditBlockQueue {
document: Arc<RwLock<ClientDocument>>, document: Arc<RwLock<ClientDocument>>,
user: Arc<dyn TextBlockUser>, user: Arc<dyn TextEditorUser>,
rev_manager: Arc<RevisionManager>, rev_manager: Arc<RevisionManager>,
receiver: Option<EditorCommandReceiver>, receiver: Option<EditorCommandReceiver>,
} }
impl EditBlockQueue { impl EditBlockQueue {
pub(crate) fn new( pub(crate) fn new(
user: Arc<dyn TextBlockUser>, user: Arc<dyn TextEditorUser>,
rev_manager: Arc<RevisionManager>, rev_manager: Arc<RevisionManager>,
delta: TextDelta, delta: TextDelta,
receiver: EditorCommandReceiver, receiver: EditorCommandReceiver,

View File

@ -27,7 +27,7 @@ impl TextBlockEditorTest {
let sdk = FlowySDKTest::default(); let sdk = FlowySDKTest::default();
let _ = sdk.init_user().await; let _ = sdk.init_user().await;
let test = ViewTest::new_text_block_view(&sdk).await; let test = ViewTest::new_text_block_view(&sdk).await;
let editor = sdk.text_block_manager.open_block(&test.view.id).await.unwrap(); let editor = sdk.text_block_manager.open_text_editor(&test.view.id).await.unwrap();
Self { sdk, editor } Self { sdk, editor }
} }

View File

@ -69,7 +69,7 @@ pub struct ResetTextBlockParams {
#[derive(ProtoBuf, Default, Debug, Clone)] #[derive(ProtoBuf, Default, Debug, Clone)]
pub struct TextBlockDeltaPB { pub struct TextBlockDeltaPB {
#[pb(index = 1)] #[pb(index = 1)]
pub block_id: String, pub text_block_id: String,
#[pb(index = 2)] #[pb(index = 2)]
pub delta_str: String, pub delta_str: String,

View File

@ -7,7 +7,7 @@ edition = "2018"
[dependencies] [dependencies]
bytecount = "0.6.0" bytecount = "0.6.0"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive", "rc"] }
#protobuf = {version = "2.18.0"} #protobuf = {version = "2.18.0"}
#flowy-derive = { path = "../flowy-derive" } #flowy-derive = { path = "../flowy-derive" }
tokio = { version = "1", features = ["sync"] } tokio = { version = "1", features = ["sync"] }

View File

@ -1,143 +0,0 @@
use crate::core::attributes::Attributes;
use crate::core::document::path::Path;
use crate::core::{NodeBodyChangeset, NodeData};
use crate::errors::OTError;
use serde::{Deserialize, Serialize};
#[derive(Clone, Serialize, Deserialize)]
#[serde(tag = "op")]
pub enum NodeOperation {
#[serde(rename = "insert")]
Insert { path: Path, nodes: Vec<NodeData> },
#[serde(rename = "update")]
UpdateAttributes {
path: Path,
attributes: Attributes,
#[serde(rename = "oldAttributes")]
old_attributes: Attributes,
},
#[serde(rename = "update-body")]
// #[serde(serialize_with = "serialize_edit_body")]
// #[serde(deserialize_with = "deserialize_edit_body")]
UpdateBody { path: Path, changeset: NodeBodyChangeset },
#[serde(rename = "delete")]
Delete { path: Path, nodes: Vec<NodeData> },
}
impl NodeOperation {
pub fn path(&self) -> &Path {
match self {
NodeOperation::Insert { path, .. } => path,
NodeOperation::UpdateAttributes { path, .. } => path,
NodeOperation::Delete { path, .. } => path,
NodeOperation::UpdateBody { path, .. } => path,
}
}
pub fn invert(&self) -> NodeOperation {
match self {
NodeOperation::Insert { path, nodes } => NodeOperation::Delete {
path: path.clone(),
nodes: nodes.clone(),
},
NodeOperation::UpdateAttributes {
path,
attributes,
old_attributes,
} => NodeOperation::UpdateAttributes {
path: path.clone(),
attributes: old_attributes.clone(),
old_attributes: attributes.clone(),
},
NodeOperation::Delete { path, nodes } => NodeOperation::Insert {
path: path.clone(),
nodes: nodes.clone(),
},
NodeOperation::UpdateBody { path, changeset: body } => NodeOperation::UpdateBody {
path: path.clone(),
changeset: body.inverted(),
},
}
}
pub fn clone_with_new_path(&self, path: Path) -> NodeOperation {
match self {
NodeOperation::Insert { nodes, .. } => NodeOperation::Insert {
path,
nodes: nodes.clone(),
},
NodeOperation::UpdateAttributes {
attributes,
old_attributes,
..
} => NodeOperation::UpdateAttributes {
path,
attributes: attributes.clone(),
old_attributes: old_attributes.clone(),
},
NodeOperation::Delete { nodes, .. } => NodeOperation::Delete {
path,
nodes: nodes.clone(),
},
NodeOperation::UpdateBody { path, changeset } => NodeOperation::UpdateBody {
path: path.clone(),
changeset: changeset.clone(),
},
}
}
pub fn transform(a: &NodeOperation, b: &NodeOperation) -> NodeOperation {
match a {
NodeOperation::Insert { path: a_path, nodes } => {
let new_path = Path::transform(a_path, b.path(), nodes.len() as i64);
b.clone_with_new_path(new_path)
}
NodeOperation::Delete { path: a_path, nodes } => {
let new_path = Path::transform(a_path, b.path(), nodes.len() as i64);
b.clone_with_new_path(new_path)
}
_ => b.clone(),
}
}
}
#[derive(Serialize, Deserialize, Default)]
pub struct NodeOperationList {
operations: Vec<NodeOperation>,
}
impl NodeOperationList {
pub fn into_inner(self) -> Vec<NodeOperation> {
self.operations
}
}
impl std::ops::Deref for NodeOperationList {
type Target = Vec<NodeOperation>;
fn deref(&self) -> &Self::Target {
&self.operations
}
}
impl std::ops::DerefMut for NodeOperationList {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.operations
}
}
impl NodeOperationList {
pub fn new(operations: Vec<NodeOperation>) -> Self {
Self { operations }
}
pub fn from_bytes(bytes: Vec<u8>) -> Result<Self, OTError> {
let operation_list = serde_json::from_slice(&bytes).map_err(|err| OTError::serde().context(err))?;
Ok(operation_list)
}
pub fn to_bytes(&self) -> Result<Vec<u8>, OTError> {
let bytes = serde_json::to_vec(self).map_err(|err| OTError::serde().context(err))?;
Ok(bytes)
}
}

View File

@ -1,127 +0,0 @@
use serde::{Deserialize, Serialize};
#[derive(Clone, Serialize, Deserialize, Eq, PartialEq, Debug)]
pub struct Path(pub Vec<usize>);
impl std::ops::Deref for Path {
type Target = Vec<usize>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl std::convert::From<usize> for Path {
fn from(val: usize) -> Self {
Path(vec![val])
}
}
impl std::convert::From<&usize> for Path {
fn from(val: &usize) -> Self {
Path(vec![*val])
}
}
impl std::convert::From<&Path> for Path {
fn from(path: &Path) -> Self {
path.clone()
}
}
impl From<Vec<usize>> for Path {
fn from(v: Vec<usize>) -> Self {
Path(v)
}
}
impl From<&Vec<usize>> for Path {
fn from(values: &Vec<usize>) -> Self {
Path(values.clone())
}
}
impl From<&[usize]> for Path {
fn from(values: &[usize]) -> Self {
Path(values.to_vec())
}
}
impl Path {
// delta is default to be 1
pub fn transform(pre_insert_path: &Path, b: &Path, offset: i64) -> Path {
if pre_insert_path.len() > b.len() {
return b.clone();
}
if pre_insert_path.is_empty() || b.is_empty() {
return b.clone();
}
// check the prefix
for i in 0..(pre_insert_path.len() - 1) {
if pre_insert_path.0[i] != b.0[i] {
return b.clone();
}
}
let mut prefix: Vec<usize> = pre_insert_path.0[0..(pre_insert_path.len() - 1)].into();
let mut suffix: Vec<usize> = b.0[pre_insert_path.0.len()..].into();
let prev_insert_last: usize = *pre_insert_path.0.last().unwrap();
let b_at_index = b.0[pre_insert_path.0.len() - 1];
if prev_insert_last <= b_at_index {
prefix.push(((b_at_index as i64) + offset) as usize);
} else {
prefix.push(b_at_index);
}
prefix.append(&mut suffix);
Path(prefix)
}
}
#[cfg(test)]
mod tests {
use crate::core::Path;
#[test]
fn path_transform_test_1() {
assert_eq!(
{ Path::transform(&Path(vec![0, 1]), &Path(vec![0, 1]), 1) }.0,
vec![0, 2]
);
assert_eq!(
{ Path::transform(&Path(vec![0, 1]), &Path(vec![0, 1]), 5) }.0,
vec![0, 6]
);
}
#[test]
fn path_transform_test_2() {
assert_eq!(
{ Path::transform(&Path(vec![0, 1]), &Path(vec![0, 2]), 1) }.0,
vec![0, 3]
);
}
#[test]
fn path_transform_test_3() {
assert_eq!(
{ Path::transform(&Path(vec![0, 1]), &Path(vec![0, 2, 7, 8, 9]), 1) }.0,
vec![0, 3, 7, 8, 9]
);
}
#[test]
fn path_transform_no_changed_test() {
assert_eq!(
{ Path::transform(&Path(vec![0, 1, 2]), &Path(vec![0, 0, 7, 8, 9]), 1) }.0,
vec![0, 0, 7, 8, 9]
);
assert_eq!(
{ Path::transform(&Path(vec![0, 1, 2]), &Path(vec![0, 1]), 1) }.0,
vec![0, 1]
);
assert_eq!(
{ Path::transform(&Path(vec![1, 1]), &Path(vec![1, 0]), 1) }.0,
vec![1, 0]
);
}
}

View File

@ -1,12 +1,12 @@
pub mod attributes; pub mod attributes;
mod delta; mod delta;
mod document;
mod interval; mod interval;
mod node_tree;
mod ot_str; mod ot_str;
pub use attributes::*; pub use attributes::*;
pub use delta::operation::*; pub use delta::operation::*;
pub use delta::*; pub use delta::*;
pub use document::*;
pub use interval::*; pub use interval::*;
pub use node_tree::*;
pub use ot_str::*; pub use ot_str::*;

View File

@ -2,14 +2,14 @@
mod node; mod node;
mod node_serde; mod node_serde;
mod node_tree;
mod operation; mod operation;
mod operation_serde; mod operation_serde;
mod path; mod path;
mod transaction; mod transaction;
mod tree;
pub use node::*; pub use node::*;
pub use node_tree::*;
pub use operation::*; pub use operation::*;
pub use path::*; pub use path::*;
pub use transaction::*; pub use transaction::*;
pub use tree::*;

View File

@ -6,7 +6,7 @@ use crate::errors::OTError;
use crate::text_delta::TextDelta; use crate::text_delta::TextDelta;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[derive(Default, Clone, Serialize, Deserialize, Eq, PartialEq)] #[derive(Default, Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
pub struct NodeData { pub struct NodeData {
#[serde(rename = "type")] #[serde(rename = "type")]
pub node_type: String, pub node_type: String,

View File

@ -0,0 +1,174 @@
use crate::core::attributes::Attributes;
use crate::core::{NodeBodyChangeset, NodeData, Path};
use crate::errors::OTError;
use serde::{Deserialize, Serialize};
use std::rc::Rc;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "op")]
pub enum NodeOperation {
#[serde(rename = "insert")]
Insert { path: Path, nodes: Vec<NodeData> },
#[serde(rename = "update-attribute")]
UpdateAttributes {
path: Path,
new: Attributes,
old: Attributes,
},
#[serde(rename = "update-body")]
// #[serde(serialize_with = "serialize_edit_body")]
// #[serde(deserialize_with = "deserialize_edit_body")]
UpdateBody { path: Path, changeset: NodeBodyChangeset },
#[serde(rename = "delete")]
Delete { path: Path, nodes: Vec<NodeData> },
}
impl NodeOperation {
pub fn get_path(&self) -> &Path {
match self {
NodeOperation::Insert { path, .. } => path,
NodeOperation::UpdateAttributes { path, .. } => path,
NodeOperation::Delete { path, .. } => path,
NodeOperation::UpdateBody { path, .. } => path,
}
}
pub fn get_mut_path(&mut self) -> &mut Path {
match self {
NodeOperation::Insert { path, .. } => path,
NodeOperation::UpdateAttributes { path, .. } => path,
NodeOperation::Delete { path, .. } => path,
NodeOperation::UpdateBody { path, .. } => path,
}
}
pub fn invert(&self) -> NodeOperation {
match self {
NodeOperation::Insert { path, nodes } => NodeOperation::Delete {
path: path.clone(),
nodes: nodes.clone(),
},
NodeOperation::UpdateAttributes {
path,
new: attributes,
old: old_attributes,
} => NodeOperation::UpdateAttributes {
path: path.clone(),
new: old_attributes.clone(),
old: attributes.clone(),
},
NodeOperation::Delete { path, nodes } => NodeOperation::Insert {
path: path.clone(),
nodes: nodes.clone(),
},
NodeOperation::UpdateBody { path, changeset: body } => NodeOperation::UpdateBody {
path: path.clone(),
changeset: body.inverted(),
},
}
}
/// Make the `other` operation can be applied to the version after applying the `self` operation.
/// The semantics of transform is used when editing conflicts occur, which is often determined by the version id。
/// For example, if the inserted position has been acquired by others, then it's needed to do the transform to
/// make sure the inserted position is right.
///
/// # Arguments
///
/// * `other`: The operation that is going to be transformed
///
/// # Examples
///
/// ```
/// use lib_ot::core::{NodeDataBuilder, NodeOperation, Path};
/// 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 op_2 = NodeOperation::Insert {
/// path: Path(vec![0, 1]),
/// nodes: vec![node_2],
/// };
///
/// assert_eq!(serde_json::to_string(&op_2).unwrap(), r#"{"op":"insert","path":[0,1],"nodes":[{"type":"text_2"}]}"#);
///
/// op_1.transform(&mut op_2);
/// assert_eq!(serde_json::to_string(&op_2).unwrap(), r#"{"op":"insert","path":[0,2],"nodes":[{"type":"text_2"}]}"#);
///
/// ```
pub fn transform(&self, other: &mut NodeOperation) {
match self {
NodeOperation::Insert { path, nodes } => {
let new_path = path.transform(other.get_path(), nodes.len());
*other.get_mut_path() = new_path;
}
NodeOperation::Delete { path, nodes } => {
let new_path = path.transform(other.get_path(), nodes.len());
*other.get_mut_path() = new_path;
}
_ => {
// Only insert/delete will change the path.
}
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct NodeOperationList {
operations: Vec<Rc<NodeOperation>>,
}
impl NodeOperationList {
pub fn into_inner(self) -> Vec<Rc<NodeOperation>> {
self.operations
}
pub fn add_op(&mut self, operation: NodeOperation) {
self.operations.push(Rc::new(operation));
}
}
impl std::ops::Deref for NodeOperationList {
type Target = Vec<Rc<NodeOperation>>;
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 std::convert::From<Vec<NodeOperation>> for NodeOperationList {
fn from(operations: Vec<NodeOperation>) -> Self {
Self::new(operations)
}
}
impl NodeOperationList {
pub fn new(operations: Vec<NodeOperation>) -> Self {
Self {
operations: operations.into_iter().map(Rc::new).collect(),
}
}
pub fn from_bytes(bytes: Vec<u8>) -> Result<Self, OTError> {
let operation_list = serde_json::from_slice(&bytes).map_err(|err| OTError::serde().context(err))?;
Ok(operation_list)
}
pub fn to_bytes(&self) -> Result<Vec<u8>, OTError> {
let bytes = serde_json::to_vec(self).map_err(|err| OTError::serde().context(err))?;
Ok(bytes)
}
}

View File

@ -0,0 +1,190 @@
use serde::{Deserialize, Serialize};
/// The `Path` represents as a path to reference to the node in the `NodeTree`.
/// ┌─────────┐
/// │ Root │
/// └─────────┼──────────┐
/// │0: Node A │
/// └──────────┼────────────┐
/// │0: Node A-1 │
/// ├────────────┤
/// │1: Node A-2 │
/// ┌──────────┼────────────┘
/// │1: Node B │
/// └──────────┼────────────┐
/// │0: Node B-1 │
/// ├────────────┤
/// │1: Node B-2 │
/// ┌──────────┼────────────┘
/// │2: Node C │
/// └──────────┘
///
/// The path of Node A will be [0]
/// 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)]
pub struct Path(pub Vec<usize>);
impl std::ops::Deref for Path {
type Target = Vec<usize>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl std::convert::From<usize> for Path {
fn from(val: usize) -> Self {
Path(vec![val])
}
}
impl std::convert::From<&usize> for Path {
fn from(val: &usize) -> Self {
Path(vec![*val])
}
}
impl std::convert::From<&Path> for Path {
fn from(path: &Path) -> Self {
path.clone()
}
}
impl From<Vec<usize>> for Path {
fn from(v: Vec<usize>) -> Self {
Path(v)
}
}
impl From<&Vec<usize>> for Path {
fn from(values: &Vec<usize>) -> Self {
Path(values.clone())
}
}
impl From<&[usize]> for Path {
fn from(values: &[usize]) -> Self {
Path(values.to_vec())
}
}
impl Path {
/// Calling this function if there are two changes want to modify the same path.
///
/// # Arguments
///
/// * `other`: the path that need to be transformed
/// * `offset`: represents the len of nodes referenced by the current path
///
/// If two changes modify the same path or the path was shared by them. Then it needs to do the
/// transformation to make sure the changes are applied to the right path.
///
/// returns: the path represents the position that the other path reference to.
///
/// # Examples
///
/// ```
/// use lib_ot::core::Path;
/// let path = Path(vec![0, 1]);
/// for (old_path, len_of_nodes, expected_path) in vec![
/// // Try to modify the path [0, 1], but someone has inserted one element before the
/// // current path [0,1] in advance. That causes the modified path [0,1] to no longer
/// // valid. It needs to do the transformation to get the right path.
/// //
/// // [0,2] is the path you want to modify.
/// (Path(vec![0, 1]), 1, Path(vec![0, 2])),
/// (Path(vec![0, 1]), 5, Path(vec![0, 6])),
/// (Path(vec![0, 2]), 1, Path(vec![0, 3])),
/// // Try to modify the path [0, 2,3,4], but someone has inserted one element before the
/// // current path [0,1] in advance. That cause the prefix path [0,2] of [0,2,3,4]
/// // no longer valid.
/// // It needs to do the transformation to get the right path. So [0,2] is transformed to [0,3]
/// // and the suffix [3,4] of the [0,2,3,4] remains the same. So the transformed result is
/// //
/// // [0,3,3,4]
/// (Path(vec![0, 2, 3, 4]), 1, Path(vec![0, 3, 3, 4])),
/// ] {
/// assert_eq!(path.transform(&old_path, len_of_nodes), expected_path);
/// }
/// // The path remains the same in the following test. Because the shared path is not changed.
/// let path = Path(vec![0, 1, 2]);
/// for (old_path, len_of_nodes, expected_path) in vec![
/// // Try to modify the path [0,0,0,1,2], but someone has inserted one element
/// // before [0,1,2]. [0,0,0,1,2] and [0,1,2] share the same path [0,x], because
/// // the element was inserted at [0,1,2] that didn't affect the shared path [0, x].
/// // So, after the transformation, the path is not changed.
/// (Path(vec![0, 0, 0, 1, 2]), 1, Path(vec![0, 0, 0, 1, 2])),
/// (Path(vec![0, 1]), 1, Path(vec![0, 1])),
/// ] {
/// assert_eq!(path.transform(&old_path, len_of_nodes), expected_path);
/// }
///
/// let path = Path(vec![1, 1]);
/// for (old_path, len_of_nodes, expected_path) in vec![(Path(vec![1, 0]), 1, Path(vec![1, 0]))] {
/// assert_eq!(path.transform(&old_path, len_of_nodes), expected_path);
/// }
/// ```
/// For example, client A and client B want to insert a node at the same index, the server applies
/// the changes made by client B. But, before applying the client A's changes, server transforms
/// the changes first in order to make sure that client A modify the right position. After that,
/// the changes can be applied to the server.
///
/// ┌──────────┐ ┌──────────┐ ┌──────────┐
/// │ Client A │ │ Server │ │ Client B │
/// └─────┬────┘ └─────┬────┘ └────┬─────┘
/// │ │ ┌ ─ ─ ─ ─ ─ ─ ─ ┐ │
/// │ │ Root │
/// │ │ │ 0:A │ │
/// │ │ ─ ─ ─ ─ ─ ─ ─ ─ │
/// │ │ ◀───────────────────────│
/// │ │ Insert B at index 1 │
/// │ │ │
/// │ │ ┌ ─ ─ ─ ─ ─ ─ ─ ┐ │
/// │ │ Root │
/// │ │ │ 0:A │ │
/// ├──────────────────────▶│ 1:B │
/// │ Insert C at index 1 │ └ ─ ─ ─ ─ ─ ─ ─ ┘ │
/// │ │ │
/// │ │ transform index 1 to 2 │
/// │ │ │
/// │ │ ┌ ─ ─ ─ ─ ─ ─ ─ ─ │
/// │ │ Root │ │
/// │ │ │ 0:A │
/// ▼ ▼ 1:B │ ▼
/// │ 2:C
/// ─ ─ ─ ─ ─ ─ ─ ─ ┘
pub fn transform(&self, other: &Path, offset: usize) -> Path {
if self.len() > other.len() {
return other.clone();
}
if self.is_empty() || other.is_empty() {
return other.clone();
}
for i in 0..(self.len() - 1) {
if self.0[i] != other.0[i] {
return other.clone();
}
}
// Splits the `Path` into two part. The suffix will contain the last element of the `Path`.
let second_last_index = self.0.len() - 1;
let mut prefix: Vec<usize> = self.0[0..second_last_index].into();
let mut suffix: Vec<usize> = other.0[self.0.len()..].into();
let last_value = *self.0.last().unwrap();
let other_second_last_value = other.0[second_last_index];
//
if last_value <= other_second_last_value {
prefix.push(other_second_last_value + offset);
} else {
prefix.push(other_second_last_value);
}
// concat the prefix and suffix into a new path
prefix.append(&mut suffix);
Path(prefix)
}
}

View File

@ -1,26 +1,57 @@
use crate::core::attributes::Attributes; use crate::core::attributes::Attributes;
use crate::core::document::path::Path; use crate::core::{NodeData, NodeOperation, NodeTree, Path};
use crate::core::{NodeData, NodeOperation, NodeTree}; use crate::errors::OTError;
use indextree::NodeId; use indextree::NodeId;
use std::rc::Rc;
use super::{NodeBodyChangeset, NodeOperationList}; use super::{NodeBodyChangeset, NodeOperationList};
#[derive(Debug, Clone, Default)]
pub struct Transaction { pub struct Transaction {
operations: NodeOperationList, operations: NodeOperationList,
} }
impl Transaction { impl Transaction {
pub fn new(operations: NodeOperationList) -> Transaction { pub fn new() -> Self {
Transaction { operations } Self::default()
} }
pub fn into_operations(self) -> Vec<NodeOperation> { pub fn from_operations<T: Into<NodeOperationList>>(operations: T) -> Self {
Self {
operations: operations.into(),
}
}
pub fn into_operations(self) -> Vec<Rc<NodeOperation>> {
self.operations.into_inner() self.operations.into_inner()
} }
/// Make the `other` can be applied to the version after applying the `self` transaction.
///
/// The semantics of transform is used when editing conflicts occur, which is often determined by the version id。
/// the operations of the transaction will be transformed into the conflict operations.
pub fn transform(&self, other: &Transaction) -> Result<Transaction, OTError> {
let mut new_transaction = other.clone();
for other_operation in new_transaction.iter_mut() {
let other_operation = Rc::make_mut(other_operation);
for operation in self.operations.iter() {
operation.transform(other_operation);
}
}
Ok(new_transaction)
}
pub fn compose(&mut self, other: &Transaction) -> Result<(), OTError> {
// For the moment, just append `other` operations to the end of `self`.
for operation in other.operations.iter() {
self.operations.push(operation.clone());
}
Ok(())
}
} }
impl std::ops::Deref for Transaction { impl std::ops::Deref for Transaction {
type Target = NodeOperationList; type Target = Vec<Rc<NodeOperation>>;
fn deref(&self) -> &Self::Target { fn deref(&self) -> &Self::Target {
&self.operations &self.operations
@ -64,7 +95,7 @@ impl<'a> TransactionBuilder<'a> {
/// let transaction = TransactionBuilder::new(&node_tree) /// let transaction = TransactionBuilder::new(&node_tree)
/// .insert_nodes_at_path(0,vec![ NodeData::new("text_1"), NodeData::new("text_2")]) /// .insert_nodes_at_path(0,vec![ NodeData::new("text_1"), NodeData::new("text_2")])
/// .finalize(); /// .finalize();
/// node_tree.apply(transaction).unwrap(); /// node_tree.apply_transaction(transaction).unwrap();
/// ///
/// node_tree.node_id_at_path(vec![0, 0]); /// node_tree.node_id_at_path(vec![0, 0]);
/// ``` /// ```
@ -94,7 +125,7 @@ impl<'a> TransactionBuilder<'a> {
/// let transaction = TransactionBuilder::new(&node_tree) /// let transaction = TransactionBuilder::new(&node_tree)
/// .insert_node_at_path(0, NodeData::new("text")) /// .insert_node_at_path(0, NodeData::new("text"))
/// .finalize(); /// .finalize();
/// node_tree.apply(transaction).unwrap(); /// node_tree.apply_transaction(transaction).unwrap();
/// ``` /// ```
/// ///
pub fn insert_node_at_path<T: Into<Path>>(self, path: T, node: NodeData) -> Self { pub fn insert_node_at_path<T: Into<Path>>(self, path: T, node: NodeData) -> Self {
@ -112,10 +143,10 @@ impl<'a> TransactionBuilder<'a> {
} }
} }
self.operations.push(NodeOperation::UpdateAttributes { self.operations.add_op(NodeOperation::UpdateAttributes {
path: path.clone(), path: path.clone(),
attributes, new: attributes,
old_attributes, old: old_attributes,
}); });
} }
None => tracing::warn!("Update attributes at path: {:?} failed. Node is not exist", path), None => tracing::warn!("Update attributes at path: {:?} failed. Node is not exist", path),
@ -126,7 +157,7 @@ impl<'a> TransactionBuilder<'a> {
pub fn update_body_at_path(mut self, path: &Path, changeset: NodeBodyChangeset) -> Self { pub fn update_body_at_path(mut self, path: &Path, changeset: NodeBodyChangeset) -> Self {
match self.node_tree.node_id_at_path(path) { match self.node_tree.node_id_at_path(path) {
Some(_) => { Some(_) => {
self.operations.push(NodeOperation::UpdateBody { self.operations.add_op(NodeOperation::UpdateBody {
path: path.clone(), path: path.clone(),
changeset, changeset,
}); });
@ -148,7 +179,7 @@ impl<'a> TransactionBuilder<'a> {
node = self.node_tree.following_siblings(node).next().unwrap(); node = self.node_tree.following_siblings(node).next().unwrap();
} }
self.operations.push(NodeOperation::Delete { self.operations.add_op(NodeOperation::Delete {
path: path.clone(), path: path.clone(),
nodes: deleted_nodes, nodes: deleted_nodes,
}); });
@ -172,11 +203,11 @@ impl<'a> TransactionBuilder<'a> {
} }
pub fn push(mut self, op: NodeOperation) -> Self { pub fn push(mut self, op: NodeOperation) -> Self {
self.operations.push(op); self.operations.add_op(op);
self self
} }
pub fn finalize(self) -> Transaction { pub fn finalize(self) -> Transaction {
Transaction::new(self.operations) Transaction::from_operations(self.operations)
} }
} }

View File

@ -1,8 +1,8 @@
use crate::core::attributes::Attributes; use crate::core::attributes::Attributes;
use crate::core::document::path::Path; use crate::core::{Node, NodeBodyChangeset, NodeData, NodeOperation, OperationTransform, Path, Transaction};
use crate::core::{Node, NodeBodyChangeset, NodeData, NodeOperation, OperationTransform, Transaction};
use crate::errors::{ErrorBuilder, OTError, OTErrorCode}; use crate::errors::{ErrorBuilder, OTError, OTErrorCode};
use indextree::{Arena, Children, FollowingSiblings, NodeId}; use indextree::{Arena, Children, FollowingSiblings, NodeId};
use std::rc::Rc;
use super::NodeOperationList; use super::NodeOperationList;
@ -26,14 +26,13 @@ impl NodeTree {
} }
pub fn from_bytes(root_name: &str, bytes: Vec<u8>) -> Result<Self, OTError> { pub fn from_bytes(root_name: &str, bytes: Vec<u8>) -> Result<Self, OTError> {
let operations = NodeOperationList::from_bytes(bytes)?.into_inner(); let operations = NodeOperationList::from_bytes(bytes)?;
Self::from_operations(root_name, operations) Self::from_operations(root_name, operations)
} }
pub fn from_operations(root_name: &str, operations: Vec<NodeOperation>) -> Result<Self, OTError> { pub fn from_operations(root_name: &str, operations: NodeOperationList) -> Result<Self, OTError> {
let mut node_tree = NodeTree::new(root_name); let mut node_tree = NodeTree::new(root_name);
for operation in operations.into_inner().into_iter() {
for operation in operations {
let _ = node_tree.apply_op(operation)?; let _ = node_tree.apply_op(operation)?;
} }
Ok(node_tree) Ok(node_tree)
@ -54,13 +53,14 @@ impl NodeTree {
/// # Examples /// # Examples
/// ///
/// ``` /// ```
/// use std::rc::Rc;
/// use lib_ot::core::{NodeOperation, NodeTree, NodeData, Path}; /// use lib_ot::core::{NodeOperation, NodeTree, NodeData, Path};
/// let nodes = vec![NodeData::new("text".to_string())]; /// let nodes = vec![NodeData::new("text".to_string())];
/// let root_path: Path = vec![0].into(); /// let root_path: Path = vec![0].into();
/// let op = NodeOperation::Insert {path: root_path.clone(),nodes }; /// let op = NodeOperation::Insert {path: root_path.clone(),nodes };
/// ///
/// let mut node_tree = NodeTree::new("root"); /// let mut node_tree = NodeTree::new("root");
/// node_tree.apply_op(op).unwrap(); /// node_tree.apply_op(Rc::new(op)).unwrap();
/// let node_id = node_tree.node_id_at_path(&root_path).unwrap(); /// let node_id = node_tree.node_id_at_path(&root_path).unwrap();
/// let node_path = node_tree.path_from_node_id(node_id); /// let node_path = node_tree.path_from_node_id(node_id);
/// debug_assert_eq!(node_path, root_path); /// debug_assert_eq!(node_path, root_path);
@ -105,23 +105,25 @@ impl NodeTree {
counter counter
} }
/// /// Returns the note_id at the position of the tree with id note_id
/// # Arguments /// # Arguments
/// ///
/// * `node_id`: /// * `node_id`: the node id of the child's parent
/// * `index`: /// * `index`: index of the node in parent children list
/// ///
/// returns: Option<NodeId> /// returns: Option<NodeId>
/// ///
/// # Examples /// # Examples
/// ///
/// ``` /// ```
/// use std::rc::Rc;
/// use lib_ot::core::{NodeOperation, NodeTree, NodeData, Path}; /// use lib_ot::core::{NodeOperation, NodeTree, NodeData, Path};
/// let node_1 = NodeData::new("text".to_string()); /// let node_1 = NodeData::new("text".to_string());
/// let inserted_path: Path = vec![0].into(); /// let inserted_path: Path = vec![0].into();
/// ///
/// let mut node_tree = NodeTree::new("root"); /// let mut node_tree = NodeTree::new("root");
/// node_tree.apply_op(NodeOperation::Insert {path: inserted_path.clone(),nodes: vec![node_1.clone()] }).unwrap(); /// let op = NodeOperation::Insert {path: inserted_path.clone(),nodes: vec![node_1.clone()] };
/// node_tree.apply_op(Rc::new(op)).unwrap();
/// ///
/// let node_2 = node_tree.get_node_at_path(&inserted_path).unwrap(); /// let node_2 = node_tree.get_node_at_path(&inserted_path).unwrap();
/// assert_eq!(node_2.node_type, node_1.node_type); /// assert_eq!(node_2.node_type, node_1.node_type);
@ -137,6 +139,10 @@ impl NodeTree {
None None
} }
/// Returns all children whose parent node id is node_id
///
/// * `node_id`: the children's parent node id
///
pub fn children_from_node(&self, node_id: NodeId) -> Children<'_, Node> { pub fn children_from_node(&self, node_id: NodeId) -> Children<'_, Node> {
node_id.children(&self.arena) node_id.children(&self.arena)
} }
@ -159,7 +165,7 @@ impl NodeTree {
node_id.following_siblings(&self.arena) node_id.following_siblings(&self.arena)
} }
pub fn apply(&mut self, transaction: Transaction) -> Result<(), OTError> { pub fn apply_transaction(&mut self, transaction: Transaction) -> Result<(), OTError> {
let operations = transaction.into_operations(); let operations = transaction.into_operations();
for operation in operations { for operation in operations {
self.apply_op(operation)?; self.apply_op(operation)?;
@ -167,10 +173,15 @@ impl NodeTree {
Ok(()) Ok(())
} }
pub fn apply_op(&mut self, op: NodeOperation) -> Result<(), OTError> { pub fn apply_op(&mut self, op: Rc<NodeOperation>) -> Result<(), OTError> {
let op = match Rc::try_unwrap(op) {
Ok(op) => op,
Err(op) => op.as_ref().clone(),
};
match op { match op {
NodeOperation::Insert { path, nodes } => self.insert_nodes(&path, nodes), NodeOperation::Insert { path, nodes } => self.insert_nodes(&path, nodes),
NodeOperation::UpdateAttributes { path, attributes, .. } => self.update_attributes(&path, attributes), NodeOperation::UpdateAttributes { path, new, .. } => self.update_attributes(&path, new),
NodeOperation::UpdateBody { path, changeset } => self.update_body(&path, changeset), NodeOperation::UpdateBody { path, changeset } => self.update_body(&path, changeset),
NodeOperation::Delete { path, nodes } => self.delete_node(&path, nodes), NodeOperation::Delete { path, nodes } => self.delete_node(&path, nodes),
} }
@ -216,7 +227,9 @@ impl NodeTree {
return Ok(()); return Ok(());
} }
if index == parent.children(&self.arena).count() { // 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() {
self.append_nodes(&parent, nodes); self.append_nodes(&parent, nodes);
return Ok(()); return Ok(());
} }

View File

@ -2,5 +2,4 @@ use crate::core::{Attributes, Operation, OperationBuilder, Operations};
pub type TextDelta = Operations<Attributes>; pub type TextDelta = Operations<Attributes>;
pub type TextDeltaBuilder = OperationBuilder<Attributes>; pub type TextDeltaBuilder = OperationBuilder<Attributes>;
pub type TextOperation = Operation<Attributes>; pub type TextOperation = Operation<Attributes>;

View File

@ -26,7 +26,8 @@ fn editor_deserialize_node_test() {
test.run_scripts(vec![ test.run_scripts(vec![
InsertNode { InsertNode {
path, path,
node: node.clone(), node_data: node.clone(),
rev_id: 1,
}, },
AssertNumberOfNodesAtPath { path: None, len: 1 }, AssertNumberOfNodesAtPath { path: None, len: 1 },
AssertNumberOfNodesAtPath { AssertNumberOfNodesAtPath {
@ -41,11 +42,11 @@ fn editor_deserialize_node_test() {
path: vec![0, 1].into(), path: vec![0, 1].into(),
expected: expected_delta, expected: expected_delta,
}, },
AssertNode { AssertNodeData {
path: vec![0, 0].into(), path: vec![0, 0].into(),
expected: Some(node.children[0].clone()), expected: Some(node.children[0].clone()),
}, },
AssertNode { AssertNodeData {
path: vec![0, 3].into(), path: vec![0, 3].into(),
expected: Some(node.children[3].clone()), expected: Some(node.children[3].clone()),
}, },

View File

@ -1,4 +1,6 @@
use lib_ot::core::AttributeBuilder; use crate::node::script::NodeScript::*;
use crate::node::script::NodeTest;
use lib_ot::core::{AttributeBuilder, Node};
use lib_ot::{ use lib_ot::{
core::{NodeBodyChangeset, NodeData, NodeDataBuilder, NodeOperation, Path}, core::{NodeBodyChangeset, NodeData, NodeDataBuilder, NodeOperation, Path},
text_delta::TextDeltaBuilder, text_delta::TextDeltaBuilder,
@ -33,15 +35,14 @@ fn operation_insert_node_with_children_serde_test() {
fn operation_update_node_attributes_serde_test() { fn operation_update_node_attributes_serde_test() {
let operation = NodeOperation::UpdateAttributes { let operation = NodeOperation::UpdateAttributes {
path: Path(vec![0, 1]), path: Path(vec![0, 1]),
attributes: AttributeBuilder::new().insert("bold", true).build(), new: AttributeBuilder::new().insert("bold", true).build(),
old_attributes: AttributeBuilder::new().insert("bold", false).build(), old: AttributeBuilder::new().insert("bold", false).build(),
}; };
let result = serde_json::to_string(&operation).unwrap(); let result = serde_json::to_string(&operation).unwrap();
assert_eq!( assert_eq!(
result, result,
r#"{"op":"update","path":[0,1],"attributes":{"bold":true},"oldAttributes":{"bold":null}}"# r#"{"op":"update-attribute","path":[0,1],"new":{"bold":true},"old":{"bold":null}}"#
); );
} }
@ -69,3 +70,166 @@ fn operation_update_node_body_deserialize_test() {
let json_2 = serde_json::to_string(&operation).unwrap(); let json_2 = serde_json::to_string(&operation).unwrap();
assert_eq!(json_1, json_2); assert_eq!(json_1, json_2);
} }
#[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_transform_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 = node_data_3.clone().into();
// 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_transform_multiple_level_path_test() {
let mut test = NodeTest::new();
let node_data_1 = NodeDataBuilder::new("text_1")
.add_node(NodeDataBuilder::new("text_1_1").build())
.add_node(NodeDataBuilder::new("text_1_2").build())
.build();
let node_data_2 = NodeDataBuilder::new("text_2")
.add_node(NodeDataBuilder::new("text_2_1").build())
.add_node(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.into()),
},
];
test.run_scripts(scripts);
}
#[test]
fn operation_delete_transform_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 = node_data_3.clone().into();
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
AssertNumberOfNodesAtPath { path: None, len: 2 },
AssertNode {
path: 1.into(),
expected: Some(node_3),
},
AssertNode {
path: 2.into(),
expected: None,
},
];
test.run_scripts(scripts);
}

View File

@ -1,26 +1,58 @@
use lib_ot::core::{Node, Transaction};
use lib_ot::{ use lib_ot::{
core::attributes::Attributes, core::attributes::Attributes,
core::{NodeBody, NodeBodyChangeset, NodeData, NodeTree, Path, TransactionBuilder}, core::{NodeBody, NodeBodyChangeset, NodeData, NodeTree, Path, TransactionBuilder},
text_delta::TextDelta, text_delta::TextDelta,
}; };
use std::collections::HashMap;
pub enum NodeScript { pub enum NodeScript {
InsertNode { path: Path, node: NodeData }, InsertNode {
UpdateAttributes { path: Path, attributes: Attributes }, path: Path,
UpdateBody { path: Path, changeset: NodeBodyChangeset }, node_data: NodeData,
DeleteNode { path: Path }, rev_id: usize,
AssertNumberOfNodesAtPath { path: Option<Path>, len: usize }, },
AssertNode { path: Path, expected: Option<NodeData> }, UpdateAttributes {
AssertNodeDelta { path: Path, expected: TextDelta }, path: Path,
attributes: Attributes,
},
UpdateBody {
path: Path,
changeset: NodeBodyChangeset,
},
DeleteNode {
path: Path,
rev_id: usize,
},
AssertNumberOfNodesAtPath {
path: Option<Path>,
len: usize,
},
AssertNodeData {
path: Path,
expected: Option<NodeData>,
},
AssertNode {
path: Path,
expected: Option<Node>,
},
AssertNodeDelta {
path: Path,
expected: TextDelta,
},
} }
pub struct NodeTest { pub struct NodeTest {
rev_id: usize,
rev_operations: HashMap<usize, Transaction>,
node_tree: NodeTree, node_tree: NodeTree,
} }
impl NodeTest { impl NodeTest {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
rev_id: 0,
rev_operations: HashMap::new(),
node_tree: NodeTree::new("root"), node_tree: NodeTree::new("root"),
} }
} }
@ -33,40 +65,54 @@ impl NodeTest {
pub fn run_script(&mut self, script: NodeScript) { pub fn run_script(&mut self, script: NodeScript) {
match script { match script {
NodeScript::InsertNode { path, node } => { NodeScript::InsertNode {
let transaction = TransactionBuilder::new(&self.node_tree) path,
node_data: node,
rev_id,
} => {
let mut transaction = TransactionBuilder::new(&self.node_tree)
.insert_node_at_path(path, node) .insert_node_at_path(path, node)
.finalize(); .finalize();
self.transform_transaction_if_need(&mut transaction, rev_id);
self.node_tree.apply(transaction).unwrap(); self.apply_transaction(transaction);
} }
NodeScript::UpdateAttributes { path, attributes } => { NodeScript::UpdateAttributes { path, attributes } => {
let transaction = TransactionBuilder::new(&self.node_tree) let transaction = TransactionBuilder::new(&self.node_tree)
.update_attributes_at_path(&path, attributes) .update_attributes_at_path(&path, attributes)
.finalize(); .finalize();
self.node_tree.apply(transaction).unwrap(); self.apply_transaction(transaction);
} }
NodeScript::UpdateBody { path, changeset } => { NodeScript::UpdateBody { path, changeset } => {
// //
let transaction = TransactionBuilder::new(&self.node_tree) let transaction = TransactionBuilder::new(&self.node_tree)
.update_body_at_path(&path, changeset) .update_body_at_path(&path, changeset)
.finalize(); .finalize();
self.node_tree.apply(transaction).unwrap(); self.apply_transaction(transaction);
} }
NodeScript::DeleteNode { path } => { NodeScript::DeleteNode { path, rev_id } => {
let transaction = TransactionBuilder::new(&self.node_tree) let mut transaction = TransactionBuilder::new(&self.node_tree)
.delete_node_at_path(&path) .delete_node_at_path(&path)
.finalize(); .finalize();
self.node_tree.apply(transaction).unwrap(); self.transform_transaction_if_need(&mut transaction, rev_id);
self.apply_transaction(transaction);
} }
NodeScript::AssertNode { path, expected } => { NodeScript::AssertNode { path, expected } => {
let node_id = self.node_tree.node_id_at_path(path); let node_id = self.node_tree.node_id_at_path(path);
if expected.is_none() && node_id.is_none() {
return;
}
let node = self.node_tree.get_node(node_id.unwrap()).cloned();
assert_eq!(node, expected);
}
NodeScript::AssertNodeData { path, expected } => {
let node_id = self.node_tree.node_id_at_path(path);
match node_id { match node_id {
None => assert!(node_id.is_none()), None => assert!(node_id.is_none()),
Some(node_id) => { Some(node_id) => {
let node_data = self.node_tree.get_node(node_id).cloned(); let node = self.node_tree.get_node(node_id).cloned();
assert_eq!(node_data, expected.map(|e| e.into())); assert_eq!(node, expected.map(|e| e.into()));
} }
} }
} }
@ -94,4 +140,19 @@ impl NodeTest {
} }
} }
} }
fn apply_transaction(&mut self, transaction: Transaction) {
self.rev_id += 1;
self.rev_operations.insert(self.rev_id, transaction.clone());
self.node_tree.apply_transaction(transaction).unwrap();
}
fn transform_transaction_if_need(&mut self, transaction: &mut Transaction, rev_id: usize) {
if self.rev_id >= rev_id {
for rev_id in rev_id..=self.rev_id {
let old_transaction = self.rev_operations.get(&rev_id).unwrap();
*transaction = old_transaction.transform(transaction).unwrap();
}
}
}
} }

View File

@ -14,9 +14,10 @@ fn node_insert_test() {
let scripts = vec![ let scripts = vec![
InsertNode { InsertNode {
path: path.clone(), path: path.clone(),
node: inserted_node.clone(), node_data: inserted_node.clone(),
rev_id: 1,
}, },
AssertNode { AssertNodeData {
path, path,
expected: Some(inserted_node), expected: Some(inserted_node),
}, },
@ -32,9 +33,10 @@ fn node_insert_node_with_children_test() {
let scripts = vec![ let scripts = vec![
InsertNode { InsertNode {
path: path.clone(), path: path.clone(),
node: inserted_node.clone(), node_data: inserted_node.clone(),
rev_id: 1,
}, },
AssertNode { AssertNodeData {
path, path,
expected: Some(inserted_node), expected: Some(inserted_node),
}, },
@ -57,25 +59,28 @@ fn node_insert_multi_nodes_test() {
let scripts = vec![ let scripts = vec![
InsertNode { InsertNode {
path: path_1.clone(), path: path_1.clone(),
node: node_1.clone(), node_data: node_1.clone(),
rev_id: 1,
}, },
InsertNode { InsertNode {
path: path_2.clone(), path: path_2.clone(),
node: node_2.clone(), node_data: node_2.clone(),
rev_id: 2,
}, },
InsertNode { InsertNode {
path: path_3.clone(), path: path_3.clone(),
node: node_3.clone(), node_data: node_3.clone(),
rev_id: 3,
}, },
AssertNode { AssertNodeData {
path: path_1, path: path_1,
expected: Some(node_1), expected: Some(node_1),
}, },
AssertNode { AssertNodeData {
path: path_2, path: path_2,
expected: Some(node_2), expected: Some(node_2),
}, },
AssertNode { AssertNodeData {
path: path_3, path: path_3,
expected: Some(node_3), expected: Some(node_3),
}, },
@ -96,48 +101,145 @@ fn node_insert_node_in_ordered_nodes_test() {
let path_3: Path = 2.into(); let path_3: Path = 2.into();
let node_3 = NodeData::new("text_3"); let node_3 = NodeData::new("text_3");
let path_4: Path = 3.into();
let scripts = vec![ let scripts = vec![
InsertNode { InsertNode {
path: path_1.clone(), path: path_1.clone(),
node: node_1.clone(), node_data: node_1.clone(),
rev_id: 1,
}, },
InsertNode { InsertNode {
path: path_2.clone(), path: path_2.clone(),
node: node_2_1.clone(), node_data: node_2_1.clone(),
rev_id: 2,
}, },
InsertNode { InsertNode {
path: path_3.clone(), path: path_3.clone(),
node: node_3.clone(), node_data: node_3,
rev_id: 3,
}, },
// 0:note_1 , 1: note_2_1, 2: note_3 // 0:text_1
// 1:text_2_1
// 2:text_3
InsertNode { InsertNode {
path: path_2.clone(), path: path_2.clone(),
node: node_2_2.clone(), node_data: node_2_2.clone(),
rev_id: 4,
}, },
// 0:note_1 , 1:note_2_2, 2: note_2_1, 3: note_3 // 0:text_1
AssertNode { // 1:text_2_2
// 2:text_2_1
// 3:text_3
AssertNodeData {
path: path_1, path: path_1,
expected: Some(node_1), expected: Some(node_1),
}, },
AssertNode { AssertNodeData {
path: path_2, path: path_2,
expected: Some(node_2_2), expected: Some(node_2_2),
}, },
AssertNode { AssertNodeData {
path: path_3, path: path_3,
expected: Some(node_2_1), expected: Some(node_2_1),
}, },
AssertNode {
path: path_4,
expected: Some(node_3),
},
AssertNumberOfNodesAtPath { path: None, len: 4 }, AssertNumberOfNodesAtPath { path: None, len: 4 },
]; ];
test.run_scripts(scripts); test.run_scripts(scripts);
} }
#[test]
fn node_insert_nested_nodes_test() {
let mut test = NodeTest::new();
let node_data_1_1 = NodeDataBuilder::new("text_1_1").build();
let node_data_1_2 = NodeDataBuilder::new("text_1_2").build();
let node_data_1 = NodeDataBuilder::new("text_1")
.add_node(node_data_1_1.clone())
.add_node(node_data_1_2.clone())
.build();
let node_data_2_1 = NodeDataBuilder::new("text_2_1").build();
let node_data_2_2 = NodeDataBuilder::new("text_2_2").build();
let node_data_2 = NodeDataBuilder::new("text_2")
.add_node(node_data_2_1.clone())
.add_node(node_data_2_2.clone())
.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,
},
// the tree will be:
// 0:text_1
// 0:text_1_1
// 1:text_1_2
// 1:text_2
// 0:text_2_1
// 1:text_2_2
AssertNode {
path: vec![0, 0].into(),
expected: Some(node_data_1_1.into()),
},
AssertNode {
path: vec![0, 1].into(),
expected: Some(node_data_1_2.into()),
},
AssertNode {
path: vec![1, 0].into(),
expected: Some(node_data_2_1.into()),
},
AssertNode {
path: vec![1, 1].into(),
expected: Some(node_data_2_2.into()),
},
];
test.run_scripts(scripts);
}
#[test]
fn node_insert_node_before_existing_nested_nodes_test() {
let mut test = NodeTest::new();
let node_data_1_1 = NodeDataBuilder::new("text_1_1").build();
let node_data_1_2 = NodeDataBuilder::new("text_1_2").build();
let node_data_1 = NodeDataBuilder::new("text_1")
.add_node(node_data_1_1.clone())
.add_node(node_data_1_2.clone())
.build();
let scripts = vec![
InsertNode {
path: 0.into(),
node_data: node_data_1,
rev_id: 1,
},
// 0:text_1
// 0:text_1_1
// 1:text_1_2
InsertNode {
path: 0.into(),
node_data: NodeDataBuilder::new("text_0").build(),
rev_id: 2,
},
// 0:text_0
// 1:text_1
// 0:text_1_1
// 1:text_1_2
AssertNode {
path: vec![1, 0].into(),
expected: Some(node_data_1_1.into()),
},
AssertNode {
path: vec![1, 1].into(),
expected: Some(node_data_1_2.into()),
},
];
test.run_scripts(scripts);
}
#[test] #[test]
fn node_insert_with_attributes_test() { fn node_insert_with_attributes_test() {
let mut test = NodeTest::new(); let mut test = NodeTest::new();
@ -149,13 +251,14 @@ fn node_insert_with_attributes_test() {
let scripts = vec![ let scripts = vec![
InsertNode { InsertNode {
path: path.clone(), path: path.clone(),
node: inserted_node.clone(), node_data: inserted_node.clone(),
rev_id: 1,
}, },
UpdateAttributes { UpdateAttributes {
path: path.clone(), path: path.clone(),
attributes: inserted_node.attributes.clone(), attributes: inserted_node.attributes.clone(),
}, },
AssertNode { AssertNodeData {
path, path,
expected: Some(inserted_node), expected: Some(inserted_node),
}, },
@ -172,10 +275,14 @@ fn node_delete_test() {
let scripts = vec![ let scripts = vec![
InsertNode { InsertNode {
path: path.clone(), path: path.clone(),
node: inserted_node, node_data: inserted_node,
rev_id: 1,
}, },
DeleteNode { path: path.clone() }, DeleteNode {
AssertNode { path, expected: None }, path: path.clone(),
rev_id: 2,
},
AssertNodeData { path, expected: None },
]; ];
test.run_scripts(scripts); test.run_scripts(scripts);
} }
@ -198,7 +305,8 @@ fn node_update_body_test() {
let scripts = vec![ let scripts = vec![
InsertNode { InsertNode {
path: path.clone(), path: path.clone(),
node, node_data: node,
rev_id: 1,
}, },
UpdateBody { UpdateBody {
path: path.clone(), path: path.clone(),