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/python-project",
|
||||||
"crates/adapters/d2",
|
"crates/adapters/d2",
|
||||||
"crates/adapters/html-viewer",
|
"crates/adapters/html-viewer",
|
||||||
|
"crates/adapters/rendering-primitives",
|
||||||
]
|
]
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
@@ -32,6 +33,7 @@ archlens-cargo-workspace = { path = "crates/adapters/cargo-workspace" }
|
|||||||
archlens-python-project = { path = "crates/adapters/python-project" }
|
archlens-python-project = { path = "crates/adapters/python-project" }
|
||||||
archlens-d2 = { path = "crates/adapters/d2" }
|
archlens-d2 = { path = "crates/adapters/d2" }
|
||||||
archlens-html = { path = "crates/adapters/html-viewer" }
|
archlens-html = { path = "crates/adapters/html-viewer" }
|
||||||
|
archlens-rendering-primitives = { path = "crates/adapters/rendering-primitives" }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
|
|
||||||
# Error handling
|
# Error handling
|
||||||
|
|||||||
@@ -6,5 +6,6 @@ publish = false
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
archlens-domain.workspace = true
|
archlens-domain.workspace = true
|
||||||
|
archlens-rendering-primitives.workspace = true
|
||||||
thiserror.workspace = true
|
thiserror.workspace = true
|
||||||
tracing.workspace = true
|
tracing.workspace = true
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ use archlens_domain::{
|
|||||||
CodeElement, CodeGraph, DomainError, RelationshipKind, RenderOutput, RenderedFile,
|
CodeElement, CodeGraph, DomainError, RelationshipKind, RenderOutput, RenderedFile,
|
||||||
ports::DiagramRenderer,
|
ports::DiagramRenderer,
|
||||||
};
|
};
|
||||||
|
use archlens_rendering_primitives::non_import_rels;
|
||||||
|
|
||||||
pub struct AsciiRenderer;
|
pub struct AsciiRenderer;
|
||||||
|
|
||||||
@@ -94,16 +95,12 @@ impl DiagramRenderer for AsciiRenderer {
|
|||||||
lines.push("└───".to_string());
|
lines.push("└───".to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
let non_import_rels: Vec<_> = graph
|
let filtered_rels: Vec<_> = non_import_rels(graph.relationships()).collect();
|
||||||
.relationships()
|
|
||||||
.iter()
|
|
||||||
.filter(|r| r.kind() != RelationshipKind::Import)
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
if !non_import_rels.is_empty() {
|
if !filtered_rels.is_empty() {
|
||||||
lines.push(String::new());
|
lines.push(String::new());
|
||||||
lines.push("── Relationships ──".to_string());
|
lines.push("── Relationships ──".to_string());
|
||||||
for rel in &non_import_rels {
|
for rel in &filtered_rels {
|
||||||
let arrow = match rel.kind() {
|
let arrow = match rel.kind() {
|
||||||
RelationshipKind::Inheritance => "extends",
|
RelationshipKind::Inheritance => "extends",
|
||||||
RelationshipKind::Composition => "has",
|
RelationshipKind::Composition => "has",
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ publish = false
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
archlens-domain.workspace = true
|
archlens-domain.workspace = true
|
||||||
|
archlens-rendering-primitives.workspace = true
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tempfile.workspace = true
|
tempfile.workspace = true
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
use archlens_domain::{
|
use archlens_domain::{
|
||||||
CodeGraph, DiagramLevel, DomainError, RenderOutput, RenderedFile, ports::DiagramRenderer,
|
CodeGraph, DiagramLevel, DomainError, RenderOutput, RenderedFile, ports::DiagramRenderer,
|
||||||
};
|
};
|
||||||
|
use archlens_rendering_primitives::{non_import_rels, sanitize_identifier};
|
||||||
|
|
||||||
pub struct D2Renderer {
|
pub struct D2Renderer {
|
||||||
level: DiagramLevel,
|
level: DiagramLevel,
|
||||||
@@ -36,20 +37,16 @@ impl DiagramRenderer for D2Renderer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn sanitize(name: &str) -> String {
|
|
||||||
name.replace("::", "_").replace(['-', ' '], "_")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_type(graph: &CodeGraph) -> String {
|
fn render_type(graph: &CodeGraph) -> String {
|
||||||
let mut lines = Vec::new();
|
let mut lines = Vec::new();
|
||||||
let (by_module, ungrouped) = graph.elements_by_module();
|
let (by_module, ungrouped) = graph.elements_by_module();
|
||||||
|
|
||||||
// Grouped by module
|
// Grouped by module
|
||||||
for (module, elements) in &by_module {
|
for (module, elements) in &by_module {
|
||||||
let mod_id = sanitize(module);
|
let mod_id = sanitize_identifier(module);
|
||||||
lines.push(format!("{mod_id}: {{"));
|
lines.push(format!("{mod_id}: {{"));
|
||||||
for el in elements {
|
for el in elements {
|
||||||
let el_id = sanitize(el.name());
|
let el_id = sanitize_identifier(el.name());
|
||||||
lines.push(format!(" {el_id}: {{"));
|
lines.push(format!(" {el_id}: {{"));
|
||||||
lines.push(" shape: class".to_string());
|
lines.push(" shape: class".to_string());
|
||||||
for field in el.fields() {
|
for field in el.fields() {
|
||||||
@@ -69,21 +66,21 @@ fn render_type(graph: &CodeGraph) -> String {
|
|||||||
|
|
||||||
// Ungrouped elements
|
// Ungrouped elements
|
||||||
for el in &ungrouped {
|
for el in &ungrouped {
|
||||||
let el_id = sanitize(el.name());
|
let el_id = sanitize_identifier(el.name());
|
||||||
lines.push(format!("{el_id}: {{"));
|
lines.push(format!("{el_id}: {{"));
|
||||||
lines.push(" shape: class".to_string());
|
lines.push(" shape: class".to_string());
|
||||||
lines.push("}".to_string());
|
lines.push("}".to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Relationships
|
// Relationships
|
||||||
for rel in graph.relationships() {
|
for rel in non_import_rels(graph.relationships()) {
|
||||||
use archlens_domain::RelationshipKind;
|
use archlens_domain::RelationshipKind;
|
||||||
let src = sanitize(rel.source());
|
let src = sanitize_identifier(rel.source());
|
||||||
let tgt = sanitize(rel.target());
|
let tgt = sanitize_identifier(rel.target());
|
||||||
let arrow = match rel.kind() {
|
let arrow = match rel.kind() {
|
||||||
RelationshipKind::Inheritance => format!("{src} -> {tgt}: {{style.stroke-dash: 0}}"),
|
RelationshipKind::Inheritance => format!("{src} -> {tgt}: {{style.stroke-dash: 0}}"),
|
||||||
RelationshipKind::Composition => format!("{src} -> {tgt}"),
|
RelationshipKind::Composition => format!("{src} -> {tgt}"),
|
||||||
RelationshipKind::Import => continue,
|
RelationshipKind::Import => unreachable!("imports filtered by non_import_rels"),
|
||||||
};
|
};
|
||||||
lines.push(arrow);
|
lines.push(arrow);
|
||||||
}
|
}
|
||||||
@@ -95,47 +92,43 @@ fn render_module(graph: &CodeGraph) -> String {
|
|||||||
let mut lines = Vec::new();
|
let mut lines = Vec::new();
|
||||||
|
|
||||||
for module in graph.modules() {
|
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()));
|
lines.push(format!("{id}: {}", module.as_str()));
|
||||||
}
|
}
|
||||||
|
|
||||||
for (src, tgt) in graph.module_edges().keys() {
|
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")
|
lines.join("\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_project(graph: &CodeGraph) -> String {
|
fn render_project(graph: &CodeGraph) -> String {
|
||||||
use archlens_domain::RelationshipKind;
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
let mut lines = Vec::new();
|
let mut lines = Vec::new();
|
||||||
let (by_module, ungrouped) = graph.elements_by_module();
|
let (by_module, ungrouped) = graph.elements_by_module();
|
||||||
|
|
||||||
for (module, elements) in &by_module {
|
for (module, elements) in &by_module {
|
||||||
let mod_id = sanitize(module);
|
let mod_id = sanitize_identifier(module);
|
||||||
lines.push(format!("{mod_id}: {{"));
|
lines.push(format!("{mod_id}: {{"));
|
||||||
for el in elements {
|
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());
|
lines.push("}".to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
for el in &ungrouped {
|
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
|
let name_to_id: HashMap<&str, String> = graph
|
||||||
.elements()
|
.elements()
|
||||||
.iter()
|
.iter()
|
||||||
.map(|e| (e.name(), sanitize(e.name())))
|
.map(|e| (e.name(), sanitize_identifier(e.name())))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
for rel in graph.relationships() {
|
for rel in non_import_rels(graph.relationships()) {
|
||||||
if rel.kind() == RelationshipKind::Import {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if let (Some(src), Some(tgt)) = (name_to_id.get(rel.source()), name_to_id.get(rel.target()))
|
if let (Some(src), Some(tgt)) = (name_to_id.get(rel.source()), name_to_id.get(rel.target()))
|
||||||
{
|
{
|
||||||
lines.push(format!("{src} -> {tgt}"));
|
lines.push(format!("{src} -> {tgt}"));
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ publish = false
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
archlens-domain.workspace = true
|
archlens-domain.workspace = true
|
||||||
|
archlens-rendering-primitives.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
|
|
||||||
|
|||||||
@@ -3,8 +3,9 @@ use std::collections::HashMap;
|
|||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
use archlens_domain::{
|
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;
|
pub struct HtmlRenderer;
|
||||||
|
|
||||||
@@ -66,10 +67,7 @@ impl DiagramRenderer for HtmlRenderer {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let edges = graph
|
let edges = non_import_rels(graph.relationships())
|
||||||
.relationships()
|
|
||||||
.iter()
|
|
||||||
.filter(|r| r.kind() != RelationshipKind::Import)
|
|
||||||
.filter_map(|r| {
|
.filter_map(|r| {
|
||||||
let src = id_map.get(r.source())?;
|
let src = id_map.get(r.source())?;
|
||||||
let tgt = id_map.get(r.target())?;
|
let tgt = id_map.get(r.target())?;
|
||||||
|
|||||||
@@ -6,5 +6,6 @@ publish = false
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
archlens-domain.workspace = true
|
archlens-domain.workspace = true
|
||||||
|
archlens-rendering-primitives.workspace = true
|
||||||
thiserror.workspace = true
|
thiserror.workspace = true
|
||||||
tracing.workspace = true
|
tracing.workspace = true
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ use archlens_domain::{
|
|||||||
CodeElement, CodeGraph, DiagramLevel, DomainError, ModuleName, RelationshipKind, RenderOutput,
|
CodeElement, CodeGraph, DiagramLevel, DomainError, ModuleName, RelationshipKind, RenderOutput,
|
||||||
RenderedFile, Visibility, ports::DiagramRenderer,
|
RenderedFile, Visibility, ports::DiagramRenderer,
|
||||||
};
|
};
|
||||||
|
use archlens_rendering_primitives::non_import_rels;
|
||||||
|
|
||||||
pub struct MermaidRenderer {
|
pub struct MermaidRenderer {
|
||||||
level: DiagramLevel,
|
level: DiagramLevel,
|
||||||
@@ -94,14 +95,11 @@ impl MermaidRenderer {
|
|||||||
lines.extend(deferred_members);
|
lines.extend(deferred_members);
|
||||||
|
|
||||||
let mut rel_seen: HashSet<String> = HashSet::new();
|
let mut rel_seen: HashSet<String> = HashSet::new();
|
||||||
for rel in graph.relationships() {
|
for rel in non_import_rels(graph.relationships()) {
|
||||||
if rel.kind() == RelationshipKind::Import {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let arrow = match rel.kind() {
|
let arrow = match rel.kind() {
|
||||||
RelationshipKind::Inheritance => "<|--",
|
RelationshipKind::Inheritance => "<|--",
|
||||||
RelationshipKind::Composition => "-->",
|
RelationshipKind::Composition => "-->",
|
||||||
RelationshipKind::Import => "..>",
|
RelationshipKind::Import => unreachable!("imports filtered by non_import_rels"),
|
||||||
};
|
};
|
||||||
let src = Self::display_name(rel.source());
|
let src = Self::display_name(rel.source());
|
||||||
let tgt = Self::display_name(rel.target());
|
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