diff --git a/shared-lib/Cargo.lock b/shared-lib/Cargo.lock index 596f1fe694..79357da09b 100644 --- a/shared-lib/Cargo.lock +++ b/shared-lib/Cargo.lock @@ -821,6 +821,7 @@ dependencies = [ "md5", "serde", "serde_json", + "serde_repr", "strum", "strum_macros", "thiserror", diff --git a/shared-lib/lib-ot/Cargo.toml b/shared-lib/lib-ot/Cargo.toml index d47968dcfe..3d0bd5aadd 100644 --- a/shared-lib/lib-ot/Cargo.toml +++ b/shared-lib/lib-ot/Cargo.toml @@ -10,14 +10,15 @@ bytecount = "0.6.0" serde = { version = "1.0", features = ["derive"] } #protobuf = {version = "2.18.0"} #flowy-derive = { path = "../flowy-derive" } -tokio = {version = "1", features = ["sync"]} +tokio = { version = "1", features = ["sync"] } dashmap = "5" md5 = "0.7.0" anyhow = "1.0" thiserror = "1.0" -serde_json = {version = "1.0"} -derive_more = {version = "0.99", features = ["display"]} +serde_json = { version = "1.0" } +serde_repr = { version = "0.1" } +derive_more = { version = "0.99", features = ["display"] } log = "0.4" tracing = { version = "0.1", features = ["log"] } lazy_static = "1.4.0" @@ -29,5 +30,3 @@ indextree = "4.4.0" [features] flowy_unit_test = [] - - diff --git a/shared-lib/lib-ot/src/core/document/attributes.rs b/shared-lib/lib-ot/src/core/document/attributes.rs index f36654fa54..cb102b6f9e 100644 --- a/shared-lib/lib-ot/src/core/document/attributes.rs +++ b/shared-lib/lib-ot/src/core/document/attributes.rs @@ -1,8 +1,8 @@ use crate::core::OperationTransform; use crate::errors::OTError; use serde::{Deserialize, Serialize}; +use serde_repr::*; use std::collections::HashMap; - pub type AttributeMap = HashMap; #[derive(Default, Clone, Serialize, Deserialize, Eq, PartialEq, Debug)] @@ -40,7 +40,7 @@ impl NodeAttributes { } pub fn delete(&mut self, key: K) { - self.insert(key.to_string(), AttributeValue(None)); + self.insert(key.to_string(), AttributeValue::empty()); } } @@ -94,54 +94,79 @@ impl OperationTransform for NodeAttributes { pub type AttributeKey = String; -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] -pub struct AttributeValue(pub Option); - -impl std::convert::From<&usize> for AttributeValue { - fn from(val: &usize) -> Self { - AttributeValue::from(*val) - } +#[derive(Eq, PartialEq, Hash, Debug, Clone, Serialize_repr, Deserialize_repr)] +#[repr(u8)] +pub enum ValueType { + IntType = 0, + FloatType = 1, + StrType = 2, + BoolType = 3, } -impl std::convert::From for AttributeValue { - fn from(val: usize) -> Self { - if val > 0_usize { - AttributeValue(Some(format!("{}", val))) - } else { - AttributeValue(None) +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct AttributeValue { + pub ty: ValueType, + pub value: Option, +} + +impl AttributeValue { + pub fn empty() -> Self { + Self { + ty: ValueType::StrType, + value: None, } } -} - -impl std::convert::From<&str> for AttributeValue { - fn from(val: &str) -> Self { - val.to_owned().into() - } -} - -impl std::convert::From for AttributeValue { - fn from(val: String) -> Self { - if val.is_empty() { - AttributeValue(None) - } else { - AttributeValue(Some(val)) + pub fn from_int(val: usize) -> Self { + Self { + ty: ValueType::IntType, + value: Some(val.to_string()), } } -} -impl std::convert::From<&bool> for AttributeValue { - fn from(val: &bool) -> Self { - AttributeValue::from(*val) + pub fn from_float(val: f64) -> Self { + Self { + ty: ValueType::FloatType, + value: Some(val.to_string()), + } + } + + pub fn from_bool(val: bool) -> Self { + Self { + ty: ValueType::BoolType, + value: Some(val.to_string()), + } + } + pub fn from_str(s: &str) -> Self { + let value = if s.is_empty() { None } else { Some(s.to_string()) }; + Self { + ty: ValueType::StrType, + value, + } + } + + pub fn int_value(&self) -> Option { + let value = self.value.as_ref()?; + Some(value.parse::().unwrap_or(0)) + } + + pub fn bool_value(&self) -> Option { + let value = self.value.as_ref()?; + Some(value.parse::().unwrap_or(false)) + } + + pub fn str_value(&self) -> Option { + self.value.clone() + } + + pub fn float_value(&self) -> Option { + let value = self.value.as_ref()?; + Some(value.parse::().unwrap_or(0.0)) } } impl std::convert::From for AttributeValue { - fn from(val: bool) -> Self { - let val = match val { - true => Some("true".to_owned()), - false => Some("false".to_owned()), - }; - AttributeValue(val) + fn from(value: bool) -> Self { + AttributeValue::from_bool(value) } } diff --git a/shared-lib/lib-ot/src/core/document/attributes_serde.rs b/shared-lib/lib-ot/src/core/document/attributes_serde.rs new file mode 100644 index 0000000000..60e30d07df --- /dev/null +++ b/shared-lib/lib-ot/src/core/document/attributes_serde.rs @@ -0,0 +1,159 @@ +use std::fmt; + +use serde::{ + de::{self, MapAccess, Visitor}, + Deserialize, Deserializer, Serialize, Serializer, +}; + +use super::AttributeValue; + +impl Serialize for AttributeValue { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + match self.ty { + super::ValueType::IntType => { + // + if let Some(value) = self.int_value() { + serializer.serialize_i64(value) + } else { + serializer.serialize_none() + } + } + super::ValueType::FloatType => { + if let Some(value) = self.float_value() { + serializer.serialize_f64(value) + } else { + serializer.serialize_none() + } + } + super::ValueType::StrType => { + if let Some(value) = self.str_value() { + serializer.serialize_str(&value) + } else { + serializer.serialize_none() + } + } + super::ValueType::BoolType => { + if let Some(value) = self.bool_value() { + serializer.serialize_bool(value) + } else { + serializer.serialize_none() + } + } + } + } +} + +impl<'de> Deserialize<'de> for AttributeValue { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct AttributeValueVisitor; + impl<'de> Visitor<'de> for AttributeValueVisitor { + type Value = AttributeValue; + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("bool, usize or string") + } + + fn visit_bool(self, value: bool) -> Result + where + E: de::Error, + { + Ok(AttributeValue::from_bool(value)) + } + + fn visit_i8(self, value: i8) -> Result + where + E: de::Error, + { + Ok(AttributeValue::from_int(value as usize)) + } + + fn visit_i16(self, value: i16) -> Result + where + E: de::Error, + { + Ok(AttributeValue::from_int(value as usize)) + } + + fn visit_i32(self, value: i32) -> Result + where + E: de::Error, + { + Ok(AttributeValue::from_int(value as usize)) + } + + fn visit_i64(self, value: i64) -> Result + where + E: de::Error, + { + Ok(AttributeValue::from_int(value as usize)) + } + + fn visit_u8(self, value: u8) -> Result + where + E: de::Error, + { + Ok(AttributeValue::from_int(value as usize)) + } + + fn visit_u16(self, value: u16) -> Result + where + E: de::Error, + { + Ok(AttributeValue::from_int(value as usize)) + } + + fn visit_u32(self, value: u32) -> Result + where + E: de::Error, + { + Ok(AttributeValue::from_int(value as usize)) + } + + fn visit_u64(self, value: u64) -> Result + where + E: de::Error, + { + Ok(AttributeValue::from_int(value as usize)) + } + + fn visit_str(self, s: &str) -> Result + where + E: de::Error, + { + Ok(AttributeValue::from_str(s)) + } + + fn visit_none(self) -> Result + where + E: de::Error, + { + Ok(AttributeValue::empty()) + } + + fn visit_unit(self) -> Result + where + E: de::Error, + { + // the value that contains null will be processed here. + Ok(AttributeValue::empty()) + } + + fn visit_map(self, map: A) -> Result + where + A: MapAccess<'de>, + { + // https://github.com/serde-rs/json/issues/505 + let mut map = map; + let value = map.next_value::()?; + Ok(value) + } + } + + deserializer.deserialize_any(AttributeValueVisitor) + } +} diff --git a/shared-lib/lib-ot/src/core/document/mod.rs b/shared-lib/lib-ot/src/core/document/mod.rs index 8480e7664a..023cb80d14 100644 --- a/shared-lib/lib-ot/src/core/document/mod.rs +++ b/shared-lib/lib-ot/src/core/document/mod.rs @@ -1,5 +1,6 @@ #![allow(clippy::module_inception)] mod attributes; +mod attributes_serde; mod node; mod node_serde; mod node_tree; diff --git a/shared-lib/lib-ot/src/core/document/node_serde.rs b/shared-lib/lib-ot/src/core/document/node_serde.rs index c51a27c6e5..32e54d0324 100644 --- a/shared-lib/lib-ot/src/core/document/node_serde.rs +++ b/shared-lib/lib-ot/src/core/document/node_serde.rs @@ -1,6 +1,6 @@ use super::NodeBody; use crate::rich_text::RichTextDelta; -use serde::de::{self, Visitor}; +use serde::de::{self, MapAccess, Visitor}; use serde::ser::SerializeMap; use serde::{Deserializer, Serializer}; use std::fmt; @@ -44,32 +44,32 @@ where Ok(NodeBody::Delta(delta)) } - // #[inline] - // fn visit_map(self, mut map: V) -> Result - // where - // V: MapAccess<'de>, - // { - // let mut delta: Option = None; - // while let Some(key) = map.next_key()? { - // match key { - // "delta" => { - // if delta.is_some() { - // return Err(de::Error::duplicate_field("delta")); - // } - // delta = Some(map.next_value()?); - // } - // other => { - // panic!("Unexpected key: {}", other); - // } - // } - // } + #[inline] + fn visit_map(self, mut map: V) -> Result + where + V: MapAccess<'de>, + { + let mut delta: Option = None; + while let Some(key) = map.next_key()? { + match key { + "delta" => { + if delta.is_some() { + return Err(de::Error::duplicate_field("delta")); + } + delta = Some(map.next_value()?); + } + other => { + panic!("Unexpected key: {}", other); + } + } + } - // if delta.is_some() { - // return Ok(NodeBody::Delta(delta.unwrap())); - // } + if delta.is_some() { + return Ok(NodeBody::Delta(delta.unwrap())); + } - // Err(de::Error::missing_field("delta")) - // } + Err(de::Error::missing_field("delta")) + } } deserializer.deserialize_any(NodeBodyVisitor()) } diff --git a/shared-lib/lib-ot/src/rich_text/attributes.rs b/shared-lib/lib-ot/src/rich_text/attributes.rs index e17711c5e2..d4d1d652f7 100644 --- a/shared-lib/lib-ot/src/rich_text/attributes.rs +++ b/shared-lib/lib-ot/src/rich_text/attributes.rs @@ -246,7 +246,6 @@ impl std::convert::From for TextAttributes { } #[derive(Clone, Debug, Display, Hash, Eq, PartialEq, serde::Serialize, serde::Deserialize)] -// serde.rs/variant-attrs.html // #[serde(rename_all = "snake_case")] pub enum TextAttributeKey { #[serde(rename = "bold")] diff --git a/shared-lib/lib-ot/tests/node/editor_test.rs b/shared-lib/lib-ot/tests/node/editor_test.rs new file mode 100644 index 0000000000..9cb4b5d98f --- /dev/null +++ b/shared-lib/lib-ot/tests/node/editor_test.rs @@ -0,0 +1,162 @@ +use super::script::{NodeScript::*, *}; +use lib_ot::{ + core::{NodeData, Path}, + rich_text::{AttributeBuilder, RichTextDeltaBuilder, TextAttribute, TextAttributes}, +}; + +#[test] +fn appflowy_editor_deserialize_node_test() { + let mut test = NodeTest::new(); + let node: NodeData = serde_json::from_str(EXAMPLE_JSON).unwrap(); + let path: Path = 0.into(); + + let expected_delta = RichTextDeltaBuilder::new() + .insert("👋 ") + .insert_with_attributes( + "Welcome to ", + AttributeBuilder::new().add_attr(TextAttribute::Bold(true)).build(), + ) + .insert_with_attributes( + "AppFlowy Editor", + AttributeBuilder::new().add_attr(TextAttribute::Italic(true)).build(), + ) + .build(); + + test.run_scripts(vec![ + InsertNode { + path: path.clone(), + node: node.clone(), + }, + AssertNumberOfNodesAtPath { path: None, len: 1 }, + AssertNumberOfNodesAtPath { + path: Some(0.into()), + len: 14, + }, + AssertNumberOfNodesAtPath { + path: Some(0.into()), + len: 14, + }, + AssertNodeDelta { + path: vec![0, 1].into(), + expected: expected_delta, + }, + AssertNode { + path: vec![0, 0].into(), + expected: Some(node.children[0].clone()), + }, + AssertNode { + path: vec![0, 3].into(), + expected: Some(node.children[3].clone()), + }, + ]); +} + +#[allow(dead_code)] +const EXAMPLE_JSON: &str = r#" +{ + "type": "editor", + "children": [ + { + "type": "image", + "attributes": { + "image_src": "https://s1.ax1x.com/2022/08/26/v2sSbR.jpg", + "align": "center" + } + }, + { + "type": "text", + "attributes": { + "subtype": "heading", + "heading": "h1" + }, + "body": { + "delta": [ + { + "insert": "👋 " + }, + { + "insert": "Welcome to ", + "attributes": { + "bold": true + } + }, + { + "insert": "AppFlowy Editor", + "attributes": { + "italic": true + } + } + ] + } + }, + { "type": "text", "delta": [] }, + { + "type": "text", + "body": { + "delta": [ + { "insert": "AppFlowy Editor is a " }, + { "insert": "highly customizable", "attributes": { "bold": true } }, + { "insert": " " }, + { "insert": "rich-text editor", "attributes": { "italic": true } }, + { "insert": " for " }, + { "insert": "Flutter", "attributes": { "underline": true } } + ] + } + }, + { + "type": "text", + "attributes": { "checkbox": true, "subtype": "checkbox" }, + "body": { + "delta": [{ "insert": "Customizable" }] + } + }, + { + "type": "text", + "attributes": { "checkbox": true, "subtype": "checkbox" }, + "delta": [{ "insert": "Test-covered" }] + }, + { + "type": "text", + "attributes": { "checkbox": false, "subtype": "checkbox" }, + "delta": [{ "insert": "more to come!" }] + }, + { "type": "text", "delta": [] }, + { + "type": "text", + "attributes": { "subtype": "quote" }, + "delta": [{ "insert": "Here is an exmaple you can give it a try" }] + }, + { "type": "text", "delta": [] }, + { + "type": "text", + "delta": [ + { "insert": "You can also use " }, + { + "insert": "AppFlowy Editor", + "attributes": { + "italic": true, + "bold": true, + "backgroundColor": "0x6000BCF0" + } + }, + { "insert": " as a component to build your own app." } + ] + }, + { "type": "text", "delta": [] }, + { + "type": "text", + "attributes": { "subtype": "bulleted-list" }, + "delta": [{ "insert": "Use / to insert blocks" }] + }, + { + "type": "text", + "attributes": { "subtype": "bulleted-list" }, + "delta": [ + { + "insert": "Select text to trigger to the toolbar to format your notes." + } + ] + } + ] +} +"#; diff --git a/shared-lib/lib-ot/tests/node/mod.rs b/shared-lib/lib-ot/tests/node/mod.rs index 95f6886069..dddad56eb5 100644 --- a/shared-lib/lib-ot/tests/node/mod.rs +++ b/shared-lib/lib-ot/tests/node/mod.rs @@ -1,3 +1,4 @@ +mod editor_test; mod operation_test; mod script; mod tree_test; diff --git a/shared-lib/lib-ot/tests/node/operation_test.rs b/shared-lib/lib-ot/tests/node/operation_test.rs index 6238b468d6..87a760b904 100644 --- a/shared-lib/lib-ot/tests/node/operation_test.rs +++ b/shared-lib/lib-ot/tests/node/operation_test.rs @@ -40,7 +40,7 @@ fn operation_update_node_attributes_serde_test() { assert_eq!( result, - r#"{"op":"update","path":[0,1],"attributes":{"bold":"true"},"oldAttributes":{"bold":"false"}}"# + r#"{"op":"update","path":[0,1],"attributes":{"bold":true},"oldAttributes":{"bold":false}}"# ); } diff --git a/shared-lib/lib-ot/tests/node/tree_test.rs b/shared-lib/lib-ot/tests/node/tree_test.rs index 8e35d5e428..b1a954055f 100644 --- a/shared-lib/lib-ot/tests/node/tree_test.rs +++ b/shared-lib/lib-ot/tests/node/tree_test.rs @@ -208,56 +208,3 @@ fn node_update_body_test() { ]; test.run_scripts(scripts); } - -// #[test] -// fn node_tree_deserial_from_operations_test() { -// let mut test = NodeTest::new(); -// let node: NodeData = serde_json::from_str(EXAMPLE_JSON).unwrap(); -// let path: Path = 0.into(); -// test.run_scripts(vec![InsertNode { -// path: path.clone(), -// node: node.clone(), -// }]); -// } - -#[allow(dead_code)] -const EXAMPLE_JSON: &str = r#" -{ - "type": "editor", - "children": [ - { - "type": "image", - "attributes": { - "image_src": "https://s1.ax1x.com/2022/08/26/v2sSbR.jpg", - "align": "center" - } - }, - { - "type": "text", - "attributes": { - "subtype": "heading", - "heading": "h1" - }, - "body": [ - { - "insert": "👋 " - }, - { - "insert": "Welcome to ", - "attributes": { - "bold": true - } - }, - { - "insert": "AppFlowy Editor", - "attributes": { - "href": "appflowy.io", - "italic": true, - "bold": true - } - } - ] - } - ] -} -"#;