Integrate typed port authoring and reviewable exchange workflows
This commit is contained in:
@@ -6,7 +6,9 @@ use crate::{
|
||||
};
|
||||
use iced::{
|
||||
Element, Fill,
|
||||
widget::{Space, button, column, pick_list, row, scrollable, text, text_editor, text_input},
|
||||
widget::{
|
||||
Space, button, checkbox, column, pick_list, row, scrollable, text, text_editor, text_input,
|
||||
},
|
||||
};
|
||||
use oa_core::{transitions::*, *};
|
||||
use std::collections::BTreeSet;
|
||||
@@ -25,6 +27,10 @@ impl std::fmt::Display for RelationAction {
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Event {
|
||||
Port(crate::port_workflow::Event),
|
||||
ExchangeItem(Id, bool),
|
||||
Endpoint(bool, EndpointChoice),
|
||||
Oriented(bool),
|
||||
Cancel,
|
||||
Apply,
|
||||
RelationName(String),
|
||||
@@ -39,7 +45,8 @@ pub enum Event {
|
||||
Preview,
|
||||
}
|
||||
pub enum Workflow {
|
||||
Relation(Relationship),
|
||||
Ports(Box<crate::port_workflow::State>),
|
||||
Relation(Box<Relationship>),
|
||||
ManualTrace {
|
||||
name: String,
|
||||
source: Choice,
|
||||
@@ -48,6 +55,7 @@ pub enum Workflow {
|
||||
Transition(Box<Transition>),
|
||||
}
|
||||
pub struct Relationship {
|
||||
id: Id,
|
||||
pub name: String,
|
||||
pub kind: RelationKind,
|
||||
pub source: Option<Choice>,
|
||||
@@ -56,6 +64,60 @@ pub struct Relationship {
|
||||
layer: Layer,
|
||||
sources: Vec<Choice>,
|
||||
targets: Vec<Choice>,
|
||||
items: Vec<Id>,
|
||||
source_port: EndpointChoice,
|
||||
target_port: EndpointChoice,
|
||||
oriented: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct EndpointChoice {
|
||||
pub choice: PortChoice,
|
||||
pub label: String,
|
||||
}
|
||||
impl std::fmt::Display for EndpointChoice {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(&self.label)
|
||||
}
|
||||
}
|
||||
pub(crate) fn automatic_port() -> EndpointChoice {
|
||||
EndpointChoice {
|
||||
choice: PortChoice::Auto,
|
||||
label: "Automatic · reuse only if unique".into(),
|
||||
}
|
||||
}
|
||||
fn endpoint_choices(
|
||||
p: &Project,
|
||||
owner: Option<Id>,
|
||||
outgoing: bool,
|
||||
items: &[Id],
|
||||
) -> Vec<EndpointChoice> {
|
||||
let mut choices = vec![
|
||||
automatic_port(),
|
||||
EndpointChoice {
|
||||
choice: PortChoice::Create,
|
||||
label: "Create a new port".into(),
|
||||
},
|
||||
];
|
||||
choices.extend(
|
||||
p.elements
|
||||
.values()
|
||||
.filter(|e| e.parent == owner && e.layer() == Layer::Sa)
|
||||
.filter(|e| match port_contract(e) {
|
||||
Some(PortContract::FunctionInput { incoming_items }) if !outgoing => {
|
||||
items_satisfy(p, items, incoming_items)
|
||||
}
|
||||
Some(PortContract::FunctionOutput { outgoing_items }) if outgoing => {
|
||||
items_satisfy(p, items, outgoing_items)
|
||||
}
|
||||
_ => false,
|
||||
})
|
||||
.map(|e| EndpointChoice {
|
||||
choice: PortChoice::Reuse(e.id),
|
||||
label: format!("Reuse {} · @{}", e.name, e.alias),
|
||||
}),
|
||||
);
|
||||
choices
|
||||
}
|
||||
|
||||
impl Relationship {
|
||||
@@ -63,6 +125,12 @@ impl Relationship {
|
||||
if p.layer_of(id) != Some(self.layer) {
|
||||
return false;
|
||||
}
|
||||
if matches!(
|
||||
self.kind,
|
||||
RelationKind::InterfaceProvision | RelationKind::InterfaceRequirement
|
||||
) {
|
||||
return validation::can_create_interface_role(p, id);
|
||||
}
|
||||
if self.kind == RelationKind::ExchangeAllocation {
|
||||
return p
|
||||
.relations
|
||||
@@ -91,16 +159,45 @@ impl Relationship {
|
||||
.keys()
|
||||
.chain(p.relations.keys())
|
||||
.copied()
|
||||
.filter(|id| {
|
||||
self.source
|
||||
.as_ref()
|
||||
.is_some_and(|source| validation::can_connect(p, self.kind, source.id, *id))
|
||||
})
|
||||
.filter(|id| self.accepts_target(p, *id))
|
||||
.map(|id| choice(p, id))
|
||||
.collect();
|
||||
self.targets.sort_by(|a, b| a.label.cmp(&b.label));
|
||||
}
|
||||
|
||||
fn accepts_target(&self, p: &Project, target: Id) -> bool {
|
||||
let Some(source) = self.source.as_ref().map(|source| source.id) else {
|
||||
return false;
|
||||
};
|
||||
let directed = validation::can_connect(p, self.kind, source, target);
|
||||
if self.kind != RelationKind::BoundaryExchange || self.oriented || directed {
|
||||
return directed;
|
||||
}
|
||||
if ![source, target].iter().all(|id| {
|
||||
p.elements
|
||||
.get(id)
|
||||
.is_some_and(|element| element.kind() == Kind::Port)
|
||||
}) {
|
||||
return false;
|
||||
}
|
||||
// An unoriented connection may use the reverse stored endpoint order.
|
||||
// Ask the exact core rules, including preserved Legacy restrictions;
|
||||
// reversing can_connect alone would incorrectly relax those rules.
|
||||
let candidate = Relation {
|
||||
id: self.id,
|
||||
alias: p.fresh_alias("connection", "relation", self.id),
|
||||
name: "Connection".into(),
|
||||
description: String::new(),
|
||||
body: RelationBody::BoundaryExchange {
|
||||
source,
|
||||
target,
|
||||
oriented: false,
|
||||
items: vec![],
|
||||
},
|
||||
};
|
||||
execute_batch(p, &[Command::CreateRelation(candidate)]).is_ok()
|
||||
}
|
||||
|
||||
fn engineering_name(&self, p: &Project, source: Id, target: Id) -> String {
|
||||
if !self.name.trim().is_empty() {
|
||||
return self.name.clone();
|
||||
@@ -118,6 +215,64 @@ impl Relationship {
|
||||
};
|
||||
format!("{} {verb} {}", p.named(source), p.named(target))
|
||||
}
|
||||
fn default_orientation(&mut self, p: &Project) {
|
||||
self.oriented = !self
|
||||
.source
|
||||
.as_ref()
|
||||
.and_then(|v| p.elements.get(&v.id))
|
||||
.is_some_and(|e| {
|
||||
matches!(
|
||||
port_contract(e),
|
||||
Some(PortContract::Component {
|
||||
kind: ComponentPortKind::Standard,
|
||||
..
|
||||
})
|
||||
)
|
||||
});
|
||||
}
|
||||
fn commands(&self, p: &Project) -> Result<Vec<Command>, String> {
|
||||
let a = self.source.as_ref().ok_or("Choose a source.")?.id;
|
||||
let b = self.target.as_ref().ok_or("Choose a target.")?.id;
|
||||
if !self.accepts_source(p, a) {
|
||||
return Err("Choose an eligible source for this relationship.".into());
|
||||
}
|
||||
let id = self.id;
|
||||
let name = self.engineering_name(p, a, b);
|
||||
if self.kind == RelationKind::FunctionalExchange {
|
||||
return plan_functional_exchange(
|
||||
p,
|
||||
&FunctionalExchangeRequest {
|
||||
id,
|
||||
alias: p.fresh_alias(&name, "exchange", id),
|
||||
name,
|
||||
description: String::new(),
|
||||
source: a,
|
||||
target: b,
|
||||
source_port: self.source_port.choice,
|
||||
target_port: self.target_port.choice,
|
||||
items: self.items.clone(),
|
||||
},
|
||||
)
|
||||
.map_err(|e| e.to_string());
|
||||
}
|
||||
let mut body = RelationBody::new(self.kind, a, b);
|
||||
if let RelationBody::CapabilityExtend { condition, .. } = &mut body {
|
||||
*condition = self.condition.text();
|
||||
}
|
||||
if let RelationBody::BoundaryExchange { oriented, .. } = &mut body {
|
||||
*oriented = self.oriented;
|
||||
}
|
||||
let commands = vec![Command::CreateRelation(Relation {
|
||||
id,
|
||||
alias: p.fresh_alias(&name, "relation", id),
|
||||
name,
|
||||
description: String::new(),
|
||||
body,
|
||||
})];
|
||||
execute_batch(p, &commands)
|
||||
.map(|_| commands)
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
}
|
||||
pub struct Transition {
|
||||
pub request: TransitionRequest,
|
||||
@@ -158,6 +313,7 @@ impl Workflow {
|
||||
incoming: bool,
|
||||
) -> Self {
|
||||
let mut draft = Relationship {
|
||||
id: Id::new_v4(),
|
||||
name: String::new(),
|
||||
kind,
|
||||
source: (!incoming).then(|| choice(p, selected)),
|
||||
@@ -166,6 +322,10 @@ impl Workflow {
|
||||
layer,
|
||||
sources: vec![],
|
||||
targets: vec![],
|
||||
items: vec![],
|
||||
source_port: automatic_port(),
|
||||
target_port: automatic_port(),
|
||||
oriented: true,
|
||||
};
|
||||
if draft
|
||||
.source
|
||||
@@ -174,8 +334,9 @@ impl Workflow {
|
||||
{
|
||||
draft.source = None;
|
||||
}
|
||||
draft.default_orientation(p);
|
||||
draft.refresh_choices(p);
|
||||
Self::Relation(draft)
|
||||
Self::Relation(Box::new(draft))
|
||||
}
|
||||
pub fn transition(p: &Project, selected: Id) -> Result<Self, String> {
|
||||
let layer = p
|
||||
@@ -195,6 +356,7 @@ impl Workflow {
|
||||
/// choices and previews never mutate the Session.
|
||||
pub fn update(&mut self, p: &Project, event: Event) -> Result<Option<Project>, String> {
|
||||
match self {
|
||||
Self::Ports(state) => return state.update(p, event),
|
||||
Self::ManualTrace {
|
||||
name,
|
||||
source,
|
||||
@@ -233,6 +395,25 @@ impl Workflow {
|
||||
_ => {}
|
||||
},
|
||||
Self::Relation(d) => match event {
|
||||
Event::Oriented(value) => {
|
||||
d.oriented = value;
|
||||
d.refresh_choices(p);
|
||||
}
|
||||
Event::ExchangeItem(id, enabled) => {
|
||||
if enabled && !d.items.contains(&id) {
|
||||
d.items.push(id);
|
||||
}
|
||||
if !enabled {
|
||||
d.items.retain(|item| *item != id);
|
||||
}
|
||||
}
|
||||
Event::Endpoint(source, choice) => {
|
||||
if source {
|
||||
d.source_port = choice;
|
||||
} else {
|
||||
d.target_port = choice;
|
||||
}
|
||||
}
|
||||
Event::RelationName(name) => d.name = name,
|
||||
Event::RelationKind(kind) => {
|
||||
d.kind = kind;
|
||||
@@ -243,18 +424,16 @@ impl Workflow {
|
||||
{
|
||||
d.source = None;
|
||||
}
|
||||
d.default_orientation(p);
|
||||
d.refresh_choices(p);
|
||||
}
|
||||
Event::Source(source) => {
|
||||
d.source = Some(source);
|
||||
if d.target.as_ref().is_some_and(|target| {
|
||||
!validation::can_connect(
|
||||
p,
|
||||
d.kind,
|
||||
d.source.as_ref().unwrap().id,
|
||||
target.id,
|
||||
)
|
||||
}) {
|
||||
d.default_orientation(p);
|
||||
if d.target
|
||||
.as_ref()
|
||||
.is_some_and(|target| !d.accepts_target(p, target.id))
|
||||
{
|
||||
d.target = None;
|
||||
}
|
||||
d.refresh_choices(p);
|
||||
@@ -262,22 +441,7 @@ impl Workflow {
|
||||
Event::Target(target) => d.target = Some(target),
|
||||
Event::Condition(action) => d.condition.perform(action),
|
||||
Event::Apply => {
|
||||
let a = d.source.as_ref().ok_or("Choose a source.")?.id;
|
||||
let b = d.target.as_ref().ok_or("Choose a target.")?.id;
|
||||
let id = Id::new_v4();
|
||||
let name = d.engineering_name(p, a, b);
|
||||
let mut body = RelationBody::new(d.kind, a, b);
|
||||
if let RelationBody::CapabilityExtend { condition, .. } = &mut body {
|
||||
*condition = d.condition.text();
|
||||
}
|
||||
let relation = Relation {
|
||||
id,
|
||||
alias: p.fresh_alias(&name, "relation", id),
|
||||
name,
|
||||
description: String::new(),
|
||||
body,
|
||||
};
|
||||
return execute_batch(p, &[Command::CreateRelation(relation)])
|
||||
return execute_batch(p, &d.commands(p)?)
|
||||
.map(Some)
|
||||
.map_err(|e| e.to_string());
|
||||
}
|
||||
@@ -430,6 +594,7 @@ impl Workflow {
|
||||
}
|
||||
pub fn title(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Ports(state) => state.title(),
|
||||
Self::ManualTrace { .. } => "Link existing counterparts",
|
||||
Self::Relation(_) => "Create relationship",
|
||||
Self::Transition(t) => {
|
||||
@@ -442,8 +607,17 @@ impl Workflow {
|
||||
}
|
||||
}
|
||||
pub fn view<'a>(&'a self, p: &'a Project) -> Element<'a, Message> {
|
||||
if let Self::Ports(state) = self {
|
||||
return state.view(p);
|
||||
}
|
||||
let mut content = column![text(self.title()).size(23)].spacing(14);
|
||||
let relation_plan = if let Self::Relation(d) = self {
|
||||
Some(d.commands(p))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
match self {
|
||||
Self::Ports(_) => unreachable!("Port dialog returned above"),
|
||||
Self::ManualTrace {
|
||||
name,
|
||||
source,
|
||||
@@ -535,6 +709,54 @@ impl Workflow {
|
||||
.padding(9),
|
||||
);
|
||||
}
|
||||
if d.kind == RelationKind::BoundaryExchange {
|
||||
content = content.push(
|
||||
checkbox(d.oriented)
|
||||
.label("Oriented component exchange")
|
||||
.on_toggle(|v| Message::Workflow(Event::Oriented(v))),
|
||||
);
|
||||
}
|
||||
if d.kind == RelationKind::FunctionalExchange {
|
||||
content = content.push(text("Function ports and carried items").size(14))
|
||||
.push(text("Automatic uses one compatible typed port, or creates a new one. Creating this exchange and its ports is one undoable change.").size(12));
|
||||
for source in [true, false] {
|
||||
let owner = if source { &d.source } else { &d.target };
|
||||
let selected = if source {
|
||||
&d.source_port
|
||||
} else {
|
||||
&d.target_port
|
||||
};
|
||||
let mut options =
|
||||
endpoint_choices(p, owner.as_ref().map(|v| v.id), source, &d.items);
|
||||
if !options.contains(selected) {
|
||||
options.push(selected.clone());
|
||||
}
|
||||
content = content
|
||||
.push(text(if source { "Output port" } else { "Input port" }).size(12))
|
||||
.push(
|
||||
pick_list(options, Some(selected.clone()), move |v| {
|
||||
Message::Workflow(Event::Endpoint(source, v))
|
||||
})
|
||||
.padding(8)
|
||||
.text_size(12)
|
||||
.width(Fill),
|
||||
);
|
||||
}
|
||||
for item in p
|
||||
.elements
|
||||
.values()
|
||||
.filter(|e| e.layer() == d.layer && e.kind() == Kind::ExchangeItem)
|
||||
{
|
||||
let id = item.id;
|
||||
content = content.push(
|
||||
checkbox(d.items.contains(&id))
|
||||
.label(format!("{} · @{}", item.name, item.alias))
|
||||
.on_toggle(move |enabled| {
|
||||
Message::Workflow(Event::ExchangeItem(id, enabled))
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
content = content.push(
|
||||
text_input("Name (generated from endpoints when blank)", &d.name)
|
||||
.on_input(|v| Message::Workflow(Event::RelationName(v)))
|
||||
@@ -858,14 +1080,18 @@ impl Workflow {
|
||||
}
|
||||
}
|
||||
let enabled = match self {
|
||||
Self::Ports(_) => false,
|
||||
Self::ManualTrace { name, target, .. } => target.is_some() && !name.trim().is_empty(),
|
||||
Self::Relation(d) => d
|
||||
.source
|
||||
.as_ref()
|
||||
.zip(d.target.as_ref())
|
||||
.is_some_and(|(a, b)| validation::can_connect(p, d.kind, a.id, b.id)),
|
||||
Self::Relation(_) => relation_plan.as_ref().is_some_and(Result::is_ok),
|
||||
Self::Transition(d) => d.plan.conflicts.is_empty(),
|
||||
};
|
||||
if let Some(Err(error)) = relation_plan {
|
||||
content = content.push(
|
||||
text(error)
|
||||
.size(12)
|
||||
.color(iced::Color::from_rgb8(178, 54, 58)),
|
||||
);
|
||||
}
|
||||
column![
|
||||
scrollable(content.padding(iced::Padding {
|
||||
right: 14.,
|
||||
@@ -898,3 +1124,217 @@ fn choice(p: &Project, id: Id) -> Choice {
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod port_authoring_tests {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn boundary_picker_recomputes_for_unoriented_reverse_endpoints_without_relaxing_contracts() {
|
||||
let mut p = Project::empty();
|
||||
let system = p
|
||||
.add_element(Kind::System, Layer::Sa, "System", None)
|
||||
.unwrap();
|
||||
let actor = p
|
||||
.add_element(Kind::SystemActor, Layer::Sa, "Actor", None)
|
||||
.unwrap();
|
||||
let mut create = |owner, name, contract| {
|
||||
let id = Id::new_v4();
|
||||
let port = make_port(&p, id, owner, name, contract).unwrap();
|
||||
p.elements.insert(id, port);
|
||||
id
|
||||
};
|
||||
let flow = |orientation| PortContract::Component {
|
||||
kind: ComponentPortKind::Flow,
|
||||
orientation,
|
||||
};
|
||||
let input = create(system, "Receive", flow(ComponentPortOrientation::In));
|
||||
let output = create(actor, "Send", flow(ComponentPortOrientation::Out));
|
||||
let incompatible = create(actor, "Other input", flow(ComponentPortOrientation::In));
|
||||
let same_owner = create(system, "Local output", flow(ComponentPortOrientation::Out));
|
||||
let standard = create(
|
||||
actor,
|
||||
"Standard",
|
||||
PortContract::Component {
|
||||
kind: ComponentPortKind::Standard,
|
||||
orientation: ComponentPortOrientation::Unset,
|
||||
},
|
||||
);
|
||||
let legacy = create(actor, "Preserved output", PortContract::Legacy);
|
||||
if let Some(SharedBody::Port { legacy, .. }) =
|
||||
p.elements.get_mut(&legacy).unwrap().shared_mut()
|
||||
{
|
||||
*legacy = Some(LegacyPortContract {
|
||||
direction: PortDirection::Out,
|
||||
interface: None,
|
||||
});
|
||||
}
|
||||
let before = p.clone();
|
||||
let mut workflow = Workflow::relationship_kind(
|
||||
&p,
|
||||
input,
|
||||
Layer::Sa,
|
||||
RelationKind::BoundaryExchange,
|
||||
false,
|
||||
);
|
||||
let Workflow::Relation(draft) = &workflow else {
|
||||
unreachable!()
|
||||
};
|
||||
assert!(!draft.targets.iter().any(|choice| choice.id == output));
|
||||
workflow.update(&p, Event::Oriented(false)).unwrap();
|
||||
let Workflow::Relation(draft) = &workflow else {
|
||||
unreachable!()
|
||||
};
|
||||
assert!(draft.targets.iter().any(|choice| choice.id == output));
|
||||
for invalid in [input, incompatible, same_owner, standard, legacy] {
|
||||
assert!(
|
||||
!draft.targets.iter().any(|choice| choice.id == invalid),
|
||||
"Rejected endpoint {invalid}"
|
||||
);
|
||||
}
|
||||
workflow
|
||||
.update(&p, Event::Target(choice(&p, output)))
|
||||
.unwrap();
|
||||
let Workflow::Relation(draft) = &workflow else {
|
||||
unreachable!()
|
||||
};
|
||||
assert!(draft.commands(&p).is_ok());
|
||||
workflow.update(&p, Event::Oriented(true)).unwrap();
|
||||
let Workflow::Relation(draft) = &workflow else {
|
||||
unreachable!()
|
||||
};
|
||||
assert_eq!(
|
||||
draft.target.as_ref().unwrap().id,
|
||||
output,
|
||||
"Keep the selected identity available for correction"
|
||||
);
|
||||
assert!(!draft.targets.iter().any(|choice| choice.id == output));
|
||||
assert!(
|
||||
draft.commands(&p).is_err(),
|
||||
"The actual Apply gate rejects the incompatible draft"
|
||||
);
|
||||
assert!(workflow.update(&p, Event::Apply).is_err());
|
||||
assert_eq!(p, before);
|
||||
workflow.update(&p, Event::Oriented(false)).unwrap();
|
||||
let accepted = workflow.update(&p, Event::Apply).unwrap().unwrap();
|
||||
assert!(accepted.relations.values().any(|r| matches!(r.body, RelationBody::BoundaryExchange { source, target, oriented: false, .. } if source == input && target == output)));
|
||||
}
|
||||
#[test]
|
||||
fn fresh_functions_are_actual_picker_targets_and_plan_enables_apply() {
|
||||
let mut p = Project::empty();
|
||||
let a = p
|
||||
.add_element(Kind::Function, Layer::Sa, "Source", None)
|
||||
.unwrap();
|
||||
let b = p
|
||||
.add_element(Kind::Function, Layer::Sa, "Target", None)
|
||||
.unwrap();
|
||||
let oa = p
|
||||
.add_element(Kind::Activity, Layer::Oa, "Outside layer", None)
|
||||
.unwrap();
|
||||
let mut w =
|
||||
Workflow::relationship_kind(&p, a, Layer::Sa, RelationKind::FunctionalExchange, false);
|
||||
let Workflow::Relation(d) = &w else {
|
||||
unreachable!()
|
||||
};
|
||||
assert!(d.targets.iter().any(|v| v.id == b));
|
||||
assert!(!d.targets.iter().any(|v| v.id == a || v.id == oa));
|
||||
assert!(d.commands(&p).is_err());
|
||||
w.update(&p, Event::Target(choice(&p, b))).unwrap();
|
||||
let Workflow::Relation(d) = &w else {
|
||||
unreachable!()
|
||||
};
|
||||
assert!(
|
||||
d.commands(&p).is_ok(),
|
||||
"The same plan enables the actual Apply control"
|
||||
);
|
||||
let accepted = w.update(&p, Event::Apply).unwrap().unwrap();
|
||||
assert_eq!(
|
||||
accepted
|
||||
.elements
|
||||
.values()
|
||||
.filter(|e| e.kind() == Kind::Port)
|
||||
.count(),
|
||||
2
|
||||
);
|
||||
let invalid = Relation {
|
||||
id: Id::new_v4(),
|
||||
alias: "unbound".into(),
|
||||
name: "Unbound".into(),
|
||||
description: String::new(),
|
||||
body: RelationBody::new(RelationKind::FunctionalExchange, a, b),
|
||||
};
|
||||
assert!(
|
||||
execute_batch(&p, &[Command::CreateRelation(invalid)]).is_err(),
|
||||
"Exact records remain strict"
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn ordinary_standard_connections_default_unoriented_and_new_roles_exclude_legacy() {
|
||||
let mut p = Project::empty();
|
||||
let a = p
|
||||
.add_element(Kind::System, Layer::Sa, "System", None)
|
||||
.unwrap();
|
||||
let b = p
|
||||
.add_element(Kind::SystemActor, Layer::Sa, "Actor", None)
|
||||
.unwrap();
|
||||
let f = p
|
||||
.add_element(Kind::Function, Layer::Sa, "Function", None)
|
||||
.unwrap();
|
||||
let interface = p
|
||||
.add_element(Kind::Interface, Layer::Sa, "Interface", None)
|
||||
.unwrap();
|
||||
let mut ids = vec![];
|
||||
for owner in [a, b] {
|
||||
let id = Id::new_v4();
|
||||
let port = make_port(
|
||||
&p,
|
||||
id,
|
||||
owner,
|
||||
"Standard",
|
||||
PortContract::Component {
|
||||
kind: ComponentPortKind::Standard,
|
||||
orientation: ComponentPortOrientation::Unset,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
p.elements.insert(id, port);
|
||||
ids.push(id);
|
||||
}
|
||||
let legacy = Id::new_v4();
|
||||
let mut port = make_port(&p, legacy, f, "Preserved", PortContract::Legacy).unwrap();
|
||||
if let Some(SharedBody::Port { legacy, .. }) = port.shared_mut() {
|
||||
*legacy = Some(LegacyPortContract {
|
||||
direction: PortDirection::In,
|
||||
interface: Some(interface),
|
||||
});
|
||||
}
|
||||
p.elements.insert(legacy, port);
|
||||
let mut w = Workflow::relationship_kind(
|
||||
&p,
|
||||
ids[0],
|
||||
Layer::Sa,
|
||||
RelationKind::BoundaryExchange,
|
||||
false,
|
||||
);
|
||||
w.update(&p, Event::Target(choice(&p, ids[1]))).unwrap();
|
||||
let accepted = w.update(&p, Event::Apply).unwrap().unwrap();
|
||||
assert!(accepted.relations.values().any(|r| matches!(
|
||||
r.body,
|
||||
RelationBody::BoundaryExchange {
|
||||
oriented: false,
|
||||
..
|
||||
}
|
||||
)));
|
||||
for kind in [
|
||||
RelationKind::InterfaceProvision,
|
||||
RelationKind::InterfaceRequirement,
|
||||
] {
|
||||
let w = Workflow::relationship_kind(&p, legacy, Layer::Sa, kind, false);
|
||||
let Workflow::Relation(d) = w else {
|
||||
unreachable!()
|
||||
};
|
||||
assert!(d.source.is_none());
|
||||
assert!(!d.sources.iter().any(|v| v.id == legacy));
|
||||
assert!(d.sources.iter().any(|v| v.id == ids[0]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user