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",
"serde",
"serde_json",
"serde_repr",
"strum",
"strum_macros",
"thiserror",

View File

@ -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 = []

View File

@ -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<AttributeKey, AttributeValue>;
#[derive(Default, Clone, Serialize, Deserialize, Eq, PartialEq, Debug)]
@ -40,7 +40,7 @@ impl NodeAttributes {
}
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;
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct AttributeValue(pub Option<String>);
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<usize> 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<String>,
}
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<String> 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<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 {
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)
}
}

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)]
mod attributes;
mod attributes_serde;
mod node;
mod node_serde;
mod node_tree;

View File

@ -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<V>(self, mut map: V) -> Result<Self::Value, V::Error>
// where
// V: MapAccess<'de>,
// {
// let mut delta: Option<RichTextDelta> = 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<V>(self, mut map: V) -> Result<Self::Value, V::Error>
where
V: MapAccess<'de>,
{
let mut delta: Option<RichTextDelta> = 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())
}

View File

@ -246,7 +246,6 @@ impl std::convert::From<TextAttribute> 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")]

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 script;
mod tree_test;

View File

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

View File

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