chore: add deserial appflowy editor test

This commit is contained in:
nathan
2022-09-11 22:06:36 +08:00
parent b7f65ff11d
commit 04ba711441
11 changed files with 418 additions and 124 deletions

1
shared-lib/Cargo.lock generated
View File

@ -821,6 +821,7 @@ dependencies = [
"md5", "md5",
"serde", "serde",
"serde_json", "serde_json",
"serde_repr",
"strum", "strum",
"strum_macros", "strum_macros",
"thiserror", "thiserror",

View File

@ -10,14 +10,15 @@ bytecount = "0.6.0"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
#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"] }
dashmap = "5" dashmap = "5"
md5 = "0.7.0" md5 = "0.7.0"
anyhow = "1.0" anyhow = "1.0"
thiserror = "1.0" thiserror = "1.0"
serde_json = {version = "1.0"} serde_json = { version = "1.0" }
derive_more = {version = "0.99", features = ["display"]} serde_repr = { version = "0.1" }
derive_more = { version = "0.99", features = ["display"] }
log = "0.4" log = "0.4"
tracing = { version = "0.1", features = ["log"] } tracing = { version = "0.1", features = ["log"] }
lazy_static = "1.4.0" lazy_static = "1.4.0"
@ -29,5 +30,3 @@ indextree = "4.4.0"
[features] [features]
flowy_unit_test = [] flowy_unit_test = []

View File

@ -1,8 +1,8 @@
use crate::core::OperationTransform; use crate::core::OperationTransform;
use crate::errors::OTError; use crate::errors::OTError;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_repr::*;
use std::collections::HashMap; use std::collections::HashMap;
pub type AttributeMap = HashMap<AttributeKey, AttributeValue>; pub type AttributeMap = HashMap<AttributeKey, AttributeValue>;
#[derive(Default, Clone, Serialize, Deserialize, Eq, PartialEq, Debug)] #[derive(Default, Clone, Serialize, Deserialize, Eq, PartialEq, Debug)]
@ -40,7 +40,7 @@ impl NodeAttributes {
} }
pub fn delete<K: ToString>(&mut self, key: K) { pub fn delete<K: ToString>(&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; pub type AttributeKey = String;
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] #[derive(Eq, PartialEq, Hash, Debug, Clone, Serialize_repr, Deserialize_repr)]
pub struct AttributeValue(pub Option<String>); #[repr(u8)]
pub enum ValueType {
impl std::convert::From<&usize> for AttributeValue { IntType = 0,
fn from(val: &usize) -> Self { FloatType = 1,
AttributeValue::from(*val) StrType = 2,
} BoolType = 3,
} }
impl std::convert::From<usize> for AttributeValue { #[derive(Debug, Clone, PartialEq, Eq, Hash)]
fn from(val: usize) -> Self { pub struct AttributeValue {
if val > 0_usize { pub ty: ValueType,
AttributeValue(Some(format!("{}", val))) pub value: Option<String>,
} else { }
AttributeValue(None)
impl AttributeValue {
pub fn empty() -> Self {
Self {
ty: ValueType::StrType,
value: None,
} }
} }
} pub fn from_int(val: usize) -> Self {
Self {
impl std::convert::From<&str> for AttributeValue { ty: ValueType::IntType,
fn from(val: &str) -> Self { value: Some(val.to_string()),
val.to_owned().into()
}
}
impl std::convert::From<String> for AttributeValue {
fn from(val: String) -> Self {
if val.is_empty() {
AttributeValue(None)
} else {
AttributeValue(Some(val))
} }
} }
}
impl std::convert::From<&bool> for AttributeValue { pub fn from_float(val: f64) -> Self {
fn from(val: &bool) -> Self { Self {
AttributeValue::from(*val) 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<i64> {
let value = self.value.as_ref()?;
Some(value.parse::<i64>().unwrap_or(0))
}
pub fn bool_value(&self) -> Option<bool> {
let value = self.value.as_ref()?;
Some(value.parse::<bool>().unwrap_or(false))
}
pub fn str_value(&self) -> Option<String> {
self.value.clone()
}
pub fn float_value(&self) -> Option<f64> {
let value = self.value.as_ref()?;
Some(value.parse::<f64>().unwrap_or(0.0))
} }
} }
impl std::convert::From<bool> for AttributeValue { impl std::convert::From<bool> for AttributeValue {
fn from(val: bool) -> Self { fn from(value: bool) -> Self {
let val = match val { AttributeValue::from_bool(value)
true => Some("true".to_owned()),
false => Some("false".to_owned()),
};
AttributeValue(val)
} }
} }

View File

@ -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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
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<D>(deserializer: D) -> Result<AttributeValue, D::Error>
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<E>(self, value: bool) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(AttributeValue::from_bool(value))
}
fn visit_i8<E>(self, value: i8) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(AttributeValue::from_int(value as usize))
}
fn visit_i16<E>(self, value: i16) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(AttributeValue::from_int(value as usize))
}
fn visit_i32<E>(self, value: i32) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(AttributeValue::from_int(value as usize))
}
fn visit_i64<E>(self, value: i64) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(AttributeValue::from_int(value as usize))
}
fn visit_u8<E>(self, value: u8) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(AttributeValue::from_int(value as usize))
}
fn visit_u16<E>(self, value: u16) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(AttributeValue::from_int(value as usize))
}
fn visit_u32<E>(self, value: u32) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(AttributeValue::from_int(value as usize))
}
fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(AttributeValue::from_int(value as usize))
}
fn visit_str<E>(self, s: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(AttributeValue::from_str(s))
}
fn visit_none<E>(self) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(AttributeValue::empty())
}
fn visit_unit<E>(self) -> Result<Self::Value, E>
where
E: de::Error,
{
// the value that contains null will be processed here.
Ok(AttributeValue::empty())
}
fn visit_map<A>(self, map: A) -> Result<Self::Value, A::Error>
where
A: MapAccess<'de>,
{
// https://github.com/serde-rs/json/issues/505
let mut map = map;
let value = map.next_value::<AttributeValue>()?;
Ok(value)
}
}
deserializer.deserialize_any(AttributeValueVisitor)
}
}

