Model typed SA ports and atomic exchange allocation workflows

This commit is contained in:
2026-09-05 18:22:00 +03:00
parent c20a2073d7
commit 0d14438b7b
13 changed files with 2402 additions and 256 deletions
+601
View File
@@ -0,0 +1,601 @@
use oa_core::{projection::*, transitions::*, validation, *};
fn add(p: &mut Project, kind: Kind, layer: Layer, name: &str) -> Id {
p.add_element(kind, layer, name, None).unwrap()
}
fn request(p: &Project, source: Id, target: Id, items: Vec<Id>) -> FunctionalExchangeRequest {
let id = Id::new_v4();
FunctionalExchangeRequest {
id,
alias: p.fresh_alias("message", "exchange", id),
name: "Message".into(),
description: "Engineering rationale".into(),
source,
target,
source_port: PortChoice::Auto,
target_port: PortChoice::Auto,
items,
}
}
fn fixture() -> (Project, Id, Id, Id, Id) {
let mut p = Project::empty();
let a = add(&mut p, Kind::Function, Layer::Sa, "Send");
let b = add(&mut p, Kind::Function, Layer::Sa, "Receive");
let item = add(&mut p, Kind::ExchangeItem, Layer::Sa, "Information");
let request = request(&p, a, b, vec![item]);
p = execute_batch(&p, &plan_functional_exchange(&p, &request).unwrap()).unwrap();
(p, a, b, item, request.id)
}
fn endpoints(p: &Project, id: Id) -> (Id, Id) {
match p.relations[&id].body {
RelationBody::FunctionalExchange {
source_port,
target_port,
..
} => (source_port, target_port),
_ => panic!("not a functional exchange"),
}
}
fn allocate(p: &mut Project, a: Id, b: Id) -> (Id, Id) {
let first = add(p, Kind::System, Layer::Sa, "System");
let second = add(p, Kind::SystemActor, Layer::Sa, "Partner");
p.relate(RelationKind::Allocation, "Send allocation", first, a);
p.relate(RelationKind::Allocation, "Receive allocation", second, b);
(first, second)
}
fn component_request(p: &Project, fe: Id, kind: ComponentPortKind) -> ComponentExchangeRequest {
let id = Id::new_v4();
ComponentExchangeRequest {
functional_exchange: fe,
boundary: ComponentExchangeChoice::Create {
id,
alias: p.fresh_alias("boundary", "exchange", id),
name: "Boundary".into(),
description: "Physical realization".into(),
port_kind: kind,
oriented: kind == ComponentPortKind::Flow,
source_port: PortChoice::Auto,
target_port: PortChoice::Auto,
},
}
}
fn boundary_id(request: &ComponentExchangeRequest) -> Id {
match request.boundary {
ComponentExchangeChoice::Create { id, .. } | ComponentExchangeChoice::Reuse(id) => id,
}
}
fn code(p: &Project, expected: &str) -> bool {
validation::validate(p)
.iter()
.any(|issue| issue.code == expected && issue.severity == validation::Severity::Error)
}
#[test]
fn functional_creation_reuse_and_item_sync_are_atomic_and_identity_safe() {
let (p, a, b, item, fe) = fixture();
let (output, input) = endpoints(&p, fe);
assert!(
matches!(port_contract(&p.elements[&output]), Some(PortContract::FunctionOutput { outgoing_items }) if outgoing_items == &[item])
);
assert!(
matches!(port_contract(&p.elements[&input]), Some(PortContract::FunctionInput { incoming_items }) if incoming_items == &[item])
);
let next = request(&p, a, b, vec![item]);
let commands = plan_functional_exchange(&p, &next).unwrap();
assert_eq!(commands.len(), 1);
let mut session = Session::new(p.clone());
session.execute(Command::Batch(commands)).unwrap();
assert_eq!(endpoints(session.project(), next.id), (output, input));
assert_eq!(session.revision(), 1);
session.undo();
assert_eq!(session.project(), &p);
let mut wrong = request(&p, a, b, vec![item]);
wrong.source_port = PortChoice::Reuse(input);
assert!(plan_functional_exchange(&p, &wrong).is_err());
assert!(plan_function_port_items(&p, output, vec![]).is_err());
assert_eq!(session.project(), &p);
assert!(ElementBody::new(Kind::Port, Layer::Sa).is_none());
}
#[test]
fn ambiguous_reuse_creates_a_fresh_port_and_collisions_never_overwrite() {
let (mut p, a, b, item, _) = fixture();
let other = Id::new_v4();
p.elements.insert(
other,
make_port(
&p,
other,
a,
"Alternative output",
PortContract::FunctionOutput {
outgoing_items: vec![item],
},
)
.unwrap(),
);
let req = request(&p, a, b, vec![item]);
let expected = Id::new_v5(&req.id, b"function-source-port");
let commands = plan_functional_exchange(&p, &req).unwrap();
assert_eq!(
serde_json::to_value(&commands).unwrap(),
serde_json::to_value(plan_functional_exchange(&p, &req).unwrap()).unwrap()
);
let result = execute_batch(&p, &commands).unwrap();
assert_eq!(endpoints(&result, req.id).0, expected);
let occupied = p.elements[&other].clone();
let mut blocker = occupied.clone();
blocker.id = expected;
blocker.alias = "block_generated_identity".into();
p.elements.insert(expected, blocker);
assert!(
plan_functional_exchange(&p, &req)
.unwrap_err()
.0
.contains("port_id_collision")
);
assert_eq!(p.elements[&other], occupied);
}
#[test]
fn component_generation_requires_real_allocations_and_reuses_existing_facts() {
let (mut p, a, b, _, fe) = fixture();
let initial = component_request(&p, fe, ComponentPortKind::Flow);
assert!(
plan_component_exchange(&p, &initial)
.unwrap_err()
.0
.contains("port_allocation_owner")
);
allocate(&mut p, a, b);
let req = component_request(&p, fe, ComponentPortKind::Flow);
let boundary = boundary_id(&req);
let result = execute_batch(&p, &plan_component_exchange(&p, &req).unwrap()).unwrap();
assert_eq!(
result
.relations
.values()
.filter(|r| r.kind() == RelationKind::PortAllocation)
.count(),
2
);
assert_eq!(
result
.relations
.values()
.filter(|r| r.kind() == RelationKind::ExchangeAllocation)
.count(),
1
);
let again = ComponentExchangeRequest {
functional_exchange: fe,
boundary: ComponentExchangeChoice::Reuse(boundary),
};
assert!(plan_component_exchange(&result, &again).unwrap().is_empty());
let mut wrong = result.clone();
let source = wrong.relations[&boundary].source();
if let Some(SharedBody::Port { contract, .. }) =
wrong.elements.get_mut(&source).unwrap().shared_mut()
{
*contract = PortContract::Component {
kind: ComponentPortKind::Flow,
orientation: ComponentPortOrientation::In,
};
}
assert!(code(&wrong, "boundary_port"));
assert!(code(&wrong, "port_allocation_contract"));
let mut session = Session::new(p.clone());
session
.execute(Command::Batch(plan_component_exchange(&p, &req).unwrap()))
.unwrap();
session.undo();
assert_eq!(session.project(), &p);
}
#[test]
fn standard_interface_roles_do_not_follow_message_direction_and_kind_mixing_is_rejected() {
let (mut p, a, b, item, fe) = fixture();
allocate(&mut p, a, b);
let req = component_request(&p, fe, ComponentPortKind::Standard);
let boundary = boundary_id(&req);
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] };
let requirement = p.relate(
RelationKind::InterfaceRequirement,
"Caller requires service",
source,
interface,
);
let provider = p.relate(
RelationKind::InterfaceProvision,
"Callee provides service",
target,
interface,
);
validate_candidate(&p).unwrap();
assert!(
!validation::validate(&p)
.iter()
.any(|i| i.code == "unsatisfied_interface_requirement")
);
assert!(matches!(
p.relations[&boundary].body,
RelationBody::BoundaryExchange {
oriented: false,
..
}
));
let without_provider = execute_batch(&p, &[Command::Delete(provider)]).unwrap();
assert!(
validation::validate(&without_provider).iter().any(
|i| i.element == Some(requirement) && i.code == "unsatisfied_interface_requirement"
)
);
if let Some(SharedBody::Port { contract, .. }) =
p.elements.get_mut(&source).unwrap().shared_mut()
{
*contract = PortContract::Component {
kind: ComponentPortKind::Flow,
orientation: ComponentPortOrientation::Out,
};
}
assert!(code(&p, "mixed_component_ports"));
}
#[test]
fn legacy_split_preserves_original_identity_prose_interface_artifact_constraint_and_trace() {
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.elements.get_mut(&old).unwrap().description = "Original bidirectional rationale".into();
*p.elements.get_mut(&old).unwrap().shared_mut().unwrap() = SharedBody::Port {
contract: PortContract::Legacy,
legacy: Some(LegacyPortContract {
direction: PortDirection::InOut,
interface: Some(interface),
}),
};
let reverse_req = request(&p, b, a, vec![item]);
p = execute_batch(&p, &plan_functional_exchange(&p, &reverse_req).unwrap()).unwrap();
if let RelationBody::FunctionalExchange { target_port, .. } =
&mut p.relations.get_mut(&reverse_req.id).unwrap().body
{
*target_port = old;
}
let artifact = Id::new_v4();
p.artifact_links.insert(
artifact,
ArtifactLink {
id: artifact,
alias: "rationale".into(),
name: "Evidence".into(),
description: "Attached original evidence".into(),
owner: old,
relation: ArtifactRelation::References,
uri: "docs/evidence.md".into(),
revision: Some("v2".into()),
fragment: None,
},
);
let constraint = add(&mut p, Kind::Constraint, Layer::Sa, "Port requirement");
*p.elements
.get_mut(&constraint)
.unwrap()
.shared_mut()
.unwrap() = SharedBody::Constraint {
expression: "original meaning".into(),
constrained: vec![old],
};
let oa_work = add(&mut p, Kind::Activity, Layer::Oa, "Original work");
let oa_port = Id::new_v4();
let mut legacy_oa = p.elements[&old].clone();
legacy_oa.id = oa_port;
legacy_oa.alias = "original_oa_port".into();
legacy_oa.parent = Some(oa_work);
legacy_oa.body = ElementBody::shared_in(
Layer::Oa,
SharedBody::Port {
contract: PortContract::Legacy,
legacy: Some(LegacyPortContract {
direction: PortDirection::InOut,
interface: None,
}),
},
);
p.elements.insert(oa_port, legacy_oa);
let trace = Id::new_v4();
p.traces.insert(
trace,
Trace {
id: trace,
alias: "port_trace".into(),
name: "Original correspondence".into(),
description: String::new(),
oa: oa_port,
sa: old,
kind: TraceKind::Item,
baseline: None,
},
);
validate_candidate(&p).unwrap();
let req = LegacyWorkPortSplitRequest {
port: old,
primary: FunctionPortSide::Input,
incoming_items: vec![item],
outgoing_items: vec![item],
};
let commands = plan_legacy_work_port_split(&p, &req).unwrap();
let mut session = Session::new(p.clone());
session.execute(Command::Batch(commands)).unwrap();
let result = session.project();
let opposite = Id::new_v5(&old, b"split-opposite-port");
assert_eq!(endpoints(result, forward).0, opposite);
assert_eq!(endpoints(result, reverse_req.id).1, old);
assert_eq!(
result.elements[&old].description,
p.elements[&old].description
);
assert!(
matches!(result.elements[&old].shared(), Some(SharedBody::Port { contract: PortContract::FunctionInput { .. }, legacy: Some(LegacyPortContract { direction: PortDirection::InOut, interface: Some(id) }) }) if *id == interface)
);
assert_eq!(
result.artifact_links[&artifact],
p.artifact_links[&artifact]
);
assert_eq!(result.traces[&trace], p.traces[&trace]);
assert_eq!(result.elements[&constraint], p.elements[&constraint]);
session.undo();
assert_eq!(session.project(), &p);
assert!(
plan_legacy_port_resolution(
&p,
&LegacyPortResolution {
port: oa_port,
contract: PortContract::FunctionInput {
incoming_items: vec![]
}
}
)
.is_err()
);
}
#[test]
fn deletion_preserves_independent_ports_but_removes_required_dependents() {
let (mut p, a, b, _, fe) = fixture();
allocate(&mut p, a, b);
let req = component_request(&p, fe, ComponentPortKind::Flow);
p = execute_batch(&p, &plan_component_exchange(&p, &req).unwrap()).unwrap();
let (output, input) = endpoints(&p, fe);
let allocation = p
.relations
.values()
.find(|r| r.kind() == RelationKind::PortAllocation)
.unwrap()
.id;
let removed_allocation = execute_batch(&p, &[Command::Delete(allocation)]).unwrap();
assert!(removed_allocation.elements.contains_key(&output));
assert!(
removed_allocation
.relations
.values()
.all(|r| r.kind() != RelationKind::ExchangeAllocation)
);
let removed_fe = execute_batch(&p, &[Command::Delete(fe)]).unwrap();
assert!(removed_fe.elements.contains_key(&output) && removed_fe.elements.contains_key(&input));
let removed_port = execute_batch(&p, &[Command::Delete(output)]).unwrap();
assert!(!removed_port.relations.contains_key(&fe));
assert!(removed_port.elements.contains_key(&input));
let new = Id::new_v4();
let changed = execute_batch(&p, &[Command::Reidentify(output, new)]).unwrap();
assert_eq!(endpoints(&changed, fe).0, new);
assert!(
changed
.relations
.values()
.any(|r| r.kind() == RelationKind::PortAllocation && r.target() == new)
);
}
#[test]
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.elements.get_mut(&output).unwrap().shared_mut().unwrap() = SharedBody::Port {
contract: PortContract::Legacy,
legacy: Some(LegacyPortContract {
direction: PortDirection::Out,
interface: Some(interface),
}),
};
let role = p.relate(
RelationKind::InterfaceRequirement,
"Original unreviewed role",
output,
interface,
);
let req = LegacyPortResolution {
port: output,
contract: PortContract::FunctionOutput {
outgoing_items: vec![item],
},
};
let result = execute_batch(&p, &plan_legacy_port_resolution(&p, &req).unwrap()).unwrap();
assert_eq!(result.relations[&role], p.relations[&role]);
assert_eq!(result.elements[&output].alias, p.elements[&output].alias);
assert!(
validation::validate(&result)
.iter()
.any(|issue| issue.element == Some(role) && issue.code == "legacy_interface_role")
);
let replacement = Id::new_v4();
let remapped = execute_batch(&result, &[Command::Reidentify(interface, replacement)]).unwrap();
assert!(
matches!(remapped.elements[&output].shared(), Some(SharedBody::Port { legacy: Some(LegacyPortContract { interface: Some(id), .. }), .. }) if *id == replacement)
);
let removed = execute_batch(&remapped, &[Command::Delete(replacement)]).unwrap();
assert!(removed.elements.contains_key(&item));
assert!(!removed.relations.contains_key(&role));
assert!(
matches!(removed.elements[&output].shared(), Some(SharedBody::Port { contract: PortContract::FunctionOutput { outgoing_items }, legacy: Some(LegacyPortContract { interface: None, .. }) }) if outgoing_items == &[item])
);
}
#[test]
fn transitions_create_sa_ports_and_omit_sa_implementation_from_portless_oa() {
let mut oa = Project::empty();
let a = add(&mut oa, Kind::Activity, Layer::Oa, "Send");
let b = add(&mut oa, Kind::Activity, Layer::Oa, "Receive");
let interaction = oa.relate(RelationKind::Interaction, "Operational exchange", a, b);
let mut req = TransitionRequest::new(TransitionDirection::OaToSa, vec![interaction]);
req.scope = DependencyScope::Required;
let plan = plan_transition(&oa, &req).unwrap();
assert!(plan.conflicts.is_empty(), "{:?}", plan.conflicts);
let sa = apply_transition(&oa, &plan).unwrap();
assert_eq!(
sa.elements
.values()
.filter(|e| e.kind() == Kind::Port && e.layer() == Layer::Sa)
.count(),
2
);
assert_eq!(
sa.elements
.values()
.filter(|e| e.kind() == Kind::Port && e.layer() == Layer::Oa)
.count(),
0
);
let (mut p, a, b, _, fe) = fixture();
allocate(&mut p, a, b);
let component_req = component_request(&p, fe, ComponentPortKind::Flow);
let boundary = boundary_id(&component_req);
p = execute_batch(&p, &plan_component_exchange(&p, &component_req).unwrap()).unwrap();
let mut reverse = TransitionRequest::new(TransitionDirection::SaToOa, vec![fe, boundary]);
reverse.scope = DependencyScope::Required;
let plan = plan_transition(&p, &reverse).unwrap();
assert!(plan.conflicts.is_empty(), "{:?}", plan.conflicts);
assert!(plan.warnings.iter().any(|w| w.contains("SA-only omission")));
let result = apply_transition(&p, &plan).unwrap();
assert_eq!(
result
.elements
.values()
.filter(|e| e.kind() == Kind::Port && e.layer() == Layer::Oa)
.count(),
0
);
assert!(
result
.relations
.values()
.any(|r| r.kind() == RelationKind::Interaction)
);
for e in p.elements.values() {
assert_eq!(result.elements[&e.id], *e);
}
for r in p.relations.values() {
assert_eq!(result.relations[&r.id], *r);
}
// A required FE dependency is portless in OA even while an SA endpoint
// retains unresolved legacy semantics. No incidental legacy copy is made.
let (mut legacy, _, _, _, legacy_fe) = fixture();
let legacy_port = endpoints(&legacy, legacy_fe).0;
*legacy
.elements
.get_mut(&legacy_port)
.unwrap()
.shared_mut()
.unwrap() = SharedBody::Port {
contract: PortContract::Legacy,
legacy: Some(LegacyPortContract {
direction: PortDirection::InOut,
interface: None,
}),
};
let mut req = TransitionRequest::new(TransitionDirection::SaToOa, vec![legacy_fe]);
req.scope = DependencyScope::Required;
let plan = plan_transition(&legacy, &req).unwrap();
assert!(plan.conflicts.is_empty(), "{:?}", plan.conflicts);
let result = apply_transition(&legacy, &plan).unwrap();
assert!(
!result
.elements
.values()
.any(|e| e.kind() == Kind::Port && e.layer() == Layer::Oa)
);
}
#[test]
fn projection_anchors_are_contract_aware_and_unoriented_connections_keep_their_identity() {
let (mut p, a, b, _, fe) = fixture();
let (system, actor) = allocate(&mut p, a, b);
let req = component_request(&p, fe, ComponentPortKind::Standard);
let boundary = boundary_id(&req);
p = execute_batch(&p, &plan_component_exchange(&p, &req).unwrap()).unwrap();
let diagram = p.ensure_diagram(Layer::Sa);
for (index, id) in [system, actor, a, b].into_iter().enumerate() {
p.place(
diagram,
id,
Position {
x: 50. + index as f32 * 280.,
y: 100.,
},
);
}
let view = project_view(&p, diagram);
let edge = view.edges.iter().find(|e| e.id == boundary).unwrap();
assert!(!edge.oriented);
assert_eq!(edge.source_port, Some(p.relations[&boundary].source()));
assert!(
view.ports
.iter()
.filter(|port| [
p.relations[&boundary].source(),
p.relations[&boundary].target()
]
.contains(&port.element))
.all(|port| port.side == PortAnchorSide::Bottom)
);
let (output, input) = endpoints(&p, fe);
assert_eq!(
view.ports
.iter()
.find(|port| port.element == output)
.unwrap()
.side,
PortAnchorSide::Right
);
assert_eq!(
view.ports
.iter()
.find(|port| port.element == input)
.unwrap()
.side,
PortAnchorSide::Left
);
assert!(
p.diagrams[&diagram]
.nodes
.values()
.all(|node| p.elements[&node.element].kind() != Kind::Port)
);
}
+41 -3
View File
@@ -122,6 +122,26 @@ fn fixture(layer: Layer) -> Fixture {
| RelationBody::FunctionalExchange { items, .. } => items.push(item),
_ => unreachable!(),
}
if let RelationBody::FunctionalExchange {
source_port,
target_port,
..
} = p.relations[&exchange].body
{
for port in [source_port, target_port] {
match p.elements.get_mut(&port).unwrap().shared_mut() {
Some(SharedBody::Port {
contract: PortContract::FunctionInput { incoming_items },
..
}) => incoming_items.push(item),
Some(SharedBody::Port {
contract: PortContract::FunctionOutput { outgoing_items },
..
}) => outgoing_items.push(item),
_ => unreachable!(),
}
}
}
let call = add_occurrence(
&mut p,
scenario,
@@ -1010,9 +1030,27 @@ fn role_lifelines_require_explicit_work_assignment_not_just_a_shared_realizing_e
validation::semantic_representation_compatible(&p, target, work),
Some(false)
);
let port = p
.add_element(Kind::Port, Layer::Oa, "Controller port", Some(entity))
.unwrap();
let port = Id::new_v4();
p.elements.insert(
port,
Element {
id: port,
alias: "legacy_controller_port".into(),
name: "Controller port".into(),
description: String::new(),
parent: Some(entity),
body: ElementBody::shared_in(
Layer::Oa,
SharedBody::Port {
contract: PortContract::Legacy,
legacy: Some(LegacyPortContract {
direction: PortDirection::InOut,
interface: None,
}),
},
),
},
);
assert_eq!(
validation::semantic_representation_compatible(&p, entity, port),
Some(true)
+53 -10
View File
@@ -334,11 +334,29 @@ fn items_ports_interfaces_and_exchange_allocation_are_typed() {
.shared_mut()
.unwrap() = SharedBody::Interface { items: vec![item] };
let port = |p: &mut Project, name: &str, parent, direction| {
let id = add(p, Kind::Port, Layer::Sa, name, Some(parent));
*p.elements.get_mut(&id).unwrap().shared_mut().unwrap() = SharedBody::Port {
direction,
interface: Some(interface),
let id = Id::new_v4();
let contract = if p.elements[&parent].kind() == Kind::Function {
if direction == PortDirection::Out {
PortContract::FunctionOutput {
outgoing_items: vec![item],
}
} else {
PortContract::FunctionInput {
incoming_items: vec![item],
}
}
} else {
PortContract::Component {
kind: ComponentPortKind::Flow,
orientation: if direction == PortDirection::Out {
ComponentPortOrientation::Out
} else {
ComponentPortOrientation::In
},
}
};
p.elements
.insert(id, make_port(p, id, parent, name, contract).unwrap());
id
};
let out = port(&mut p, "System output", system, PortDirection::Out);
@@ -355,10 +373,14 @@ fn items_ports_interfaces_and_exchange_allocation_are_typed() {
p.relations.get_mut(&exchange).unwrap().body = RelationBody::FunctionalExchange {
source: f1,
target: f2,
source_port: Some(fp1),
target_port: Some(fp2),
source_port: fp1,
target_port: fp2,
items: vec![item],
};
p.relate(RelationKind::Allocation, "Send allocation", system, f1);
p.relate(RelationKind::Allocation, "Receive allocation", actor, f2);
p.relate(RelationKind::PortAllocation, "Output allocation", out, fp1);
p.relate(RelationKind::PortAllocation, "Input allocation", input, fp2);
p.relate(
RelationKind::ExchangeAllocation,
"Carried on",
@@ -381,7 +403,7 @@ fn items_ports_interfaces_and_exchange_allocation_are_typed() {
if let RelationBody::FunctionalExchange { source_port, .. } =
&mut p.relations.get_mut(&exchange).unwrap().body
{
*source_port = Some(input);
*source_port = input;
}
rejected(&p, "functional_port");
}
@@ -721,15 +743,36 @@ fn reidentifying_data_types_and_function_ports_updates_every_typed_reference() {
};
let function = add(&mut p, Kind::Function, Layer::Sa, "Function", None);
let other = add(&mut p, Kind::Function, Layer::Sa, "Other", None);
let port = add(&mut p, Kind::Port, Layer::Sa, "Output", Some(function));
let port = Id::new_v4();
p.elements.insert(
port,
make_port(
&p,
port,
function,
"Output",
PortContract::FunctionOutput {
outgoing_items: vec![item],
},
)
.unwrap(),
);
let rel = p.relate(RelationKind::FunctionalExchange, "Data", function, other);
if let RelationBody::FunctionalExchange {
source_port, items, ..
} = &mut p.relations.get_mut(&rel).unwrap().body
{
*source_port = Some(port);
*source_port = port;
items.push(item);
}
if let RelationBody::FunctionalExchange { target_port, .. } = p.relations[&rel].body
&& let Some(SharedBody::Port {
contract: PortContract::FunctionInput { incoming_items },
..
}) = p.elements.get_mut(&target_port).unwrap().shared_mut()
{
incoming_items.push(item);
}
let dtype_new = Id::new_v4();
let port_new = Id::new_v4();
let result = execute_batch(
@@ -744,7 +787,7 @@ fn reidentifying_data_types_and_function_ports_updates_every_typed_reference() {
matches!(result.elements[&field].shared(),Some(SharedBody::DataField{type_ref:TypeRef::Named(id),..})if *id==dtype_new)
);
assert!(
matches!(&result.relations[&rel].body,RelationBody::FunctionalExchange{source_port:Some(id),..}if *id==port_new)
matches!(&result.relations[&rel].body,RelationBody::FunctionalExchange{source_port:id,..}if *id==port_new)
);
}