238 lines
8.8 KiB
Rust
238 lines
8.8 KiB
Rust
//! Duplicate presentation identities while retaining the same engineering model.
|
|
use oa_core::{Command, Id, Project};
|
|
use std::collections::BTreeSet;
|
|
|
|
fn fresh_id(used: &mut BTreeSet<Id>) -> Id {
|
|
loop {
|
|
let id = Id::new_v4();
|
|
if used.insert(id) {
|
|
return id;
|
|
}
|
|
}
|
|
}
|
|
|
|
fn fresh_alias(used: &mut BTreeSet<String>, original: &str) -> String {
|
|
let stem = format!("{original}_copy");
|
|
let mut candidate = stem.clone();
|
|
let mut suffix = 2usize;
|
|
while !used.insert(candidate.clone()) {
|
|
candidate = format!("{stem}_{suffix}");
|
|
suffix += 1;
|
|
}
|
|
candidate
|
|
}
|
|
|
|
/// The returned existing core command creates the whole presentation atomically.
|
|
/// It does not copy artifact claims or reidentify any existing model record.
|
|
pub fn command(project: &Project, source: Id) -> Result<Command, String> {
|
|
let mut diagram = project
|
|
.diagrams
|
|
.get(&source)
|
|
.cloned()
|
|
.ok_or("The saved view to duplicate no longer exists.")?;
|
|
let identities = project.identities();
|
|
let mut used_ids: BTreeSet<_> = identities.iter().map(|(id, _)| *id).collect();
|
|
let mut used_aliases: BTreeSet<_> = identities
|
|
.iter()
|
|
.map(|(_, alias)| (*alias).to_owned())
|
|
.collect();
|
|
diagram.id = fresh_id(&mut used_ids);
|
|
diagram.alias = fresh_alias(&mut used_aliases, &diagram.alias);
|
|
diagram.name = format!("{} copy", diagram.name);
|
|
diagram.nodes = diagram
|
|
.nodes
|
|
.into_values()
|
|
.map(|mut node| {
|
|
node.id = fresh_id(&mut used_ids);
|
|
node.alias = fresh_alias(&mut used_aliases, &node.alias);
|
|
(node.id, node)
|
|
})
|
|
.collect();
|
|
Ok(Command::CreateDiagram(diagram))
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use oa_core::{view_options::*, *};
|
|
|
|
fn assert_only_presentation_ids_changed(source: &Diagram, copy: &Diagram) {
|
|
assert_ne!(source.id, copy.id);
|
|
assert_ne!(source.alias, copy.alias);
|
|
assert_eq!(copy.name, format!("{} copy", source.name));
|
|
assert_eq!(copy.description, source.description);
|
|
assert_eq!(copy.layer, source.layer);
|
|
assert_eq!(copy.projection, source.projection);
|
|
assert_eq!(copy.view_options, source.view_options);
|
|
assert_eq!(copy.routes, source.routes);
|
|
assert_eq!(copy.nodes.len(), source.nodes.len());
|
|
for node in source.nodes.values() {
|
|
let copied = copy
|
|
.nodes
|
|
.values()
|
|
.find(|copy| copy.element == node.element)
|
|
.unwrap();
|
|
assert_ne!(node.id, copied.id);
|
|
assert_ne!(node.alias, copied.alias);
|
|
let mut normalized = copied.clone();
|
|
normalized.id = node.id;
|
|
normalized.alias.clone_from(&node.alias);
|
|
assert_eq!(&normalized, node);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn duplicate_retains_ports_routes_options_and_artifact_claims_with_exact_undo_redo() {
|
|
let mut project = Project::empty();
|
|
let a = project
|
|
.add_element(Kind::Function, Layer::Sa, "Request", None)
|
|
.unwrap();
|
|
let b = project
|
|
.add_element(Kind::Function, Layer::Sa, "Provide", None)
|
|
.unwrap();
|
|
let exchange = project.relate(RelationKind::FunctionalExchange, "Data", a, b);
|
|
let port = match project.relations[&exchange].body {
|
|
RelationBody::FunctionalExchange { source_port, .. } => source_port,
|
|
_ => unreachable!(),
|
|
};
|
|
let diagram = project.ensure_diagram(Layer::Sa);
|
|
let first = project.place(diagram, a, Position { x: 80., y: 120. });
|
|
project.place(diagram, b, Position { x: 700., y: 240. });
|
|
let view = project.diagrams.get_mut(&diagram).unwrap();
|
|
view.description = "Engineering concern with manual routing".into();
|
|
view.projection = ProjectionKind::FunctionalDataflow;
|
|
view.nodes
|
|
.get_mut(&first)
|
|
.unwrap()
|
|
.port_anchors
|
|
.push(PortAnchor {
|
|
port,
|
|
side: PortAnchorSide::Bottom,
|
|
offset: 0.35,
|
|
});
|
|
view.routes.push(EdgeRoute {
|
|
edge: exchange,
|
|
waypoints: vec![Position { x: 420., y: 440. }],
|
|
label: Some(EdgeLabel {
|
|
fraction: 0.4,
|
|
offset: Position { x: 8., y: -15. },
|
|
}),
|
|
});
|
|
view.view_options = ViewOptions {
|
|
hidden_edges: vec![exchange],
|
|
work_categories: CategorySelection::Any(vec![]),
|
|
work_contexts: ContextSelection::Any(vec![]),
|
|
exchange_labels: ExchangeLabels::NameAndItems,
|
|
..ViewOptions::default()
|
|
};
|
|
let artifact = Id::new_v4();
|
|
project.artifact_links.insert(
|
|
artifact,
|
|
ArtifactLink {
|
|
id: artifact,
|
|
alias: "view_review".into(),
|
|
name: "View review".into(),
|
|
description: String::new(),
|
|
owner: first,
|
|
relation: ArtifactRelation::References,
|
|
uri: "./artifacts/review.md".into(),
|
|
revision: None,
|
|
fragment: None,
|
|
},
|
|
);
|
|
let original_source = language::format(&project);
|
|
let mut session = Session::new(project.clone());
|
|
let copy_id = session
|
|
.execute(command(&project, diagram).unwrap())
|
|
.unwrap()
|
|
.unwrap();
|
|
let copied = session.project().clone();
|
|
assert_only_presentation_ids_changed(
|
|
&project.diagrams[&diagram],
|
|
&copied.diagrams[©_id],
|
|
);
|
|
let mut normalized = copied.clone();
|
|
normalized.diagrams.remove(©_id);
|
|
assert_eq!(normalized, project);
|
|
assert_eq!(language::format(&normalized), original_source);
|
|
assert_eq!(language::parse(&language::format(&copied)).unwrap(), copied);
|
|
session.undo();
|
|
assert_eq!(session.project(), &project);
|
|
session.redo();
|
|
assert_eq!(session.project(), &copied);
|
|
assert_eq!(copied.artifact_links, project.artifact_links);
|
|
}
|
|
|
|
#[test]
|
|
fn duplicate_state_view_preserves_machine_nesting_and_collapse_with_unique_aliases() {
|
|
let mut project = language::parse(
|
|
r#"arcadia 3; project operating_modes "Operating modes" {
|
|
oa_entity crew "Crew" {}
|
|
oa_state_machine modes "Modes" { parent @crew; }
|
|
oa_region main "Main" { parent @modes; }
|
|
oa_state ready "Ready" { parent @main; }
|
|
oa_region nested "Nested" { parent @ready; }
|
|
oa_state active "Active" { parent @nested; }
|
|
diagram states "Operating modes" { projection { StateMachine { machine @modes; }; }; }
|
|
node ready_view { diagram @states; element @ready; }
|
|
node main_view { diagram @states; element @main; }
|
|
node nested_view { diagram @states; element @nested; }
|
|
node active_view { diagram @states; element @active; }
|
|
}"#,
|
|
)
|
|
.unwrap();
|
|
let diagram = project.diagrams.values().next().unwrap().id;
|
|
let ready = project
|
|
.elements
|
|
.values()
|
|
.find(|element| element.alias == "ready")
|
|
.unwrap()
|
|
.id;
|
|
project
|
|
.diagrams
|
|
.get_mut(&diagram)
|
|
.unwrap()
|
|
.nodes
|
|
.values_mut()
|
|
.find(|node| node.element == ready)
|
|
.unwrap()
|
|
.collapsed = true;
|
|
let source = project.diagrams[&diagram].clone();
|
|
let mut session = Session::new(project);
|
|
for _ in 0..3 {
|
|
let id = session
|
|
.execute(command(session.project(), diagram).unwrap())
|
|
.unwrap()
|
|
.unwrap();
|
|
assert_only_presentation_ids_changed(&source, &session.project().diagrams[&id]);
|
|
let projected = projection::project_view(session.project(), id);
|
|
let expected = projection::project_view(session.project(), diagram);
|
|
assert_eq!(
|
|
projected
|
|
.nodes
|
|
.iter()
|
|
.map(|node| node.element)
|
|
.collect::<BTreeSet<_>>(),
|
|
expected.nodes.iter().map(|node| node.element).collect()
|
|
);
|
|
}
|
|
let aliases: Vec<_> = session
|
|
.project()
|
|
.identities()
|
|
.into_iter()
|
|
.map(|(_, alias)| alias)
|
|
.collect();
|
|
assert_eq!(aliases.len(), aliases.iter().collect::<BTreeSet<_>>().len());
|
|
assert_eq!(
|
|
language::parse(&language::format(session.project())).unwrap(),
|
|
*session.project()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn missing_view_does_not_propose_an_empty_replacement() {
|
|
let project = Project::empty();
|
|
assert!(command(&project, Id::new_v4()).is_err());
|
|
}
|
|
}
|