///|
pub struct Conversion {
diagram : @render.Diagram?
diagnostics : ReadOnlyArray[@diagrams.Diagnostic]
} derive(Eq, Debug)
///|
pub extend Conversion with Eq::{equal, not_equal}
///|
pub extend Conversion with @debug.Debug::{to_repr}
///|
priv struct Instance {
resource : Int32
root : Int32
tag : String
pool : String?
container : String?
members : Map[Int32, Bool]
}
///|
priv struct Projection {
converter : Converter
instances : Array[Instance]
nodes : Array[@model.Node]
flows : Array[@model.Flow]
visuals : Map[(Int32, Int32, String), Int32]
selected : Map[(Int32, Int32), Bool]
di : Map[(Int32, Int32), Array[Int32]]
di_order : Array[(Int32, Int32)]
used_di : Map[Int32, Bool]
shapes : Map[Int32, Array[Int32]]
visual_order : Array[(Int32, Int32, String)]
}
///|
fn Projection::include_scope(
self : Projection,
resource : Int32,
root : Int32,
tag : String,
pool : String?,
container? : String,
) -> Unit {
let c = self.converter
let index = c.sources[resource].index
let members = Map([(root, true)])
if !c.charge(index.nodes.length()) {
return
}
for n in index.nodes {
if n.parent.map(p => members.contains(p)).unwrap_or(false) {
members[n.ordinal] = true
}
}
self.instances.push({ resource, root, tag, pool, container, members, })
for ordinal in members.keys() {
self.selected[(resource, ordinal)] = true
}
}
///|
fn Projection::selected_di(self : Projection, selected : Selected) -> Unit {
let c = self.converter
if selected.plane is Some(plane) {
self.selected[(0, plane)] = true
let index = c.sources[0].index
let members = Map([(plane, true)])
for n in index.nodes {
if n.parent.map(p => members.contains(p)).unwrap_or(false) {
members[n.ordinal] = true
self.selected[(0, n.ordinal)] = true
}
}
for ordinal in index.nodes[plane].children {
let e = index.nodes[ordinal].element
if actual(e) != Some(BpmnBPMNShape) && actual(e) != Some(BpmnBPMNEdge) {
c.add(
"bpmn/unsupported-element",
"Selected plane contains an unsupported graphical element",
e~,
)
continue
}
let targets = c.resolve_field(0, ordinal, "bpmnElement")
if targets.length() != 1 {
c.add(
"bpmn/invalid-reference",
"Selected DI must identify one graphical element",
e~,
)
continue
}
let key = targets[0]
if !self.di.contains(key) {
self.di_order.push(key)
}
match self.di.get(key) {
Some(values) => values.push(ordinal)
None => self.di[key] = [ordinal]
}
}
}
}
///|
fn Projection::geometry_for(
self : Projection,
key : (Int32, Int32),
shape : Bool,
) -> Int32? {
let candidates = self.di.get(key).unwrap_or([])
let expected = if shape { @bpmn.TypeId::BpmnBPMNShape } else { BpmnBPMNEdge }
let values = candidates.filter(i => {
actual(self.converter.sources[0].index.nodes[i].element) == Some(expected)
})
if values.length() > 1 {
self.converter.add(
"bpmn/ambiguous-di",
"Multiple DI elements depict one source identity; select an unambiguous plane",
e=self.converter.sources[key.0].index.nodes[key.1].element,
)
return None
}
values.get(0)
}
///|
fn Projection::add_node(
self : Projection,
instance : Instance,
ordinal : Int32,
kind : @model.NodeKind,
) -> Unit {
let c = self.converter
let e = c.sources[instance.resource].index.nodes[ordinal].element
let id = c.render_id(instance.resource, ordinal, instance.tag)
let geometry_ordinal = self.geometry_for((instance.resource, ordinal), true)
let geometry = geometry_ordinal.map(i => c.shape(i))
let ownership = @model.Ownership::new(pool?=instance.pool)
let mut label = if kind == TextAnnotation {
e.children
.iter()
.find_first(c => actual(c) == Some(Text))
.map(c => c.content)
.unwrap_or("")
} else {
e.attribute("name").unwrap_or("")
}
let label_ref = if kind == Group {
"categoryValueRef"
} else if kind == DataObjectReference {
"dataObjectRef"
} else if kind == DataStoreReference {
"dataStoreRef"
} else {
""
}
if label_ref != "" && label.is_empty() {
for key in c.resolve_field(instance.resource, ordinal, label_ref) {
let element = c.sources[key.0].index.nodes[key.1].element
label = element
.attribute(if kind == Group { "value" } else { "name" })
.unwrap_or("")
}
}
let markers = c.markers(instance.resource, ordinal)
self.visuals[(instance.resource, ordinal, instance.tag)] = self.nodes.length()
self.visual_order.push((instance.resource, ordinal, instance.tag))
if geometry_ordinal is Some(i) {
self.used_di[i] = true
match self.shapes.get(i) {
Some(values) => values.push(self.nodes.length())
None => self.shapes[i] = [self.nodes.length()]
}
}
self.nodes.push(
@model.Node::new(
id,
kind,
bounds=integer_bounds(geometry),
source=c.identity(instance.resource, ordinal),
label~,
geometry?,
ownership~,
markers~,
expanded?=geometry.bind(g => g.expanded),
black_box=kind == Participant && e.attribute("processRef") == None,
),
)
}
///|
fn Projection::collect(self : Projection, instance : Instance) -> Unit {
let c = self.converter
let index = c.sources[instance.resource].index
let referenced_data = Map([])
for n in index.nodes {
if !instance.members.contains(n.ordinal) {
continue
}
if actual(n.element) == Some(DataObjectReference) {
for key in c.resolve_field(instance.resource, n.ordinal, "dataObjectRef") {
referenced_data[key] = true
}
}
}
for n in index.nodes {
if !instance.members.contains(n.ordinal) ||
(
n.ordinal == instance.root &&
!(is_type(n.element, SubProcess) &&
self.di.contains((instance.resource, n.ordinal)))
) {
continue
}
let e = n.element
if e.kind is Unknown(_) {
c.add(
"bpmn/unsupported-element",
"Selected standard element is unknown",
e~,
)
continue
}
if n.payload {
continue
}
if actual(e) == Some(ImplicitThrowEvent) {
if self.di.contains((instance.resource, n.ordinal)) {
c.add(
"bpmn/unsupported-element",
"Implicit throw events are non-graphical",
e~,
)
}
continue
}
if is_type(e, ChoreographyActivity) ||
is_type(e, ConversationNode) ||
actual(e) == Some(ConversationLink) ||
actual(e) == Some(ConversationAssociation) {
c.add(
"bpmn/unsupported-view",
"Choreography and conversation rendering is deferred; the document remains intact",
e~,
)
continue
}
if node_kind(e) is Some(kind) {
let has_shape = self.di.contains((instance.resource, n.ordinal))
// Data definitions can have explicit depictions; references carry the usual
// data symbol. Activity-local IO stays a semantic endpoint, never a shape.
if kind == DataObject &&
referenced_data.contains((instance.resource, n.ordinal)) &&
!has_shape {
continue
}
if kind == DataInput || kind == DataOutput {
let parent = n.parent.map(p => index.nodes[p])
let process_io = parent
.bind(p => p.parent)
.map(p => {
p == instance.root &&
actual(index.nodes[p].element) == Some(Process)
})
.unwrap_or(false)
if !process_io {
if has_shape {
c.add(
"bpmn/unsupported-element",
"Only process data inputs and outputs have graphical depictions",
e~,
)
}
continue
}
}
self.add_node(instance, n.ordinal, kind)
} else if connection_kind(e) == None &&
(e.kind is Unknown(_) || is_type(e, FlowNode) || is_type(e, Artifact)) {
c.add(
"bpmn/unsupported-element",
"Selected graphical kind \{e.kind.local_name()} has no conversion",
e~,
)
}
}
}
///|
fn Projection::visual(
self : Projection,
key : (Int32, Int32),
instance : String,
owner : @bpmn.Element,
) -> Int32? {
if self.visuals.get((key.0, key.1, instance)) is Some(i) {
return Some(i)
}
let candidates = []
for ((resource, ordinal, _), i) in self.visuals {
if resource == key.0 && ordinal == key.1 {
candidates.push(i)
}
}
if candidates.length() == 1 {
return Some(candidates[0])
}
if candidates.length() > 1 {
self.converter.add(
"bpmn/ambiguous-endpoint",
"Endpoint has multiple visible participant instances; supply an explicit DI endpoint",
e=owner,
)
return None
}
let index = self.converter.sources[key.0].index
let mut parent = index.nodes[key.1].parent
while parent is Some(ordinal) {
if self.visuals.get((key.0, ordinal, instance)) is Some(i) {
return Some(i)
}
parent = index.nodes[ordinal].parent
}
self.converter.add(
"bpmn/invalid-reference",
"Selected endpoint has no graphical owner",
e=owner,
)
None
}
///|
fn Projection::display_endpoint(
self : Projection,
semantic : (Int32, Int32),
tag : String,
di : Int32?,
field : String,
owner : @bpmn.Element,
) -> Int32? {
if di is Some(di) {
let refs = self.converter.resolve_field(0, di, field)
if refs.length() == 1 {
let key = refs[0]
if key.0 != 0 {
self.converter.add(
"bpmn/invalid-reference",
"DI endpoint must belong to the selected plane",
e=owner,
)
return None
}
let candidates = self.shapes.get(key.1).unwrap_or([])
if candidates.length() != 1 {
self.converter.add(
"bpmn/ambiguous-endpoint",
"DI endpoint does not identify one selected shape",
e=owner,
)
return None
}
return Some(candidates[0])
}
}
let visual = self.visual(semantic, tag, owner)
if actual(owner) == Some(MessageFlow) &&
visual is Some(i) &&
self.nodes[i].ownership.pool is Some(pool) {
if !self.converter.charge(self.nodes.length()) {
return None
}
for index, node in self.nodes {
if node.id == pool && node.black_box {
return Some(index)
}
}
}
visual
}
///|
fn Projection::ownership(self : Projection) -> Unit {
let c = self.converter
for instance in self.instances {
let index = c.sources[instance.resource].index
let lane_members : Map[Int32, Array[String]] = Map([])
let category_groups : Map[(Int32, Int32), Array[String]] = Map([])
for (resource, ordinal, tag) in self.visual_order {
let node = self.nodes[self.visuals[(resource, ordinal, tag)]]
if resource != instance.resource ||
tag != instance.tag ||
node.kind != Group {
continue
}
for key in c.resolve_field(resource, ordinal, "categoryValueRef") {
match category_groups.get(key) {
Some(groups) => groups.push(node.id)
None => category_groups[key] = [node.id]
}
}
}
for n in index.nodes {
if !instance.members.contains(n.ordinal) ||
actual(n.element) != Some(Lane) {
continue
}
let mut lanes = [c.render_id(instance.resource, n.ordinal, instance.tag)]
let mut parent = n.parent
while parent is Some(p) {
if actual(index.nodes[p].element) == Some(Lane) {
lanes.insert(0, c.render_id(instance.resource, p, instance.tag))
}
parent = index.nodes[p].parent
}
for key in c.resolve_field(instance.resource, n.ordinal, "flowNodeRef") {
if key.0 != instance.resource || !instance.members.contains(key.1) {
c.add(
"bpmn/invalid-reference",
"Lane member is outside its participant process",
e=n.element,
)
continue
}
match lane_members.get(key.1) {
Some(existing) =>
for lane in lanes {
if !existing.contains(lane) {
existing.push(lane)
}
}
None => lane_members[key.1] = lanes.copy()
}
}
lanes = []
}
for (resource, ordinal, tag) in self.visual_order {
let i = self.visuals[(resource, ordinal, tag)]
if resource != instance.resource ||
tag != instance.tag ||
!instance.members.contains(ordinal) {
continue
}
let n = index.nodes[ordinal]
let node = self.nodes[i]
let mut subprocess = None
let mut parent = n.parent
let lanes = lane_members.get(ordinal).unwrap_or([])
while parent is Some(p) {
if p == instance.root && !self.visuals.contains((resource, p, tag)) {
break
}
if subprocess == None && is_type(index.nodes[p].element, SubProcess) {
subprocess = Some(c.render_id(resource, p, tag))
}
if node.kind == Lane && actual(index.nodes[p].element) == Some(Lane) {
let lane = c.render_id(resource, p, tag)
if !lanes.contains(lane) {
lanes.insert(0, lane)
}
}
if p == instance.root {
break
}
parent = index.nodes[p].parent
}
if subprocess == None {
subprocess = instance.container
}
let attached = c
.resolve_field(resource, ordinal, "attachedToRef")
.get(0)
.bind(key => self.visual(key, tag, n.element))
.map(i => self.nodes[i].id)
let groups : Array[String] = []
if node.kind != Group {
for key in c.resolve_field(resource, ordinal, "categoryValueRef") {
for group in category_groups.get(key).unwrap_or([]) {
if !groups.contains(group) {
groups.push(group)
}
}
}
}
self.nodes[i] = node.with_ownership(
@model.Ownership::new(
pool?=instance.pool,
lanes~,
subprocess?,
attached_to?=attached,
groups~,
),
)
}
}
}
///|
fn Projection::connections(self : Projection, instance : Instance) -> Unit {
let c = self.converter
let index = c.sources[instance.resource].index
for n in index.nodes {
if !instance.members.contains(n.ordinal) || n.payload {
continue
}
let e = n.element
guard connection_kind(e) is Some(kind) else { continue }
let sources = c.resolve_field(instance.resource, n.ordinal, "sourceRef")
let targets = c.resolve_field(instance.resource, n.ordinal, "targetRef")
guard targets.length() == 1 else {
c.add("bpmn/invalid-reference", "Connection requires one target", e~)
continue
}
let data = kind == DataAssociation ||
kind == DataInputAssociation ||
kind == DataOutputAssociation
if !data && sources.length() != 1 {
c.add("bpmn/invalid-reference", "Connection requires one source", e~)
continue
}
let di = self.geometry_for((instance.resource, n.ordinal), false)
let geometry = di.map(i => {
self.used_di[i] = true
c.edge(i)
})
guard self.display_endpoint(
targets[0],
instance.tag,
di,
"targetElement",
e,
)
is Some(target) else {
continue
}
let semantic_target = c.identity(targets[0].0, targets[0].1)
let condition = e.children
.iter()
.find_first(c => c.kind == ConditionExpression)
.map(c => c.content)
let count = Int32::max(1, sources.length())
for j in 0.. {
self.display_endpoint(key, instance.tag, di, "sourceElement", e)
})
if source_key != None && source == None {
continue
}
let identity = c.identity(instance.resource, n.ordinal)
let base = c.render_id(instance.resource, n.ordinal, instance.tag)
let id = if count == 1 { base } else { base + "@source-\{j}" }
let is_default = source_key
.map(key => {
c
.resolve_field(key.0, key.1, "default")
.contains((instance.resource, n.ordinal))
})
.unwrap_or(false)
let direction : @model.AssociationDirection = match
e.attribute("associationDirection") {
Some("One") => One
Some("Both") => Both
_ => None
}
self.flows.push(
@model.Flow::new(
id,
source.map(i => self.nodes[i].id).unwrap_or(""),
self.nodes[target].id,
points=integer_points(geometry),
identity~,
kind~,
has_source=source_key != None,
semantic_source?=source_key.map(key => c.identity(key.0, key.1)),
semantic_target~,
geometry?,
label=e.attribute("name").unwrap_or(""),
condition?,
is_default~,
direction~,
),
)
}
}
}
///|
/// Pure view conversion over caller-supplied resources. Geometry remains optional;
/// the SVG renderer owns complete-DI versus whole-view layout policy.
pub fn to_diagram(
document : @bpmn.Document,
selector~ : ViewSelector = Automatic,
resources~ : @bpmn.ResourceSet = @bpmn.ResourceSet::new(),
resource_id? : String,
limits~ : @bpmn.Limits = @bpmn.Limits::new(),
geometry_policy~ : @model.GeometryPolicy = PreferDi,
) -> Conversion {
let documents = resources.documents.iter().to_array()
if resource_id is Some(id) && !documents.iter().any(d => d.location == id) {
documents.push(@bpmn.DocumentResource::new(id, document))
}
let resources = @bpmn.ResourceSet::new(
documents~,
external=resources.external,
)
let c = Converter::{
ignore_di: geometry_policy == @model.GeometryPolicy::Automatic,
sources: [],
dependencies: Map([]),
resources,
limits,
diagnostics: [],
failed: false,
work: 0,
}
fn result(diagram : @render.Diagram?) -> Conversion {
{
diagram: if c.failed {
None
} else {
diagram
},
diagnostics: ReadOnlyArray::from_array(c.diagnostics),
}
}
if limits.nodes <= 0 ||
limits.depth <= 0 ||
limits.diagnostics <= 0 ||
limits.token_units <= 0 ||
limits.source_units <= 0 ||
limits.output_units <= 0 {
return {
diagram: None,
diagnostics: [
@diagrams.Diagnostic::error(
"bpmn/conversion-limit", "All limits must be positive",
),
],
}
}
let root = make_source(document, resource_id, resources, limits)
c.sources.push(root)
if !c.charge(root.references.references.length()) {
return result(None)
}
if !root.index.complete || !root.references.complete {
c.add("bpmn/conversion-limit", "View validation exceeded its limits")
return result(None)
}
guard c.select(selector) is Some(selected) else { return result(None) }
let view = c.sources[selected.resource].index.nodes[selected.node].element
if actual(view) != Some(Process) &&
!is_type(view, SubProcess) &&
actual(view) != Some(Collaboration) {
c.add(
"bpmn/unsupported-view",
"Choreography and conversation rendering is deferred",
e=view,
)
return result(None)
}
let p = Projection::{
converter: c,
instances: [],
nodes: [],
flows: [],
visuals: Map([]),
selected: Map([]),
di: Map([]),
di_order: [],
used_di: Map([]),
shapes: Map([]),
visual_order: [],
}
if !c.ignore_di {
p.selected_di(selected)
}
if c.failed {
return result(None)
}
p.include_scope(selected.resource, selected.node, "", None)
if c.failed {
return result(None)
}
guard p.instances.get(0) is Some(root_instance) else {
c.add("bpmn/conversion-limit", "Root view scope was not completed")
return result(None)
}
if actual(view) == Some(Collaboration) {
let index = c.sources[selected.resource].index
for n in index.nodes {
if !root_instance.members.contains(n.ordinal) ||
actual(n.element) != Some(Participant) {
continue
}
let pool = c.render_id(selected.resource, n.ordinal, "")
let processes = c.resolve_field(
selected.resource,
n.ordinal,
"processRef",
)
if processes.length() > 1 {
c.add(
"bpmn/ambiguous-endpoint",
"Participant has multiple process targets",
e=n.element,
)
}
for key in processes {
if actual(c.sources[key.0].index.nodes[key.1].element) != Some(Process) {
c.add(
"bpmn/invalid-reference",
"Participant must identify a process",
e=n.element,
)
continue
}
p.include_scope(key.0, key.1, pool, Some(pool))
}
}
}
let mut instance_index = 0
while instance_index < p.instances.length() && !c.failed {
let instance = p.instances[instance_index]
instance_index += 1
p.collect(instance)
let index = c.sources[instance.resource].index
for n in index.nodes {
if !instance.members.contains(n.ordinal) ||
actual(n.element) != Some(CallActivity) {
continue
}
let visual = p.visuals.get((instance.resource, n.ordinal, instance.tag))
if visual.map(i => p.nodes[i].expanded == Some(true)).unwrap_or(false) {
for
key in c.resolve_field(instance.resource, n.ordinal, "calledElement") {
if actual(c.sources[key.0].index.nodes[key.1].element) ==
Some(Process) {
let call = p.nodes[visual.unwrap()].id
p.include_scope(key.0, key.1, call, instance.pool, container=call)
}
}
}
}
}
// Root-level messages and stores can have explicit selected-plane shapes.
for key in p.di_order {
if p.selected.contains(key) {
continue
}
let element = c.sources[key.0].index.nodes[key.1].element
if actual(element) == Some(Message) || actual(element) == Some(DataStore) {
p.selected[key] = true
p.add_node(
{
resource: key.0,
root: key.1,
tag: "",
pool: None,
container: None,
members: Map([(key.1, true)]),
},
key.1,
node_kind(element).unwrap(),
)
} else {
c.add(
"bpmn/invalid-reference",
"DI element is outside the selected view",
e=element,
)
}
}
if selected.plane != None {
for instance in p.instances {
if instance.pool is Some(pool) {
let has_internal_di = p.di_order
.iter()
.any(key => {
key.0 == instance.resource && instance.members.contains(key.1)
})
if !has_internal_di {
for i, node in p.nodes {
if node.id == pool && node.geometry != None {
p.nodes[i] = node.with_black_box(true)
}
}
}
}
}
}
p.ownership()
for instance in p.instances {
p.connections(instance)
}
for key in p.di_order {
for di in p.di[key] {
if !p.used_di.contains(di) {
c.add(
"bpmn/unsupported-element",
"Selected DI has no graphical projection",
e=c.sources[0].index.nodes[di].element,
)
}
}
}
let seen : Array[@bpmn.ValidationFinding] = []
for source in c.sources {
for finding in source.findings {
if seen.contains(finding) {
continue
}
seen.push(finding)
let mut resource : Int32? = None
for r, s in c.sources {
if s.id == finding.resource_id {
resource = Some(r)
break
}
}
let selected = resource
.map(r => {
finding.node
.map(n => {
p.selected.contains((r, n)) ||
c.dependencies.contains((r, n)) ||
n == 0
})
.unwrap_or(false)
})
.unwrap_or(false)
let fatal = selected &&
finding.diagnostic.code != "bpmn/unresolved-import"
let diagnostic = map_diagnostic(finding.diagnostic)
if c.diagnostics.length() < limits.diagnostics {
c.diagnostics.push(
if fatal {
diagnostic
} else {
{ ..diagnostic, severity: Warning, }
},
)
} else {
c.add("bpmn/conversion-limit", "Combined diagnostic limit exceeded")
}
if fatal {
c.failed = true
}
}
}
let ids = Map([])
for node in p.nodes {
if ids.contains(node.id) {
c.add("bpmn/duplicate-id", "Duplicate generated render identity")
}
ids[node.id] = true
}
for flow in p.flows {
if ids.contains(flow.id) {
c.add("bpmn/duplicate-id", "Duplicate generated render identity")
}
ids[flow.id] = true
}
let id = c.render_id(selected.resource, selected.node, "")
let bpmn = @model.Bpmn::new(
id,
p.nodes,
p.flows,
label=view.attribute("name").unwrap_or(id),
source=c.identity(selected.resource, selected.node),
kind=if actual(view) == Some(Collaboration) {
Collaboration
} else {
Process
},
plane?=selected.plane.map(i => c.identity(0, i)),
)
result(Some(Bpmn(bpmn)))
}
///|
/// Read, select, convert and render without performing filesystem/network I/O.
pub fn render_svg(
source : String,
selector~ : ViewSelector = Automatic,
resources~ : @bpmn.ResourceSet = @bpmn.ResourceSet::new(),
geometry_policy~ : @model.GeometryPolicy = PreferDi,
connectors~ : @svg.ConnectorOptions = @svg.ConnectorOptions::new(),
) -> @render.Rendered {
let read = @bpmn.read(source)
guard read.document is Some(document) else {
return @render.Rendered::failed(
"image/svg+xml",
read.diagnostics.map(map_diagnostic),
)
}
let converted = to_diagram(document, selector~, resources~, geometry_policy~)
// A successful selected view has already checked its semantic fields. Lowering
// findings in other views or explicitly ignored DI remain visible as warnings.
let diagnostics = read.diagnostics
.map(d => {
let mapped = map_diagnostic(d)
if converted.diagram != None {
{ ..mapped, severity: Warning, }
} else {
mapped
}
})
.iter()
.to_array()
diagnostics.append(converted.diagnostics)
guard converted.diagram is Some(diagram) else {
return @render.Rendered::failed("image/svg+xml", diagnostics)
}
let rendered = @svg.SvgRenderer::new(bpmn_policy=geometry_policy, connectors~).render(
diagram,
)
diagnostics.append(rendered.diagnostics)
match rendered.output {
Some(output) =>
@render.Rendered::ok(output, rendered.media_type, diagnostics~)
None => @render.Rendered::failed(rendered.media_type, diagnostics)
}
}