///|
fn supplied_resolver(index : DocumentIndex, resources : ResourceSet, log : ValidationLog) -> ReferenceResolver {
let documents : Array[AvailableDocument] = [{resource_id:log.resource_id,namespace_uri:target_namespace(index.document),index}]
let external : Array[ExternalResource] = []
let mut complete=index.complete
let mut records=index.records
let original_id=log.resource_id
let seen_documents : Map[Int,Bool] = Map([])
let seen_external : Map[Int,Bool] = Map([])
let locations : Map[String,Array[(Bool,Int)]] = Map([])
if original_id is Some(key) { locations[key]=[(true,-1)] }
if index.records > log.limits.nodes {
log.add(References,"bpmn/resource-limit","The root index exceeds the reference-stage node budget",None,None)
complete=false
}
if resources.documents.length() > log.limits.nodes || resources.external.length() > log.limits.nodes-resources.documents.length() {
log.add(References,"bpmn/resource-limit","Explicit resource count exceeds the node budget",None,None)
complete=false
} else if resources.documents.iter().any(r => r.location.length() > log.limits.token_units) || resources.external.iter().any(r => r.location.length() > log.limits.token_units || r.namespace_uri.length() > log.limits.token_units || r.import_type.length() > log.limits.token_units) {
log.add(References,"bpmn/validation-token-limit","Explicit resource metadata exceeds the token limit",None,None)
complete=false
} else {
for i in 0.. xs.push((true,i)); None => locations[key]=[(true,i)] } }
for i in 0.. xs.push((false,i)); None => locations[key]=[(false,i)] } }
}
let mut next=0
while complete && next < documents.length() && !log.stopped {
let current=documents[next]; next+=1; log.resource_id=current.resource_id
for node in current.index.nodes {
if log.stopped || !complete { break }
if node.payload || node.parent != Some(0) || current.index.document.root.kind != Definitions || node.element.kind != Import || !typed_element_is(node.element,TypeId::Import) { continue }
guard node.element.attribute("location") is Some(location) && node.element.attribute("namespace") is Some(namespace_uri) && node.element.attribute("importType") is Some(import_type) else { continue }
guard locations.get(location) is Some(matches) else {
log.add(References,"bpmn/unresolved-import","No supplied resource for import location: "+location,Some(node),Some(ExpandedName::new("","location"))); continue
}
if matches.length() != 1 { log.add(References,"bpmn/ambiguous-import","Several supplied resources have import location: "+location,Some(node),Some(ExpandedName::new("","location"))); continue }
let (is_document,i)=matches[0]
let actual_namespace=if i == -1 { target_namespace(index.document) } else if is_document { target_namespace(resources.documents[i].document) } else { resources.external[i].namespace_uri }
if actual_namespace != namespace_uri { log.add(References,"bpmn/import-namespace","Import namespace differs from the supplied resource",Some(node),Some(ExpandedName::new("","namespace"))); continue }
let actual_type=if is_document { model_namespace } else { resources.external[i].import_type }
if import_type != actual_type { log.add(References,"bpmn/import-type","Import type differs from the supplied resource",Some(node),Some(ExpandedName::new("","importType"))); continue }
if i == -1 { continue }
if is_document {
if seen_documents.contains(i) { continue }; seen_documents[i]=true
let resource=resources.documents[i]
if records >= log.limits.nodes { log.add(References,"bpmn/resource-limit","Imported document budget exceeded",Some(node),None); complete=false; break }
let result=validate_structure(resource.document,resource_id=resource.location,limits={..log.limits,nodes:log.limits.nodes-records})
records+=result.index.records
for item in result.findings {
if log.findings.length() < log.limits.diagnostics { log.findings.push(item) }
else { log.add(References,"bpmn/resource-limit","Imported finding budget exceeded",Some(node),None); break }
}
if !result.complete { complete=false }
documents.push({resource_id:Some(resource.location),namespace_uri:actual_namespace,index:result.index})
} else {
if seen_external.contains(i) { continue }; seen_external[i]=true
let resource=resources.external[i]
if resource.symbols.length() >= log.limits.nodes-records { log.add(References,"bpmn/resource-limit","External symbol budget exceeded",Some(node),None); complete=false; break }
records+=1+resource.symbols.length()
if resource.symbols.iter().any(s => s.length() > log.limits.token_units) { log.add(References,"bpmn/validation-token-limit","External symbol exceeds the token limit",Some(node),None); complete=false; break }
if resource.symbols.iter().any(s => !ncname(s)) { log.add(References,"bpmn/invalid-reference","External symbols must be NCNames",Some(node),None); continue }
external.push(resource)
}
}
}
log.resource_id=original_id
make_resolver(documents,external,complete && !log.stopped,log.limits.nodes,log.limits.token_units)
}
///|
fn reference_rule(element : Element, field : SchemaField) -> ReferenceRule? {
reference_rules.iter().find_first(r => r.owner == field.owner && r.kind == field.kind && Some(r.field) == field.name && typed_element_is(element,field.owner))
}
///|
fn ValidationLog::resolved(self : ValidationLog, source : IndexedElement, field : ExpandedName, value : Reference, target : ReferenceTarget, resolution : ReferenceResolution, attribute : Attribute?) -> Unit {
let suffix=" on "+source.element.attribute("id").unwrap_or(source.element.kind.local_name())+": "+value.spelling
match resolution {
Found(found) => {
if !reference_target_matches(target,found.node.element) { self.add(References,"bpmn/wrong-reference-kind","Reference target has an incorrect kind"+suffix,Some(source),Some(field),attribute?) }
if typed_element_is(source.element,TypeId::SequenceFlow) && (found.resource_id != self.resource_id || found.node.scope != source.scope) {
self.add(References,"bpmn/sequence-flow-scope","Sequence flow crosses a containment scope"+suffix,Some(source),Some(field),attribute?)
}
if typed_element_is(source.element,TypeId::BoundaryEvent) && field.local_name == "attachedToRef" && (found.resource_id != self.resource_id || found.node.scope != source.scope || !typed_element_is(found.node.element,TypeId::Activity)) {
self.add(References,"bpmn/boundary-attachment","Boundary event must attach to an activity in its flow scope"+suffix,Some(source),Some(field),attribute?)
}
if field.local_name == "default" && found.resource_id == self.resource_id && typed_element_is(found.node.element,TypeId::SequenceFlow) {
let flow_source=found.node.element.reference("sourceRef").value()
if flow_source.map(r => r.name.map(n => n.local_name)) != Some(source.element.attribute("id")) { self.add(References,"bpmn/reference-endpoint","Default flow does not originate at its owner"+suffix,Some(source),Some(field),attribute?) }
}
if typed_element_is(source.element,TypeId::Lane) && field.local_name == "flowNodeRef" && (found.resource_id != self.resource_id || found.node.scope != source.scope) {
self.add(References,"bpmn/reference-endpoint","Lane references a flow node outside its flow scope"+suffix,Some(source),Some(field),attribute?)
}
if typed_element_is(source.element,TypeId::FlowNode) && ["incoming","outgoing"].contains(field.local_name) {
let endpoint=if field.local_name == "incoming" { "targetRef" } else { "sourceRef" }
if found.resource_id != self.resource_id || found.node.element.reference(endpoint).value().map(r => r.name.map(n => n.local_name)) != Some(source.element.attribute("id")) { self.add(References,"bpmn/reference-endpoint","Flow reference does not name the corresponding endpoint"+suffix,Some(source),Some(field),attribute?) }
}
}
External(_) => if target != External && target != AnyElement { self.add(References,"bpmn/wrong-reference-kind","BPMN reference targets an external-language symbol"+suffix,Some(source),Some(field),attribute?) }
Ambiguous(_,_) => self.add(References,"bpmn/ambiguous-reference","Reference target is ambiguous"+suffix,Some(source),Some(field),attribute?)
Unresolved => self.add(References,"bpmn/unresolved-reference","Reference target was not found in the explicit import closure"+suffix,Some(source),Some(field),attribute?)
Invalid => self.add(References,"bpmn/invalid-reference","Reference has no valid expanded identity"+suffix,Some(source),Some(field),attribute?)
Limit => self.add(References,"bpmn/resource-limit","Reference lookup has an incomplete resource index"+suffix,Some(source),Some(field),attribute?)
}
}
///|
/// Resolve declared BPMN references over one document and its explicit imports.
/// Missing imports are findings; they do not invalidate unrelated local lookups.
pub fn resolve_references(index : DocumentIndex, resources? : ResourceSet = ResourceSet::new(), resource_id? : String, limits? : Limits = Limits::new()) -> ReferenceResult {
let log=ValidationLog::{limits:if valid_limits(limits) { limits } else { Limits::new() },findings:[],resource_id,stopped:false}
if !valid_limits(limits) { log.add(References,"bpmn/validation-invalid-limits","All validation limits must be positive",None,None) }
let resolver=if valid_limits(limits) { supplied_resolver(index,resources,log) } else { make_resolver([],[],false,0,0) }
let references : Array[ResolvedReference] = []
let mut remaining_work=log.limits.nodes
let mut complete=resolver.complete
for source in index.nodes {
if log.stopped || !complete { break }
if source.payload || source.element.value_type == None { continue }
guard schema_type(source.element.value_type.unwrap()) is Some(schema) else { continue }
for field in schema.fields() {
if log.stopped || !complete { break }
guard reference_rule(source.element,field) is Some(rule) else { continue }
let occurrences : Array[(Reference,Attribute?,IndexedElement)] = []
if field.kind == Attribute {
for attribute in source.element.values { if attribute.name == field.name && attribute.value is Reference(value) { occurrences.push((value,Some(attribute),source)) } }
} else {
for child_index in source.children {
let child=index.nodes[child_index]
if child.payload || Some(child.element.kind.name()) != field.name { continue }
if child.element.text_value is Some(Reference(value)) { occurrences.push((value,None,child)) }
}
}
for (value,attribute,location_node) in occurrences {
if log.stopped || !complete { break }
let (resolution,work)=resolver.lookup_bounded(value,remaining_work)
remaining_work-=work
if resolution is Limit { complete=false }
references.push({source,field:rule.field,value,target:rule.target,resolution})
// Child text has its own source span but its owner supplies identity.
let before=log.findings.length()
log.resolved(source,rule.field,value,rule.target,resolution,attribute)
if attribute == None && log.findings.length() > before {
for i in before..