//! Bounded data dictionary traversal and validation, shared by every editor. use crate::validation::{Issue, error, valid_text, warning}; use crate::*; use std::collections::{BTreeMap, BTreeSet}; /// A contextual field identity is `(path, field)`; item identifies its origin. #[derive(Debug, Clone, PartialEq, Eq)] pub struct CarriedFieldPath { pub item: Id, pub path: Vec, pub field: Id, pub direction: ParameterDirection, } #[derive(Debug, Clone, PartialEq, Eq)] pub enum LegacyFieldPathResolution { Missing, Unique(Vec), Ambiguous, } pub fn supertype(e: &Element) -> Option { match e.shared()? { SharedBody::ExchangeItem { supertype, .. } | SharedBody::Interface { supertype } | SharedBody::DataType { definition: DataTypeDefinition::Record { supertype }, .. } => *supertype, _ => None, } } pub fn is_record(p: &Project, id: Id) -> bool { matches!( p.elements.get(&id).and_then(Element::shared), Some(SharedBody::DataType { definition: DataTypeDefinition::Record { .. }, .. }) ) } fn ancestry(p: &Project, id: Id) -> Vec { let mut current = Some(id); let mut seen = BTreeSet::new(); let mut result = Vec::new(); while let Some(id) = current { if !seen.insert(id) { break; } result.push(id); current = p.elements.get(&id).and_then(supertype); } result } /// Base fields precede local fields. Original field IDs and owners are retained. /// Scan the model once, so a deep empty inheritance chain does not rescan every /// element for every ancestor during validation. pub fn effective_fields(p: &Project, owner: Id) -> Vec<&Element> { let ranks: BTreeMap<_, _> = ancestry(p, owner) .into_iter() .rev() .enumerate() .map(|(rank, id)| (id, rank)) .collect(); let mut fields: Vec<_> = p .elements .values() .filter(|e| { e.kind() == Kind::DataField && e.parent.is_some_and(|id| ranks.contains_key(&id)) }) .collect(); fields.sort_by_key(|e| (ranks[&e.parent.expect("field owner was filtered")], e.id)); fields } pub fn effective_interface_allocations(p: &Project, interface: Id) -> Vec<&Relation> { let ranks: BTreeMap<_, _> = ancestry(p, interface) .into_iter() .rev() .enumerate() .map(|(rank, id)| (id, rank)) .collect(); let mut allocations: Vec<_> = p .relations .values() .filter(|r| { r.kind() == RelationKind::InterfaceItemAllocation && ranks.contains_key(&r.source()) }) .collect(); allocations.sort_by_key(|r| { let RelationBody::InterfaceItemAllocation { order, .. } = &r.body else { unreachable!() }; (ranks[&r.source()], *order, &r.alias, r.id) }); allocations } pub fn interface_items(p: &Project, interface: Id) -> Vec { effective_interface_allocations(p, interface) .into_iter() .map(Relation::target) .collect() } /// An association role and multiplicity belong to the named endpoint. pub fn association_end_label(relation: &Relation, source_end: bool) -> Option { let RelationBody::DataAssociation { source_role, target_role, source_minimum, source_maximum, target_minimum, target_maximum, .. } = &relation.body else { return None; }; let (role, minimum, maximum) = if source_end { (source_role, source_minimum, source_maximum) } else { (target_role, target_minimum, target_maximum) }; Some(format!( "{} [{}..{}]", role, minimum, maximum.map_or_else(|| "*".into(), |n| n.to_string()) )) } pub fn item_satisfies(p: &Project, carried: Id, declared: Id) -> bool { p.elements .get(&carried) .is_some_and(|e| e.kind() == Kind::ExchangeItem) && p.elements .get(&declared) .is_some_and(|e| e.kind() == Kind::ExchangeItem) && ancestry(p, carried).contains(&declared) } pub fn interface_satisfies(p: &Project, provided: Id, required: Id) -> bool { p.elements .get(&provided) .is_some_and(|e| e.kind() == Kind::Interface) && p.elements .get(&required) .is_some_and(|e| e.kind() == Kind::Interface) && ancestry(p, provided).contains(&required) } pub fn item_mechanism(p: &Project, item: Id) -> Option { match p.elements.get(&item)?.shared()? { SharedBody::ExchangeItem { mechanism, .. } => Some(*mechanism), _ => None, } } const MAX_FIELD_PATHS: usize = 16_384; pub const MAX_VALUE_PATH_DEPTH: usize = 128; /// Enumerate finite representative paths, stopping recursive record descent. /// Work exhaustion is separate from a valid recursive type and is diagnosed. pub fn try_carried_field_paths( p: &Project, relation: &Relation, ) -> Result, String> { let mut result = Vec::new(); let mut identities = BTreeSet::new(); let mut pending: Vec<_> = relation .body .items() .iter() .rev() .map(|id| { ( *id, *id, Vec::::new(), ParameterDirection::Unspecified, BTreeSet::::new(), ) }) .collect(); let mut work = 0usize; while let Some((item, owner, path, inherited_direction, mut ancestors)) = pending.pop() { if !ancestors.insert(owner) { continue; } if path.len() > MAX_VALUE_PATH_DEPTH { return Err(format!( "Carried field paths exceed the {MAX_VALUE_PATH_DEPTH}-hop authoring limit." )); } for field in effective_fields(p, owner) { work += 1; if work > MAX_FIELD_PATHS { return Err(format!( "Carried field expansion exceeds the {MAX_FIELD_PATHS}-field authoring limit." )); } let Some(SharedBody::DataField { direction, storage, type_ref, .. }) = field.shared() else { continue; }; let direction = if path.is_empty() { *direction } else { inherited_direction }; if identities.insert((path.clone(), field.id)) { result.push(CarriedFieldPath { item, path: path.clone(), field: field.id, direction, }); } if *storage == FieldStorage::Value && let TypeRef::Named(id) = type_ref && is_record(p, *id) { let mut nested = path.clone(); nested.push(field.id); pending.push((item, *id, nested, direction, ancestors.clone())); } } } Ok(result) } /// UI convenience; validation always uses the checked traversal above. pub fn carried_field_paths(p: &Project, relation: &Relation) -> Vec { try_carried_field_paths(p, relation).unwrap_or_default() } /// Unlike the finite picker, legacy inference accounts for paths that revisit /// a recursive Value record. A reachable cycle before this field is ambiguous. pub fn resolve_legacy_field_path( p: &Project, relation: &Relation, field: Id, ) -> Result { let paths: Vec<_> = try_carried_field_paths(p, relation)? .into_iter() .filter(|value| value.field == field) .collect(); if paths.is_empty() { return Ok(LegacyFieldPathResolution::Missing); } if paths.len() > 1 { return Ok(LegacyFieldPathResolution::Ambiguous); } let mut graph: BTreeMap> = BTreeMap::new(); let mut reverse: BTreeMap> = BTreeMap::new(); let mut targets = Vec::new(); let mut pending = relation.body.items().to_vec(); while let Some(owner) = pending.pop() { if graph.contains_key(&owner) { continue; } let mut edges = Vec::new(); for e in effective_fields(p, owner) { if e.id == field { targets.push(owner); } if let Some(SharedBody::DataField { storage: FieldStorage::Value, type_ref: TypeRef::Named(target), .. }) = e.shared() && is_record(p, *target) { edges.push(*target); reverse.entry(*target).or_default().push(owner); pending.push(*target); } } graph.insert(owner, edges); } let mut relevant = BTreeSet::new(); while let Some(owner) = targets.pop() { if relevant.insert(owner) { targets.extend(reverse.get(&owner).into_iter().flatten()); } } let mut remaining: BTreeMap<_, _> = graph .iter() .filter(|(owner, _)| relevant.contains(owner)) .map(|(owner, edges)| { ( *owner, edges .iter() .filter(|target| relevant.contains(target)) .count(), ) }) .collect(); let mut pending: Vec<_> = remaining .iter() .filter_map(|(owner, count)| (*count == 0).then_some(*owner)) .collect(); while let Some(owner) = pending.pop() { remaining.remove(&owner); for source in reverse.get(&owner).into_iter().flatten() { if let Some(count) = remaining.get_mut(source) { *count -= 1; if *count == 0 { pending.push(*source); } } } } Ok(if remaining.is_empty() { LegacyFieldPathResolution::Unique(paths[0].path.clone()) } else { LegacyFieldPathResolution::Ambiguous }) } /// Resolve an explicit finite path, including a finite path into a recursive type. pub fn carried_value_path( p: &Project, relation: &Relation, path: &[Id], field: Id, ) -> Option { if path.len() > MAX_VALUE_PATH_DEPTH { return None; } for item in relation.body.items() { let mut owner = *item; let mut direction = ParameterDirection::Unspecified; let mut valid = true; for (index, hop) in path.iter().chain(std::iter::once(&field)).enumerate() { let Some(e) = effective_fields(p, owner) .into_iter() .find(|e| e.id == *hop) else { valid = false; break; }; let Some(SharedBody::DataField { direction: parameter_direction, storage, type_ref, .. }) = e.shared() else { valid = false; break; }; if index == 0 { direction = *parameter_direction; } if index < path.len() { match (storage, type_ref) { (FieldStorage::Value, TypeRef::Named(id)) if is_record(p, *id) => owner = *id, _ => { valid = false; break; } } } } if valid { return Some(CarriedFieldPath { item: *item, path: path.to_vec(), field, direction, }); } } None } /// Existing annotations whose inherited field disappears follow the ordinary /// deletion pruning policy. Explicitly edited/new annotations still validate. pub(crate) fn prune_removed_inheritance( before: &Project, after: &mut Project, origins: &BTreeMap, ) { let current: BTreeMap<_, _> = origins.iter().map(|(now, old)| (*old, *now)).collect(); let removed: BTreeSet<_> = before .elements .values() .filter_map(|e| { let base = supertype(e)?; let now = after.elements.get(current.get(&e.id)?)?; (supertype(now) != current.get(&base).copied()).then_some(e.id) }) .collect(); if removed.is_empty() { return; } let mut pruned = BTreeMap::new(); for e in after.elements.values() { let Some(SharedBody::Occurrence { body: OccurrenceBody::Exchange { exchange: Some(exchange), values, .. }, .. }) = e.shared() else { continue; }; let Some(old) = origins.get(&e.id).and_then(|id| before.elements.get(id)) else { continue; }; let Some(SharedBody::Occurrence { body: OccurrenceBody::Exchange { exchange: Some(old_exchange), values: old_values, .. }, .. }) = old.shared() else { continue; }; if origins.get(exchange) != Some(old_exchange) { continue; } let (Some(relation), Some(old_relation)) = ( after.relations.get(exchange), before.relations.get(old_exchange), ) else { continue; }; let mut keep = Vec::new(); for value in values { if carried_value_path(after, relation, &value.path, value.field).is_some() || (value.path.is_empty() && matches!( resolve_legacy_field_path(after, relation, value.field), Ok(LegacyFieldPathResolution::Unique(_) | LegacyFieldPathResolution::Ambiguous) )) { keep.push(value.clone()); continue; } let old_value = MessageValue { path: value .path .iter() .map(|id| origins.get(id).copied().unwrap_or(*id)) .collect(), field: origins.get(&value.field).copied().unwrap_or(value.field), expression: value.expression.clone(), }; let prior_paths = if old_value.path.is_empty() && carried_value_path(before, old_relation, &old_value.path, old_value.field) .is_none() { carried_field_paths(before, old_relation) .into_iter() .filter(|path| path.field == old_value.field) .map(|path| MessageValue { path: path.path, ..old_value.clone() }) .collect::>() } else { vec![old_value.clone()] }; let lost = old_values.contains(&old_value) && prior_paths.iter().any(|old_value| { old_relation.body.items().iter().any(|item| { if carried_value_path( before, old_relation, &old_value.path, old_value.field, ) .is_none() { return false; } let mut owner = *item; for hop in old_value .path .iter() .chain(std::iter::once(&old_value.field)) { let Some(field) = effective_fields(before, owner) .into_iter() .find(|e| e.id == *hop) else { return false; }; let mut inherited_owner = Some(owner); let mut seen = BTreeSet::new(); while let Some(id) = inherited_owner { if Some(id) == field.parent || !seen.insert(id) { break; } if removed.contains(&id) { return true; } inherited_owner = before.elements.get(&id).and_then(supertype); } if let Some(SharedBody::DataField { type_ref: TypeRef::Named(id), .. }) = field.shared() { owner = *id; } } false }) }); if !lost { keep.push(value.clone()); } } if keep != *values { pruned.insert(e.id, keep); } } for (id, keep) in pruned { if let Some(SharedBody::Occurrence { body: OccurrenceBody::Exchange { values, .. }, .. }) = after.elements.get_mut(&id).and_then(Element::shared_mut) { *values = keep; } } } pub fn validate_data_value( p: &Project, type_ref: &TypeRef, value: &DataValue, ) -> Result<(), String> { let primitive = match type_ref { TypeRef::Boolean => DataTypeDefinition::Boolean, TypeRef::Integer => DataTypeDefinition::Integer { minimum: None, maximum: None, }, TypeRef::Real => DataTypeDefinition::Real { minimum: None, maximum: None, }, TypeRef::String => DataTypeDefinition::String { minimum_length: 0, maximum_length: None, }, TypeRef::Named(id) => { return match p.elements.get(id).and_then(Element::shared) { Some(SharedBody::DataType { definition, .. }) => validate_scalar(definition, value), _ => Err("The named scalar type does not exist.".into()), }; } }; validate_scalar(&primitive, value) } fn validate_scalar(definition: &DataTypeDefinition, value: &DataValue) -> Result<(), String> { let valid = match (definition, value) { (DataTypeDefinition::Boolean, DataValue::Boolean(_)) => true, (DataTypeDefinition::Integer { minimum, maximum }, DataValue::Integer(n)) => { minimum.is_none_or(|min| *n >= min) && maximum.is_none_or(|max| *n <= max) } (DataTypeDefinition::Real { minimum, maximum }, DataValue::Real(n)) => { n.is_finite() && minimum.is_none_or(|min| *n >= min) && maximum.is_none_or(|max| *n <= max) } ( DataTypeDefinition::String { minimum_length, maximum_length, }, DataValue::String(s), ) => { valid_text(s) && s.chars().count() >= *minimum_length as usize && maximum_length.is_none_or(|max| s.chars().count() <= max as usize) } (DataTypeDefinition::Enumeration { literals }, DataValue::Enumeration(s)) => { literals.contains(s) } (DataTypeDefinition::Record { .. }, _) => { return Err("Record/object literals are not supported as scalar defaults.".into()); } _ => return Err( "The default must have exactly the declared scalar type; values are not coerced." .into(), ), }; if valid { Ok(()) } else { Err( "Default is nonfinite, outside the declared bounds, or not a declared enum literal." .into(), ) } } pub fn effective_field_unit<'a>(p: &'a Project, field: &'a Element) -> Option<&'a str> { let SharedBody::DataField { type_ref, unit, .. } = field.shared()? else { return None; }; if !unit.is_empty() { return Some(unit); } match type_ref { TypeRef::Named(id) => match p.elements.get(id)?.shared()? { SharedBody::DataType { unit, .. } => Some(unit), _ => None, }, _ => Some(unit), } } pub fn effective_element_default<'a>(p: &'a Project, field: &'a Element) -> Option<&'a DataValue> { let SharedBody::DataField { type_ref: TypeRef::Named(id), default: None, .. } = field.shared()? else { return None; }; match p.elements.get(id)?.shared()? { SharedBody::DataType { default, .. } => default.as_ref(), _ => None, } } fn check_text(out: &mut Vec, id: Id, field: &str, value: &str) { if !valid_text(value) { error( out, id, "invalid_text", field, "Text contains a forbidden XML character.", ); } } fn numeric_type(p: &Project, reference: &TypeRef) -> bool { match reference { TypeRef::Integer | TypeRef::Real => true, TypeRef::Named(id) => matches!( p.elements.get(id).and_then(Element::shared), Some(SharedBody::DataType { definition: DataTypeDefinition::Integer { .. } | DataTypeDefinition::Real { .. }, .. }) ), _ => false, } } pub(crate) fn validate_element(p: &Project, e: &Element, out: &mut Vec) { if let Some(base) = supertype(e) { let compatible = p.elements.get(&base).is_some_and(|b| { b.kind() == e.kind() && b.layer() == e.layer() && (e.kind() != Kind::DataType || is_record(p, base)) }); if !compatible { error( out, e.id, "data_supertype", "supertype", "Choose a same-family supertype in this layer.", ); } if ancestry(p, base).contains(&e.id) { error( out, e.id, "data_inheritance_cycle", "supertype", "Single specialization must be acyclic.", ); } if e.kind() != Kind::Interface { let names: BTreeSet<_> = effective_fields(p, base) .into_iter() .map(|f| &f.name) .collect(); for local in p.children(e.id).filter(|f| f.kind() == Kind::DataField) { if names.contains(&local.name) { error( out, local.id, "inherited_field_shadow", "name", "A local field cannot shadow an inherited field name.", ); } } } } match e.shared() { Some(SharedBody::ExchangeItem { kind, mechanism, supertype: base, }) => { if *mechanism == ExchangeMechanism::Unspecified { warning( out, e.id, "unspecified_exchange_mechanism", "mechanism", "Choose the exchange mechanism when its contract is known.", ); } if *kind != ExchangeItemKind::Information && matches!( mechanism, ExchangeMechanism::Event | ExchangeMechanism::Operation | ExchangeMechanism::SharedData ) { warning( out, e.id, "physical_exchange_mechanism", "mechanism", "This mechanism usually describes information about a physical exchange; review the nature.", ); } if let Some(SharedBody::ExchangeItem { kind: base_kind, mechanism: base_mechanism, .. }) = base .and_then(|id| p.elements.get(&id)) .and_then(Element::shared) && (kind != base_kind || (*base_mechanism != ExchangeMechanism::Unspecified && mechanism != base_mechanism)) { error( out, e.id, "exchange_specialization_conflict", "supertype", "An item specialization preserves its base nature and any specified base mechanism.", ); } let fields = effective_fields(p, e.id); if *mechanism == ExchangeMechanism::Operation { for field in fields.iter().filter(|f| { f.parent != Some(e.id) && matches!( f.shared(), Some(SharedBody::DataField { direction: ParameterDirection::Unspecified, .. }) ) }) { warning( out, e.id, "unspecified_inherited_parameter_direction", "supertype", format!( "Inherited parameter '{}' has no direction in this operation.", field.name ), ); } } if *mechanism == ExchangeMechanism::Operation && fields .iter() .filter(|f| { matches!( f.shared(), Some(SharedBody::DataField { direction: ParameterDirection::Return, .. }) ) }) .count() > 1 { error( out, e.id, "multiple_return_parameters", "supertype", "An operation has at most one Return parameter, including inherited parameters.", ); } if fields.is_empty() && !matches!( mechanism, ExchangeMechanism::Event | ExchangeMechanism::Operation ) && p.relations.values().any(|r| { r.body.items().contains(&e.id) || (r.kind() == RelationKind::InterfaceItemAllocation && r.target() == e.id) }) { warning( out, e.id, "empty_exchange_item", "body", "This carried or allocated item has no defined fields; an empty notification or void operation may be intentional.", ); } } Some(SharedBody::DataType { definition, default, unit, }) => { if matches!(definition, DataTypeDefinition::Record { .. }) && effective_fields(p, e.id).is_empty() && p.elements.values().any(|field| matches!(field.shared(), Some(SharedBody::DataField { type_ref: TypeRef::Named(id), .. }) if *id == e.id)) { warning(out,e.id,"empty_record","definition","This referenced Record has no defined fields yet."); } let invalid = match definition { DataTypeDefinition::Integer { minimum, maximum } => { minimum.zip(*maximum).is_some_and(|(a, b)| a > b) } DataTypeDefinition::Real { minimum, maximum } => { minimum.is_some_and(|n| !n.is_finite()) || maximum.is_some_and(|n| !n.is_finite()) || minimum.zip(*maximum).is_some_and(|(a, b)| a > b) } DataTypeDefinition::String { minimum_length, maximum_length, } => maximum_length.is_some_and(|n| n < *minimum_length), _ => false, }; if invalid { error( out, e.id, "invalid_data_bounds", "definition", "Bounds must be ordered and real bounds must be finite.", ); } if let DataTypeDefinition::Enumeration { literals } = definition { if literals.is_empty() { warning( out, e.id, "empty_enumeration", "literals", "Add enumeration literals when the available values are known.", ); } let mut seen = BTreeSet::new(); for literal in literals { check_text(out, e.id, "literals", literal); if literal.is_empty() { error( out, e.id, "empty_literal", "literals", "Enumeration literals must be nonempty.", ); } if !seen.insert(literal) { error( out, e.id, "duplicate_literal", "literals", "Enumeration literals must be unique and case-sensitive.", ); } } } if let Some(value) = default && let Err(message) = validate_scalar(definition, value) { error(out, e.id, "invalid_data_default", "default", message); } check_text(out, e.id, "unit", unit); if !unit.is_empty() && !matches!( definition, DataTypeDefinition::Integer { .. } | DataTypeDefinition::Real { .. } ) { warning( out, e.id, "nonnumeric_unit", "unit", "Units on nonnumeric types are preserved as annotations without conversion semantics.", ); } } Some(SharedBody::DataField { type_ref, minimum, maximum, unit, direction, storage, default, }) => { if maximum.is_some_and(|max| max < *minimum) { error( out, e.id, "invalid_multiplicity", "maximum", "Maximum multiplicity must be at least minimum, or unbounded.", ); } if let TypeRef::Named(id) = type_ref && !p .elements .get(id) .is_some_and(|t| t.kind() == Kind::DataType && t.layer() == e.layer()) { error( out, e.id, "field_type", "type_ref", "Choose a data type in the field's layer.", ); } if let Some(owner) = e.parent.and_then(|id| p.elements.get(&id)) && owner.kind() == Kind::DataType && !is_record(p, owner.id) { error( out, e.id, "field_owner", "parent", "Fields under a data type require a Record type.", ); } let operation = e.parent.and_then(|id| item_mechanism(p, id)) == Some(ExchangeMechanism::Operation); if !operation && *direction != ParameterDirection::Unspecified { error( out, e.id, "parameter_direction", "direction", "Only directly owned operation parameters may declare a direction.", ); } else if operation && *direction == ParameterDirection::Unspecified { warning( out, e.id, "unspecified_parameter_direction", "direction", "Choose the parameter direction from the provider's perspective.", ); } if *storage == FieldStorage::Reference && (!matches!(type_ref, TypeRef::Named(id) if is_record(p,*id)) || default.is_some()) { error( out, e.id, "reference_field_contract", "storage", "Reference storage requires a named Record type and no scalar default.", ); } if let Some(values) = default { if values.len() < *minimum as usize || maximum.is_some_and(|max| values.len() > max as usize) { error( out, e.id, "field_default_multiplicity", "default", "The explicit default collection must satisfy the field's multiplicity.", ); } if !values.is_empty() && matches!(type_ref, TypeRef::Named(id) if is_record(p,*id)) { error( out, e.id, "record_field_default", "default", "Record and object-reference defaults are not supported.", ); } for value in values { if let Err(message) = validate_data_value(p, type_ref, value) { error(out, e.id, "invalid_field_default", "default", message); } } } check_text(out, e.id, "unit", unit); if !unit.is_empty() && !numeric_type(p, type_ref) { warning( out, e.id, "nonnumeric_unit", "unit", "Units on nonnumeric fields are preserved as annotations.", ); } if let TypeRef::Named(id) = type_ref && let Some(SharedBody::DataType { unit: inherited, .. }) = p.elements.get(id).and_then(Element::shared) && !unit.is_empty() && !inherited.is_empty() && unit != inherited { warning( out, e.id, "field_unit_override", "unit", "The field unit differs from its named type; conversion compatibility is not evaluated.", ); } } Some(SharedBody::Interface { .. }) if effective_interface_allocations(p, e.id).is_empty() => { warning( out, e.id, "empty_interface", "body", "Allocate existing exchange items to define this interface's contract.", ); } _ => {} } } pub(crate) fn validate_relation(p: &Project, r: &Relation, out: &mut Vec) { match &r.body { RelationBody::InterfaceItemAllocation { source, target, .. } => { if effective_interface_allocations(p, *source) .into_iter() .any(|other| other.id != r.id && other.target() == *target) { error( out, r.id, "duplicate_interface_item", "target", "An interface cannot allocate an item already allocated locally or inherited from a base.", ); } } RelationBody::DataAssociation { source, target, source_role, target_role, source_minimum, source_maximum, target_minimum, target_maximum, source_navigable, target_navigable, } => { if !is_record(p, *source) || !is_record(p, *target) { error( out, r.id, "association_record_endpoints", "endpoints", "A data association connects two Record data types.", ); } for (field, min, max, role) in [ ("source", source_minimum, source_maximum, source_role), ("target", target_minimum, target_maximum, target_role), ] { if max.is_some_and(|max| max < *min) { error( out, r.id, "association_multiplicity", field, "An association end maximum must be at least its minimum.", ); } check_text(out, r.id, field, role); if role.trim().is_empty() { warning( out, r.id, "empty_association_role", field, "Name this association end to explain its role.", ); } } if source == target && *source_navigable && *target_navigable && (source_role.trim().is_empty() || target_role.trim().is_empty() || source_role == target_role) { error( out, r.id, "self_association_roles", "source_role", "A self-association with two navigable ends requires distinct nonempty role names.", ); } if p.relations.values().any(|other| other.id != r.id && matches!(&other.body, RelationBody::DataAssociation { source:s,target:t,source_role:sr,target_role:tr,.. } if (s == source && t == target && sr == source_role && tr == target_role) || (s == target && t == source && sr == target_role && tr == source_role))) { warning(out,r.id,"duplicate_association_roles","endpoints","Another identified association describes these same endpoint roles; review whether both are intended."); } } _ => {} } if !r.body.items().is_empty() && let Err(message) = try_carried_field_paths(p, r) { error(out, r.id, "data_path_expansion_limit", "items", message); } } pub(crate) fn validate_record_cycles(p: &Project, out: &mut Vec) { let graph: BTreeMap<_, Vec<_>> = p .elements .values() .filter(|e| is_record(p, e.id)) .map(|e| { let edges = effective_fields(p, e.id) .into_iter() .filter_map(|f| match f.shared() { Some(SharedBody::DataField { storage: FieldStorage::Value, minimum, type_ref: TypeRef::Named(id), .. }) if *minimum > 0 && is_record(p, *id) => Some(*id), _ => None, }) .collect(); (e.id, edges) }) .collect(); // Remove finite sinks, then their predecessors. Remaining records either // contain a mandatory cycle or necessarily contain a record that does. let mut remaining: BTreeMap<_, _> = graph.iter().map(|(id, edges)| (*id, edges.len())).collect(); let mut incoming: BTreeMap> = BTreeMap::new(); for (id, edges) in &graph { for target in edges { incoming.entry(*target).or_default().push(*id); } } let mut pending: Vec<_> = remaining .iter() .filter_map(|(id, count)| (*count == 0).then_some(*id)) .collect(); while let Some(id) = pending.pop() { remaining.remove(&id); for source in incoming.get(&id).into_iter().flatten() { if let Some(count) = remaining.get_mut(source) { *count -= 1; if *count == 0 { pending.push(*source); } } } } for id in remaining.keys() { warning( out, *id, "record_no_finite_value", "definition", "Mandatory Value record fields require a cycle; this type cannot have a finite fully populated value.", ); } }