//! 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 { loop { let id = Id::new_v4(); if used.insert(id) { return id; } } } fn fresh_alias(used: &mut BTreeSet, 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 { 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::>(), 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::>().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()); } }