///|
pub(all) enum BpmnKind {
Start
End
Task
Exclusive
Parallel
}
///|
pub(all) struct BpmnNode {
id : String
kind : BpmnKind
activity : String?
}
///|
pub(all) struct BpmnFlow {
id : String
source : String
target : String
}
///|
/// Control-flow subset with one start/end, plain tasks, exclusive and parallel
/// split/join gateways. No conditions, event semantics, subprocesses or data.
pub struct BpmnModel {
priv nodes : Array[BpmnNode]
priv flows : Array[BpmnFlow]
}
///|
pub fn BpmnModel::new(
nodes : Array[BpmnNode],
flows : Array[BpmnFlow],
) -> BpmnModel raise ProcessError {
if nodes.length() > 4096 || flows.length() > 4096 {
raise Capacity(resource="BPMN elements", limit=4096)
}
let ids : Map[String, Bool] = Map([])
let node_ids : Map[String, Bool] = Map([])
let mut starts = 0
let mut ends = 0
for node in nodes {
process_label(node.id, "bpmn.id")
model_require(!ids.contains(node.id), "bpmn.id", "duplicate node")
ids[node.id] = true
node_ids[node.id] = true
match node.kind {
Start => starts += 1
End => ends += 1
Task => {
let label = node.activity.unwrap_or_else(() => {
raise InvalidLog(
path="bpmn.task",
reason="task requires activity label",
)
})
process_label(label, "bpmn.task.name")
}
_ => ()
}
if node.kind is Task {
} else {
model_require(
node.activity is None,
"bpmn.gateway",
"non-task label must be absent",
)
}
}
model_require(
starts == 1 && ends == 1,
"bpmn",
"exactly one plain start and end required",
)
for flow in flows {
process_label(flow.id, "bpmn.flow.id")
model_require(
!ids.contains(flow.id) &&
node_ids.contains(flow.source) &&
node_ids.contains(flow.target),
"bpmn.flow",
"duplicate ID or dangling endpoint",
)
ids[flow.id] = true
}
for node in nodes {
let incoming = flows.filter(edge => edge.target == node.id).length()
let outgoing = flows.filter(edge => edge.source == node.id).length()
let valid = match node.kind {
Start => incoming == 0 && outgoing == 1
End => incoming == 1 && outgoing == 0
Task => incoming == 1 && outgoing == 1
Exclusive | Parallel =>
incoming >= 1 && outgoing >= 1 && (incoming == 1 || outgoing == 1)
}
model_require(
valid, "bpmn.degree", "invalid node degree or unsupported mixed gateway/task join",
)
}
{ nodes: nodes.copy(), flows: flows.copy(), }
}
///|
pub fn BpmnModel::nodes(self : BpmnModel) -> Array[BpmnNode] {
self.nodes.copy()
}
///|
pub fn BpmnModel::flows(self : BpmnModel) -> Array[BpmnFlow] {
self.flows.copy()
}
///|
pub fn BpmnModel::to_petri_net(self : BpmnModel) -> PetriNet raise ProcessError {
let places = ["source", "sink"]
let flow_places : Map[String, String] = Map([])
for i, flow in self.flows {
let name = "f\{i}"
places.push(name)
flow_places[flow.id] = name
}
let transitions : Array[NetTransition] = []
let arcs : Array[NetArc] = []
for node in self.nodes {
let incoming = self.flows
.filter(flow => flow.target == node.id)
.map(flow => flow_places[flow.id])
let outgoing = self.flows
.filter(flow => flow.source == node.id)
.map(flow => flow_places[flow.id])
match node.kind {
Exclusive =>
for input in incoming {
for output in outgoing {
let id = "t\{transitions.length()}"
transitions.push({ id, activity: None, })
arcs.push({ source: input, target: id, weight: 1, })
arcs.push({ source: id, target: output, weight: 1, })
}
}
_ => {
let id = "t\{transitions.length()}"
transitions.push({ id, activity: node.activity, })
for source in (if node.kind is Start { ["source"] } else { incoming }) {
arcs.push({ source, target: id, weight: 1, })
}
for target in (if node.kind is End { ["sink"] } else { outgoing }) {
arcs.push({ source: id, target, weight: 1, })
}
}
}
}
PetriNet::new(places, transitions, arcs, [{ place: "source", tokens: 1, }], [
{ place: "sink", tokens: 1, },
])
}
///|
fn bpmn_children(node : ModelXml) -> Unit raise ProcessError {
for item in node.children {
model_require(
item.ns == node.ns &&
["documentation", "incoming", "outgoing"].contains(item.name) &&
item.children.is_empty(),
"bpmn.node",
"event definitions, subprocesses and execution extensions unsupported",
)
}
}
///|
pub fn parse_bpmn(text : String) -> BpmnModel raise ProcessError {
let root = model_xml(text)
let ns = "http://www.omg.org/spec/BPMN/20100524/MODEL"
model_require(
root.name == "definitions" && root.ns == ns,
"bpmn",
"expected BPMN 2.0 definitions namespace",
)
let processes = root.children.filter(child => {
child.name == "process" && child.ns == ns
})
model_require(processes.length() == 1, "bpmn", "one process required")
for child in root.children {
model_require(
(child.ns == ns && child.name == "process") ||
(
child.ns == "http://www.omg.org/spec/BPMN/20100524/DI" &&
child.name == "BPMNDiagram"
),
"bpmn.definitions",
"unsupported definitions semantics",
)
}
let process = processes[0]
process.only_attributes(["id", "name", "isExecutable"])
model_require(
process.attributes.get("isExecutable").unwrap_or("false") == "false",
"bpmn",
"executable process semantics unsupported",
)
let nodes : Array[BpmnNode] = []
let flows : Array[BpmnFlow] = []
for child in process.children {
model_require(child.ns == ns, "bpmn", "foreign process extension")
if child.name == "documentation" {
continue
}
if child.name == "sequenceFlow" {
child.only_attributes(["id", "name", "sourceRef", "targetRef"])
model_require(
child.children.all(item => item.name == "documentation" && item.ns == ns),
"bpmn.flow",
"conditional/default execution unsupported",
)
model_require(
!child.attributes.contains("isImmediate"),
"bpmn.flow",
"execution attribute unsupported",
)
flows.push({
id: child.attribute("id"),
source: child.attribute("sourceRef"),
target: child.attribute("targetRef"),
})
continue
}
bpmn_children(child)
child.only_attributes(["id", "name", "gatewayDirection"])
model_require(
!child.attributes.contains("default") &&
!child.attributes.contains("isForCompensation"),
"bpmn.node",
"default/compensation semantics unsupported",
)
let kind = match child.name {
"startEvent" => Start
"endEvent" => End
"task" => Task
"exclusiveGateway" => Exclusive
"parallelGateway" => Parallel
_ =>
raise InvalidLog(
path="bpmn.node",
reason="unsupported BPMN node: \{child.name}",
)
}
nodes.push({
id: child.attribute("id"),
kind,
activity: if kind is Task {
Some(child.attribute("name"))
} else {
None
},
})
}
let result = BpmnModel::new(nodes, flows)
// Redundant incoming/outgoing XML must agree with sequenceFlow endpoints.
for node in process.children {
if node.name == "sequenceFlow" || node.name == "documentation" {
continue
}
for child in node.children {
if child.name == "incoming" || child.name == "outgoing" {
let candidates = flows.filter(flow => {
flow.id == child.text.trim().to_owned()
})
model_require(
candidates.length() == 1,
"bpmn.flowRef",
"unknown flow reference",
)
model_require(
if child.name == "incoming" {
candidates[0].target == node.attribute("id")
} else {
candidates[0].source == node.attribute("id")
},
"bpmn.flowRef",
"flow endpoint mismatch",
)
}
}
}
result
}
///|
pub fn BpmnModel::to_bpmn(self : BpmnModel) -> String {
let out = StringBuilder()
let xml_ids : Map[String, String] = Map([])
for i, node in self.nodes {
xml_ids[node.id] = "n\{i}"
}
out.write_string(
"",
)
for node in self.nodes {
let tag = match node.kind {
Start => "startEvent"
End => "endEvent"
Task => "task"
Exclusive => "exclusiveGateway"
Parallel => "parallelGateway"
}
out.write_string("<\{tag} id=\"\{xml_ids[node.id]}\"")
if node.activity is Some(activity) {
out.write_string(" name=\"\{xml_escape(activity)}\"")
}
out.write_string("/>")
}
for i, flow in self.flows {
out.write_string(
"",
)
}
out.write_string("")
out.to_string()
}