FlowDocument 序列化协议
`FlowDocument` 是流程图的 JSON 序列化协议,也是框架与外界(文件系统、网络、其他语言)交换流程定义的唯一格式。第五章已介绍它与 `FlowGraph` 的互转,本节聚焦协议本身的字段语义与 JSON 结构。
协议顶层结构
pub struct FlowDocument {
pub version: String, // 协议版本(当前 "1.0")
pub metadata: FlowMetadata, // 流程元数据
pub nodes: Vec<NodeDef>, // 节点定义列表
pub edges: Vec<EdgeDef>, // 边定义列表(用节点索引引用)
}
flowchart TB
FD[FlowDocument] --> VER[version: String]
FD --> META[metadata: FlowMetadata]
FD --> NODES[nodes: Vec<NodeDef>]
FD --> EDGES[edges: Vec<EdgeDef>]
META --> M_NAME[name]
META --> M_DESC[description?]
NODES --> ND1[NodeDef...]
EDGES --> ED1[EdgeDef...]
`version` 是协议版本号(区别于 `FlowGraph.version` 那个缓存计数器)。当前固定 `"1.0"`,未来若协议发生不兼容变更(如重命名字段),通过它做版本路由。
FlowMetadata:元数据
pub struct FlowMetadata {
pub name: String, // 流程名称
pub description: Option<String>, // 流程描述(可选)
}
impl FlowDocument {
pub fn new(name) -> Self { /* version="1.0", description=None, 空 nodes/edges */ }
pub fn with_description(mut self, desc) -> Self { ... }
}
元数据用于文件名建议、流程列表展示、文档说明,不参与布局或渲染逻辑。
NodeDef:节点定义
pub struct NodeDef {
pub kind: String, // 匹配 IFlowNode::kind
pub data: serde_json::Value, // 业务数据
pub size: Option<SizeF>, // None → 用 schema.default_size
pub position: Option<PointF>, // None → 由布局引擎计算
}
| 字段 | 序列化默认 | 含义 | |------|-----------|------| | `kind` | 必填 | 节点类型,决定 schema 与 `IFlowNode` 实现 | | `data` | 必填 | 业务数据,结构由 `NodeSchema.fields` 约束 | | `size` | `null` | 节点尺寸,`null` 时由 schema 兜底 | | `position` | `null` | 节点位置,`null` 时由 dagre 计算 |
构建器:
NodeDef::new("action", json!({"label":"处理","code":""}))
.with_size(SizeF::new(200.0, 80.0))
.with_position(PointF::new(100.0, 50.0))
> `size`/`position` 都设计为 `Option`,是因为序列化场景下「未指定」与「指定为零尺寸/零坐标」语义不同:未指定让框架兜底,指定则尊重用户布局。
EdgeDef:边定义
pub struct EdgeDef {
pub source: usize, // 源节点在 nodes 数组中的下标
pub target: usize, // 目标节点下标
pub source_port: Option<String>, // 源端口 ID(None → 自动推导)
pub target_port: Option<String>, // 目标端口 ID
pub edge_type: Option<EdgeType>, // 边类型(None → Bezier 默认)
}
**关键设计:用 `usize` 索引引用节点,而非 slotmap 键**。这是序列化稳定性的根基——数组下标天然可移植,slotmap 键含内部版本号不可跨进程。
构建器:
EdgeDef::new(0, 1) // nodes[0] → nodes[1]
.with_source_port("if_0")
.with_target_port("in")
.with_edge_type(EdgeType::SmoothStep)
完整 JSON 示例
一个 Start → Action → End 的最小流程:
{
"version": "1.0",
"metadata": {
"name": "hello-flow",
"description": "最小示例"
},
"nodes": [
{ "kind": "start", "data": { "label": "开始" }, "size": null, "position": null },
{ "kind": "action", "data": { "label": "处理", "code": "x = 1" },
"size": { "w": 180.0, "h": 64.0 }, "position": { "x": 100.0, "y": 0.0 } },
{ "kind": "end", "data": { "label": "结束" }, "size": null, "position": null }
],
"edges": [
{ "source": 0, "target": 1, "source_port": null, "target_port": null, "edge_type": null },
{ "source": 1, "target": 2, "source_port": null, "target_port": null, "edge_type": "Bezier" }
]
}
结构化节点的边
Condition 的多分支边通过 `source_port` 区分:
{
"nodes": [
{ "kind": "condition", "data": { "conditions": [
{"name":"分支1","expr":"x>0","desc":""},
{"name":"else","expr":"","desc":""}
]}, "size": null, "position": null },
{ "kind": "action", "data": {}, "size": null, "position": null },
{ "kind": "action", "data": {}, "size": null, "position": null }
],
"edges": [
{ "source": 0, "target": 1, "source_port": "if_0", "target_port": null, "edge_type": "SmoothStep" },
{ "source": 0, "target": 2, "source_port": "else", "target_port": null, "edge_type": "SmoothStep" }
]
}
Loop 的回环边用 `target_port: "loop_in"` + 渲染层置 `EdgeKind::LoopBack`(注意 `EdgeDef` 不直接存 `EdgeKind`,由渲染层根据 `loop_in` 端口推断):
{ "source": 2, "target": 0, "source_port": null, "target_port": "loop_in", "edge_type": "SmoothStep" }
构建文档的 API
impl FlowDocument {
pub fn new(name) -> Self;
pub fn with_description(self, desc) -> Self;
pub fn add_node(&mut self, node: NodeDef) -> usize; // 返回索引供边引用
pub fn add_edge(&mut self, edge: EdgeDef);
}
`add_node` 返回索引是关键设计——边需要用这个索引引用节点:
let mut doc = FlowDocument::new("demo").with_description("示例");
let start = doc.add_node(NodeDef::new("start", json!({"label":"开始"})));
let end = doc.add_node(NodeDef::new("end", json!({"label":"结束"})));
doc.add_edge(EdgeDef::new(start, end));
协议设计要点小结
| 设计 | 选择 | 原因 | |------|------|------| | 节点引用 | `usize` 数组索引 | 序列化稳定,跨语言可消费 | | `size`/`position` | `Option` | 区分「未指定」与「显式零值」 | | `edge_type` | `Option<EdgeType>` | `None` 时用默认 Bezier | | `source_port`/`target_port` | `Option<String>` | `None` 时框架自动推导 | | `EdgeKind` | 不存入文档 | 由渲染层根据 `loop_in` 端口推断,减少冗余 | | `version` | 字符串 | 未来协议演进做版本路由 |
与 FlowGraph 的关系回顾
flowchart LR
JSON[JSON 文件] -->|serde Deserialize| FD[FlowDocument]
FD -->|from_document| FG[FlowGraph 运行时]
FG -->|to_document| FD2[FlowDocument]
FD2 -->|serde Serialize| JSON2[JSON 文件]
往返保证逻辑结构等价(kind/data/边连接/端口引用),slotmap 键的具体值不保证相同——这是刻意的:运行时键是内存态实现细节,不应泄漏到序列化协议。
小结
`FlowDocument` = `version` + `metadata` + `nodes` + `edges`,节点用数组索引引用保证序列化稳定。`Option` 字段区分「未指定」与「显式值」,让框架能在「用户布局」与「自动布局」间切换。`EdgeKind` 不入文档,由渲染层按 `loop_in` 端口推断,减少冗余。这是框架与外界交换流程定义的唯一协议。
下一节:[边路径算法](../07-geometry-layout/edge-path-algorithms.md)