bpm-engine 0.2.0

Lightweight embeddable BPM runtime for long-running, stateful workflows with tokens, timers, Saga compensation, and crash recovery
Documentation
非常好,**B1 是整个 BPMN 能否落地的第一块基石**。
我下面给你的是 **「工程级 BPMN XML Parser 设计 + Rust 示例」**,目标是:

> ✅ 能稳定解析真实 BPMN 文件
> ✅ 不和 runtime 耦合
> ✅ 为后续 Compiler / Gateway / Event 留扩展点

不是一次性写完 BPMN,而是 **“先跑起来、可持续演进”** 的方案。

---

# 🎯 一、Parser 的设计目标(先立边界)

### Parser 只负责 4 件事

1. 解析 XML
2. 抽取 BPMN 元素
3. 建立 ID 引用关系
4. 输出 **BpmnModel(AST)**

❌ **绝不做**

- 执行语义
- Token 逻辑
- 条件计算
- 并行判断

---

# 🧩 二、推荐技术选型(Rust)

### XML 解析库(推荐)

|| 原因                        |
| ----------- | --------------------------- |
| `roxmltree` | 只读、零拷贝、非常适合 AST  |
| `quick-xml` | 流式,适合大文件(Phase 2) |

👉 **B1 阶段我建议用 `roxmltree`**
简单、可维护、Debug 体验好。

```toml
[dependencies]
roxmltree = "0.18"
```

---

# 📐 三、BpmnModel(AST)设计(核心)

### 顶层模型

```rust
pub struct BpmnModel {
    pub process_id: String,
    pub nodes: HashMap<String, BpmnNode>,
    pub flows: Vec<BpmnSequenceFlow>,
}
```

---

## BpmnNode(统一表示)

```rust
pub enum BpmnNode {
    StartEvent {
        id: String,
        outgoing: Vec<String>,
    },
    EndEvent {
        id: String,
        incoming: Vec<String>,
    },
    ServiceTask {
        id: String,
        name: Option<String>,
        task_type: String,
        incoming: Vec<String>,
        outgoing: Vec<String>,
    },
    UserTask {
        id: String,
        name: Option<String>,
        incoming: Vec<String>,
        outgoing: Vec<String>,
    },
    ExclusiveGateway {
        id: String,
        incoming: Vec<String>,
        outgoing: Vec<String>,
        default_flow: Option<String>,
    },
    ParallelGateway {
        id: String,
        incoming: Vec<String>,
        outgoing: Vec<String>,
    },
}
```

👉 **注意**

- outgoing / incoming 存的是 **sequenceFlow ID**
- 这样后续可以单独解析 flow

---

## SequenceFlow

```rust
pub struct BpmnSequenceFlow {
    pub id: String,
    pub source_ref: String,
    pub target_ref: String,
    pub condition: Option<String>,
}
```

condition 暂时保留字符串(Compiler 再处理)

---

# 🔍 四、XML 结构识别(你只需关心这些)

BPMN XML 本质长这样:

```xml
<definitions>
  <process id="order-process">
    <startEvent id="start"/>
    <serviceTask id="pay" name="Pay">
      <extensionElements>
        <camunda:type>payment</camunda:type>
      </extensionElements>
    </serviceTask>
    <sequenceFlow id="flow1" sourceRef="start" targetRef="pay"/>
  </process>
</definitions>
```

---

# 🧠 五、Parser 的模块拆分(非常重要)

```text
bpmn/
├── parser.rs      // parse entry
├── model.rs       // AST
├── xml.rs         // xml helpers
└── error.rs       // parse errors
```

---

# 🛠 六、核心解析流程(逐步)

## 1️⃣ 入口函数

```rust
pub fn parse_bpmn(xml: &str) -> Result<BpmnModel, BpmnError> {
    let doc = roxmltree::Document::parse(xml)?;
    let process = find_process(&doc)?;

    let nodes = parse_nodes(&process)?;
    let flows = parse_sequence_flows(&process)?;

    Ok(BpmnModel {
        process_id: process.attribute("id").unwrap().to_string(),
        nodes,
        flows,
    })
}
```

