Files
ARCADIA-Studio/crates/oa-core/src/data.rs
T

1203 lines
42 KiB
Rust

//! 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<Id>,
pub field: Id,
pub direction: ParameterDirection,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum LegacyFieldPathResolution {
Missing,
Unique(Vec<Id>),
Ambiguous,
}
pub fn supertype(e: &Element) -> Option<Id> {
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<Id> {
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<Id> {
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<String> {
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<ExchangeMechanism> {
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<Vec<CarriedFieldPath>, 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::<Id>::new(),
ParameterDirection::Unspecified,
BTreeSet::<Id>::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<CarriedFieldPath> {
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<LegacyFieldPathResolution, String> {
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<Id, Vec<Id>> = BTreeMap::new();
let mut reverse: BTreeMap<Id, Vec<Id>> = 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<CarriedFieldPath> {
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<Id, Id>,
) {
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::<Vec<_>>()
} 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<Issue>, 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<Issue>) {
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<Issue>) {
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<Issue>) {
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<Id, Vec<Id>> = 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.",
);
}
}