///|
pub(all) enum ViewSelector { Automatic; Process(String); Collaboration(String); Plane(String); View(String) } derive(Eq, Debug)
///|
pub extend ViewSelector with Eq::{equal,not_equal}
///|
pub extend ViewSelector with @debug.Debug::{to_repr}
///|
priv struct Source { id : String?; index : @bpmn.DocumentIndex; references : @bpmn.ReferenceResult; findings : ReadOnlyArray[@bpmn.ValidationFinding]; fields : Map[(Int,String),Array[@bpmn.ResolvedReference]] }
///|
priv struct Selected { resource : Int; node : Int; plane : Int? }
///|
priv struct Converter {
ignore_di : Bool; sources : Array[Source]; dependencies : Map[(Int,Int),Bool]; resources : @bpmn.ResourceSet; limits : @bpmn.Limits
diagnostics : Array[@diagrams.Diagnostic]; mut failed : Bool; mut work : Int
}
///|
fn actual(e : @bpmn.Element) -> @bpmn.TypeId? {
if e.kind is Foreign(_) || e.kind is Unknown(_) { return None }
e.value_type.bind(@bpmn.schema_type).map(t => t.id)
}
///|
fn is_type(e : @bpmn.Element, t : @bpmn.TypeId) -> Bool {
if actual(e) == None { return false }
e.value_type.bind(@bpmn.schema_type).map(v => v.is_subtype_of(t.name())).unwrap_or(false)
}
///|
fn mapped_location(location : @bpmn.Location?) -> @diagrams.Span? {
location.map(l => @diagrams.Span::{start:{offset:l.start.offset,line:l.start.line,column:l.start.column},end:{offset:l.end.offset,line:l.end.line,column:l.end.column}})
}
///|
/// Public mapping retains code, severity and half-open UTF-16 source positions.
pub fn map_diagnostic(d : @bpmn.Diagnostic) -> @diagrams.Diagnostic {
{code:d.code,severity:if d.severity == Error { Error } else { Warning },message:d.message,span:mapped_location(d.location)}
}
///|
fn Converter::add(self : Converter, code : String, message : String, e? : @bpmn.Element, warning? : Bool = false) -> Unit {
if self.diagnostics.length() >= self.limits.diagnostics {
self.diagnostics[self.limits.diagnostics-1]=@diagrams.Diagnostic::error("bpmn/conversion-limit","Conversion diagnostic limit exceeded")
self.failed=true; return
}
self.diagnostics.push({code,severity:if warning { Warning } else { Error },message,span:mapped_location(e.bind(e => e.location()))})
if !warning { self.failed=true }
}
///|
fn Converter::charge(self : Converter, count : Int) -> Bool {
if count < 0 || count > self.limits.nodes-self.work { self.add("bpmn/conversion-limit","View conversion exceeded its node/work budget"); return false }
self.work+=count; true
}
///|
fn reference_fields(references : @bpmn.ReferenceResult) -> Map[(Int,String),Array[@bpmn.ResolvedReference]] {
let fields : Map[(Int,String),Array[@bpmn.ResolvedReference]]=Map([])
for reference in references.references {
let key=(reference.source.ordinal,reference.field.local_name)
match fields.get(key) { Some(values) => values.push(reference); None => fields[key]=[reference] }
}
fields
}
///|
fn make_source(document : @bpmn.Document, id : String?, resources : @bpmn.ResourceSet, limits : @bpmn.Limits) -> Source {
let structure=@bpmn.validate_structure(document,resource_id?=id,limits~)
let references=@bpmn.resolve_references(structure.index,resources~,resource_id?=id,limits~)
{id,index:structure.index,references,fields:reference_fields(references),findings:ReadOnlyArray::from_array(structure.findings.iter().concat(references.findings.iter()).to_array())}
}
///|
fn Converter::source_for(self : Converter, resolved : @bpmn.ResolvedElement) -> (Int,Int)? {
if self.sources[0].id == resolved.resource_id { return Some((0,resolved.node.ordinal)) }
for i,s in self.sources { if s.id == resolved.resource_id { return Some((i,resolved.node.ordinal)) } }
guard resolved.resource_id is Some(id) else { self.add("bpmn/invalid-reference","Resolved resource has no caller identity"); return None }
let available=self.sources[0].references.indexed_resources()
let candidates=available.filter(d => d.resource_id == Some(id))
guard candidates.length() == 1 else { self.add("bpmn/invalid-reference","Imported view resource is missing or ambiguous"); return None }
// Each imported process resolves against its own closure. Charge an upper
// estimate before rebuilding it, reusing the already validated source index.
if !self.charge(available.length()+self.resources.documents.length()+self.resources.external.length()) { return None }
for resource in available { if !self.charge(resource.records) { return None } }
let index=candidates[0].index
let references=@bpmn.resolve_references(index,resources=self.resources,resource_id=id,limits=self.limits)
if !references.complete { self.add("bpmn/conversion-limit","Imported reference processing did not complete"); return None }
let structure_findings=self.sources[0].references.findings.filter(f => f.stage == Structure && f.resource_id == Some(id))
let source=Source::{id:Some(id),index,references,fields:reference_fields(references),findings:ReadOnlyArray::from_array(structure_findings.iter().concat(references.findings.iter()).to_array())}
self.sources.push(source); Some((self.sources.length()-1,resolved.node.ordinal))
}
///|
fn Converter::resolve_field(self : Converter, resource : Int, ordinal : Int, field : String) -> Array[(Int,Int)] {
let source=self.sources[resource]
let out=[]
let references=source.fields.get((ordinal,field)).unwrap_or([])
if !self.charge(references.length()+1) { return out }
for reference in references {
match reference.resolution {
Found(target) => if self.source_for(target) is Some(key) {
out.push(key)
if ["calledElement","dataObjectRef","dataStoreRef","categoryValueRef","eventDefinitionRef","labelStyle"].contains(field) {
let known=self.dependencies.contains(key)
self.dependencies[key]=true
if field != "calledElement" && !known {
let index=self.sources[key.0].index
if self.charge(index.nodes.length()) {
let members=Map([(key.1,true)])
for n in index.nodes { if n.parent.map(p => members.contains(p)).unwrap_or(false) { members[n.ordinal]=true; self.dependencies[(key.0,n.ordinal)]=true } }
}
}
}
}
_ => self.add("bpmn/invalid-reference","Selected \{field} reference is unresolved or ambiguous",e=source.index.nodes[ordinal].element)
}
}
out
}
///|
fn Converter::identity(self : Converter, resource : Int, ordinal : Int) -> @model.SourceIdentity {
let source=self.sources[resource]
let node=source.index.nodes[ordinal]
let reversed=[]
let mut current=Some(ordinal)
while current is Some(i) { if !self.charge(1) { break }; let n=source.index.nodes[i]; reversed.push(n.child_index); current=n.parent }
let path=[]
for i=reversed.length()-1; i>=0; i=i-1 { path.push(reversed[i]) }
@model.SourceIdentity::new(source_id?=node.element.attribute("id"),resource_id?=source.id,path~)
}
///|
fn Converter::render_id(self : Converter, resource : Int, ordinal : Int, instance : String) -> String {
let identity=self.identity(resource,ordinal)
let suffix=identity.source_id.unwrap_or_else(() => "path-"+identity.path.iter().map(i => i.to_string()).join("-"))
if resource == 0 && instance == "" && identity.source_id != None { return suffix }
// Length-prefixed segments cannot collide with authored IDs or each other.
if instance.length() > self.limits.token_units-64 || suffix.length() > self.limits.token_units-64-instance.length() { self.add("bpmn/conversion-limit","Generated identity exceeded the token budget"); return "" }
"@bpmn:\{resource}:\{instance.length()}:\{instance}:\{suffix.length()}:\{suffix}"
}
///|
fn Converter::plane_target(self : Converter, plane : Int) -> (Int,Int)? {
let targets=self.resolve_field(0,plane,"bpmnElement")
if targets.length() == 1 {
let key=targets[0]; let e=self.sources[key.0].index.nodes[key.1].element
if actual(e) == Some(Process) || is_type(e,SubProcess) || is_type(e,Collaboration) { Some(key) }
else { self.add("bpmn/invalid-reference","Selected plane does not reference a supported view container",e~); None }
} else { self.add("bpmn/invalid-reference","Selected plane must identify exactly one view",e=self.sources[0].index.nodes[plane].element); None }
}
///|
fn Converter::select(self : Converter, selector : ViewSelector) -> Selected? {
let index=self.sources[0].index
let planes=index.nodes.filter(n => actual(n.element) == Some(BpmnBPMNPlane))
let roots=index.nodes.filter(n => n.parent == Some(0))
let candidates : Array[Int]=[]
match selector {
Automatic => {
if !planes.is_empty() { for n in planes { candidates.push(n.ordinal) } }
else {
for n in roots { if is_type(n.element,Collaboration) { candidates.push(n.ordinal) } }
if candidates.is_empty() { for n in roots { if actual(n.element) == Some(Process) { candidates.push(n.ordinal) } } }
}
}
Plane(id) => for n in planes {
if n.element.attribute("id") == Some(id) || n.parent.bind(p => index.nodes[p].element.attribute("id")) == Some(id) { candidates.push(n.ordinal) }
}
Process(id) | Collaboration(id) | View(id) => {
for n in index.nodes { if n.element.attribute("id") == Some(id) && !n.payload { candidates.push(n.ordinal) } }
}
}
if candidates.is_empty() {
match selector { Plane(id) => if index.nodes.iter().any(n => actual(n.element) == Some(BpmnBPMNDiagram) && n.element.attribute("id") == Some(id)) { self.add("bpmn/missing-plane","Selected diagram contains no plane"); return None }; _ => () }
self.add("bpmn/view-not-found","No view matches the selector"); return None
}
if candidates.length() != 1 {
let identities=candidates.map(i => index.nodes[i].element.attribute("id").unwrap_or("path:\{i}")).join(", ")
self.add("bpmn/ambiguous-view","Select one view from: \{identities}"); return None
}
let chosen=candidates[0]
let e=index.nodes[chosen].element
if actual(e) == Some(BpmnBPMNDiagram) {
let children=index.nodes[chosen].children.filter(i => actual(index.nodes[i].element) == Some(BpmnBPMNPlane))
if children.length() != 1 { self.add("bpmn/missing-plane","Selected diagram must contain one plane",e~); return None }
return self.plane_target(children[0]).map(key => Selected::{resource:key.0,node:key.1,plane:Some(children[0])})
}
if actual(e) == Some(BpmnBPMNPlane) { return self.plane_target(chosen).map(key => Selected::{resource:key.0,node:key.1,plane:Some(chosen)}) }
match selector {
Process(_) if actual(e) != Some(Process) => { self.add("bpmn/unsupported-view","Selected identity is not a process",e~); return None }
Collaboration(_) if actual(e) != Some(Collaboration) => { self.add("bpmn/unsupported-view","Selected identity is not a supported collaboration",e~); return None }
_ => ()
}
if self.ignore_di { return Some({resource:0,node:chosen,plane:None}) }
let matching=[]
for p in planes {
let references=self.sources[0].references.references.filter(r => r.source.ordinal == p.ordinal && r.field.local_name == "bpmnElement")
if references.length() == 1 && references[0].resolution is Found(target) && target.resource_id == self.sources[0].id && target.node.ordinal == chosen { matching.push(p.ordinal) }
}
if matching.length() > 1 { self.add("bpmn/ambiguous-plane","Multiple planes depict the selected view; select a plane explicitly",e~); return None }
Some({resource:0,node:chosen,plane:matching.get(0)})
}