---

## 2️⃣ 找到 `<process>`

```rust
fn find_process(doc: &Document) -> Result<Node, BpmnError> {
    doc.descendants()
        .find(|n| n.has_tag_name("process"))
        .ok_or(BpmnError::ProcessNotFound)
}
```

---

## 3️⃣ 解析节点(Start / Task / Gateway)

```rust
fn parse_nodes(process: &Node) -> Result<HashMap<String, BpmnNode>, BpmnError> {
    let mut nodes = HashMap::new();

    for node in process.children().filter(|n| n.is_element()) {
        match node.tag_name().name() {
            "startEvent" => {
                let id = node.attribute("id").unwrap().to_string();
                nodes.insert(id.clone(), BpmnNode::StartEvent {
                    id,
                    outgoing: vec![],
                });
            }
            "serviceTask" => {
                let id = node.attribute("id").unwrap().to_string();
                let task_type = parse_task_type(&node)?;
                nodes.insert(id.clone(), BpmnNode::ServiceTask {
                    id,
                    name: node.attribute("name").map(|s| s.to_string()),
                    task_type,
                    incoming: vec![],
                    outgoing: vec![],
                });
            }
            _ => {}
        }
    }

    Ok(nodes)
}
```

---

## 4️⃣ 解析 Sequence Flow

```rust
fn parse_sequence_flows(process: &Node) -> Result<Vec<BpmnSequenceFlow>, BpmnError> {
    let mut flows = vec![];

    for flow in process.children().filter(|n| n.has_tag_name("sequenceFlow")) {
        flows.push(BpmnSequenceFlow {
            id: flow.attribute("id").unwrap().to_string(),
            source_ref: flow.attribute("sourceRef").unwrap().to_string(),
            target_ref: flow.attribute("targetRef").unwrap().to_string(),
            condition: parse_condition(flow),
        });
    }

    Ok(flows)
}
```

---

## 5️⃣ 建立 incoming / outgoing 引用

Parser 的最后一步(非常重要):

```rust
fn wire_flows(model: &mut BpmnModel) {
    for flow in &model.flows {
        model.nodes.get_mut(&flow.source_ref)
            .map(|n| n.add_outgoing(&flow.id));
        model.nodes.get_mut(&flow.target_ref)
            .map(|n| n.add_incoming(&flow.id));
    }
}
```

👉 建议用 trait:

```rust
trait FlowAttach {
    fn add_incoming(&mut self, flow_id: &str);
    fn add_outgoing(&mut self, flow_id: &str);
}
```

---

# ⚠️ 七、错误模型(别忽略)

```rust
pub enum BpmnError {
    Xml(roxmltree::Error),
    ProcessNotFound,
    MissingAttribute(String),
    UnsupportedElement(String),
}
```

Parser **应该严格**:

- ID 不存在 → error
- Flow source / target 不存在 → error

---

# 🧪 八、最小测试(必须有)

```rust
#[test]
fn parse_simple_bpmn() {
    let xml = include_str!("simple.bpmn");
    let model = parse_bpmn(xml).unwrap();

    assert!(model.nodes.contains_key("start"));
    assert!(model.nodes.contains_key("pay"));
    assert_eq!(model.flows.len(), 1);
}
```

---

# 🏁 九、你现在做到什么程度就算 B1 完成?

✅ 能 parse 一个标准 BPMN XML
✅ 能构建 AST(nodes + flows)
✅ 不 panic,错误可读
✅ Compiler 可以无痛接入

---

# 🔜 下一步建议(顺序)

你现在 **100% 应该继续**:

- **B2** → Exclusive / Parallel Gateway 的 join & 并发细节
- **B4** → BPMN 编译期错误模型(比 Parser 错误更重要)

如果你愿意,我可以 **下一步直接帮你写 `compiler.rs` 的骨架**,把 BPMN AST → 你现有的 `ProcessDefinition` 串起来。

直接回我:
👉 **B2** 或 **B4**