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
|
id
|
||||||
}
|
}
|
||||||
fn port(p: &mut Project, owner: Id, name: &str, direction: PortDirection, interface: Id) -> Id {
|
fn port(p: &mut Project, owner: Id, name: &str, direction: PortDirection, interface: Id) -> Id {
|
||||||
let items = match p.elements[&interface].shared() {
|
let items = crate::data::interface_items(p, interface);
|
||||||
Some(SharedBody::Interface { items }) => items.clone(),
|
|
||||||
_ => vec![],
|
|
||||||
};
|
|
||||||
let contract = if p.elements[&owner].kind() == Kind::Function {
|
let contract = if p.elements[&owner].kind() == Kind::Function {
|
||||||
if direction == PortDirection::Out {
|
if direction == PortDirection::Out {
|
||||||
PortContract::FunctionOutput {
|
PortContract::FunctionOutput {
|
||||||
@@ -168,6 +165,11 @@ pub fn coastal_rescue() -> Project {
|
|||||||
"Distress report",
|
"Distress report",
|
||||||
None,
|
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, "Reports distress", vessel, report);
|
||||||
p.relate(
|
p.relate(
|
||||||
RelationKind::Allocation,
|
RelationKind::Allocation,
|
||||||
@@ -308,6 +310,11 @@ pub fn coastal_rescue() -> Project {
|
|||||||
"Digital distress report",
|
"Digital distress report",
|
||||||
None,
|
None,
|
||||||
);
|
);
|
||||||
|
if let Some(SharedBody::ExchangeItem { mechanism, .. }) =
|
||||||
|
p.elements.get_mut(&information).unwrap().shared_mut()
|
||||||
|
{
|
||||||
|
*mechanism = ExchangeMechanism::Event;
|
||||||
|
}
|
||||||
let material = add(
|
let material = add(
|
||||||
&mut p,
|
&mut p,
|
||||||
Kind::ExchangeItem,
|
Kind::ExchangeItem,
|
||||||
@@ -317,6 +324,8 @@ pub fn coastal_rescue() -> Project {
|
|||||||
);
|
);
|
||||||
*p.elements.get_mut(&material).unwrap().shared_mut().unwrap() = SharedBody::ExchangeItem {
|
*p.elements.get_mut(&material).unwrap().shared_mut().unwrap() = SharedBody::ExchangeItem {
|
||||||
kind: ExchangeItemKind::Material,
|
kind: ExchangeItemKind::Material,
|
||||||
|
mechanism: ExchangeMechanism::Flow,
|
||||||
|
supertype: None,
|
||||||
};
|
};
|
||||||
let energy = add(
|
let energy = add(
|
||||||
&mut p,
|
&mut p,
|
||||||
@@ -327,6 +336,8 @@ pub fn coastal_rescue() -> Project {
|
|||||||
);
|
);
|
||||||
*p.elements.get_mut(&energy).unwrap().shared_mut().unwrap() = SharedBody::ExchangeItem {
|
*p.elements.get_mut(&energy).unwrap().shared_mut().unwrap() = SharedBody::ExchangeItem {
|
||||||
kind: ExchangeItemKind::Energy,
|
kind: ExchangeItemKind::Energy,
|
||||||
|
mechanism: ExchangeMechanism::Flow,
|
||||||
|
supertype: None,
|
||||||
};
|
};
|
||||||
let field = add(
|
let field = add(
|
||||||
&mut p,
|
&mut p,
|
||||||
@@ -340,6 +351,9 @@ pub fn coastal_rescue() -> Project {
|
|||||||
minimum: 1,
|
minimum: 1,
|
||||||
maximum: Some(1),
|
maximum: Some(1),
|
||||||
unit: "degrees north".into(),
|
unit: "degrees north".into(),
|
||||||
|
direction: ParameterDirection::Unspecified,
|
||||||
|
storage: FieldStorage::Value,
|
||||||
|
default: None,
|
||||||
};
|
};
|
||||||
let interface = add(
|
let interface = add(
|
||||||
&mut p,
|
&mut p,
|
||||||
@@ -348,13 +362,12 @@ pub fn coastal_rescue() -> Project {
|
|||||||
"Distress reporting interface",
|
"Distress reporting interface",
|
||||||
None,
|
None,
|
||||||
);
|
);
|
||||||
*p.elements
|
p.relate(
|
||||||
.get_mut(&interface)
|
RelationKind::InterfaceItemAllocation,
|
||||||
.unwrap()
|
"Distress payload",
|
||||||
.shared_mut()
|
interface,
|
||||||
.unwrap() = SharedBody::Interface {
|
information,
|
||||||
items: vec![information],
|
);
|
||||||
};
|
|
||||||
let vessel_port = port(
|
let vessel_port = port(
|
||||||
&mut p,
|
&mut p,
|
||||||
actor,
|
actor,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
//! Framework-independent OA/SA model, transactional commands and editing services.
|
//! Framework-independent OA/SA model, transactional commands and editing services.
|
||||||
|
pub mod data;
|
||||||
pub mod language;
|
pub mod language;
|
||||||
pub mod model;
|
pub mod model;
|
||||||
pub mod persistence;
|
pub mod persistence;
|
||||||
|
|||||||
+149
-14
@@ -6,7 +6,7 @@ use std::{
|
|||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
pub type Id = 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.;
|
pub const MAX_COORDINATE: f32 = 1_000_000.;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
|
||||||
@@ -265,17 +265,79 @@ pub enum ExchangeItemKind {
|
|||||||
Information,
|
Information,
|
||||||
Material,
|
Material,
|
||||||
Energy,
|
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)]
|
#[serde(deny_unknown_fields)]
|
||||||
pub enum DataTypeDefinition {
|
pub enum DataTypeDefinition {
|
||||||
Boolean,
|
Boolean,
|
||||||
Integer,
|
Integer {
|
||||||
Real,
|
minimum: Option<i64>,
|
||||||
String,
|
maximum: Option<i64>,
|
||||||
Enumeration { literals: Vec<String> },
|
},
|
||||||
Record,
|
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)]
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
#[serde(deny_unknown_fields)]
|
#[serde(deny_unknown_fields)]
|
||||||
@@ -453,6 +515,8 @@ pub struct LifelineBinding {
|
|||||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
#[serde(deny_unknown_fields)]
|
#[serde(deny_unknown_fields)]
|
||||||
pub struct MessageValue {
|
pub struct MessageValue {
|
||||||
|
#[serde(default)]
|
||||||
|
pub path: Vec<Id>,
|
||||||
pub field: Id,
|
pub field: Id,
|
||||||
pub expression: String,
|
pub expression: String,
|
||||||
}
|
}
|
||||||
@@ -502,18 +566,25 @@ pub enum OccurrenceBody {
|
|||||||
pub enum SharedBody {
|
pub enum SharedBody {
|
||||||
ExchangeItem {
|
ExchangeItem {
|
||||||
kind: ExchangeItemKind,
|
kind: ExchangeItemKind,
|
||||||
|
mechanism: ExchangeMechanism,
|
||||||
|
supertype: Option<Id>,
|
||||||
},
|
},
|
||||||
DataType {
|
DataType {
|
||||||
definition: DataTypeDefinition,
|
definition: DataTypeDefinition,
|
||||||
|
default: Option<DataValue>,
|
||||||
|
unit: String,
|
||||||
},
|
},
|
||||||
DataField {
|
DataField {
|
||||||
type_ref: TypeRef,
|
type_ref: TypeRef,
|
||||||
minimum: u32,
|
minimum: u32,
|
||||||
maximum: Option<u32>,
|
maximum: Option<u32>,
|
||||||
unit: String,
|
unit: String,
|
||||||
|
direction: ParameterDirection,
|
||||||
|
storage: FieldStorage,
|
||||||
|
default: Option<Vec<DataValue>>,
|
||||||
},
|
},
|
||||||
Interface {
|
Interface {
|
||||||
items: Vec<Id>,
|
supertype: Option<Id>,
|
||||||
},
|
},
|
||||||
Port {
|
Port {
|
||||||
contract: PortContract,
|
contract: PortContract,
|
||||||
@@ -672,17 +743,27 @@ impl ElementBody {
|
|||||||
match k {
|
match k {
|
||||||
Kind::ExchangeItem => SharedBody::ExchangeItem {
|
Kind::ExchangeItem => SharedBody::ExchangeItem {
|
||||||
kind: ExchangeItemKind::Information,
|
kind: ExchangeItemKind::Information,
|
||||||
|
mechanism: ExchangeMechanism::Unspecified,
|
||||||
|
supertype: None,
|
||||||
},
|
},
|
||||||
Kind::DataType => SharedBody::DataType {
|
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 {
|
Kind::DataField => SharedBody::DataField {
|
||||||
type_ref: TypeRef::String,
|
type_ref: TypeRef::String,
|
||||||
minimum: 1,
|
minimum: 1,
|
||||||
maximum: Some(1),
|
maximum: Some(1),
|
||||||
unit: String::new(),
|
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.
|
// Port creation requires an explicit contract and owner.
|
||||||
Kind::Port => return None,
|
Kind::Port => return None,
|
||||||
Kind::Scenario => SharedBody::Scenario {
|
Kind::Scenario => SharedBody::Scenario {
|
||||||
@@ -752,6 +833,8 @@ impl SharedBody {
|
|||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
pub enum RelationKind {
|
pub enum RelationKind {
|
||||||
|
InterfaceItemAllocation,
|
||||||
|
DataAssociation,
|
||||||
Allocation,
|
Allocation,
|
||||||
PortAllocation,
|
PortAllocation,
|
||||||
Interaction,
|
Interaction,
|
||||||
@@ -775,7 +858,9 @@ pub enum RelationKind {
|
|||||||
StateAction,
|
StateAction,
|
||||||
}
|
}
|
||||||
impl RelationKind {
|
impl RelationKind {
|
||||||
pub const ALL: [Self; 21] = [
|
pub const ALL: [Self; 23] = [
|
||||||
|
Self::InterfaceItemAllocation,
|
||||||
|
Self::DataAssociation,
|
||||||
Self::Allocation,
|
Self::Allocation,
|
||||||
Self::PortAllocation,
|
Self::PortAllocation,
|
||||||
Self::Interaction,
|
Self::Interaction,
|
||||||
@@ -800,6 +885,8 @@ impl RelationKind {
|
|||||||
];
|
];
|
||||||
pub fn permits(self, from: Kind, to: Kind) -> bool {
|
pub fn permits(self, from: Kind, to: Kind) -> bool {
|
||||||
match self {
|
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::Allocation => from.is_performer() && to.is_work(),
|
||||||
Self::PortAllocation => from == Kind::Port && to == Kind::Port,
|
Self::PortAllocation => from == Kind::Port && to == Kind::Port,
|
||||||
Self::Interaction => from == Kind::Activity && to == Kind::Activity,
|
Self::Interaction => from == Kind::Activity && to == Kind::Activity,
|
||||||
@@ -855,6 +942,8 @@ impl RelationKind {
|
|||||||
}
|
}
|
||||||
pub fn hint(self) -> &'static str {
|
pub fn hint(self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
|
Self::InterfaceItemAllocation => "Interface → allocated exchange item",
|
||||||
|
Self::DataAssociation => "Record type → record type",
|
||||||
Self::Allocation => "Performer → leaf activity/function",
|
Self::Allocation => "Performer → leaf activity/function",
|
||||||
Self::PortAllocation => "Component port → allocated function port",
|
Self::PortAllocation => "Component port → allocated function port",
|
||||||
Self::Interaction => "OA activity → OA activity",
|
Self::Interaction => "OA activity → OA activity",
|
||||||
@@ -886,6 +975,8 @@ impl RelationKind {
|
|||||||
impl fmt::Display for RelationKind {
|
impl fmt::Display for RelationKind {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
f.write_str(match self {
|
f.write_str(match self {
|
||||||
|
Self::InterfaceItemAllocation => "Interface item allocation",
|
||||||
|
Self::DataAssociation => "Data association",
|
||||||
Self::Allocation => "Allocation",
|
Self::Allocation => "Allocation",
|
||||||
Self::PortAllocation => "Port allocation",
|
Self::PortAllocation => "Port allocation",
|
||||||
Self::Interaction => "Interaction",
|
Self::Interaction => "Interaction",
|
||||||
@@ -922,6 +1013,25 @@ pub struct Relation {
|
|||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
#[serde(deny_unknown_fields)]
|
#[serde(deny_unknown_fields)]
|
||||||
pub enum RelationBody {
|
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 {
|
Allocation {
|
||||||
source: Id,
|
source: Id,
|
||||||
target: Id,
|
target: Id,
|
||||||
@@ -1039,6 +1149,8 @@ impl Relation {
|
|||||||
impl RelationBody {
|
impl RelationBody {
|
||||||
pub fn kind(&self) -> RelationKind {
|
pub fn kind(&self) -> RelationKind {
|
||||||
match self {
|
match self {
|
||||||
|
Self::InterfaceItemAllocation { .. } => RelationKind::InterfaceItemAllocation,
|
||||||
|
Self::DataAssociation { .. } => RelationKind::DataAssociation,
|
||||||
Self::Allocation { .. } => RelationKind::Allocation,
|
Self::Allocation { .. } => RelationKind::Allocation,
|
||||||
Self::PortAllocation { .. } => RelationKind::PortAllocation,
|
Self::PortAllocation { .. } => RelationKind::PortAllocation,
|
||||||
Self::Interaction { .. } => RelationKind::Interaction,
|
Self::Interaction { .. } => RelationKind::Interaction,
|
||||||
@@ -1064,7 +1176,9 @@ impl RelationBody {
|
|||||||
}
|
}
|
||||||
pub fn endpoints(&self) -> (Id, Id) {
|
pub fn endpoints(&self) -> (Id, Id) {
|
||||||
match self {
|
match self {
|
||||||
Self::Allocation { source, target }
|
Self::InterfaceItemAllocation { source, target, .. }
|
||||||
|
| Self::DataAssociation { source, target, .. }
|
||||||
|
| Self::Allocation { source, target }
|
||||||
| Self::PortAllocation { source, target }
|
| Self::PortAllocation { source, target }
|
||||||
| Self::Interaction { source, target, .. }
|
| Self::Interaction { source, target, .. }
|
||||||
| Self::CapabilityInvolvement { source, target }
|
| Self::CapabilityInvolvement { source, target }
|
||||||
@@ -1089,7 +1203,9 @@ impl RelationBody {
|
|||||||
}
|
}
|
||||||
pub fn set_endpoints(&mut self, a: Id, b: Id) {
|
pub fn set_endpoints(&mut self, a: Id, b: Id) {
|
||||||
match self {
|
match self {
|
||||||
Self::Allocation { source, target }
|
Self::InterfaceItemAllocation { source, target, .. }
|
||||||
|
| Self::DataAssociation { source, target, .. }
|
||||||
|
| Self::Allocation { source, target }
|
||||||
| Self::PortAllocation { source, target }
|
| Self::PortAllocation { source, target }
|
||||||
| Self::Interaction { source, target, .. }
|
| Self::Interaction { source, target, .. }
|
||||||
| Self::CapabilityInvolvement { source, target }
|
| Self::CapabilityInvolvement { source, target }
|
||||||
@@ -1125,6 +1241,25 @@ impl RelationBody {
|
|||||||
}
|
}
|
||||||
pub fn new(kind: RelationKind, source: Id, target: Id) -> Self {
|
pub fn new(kind: RelationKind, source: Id, target: Id) -> Self {
|
||||||
match kind {
|
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::Allocation => Self::Allocation { source, target },
|
||||||
RelationKind::PortAllocation => Self::PortAllocation { source, target },
|
RelationKind::PortAllocation => Self::PortAllocation { source, target },
|
||||||
RelationKind::Interaction => Self::Interaction {
|
RelationKind::Interaction => Self::Interaction {
|
||||||
|
|||||||
@@ -64,9 +64,9 @@ pub struct LegacyWorkPortSplitRequest {
|
|||||||
pub outgoing_items: Vec<Id>,
|
pub outgoing_items: Vec<Id>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// R6 specialization has one explicit seam; current models support identity only.
|
/// A specialized carried item satisfies a declared base item.
|
||||||
pub fn item_satisfies(_project: &Project, carried: Id, declared: Id) -> bool {
|
pub fn item_satisfies(project: &Project, carried: Id, declared: Id) -> bool {
|
||||||
carried == declared
|
crate::data::item_satisfies(project, carried, declared)
|
||||||
}
|
}
|
||||||
pub fn items_satisfy(project: &Project, carried: &[Id], declared: &[Id]) -> bool {
|
pub fn items_satisfy(project: &Project, carried: &[Id], declared: &[Id]) -> bool {
|
||||||
carried.iter().all(|item| {
|
carried.iter().all(|item| {
|
||||||
@@ -130,13 +130,7 @@ fn declared_interface_items(p: &Project, port: Id) -> Option<Vec<Id>> {
|
|||||||
Some(
|
Some(
|
||||||
interfaces
|
interfaces
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter_map(|id| p.elements.get(&id))
|
.flat_map(|id| crate::data::interface_items(p, id))
|
||||||
.filter_map(|e| match e.shared() {
|
|
||||||
Some(SharedBody::Interface { items }) => Some(items),
|
|
||||||
_ => None,
|
|
||||||
})
|
|
||||||
.flatten()
|
|
||||||
.copied()
|
|
||||||
.collect(),
|
.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);
|
let mut allowed = declared_interface_items(p, port.id);
|
||||||
if let Some(SharedBody::Interface { items }) = legacy
|
if let Some(interface) = legacy.as_ref().and_then(|l| l.interface) {
|
||||||
.as_ref()
|
let items = crate::data::interface_items(p, interface);
|
||||||
.and_then(|l| l.interface)
|
|
||||||
.and_then(|id| p.elements.get(&id))
|
|
||||||
.and_then(Element::shared)
|
|
||||||
{
|
|
||||||
if r.kind() == RelationKind::BoundaryExchange
|
if r.kind() == RelationKind::BoundaryExchange
|
||||||
&& !items_satisfy(p, r.body.items(), items)
|
&& !items_satisfy(p, r.body.items(), &items)
|
||||||
{
|
{
|
||||||
error(
|
error(
|
||||||
out,
|
out,
|
||||||
@@ -1173,7 +1163,7 @@ pub(crate) fn validate_boundary_interface_requirements(
|
|||||||
if !p.relations.values().any(|role| {
|
if !p.relations.values().any(|role| {
|
||||||
role.kind() == RelationKind::InterfaceProvision
|
role.kind() == RelationKind::InterfaceProvision
|
||||||
&& role.source() == peer.id
|
&& role.source() == peer.id
|
||||||
&& role.target() == required.target()
|
&& crate::data::interface_satisfies(p, role.target(), required.target())
|
||||||
}) {
|
}) {
|
||||||
warning(
|
warning(
|
||||||
out,
|
out,
|
||||||
|
|||||||
@@ -335,6 +335,11 @@ pub fn project_diagram(p: &Project, d: &Diagram) -> ProjectedView {
|
|||||||
.collect();
|
.collect();
|
||||||
let mut edges = Vec::new();
|
let mut edges = Vec::new();
|
||||||
for r in p.relations.values() {
|
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 {
|
if d.projection == ProjectionKind::Allocation && r.kind() == RelationKind::Allocation {
|
||||||
continue;
|
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()))
|
if let (Some(source), Some(target)) = (anchor(r.source()), anchor(r.target()))
|
||||||
&& (source != target
|
&& (source != target
|
||||||
|| (d.projection.state_machine().is_some()
|
|| ((d.projection.state_machine().is_some()
|
||||||
|
|| r.kind() == RelationKind::DataAssociation)
|
||||||
&& r.source() == r.target()
|
&& r.source() == r.target()
|
||||||
&& nodes
|
&& nodes
|
||||||
.iter()
|
.iter()
|
||||||
@@ -396,7 +402,7 @@ pub fn project_diagram(p: &Project, d: &Diagram) -> ProjectedView {
|
|||||||
RelationBody::BoundaryExchange {
|
RelationBody::BoundaryExchange {
|
||||||
oriented: false,
|
oriented: false,
|
||||||
..
|
..
|
||||||
}
|
} | RelationBody::DataAssociation { .. }
|
||||||
),
|
),
|
||||||
source_port: source_port.filter(|id| visible_ports.contains(id)),
|
source_port: source_port.filter(|id| visible_ports.contains(id)),
|
||||||
target_port: target_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),
|
type_ref: TypeRef::Named(id),
|
||||||
..
|
..
|
||||||
} => visit(id),
|
} => visit(id),
|
||||||
SharedBody::Interface { items } => {
|
SharedBody::Interface { supertype }
|
||||||
for id in items {
|
| SharedBody::ExchangeItem { supertype, .. }
|
||||||
|
| SharedBody::DataType {
|
||||||
|
definition: DataTypeDefinition::Record { supertype },
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
if let Some(id) = supertype {
|
||||||
visit(id);
|
visit(id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -83,6 +88,9 @@ pub fn visit_element_refs(e: &mut Element, mut visit: impl FnMut(&mut Id)) {
|
|||||||
visit(id);
|
visit(id);
|
||||||
}
|
}
|
||||||
for value in values {
|
for value in values {
|
||||||
|
for id in &mut value.path {
|
||||||
|
visit(id);
|
||||||
|
}
|
||||||
visit(&mut value.field);
|
visit(&mut value.field);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -377,7 +385,12 @@ fn remove_objects(p: &mut Project, removed: BTreeSet<Id>) {
|
|||||||
}
|
}
|
||||||
if let Some(shared) = e.shared_mut() {
|
if let Some(shared) = e.shared_mut() {
|
||||||
match shared {
|
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 {
|
SharedBody::Event {
|
||||||
body: EventBody::Signal { items },
|
body: EventBody::Signal { items },
|
||||||
} => items.retain(|id| !removed.contains(id)),
|
} => items.retain(|id| !removed.contains(id)),
|
||||||
@@ -413,7 +426,10 @@ fn remove_objects(p: &mut Project, removed: BTreeSet<Id>) {
|
|||||||
clear(sender);
|
clear(sender);
|
||||||
clear(receiver);
|
clear(receiver);
|
||||||
clear(reply_to);
|
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 } => {
|
OccurrenceBody::Action { action, lifeline } => {
|
||||||
clear(action);
|
clear(action);
|
||||||
@@ -485,20 +501,37 @@ fn remove_objects(p: &mut Project, removed: BTreeSet<Id>) {
|
|||||||
exchange: Some(id), ..
|
exchange: Some(id), ..
|
||||||
},
|
},
|
||||||
..
|
..
|
||||||
}) => p
|
}) => p.relations.get(id).map(|r| {
|
||||||
.relations
|
let Some(SharedBody::Occurrence {
|
||||||
.get(id)
|
body: OccurrenceBody::Exchange { values, .. },
|
||||||
.map(|r| (e.id, crate::validation::carried_fields(p, r))),
|
..
|
||||||
|
}) = 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,
|
_ => None,
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
for (id, fields) in allowed {
|
for (id, retained) in allowed {
|
||||||
if let Some(SharedBody::Occurrence {
|
if let Some(SharedBody::Occurrence {
|
||||||
body: OccurrenceBody::Exchange { values, .. },
|
body: OccurrenceBody::Exchange { values, .. },
|
||||||
..
|
..
|
||||||
}) = p.elements.get_mut(&id).and_then(Element::shared_mut)
|
}) = 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;
|
//! Deterministic, bounded diagram geometry. Only authored pins belong in the model;
|
||||||
//! callers share this derived geometry for drawing, picking, fitting and export.
|
//! callers share this derived geometry for drawing, picking, fitting and export.
|
||||||
use crate::projection::{self, ProjectedEdge, ProjectedNode, ProjectedView};
|
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::cmp::Ordering;
|
||||||
use std::collections::{BTreeMap, BTreeSet, BinaryHeap};
|
use std::collections::{BTreeMap, BTreeSet, BinaryHeap};
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
@@ -55,6 +55,122 @@ pub struct EdgeLabelGeometry {
|
|||||||
pub bounds: RoutingBounds,
|
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)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub enum RouteWarning {
|
pub enum RouteWarning {
|
||||||
PinInsideObstacle { pin: usize, obstacle: Id },
|
PinInsideObstacle { pin: usize, obstacle: Id },
|
||||||
@@ -466,6 +582,14 @@ fn route_diagram_impl(
|
|||||||
// never changes paths or authored metadata. Previews retain other cached
|
// never changes paths or authored metadata. Previews retain other cached
|
||||||
// labels and avoid the paths actually shown during the gesture.
|
// labels and avoid the paths actually shown during the gesture.
|
||||||
let mut previous_labels = Vec::new();
|
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 {
|
for edge in ordered {
|
||||||
let Some(routed) = edges.get_mut(&edge.id) else {
|
let Some(routed) = edges.get_mut(&edge.id) else {
|
||||||
continue;
|
continue;
|
||||||
@@ -579,7 +703,7 @@ fn route_edge(
|
|||||||
let mut candidates = Vec::new();
|
let mut candidates = Vec::new();
|
||||||
for a in endpoints(source, edge.source_port, view) {
|
for a in endpoints(source, edge.source_port, view) {
|
||||||
for b in endpoints(target, edge.target_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.
|
// Reusing one anchor makes the cheapest path a degenerate stub.
|
||||||
if edge.source == edge.target
|
if edge.source == edge.target
|
||||||
&& edge.source_port.is_none()
|
&& edge.source_port.is_none()
|
||||||
@@ -1107,7 +1231,8 @@ fn label_text(project: &Project, edge: Id) -> String {
|
|||||||
let text = match relation.kind() {
|
let text = match relation.kind() {
|
||||||
RelationKind::Interaction
|
RelationKind::Interaction
|
||||||
| RelationKind::FunctionalExchange
|
| RelationKind::FunctionalExchange
|
||||||
| RelationKind::BoundaryExchange => relation.name.as_str(),
|
| RelationKind::BoundaryExchange
|
||||||
|
| RelationKind::DataAssociation => relation.name.as_str(),
|
||||||
RelationKind::CapabilityInclude => "«include»",
|
RelationKind::CapabilityInclude => "«include»",
|
||||||
RelationKind::CapabilityExtend => "«extend»",
|
RelationKind::CapabilityExtend => "«extend»",
|
||||||
RelationKind::CapabilityGeneralization => "specializes",
|
RelationKind::CapabilityGeneralization => "specializes",
|
||||||
|
|||||||
@@ -114,6 +114,7 @@ pub struct PreparedCommand {
|
|||||||
struct IdentityProvenance {
|
struct IdentityProvenance {
|
||||||
original: BTreeSet<Id>,
|
original: BTreeSet<Id>,
|
||||||
current_to_original: BTreeMap<Id, Id>,
|
current_to_original: BTreeMap<Id, Id>,
|
||||||
|
explicit_replacement: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IdentityProvenance {
|
impl IdentityProvenance {
|
||||||
@@ -123,10 +124,12 @@ impl IdentityProvenance {
|
|||||||
Self {
|
Self {
|
||||||
original,
|
original,
|
||||||
current_to_original,
|
current_to_original,
|
||||||
|
explicit_replacement: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn replace(&mut self, project: &Project) {
|
fn replace(&mut self, project: &Project) {
|
||||||
|
self.explicit_replacement = true;
|
||||||
let present: BTreeSet<_> = project.identities().into_iter().map(|(id, _)| id).collect();
|
let present: BTreeSet<_> = project.identities().into_iter().map(|(id, _)| id).collect();
|
||||||
self.current_to_original = self
|
self.current_to_original = self
|
||||||
.original
|
.original
|
||||||
@@ -180,6 +183,13 @@ pub fn prepare_command(
|
|||||||
)?;
|
)?;
|
||||||
crate::references::finish_deletions(&mut candidate, &pending_deletions);
|
crate::references::finish_deletions(&mut candidate, &pending_deletions);
|
||||||
provenance.retain_present(&candidate);
|
provenance.retain_present(&candidate);
|
||||||
|
if !provenance.explicit_replacement {
|
||||||
|
crate::data::prune_removed_inheritance(
|
||||||
|
project,
|
||||||
|
&mut candidate,
|
||||||
|
&provenance.current_to_original,
|
||||||
|
);
|
||||||
|
}
|
||||||
validate_candidate(&candidate)?;
|
validate_candidate(&candidate)?;
|
||||||
Ok(PreparedCommand {
|
Ok(PreparedCommand {
|
||||||
project: candidate,
|
project: candidate,
|
||||||
@@ -287,6 +297,13 @@ pub fn execute_batch(project: &Project, commands: &[Command]) -> Result<Project,
|
|||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
crate::references::finish_deletions(&mut candidate, &pending_deletions);
|
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)?;
|
validate_candidate(&candidate)?;
|
||||||
Ok(candidate)
|
Ok(candidate)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -251,6 +251,8 @@ fn mapped_relation(r: &Relation, direction: TransitionDirection) -> Option<Relat
|
|||||||
}
|
}
|
||||||
(
|
(
|
||||||
RelationBody::Allocation { .. }
|
RelationBody::Allocation { .. }
|
||||||
|
| RelationBody::InterfaceItemAllocation { .. }
|
||||||
|
| RelationBody::DataAssociation { .. }
|
||||||
| RelationBody::ExchangeAllocation { .. }
|
| RelationBody::ExchangeAllocation { .. }
|
||||||
| RelationBody::CapabilityInvolvement { .. }
|
| RelationBody::CapabilityInvolvement { .. }
|
||||||
| RelationBody::CapabilityInclude { .. }
|
| RelationBody::CapabilityInclude { .. }
|
||||||
@@ -421,6 +423,24 @@ pub fn plan_transition(
|
|||||||
}
|
}
|
||||||
if let Some(e) = project.elements.get(&id) {
|
if let Some(e) = project.elements.get(&id) {
|
||||||
selected.extend(crate::references::element_references(e));
|
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
|
// Contextual links are owned by the process/chain shared by
|
||||||
// their endpoint involvements, independently of list order.
|
// their endpoint involvements, independently of list order.
|
||||||
if matches!(e.kind(), Kind::Process | Kind::FunctionalChain) {
|
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() {
|
if let Some(shared) = e.shared() {
|
||||||
match shared {
|
match shared {
|
||||||
SharedBody::DataType {
|
SharedBody::DataType { .. }
|
||||||
definition: DataTypeDefinition::Enumeration { literals },
|
| SharedBody::DataField { .. }
|
||||||
} => {
|
| SharedBody::Interface { .. }
|
||||||
let mut seen = BTreeSet::new();
|
| SharedBody::ExchangeItem { .. } => crate::data::validate_element(p, e, &mut out),
|
||||||
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::Port { .. } => crate::ports::validate_port(p, e, &mut out),
|
SharedBody::Port { .. } => crate::ports::validate_port(p, e, &mut out),
|
||||||
SharedBody::Scenario {
|
SharedBody::Scenario {
|
||||||
viewpoint,
|
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, "name", &r.name);
|
||||||
text(&mut out, r.id, "description", &r.description);
|
text(&mut out, r.id, "description", &r.description);
|
||||||
validate_relation(p, r, &mut out);
|
validate_relation(p, r, &mut out);
|
||||||
|
crate::data::validate_relation(p, r, &mut out);
|
||||||
crate::ports::validate_boundary_interface_requirements(p, r, &mut out);
|
crate::ports::validate_boundary_interface_requirements(p, r, &mut out);
|
||||||
validate_relation_context(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 {
|
for (key, d) in &p.diagrams {
|
||||||
if *key != d.id {
|
if *key != d.id {
|
||||||
error(
|
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.
|
/// Fields carried by an exchange, including fields of nested named records.
|
||||||
pub fn carried_fields(p: &Project, relation: &Relation) -> BTreeSet<Id> {
|
pub fn carried_fields(p: &Project, relation: &Relation) -> BTreeSet<Id> {
|
||||||
let mut pending = relation.body.items().to_vec();
|
crate::data::carried_field_paths(p, relation)
|
||||||
let mut seen = BTreeSet::new();
|
.into_iter()
|
||||||
let mut fields = BTreeSet::new();
|
.map(|f| f.field)
|
||||||
while let Some(owner) = pending.pop() {
|
.collect()
|
||||||
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
|
|
||||||
}
|
}
|
||||||
fn occurrence_uses_lifeline(body: &OccurrenceBody, line: Id) -> bool {
|
fn occurrence_uses_lifeline(body: &OccurrenceBody, line: Id) -> bool {
|
||||||
match body {
|
match body {
|
||||||
@@ -1683,36 +1614,125 @@ fn validate_message(p: &Project, e: &Element, body: &OccurrenceBody, out: &mut V
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let allowed = exchange
|
let relation = exchange.and_then(|id| p.relations.get(&id));
|
||||||
.and_then(|id| p.relations.get(&id))
|
if message_kind == &MessageKind::Call
|
||||||
.map(|r| carried_fields(p, r));
|
&& 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();
|
let mut fields = BTreeSet::new();
|
||||||
for value in values {
|
for value in values {
|
||||||
expect_element(p, out, e.id, value.field, "values.field", |kind| {
|
expect_element(p, out, e.id, value.field, "values.field", |kind| {
|
||||||
kind == Kind::DataField
|
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);
|
text(out, e.id, "values.expression", &value.expression);
|
||||||
if !fields.insert(value.field) {
|
if !fields.insert((&value.path, value.field)) {
|
||||||
error(
|
error(
|
||||||
out,
|
out,
|
||||||
e.id,
|
e.id,
|
||||||
"duplicate_message_value",
|
"duplicate_message_value",
|
||||||
"values",
|
"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
|
if value.path.len() > crate::data::MAX_VALUE_PATH_DEPTH {
|
||||||
.as_ref()
|
|
||||||
.is_some_and(|allowed| !allowed.contains(&value.field))
|
|
||||||
{
|
|
||||||
error(
|
error(
|
||||||
out,
|
out,
|
||||||
e.id,
|
e.id,
|
||||||
"message_value_field",
|
"message_value_path_limit",
|
||||||
"values.field",
|
"values.path",
|
||||||
"Value annotations must reference fields carried by this message's exchange, including nested record fields.",
|
"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() {
|
if value.expression.trim().is_empty() {
|
||||||
warning(
|
warning(
|
||||||
out,
|
out,
|
||||||
@@ -2130,7 +2150,11 @@ fn validate_relation(p: &Project, r: &Relation, out: &mut Vec<Issue>) {
|
|||||||
};
|
};
|
||||||
if a.layer() != b.layer()
|
if a.layer() != b.layer()
|
||||||
|| !r.kind().permits(a.kind(), b.kind())
|
|| !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(
|
error(
|
||||||
out,
|
out,
|
||||||
@@ -2381,8 +2405,10 @@ pub fn can_parent(p: &Project, child_kind: Kind, layer: Layer, parent: Option<Id
|
|||||||
owner.layer() == layer
|
owner.layer() == layer
|
||||||
&& parent_permits(owner.kind(), child_kind)
|
&& parent_permits(owner.kind(), child_kind)
|
||||||
&& match owner.shared() {
|
&& match owner.shared() {
|
||||||
Some(SharedBody::DataType { definition }) if child_kind == Kind::DataField => {
|
Some(SharedBody::DataType { definition, .. })
|
||||||
*definition == DataTypeDefinition::Record
|
if child_kind == Kind::DataField =>
|
||||||
|
{
|
||||||
|
matches!(definition, DataTypeDefinition::Record { .. })
|
||||||
}
|
}
|
||||||
Some(SharedBody::Occurrence { body, .. }) if child_kind == Kind::Occurrence => {
|
Some(SharedBody::Occurrence { body, .. }) if child_kind == Kind::Occurrence => {
|
||||||
matches!(
|
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();
|
p = execute_batch(&p, &plan_component_exchange(&p, &req).unwrap()).unwrap();
|
||||||
let (source, target) = p.relations[&boundary].body.endpoints();
|
let (source, target) = p.relations[&boundary].body.endpoints();
|
||||||
let interface = add(&mut p, Kind::Interface, Layer::Sa, "Service");
|
let interface = add(&mut p, Kind::Interface, Layer::Sa, "Service");
|
||||||
*p.elements
|
p.relate(
|
||||||
.get_mut(&interface)
|
RelationKind::InterfaceItemAllocation,
|
||||||
.unwrap()
|
"Interface payload",
|
||||||
.shared_mut()
|
interface,
|
||||||
.unwrap() = SharedBody::Interface { items: vec![item] };
|
item,
|
||||||
|
);
|
||||||
let requirement = p.relate(
|
let requirement = p.relate(
|
||||||
RelationKind::InterfaceRequirement,
|
RelationKind::InterfaceRequirement,
|
||||||
"Caller requires service",
|
"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 (mut p, a, b, item, forward) = fixture();
|
||||||
let (old, _) = endpoints(&p, forward);
|
let (old, _) = endpoints(&p, forward);
|
||||||
let interface = add(&mut p, Kind::Interface, Layer::Sa, "Original interface");
|
let interface = add(&mut p, Kind::Interface, Layer::Sa, "Original interface");
|
||||||
*p.elements
|
p.relate(
|
||||||
.get_mut(&interface)
|
RelationKind::InterfaceItemAllocation,
|
||||||
.unwrap()
|
"Interface payload",
|
||||||
.shared_mut()
|
interface,
|
||||||
.unwrap() = SharedBody::Interface { items: vec![item] };
|
item,
|
||||||
|
);
|
||||||
p.elements.get_mut(&old).unwrap().description = "Original bidirectional rationale".into();
|
p.elements.get_mut(&old).unwrap().description = "Original bidirectional rationale".into();
|
||||||
*p.elements.get_mut(&old).unwrap().shared_mut().unwrap() = SharedBody::Port {
|
*p.elements.get_mut(&old).unwrap().shared_mut().unwrap() = SharedBody::Port {
|
||||||
contract: PortContract::Legacy,
|
contract: PortContract::Legacy,
|
||||||
@@ -659,11 +661,12 @@ fn directed_legacy_resolution_retains_unclassified_interface_and_role_identity()
|
|||||||
let (mut p, _, _, item, fe) = fixture();
|
let (mut p, _, _, item, fe) = fixture();
|
||||||
let output = endpoints(&p, fe).0;
|
let output = endpoints(&p, fe).0;
|
||||||
let interface = add(&mut p, Kind::Interface, Layer::Sa, "Original service");
|
let interface = add(&mut p, Kind::Interface, Layer::Sa, "Original service");
|
||||||
*p.elements
|
p.relate(
|
||||||
.get_mut(&interface)
|
RelationKind::InterfaceItemAllocation,
|
||||||
.unwrap()
|
"Interface payload",
|
||||||
.shared_mut()
|
interface,
|
||||||
.unwrap() = SharedBody::Interface { items: vec![item] };
|
item,
|
||||||
|
);
|
||||||
*p.elements.get_mut(&output).unwrap().shared_mut().unwrap() = SharedBody::Port {
|
*p.elements.get_mut(&output).unwrap().shared_mut().unwrap() = SharedBody::Port {
|
||||||
contract: PortContract::Legacy,
|
contract: PortContract::Legacy,
|
||||||
legacy: Some(LegacyPortContract {
|
legacy: Some(LegacyPortContract {
|
||||||
|
|||||||
@@ -94,7 +94,9 @@ fn fixture(layer: Layer) -> Fixture {
|
|||||||
.add_element(Kind::DataType, layer, "Request record", None)
|
.add_element(Kind::DataType, layer, "Request record", None)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
*p.elements.get_mut(&record).unwrap().shared_mut().unwrap() = SharedBody::DataType {
|
*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
|
let field = p
|
||||||
.add_element(Kind::DataField, layer, "Requested level", Some(record))
|
.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()
|
}) = p.elements.get_mut(&call).unwrap().shared_mut()
|
||||||
{
|
{
|
||||||
values.push(MessageValue {
|
values.push(MessageValue {
|
||||||
|
path: vec![payload],
|
||||||
field,
|
field,
|
||||||
expression: "requested_level".into(),
|
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 f2 = add(&mut p, Kind::Function, Layer::Sa, "Receive", None);
|
||||||
let item = add(&mut p, Kind::ExchangeItem, Layer::Sa, "Message", None);
|
let item = add(&mut p, Kind::ExchangeItem, Layer::Sa, "Message", None);
|
||||||
let interface = add(&mut p, Kind::Interface, Layer::Sa, "Link interface", None);
|
let interface = add(&mut p, Kind::Interface, Layer::Sa, "Link interface", None);
|
||||||
*p.elements
|
p.relate(
|
||||||
.get_mut(&interface)
|
RelationKind::InterfaceItemAllocation,
|
||||||
.unwrap()
|
"Interface payload",
|
||||||
.shared_mut()
|
interface,
|
||||||
.unwrap() = SharedBody::Interface { items: vec![item] };
|
item,
|
||||||
|
);
|
||||||
let port = |p: &mut Project, name: &str, parent, direction| {
|
let port = |p: &mut Project, name: &str, parent, direction| {
|
||||||
let id = Id::new_v4();
|
let id = Id::new_v4();
|
||||||
let contract = if p.elements[&parent].kind() == Kind::Function {
|
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,
|
minimum: 0,
|
||||||
maximum: None,
|
maximum: None,
|
||||||
unit: "items".into(),
|
unit: "items".into(),
|
||||||
|
direction: ParameterDirection::Unspecified,
|
||||||
|
storage: FieldStorage::Value,
|
||||||
|
default: None,
|
||||||
};
|
};
|
||||||
let function = add(&mut p, Kind::Function, Layer::Sa, "Function", None);
|
let function = add(&mut p, Kind::Function, Layer::Sa, "Function", None);
|
||||||
let other = add(&mut p, Kind::Function, Layer::Sa, "Other", 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"
|
"Reidentification must preserve all values and order"
|
||||||
);
|
);
|
||||||
if id != p.id {
|
if id != p.id {
|
||||||
let deleted = execute_batch(&p, &[Command::Delete(id)])
|
let deleted = if p
|
||||||
.unwrap_or_else(|err| panic!("Delete {id}: {err}"));
|
.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);
|
accepted(&deleted);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -890,13 +926,15 @@ fn demo_has_both_layers_complete_contracts_and_no_checks() {
|
|||||||
assert!(p.elements.values().any(|e| matches!(
|
assert!(p.elements.values().any(|e| matches!(
|
||||||
e.shared(),
|
e.shared(),
|
||||||
Some(SharedBody::ExchangeItem {
|
Some(SharedBody::ExchangeItem {
|
||||||
kind: ExchangeItemKind::Material
|
kind: ExchangeItemKind::Material,
|
||||||
|
..
|
||||||
})
|
})
|
||||||
)));
|
)));
|
||||||
assert!(p.elements.values().any(|e| matches!(
|
assert!(p.elements.values().any(|e| matches!(
|
||||||
e.shared(),
|
e.shared(),
|
||||||
Some(SharedBody::ExchangeItem {
|
Some(SharedBody::ExchangeItem {
|
||||||
kind: ExchangeItemKind::Energy
|
kind: ExchangeItemKind::Energy,
|
||||||
|
..
|
||||||
})
|
})
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user