feat: add rendering-primitives crate, share non_import_rels across renderers
This commit is contained in:
@@ -15,6 +15,7 @@ members = [
|
||||
"crates/adapters/python-project",
|
||||
"crates/adapters/d2",
|
||||
"crates/adapters/html-viewer",
|
||||
"crates/adapters/rendering-primitives",
|
||||
]
|
||||
|
||||
[workspace.dependencies]
|
||||
@@ -32,6 +33,7 @@ archlens-cargo-workspace = { path = "crates/adapters/cargo-workspace" }
|
||||
archlens-python-project = { path = "crates/adapters/python-project" }
|
||||
archlens-d2 = { path = "crates/adapters/d2" }
|
||||
archlens-html = { path = "crates/adapters/html-viewer" }
|
||||
archlens-rendering-primitives = { path = "crates/adapters/rendering-primitives" }
|
||||
serde_json = "1"
|
||||
|
||||
# Error handling
|
||||
|
||||
@@ -6,5 +6,6 @@ publish = false
|
||||
|
||||
[dependencies]
|
||||
archlens-domain.workspace = true
|
||||
archlens-rendering-primitives.workspace = true
|
||||
thiserror.workspace = true
|
||||
tracing.workspace = true
|
||||
|
||||
@@ -2,6 +2,7 @@ use archlens_domain::{
|
||||
CodeElement, CodeGraph, DomainError, RelationshipKind, RenderOutput, RenderedFile,
|
||||
ports::DiagramRenderer,
|
||||
};
|
||||
use archlens_rendering_primitives::non_import_rels;
|
||||
|
||||
pub struct AsciiRenderer;
|
||||
|
||||
@@ -94,16 +95,12 @@ impl DiagramRenderer for AsciiRenderer {
|
||||
lines.push("└───".to_string());
|
||||
}
|
||||
|
||||
let non_import_rels: Vec<_> = graph
|
||||
.relationships()
|
||||
.iter()
|
||||
.filter(|r| r.kind() != RelationshipKind::Import)
|
||||
.collect();
|
||||
let filtered_rels: Vec<_> = non_import_rels(graph.relationships()).collect();
|
||||
|
||||
if !non_import_rels.is_empty() {
|
||||
if !filtered_rels.is_empty() {
|
||||
lines.push(String::new());
|
||||
lines.push("── Relationships ──".to_string());
|
||||
for rel in &non_import_rels {
|
||||
for rel in &filtered_rels {
|
||||
let arrow = match rel.kind() {
|
||||
RelationshipKind::Inheritance => "extends",
|
||||
RelationshipKind::Composition => "has",
|
||||
|
||||
@@ -6,6 +6,7 @@ publish = false
|
||||
|
||||
[dependencies]
|
||||
archlens-domain.workspace = true
|
||||
archlens-rendering-primitives.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile.workspace = true
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use archlens_domain::{
|
||||
CodeGraph, DiagramLevel, DomainError, RenderOutput, RenderedFile, ports::DiagramRenderer,
|
||||
};
|
||||
use archlens_rendering_primitives::{non_import_rels, sanitize_identifier};
|
||||
|
||||
pub struct D2Renderer {
|
||||
level: DiagramLevel,
|
||||
@@ -36,20 +37,16 @@ impl DiagramRenderer for D2Renderer {
|
||||
}
|
||||
}
|
||||
|
||||
fn sanitize(name: &str) -> String {
|
||||
name.replace("::", "_").replace(['-', ' '], "_")
|
||||
}
|
||||
|
||||
fn render_type(graph: &CodeGraph) -> String {
|
||||
let mut lines = Vec::new();
|
||||
let (by_module, ungrouped) = graph.elements_by_module();
|
||||
|
||||
// Grouped by module
|
||||
for (module, elements) in &by_module {
|
||||
let mod_id = sanitize(module);
|
||||
let mod_id = sanitize_identifier(module);
|
||||
lines.push(format!("{mod_id}: {{"));
|
||||
for el in elements {
|
||||
let el_id = sanitize(el.name());
|
||||
let el_id = sanitize_identifier(el.name());
|
||||
lines.push(format!(" {el_id}: {{"));
|
||||
lines.push(" shape: class".to_string());
|
||||
for field in el.fields() {
|
||||
@@ -69,21 +66,21 @@ fn render_type(graph: &CodeGraph) -> String {
|
||||
|
||||
// Ungrouped elements
|
||||
for el in &ungrouped {
|
||||
let el_id = sanitize(el.name());
|
||||
let el_id = sanitize_identifier(el.name());
|
||||
lines.push(format!("{el_id}: {{"));
|
||||
lines.push(" shape: class".to_string());
|
||||
lines.push("}".to_string());
|
||||
}
|
||||
|
||||
// Relationships
|
||||
for rel in graph.relationships() {
|
||||
for rel in non_import_rels(graph.relationships()) {
|
||||
use archlens_domain::RelationshipKind;
|
||||
let src = sanitize(rel.source());
|
||||
let tgt = sanitize(rel.target());
|
||||
let src = sanitize_identifier(rel.source());
|
||||
let tgt = sanitize_identifier(rel.target());
|
||||
let arrow = match rel.kind() {
|
||||
RelationshipKind::Inheritance => format!("{src} -> {tgt}: {{style.stroke-dash: 0}}"),
|
||||
RelationshipKind::Composition => format!("{src} -> {tgt}"),
|
||||
RelationshipKind::Import => continue,
|
||||
RelationshipKind::Import => unreachable!("imports filtered by non_import_rels"),
|
||||
};
|
||||
lines.push(arrow);
|
||||
}
|
||||
@@ -95,47 +92,43 @@ fn render_module(graph: &CodeGraph) -> String {
|
||||
let mut lines = Vec::new();
|
||||
|
||||
for module in graph.modules() {
|
||||
let id = sanitize(module.as_str());
|
||||
let id = sanitize_identifier(module.as_str());
|
||||
lines.push(format!("{id}: {}", module.as_str()));
|
||||
}
|
||||
|
||||
for (src, tgt) in graph.module_edges().keys() {
|
||||
lines.push(format!("{} -> {}", sanitize(src), sanitize(tgt)));
|
||||
lines.push(format!("{} -> {}", sanitize_identifier(src), sanitize_identifier(tgt)));
|
||||
}
|
||||
|
||||
lines.join("\n")
|
||||
}
|
||||
|
||||
fn render_project(graph: &CodeGraph) -> String {
|
||||
use archlens_domain::RelationshipKind;
|
||||
use std::collections::HashMap;
|
||||
|
||||
let mut lines = Vec::new();
|
||||
let (by_module, ungrouped) = graph.elements_by_module();
|
||||
|
||||
for (module, elements) in &by_module {
|
||||
let mod_id = sanitize(module);
|
||||
let mod_id = sanitize_identifier(module);
|
||||
lines.push(format!("{mod_id}: {{"));
|
||||
for el in elements {
|
||||
lines.push(format!(" {}: {}", sanitize(el.name()), el.name()));
|
||||
lines.push(format!(" {}: {}", sanitize_identifier(el.name()), el.name()));
|
||||
}
|
||||
lines.push("}".to_string());
|
||||
}
|
||||
|
||||
for el in &ungrouped {
|
||||
lines.push(format!("{}: {}", sanitize(el.name()), el.name()));
|
||||
lines.push(format!("{}: {}", sanitize_identifier(el.name()), el.name()));
|
||||
}
|
||||
|
||||
let name_to_id: HashMap<&str, String> = graph
|
||||
.elements()
|
||||
.iter()
|
||||
.map(|e| (e.name(), sanitize(e.name())))
|
||||
.map(|e| (e.name(), sanitize_identifier(e.name())))
|
||||
.collect();
|
||||
|
||||
for rel in graph.relationships() {
|
||||
if rel.kind() == RelationshipKind::Import {
|
||||
continue;
|
||||
}
|
||||
for rel in non_import_rels(graph.relationships()) {
|
||||
if let (Some(src), Some(tgt)) = (name_to_id.get(rel.source()), name_to_id.get(rel.target()))
|
||||
{
|
||||
lines.push(format!("{src} -> {tgt}"));
|
||||
|
||||
@@ -6,6 +6,7 @@ publish = false
|
||||
|
||||
[dependencies]
|
||||
archlens-domain.workspace = true
|
||||
archlens-rendering-primitives.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json = "1"
|
||||
|
||||
|
||||
@@ -3,8 +3,9 @@ use std::collections::HashMap;
|
||||
use serde::Serialize;
|
||||
|
||||
use archlens_domain::{
|
||||
CodeGraph, DomainError, RelationshipKind, RenderOutput, RenderedFile, ports::DiagramRenderer,
|
||||
CodeGraph, DomainError, RenderOutput, RenderedFile, ports::DiagramRenderer,
|
||||
};
|
||||
use archlens_rendering_primitives::non_import_rels;
|
||||
|
||||
pub struct HtmlRenderer;
|
||||
|
||||
@@ -66,10 +67,7 @@ impl DiagramRenderer for HtmlRenderer {
|
||||
});
|
||||
}
|
||||
|
||||
let edges = graph
|
||||
.relationships()
|
||||
.iter()
|
||||
.filter(|r| r.kind() != RelationshipKind::Import)
|
||||
let edges = non_import_rels(graph.relationships())
|
||||
.filter_map(|r| {
|
||||
let src = id_map.get(r.source())?;
|
||||
let tgt = id_map.get(r.target())?;
|
||||
|
||||
@@ -6,5 +6,6 @@ publish = false
|
||||
|
||||
[dependencies]
|
||||
archlens-domain.workspace = true
|
||||
archlens-rendering-primitives.workspace = true
|
||||
thiserror.workspace = true
|
||||
tracing.workspace = true
|
||||
|
||||
@@ -4,6 +4,7 @@ use archlens_domain::{
|
||||
CodeElement, CodeGraph, DiagramLevel, DomainError, ModuleName, RelationshipKind, RenderOutput,
|
||||
RenderedFile, Visibility, ports::DiagramRenderer,
|
||||
};
|
||||
use archlens_rendering_primitives::non_import_rels;
|
||||
|
||||
pub struct MermaidRenderer {
|
||||
level: DiagramLevel,
|
||||
@@ -94,14 +95,11 @@ impl MermaidRenderer {
|
||||
lines.extend(deferred_members);
|
||||
|
||||
let mut rel_seen: HashSet<String> = HashSet::new();
|
||||
for rel in graph.relationships() {
|
||||
if rel.kind() == RelationshipKind::Import {
|
||||
continue;
|
||||
}
|
||||
for rel in non_import_rels(graph.relationships()) {
|
||||
let arrow = match rel.kind() {
|
||||
RelationshipKind::Inheritance => "<|--",
|
||||
RelationshipKind::Composition => "-->",
|
||||
RelationshipKind::Import => "..>",
|
||||
RelationshipKind::Import => unreachable!("imports filtered by non_import_rels"),
|
||||
};
|
||||
let src = Self::display_name(rel.source());
|
||||
let tgt = Self::display_name(rel.target());
|
||||
|
||||
8
crates/adapters/rendering-primitives/Cargo.toml
Normal file
8
crates/adapters/rendering-primitives/Cargo.toml
Normal file
@@ -0,0 +1,8 @@
|
||||
[package]
|
||||
name = "archlens-rendering-primitives"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
archlens-domain.workspace = true
|
||||
11
crates/adapters/rendering-primitives/src/lib.rs
Normal file
11
crates/adapters/rendering-primitives/src/lib.rs
Normal file
@@ -0,0 +1,11 @@
|
||||
use archlens_domain::{Relationship, RelationshipKind};
|
||||
|
||||
/// Returns an iterator over all relationships except those with kind `Import`.
|
||||
pub fn non_import_rels(rels: &[Relationship]) -> impl Iterator<Item = &Relationship> {
|
||||
rels.iter().filter(|r| r.kind() != RelationshipKind::Import)
|
||||
}
|
||||
|
||||
/// Replaces `::`, `-`, `.`, and space with `_`.
|
||||
pub fn sanitize_identifier(name: &str) -> String {
|
||||
name.replace("::", "_").replace(['-', '.', ' '], "_")
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
use archlens_domain::{
|
||||
FilePath, Relationship, RelationshipKind,
|
||||
};
|
||||
use archlens_rendering_primitives::{non_import_rels, sanitize_identifier};
|
||||
|
||||
fn rel(src: &str, tgt: &str, kind: RelationshipKind) -> Relationship {
|
||||
Relationship::new(src, tgt, kind).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_import_rels_excludes_import_relationships() {
|
||||
let rels = vec![
|
||||
rel("A", "B", RelationshipKind::Composition),
|
||||
rel("C", "D", RelationshipKind::Import),
|
||||
rel("E", "F", RelationshipKind::Inheritance),
|
||||
];
|
||||
let filtered: Vec<_> = non_import_rels(&rels).collect();
|
||||
assert_eq!(filtered.len(), 2);
|
||||
assert!(filtered.iter().all(|r| r.kind() != RelationshipKind::Import));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_import_rels_passes_all_non_import_kinds() {
|
||||
let rels = vec![
|
||||
rel("A", "B", RelationshipKind::Composition),
|
||||
rel("C", "D", RelationshipKind::Inheritance),
|
||||
];
|
||||
let filtered: Vec<_> = non_import_rels(&rels).collect();
|
||||
assert_eq!(filtered.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sanitize_identifier_replaces_double_colon_with_underscore() {
|
||||
assert_eq!(sanitize_identifier("foo::bar"), "foo_bar");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sanitize_identifier_replaces_hyphen_with_underscore() {
|
||||
assert_eq!(sanitize_identifier("my-crate"), "my_crate");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sanitize_identifier_replaces_dot_with_underscore() {
|
||||
assert_eq!(sanitize_identifier("v1.2"), "v1_2");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sanitize_identifier_replaces_space_with_underscore() {
|
||||
assert_eq!(sanitize_identifier("my crate"), "my_crate");
|
||||
}
|
||||
Reference in New Issue
Block a user