Implement bounded typed data and identified interface contracts
This commit is contained in:
File diff suppressed because it is too large
Load Diff
+24
-11
@@ -34,10 +34,7 @@ fn occurrence(
|
||||
id
|
||||
}
|
||||
fn port(p: &mut Project, owner: Id, name: &str, direction: PortDirection, interface: Id) -> Id {
|
||||
let items = match p.elements[&interface].shared() {
|
||||
Some(SharedBody::Interface { items }) => items.clone(),
|
||||
_ => vec![],
|
||||
};
|
||||
let items = crate::data::interface_items(p, interface);
|
||||
let contract = if p.elements[&owner].kind() == Kind::Function {
|
||||
if direction == PortDirection::Out {
|
||||
PortContract::FunctionOutput {
|
||||
@@ -168,6 +165,11 @@ pub fn coastal_rescue() -> Project {
|
||||
"Distress report",
|
||||
None,
|
||||
);
|
||||
if let Some(SharedBody::ExchangeItem { mechanism, .. }) =
|
||||
p.elements.get_mut(&oa_item).unwrap().shared_mut()
|
||||
{
|
||||
*mechanism = ExchangeMechanism::Event;
|
||||
}
|
||||
p.relate(RelationKind::Allocation, "Reports distress", vessel, report);
|
||||
p.relate(
|
||||
RelationKind::Allocation,
|
||||
@@ -308,6 +310,11 @@ pub fn coastal_rescue() -> Project {
|
||||
"Digital distress report",
|
||||
None,
|
||||
);
|
||||
if let Some(SharedBody::ExchangeItem { mechanism, .. }) =
|
||||
p.elements.get_mut(&information).unwrap().shared_mut()
|
||||
{
|
||||
*mechanism = ExchangeMechanism::Event;
|
||||
}
|
||||
let material = add(
|
||||
&mut p,
|
||||
Kind::ExchangeItem,
|
||||
@@ -317,6 +324,8 @@ pub fn coastal_rescue() -> Project {
|
||||
);
|
||||
*p.elements.get_mut(&material).unwrap().shared_mut().unwrap() = SharedBody::ExchangeItem {
|
||||
kind: ExchangeItemKind::Material,
|
||||
mechanism: ExchangeMechanism::Flow,
|
||||
supertype: None,
|
||||
};
|
||||
let energy = add(
|
||||
&mut p,
|
||||
@@ -327,6 +336,8 @@ pub fn coastal_rescue() -> Project {
|
||||
);
|
||||
*p.elements.get_mut(&energy).unwrap().shared_mut().unwrap() = SharedBody::ExchangeItem {
|
||||
kind: ExchangeItemKind::Energy,
|
||||
mechanism: ExchangeMechanism::Flow,
|
||||
supertype: None,
|
||||
};
|
||||
let field = add(
|
||||
&mut p,
|
||||
@@ -340,6 +351,9 @@ pub fn coastal_rescue() -> Project {
|
||||
minimum: 1,
|
||||
maximum: Some(1),
|
||||
unit: "degrees north".into(),
|
||||
direction: ParameterDirection::Unspecified,
|
||||
storage: FieldStorage::Value,
|
||||
default: None,
|
||||
};
|
||||
let interface = add(
|
||||
&mut p,
|
||||
@@ -348,13 +362,12 @@ pub fn coastal_rescue() -> Project {
|
||||
"Distress reporting interface",
|
||||
None,
|
||||
);
|
||||
*p.elements
|
||||
.get_mut(&interface)
|
||||
.unwrap()
|
||||
.shared_mut()
|
||||
.unwrap() = SharedBody::Interface {
|
||||
items: vec![information],
|
||||
};
|
||||
p.relate(
|
||||
RelationKind::InterfaceItemAllocation,
|
||||
"Distress payload",
|
||||
interface,
|
||||
information,
|
||||
);
|
||||
let vessel_port = port(
|
||||
&mut p,
|
||||
actor,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
//! Framework-independent OA/SA model, transactional commands and editing services.
|
||||
pub mod data;
|
||||
pub mod language;
|
||||
pub mod model;
|
||||
pub mod persistence;
|
||||
|
||||
+149
-14
@@ -6,7 +6,7 @@ use std::{
|
||||
use uuid::Uuid;
|
||||
|
||||
pub type Id = Uuid;
|
||||
pub const SCHEMA_VERSION: u32 = 3;
|
||||
pub const SCHEMA_VERSION: u32 = 4;
|
||||
pub const MAX_COORDINATE: f32 = 1_000_000.;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
|
||||
@@ -265,17 +265,79 @@ pub enum ExchangeItemKind {
|
||||
Information,
|
||||
Material,
|
||||
Energy,
|
||||
Event,
|
||||
}
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
|
||||
pub enum ExchangeMechanism {
|
||||
#[default]
|
||||
Unspecified,
|
||||
Flow,
|
||||
Event,
|
||||
Operation,
|
||||
SharedData,
|
||||
}
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
|
||||
pub enum ParameterDirection {
|
||||
#[default]
|
||||
Unspecified,
|
||||
In,
|
||||
Out,
|
||||
InOut,
|
||||
Return,
|
||||
}
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
|
||||
pub enum FieldStorage {
|
||||
#[default]
|
||||
Value,
|
||||
Reference,
|
||||
}
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub enum DataValue {
|
||||
Boolean(bool),
|
||||
Integer(i64),
|
||||
Real(f64),
|
||||
String(String),
|
||||
Enumeration(String),
|
||||
}
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
|
||||
pub enum TransmissionProtocol {
|
||||
#[default]
|
||||
Unspecified,
|
||||
Synchronous,
|
||||
Asynchronous,
|
||||
Unicast,
|
||||
Multicast,
|
||||
Broadcast,
|
||||
}
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
|
||||
pub enum AcquisitionProtocol {
|
||||
#[default]
|
||||
Unspecified,
|
||||
Read,
|
||||
Accept,
|
||||
}
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub enum DataTypeDefinition {
|
||||
Boolean,
|
||||
Integer,
|
||||
Real,
|
||||
String,
|
||||
Enumeration { literals: Vec<String> },
|
||||
Record,
|
||||
Integer {
|
||||
minimum: Option<i64>,
|
||||
maximum: Option<i64>,
|
||||
},
|
||||
Real {
|
||||
minimum: Option<f64>,
|
||||
maximum: Option<f64>,
|
||||
},
|
||||
String {
|
||||
minimum_length: u32,
|
||||
maximum_length: Option<u32>,
|
||||
},
|
||||
Enumeration {
|
||||
literals: Vec<String>,
|
||||
},
|
||||
Record {
|
||||
supertype: Option<Id>,
|
||||
},
|
||||
}
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
@@ -453,6 +515,8 @@ pub struct LifelineBinding {
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct MessageValue {
|
||||
#[serde(default)]
|
||||
pub path: Vec<Id>,
|
||||
pub field: Id,
|
||||
pub expression: String,
|
||||
}
|
||||
@@ -502,18 +566,25 @@ pub enum OccurrenceBody {
|
||||
pub enum SharedBody {
|
||||
ExchangeItem {
|
||||
kind: ExchangeItemKind,
|
||||
mechanism: ExchangeMechanism,
|
||||
supertype: Option<Id>,
|
||||
},
|
||||
DataType {
|
||||
definition: DataTypeDefinition,
|
||||
default: Option<DataValue>,
|
||||
unit: String,
|
||||
},
|
||||
DataField {
|
||||
type_ref: TypeRef,
|
||||
minimum: u32,
|
||||
maximum: Option<u32>,
|
||||
unit: String,
|
||||
direction: ParameterDirection,
|
||||
storage: FieldStorage,
|
||||
default: Option<Vec<DataValue>>,
|
||||
},
|
||||
Interface {
|
||||
items: Vec<Id>,
|
||||
supertype: Option<Id>,
|
||||
},
|
||||
Port {
|
||||
contract: PortContract,
|
||||
@@ -672,17 +743,27 @@ impl ElementBody {
|
||||
match k {
|
||||
Kind::ExchangeItem => SharedBody::ExchangeItem {
|
||||
kind: ExchangeItemKind::Information,
|
||||
mechanism: ExchangeMechanism::Unspecified,
|
||||
supertype: None,
|
||||
},
|
||||
Kind::DataType => SharedBody::DataType {
|
||||
definition: DataTypeDefinition::String,
|
||||
definition: DataTypeDefinition::String {
|
||||
minimum_length: 0,
|
||||
maximum_length: None,
|
||||
},
|
||||
default: None,
|
||||
unit: String::new(),
|
||||
},
|
||||
Kind::DataField => SharedBody::DataField {
|
||||
type_ref: TypeRef::String,
|
||||
minimum: 1,
|
||||
maximum: Some(1),
|
||||
unit: String::new(),
|
||||
direction: ParameterDirection::Unspecified,
|
||||
storage: FieldStorage::Value,
|
||||
default: None,
|
||||
},
|
||||
Kind::Interface => SharedBody::Interface { items: vec![] },
|
||||
Kind::Interface => SharedBody::Interface { supertype: None },
|
||||
// Port creation requires an explicit contract and owner.
|
||||
Kind::Port => return None,
|
||||
Kind::Scenario => SharedBody::Scenario {
|
||||
@@ -752,6 +833,8 @@ impl SharedBody {
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum RelationKind {
|
||||
InterfaceItemAllocation,
|
||||
DataAssociation,
|
||||
Allocation,
|
||||
PortAllocation,
|
||||
Interaction,
|
||||
@@ -775,7 +858,9 @@ pub enum RelationKind {
|
||||
StateAction,
|
||||
}
|
||||
impl RelationKind {
|
||||
pub const ALL: [Self; 21] = [
|
||||
pub const ALL: [Self; 23] = [
|
||||
Self::InterfaceItemAllocation,
|
||||
Self::DataAssociation,
|
||||
Self::Allocation,
|
||||
Self::PortAllocation,
|
||||
Self::Interaction,
|
||||
@@ -800,6 +885,8 @@ impl RelationKind {
|
||||
];
|
||||
pub fn permits(self, from: Kind, to: Kind) -> bool {
|
||||
match self {
|
||||
Self::InterfaceItemAllocation => from == Kind::Interface && to == Kind::ExchangeItem,
|
||||
Self::DataAssociation => from == Kind::DataType && to == Kind::DataType,
|
||||
Self::Allocation => from.is_performer() && to.is_work(),
|
||||
Self::PortAllocation => from == Kind::Port && to == Kind::Port,
|
||||
Self::Interaction => from == Kind::Activity && to == Kind::Activity,
|
||||
@@ -855,6 +942,8 @@ impl RelationKind {
|
||||
}
|
||||
pub fn hint(self) -> &'static str {
|
||||
match self {
|
||||
Self::InterfaceItemAllocation => "Interface → allocated exchange item",
|
||||
Self::DataAssociation => "Record type → record type",
|
||||
Self::Allocation => "Performer → leaf activity/function",
|
||||
Self::PortAllocation => "Component port → allocated function port",
|
||||
Self::Interaction => "OA activity → OA activity",
|
||||
@@ -886,6 +975,8 @@ impl RelationKind {
|
||||
impl fmt::Display for RelationKind {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str(match self {
|
||||
Self::InterfaceItemAllocation => "Interface item allocation",
|
||||
Self::DataAssociation => "Data association",
|
||||
Self::Allocation => "Allocation",
|
||||
Self::PortAllocation => "Port allocation",
|
||||
Self::Interaction => "Interaction",
|
||||
@@ -922,6 +1013,25 @@ pub struct Relation {
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub enum RelationBody {
|
||||
InterfaceItemAllocation {
|
||||
source: Id,
|
||||
target: Id,
|
||||
order: u32,
|
||||
transmission: TransmissionProtocol,
|
||||
acquisition: AcquisitionProtocol,
|
||||
},
|
||||
DataAssociation {
|
||||
source: Id,
|
||||
target: Id,
|
||||
source_role: String,
|
||||
target_role: String,
|
||||
source_minimum: u32,
|
||||
source_maximum: Option<u32>,
|
||||
target_minimum: u32,
|
||||
target_maximum: Option<u32>,
|
||||
source_navigable: bool,
|
||||
target_navigable: bool,
|
||||
},
|
||||
Allocation {
|
||||
source: Id,
|
||||
target: Id,
|
||||
@@ -1039,6 +1149,8 @@ impl Relation {
|
||||
impl RelationBody {
|
||||
pub fn kind(&self) -> RelationKind {
|
||||
match self {
|
||||
Self::InterfaceItemAllocation { .. } => RelationKind::InterfaceItemAllocation,
|
||||
Self::DataAssociation { .. } => RelationKind::DataAssociation,
|
||||
Self::Allocation { .. } => RelationKind::Allocation,
|
||||
Self::PortAllocation { .. } => RelationKind::PortAllocation,
|
||||
Self::Interaction { .. } => RelationKind::Interaction,
|
||||
@@ -1064,7 +1176,9 @@ impl RelationBody {
|
||||
}
|
||||
pub fn endpoints(&self) -> (Id, Id) {
|
||||
match self {
|
||||
Self::Allocation { source, target }
|
||||
Self::InterfaceItemAllocation { source, target, .. }
|
||||
| Self::DataAssociation { source, target, .. }
|
||||
| Self::Allocation { source, target }
|
||||
| Self::PortAllocation { source, target }
|
||||
| Self::Interaction { source, target, .. }
|
||||
| Self::CapabilityInvolvement { source, target }
|
||||
@@ -1089,7 +1203,9 @@ impl RelationBody {
|
||||
}
|
||||
pub fn set_endpoints(&mut self, a: Id, b: Id) {
|
||||
match self {
|
||||
Self::Allocation { source, target }
|
||||
Self::InterfaceItemAllocation { source, target, .. }
|
||||
| Self::DataAssociation { source, target, .. }
|
||||
| Self::Allocation { source, target }
|
||||
| Self::PortAllocation { source, target }
|
||||
| Self::Interaction { source, target, .. }
|
||||
| Self::CapabilityInvolvement { source, target }
|
||||
@@ -1125,6 +1241,25 @@ impl RelationBody {
|
||||
}
|
||||
pub fn new(kind: RelationKind, source: Id, target: Id) -> Self {
|
||||
match kind {
|
||||
RelationKind::InterfaceItemAllocation => Self::InterfaceItemAllocation {
|
||||
source,
|
||||
target,
|
||||
order: 0,
|
||||
transmission: TransmissionProtocol::Unspecified,
|
||||
acquisition: AcquisitionProtocol::Unspecified,
|
||||
},
|
||||
RelationKind::DataAssociation => Self::DataAssociation {
|
||||
source,
|
||||
target,
|
||||
source_role: String::new(),
|
||||
target_role: String::new(),
|
||||
source_minimum: 0,
|
||||
source_maximum: None,
|
||||
target_minimum: 0,
|
||||
target_maximum: None,
|
||||
source_navigable: false,
|
||||
target_navigable: false,
|
||||
},
|
||||
RelationKind::Allocation => Self::Allocation { source, target },
|
||||
RelationKind::PortAllocation => Self::PortAllocation { source, target },
|
||||
RelationKind::Interaction => Self::Interaction {
|
||||
|
||||
@@ -64,9 +64,9 @@ pub struct LegacyWorkPortSplitRequest {
|
||||
pub outgoing_items: Vec<Id>,
|
||||
}
|
||||
|
||||
/// R6 specialization has one explicit seam; current models support identity only.
|
||||
pub fn item_satisfies(_project: &Project, carried: Id, declared: Id) -> bool {
|
||||
carried == declared
|
||||
/// A specialized carried item satisfies a declared base item.
|
||||
pub fn item_satisfies(project: &Project, carried: Id, declared: Id) -> bool {
|
||||
crate::data::item_satisfies(project, carried, declared)
|
||||
}
|
||||
pub fn items_satisfy(project: &Project, carried: &[Id], declared: &[Id]) -> bool {
|
||||
carried.iter().all(|item| {
|
||||
@@ -130,13 +130,7 @@ fn declared_interface_items(p: &Project, port: Id) -> Option<Vec<Id>> {
|
||||
Some(
|
||||
interfaces
|
||||
.into_iter()
|
||||
.filter_map(|id| p.elements.get(&id))
|
||||
.filter_map(|e| match e.shared() {
|
||||
Some(SharedBody::Interface { items }) => Some(items),
|
||||
_ => None,
|
||||
})
|
||||
.flatten()
|
||||
.copied()
|
||||
.flat_map(|id| crate::data::interface_items(p, id))
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
@@ -903,14 +897,10 @@ fn validate_carried_contract(p: &Project, r: &Relation, port: &Element, out: &mu
|
||||
..
|
||||
}) => {
|
||||
let mut allowed = declared_interface_items(p, port.id);
|
||||
if let Some(SharedBody::Interface { items }) = legacy
|
||||
.as_ref()
|
||||
.and_then(|l| l.interface)
|
||||
.and_then(|id| p.elements.get(&id))
|
||||
.and_then(Element::shared)
|
||||
{
|
||||
if let Some(interface) = legacy.as_ref().and_then(|l| l.interface) {
|
||||
let items = crate::data::interface_items(p, interface);
|
||||
if r.kind() == RelationKind::BoundaryExchange
|
||||
&& !items_satisfy(p, r.body.items(), items)
|
||||
&& !items_satisfy(p, r.body.items(), &items)
|
||||
{
|
||||
error(
|
||||
out,
|
||||
@@ -1173,7 +1163,7 @@ pub(crate) fn validate_boundary_interface_requirements(
|
||||
if !p.relations.values().any(|role| {
|
||||
role.kind() == RelationKind::InterfaceProvision
|
||||
&& role.source() == peer.id
|
||||
&& role.target() == required.target()
|
||||
&& crate::data::interface_satisfies(p, role.target(), required.target())
|
||||
}) {
|
||||
warning(
|
||||
out,
|
||||
|
||||
@@ -335,6 +335,11 @@ pub fn project_diagram(p: &Project, d: &Diagram) -> ProjectedView {
|
||||
.collect();
|
||||
let mut edges = Vec::new();
|
||||
for r in p.relations.values() {
|
||||
// Interface allocations are selectable nested dictionary rows. Their
|
||||
// independently identified contracts do not add noisy diagram arrows.
|
||||
if r.kind() == RelationKind::InterfaceItemAllocation {
|
||||
continue;
|
||||
}
|
||||
if d.projection == ProjectionKind::Allocation && r.kind() == RelationKind::Allocation {
|
||||
continue;
|
||||
}
|
||||
@@ -370,7 +375,8 @@ pub fn project_diagram(p: &Project, d: &Diagram) -> ProjectedView {
|
||||
}
|
||||
if let (Some(source), Some(target)) = (anchor(r.source()), anchor(r.target()))
|
||||
&& (source != target
|
||||
|| (d.projection.state_machine().is_some()
|
||||
|| ((d.projection.state_machine().is_some()
|
||||
|| r.kind() == RelationKind::DataAssociation)
|
||||
&& r.source() == r.target()
|
||||
&& nodes
|
||||
.iter()
|
||||
@@ -396,7 +402,7 @@ pub fn project_diagram(p: &Project, d: &Diagram) -> ProjectedView {
|
||||
RelationBody::BoundaryExchange {
|
||||
oriented: false,
|
||||
..
|
||||
}
|
||||
} | RelationBody::DataAssociation { .. }
|
||||
),
|
||||
source_port: source_port.filter(|id| visible_ports.contains(id)),
|
||||
target_port: target_port.filter(|id| visible_ports.contains(id)),
|
||||
|
||||
@@ -33,8 +33,13 @@ pub fn visit_element_refs(e: &mut Element, mut visit: impl FnMut(&mut Id)) {
|
||||
type_ref: TypeRef::Named(id),
|
||||
..
|
||||
} => visit(id),
|
||||
SharedBody::Interface { items } => {
|
||||
for id in items {
|
||||
SharedBody::Interface { supertype }
|
||||
| SharedBody::ExchangeItem { supertype, .. }
|
||||
| SharedBody::DataType {
|
||||
definition: DataTypeDefinition::Record { supertype },
|
||||
..
|
||||
} => {
|
||||
if let Some(id) = supertype {
|
||||
visit(id);
|
||||
}
|
||||
}
|
||||
@@ -83,6 +88,9 @@ pub fn visit_element_refs(e: &mut Element, mut visit: impl FnMut(&mut Id)) {
|
||||
visit(id);
|
||||
}
|
||||
for value in values {
|
||||
for id in &mut value.path {
|
||||
visit(id);
|
||||
}
|
||||
visit(&mut value.field);
|
||||
}
|
||||
}
|
||||
@@ -377,7 +385,12 @@ fn remove_objects(p: &mut Project, removed: BTreeSet<Id>) {
|
||||
}
|
||||
if let Some(shared) = e.shared_mut() {
|
||||
match shared {
|
||||
SharedBody::Interface { items } => items.retain(|id| !removed.contains(id)),
|
||||
SharedBody::Interface { supertype }
|
||||
| SharedBody::ExchangeItem { supertype, .. }
|
||||
| SharedBody::DataType {
|
||||
definition: DataTypeDefinition::Record { supertype },
|
||||
..
|
||||
} => clear(supertype),
|
||||
SharedBody::Event {
|
||||
body: EventBody::Signal { items },
|
||||
} => items.retain(|id| !removed.contains(id)),
|
||||
@@ -413,7 +426,10 @@ fn remove_objects(p: &mut Project, removed: BTreeSet<Id>) {
|
||||
clear(sender);
|
||||
clear(receiver);
|
||||
clear(reply_to);
|
||||
values.retain(|value| !removed.contains(&value.field));
|
||||
values.retain(|value| {
|
||||
!removed.contains(&value.field)
|
||||
&& value.path.iter().all(|id| !removed.contains(id))
|
||||
});
|
||||
}
|
||||
OccurrenceBody::Action { action, lifeline } => {
|
||||
clear(action);
|
||||
@@ -485,20 +501,37 @@ fn remove_objects(p: &mut Project, removed: BTreeSet<Id>) {
|
||||
exchange: Some(id), ..
|
||||
},
|
||||
..
|
||||
}) => p
|
||||
.relations
|
||||
.get(id)
|
||||
.map(|r| (e.id, crate::validation::carried_fields(p, r))),
|
||||
}) => p.relations.get(id).map(|r| {
|
||||
let Some(SharedBody::Occurrence {
|
||||
body: OccurrenceBody::Exchange { values, .. },
|
||||
..
|
||||
}) = e.shared()
|
||||
else {
|
||||
unreachable!()
|
||||
};
|
||||
let retained: Vec<_> = values
|
||||
.iter()
|
||||
.filter(|value| {
|
||||
crate::data::carried_value_path(p, r, &value.path, value.field).is_some()
|
||||
|| (value.path.is_empty()
|
||||
&& crate::data::carried_field_paths(p, r)
|
||||
.iter()
|
||||
.any(|f| f.field == value.field))
|
||||
})
|
||||
.cloned()
|
||||
.collect();
|
||||
(e.id, retained)
|
||||
}),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
for (id, fields) in allowed {
|
||||
for (id, retained) in allowed {
|
||||
if let Some(SharedBody::Occurrence {
|
||||
body: OccurrenceBody::Exchange { values, .. },
|
||||
..
|
||||
}) = p.elements.get_mut(&id).and_then(Element::shared_mut)
|
||||
{
|
||||
values.retain(|value| fields.contains(&value.field));
|
||||
*values = retained;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//! Deterministic, bounded diagram geometry. Only authored pins belong in the model;
|
||||
//! callers share this derived geometry for drawing, picking, fitting and export.
|
||||
use crate::projection::{self, ProjectedEdge, ProjectedNode, ProjectedView};
|
||||
use crate::{Diagram, Id, Position, Project, RelationKind, Size};
|
||||
use crate::{Diagram, Id, Position, Project, Relation, RelationBody, RelationKind, Size};
|
||||
use std::cmp::Ordering;
|
||||
use std::collections::{BTreeMap, BTreeSet, BinaryHeap};
|
||||
use std::fmt;
|
||||
@@ -55,6 +55,122 @@ pub struct EdgeLabelGeometry {
|
||||
pub bounds: RoutingBounds,
|
||||
}
|
||||
|
||||
/// Derived labels and navigation markers for the two named association ends.
|
||||
/// Canvas and SVG consume these same coordinates; no extra diagram state exists.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct AssociationEndGeometry {
|
||||
pub source_end: bool,
|
||||
pub label: EdgeLabelGeometry,
|
||||
pub tip: Position,
|
||||
pub inward: Position,
|
||||
pub navigable: bool,
|
||||
}
|
||||
|
||||
pub fn association_end_geometry(
|
||||
relation: &Relation,
|
||||
routed: &RoutedEdge,
|
||||
) -> Vec<AssociationEndGeometry> {
|
||||
let RelationBody::DataAssociation {
|
||||
source_navigable,
|
||||
target_navigable,
|
||||
..
|
||||
} = &relation.body
|
||||
else {
|
||||
return Vec::new();
|
||||
};
|
||||
let mut ends: Vec<AssociationEndGeometry> = Vec::new();
|
||||
for (source_end, navigable) in [(true, *source_navigable), (false, *target_navigable)] {
|
||||
let Some(tip) = (if source_end {
|
||||
routed.points.first()
|
||||
} else {
|
||||
routed.points.last()
|
||||
})
|
||||
.copied() else {
|
||||
continue;
|
||||
};
|
||||
let inward = if source_end {
|
||||
routed.points.iter().find(|p| **p != tip)
|
||||
} else {
|
||||
routed.points.iter().rev().find(|p| **p != tip)
|
||||
};
|
||||
let Some(inward) = inward.copied() else {
|
||||
continue;
|
||||
};
|
||||
let full = crate::data::association_end_label(relation, source_end).unwrap_or_default();
|
||||
let text = if let Some((role, multiplicity)) = full.rsplit_once(" [") {
|
||||
let role = if role.chars().count() > 32 {
|
||||
role.chars()
|
||||
.take(31)
|
||||
.chain(std::iter::once('…'))
|
||||
.collect::<String>()
|
||||
} else {
|
||||
role.to_owned()
|
||||
};
|
||||
format!("{role} [{multiplicity}")
|
||||
} else {
|
||||
full
|
||||
};
|
||||
let size = Size {
|
||||
width: text.chars().count() as f32 * 7.0 + 12.0,
|
||||
height: 22.0,
|
||||
};
|
||||
let dx = inward.x - tip.x;
|
||||
let dy = inward.y - tip.y;
|
||||
let length = dx.hypot(dy);
|
||||
let direction = pos(dx / length, dy / length);
|
||||
// One normal sign puts opposite endpoints of a straight edge on
|
||||
// opposite sides because their inward tangent directions are reversed.
|
||||
let along =
|
||||
direction.x.abs() * size.width / 2.0 + direction.y.abs() * size.height / 2.0 + 12.0;
|
||||
let normal =
|
||||
direction.y.abs() * size.width / 2.0 + direction.x.abs() * size.height / 2.0 + 8.0;
|
||||
let mut center = pos(
|
||||
tip.x + direction.x * along - direction.y * normal,
|
||||
tip.y + direction.y * along + direction.x * normal,
|
||||
);
|
||||
if let Some(other) = ends.first()
|
||||
&& (center.x - other.label.center.x).abs()
|
||||
< (size.width + other.label.bounds.size.width) / 2.0 + 6.0
|
||||
&& (center.y - other.label.center.y).abs()
|
||||
< (size.height + other.label.bounds.size.height) / 2.0 + 6.0
|
||||
{
|
||||
// Same-side anchors on a self-loop can still be close. Separate
|
||||
// their complete bounds along this end's normal in one step.
|
||||
let perpendicular = pos(-direction.y, direction.x);
|
||||
let distance = (center.x - other.label.center.x) * perpendicular.x
|
||||
+ (center.y - other.label.center.y) * perpendicular.y;
|
||||
let clearance = (perpendicular.x.abs() * (size.width + other.label.bounds.size.width)
|
||||
+ perpendicular.y.abs() * (size.height + other.label.bounds.size.height))
|
||||
/ 2.0
|
||||
+ 8.0;
|
||||
let shift = if distance < 0.0 {
|
||||
-clearance - distance
|
||||
} else {
|
||||
clearance - distance
|
||||
};
|
||||
center.x += perpendicular.x * shift;
|
||||
center.y += perpendicular.y * shift;
|
||||
}
|
||||
ends.push(AssociationEndGeometry {
|
||||
source_end,
|
||||
label: EdgeLabelGeometry {
|
||||
text,
|
||||
fraction: if source_end { 0.0 } else { 1.0 },
|
||||
anchor: tip,
|
||||
center,
|
||||
bounds: RoutingBounds {
|
||||
position: pos(center.x - size.width / 2.0, center.y - size.height / 2.0),
|
||||
size,
|
||||
},
|
||||
},
|
||||
tip,
|
||||
inward,
|
||||
navigable,
|
||||
});
|
||||
}
|
||||
ends
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum RouteWarning {
|
||||
PinInsideObstacle { pin: usize, obstacle: Id },
|
||||
@@ -466,6 +582,14 @@ fn route_diagram_impl(
|
||||
// never changes paths or authored metadata. Previews retain other cached
|
||||
// labels and avoid the paths actually shown during the gesture.
|
||||
let mut previous_labels = Vec::new();
|
||||
for routed in edges.values() {
|
||||
if let Some(relation) = project.relations.get(&routed.id) {
|
||||
for end in association_end_geometry(relation, routed) {
|
||||
bounds.rect(end.label.bounds.position, end.label.bounds.size);
|
||||
previous_labels.push(end.label.bounds);
|
||||
}
|
||||
}
|
||||
}
|
||||
for edge in ordered {
|
||||
let Some(routed) = edges.get_mut(&edge.id) else {
|
||||
continue;
|
||||
@@ -579,7 +703,7 @@ fn route_edge(
|
||||
let mut candidates = Vec::new();
|
||||
for a in endpoints(source, edge.source_port, view) {
|
||||
for b in endpoints(target, edge.target_port, view) {
|
||||
// A real self-transition must leave and return at distinct anchors.
|
||||
// A real self-relation must leave and return at distinct anchors.
|
||||
// Reusing one anchor makes the cheapest path a degenerate stub.
|
||||
if edge.source == edge.target
|
||||
&& edge.source_port.is_none()
|
||||
@@ -1107,7 +1231,8 @@ fn label_text(project: &Project, edge: Id) -> String {
|
||||
let text = match relation.kind() {
|
||||
RelationKind::Interaction
|
||||
| RelationKind::FunctionalExchange
|
||||
| RelationKind::BoundaryExchange => relation.name.as_str(),
|
||||
| RelationKind::BoundaryExchange
|
||||
| RelationKind::DataAssociation => relation.name.as_str(),
|
||||
RelationKind::CapabilityInclude => "«include»",
|
||||
RelationKind::CapabilityExtend => "«extend»",
|
||||
RelationKind::CapabilityGeneralization => "specializes",
|
||||
|
||||
@@ -114,6 +114,7 @@ pub struct PreparedCommand {
|
||||
struct IdentityProvenance {
|
||||
original: BTreeSet<Id>,
|
||||
current_to_original: BTreeMap<Id, Id>,
|
||||
explicit_replacement: bool,
|
||||
}
|
||||
|
||||
impl IdentityProvenance {
|
||||
@@ -123,10 +124,12 @@ impl IdentityProvenance {
|
||||
Self {
|
||||
original,
|
||||
current_to_original,
|
||||
explicit_replacement: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn replace(&mut self, project: &Project) {
|
||||
self.explicit_replacement = true;
|
||||
let present: BTreeSet<_> = project.identities().into_iter().map(|(id, _)| id).collect();
|
||||
self.current_to_original = self
|
||||
.original
|
||||
@@ -180,6 +183,13 @@ pub fn prepare_command(
|
||||
)?;
|
||||
crate::references::finish_deletions(&mut candidate, &pending_deletions);
|
||||
provenance.retain_present(&candidate);
|
||||
if !provenance.explicit_replacement {
|
||||
crate::data::prune_removed_inheritance(
|
||||
project,
|
||||
&mut candidate,
|
||||
&provenance.current_to_original,
|
||||
);
|
||||
}
|
||||
validate_candidate(&candidate)?;
|
||||
Ok(PreparedCommand {
|
||||
project: candidate,
|
||||
@@ -287,6 +297,13 @@ pub fn execute_batch(project: &Project, commands: &[Command]) -> Result<Project,
|
||||
)?;
|
||||
}
|
||||
crate::references::finish_deletions(&mut candidate, &pending_deletions);
|
||||
if !provenance.explicit_replacement {
|
||||
crate::data::prune_removed_inheritance(
|
||||
project,
|
||||
&mut candidate,
|
||||
&provenance.current_to_original,
|
||||
);
|
||||
}
|
||||
validate_candidate(&candidate)?;
|
||||
Ok(candidate)
|
||||
}
|
||||
|
||||
@@ -251,6 +251,8 @@ fn mapped_relation(r: &Relation, direction: TransitionDirection) -> Option<Relat
|
||||
}
|
||||
(
|
||||
RelationBody::Allocation { .. }
|
||||
| RelationBody::InterfaceItemAllocation { .. }
|
||||
| RelationBody::DataAssociation { .. }
|
||||
| RelationBody::ExchangeAllocation { .. }
|
||||
| RelationBody::CapabilityInvolvement { .. }
|
||||
| RelationBody::CapabilityInclude { .. }
|
||||
@@ -421,6 +423,24 @@ pub fn plan_transition(
|
||||
}
|
||||
if let Some(e) = project.elements.get(&id) {
|
||||
selected.extend(crate::references::element_references(e));
|
||||
// Allocations are owned by interfaces. Independent data
|
||||
// associations travel only when both endpoint types are
|
||||
// already required; they do not expand the dictionary.
|
||||
if matches!(e.kind(), Kind::Interface | Kind::DataType) {
|
||||
let contracts: Vec<_> = project
|
||||
.relations
|
||||
.values()
|
||||
.filter_map(|r| {
|
||||
((r.kind() == RelationKind::InterfaceItemAllocation
|
||||
&& r.source() == id)
|
||||
|| (r.kind() == RelationKind::DataAssociation
|
||||
&& selected.contains(&r.source())
|
||||
&& selected.contains(&r.target())))
|
||||
.then_some(r.id)
|
||||
})
|
||||
.collect();
|
||||
selected.extend(contracts);
|
||||
}
|
||||
// Contextual links are owned by the process/chain shared by
|
||||
// their endpoint involvements, independently of list order.
|
||||
if matches!(e.kind(), Kind::Process | Kind::FunctionalChain) {
|
||||
|
||||
@@ -388,68 +388,10 @@ pub fn validate(p: &Project) -> Vec<Issue> {
|
||||
}
|
||||
if let Some(shared) = e.shared() {
|
||||
match shared {
|
||||
SharedBody::DataType {
|
||||
definition: DataTypeDefinition::Enumeration { literals },
|
||||
} => {
|
||||
let mut seen = BTreeSet::new();
|
||||
for literal in literals {
|
||||
text(&mut out, e.id, "literals", literal);
|
||||
if !seen.insert(literal) {
|
||||
error(
|
||||
&mut out,
|
||||
e.id,
|
||||
"duplicate_literal",
|
||||
"literals",
|
||||
"Enumeration literals must be unique.",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
SharedBody::DataField {
|
||||
type_ref,
|
||||
minimum,
|
||||
maximum,
|
||||
unit,
|
||||
} => {
|
||||
if maximum.is_some_and(|max| max < *minimum) {
|
||||
error(
|
||||
&mut out,
|
||||
e.id,
|
||||
"invalid_multiplicity",
|
||||
"maximum",
|
||||
"Maximum multiplicity must be at least minimum, or unbounded.",
|
||||
);
|
||||
}
|
||||
text(&mut out, e.id, "unit", unit);
|
||||
if let TypeRef::Named(id) = type_ref {
|
||||
expect_element(p, &mut out, e.id, *id, "type_ref", |k| k == Kind::DataType);
|
||||
}
|
||||
if let Some(parent) = e.parent.and_then(|id| p.elements.get(&id))
|
||||
&& parent.kind() == Kind::DataType
|
||||
&& !matches!(
|
||||
parent.shared(),
|
||||
Some(SharedBody::DataType {
|
||||
definition: DataTypeDefinition::Record
|
||||
})
|
||||
)
|
||||
{
|
||||
error(
|
||||
&mut out,
|
||||
e.id,
|
||||
"field_owner",
|
||||
"parent",
|
||||
"Fields under a data type require a Record type.",
|
||||
);
|
||||
}
|
||||
}
|
||||
SharedBody::Interface { items } => {
|
||||
check_ids(&mut out, e.id, "items", items);
|
||||
for id in items {
|
||||
expect_element(p, &mut out, e.id, *id, "items", |k| {
|
||||
k == Kind::ExchangeItem
|
||||
});
|
||||
}
|
||||
}
|
||||
SharedBody::DataType { .. }
|
||||
| SharedBody::DataField { .. }
|
||||
| SharedBody::Interface { .. }
|
||||
| SharedBody::ExchangeItem { .. } => crate::data::validate_element(p, e, &mut out),
|
||||
SharedBody::Port { .. } => crate::ports::validate_port(p, e, &mut out),
|
||||
SharedBody::Scenario {
|
||||
viewpoint,
|
||||
@@ -643,9 +585,11 @@ pub fn validate(p: &Project) -> Vec<Issue> {
|
||||
text(&mut out, r.id, "name", &r.name);
|
||||
text(&mut out, r.id, "description", &r.description);
|
||||
validate_relation(p, r, &mut out);
|
||||
crate::data::validate_relation(p, r, &mut out);
|
||||
crate::ports::validate_boundary_interface_requirements(p, r, &mut out);
|
||||
validate_relation_context(p, r, &mut out);
|
||||
}
|
||||
crate::data::validate_record_cycles(p, &mut out);
|
||||
for (key, d) in &p.diagrams {
|
||||
if *key != d.id {
|
||||
error(
|
||||
@@ -1436,23 +1380,10 @@ fn enclosing_loops(p: &Project, occurrence: Id) -> BTreeSet<Id> {
|
||||
}
|
||||
/// Fields carried by an exchange, including fields of nested named records.
|
||||
pub fn carried_fields(p: &Project, relation: &Relation) -> BTreeSet<Id> {
|
||||
let mut pending = relation.body.items().to_vec();
|
||||
let mut seen = BTreeSet::new();
|
||||
let mut fields = BTreeSet::new();
|
||||
while let Some(owner) = pending.pop() {
|
||||
if !seen.insert(owner) {
|
||||
continue;
|
||||
}
|
||||
for field in p.children(owner) {
|
||||
if let Some(SharedBody::DataField { type_ref, .. }) = field.shared() {
|
||||
fields.insert(field.id);
|
||||
if let TypeRef::Named(id) = type_ref {
|
||||
pending.push(*id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
fields
|
||||
crate::data::carried_field_paths(p, relation)
|
||||
.into_iter()
|
||||
.map(|f| f.field)
|
||||
.collect()
|
||||
}
|
||||
fn occurrence_uses_lifeline(body: &OccurrenceBody, line: Id) -> bool {
|
||||
match body {
|
||||
@@ -1683,36 +1614,125 @@ fn validate_message(p: &Project, e: &Element, body: &OccurrenceBody, out: &mut V
|
||||
}
|
||||
}
|
||||
}
|
||||
let allowed = exchange
|
||||
.and_then(|id| p.relations.get(&id))
|
||||
.map(|r| carried_fields(p, r));
|
||||
let relation = exchange.and_then(|id| p.relations.get(&id));
|
||||
if message_kind == &MessageKind::Call
|
||||
&& relation.is_some_and(|r| {
|
||||
!r.body
|
||||
.items()
|
||||
.iter()
|
||||
.any(|id| crate::data::item_mechanism(p, *id) == Some(ExchangeMechanism::Operation))
|
||||
})
|
||||
{
|
||||
warning(
|
||||
out,
|
||||
e.id,
|
||||
"call_without_operation",
|
||||
"exchange",
|
||||
"This generic call carries no Operation item; define an operation contract when appropriate.",
|
||||
);
|
||||
}
|
||||
let mut fields = BTreeSet::new();
|
||||
for value in values {
|
||||
expect_element(p, out, e.id, value.field, "values.field", |kind| {
|
||||
kind == Kind::DataField
|
||||
});
|
||||
for hop in &value.path {
|
||||
expect_element(p, out, e.id, *hop, "values.path", |kind| {
|
||||
kind == Kind::DataField
|
||||
});
|
||||
}
|
||||
text(out, e.id, "values.expression", &value.expression);
|
||||
if !fields.insert(value.field) {
|
||||
if !fields.insert((&value.path, value.field)) {
|
||||
error(
|
||||
out,
|
||||
e.id,
|
||||
"duplicate_message_value",
|
||||
"values",
|
||||
"Each carried field has at most one contextual value annotation in a message.",
|
||||
"Each qualified carried field has at most one contextual value annotation.",
|
||||
);
|
||||
}
|
||||
if allowed
|
||||
.as_ref()
|
||||
.is_some_and(|allowed| !allowed.contains(&value.field))
|
||||
{
|
||||
if value.path.len() > crate::data::MAX_VALUE_PATH_DEPTH {
|
||||
error(
|
||||
out,
|
||||
e.id,
|
||||
"message_value_field",
|
||||
"values.field",
|
||||
"Value annotations must reference fields carried by this message's exchange, including nested record fields.",
|
||||
"message_value_path_limit",
|
||||
"values.path",
|
||||
"The value path exceeds the bounded authoring depth.",
|
||||
);
|
||||
}
|
||||
if let Some(r) = relation {
|
||||
if let Some(path) = crate::data::carried_value_path(p, r, &value.path, value.field) {
|
||||
if crate::data::item_mechanism(p, path.item) == Some(ExchangeMechanism::Operation) {
|
||||
let permitted = match message_kind {
|
||||
MessageKind::Call => matches!(
|
||||
path.direction,
|
||||
ParameterDirection::Unspecified
|
||||
| ParameterDirection::In
|
||||
| ParameterDirection::InOut
|
||||
),
|
||||
MessageKind::Reply => matches!(
|
||||
path.direction,
|
||||
ParameterDirection::Unspecified
|
||||
| ParameterDirection::Out
|
||||
| ParameterDirection::InOut
|
||||
| ParameterDirection::Return
|
||||
),
|
||||
_ => true,
|
||||
};
|
||||
if !permitted {
|
||||
error(
|
||||
out,
|
||||
e.id,
|
||||
"message_parameter_phase",
|
||||
"values.path",
|
||||
"Call values address In/InOut parameters; replies address Out/InOut/Return parameters.",
|
||||
);
|
||||
}
|
||||
if path.direction == ParameterDirection::Unspecified {
|
||||
warning(
|
||||
out,
|
||||
e.id,
|
||||
"message_parameter_direction",
|
||||
"values.path",
|
||||
"The containing operation parameter has no direction yet.",
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let legacy = value
|
||||
.path
|
||||
.is_empty()
|
||||
.then(|| crate::data::resolve_legacy_field_path(p, r, value.field).ok())
|
||||
.flatten();
|
||||
if matches!(
|
||||
legacy,
|
||||
Some(
|
||||
crate::data::LegacyFieldPathResolution::Unique(_)
|
||||
| crate::data::LegacyFieldPathResolution::Ambiguous
|
||||
)
|
||||
) {
|
||||
warning(
|
||||
out,
|
||||
e.id,
|
||||
if legacy == Some(crate::data::LegacyFieldPathResolution::Ambiguous) {
|
||||
"message_value_ambiguous_path"
|
||||
} else {
|
||||
"message_value_missing_path"
|
||||
},
|
||||
"values.path",
|
||||
"This legacy nested field annotation needs an explicit containing parameter path; its expression is preserved.",
|
||||
);
|
||||
} else {
|
||||
error(
|
||||
out,
|
||||
e.id,
|
||||
"message_value_field",
|
||||
"values.path",
|
||||
"The qualified field must be carried by the exchange, following Value record fields only.",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
if value.expression.trim().is_empty() {
|
||||
warning(
|
||||
out,
|
||||
@@ -2130,7 +2150,11 @@ fn validate_relation(p: &Project, r: &Relation, out: &mut Vec<Issue>) {
|
||||
};
|
||||
if a.layer() != b.layer()
|
||||
|| !r.kind().permits(a.kind(), b.kind())
|
||||
|| (a.id == b.id && r.kind() != RelationKind::StateTransition)
|
||||
|| (a.id == b.id
|
||||
&& !matches!(
|
||||
r.kind(),
|
||||
RelationKind::StateTransition | RelationKind::DataAssociation
|
||||
))
|
||||
{
|
||||
error(
|
||||
out,
|
||||
@@ -2381,8 +2405,10 @@ pub fn can_parent(p: &Project, child_kind: Kind, layer: Layer, parent: Option<Id
|
||||
owner.layer() == layer
|
||||
&& parent_permits(owner.kind(), child_kind)
|
||||
&& match owner.shared() {
|
||||
Some(SharedBody::DataType { definition }) if child_kind == Kind::DataField => {
|
||||
*definition == DataTypeDefinition::Record
|
||||
Some(SharedBody::DataType { definition, .. })
|
||||
if child_kind == Kind::DataField =>
|
||||
{
|
||||
matches!(definition, DataTypeDefinition::Record { .. })
|
||||
}
|
||||
Some(SharedBody::Occurrence { body, .. }) if child_kind == Kind::Occurrence => {
|
||||
matches!(
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -447,11 +447,12 @@ fn standard_interface_roles_do_not_follow_message_direction_and_kind_mixing_is_r
|
||||
p = execute_batch(&p, &plan_component_exchange(&p, &req).unwrap()).unwrap();
|
||||
let (source, target) = p.relations[&boundary].body.endpoints();
|
||||
let interface = add(&mut p, Kind::Interface, Layer::Sa, "Service");
|
||||
*p.elements
|
||||
.get_mut(&interface)
|
||||
.unwrap()
|
||||
.shared_mut()
|
||||
.unwrap() = SharedBody::Interface { items: vec![item] };
|
||||
p.relate(
|
||||
RelationKind::InterfaceItemAllocation,
|
||||
"Interface payload",
|
||||
interface,
|
||||
item,
|
||||
);
|
||||
let requirement = p.relate(
|
||||
RelationKind::InterfaceRequirement,
|
||||
"Caller requires service",
|
||||
@@ -499,11 +500,12 @@ fn legacy_split_preserves_original_identity_prose_interface_artifact_constraint_
|
||||
let (mut p, a, b, item, forward) = fixture();
|
||||
let (old, _) = endpoints(&p, forward);
|
||||
let interface = add(&mut p, Kind::Interface, Layer::Sa, "Original interface");
|
||||
*p.elements
|
||||
.get_mut(&interface)
|
||||
.unwrap()
|
||||
.shared_mut()
|
||||
.unwrap() = SharedBody::Interface { items: vec![item] };
|
||||
p.relate(
|
||||
RelationKind::InterfaceItemAllocation,
|
||||
"Interface payload",
|
||||
interface,
|
||||
item,
|
||||
);
|
||||
p.elements.get_mut(&old).unwrap().description = "Original bidirectional rationale".into();
|
||||
*p.elements.get_mut(&old).unwrap().shared_mut().unwrap() = SharedBody::Port {
|
||||
contract: PortContract::Legacy,
|
||||
@@ -659,11 +661,12 @@ fn directed_legacy_resolution_retains_unclassified_interface_and_role_identity()
|
||||
let (mut p, _, _, item, fe) = fixture();
|
||||
let output = endpoints(&p, fe).0;
|
||||
let interface = add(&mut p, Kind::Interface, Layer::Sa, "Original service");
|
||||
*p.elements
|
||||
.get_mut(&interface)
|
||||
.unwrap()
|
||||
.shared_mut()
|
||||
.unwrap() = SharedBody::Interface { items: vec![item] };
|
||||
p.relate(
|
||||
RelationKind::InterfaceItemAllocation,
|
||||
"Interface payload",
|
||||
interface,
|
||||
item,
|
||||
);
|
||||
*p.elements.get_mut(&output).unwrap().shared_mut().unwrap() = SharedBody::Port {
|
||||
contract: PortContract::Legacy,
|
||||
legacy: Some(LegacyPortContract {
|
||||
|
||||
@@ -94,7 +94,9 @@ fn fixture(layer: Layer) -> Fixture {
|
||||
.add_element(Kind::DataType, layer, "Request record", None)
|
||||
.unwrap();
|
||||
*p.elements.get_mut(&record).unwrap().shared_mut().unwrap() = SharedBody::DataType {
|
||||
definition: DataTypeDefinition::Record,
|
||||
definition: DataTypeDefinition::Record { supertype: None },
|
||||
default: None,
|
||||
unit: String::new(),
|
||||
};
|
||||
let field = p
|
||||
.add_element(Kind::DataField, layer, "Requested level", Some(record))
|
||||
@@ -160,6 +162,7 @@ fn fixture(layer: Layer) -> Fixture {
|
||||
}) = p.elements.get_mut(&call).unwrap().shared_mut()
|
||||
{
|
||||
values.push(MessageValue {
|
||||
path: vec![payload],
|
||||
field,
|
||||
expression: "requested_level".into(),
|
||||
});
|
||||
|
||||
@@ -328,11 +328,12 @@ fn items_ports_interfaces_and_exchange_allocation_are_typed() {
|
||||
let f2 = add(&mut p, Kind::Function, Layer::Sa, "Receive", None);
|
||||
let item = add(&mut p, Kind::ExchangeItem, Layer::Sa, "Message", None);
|
||||
let interface = add(&mut p, Kind::Interface, Layer::Sa, "Link interface", None);
|
||||
*p.elements
|
||||
.get_mut(&interface)
|
||||
.unwrap()
|
||||
.shared_mut()
|
||||
.unwrap() = SharedBody::Interface { items: vec![item] };
|
||||
p.relate(
|
||||
RelationKind::InterfaceItemAllocation,
|
||||
"Interface payload",
|
||||
interface,
|
||||
item,
|
||||
);
|
||||
let port = |p: &mut Project, name: &str, parent, direction| {
|
||||
let id = Id::new_v4();
|
||||
let contract = if p.elements[&parent].kind() == Kind::Function {
|
||||
@@ -740,6 +741,9 @@ fn reidentifying_data_types_and_function_ports_updates_every_typed_reference() {
|
||||
minimum: 0,
|
||||
maximum: None,
|
||||
unit: "items".into(),
|
||||
direction: ParameterDirection::Unspecified,
|
||||
storage: FieldStorage::Value,
|
||||
default: None,
|
||||
};
|
||||
let function = add(&mut p, Kind::Function, Layer::Sa, "Function", None);
|
||||
let other = add(&mut p, Kind::Function, Layer::Sa, "Other", None);
|
||||
@@ -804,8 +808,40 @@ fn every_demo_identity_can_be_remapped_and_deleted_without_dangling_references()
|
||||
"Reidentification must preserve all values and order"
|
||||
);
|
||||
if id != p.id {
|
||||
let deleted = execute_batch(&p, &[Command::Delete(id)])
|
||||
.unwrap_or_else(|err| panic!("Delete {id}: {err}"));
|
||||
let deleted = if p
|
||||
.relations
|
||||
.get(&id)
|
||||
.is_some_and(|r| r.kind() == RelationKind::InterfaceItemAllocation)
|
||||
{
|
||||
// An explicitly empty interface cannot silently accept the
|
||||
// still-carried payload. Removing its definition needs a
|
||||
// reviewed edit of that dependent contract in the same batch.
|
||||
let mut session = Session::new(p.clone());
|
||||
assert!(
|
||||
session
|
||||
.execute(Command::Delete(id))
|
||||
.unwrap_err()
|
||||
.0
|
||||
.contains("port_interface_items")
|
||||
);
|
||||
assert_eq!(session.project(), &p);
|
||||
assert_eq!(session.revision(), 0);
|
||||
let item = p.relations[&id].target();
|
||||
let mut commands = vec![Command::Delete(id)];
|
||||
commands.extend(
|
||||
p.relations
|
||||
.values()
|
||||
.filter(|r| {
|
||||
r.kind() == RelationKind::BoundaryExchange
|
||||
&& r.body.items().contains(&item)
|
||||
})
|
||||
.map(|r| Command::Delete(r.id)),
|
||||
);
|
||||
execute_batch(&p, &commands).unwrap()
|
||||
} else {
|
||||
execute_batch(&p, &[Command::Delete(id)])
|
||||
.unwrap_or_else(|err| panic!("Delete {id}: {err}"))
|
||||
};
|
||||
accepted(&deleted);
|
||||
}
|
||||
}
|
||||
@@ -890,13 +926,15 @@ fn demo_has_both_layers_complete_contracts_and_no_checks() {
|
||||
assert!(p.elements.values().any(|e| matches!(
|
||||
e.shared(),
|
||||
Some(SharedBody::ExchangeItem {
|
||||
kind: ExchangeItemKind::Material
|
||||
kind: ExchangeItemKind::Material,
|
||||
..
|
||||
})
|
||||
)));
|
||||
assert!(p.elements.values().any(|e| matches!(
|
||||
e.shared(),
|
||||
Some(SharedBody::ExchangeItem {
|
||||
kind: ExchangeItemKind::Energy
|
||||
kind: ExchangeItemKind::Energy,
|
||||
..
|
||||
})
|
||||
)));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user