瀏覽代碼

chore: add deserial appflowy editor test

nathan 2 年之前
父節點
當前提交
04ba711441

+ 1 - 0
shared-lib/Cargo.lock

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

+ 4 - 5
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 = []
-
-

+ 60 - 35
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<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>);
+#[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 {
-        AttributeValue::from(*val)
-    }
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+pub struct AttributeValue {
+    pub ty: ValueType,
+    pub value: Option<String>,
 }
 
-impl std::convert::From<usize> for AttributeValue {
-    fn from(val: usize) -> Self {
-        if val > 0_usize {
-            AttributeValue(Some(format!("{}", val)))
-        } else {
-            AttributeValue(None)
+impl AttributeValue {
+    pub fn empty() -> Self {
+        Self {
+            ty: ValueType::StrType,
+            value: None,
+        }
+    }
+    pub fn from_int(val: usize) -> Self {
+        Self {
+            ty: ValueType::IntType,
+            value: Some(val.to_string()),
         }
     }
-}
 
-impl std::convert::From<&str> for AttributeValue {
-    fn from(val: &str) -> Self {
-        val.to_owned().into()
+    pub fn from_float(val: f64) -> Self {
+        Self {
+            ty: ValueType::FloatType,
+            value: Some(val.to_string()),
+        }
     }
-}
 
-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_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()
+    }
 
-impl std::convert::From<&bool> for AttributeValue {
-    fn from(val: &bool) -> Self {
-        AttributeValue::from(*val)
+    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)
     }
 }
 

+ 159 - 0
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<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)
+    }
+}

+ 1 - 0
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;

+ 25 - 25
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<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())
 }

+ 0 - 1
shared-lib/lib-ot/src/rich_text/attributes.rs

@@ -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")]

+ 162 - 0
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."
+        }
+      ]
+    }
+  ]
+}
+"#;

+ 1 - 0
shared-lib/lib-ot/tests/node/mod.rs

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

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

+ 0 - 53
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
-          }
-        }
-      ]
-    }
-  ]
-}
-"#;