use std::collections::HashMap;
use super::db::{ClassDb, LOLLIPOP, RelationEnd};
#[derive(Clone, Debug, PartialEq)]
pub(crate) struct GraphNode {
pub(crate) id: String,
pub(crate) label: String,
pub(crate) is_group: bool,
pub(crate) shape: &'static str,
pub(crate) parent_id: Option<String>,
pub(crate) parent: i32,
pub(crate) class: Option<usize>,
pub(crate) color_index: Option<usize>,
}
#[derive(Clone, Debug, PartialEq)]
pub(crate) struct GraphEdge {
pub(crate) id: String,
pub(crate) start: String,
pub(crate) end: String,
pub(crate) source: i32,
pub(crate) target: i32,
pub(crate) label: Option<String>,
pub(crate) arrow_type_start: &'static str,
pub(crate) arrow_type_end: &'static str,
pub(crate) start_label_right: String,
pub(crate) end_label_left: String,
pub(crate) pattern: &'static str,
pub(crate) classes: &'static str,
}
#[derive(Clone, Debug, Default, PartialEq)]
pub(crate) struct ClassGraph {
pub(crate) nodes: Vec<GraphNode>,
pub(crate) edges: Vec<GraphEdge>,
}
const MARKERS: [&str; 5] = [
"aggregation",
"extension",
"composition",
"dependency",
"lollipop",
];
fn marker(end: RelationEnd) -> &'static str {
end.and_then(|t| MARKERS.get(usize::from(t)).copied())
.unwrap_or("none")
}
pub(crate) fn build_class_graph(model: &ClassDb, hierarchical: bool) -> ClassGraph {
let mut nodes: Vec<GraphNode> = Vec::new();
let mut edges: Vec<GraphEdge> = Vec::new();
let mut namespace_at: HashMap<&str, usize> = HashMap::new();
let mut class_at: HashMap<&str, usize> = HashMap::new();
let mut interface_at: HashMap<&str, usize> = HashMap::new();
let mut declared: HashMap<String, Option<String>> = HashMap::new();
let mut declared_ancestor = |id: Option<&str>| -> Option<String> {
let id = id?;
if hierarchical {
return Some(id.to_string());
}
let mut chain: Vec<String> = Vec::new();
let mut current = Some(id.to_string());
let mut found = None;
while let Some(c) = current.filter(|c| !c.is_empty()) {
if let Some(f) = declared.get(&c) {
found = f.clone();
break;
}
let Some(ns) = model.namespaces.get(&c) else {
break;
};
if ns.explicit {
found = Some(c);
break;
}
current = ns.parent.clone();
chain.push(c);
}
for visited in chain {
declared.insert(visited, found.clone());
}
found
};
let group = |namespace_at: &HashMap<&str, usize>, id: Option<&str>| -> i32 {
id.and_then(|id| namespace_at.get(id))
.map_or(-1, |&i| i as i32)
};
for ns in model.namespaces.values() {
if !hierarchical && !ns.explicit {
continue;
}
namespace_at.insert(&ns.id, nodes.len());
nodes.push(GraphNode {
id: ns.id.clone(),
label: if hierarchical {
ns.label.clone()
} else {
ns.id.clone()
},
is_group: true,
shape: "rect",
parent_id: if hierarchical {
ns.parent.clone()
} else {
None
},
parent: -1,
class: None,
color_index: None,
});
}
for node in &mut nodes {
node.parent = group(&namespace_at, node.parent_id.as_deref());
}
for (color_index, class) in model.classes.values().enumerate() {
let parent_id = declared_ancestor(class.parent.as_deref());
class_at.insert(&class.id, nodes.len());
nodes.push(GraphNode {
id: class.id.clone(),
label: class.label.clone(),
is_group: false,
shape: "classBox",
parent: group(&namespace_at, parent_id.as_deref()),
parent_id,
class: Some(color_index),
color_index: Some(color_index),
});
}
for note in model.notes.values() {
let parent_id = declared_ancestor(note.parent.as_deref());
let target = note.class.as_deref().and_then(|c| class_at.get(c).copied());
if let Some(target) = target {
edges.push(GraphEdge {
id: format!("edgeNote{}", note.index),
start: note.id.clone(),
end: nodes[target].id.clone(),
source: nodes.len() as i32,
target: target as i32,
label: None,
arrow_type_start: "none",
arrow_type_end: "none",
start_label_right: String::new(),
end_label_left: String::new(),
pattern: "dotted",
classes: "relation",
});
}
nodes.push(GraphNode {
id: note.id.clone(),
label: note.text.clone(),
is_group: false,
shape: "note",
parent: group(&namespace_at, parent_id.as_deref()),
parent_id,
class: None,
color_index: None,
});
}
for item in &model.interfaces {
let owner = class_at.get(item.class_id.as_str()).copied();
interface_at.insert(&item.id, nodes.len());
nodes.push(GraphNode {
id: item.id.clone(),
label: item.label.clone(),
is_group: false,
shape: "interface",
parent_id: None,
parent: owner.map_or(-1, |o| nodes[o].parent),
class: None,
color_index: None,
});
}
for (count, relation) in model.relations.iter().enumerate() {
let (type1, type2) = (relation.type1, relation.type2);
let start_interface = type1 == Some(LOLLIPOP) && type2.is_none();
let end_interface = !start_interface && type2 == Some(LOLLIPOP) && type1.is_none();
let at = |interface: bool, id: &str| {
let map = if interface { &interface_at } else { &class_at };
map.get(id).map_or(-1, |&i| i as i32)
};
let title = |t: &str| {
if t == "none" {
String::new()
} else {
t.to_string()
}
};
edges.push(GraphEdge {
id: format!("id_{}_{}_{}", relation.id1, relation.id2, count + 1),
start: relation.id1.clone(),
end: relation.id2.clone(),
source: at(start_interface, &relation.id1),
target: at(end_interface, &relation.id2),
label: relation.title.clone(),
arrow_type_start: marker(type1),
arrow_type_end: marker(type2),
start_label_right: title(&relation.relation_title1),
end_label_left: title(&relation.relation_title2),
pattern: if relation.line_type == 1 {
"dashed"
} else {
"solid"
},
classes: "relation",
});
}
ClassGraph { nodes, edges }
}