use std::collections::{HashMap, HashSet};
use crate::ir::*;
pub fn generate_topology(project: &Project) -> String {
let mut nodes: Vec<String> = Vec::new();
let mut edges: Vec<String> = Vec::new();
let mut nid: usize = 0;
let mut eid: usize = 0;
let mut type_node: HashMap<String, String> = HashMap::new();
let mut mod_node: HashMap<String, String> = HashMap::new();
let cols_wrap = 4i32;
let col_w = 520;
let row_h = 80;
let mod_w = 340;
let mod_h = 50;
let type_w = 280;
let type_h = 44;
let fn_w = 280;
let fn_h = 44;
let pad_x = 20; let group_pad = 20;
let grid_rows: Vec<Vec<usize>> = project
.modules
.iter()
.enumerate()
.fold(Vec::new(), |mut acc, (i, _)| {
let r = i / cols_wrap as usize;
if r >= acc.len() {
acc.push(Vec::new());
}
acc[r].push(i);
acc
});
let mut grid_row_y: Vec<i32> = Vec::new();
let mut cum_y = 0i32;
for row_indices in &grid_rows {
grid_row_y.push(cum_y);
let tallest = row_indices
.iter()
.map(|&i| {
let m = &project.modules[i];
let items = m.types.len() + if m.functions.is_empty() { 0 } else { 1 };
items.max(1)
})
.max()
.unwrap_or(1);
cum_y += (tallest as i32 + 2) * row_h + group_pad * 2;
}
for (i, module) in project.modules.iter().enumerate() {
let gc = (i % cols_wrap as usize) as i32;
let gr = i / cols_wrap as usize;
let base_x = gc * col_w;
let base_y = grid_row_y[gr];
let mid = format!("m{}", nid);
nid += 1;
mod_node.insert(module.path.clone(), mid.clone());
nodes.push(format!(
concat!(
"{{",
r#""id":"{}","type":"text","#,
r#""text":"{}","#,
r#""x":{},"y":{},"width":{},"height":{},"color":"5""#,
"}}"
),
mid,
json_escape(&format!("**{}**\\n_{}_", module.path, module.language.as_str())),
base_x + pad_x,
base_y + group_pad,
mod_w - pad_x * 2,
mod_h,
));
let mut slot = 1i32;
for td in &module.types {
let tid = format!("t{}", nid);
nid += 1;
let color = type_color(&td.kind);
let label = json_escape(&format!("**{}** {}", td.kind.as_str(), td.name));
let tx = base_x + pad_x;
let ty = base_y + group_pad + slot * row_h;
slot += 1;
nodes.push(format!(
concat!(
"{{",
r#""id":"{}","type":"text","#,
r#""text":"{}","#,
r#""x":{},"y":{},"width":{},"height":{},"color":"{}""#,
"}}"
),
tid, label, tx, ty, type_w, type_h, color,
));
type_node.insert(td.name.clone(), tid.clone());
edges.push(make_edge(&mut eid, &mid, &tid, "bottom", "top", "defines", "4"));
}
if !module.functions.is_empty() {
let fid = format!("f{}", nid);
nid += 1;
let fn_names: Vec<&str> = module.functions.iter().map(|f| f.name.as_str()).collect();
let summary = if fn_names.len() <= 6 {
fn_names.join("\\n")
} else {
let mut s = fn_names[..5].join("\\n");
s.push_str(&format!("\\n_+{} more_", fn_names.len() - 5));
s
};
let label = json_escape(&format!("**functions**\\n{}", summary));
let tx = base_x + pad_x;
let ty = base_y + group_pad + slot * row_h;
slot += 1;
nodes.push(format!(
concat!(
"{{",
r#""id":"{}","type":"text","#,
r#""text":"{}","#,
r#""x":{},"y":{},"width":{},"height":{},"color":"3""#,
"}}"
),
fid, label, tx, ty, fn_w, fn_h,
));
edges.push(make_edge(&mut eid, &mid, &fid, "bottom", "top", "contains", "3"));
let mut fn_call_targets: HashSet<String> = HashSet::new();
for func in &module.functions {
for call in &func.calls {
if let Some(ref tt) = call.target_type {
if let Some(target_id) = type_node.get(tt.as_str()) {
fn_call_targets.insert(target_id.clone());
}
}
}
}
for target_id in fn_call_targets {
edges.push(make_edge(&mut eid, &fid, &target_id, "right", "left", "calls", "2"));
}
}
let group_h = (slot + 1) * row_h;
let gid = format!("g{}", nid);
nid += 1;
nodes.insert(
nodes.len() - (module.types.len() + if module.functions.is_empty() { 1 } else { 2 }),
format!(
concat!(
"{{",
r#""id":"{}","type":"group","#,
r#""label":"{}","#,
r#""x":{},"y":{},"width":{},"height":{},"color":"5""#,
"}}"
),
gid,
json_escape(&module.path),
base_x,
base_y,
mod_w,
group_h,
),
);
}
for module in &project.modules {
for td in &module.types {
let src = match type_node.get(&td.name) {
Some(id) => id,
None => continue,
};
for rel in &td.relations {
if let Some(tgt) = type_node.get(&rel.target) {
let (label, color) = match rel.kind {
RelationKind::Extends => ("extends", "1"),
RelationKind::Implements => ("implements", "6"),
RelationKind::ImplTrait => ("impl", "6"),
};
edges.push(make_edge(&mut eid, src, tgt, "right", "left", label, color));
}
}
let mut call_targets: HashSet<String> = HashSet::new();
for method in &td.methods {
for call in &method.calls {
if let Some(ref tt) = call.target_type {
if let Some(tgt) = type_node.get(tt.as_str()) {
if tgt != src {
call_targets.insert(tgt.clone());
}
}
}
}
}
for tgt in call_targets {
edges.push(make_edge(&mut eid, src, &tgt, "right", "left", "calls", "2"));
}
}
}
let mut out = String::from("{\n \"nodes\":[\n");
for (i, n) in nodes.iter().enumerate() {
out.push_str(" ");
out.push_str(n);
if i + 1 < nodes.len() {
out.push(',');
}
out.push('\n');
}
out.push_str(" ],\n \"edges\":[\n");
for (i, e) in edges.iter().enumerate() {
out.push_str(" ");
out.push_str(e);
if i + 1 < edges.len() {
out.push(',');
}
out.push('\n');
}
out.push_str(" ]\n}");
out
}
fn make_edge(
eid: &mut usize,
from: &str,
to: &str,
from_side: &str,
to_side: &str,
label: &str,
color: &str,
) -> String {
let id = format!("e{}", eid);
*eid += 1;
format!(
concat!(
"{{",
r#""id":"{}","fromNode":"{}","toNode":"{}","#,
r#""fromSide":"{}","toSide":"{}","#,
r#""label":"{}","color":"{}""#,
"}}"
),
id, from, to, from_side, to_side, label, color,
)
}
fn type_color(kind: &TypeKind) -> &'static str {
match kind {
TypeKind::Struct | TypeKind::Record | TypeKind::DataClass => "4", TypeKind::Enum | TypeKind::SealedClass => "2", TypeKind::Trait | TypeKind::Interface => "6", TypeKind::Class | TypeKind::Object => "1", }
}
fn json_escape(s: &str) -> String {
s.replace('\\', "\\\\")
.replace('"', "\\\"")
.replace('\n', "\\n")
.replace('\r', "")
.replace('\t', "\\t")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_canvas_topology_on_self() {
let manifest_dir = env!("CARGO_MANIFEST_DIR");
let path = std::path::PathBuf::from(manifest_dir);
let project = crate::scan_project(&path, &[Language::Rust], &[]);
let canvas = generate_topology(&project);
assert!(canvas.starts_with('{'), "Canvas should be JSON object");
assert!(canvas.ends_with('}'), "Canvas should end with }}");
assert!(canvas.contains("\"nodes\""), "Must have nodes array");
assert!(canvas.contains("\"edges\""), "Must have edges array");
assert!(canvas.contains("Project"), "Should have Project type");
assert!(canvas.contains("Module"), "Should have Module type");
assert!(canvas.contains("\"label\":\"defines\""), "Should have 'defines' edges");
}
}