use mf_model::{
mark::Mark, node::Node, node_definition::NodeTree, node_pool::NodePool,
types::NodeId,
};
use std::sync::Arc;
use rbatis::crud;
use serde::Serialize;
#[derive(Debug, Clone, Serialize)]
pub struct IndexDoc {
pub node_id: String,
pub node_type: String,
pub parent_id: Option<String>,
pub marks: Vec<String>,
pub marks_json: String,
pub attrs_flat: Vec<(String, String)>,
pub attrs_json: String,
pub text: Option<String>,
pub path: Vec<String>,
pub order_i64: Option<i64>,
pub created_at_i64: Option<i64>,
pub updated_at_i64: Option<i64>,
}
impl IndexDoc {
pub fn to_node(&self) -> anyhow::Result<Node> {
let marks: Vec<Mark> =
serde_json::from_str(&self.marks_json).unwrap_or_default();
let attrs_map: imbl::HashMap<String, serde_json::Value> =
serde_json::from_str(&self.attrs_json).unwrap_or_default();
let node = Node {
id: self.node_id.as_str().into(),
r#type: self.node_type.clone(),
attrs: mf_model::attrs::Attrs::from(attrs_map),
content: imbl::Vector::new(), marks: marks.into(),
};
Ok(node)
}
pub fn from_node(
pool: &NodePool,
node: &Arc<Node>,
) -> Self {
let parent_id = pool.parent_id(&node.id).cloned();
let marks: Vec<String> =
node.marks.iter().map(|m: &Mark| m.r#type.clone()).collect();
let marks_json = serde_json::to_string(&node.marks)
.unwrap_or_else(|_| "[]".to_string());
let mut attrs_flat = Vec::with_capacity(node.attrs.attrs.len());
for (k, v) in node.attrs.attrs.iter() {
attrs_flat.push((k.clone(), flatten_value(v)));
}
let attrs_json = serde_json::to_string(&node.attrs.attrs)
.unwrap_or_else(|_| "{}".to_string());
let path: Vec<String> = pool
.get_node_path(&node.id)
.into_iter()
.map(|id| id.to_string())
.collect();
let text = extract_text(node);
let order_i64 = extract_i64(node, "order");
let created_at_i64 = extract_i64(node, "created_at");
let updated_at_i64 = extract_i64(node, "updated_at");
IndexDoc {
node_id: node.id.to_string(),
node_type: node.r#type.clone(),
parent_id: parent_id.map(|id| id.to_string()),
marks,
marks_json,
attrs_flat,
attrs_json,
text,
path,
order_i64,
created_at_i64,
updated_at_i64,
}
}
}
fn flatten_value(v: &serde_json::Value) -> String {
match v {
serde_json::Value::Null => "null".to_string(),
serde_json::Value::Bool(b) => b.to_string(),
serde_json::Value::Number(n) => n.to_string(),
serde_json::Value::String(s) => s.clone(),
_ => serde_json::to_string(v).unwrap_or_default(),
}
}
fn extract_text(node: &Node) -> Option<String> {
for key in ["text", "title", "content"] {
if let Some(serde_json::Value::String(s)) = node.attrs.get(key) {
if !s.is_empty() {
return Some(s.clone());
}
}
}
None
}
fn extract_i64(
node: &Node,
key: &str,
) -> Option<i64> {
node.attrs.get(key).and_then(|v| match v {
serde_json::Value::Number(n) => n
.as_i64()
.or_else(|| n.as_u64().and_then(|u| i64::try_from(u).ok())),
serde_json::Value::String(s) => s.parse::<i64>().ok(),
_ => None,
})
}
pub fn collect_node_ids_from_enum(node_enum: &NodeTree) -> Vec<NodeId> {
let mut ids: Vec<NodeId> = vec![node_enum.0.id.clone()];
for child in &node_enum.1 {
ids.extend(collect_node_ids_from_enum(child));
}
ids
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_index_doc_to_node_roundtrip() {
let original_node = Arc::new(Node {
id: "test_node".into(),
r#type: "paragraph".into(),
attrs: mf_model::attrs::Attrs::from(
vec![
("title".to_string(), serde_json::json!("测试标题")),
("status".to_string(), serde_json::json!("published")),
]
.into_iter()
.collect(),
),
content: vec!["child1".into(), "child2".into()].into(),
marks: vec![
Mark {
r#type: "bold".into(),
attrs: mf_model::attrs::Attrs::default(),
},
Mark {
r#type: "link".into(),
attrs: mf_model::attrs::Attrs::from(
vec![(
"href".to_string(),
serde_json::json!("https://example.com"),
)]
.into_iter()
.collect(),
),
},
]
.into(),
});
let root = Node {
id: "root".into(),
r#type: "doc".into(),
attrs: mf_model::attrs::Attrs::default(),
content: imbl::Vector::new(),
marks: imbl::Vector::new(),
};
let tree = mf_model::tree::Tree::new(root);
let pool = NodePool::new(Arc::new(tree));
let index_doc = IndexDoc::from_node(&pool, &original_node);
assert_eq!(index_doc.node_id, "test_node");
assert_eq!(index_doc.node_type, "paragraph");
assert_eq!(index_doc.marks, vec!["bold", "link"]);
let converted_node = index_doc.to_node().unwrap();
assert_eq!(converted_node.id.as_ref(), "test_node");
assert_eq!(converted_node.r#type, "paragraph");
assert_eq!(
converted_node.attrs.get("title"),
Some(&serde_json::json!("测试标题"))
);
assert_eq!(
converted_node.attrs.get("status"),
Some(&serde_json::json!("published"))
);
assert_eq!(converted_node.marks.len(), 2);
assert_eq!(converted_node.marks[0].r#type, "bold");
assert_eq!(converted_node.marks[1].r#type, "link");
assert_eq!(
converted_node.marks[1].attrs.get("href"),
Some(&serde_json::json!("https://example.com"))
);
assert_eq!(converted_node.content.len(), 0);
}
#[test]
fn test_to_node_with_empty_data() {
let index_doc = IndexDoc {
node_id: "empty_node".into(),
node_type: "text".into(),
parent_id: None,
marks: vec![],
marks_json: "[]".into(),
attrs_flat: vec![],
attrs_json: "{}".into(),
text: None,
path: vec!["empty_node".into()],
order_i64: None,
created_at_i64: None,
updated_at_i64: None,
};
let node = index_doc.to_node().unwrap();
assert_eq!(node.id.as_ref(), "empty_node");
assert_eq!(node.r#type, "text");
assert_eq!(node.marks.len(), 0);
assert_eq!(node.attrs.attrs.len(), 0);
assert_eq!(node.content.len(), 0);
}
}