Files
ARCADIA-Studio/crates/oa-desktop/src/view_copy.rs
T

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[&copy_id],
);
let mut normalized = copied.clone();
normalized.diagrams.remove(&copy_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());
}
}