#[derive(Debug, Clone)]
pub struct HexGraph {
pub(crate) inner: std::sync::Arc<GraphInner>,
}
#[derive(Debug)]
pub(crate) struct GraphInner {
pub(crate) nodes:
std::collections::BTreeMap<crate::graph::node_id::NodeId, crate::graph::hex_node::HexNode>,
pub(crate) edges: Vec<crate::graph::hex_edge::HexEdge>,
pub(crate) outgoing: indexmap::IndexMap<crate::graph::node_id::NodeId, std::vec::Vec<usize>>,
pub(crate) incoming: indexmap::IndexMap<crate::graph::node_id::NodeId, std::vec::Vec<usize>>,
pub(crate) metadata: crate::graph::metadata::GraphMetadata,
}
impl HexGraph {
fn arch() -> &'static arc_swap::ArcSwap<HexGraph> {
static ARCH: std::sync::LazyLock<arc_swap::ArcSwap<HexGraph>> =
std::sync::LazyLock::new(|| {
arc_swap::ArcSwap::from_pointee(
crate::registry::component_registry::ComponentRegistry::build_graph(),
)
});
&ARCH
}
pub fn current() -> std::sync::Arc<Self> {
Self::arch().load_full()
}
pub fn install(graph: HexGraph) {
Self::arch().store(std::sync::Arc::new(graph));
}
pub fn rebuild_current() {
Self::install(crate::registry::component_registry::ComponentRegistry::build_graph());
}
pub fn new() -> Self {
Self {
inner: std::sync::Arc::new(GraphInner {
nodes: std::collections::BTreeMap::new(),
edges: Vec::new(),
outgoing: indexmap::IndexMap::new(),
incoming: indexmap::IndexMap::new(),
metadata: crate::graph::metadata::GraphMetadata::default(),
}),
}
}
#[cfg(feature = "visualization")]
pub fn to_dot(&self) -> crate::result::hex_result::HexResult<String> {
let exporter = crate::graph::visualization::adapters::dot_exporter::DotExporter::new();
let use_case =
crate::graph::visualization::application::export_graph::ExportGraph::new(&exporter);
use_case.execute(
self,
crate::graph::visualization::domain::visual_style::VisualStyle::default(),
)
}
#[cfg(feature = "visualization")]
pub fn to_mermaid(&self) -> crate::result::hex_result::HexResult<String> {
let exporter = crate::graph::visualization::adapters::mermaid_exporter::MermaidExporter::new();
let use_case =
crate::graph::visualization::application::export_graph::ExportGraph::new(&exporter);
use_case.execute(
self,
crate::graph::visualization::domain::visual_style::VisualStyle::default(),
)
}
#[cfg(feature = "visualization")]
pub fn to_json(&self) -> crate::result::hex_result::HexResult<String> {
let exporter = crate::graph::visualization::adapters::json_exporter::JsonExporter::new();
let use_case =
crate::graph::visualization::application::export_graph::ExportGraph::new(&exporter);
use_case.execute(
self,
crate::graph::visualization::domain::visual_style::VisualStyle::default(),
)
}
#[cfg(feature = "visualization")]
pub fn save_visualization(
&self,
path: &std::path::Path,
exporter: &dyn crate::graph::visualization::ports::format_exporter::FormatExporter,
) -> crate::result::hex_result::HexResult<()> {
let use_case =
crate::graph::visualization::application::export_graph::ExportGraph::new(exporter);
let content = use_case.execute(
self,
crate::graph::visualization::domain::visual_style::VisualStyle::default(),
)?;
std::fs::write(path, content).map_err(|e| {
crate::error::hex_error::Hexserror::adapter(
crate::error::codes::io::IO_FAILURE,
&format!("Failed to write file: {e}"),
)
.with_next_step("Check file path and permissions")
.with_suggestion("Verify directory exists and is writable")
})
}
pub fn builder() -> crate::graph::builder::GraphBuilder {
crate::graph::builder::GraphBuilder::new()
}
pub fn layer_count(&self) -> usize {
let mut layers = std::collections::HashSet::new();
for node in self.nodes() {
layers.insert(node.layer());
}
layers.len()
}
pub fn node_count(&self) -> usize {
self.inner.nodes.len()
}
pub fn edge_count(&self) -> usize {
self.inner.edges.len()
}
#[cfg(feature = "ai")]
pub fn to_ai_context(&self) -> crate::result::hex_result::HexResult<crate::ai::AIContext> {
crate::ai::ContextBuilder::new(self).build()
}
pub fn get_node(
&self,
id: &crate::graph::node_id::NodeId,
) -> Option<&crate::graph::hex_node::HexNode> {
self.inner.nodes.get(id)
}
#[allow(clippy::disallowed_macros)]
pub fn pretty_print(&self) {
println!("Hexagonal Architecture Graph:");
println!(" Nodes: {}", self.node_count());
println!(" Edges: {}", self.edge_count());
println!("\nBy Layer:");
for layer in [
crate::graph::layer::Layer::Domain,
crate::graph::layer::Layer::Port,
crate::graph::layer::Layer::Adapter,
crate::graph::layer::Layer::Application,
crate::graph::layer::Layer::Infrastructure,
] {
let count = self.nodes_by_layer(layer).len();
if count > 0 {
println!(" {layer:?}: {count}");
}
}
}
pub fn nodes(&self) -> impl Iterator<Item = &crate::graph::hex_node::HexNode> {
self.inner.nodes.values()
}
pub fn edges(&self) -> &[crate::graph::hex_edge::HexEdge] {
&self.inner.edges
}
pub fn nodes_by_layer(
&self,
layer: crate::graph::layer::Layer,
) -> Vec<&crate::graph::hex_node::HexNode> {
self
.inner
.nodes
.values()
.filter(|n| n.layer() == layer)
.collect()
}
pub fn nodes_by_role(
&self,
role: crate::graph::role::Role,
) -> Vec<&crate::graph::hex_node::HexNode> {
self
.inner
.nodes
.values()
.filter(|n| n.role() == role)
.collect()
}
pub fn edges_from(
&self,
source: &crate::graph::node_id::NodeId,
) -> Vec<&crate::graph::hex_edge::HexEdge> {
match self.inner.outgoing.get(source) {
std::option::Option::Some(indices) => indices.iter().map(|&i| &self.inner.edges[i]).collect(),
std::option::Option::None => std::vec::Vec::new(),
}
}
pub fn edges_to(
&self,
target: &crate::graph::node_id::NodeId,
) -> Vec<&crate::graph::hex_edge::HexEdge> {
match self.inner.incoming.get(target) {
std::option::Option::Some(indices) => indices.iter().map(|&i| &self.inner.edges[i]).collect(),
std::option::Option::None => std::vec::Vec::new(),
}
}
pub fn metadata(&self) -> &crate::graph::metadata::GraphMetadata {
&self.inner.metadata
}
pub fn is_empty(&self) -> bool {
self.inner.nodes.is_empty()
}
}
impl Default for HexGraph {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_empty_graph() {
let graph = HexGraph::new();
assert_eq!(graph.node_count(), 0);
assert_eq!(graph.edge_count(), 0);
assert!(graph.is_empty());
}
#[test]
fn test_graph_thread_safety() {
let graph = HexGraph::new();
let graph_clone = graph.clone();
std::thread::spawn(move || {
assert_eq!(graph_clone.node_count(), 0);
})
.join()
.unwrap();
}
#[test]
fn test_graph_default() {
let graph = HexGraph::default();
assert!(graph.is_empty());
}
fn node(name: &str, layer: crate::graph::layer::Layer) -> crate::graph::hex_node::HexNode {
crate::graph::hex_node::HexNode::new(
crate::graph::node_id::NodeId::from_name(name),
layer,
crate::graph::role::Role::Entity,
name,
"test",
)
}
fn edge(from: &str, to: &str) -> crate::graph::hex_edge::HexEdge {
crate::graph::hex_edge::HexEdge::new(
crate::graph::node_id::NodeId::from_name(from),
crate::graph::node_id::NodeId::from_name(to),
crate::graph::relationship::Relationship::Depends,
)
}
#[test]
#[serial_test::serial(hexser_arch)]
fn test_current_is_cached_same_arc() {
let a = HexGraph::current();
let b = HexGraph::current();
assert!(
std::sync::Arc::ptr_eq(&a, &b),
"current() must return the same graph Arc when nothing was installed between loads"
);
}
#[test]
#[serial_test::serial(hexser_arch)]
fn test_arcswap_16_thread_read_during_install() {
let original = HexGraph::current();
let stop = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
let mut readers = std::vec::Vec::new();
for _ in 0..16 {
let stop = std::sync::Arc::clone(&stop);
readers.push(std::thread::spawn(move || {
while !stop.load(std::sync::atomic::Ordering::Relaxed) {
let g = HexGraph::current();
let _ = g.node_count();
let _ = g.edge_count();
}
}));
}
for i in 1..=64u32 {
let mut builder = HexGraph::builder();
for n in 0..i {
builder = builder.with_node(node(&format!("N{n}"), crate::graph::layer::Layer::Domain));
}
HexGraph::install(builder.build());
}
stop.store(true, std::sync::atomic::Ordering::Relaxed);
for r in readers {
r.join().expect("reader thread must not panic");
}
assert_eq!(
HexGraph::current().node_count(),
64,
"final current() must reflect the last install"
);
HexGraph::install((*original).clone());
}
#[test]
fn test_adjacency_index_matches_bruteforce() {
let graph = HexGraph::builder()
.with_node(node("A", crate::graph::layer::Layer::Domain))
.with_node(node("B", crate::graph::layer::Layer::Port))
.with_node(node("C", crate::graph::layer::Layer::Adapter))
.with_edge(edge("A", "B"))
.with_edge(edge("A", "C"))
.with_edge(edge("C", "B"))
.build();
let a = crate::graph::node_id::NodeId::from_name("A");
let b = crate::graph::node_id::NodeId::from_name("B");
let from_a = graph.edges_from(&a);
assert_eq!(from_a.len(), 2, "A has two outgoing edges");
assert!(from_a.iter().all(|e| e.source() == &a));
let to_b = graph.edges_to(&b);
assert_eq!(to_b.len(), 2, "B has two incoming edges");
assert!(to_b.iter().all(|e| e.target() == &b));
let isolated = crate::graph::node_id::NodeId::from_name("Z");
assert!(graph.edges_from(&isolated).is_empty());
assert!(graph.edges_to(&isolated).is_empty());
}
#[test]
fn test_node_iteration_is_deterministic() {
let order1: std::vec::Vec<_> = HexGraph::builder()
.with_node(node("A", crate::graph::layer::Layer::Domain))
.with_node(node("B", crate::graph::layer::Layer::Port))
.with_node(node("C", crate::graph::layer::Layer::Adapter))
.build()
.nodes()
.map(|n| *n.id())
.collect();
let order2: std::vec::Vec<_> = HexGraph::builder()
.with_node(node("C", crate::graph::layer::Layer::Adapter))
.with_node(node("A", crate::graph::layer::Layer::Domain))
.with_node(node("B", crate::graph::layer::Layer::Port))
.build()
.nodes()
.map(|n| *n.id())
.collect();
assert_eq!(
order1, order2,
"iteration order must not depend on insertion order"
);
}
}