View File

@ -1,5 +1,6 @@
#![allow(clippy::module_inception)] #![allow(clippy::module_inception)]
mod attributes; mod attributes;
mod attributes_serde;
mod node; mod node;
mod node_serde; mod node_serde;
mod node_tree; mod node_tree;

View File

@ -1,6 +1,6 @@
use super::NodeBody; use super::NodeBody;
use crate::rich_text::RichTextDelta; use crate::rich_text::RichTextDelta;
use serde::de::{self, Visitor}; use serde::de::{self, MapAccess, Visitor};
use serde::ser::SerializeMap; use serde::ser::SerializeMap;
use serde::{Deserializer, Serializer}; use serde::{Deserializer, Serializer};
use std::fmt; use std::fmt;
@ -44,32 +44,32 @@ where
Ok(NodeBody::Delta(delta)) Ok(NodeBody::Delta(delta))
} }
// #[inline] #[inline]
// fn visit_map<V>(self, mut map: V) -> Result<Self::Value, V::Error> fn visit_map<V>(self, mut map: V) -> Result<Self::Value, V::Error>
// where where
// V: MapAccess<'de>, V: MapAccess<'de>,
// { {
// let mut delta: Option<RichTextDelta> = None; let mut delta: Option<RichTextDelta> = None;
// while let Some(key) = map.next_key()? { while let Some(key) = map.next_key()? {
// match key { match key {
// "delta" => { "delta" => {
// if delta.is_some() { if delta.is_some() {
// return Err(de::Error::duplicate_field("delta")); return Err(de::Error::duplicate_field("delta"));
// } }
// delta = Some(map.next_value()?); delta = Some(map.next_value()?);
// } }
// other => { other => {
// panic!("Unexpected key: {}", other); panic!("Unexpected key: {}", other);
// } }
// } }
// } }
// if delta.is_some() { if delta.is_some() {
// return Ok(NodeBody::Delta(delta.unwrap())); return Ok(NodeBody::Delta(delta.unwrap()));
// } }
// Err(de::Error::missing_field("delta")) Err(de::Error::missing_field("delta"))
// } }
} }
deserializer.deserialize_any(NodeBodyVisitor()) deserializer.deserialize_any(NodeBodyVisitor())
} }

View File

@ -246,7 +246,6 @@ impl std::convert::From<TextAttribute> for TextAttributes {
} }
#[derive(Clone, Debug, Display, Hash, Eq, PartialEq, serde::Serialize, serde::Deserialize)] #[derive(Clone, Debug, Display, Hash, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
// serde.rs/variant-attrs.html
// #[serde(rename_all = "snake_case")] // #[serde(rename_all = "snake_case")]
pub enum TextAttributeKey { pub enum TextAttributeKey {
#[serde(rename = "bold")] #[serde(rename = "bold")]

View File

@ -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."
}
]
}
]
}
"#;

View File

@ -1,3 +1,4 @@
mod editor_test;
mod operation_test; mod operation_test;
mod script; mod script;
mod tree_test; mod tree_test;

View File

@ -40,7 +40,7 @@ fn operation_update_node_attributes_serde_test() {
assert_eq!( assert_eq!(
result, 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}}"#
); );
} }

View File

@ -208,56 +208,3 @@ fn node_update_body_test() {
]; ];
test.run_scripts(scripts); 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
}
}
]
}
]
}
"#;