819 lines
30 KiB
Rust
819 lines
30 KiB
Rust
//! Contextual controls for the closed port model. Draft-only preservation never
|
|
//! changes the persisted contract or substitutes for candidate validation.
|
|
use super::*;
|
|
|
|
fn constructor(value: &Value) -> Option<&str> {
|
|
value
|
|
.as_str()
|
|
.or_else(|| value.as_object()?.keys().next().map(String::as_str))
|
|
}
|
|
|
|
pub(super) fn switch_contract(
|
|
current: &Value,
|
|
proposed: Value,
|
|
saved: &mut BTreeMap<String, Value>,
|
|
) -> Value {
|
|
let (Some(previous), Some(next)) = (constructor(current), constructor(&proposed)) else {
|
|
return proposed;
|
|
};
|
|
if previous == next {
|
|
return proposed;
|
|
}
|
|
saved.insert(previous.into(), current.clone());
|
|
if let Some(existing) = saved.get(next) {
|
|
return existing.clone();
|
|
}
|
|
// Direction changes retain the item declaration; switching constructors
|
|
// must not silently replace an authored payload with an empty template.
|
|
let items = current
|
|
.pointer("/FunctionInput/incoming_items")
|
|
.or_else(|| current.pointer("/FunctionOutput/outgoing_items"));
|
|
match (next, items) {
|
|
("FunctionInput", Some(items)) => json!({"FunctionInput":{"incoming_items":items}}),
|
|
("FunctionOutput", Some(items)) => json!({"FunctionOutput":{"outgoing_items":items}}),
|
|
_ => proposed,
|
|
}
|
|
}
|
|
|
|
pub(super) fn variants(
|
|
draft: &Draft,
|
|
project: &Project,
|
|
path: &str,
|
|
_value: &Value,
|
|
) -> Option<Vec<(String, Value)>> {
|
|
if path.ends_with("/Port/contract") {
|
|
let current = draft.context_value();
|
|
let sa = current.pointer("/body/Sa").is_some();
|
|
if !sa {
|
|
return Some(vec![]);
|
|
}
|
|
let owner = current
|
|
.get("parent")
|
|
.and_then(Value::as_str)
|
|
.and_then(|id| id.parse::<Id>().ok())
|
|
.and_then(|id| project.elements.get(&id));
|
|
let mut choices = vec![];
|
|
if owner.is_none_or(|owner| owner.kind() == Kind::Function) {
|
|
choices.push((
|
|
"Function input".into(),
|
|
json!({"FunctionInput":{"incoming_items":[]}}),
|
|
));
|
|
choices.push((
|
|
"Function output".into(),
|
|
json!({"FunctionOutput":{"outgoing_items":[]}}),
|
|
));
|
|
}
|
|
if owner.is_none_or(|owner| matches!(owner.kind(), Kind::System | Kind::SystemActor)) {
|
|
choices.push((
|
|
"Component port".into(),
|
|
json!({"Component":{"kind":"Standard","orientation":"Unset"}}),
|
|
));
|
|
}
|
|
return Some(choices);
|
|
}
|
|
if path.ends_with("/Port/contract/Component/kind") {
|
|
return Some(
|
|
["Standard", "Flow"]
|
|
.into_iter()
|
|
.map(|kind| (kind.into(), json!(kind)))
|
|
.collect(),
|
|
);
|
|
}
|
|
if path.ends_with("/Port/contract/Component/orientation") {
|
|
let current = draft.context_value();
|
|
let kind_path = path.strip_suffix("orientation").unwrap().to_owned() + "kind";
|
|
let orientations: &[&str] =
|
|
if current.pointer(&kind_path).and_then(Value::as_str) == Some("Standard") {
|
|
&["Unset"]
|
|
} else {
|
|
&["Unset", "In", "Out", "InOut"]
|
|
};
|
|
return Some(
|
|
orientations
|
|
.iter()
|
|
.map(|orientation| {
|
|
let label = if *orientation == "Unset" && orientations.len() == 1 {
|
|
"Not applicable to Standard".into()
|
|
} else {
|
|
friendly(orientation)
|
|
};
|
|
(label, json!(orientation))
|
|
})
|
|
.collect(),
|
|
);
|
|
}
|
|
None
|
|
}
|
|
|
|
pub(super) fn legacy_lines(value: &Value, project: &Project) -> Vec<String> {
|
|
let direction = value
|
|
.get("direction")
|
|
.and_then(Value::as_str)
|
|
.map(friendly)
|
|
.unwrap_or_else(|| "Unspecified".into());
|
|
let interface = value
|
|
.get("interface")
|
|
.and_then(Value::as_str)
|
|
.and_then(|id| id.parse::<Id>().ok());
|
|
let association = interface
|
|
.map(|id| {
|
|
project
|
|
.elements
|
|
.get(&id)
|
|
.map(|e| format!("{} · @{}", e.name, e.alias))
|
|
.unwrap_or_else(|| format!("Missing interface · {id}"))
|
|
})
|
|
.unwrap_or_else(|| "No interface was associated".into());
|
|
vec![format!("Original direction: {direction}"), format!("Original interface: {association}"), "Retained from the generic model; this association does not declare a provided or required interface.".into()]
|
|
}
|
|
|
|
fn component_items_fit(project: &Project, port: Id, items: &[Id]) -> bool {
|
|
let interfaces: Vec<_> = project
|
|
.relations
|
|
.values()
|
|
.filter(|role| {
|
|
matches!(
|
|
role.kind(),
|
|
RelationKind::InterfaceProvision | RelationKind::InterfaceRequirement
|
|
) && role.source() == port
|
|
})
|
|
.map(Relation::target)
|
|
.collect();
|
|
if interfaces.is_empty() {
|
|
return true;
|
|
}
|
|
let declared: Vec<_> = interfaces
|
|
.iter()
|
|
.flat_map(|id| data::interface_items(project, *id))
|
|
.collect();
|
|
items_satisfy(project, items, &declared)
|
|
}
|
|
fn boundary_pair(project: &Project, relation: &Relation, source: Id, target: Id) -> bool {
|
|
let RelationBody::BoundaryExchange {
|
|
oriented, items, ..
|
|
} = &relation.body
|
|
else {
|
|
return false;
|
|
};
|
|
let (Some(a), Some(b)) = (project.elements.get(&source), project.elements.get(&target)) else {
|
|
return false;
|
|
};
|
|
if source == target
|
|
|| a.layer() != b.layer()
|
|
|| a.parent == b.parent
|
|
|| [a, b].iter().any(|port| {
|
|
!port
|
|
.parent
|
|
.and_then(|id| project.elements.get(&id))
|
|
.is_some_and(|owner| owner.kind().is_performer())
|
|
})
|
|
{
|
|
return false;
|
|
}
|
|
let (Some(ac), Some(bc)) = (port_contract(a), port_contract(b)) else {
|
|
return false;
|
|
};
|
|
let direction = |e: &Element, contract: &PortContract, outgoing| match contract {
|
|
PortContract::Component { .. } => !*oriented || component_allows(contract, outgoing),
|
|
PortContract::Legacy => {
|
|
matches!(e.shared(), Some(SharedBody::Port { legacy: Some(LegacyPortContract { direction, .. }), .. }) if if outgoing { *direction != PortDirection::In } else { *direction != PortDirection::Out })
|
|
}
|
|
_ => false,
|
|
};
|
|
if !direction(a, ac, true) || !direction(b, bc, false) {
|
|
return false;
|
|
}
|
|
if let (PortContract::Component { kind: ak, .. }, PortContract::Component { kind: bk, .. }) =
|
|
(ac, bc)
|
|
{
|
|
if ak != bk {
|
|
return false;
|
|
}
|
|
if !*oriented
|
|
&& *ak == ComponentPortKind::Flow
|
|
&& !((component_allows(ac, true) && component_allows(bc, false))
|
|
|| (component_allows(ac, false) && component_allows(bc, true)))
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
component_items_fit(project, source, items) && component_items_fit(project, target, items)
|
|
}
|
|
|
|
pub(super) fn relation_endpoints(
|
|
project: &Project,
|
|
relation: &Relation,
|
|
field: &str,
|
|
original: Id,
|
|
) -> Option<Vec<Id>> {
|
|
if !matches!(
|
|
relation.kind(),
|
|
RelationKind::FunctionalExchange
|
|
| RelationKind::BoundaryExchange
|
|
| RelationKind::PortAllocation
|
|
| RelationKind::InterfaceProvision
|
|
| RelationKind::InterfaceRequirement
|
|
) {
|
|
return None;
|
|
}
|
|
let source_field = field == "source";
|
|
let other = if source_field {
|
|
relation.target()
|
|
} else {
|
|
relation.source()
|
|
};
|
|
if relation.kind() == RelationKind::FunctionalExchange {
|
|
return Some(
|
|
project
|
|
.elements
|
|
.values()
|
|
.filter(|e| e.kind() == Kind::Function && e.layer() == Layer::Sa && e.id != other)
|
|
.map(|e| e.id)
|
|
.collect(),
|
|
);
|
|
}
|
|
let eligible = |e: &Element, source: bool| match relation.kind() {
|
|
RelationKind::BoundaryExchange => {
|
|
e.layer() == Layer::Sa
|
|
&& matches!(port_contract(e), Some(PortContract::Component { .. }))
|
|
}
|
|
RelationKind::PortAllocation => {
|
|
e.layer() == Layer::Sa
|
|
&& if source {
|
|
matches!(port_contract(e), Some(PortContract::Component { .. }))
|
|
} else {
|
|
port_contract(e).is_some_and(PortContract::is_function)
|
|
}
|
|
}
|
|
RelationKind::InterfaceProvision | RelationKind::InterfaceRequirement => {
|
|
if source {
|
|
validation::can_create_interface_role(project, e.id)
|
|
} else {
|
|
e.kind() == Kind::Interface
|
|
}
|
|
}
|
|
_ => false,
|
|
};
|
|
let mut context = project.clone();
|
|
context.relations.remove(&original);
|
|
let permits = |candidate: Id, peer: Id| {
|
|
let (source, target) = if source_field {
|
|
(candidate, peer)
|
|
} else {
|
|
(peer, candidate)
|
|
};
|
|
if relation.kind() == RelationKind::BoundaryExchange {
|
|
boundary_pair(project, relation, source, target)
|
|
} else {
|
|
validation::can_connect(&context, relation.kind(), source, target)
|
|
}
|
|
};
|
|
Some(
|
|
project
|
|
.elements
|
|
.values()
|
|
.filter(|e| eligible(e, source_field))
|
|
.filter(|e| {
|
|
if project.elements.contains_key(&other) {
|
|
permits(e.id, other)
|
|
} else {
|
|
project
|
|
.elements
|
|
.values()
|
|
.filter(|peer| eligible(peer, !source_field))
|
|
.any(|peer| permits(e.id, peer.id))
|
|
}
|
|
})
|
|
.map(|e| e.id)
|
|
.collect(),
|
|
)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
const CONTRACT: &str = "/body/Sa/Shared/Port/contract";
|
|
|
|
fn add(project: &mut Project, kind: Kind, name: &str) -> Id {
|
|
project.add_element(kind, Layer::Sa, name, None).unwrap()
|
|
}
|
|
|
|
fn port(project: &mut Project, owner: Id, name: &str, contract: PortContract) -> Id {
|
|
let id = Id::new_v4();
|
|
let element = make_port(project, id, owner, name, contract).unwrap();
|
|
project.elements.insert(id, element);
|
|
id
|
|
}
|
|
|
|
fn component(
|
|
project: &mut Project,
|
|
owner: Id,
|
|
kind: ComponentPortKind,
|
|
orientation: ComponentPortOrientation,
|
|
) -> Id {
|
|
port(
|
|
project,
|
|
owner,
|
|
"Boundary port",
|
|
PortContract::Component { kind, orientation },
|
|
)
|
|
}
|
|
|
|
fn function_port(project: &mut Project, owner: Id, output: bool, items: Vec<Id>) -> Id {
|
|
port(
|
|
project,
|
|
owner,
|
|
"Function port",
|
|
if output {
|
|
PortContract::FunctionOutput {
|
|
outgoing_items: items,
|
|
}
|
|
} else {
|
|
PortContract::FunctionInput {
|
|
incoming_items: items,
|
|
}
|
|
},
|
|
)
|
|
}
|
|
|
|
fn legacy(project: &mut Project, owner: Id, interface: Option<Id>) -> Id {
|
|
let id = port(project, owner, "Preserved port", PortContract::Legacy);
|
|
let layer = project.elements[&id].layer();
|
|
project.elements.get_mut(&id).unwrap().body = ElementBody::shared_in(
|
|
layer,
|
|
SharedBody::Port {
|
|
contract: PortContract::Legacy,
|
|
legacy: Some(LegacyPortContract {
|
|
direction: PortDirection::InOut,
|
|
interface,
|
|
}),
|
|
},
|
|
);
|
|
id
|
|
}
|
|
|
|
fn choice_values(draft: &Draft, project: &Project, path: &str) -> Vec<Value> {
|
|
variants(draft, project, path, draft.value.pointer(path).unwrap())
|
|
.unwrap()
|
|
.into_iter()
|
|
.map(|(_, value)| value)
|
|
.collect()
|
|
}
|
|
|
|
#[test]
|
|
fn switching_function_direction_preserves_invalid_items_and_each_authored_payload() {
|
|
let mut project = Project::empty();
|
|
let function = add(&mut project, Kind::Function, "Send");
|
|
let item = add(&mut project, Kind::ExchangeItem, "Payload");
|
|
let output = function_port(&mut project, function, true, vec![item]);
|
|
let mut draft = Draft::from_project(&project, output).unwrap();
|
|
let output_item = format!("{CONTRACT}/FunctionOutput/outgoing_items/0");
|
|
let input_items = format!("{CONTRACT}/FunctionInput/incoming_items");
|
|
draft.input(&output_item, "unfinished reference".into());
|
|
draft
|
|
.shape(CONTRACT, json!({"FunctionInput":{"incoming_items":[]}}))
|
|
.unwrap();
|
|
assert_eq!(
|
|
draft.materialize().unwrap().pointer(&input_items).unwrap(),
|
|
&json!(["unfinished reference"])
|
|
);
|
|
assert!(draft.command().is_err());
|
|
draft.shape(&input_items, json!([item])).unwrap();
|
|
draft
|
|
.shape(CONTRACT, json!({"FunctionOutput":{"outgoing_items":[]}}))
|
|
.unwrap();
|
|
assert_eq!(draft.inputs[&output_item].raw, "unfinished reference");
|
|
draft
|
|
.shape(CONTRACT, json!({"FunctionInput":{"incoming_items":[]}}))
|
|
.unwrap();
|
|
assert_eq!(
|
|
draft.materialize().unwrap().pointer(&input_items).unwrap(),
|
|
&json!([item])
|
|
);
|
|
assert_eq!(
|
|
execute_batch(&project, &[draft.command().unwrap()])
|
|
.unwrap()
|
|
.elements[&output]
|
|
.shared(),
|
|
Some(&SharedBody::Port {
|
|
contract: PortContract::FunctionInput {
|
|
incoming_items: vec![item]
|
|
},
|
|
legacy: None,
|
|
})
|
|
);
|
|
assert!(draft.dirty());
|
|
assert!(
|
|
port_contract(&project.elements[&output])
|
|
.unwrap()
|
|
.is_function()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn failed_shape_keeps_incomplete_text_and_saved_contract_payloads() {
|
|
let mut project = Project::empty();
|
|
let function = add(&mut project, Kind::Function, "Send");
|
|
let output = function_port(&mut project, function, true, vec![]);
|
|
let mut draft = Draft::from_project(&project, output).unwrap();
|
|
draft.set_exact(true).unwrap();
|
|
draft.input("/description", "\"unfinished escape\\".into());
|
|
let value = draft.value.clone();
|
|
assert!(
|
|
draft
|
|
.shape(CONTRACT, json!({"FunctionInput":{"incoming_items":[]}}))
|
|
.is_err()
|
|
);
|
|
assert_eq!(draft.value, value);
|
|
assert_eq!(draft.inputs["/description"].raw, "\"unfinished escape\\");
|
|
assert!(draft.port_contract_drafts.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn contract_choices_follow_owner_and_never_offer_legacy_or_oa_typed_ports() {
|
|
let mut project = Project::empty();
|
|
let function = add(&mut project, Kind::Function, "Send");
|
|
let system = add(&mut project, Kind::System, "System");
|
|
let output = function_port(&mut project, function, true, vec![]);
|
|
let boundary = component(
|
|
&mut project,
|
|
system,
|
|
ComponentPortKind::Standard,
|
|
ComponentPortOrientation::Unset,
|
|
);
|
|
let activity = project
|
|
.add_element(Kind::Activity, Layer::Oa, "Operate", None)
|
|
.unwrap();
|
|
let retained = legacy(&mut project, activity, None);
|
|
let function_draft = Draft::from_project(&project, output).unwrap();
|
|
let component_draft = Draft::from_project(&project, boundary).unwrap();
|
|
let choices = choice_values(&function_draft, &project, CONTRACT);
|
|
assert_eq!(
|
|
choices,
|
|
vec![
|
|
json!({"FunctionInput":{"incoming_items":[]}}),
|
|
json!({"FunctionOutput":{"outgoing_items":[]}})
|
|
]
|
|
);
|
|
assert_eq!(
|
|
choice_values(&component_draft, &project, CONTRACT),
|
|
vec![json!({"Component":{"kind":"Standard","orientation":"Unset"}})]
|
|
);
|
|
assert!(
|
|
choice_values(
|
|
&Draft::from_project(&project, retained).unwrap(),
|
|
&project,
|
|
"/body/Oa/Shared/Port/contract"
|
|
)
|
|
.is_empty()
|
|
);
|
|
assert_eq!(
|
|
reference_ids(&function_draft, &project, "/parent"),
|
|
vec![function]
|
|
);
|
|
assert_eq!(
|
|
reference_ids(&component_draft, &project, "/parent"),
|
|
vec![system]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn standard_switch_retains_incompatible_orientation_until_explicit_repair() {
|
|
let mut project = Project::empty();
|
|
let system = add(&mut project, Kind::System, "System");
|
|
let boundary = component(
|
|
&mut project,
|
|
system,
|
|
ComponentPortKind::Flow,
|
|
ComponentPortOrientation::Out,
|
|
);
|
|
let mut draft = Draft::from_project(&project, boundary).unwrap();
|
|
let kind = format!("{CONTRACT}/Component/kind");
|
|
let orientation = format!("{CONTRACT}/Component/orientation");
|
|
assert!(choice_values(&draft, &project, &orientation).contains(&json!("InOut")));
|
|
draft.shape(&kind, json!("Standard")).unwrap();
|
|
assert_eq!(
|
|
draft.materialize().unwrap().pointer(&orientation),
|
|
Some(&json!("Out"))
|
|
);
|
|
assert_eq!(
|
|
choice_values(&draft, &project, &orientation),
|
|
vec![json!("Unset")]
|
|
);
|
|
let mut session = Session::new(project.clone());
|
|
assert!(session.execute(draft.command().unwrap()).is_err());
|
|
assert_eq!(session.project(), &project);
|
|
assert!(draft.dirty());
|
|
draft.shape(&orientation, json!("Unset")).unwrap();
|
|
session.execute(draft.command().unwrap()).unwrap();
|
|
assert_eq!(
|
|
port_contract(&session.project().elements[&boundary]),
|
|
Some(&PortContract::Component {
|
|
kind: ComponentPortKind::Standard,
|
|
orientation: ComponentPortOrientation::Unset,
|
|
})
|
|
);
|
|
session.undo();
|
|
assert_eq!(session.project(), &project);
|
|
}
|
|
|
|
#[test]
|
|
fn functional_endpoints_require_owner_direction_and_items_but_retain_bad_current_choice() {
|
|
let mut project = Project::empty();
|
|
let source = add(&mut project, Kind::Function, "Send");
|
|
let target = add(&mut project, Kind::Function, "Receive");
|
|
let alternative = add(&mut project, Kind::Function, "Alternative sender");
|
|
let item = add(&mut project, Kind::ExchangeItem, "Message");
|
|
let output = function_port(&mut project, source, true, vec![item]);
|
|
let input = function_port(&mut project, target, false, vec![item]);
|
|
let wrong_direction = function_port(&mut project, source, false, vec![item]);
|
|
function_port(&mut project, source, true, vec![]);
|
|
function_port(&mut project, target, true, vec![item]);
|
|
let replacement = function_port(&mut project, alternative, true, vec![item]);
|
|
let retained = legacy(&mut project, source, None);
|
|
let exchange = project.relate(RelationKind::FunctionalExchange, "Message", source, target);
|
|
project.relations.get_mut(&exchange).unwrap().body = RelationBody::FunctionalExchange {
|
|
source,
|
|
target,
|
|
source_port: output,
|
|
target_port: input,
|
|
items: vec![item],
|
|
};
|
|
let mut draft = Draft::from_project(&project, exchange).unwrap();
|
|
let source_path = "/body/FunctionalExchange/source_port";
|
|
assert_eq!(reference_ids(&draft, &project, source_path), vec![output]);
|
|
assert_eq!(
|
|
reference_ids(&draft, &project, "/body/FunctionalExchange/target_port"),
|
|
vec![input]
|
|
);
|
|
assert!(
|
|
!reference_choices(&draft, &project, source_path, output)
|
|
.iter()
|
|
.any(|choice| choice.id.is_nil())
|
|
);
|
|
for invalid in [wrong_direction, retained] {
|
|
let choices = reference_choices(&draft, &project, source_path, invalid);
|
|
assert!(choices.iter().any(|choice| choice.id == invalid
|
|
&& choice.label.starts_with("Current choice needs review")));
|
|
}
|
|
let owners = reference_ids(&draft, &project, "/body/FunctionalExchange/source");
|
|
assert!(owners.contains(&source) && owners.contains(&alternative));
|
|
assert!(!owners.contains(&target));
|
|
draft
|
|
.shape("/body/FunctionalExchange/source", json!(alternative))
|
|
.unwrap();
|
|
assert_eq!(
|
|
reference_ids(&draft, &project, source_path),
|
|
vec![replacement]
|
|
);
|
|
assert_eq!(
|
|
draft.materialize().unwrap().pointer(source_path),
|
|
Some(&json!(output))
|
|
);
|
|
assert!(execute_batch(&project, &[draft.command().unwrap()]).is_err());
|
|
draft.shape(source_path, json!(replacement)).unwrap();
|
|
assert!(execute_batch(&project, &[draft.command().unwrap()]).is_ok());
|
|
}
|
|
|
|
#[test]
|
|
fn port_allocation_choices_use_actual_owner_direction_and_ignore_edited_relation() {
|
|
let mut project = Project::empty();
|
|
let system = add(&mut project, Kind::System, "System");
|
|
let actor = add(&mut project, Kind::SystemActor, "Partner");
|
|
let function = add(&mut project, Kind::Function, "Send");
|
|
let unallocated = add(&mut project, Kind::Function, "Unallocated");
|
|
project.relate(RelationKind::Allocation, "Performs", system, function);
|
|
assert_eq!(actual_performer(&project, unallocated), None);
|
|
let output = function_port(&mut project, function, true, vec![]);
|
|
function_port(&mut project, function, false, vec![]);
|
|
function_port(&mut project, unallocated, true, vec![]);
|
|
let first = component(
|
|
&mut project,
|
|
system,
|
|
ComponentPortKind::Flow,
|
|
ComponentPortOrientation::Out,
|
|
);
|
|
let second = component(
|
|
&mut project,
|
|
system,
|
|
ComponentPortKind::Standard,
|
|
ComponentPortOrientation::Unset,
|
|
);
|
|
component(
|
|
&mut project,
|
|
system,
|
|
ComponentPortKind::Flow,
|
|
ComponentPortOrientation::In,
|
|
);
|
|
component(
|
|
&mut project,
|
|
actor,
|
|
ComponentPortKind::Flow,
|
|
ComponentPortOrientation::Out,
|
|
);
|
|
legacy(&mut project, system, None);
|
|
let allocation = project.relate(
|
|
RelationKind::PortAllocation,
|
|
"Allocated port",
|
|
first,
|
|
output,
|
|
);
|
|
let draft = Draft::from_project(&project, allocation).unwrap();
|
|
let sources = reference_ids(&draft, &project, "/body/PortAllocation/source");
|
|
assert_eq!(
|
|
sources.into_iter().collect::<BTreeSet<_>>(),
|
|
BTreeSet::from([first, second])
|
|
);
|
|
assert_eq!(
|
|
reference_ids(&draft, &project, "/body/PortAllocation/target"),
|
|
vec![output]
|
|
);
|
|
project.relate(
|
|
RelationKind::PortAllocation,
|
|
"Other allocated port",
|
|
second,
|
|
output,
|
|
);
|
|
assert_eq!(
|
|
reference_ids(&draft, &project, "/body/PortAllocation/source"),
|
|
vec![first]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn boundary_choices_honor_draft_direction_same_kind_and_direct_interface_items() {
|
|
let mut project = Project::empty();
|
|
let system = add(&mut project, Kind::System, "System");
|
|
let actor = add(&mut project, Kind::SystemActor, "Partner");
|
|
let input = component(
|
|
&mut project,
|
|
system,
|
|
ComponentPortKind::Flow,
|
|
ComponentPortOrientation::In,
|
|
);
|
|
let output = component(
|
|
&mut project,
|
|
actor,
|
|
ComponentPortKind::Flow,
|
|
ComponentPortOrientation::Out,
|
|
);
|
|
component(
|
|
&mut project,
|
|
actor,
|
|
ComponentPortKind::Flow,
|
|
ComponentPortOrientation::In,
|
|
);
|
|
component(
|
|
&mut project,
|
|
actor,
|
|
ComponentPortKind::Standard,
|
|
ComponentPortOrientation::Unset,
|
|
);
|
|
legacy(&mut project, actor, None);
|
|
let item = add(&mut project, Kind::ExchangeItem, "Message");
|
|
let interface = add(&mut project, Kind::Interface, "Empty contract");
|
|
// A participant declaration must not become the port's declaration.
|
|
project.relate(
|
|
RelationKind::InterfaceRequirement,
|
|
"Participant requires",
|
|
actor,
|
|
interface,
|
|
);
|
|
let exchange = project.relate(RelationKind::BoundaryExchange, "Boundary", input, output);
|
|
project.relations.get_mut(&exchange).unwrap().body = RelationBody::BoundaryExchange {
|
|
source: input,
|
|
target: output,
|
|
oriented: false,
|
|
items: vec![item],
|
|
};
|
|
let mut draft = Draft::from_project(&project, exchange).unwrap();
|
|
let target = "/body/BoundaryExchange/target";
|
|
assert_eq!(reference_ids(&draft, &project, target), vec![output]);
|
|
draft
|
|
.shape("/body/BoundaryExchange/oriented", json!(true))
|
|
.unwrap();
|
|
assert!(reference_ids(&draft, &project, target).is_empty());
|
|
assert_eq!(
|
|
draft.materialize().unwrap().pointer(target),
|
|
Some(&json!(output))
|
|
);
|
|
draft
|
|
.shape("/body/BoundaryExchange/oriented", json!(false))
|
|
.unwrap();
|
|
project.relate(
|
|
RelationKind::InterfaceProvision,
|
|
"Port provides",
|
|
output,
|
|
interface,
|
|
);
|
|
assert!(reference_ids(&draft, &project, target).is_empty());
|
|
project.relate(
|
|
RelationKind::InterfaceItemAllocation,
|
|
"Allocated item",
|
|
interface,
|
|
item,
|
|
);
|
|
assert_eq!(reference_ids(&draft, &project, target), vec![output]);
|
|
}
|
|
|
|
#[test]
|
|
fn interface_roles_offer_participants_and_component_ports_independent_of_orientation() {
|
|
let mut project = Project::empty();
|
|
let system = add(&mut project, Kind::System, "System");
|
|
let function = add(&mut project, Kind::Function, "Send");
|
|
let function_port = function_port(&mut project, function, true, vec![]);
|
|
let interface = add(&mut project, Kind::Interface, "Service");
|
|
let input = component(
|
|
&mut project,
|
|
system,
|
|
ComponentPortKind::Flow,
|
|
ComponentPortOrientation::In,
|
|
);
|
|
let output = component(
|
|
&mut project,
|
|
system,
|
|
ComponentPortKind::Flow,
|
|
ComponentPortOrientation::Out,
|
|
);
|
|
let preserved = legacy(&mut project, system, Some(interface));
|
|
for kind in [
|
|
RelationKind::InterfaceProvision,
|
|
RelationKind::InterfaceRequirement,
|
|
] {
|
|
let role = project.relate(kind, "Role", input, interface);
|
|
let draft = Draft::from_project(&project, role).unwrap();
|
|
let path = format!("/body/{kind:?}/source");
|
|
let choices = reference_ids(&draft, &project, &path);
|
|
assert_eq!(
|
|
choices.into_iter().collect::<BTreeSet<_>>(),
|
|
BTreeSet::from([system, input, output])
|
|
);
|
|
for invalid in [function_port, preserved] {
|
|
assert!(
|
|
reference_choices(&draft, &project, &path, invalid)
|
|
.iter()
|
|
.any(|choice| choice.id == invalid
|
|
&& choice.label.starts_with("Current choice needs review"))
|
|
);
|
|
}
|
|
assert_eq!(
|
|
reference_ids(&draft, &project, &format!("/body/{kind:?}/target")),
|
|
vec![interface]
|
|
);
|
|
}
|
|
let draft = Draft::from_project(&project, preserved).unwrap();
|
|
let lines = legacy_lines(
|
|
draft.value.pointer("/body/Sa/Shared/Port/legacy").unwrap(),
|
|
&project,
|
|
);
|
|
assert!(lines[0].contains("Input / output"));
|
|
assert!(lines[1].contains("Service"));
|
|
assert!(lines[2].contains("does not declare a provided or required interface"));
|
|
assert_eq!(
|
|
execute_batch(&project, &[draft.command().unwrap()]).unwrap(),
|
|
project
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn all_demo_records_roundtrip_through_graphical_and_escaped_string_drafts() {
|
|
let project = Project::demo();
|
|
let mut kinds = BTreeSet::new();
|
|
for (id, _) in project.identities() {
|
|
let mut draft = Draft::from_project(&project, id).unwrap();
|
|
kinds.insert(
|
|
format!("{:?}", draft.kind)
|
|
.split('(')
|
|
.next()
|
|
.unwrap()
|
|
.to_owned(),
|
|
);
|
|
assert!(!draft.dirty());
|
|
assert_eq!(
|
|
execute_batch(&project, &[draft.command().unwrap()]).unwrap(),
|
|
project,
|
|
"Normal draft {id}"
|
|
);
|
|
draft.set_exact(true).unwrap();
|
|
assert!(!draft.dirty());
|
|
assert_eq!(
|
|
execute_batch(&project, &[draft.command().unwrap()]).unwrap(),
|
|
project,
|
|
"Escaped draft {id}"
|
|
);
|
|
draft.set_exact(false).unwrap();
|
|
assert!(!draft.dirty());
|
|
}
|
|
assert_eq!(
|
|
kinds,
|
|
BTreeSet::from(
|
|
[
|
|
"Project", "Element", "Relation", "Diagram", "Node", "Artifact", "Trace"
|
|
]
|
|
.map(str::to_owned)
|
|
)
|
|
);
|
|
}
|
|
}
|