Index semantic containment and start boundary connections from component ports
This commit is contained in:
@@ -292,6 +292,17 @@ impl Workflow {
|
||||
})
|
||||
}
|
||||
pub fn relationship(p: &Project, selected: Id, layer: Layer) -> Self {
|
||||
if let Some(PortContract::Component { orientation, .. }) =
|
||||
p.elements.get(&selected).and_then(port_contract)
|
||||
{
|
||||
return Self::relationship_kind(
|
||||
p,
|
||||
selected,
|
||||
layer,
|
||||
RelationKind::BoundaryExchange,
|
||||
*orientation == ComponentPortOrientation::In,
|
||||
);
|
||||
}
|
||||
let kind = match p.elements.get(&selected).map(oa_core::Element::kind) {
|
||||
Some(Kind::OperationalMission | Kind::Mission) => RelationKind::MissionCapability,
|
||||
Some(Kind::Capability | Kind::SystemCapability) => RelationKind::CapabilityInclude,
|
||||
@@ -1115,6 +1126,30 @@ impl Workflow {
|
||||
}
|
||||
}
|
||||
fn choice(p: &Project, id: Id) -> Choice {
|
||||
if let Some(element) = p.elements.get(&id)
|
||||
&& let Some(contract) = port_contract(element)
|
||||
{
|
||||
let contract = match contract {
|
||||
PortContract::FunctionInput { .. } => "Input".into(),
|
||||
PortContract::FunctionOutput { .. } => "Output".into(),
|
||||
PortContract::Component {
|
||||
kind: ComponentPortKind::Standard,
|
||||
..
|
||||
} => "Standard".into(),
|
||||
PortContract::Component { orientation, .. } => format!("Flow {orientation:?}"),
|
||||
PortContract::Legacy => "Legacy".into(),
|
||||
};
|
||||
return Choice {
|
||||
id,
|
||||
label: format!(
|
||||
"{} / {} · {} · @{}",
|
||||
short(element.parent.map_or("Unowned", |owner| p.named(owner)), 18),
|
||||
short(&element.name, 20),
|
||||
contract,
|
||||
element.alias
|
||||
),
|
||||
};
|
||||
}
|
||||
Choice {
|
||||
id,
|
||||
label: format!(
|
||||
@@ -1128,6 +1163,115 @@ fn choice(p: &Project, id: Id) -> Choice {
|
||||
#[cfg(test)]
|
||||
mod port_authoring_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn generic_component_port_connection_keeps_its_endpoint_and_direction() {
|
||||
for (kind, orientation) in [
|
||||
(ComponentPortKind::Flow, ComponentPortOrientation::In),
|
||||
(ComponentPortKind::Flow, ComponentPortOrientation::Out),
|
||||
(ComponentPortKind::Flow, ComponentPortOrientation::InOut),
|
||||
(ComponentPortKind::Flow, ComponentPortOrientation::Unset),
|
||||
(ComponentPortKind::Standard, ComponentPortOrientation::Unset),
|
||||
] {
|
||||
let mut p = Project::empty();
|
||||
let actor = p
|
||||
.add_element(Kind::SystemActor, Layer::Sa, "Actor", None)
|
||||
.unwrap();
|
||||
let system = p
|
||||
.add_element(Kind::System, Layer::Sa, "System", None)
|
||||
.unwrap();
|
||||
let incoming = orientation == ComponentPortOrientation::In;
|
||||
let other_orientation = if kind == ComponentPortKind::Standard {
|
||||
ComponentPortOrientation::Unset
|
||||
} else if incoming {
|
||||
ComponentPortOrientation::Out
|
||||
} else {
|
||||
ComponentPortOrientation::In
|
||||
};
|
||||
let mut ports = vec![];
|
||||
for (owner, orientation) in [(actor, orientation), (system, other_orientation)] {
|
||||
let id = Id::new_v4();
|
||||
let port = make_port(
|
||||
&p,
|
||||
id,
|
||||
owner,
|
||||
"Data",
|
||||
PortContract::Component { kind, orientation },
|
||||
)
|
||||
.unwrap();
|
||||
p.elements.insert(id, port);
|
||||
ports.push(id);
|
||||
}
|
||||
let before = p.clone();
|
||||
let mut workflow = Workflow::relationship(&p, ports[0], Layer::Sa);
|
||||
let Workflow::Relation(draft) = &workflow else {
|
||||
unreachable!()
|
||||
};
|
||||
assert_eq!(draft.kind, RelationKind::BoundaryExchange);
|
||||
if incoming {
|
||||
assert!(draft.source.is_none());
|
||||
assert_eq!(draft.target.as_ref().unwrap().id, ports[0]);
|
||||
} else {
|
||||
assert_eq!(draft.source.as_ref().unwrap().id, ports[0]);
|
||||
assert!(draft.target.is_none());
|
||||
}
|
||||
assert_eq!(draft.oriented, kind == ComponentPortKind::Flow);
|
||||
workflow
|
||||
.update(
|
||||
&p,
|
||||
if incoming {
|
||||
Event::Source(choice(&p, ports[1]))
|
||||
} else {
|
||||
Event::Target(choice(&p, ports[1]))
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
let accepted = workflow.update(&p, Event::Apply).unwrap().unwrap();
|
||||
assert_eq!(p, before, "The modal never mutates its input model");
|
||||
let (expected_source, expected_target) = if incoming {
|
||||
(ports[1], ports[0])
|
||||
} else {
|
||||
(ports[0], ports[1])
|
||||
};
|
||||
assert!(accepted.relations.values().any(|relation| matches!(relation.body, RelationBody::BoundaryExchange { source, target, oriented, .. } if source == expected_source && target == expected_target && oriented == (kind == ComponentPortKind::Flow))));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duplicate_port_names_show_owner_contract_and_unique_alias() {
|
||||
let mut p = Project::empty();
|
||||
let actor = p
|
||||
.add_element(Kind::SystemActor, Layer::Sa, "Operator", None)
|
||||
.unwrap();
|
||||
let system = p
|
||||
.add_element(Kind::System, Layer::Sa, "Rescue", None)
|
||||
.unwrap();
|
||||
let mut labels = BTreeSet::new();
|
||||
for owner in [actor, actor, system] {
|
||||
let id = Id::new_v4();
|
||||
let port = make_port(
|
||||
&p,
|
||||
id,
|
||||
owner,
|
||||
"Data",
|
||||
PortContract::Component {
|
||||
kind: ComponentPortKind::Flow,
|
||||
orientation: ComponentPortOrientation::Out,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
p.elements.insert(id, port);
|
||||
let label = choice(&p, id).label;
|
||||
assert!(label.contains(&format!("{} / Data", p.named(owner))));
|
||||
assert!(label.contains("Flow Out"));
|
||||
assert!(label.contains(&format!("@{}", p.elements[&id].alias)));
|
||||
assert!(
|
||||
labels.insert(label),
|
||||
"Duplicate names must still identify distinct ports"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn boundary_picker_recomputes_for_unoriented_reverse_endpoints_without_relaxing_contracts() {
|
||||
let mut p = Project::empty();
|
||||
|
||||
Reference in New Issue
Block a user