pub struct ContextBuilder<'a> {
graph: &'a crate::graph::hex_graph::HexGraph,
}
impl<'a> ContextBuilder<'a> {
pub fn new(graph: &'a crate::graph::hex_graph::HexGraph) -> Self {
Self { graph }
}
pub fn build(self) -> crate::result::hex_result::HexResult<super::ai_context::AIContext> {
let components = self.build_components();
let relationships = self.build_relationships();
let constraints = self.build_constraints();
let suggestions = self.generate_suggestions(&components, &relationships);
Result::Ok(super::ai_context::AIContext {
architecture: String::from("hexagonal"),
version: String::from(env!("CARGO_PKG_VERSION")),
components,
relationships,
constraints,
suggestions,
metadata: super::ai_context::ContextMetadata {
generated_at: chrono::Utc::now().to_rfc3339(),
hex_version: String::from(env!("CARGO_PKG_VERSION")),
total_components: self.graph.node_count(),
total_relationships: self.graph.edge_count(),
schema_version: String::from("1.0.0"),
},
})
}
fn build_components(&self) -> Vec<super::ai_context::ComponentInfo> {
self
.graph
.nodes()
.map(|node| {
let role_str = format!("{:?}", node.role);
let methods =
crate::ai::method_extractor::extract_methods_for_type(&node.type_name, &role_str);
super::ai_context::ComponentInfo {
type_name: node.type_name.clone(),
layer: format!("{:?}", node.layer),
role: role_str,
module_path: node.module_path.clone(),
purpose: None,
dependencies: self
.graph
.edges_from(&node.id)
.into_iter()
.map(|edge| edge.target.to_string())
.collect(),
methods,
}
})
.collect()
}
fn build_relationships(&self) -> Vec<super::ai_context::RelationshipInfo> {
self
.graph
.edges()
.into_iter()
.map(|edge| {
let is_valid = self.validate_relationship(edge);
super::ai_context::RelationshipInfo {
from: edge.source.to_string(),
to: edge.target.to_string(),
relationship_type: format!("{:?}", edge.relationship),
is_valid,
validation_message: if is_valid {
None
} else {
Some(String::from("Violates layer dependency rules"))
},
}
})
.collect()
}
fn build_constraints(&self) -> super::ai_context::ConstraintSet {
super::ai_context::ConstraintSet {
dependency_rules: self.build_dependency_rules(),
layer_boundaries: self.build_layer_boundaries(),
naming_conventions: self.build_naming_conventions(),
required_patterns: vec![
String::from("One item per file"),
String::from("No use statements"),
String::from("Fully qualified paths"),
],
}
}
fn build_dependency_rules(&self) -> Vec<super::ai_context::DependencyRule> {
vec![
super::ai_context::DependencyRule {
from_layer: String::from("Domain"),
to_layer: String::from("Infrastructure"),
allowed: false,
reason: String::from("Domain must not depend on infrastructure"),
},
super::ai_context::DependencyRule {
from_layer: String::from("Application"),
to_layer: String::from("Domain"),
allowed: true,
reason: String::from("Application coordinates domain logic"),
},
]
}
fn build_layer_boundaries(&self) -> Vec<super::ai_context::LayerBoundary> {
vec![
super::ai_context::LayerBoundary {
layer: String::from("Domain"),
can_depend_on: vec![],
dependents_allowed: vec![String::from("Ports"), String::from("Application")],
purpose: String::from("Pure business logic with zero dependencies"),
},
super::ai_context::LayerBoundary {
layer: String::from("Ports"),
can_depend_on: vec![String::from("Domain")],
dependents_allowed: vec![String::from("Adapters"), String::from("Application")],
purpose: String::from("Interfaces defining what application needs"),
},
]
}
fn build_naming_conventions(&self) -> Vec<super::ai_context::NamingConvention> {
vec![
super::ai_context::NamingConvention {
applies_to: String::from("Repository ports"),
pattern: String::from("*Repository trait"),
example: String::from("trait UserRepository: Repository<User>"),
},
super::ai_context::NamingConvention {
applies_to: String::from("Directives"),
pattern: String::from("*Directive struct"),
example: String::from("struct CreateUserDirective"),
},
]
}
fn validate_relationship(&self, _edge: &crate::graph::hex_edge::HexEdge) -> bool {
true
}
fn generate_suggestions(
&self,
components: &[super::ai_context::ComponentInfo],
_relationships: &[super::ai_context::RelationshipInfo],
) -> Vec<super::ai_context::Suggestion> {
let mut suggestions = Vec::new();
let ports: Vec<_> = components.iter().filter(|c| c.layer == "Port").collect();
let adapters: Vec<_> = components.iter().filter(|c| c.layer == "Adapter").collect();
if ports.len() > adapters.len() {
suggestions.push(super::ai_context::Suggestion {
suggestion_type: super::ai_context::SuggestionType::MissingImplementation,
component: None,
description: String::from("More ports than adapters - some ports may need implementations"),
priority: super::ai_context::Priority::Medium,
code_example: None,
});
}
suggestions
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_context_builder_new() {
let graph = crate::graph::builder::GraphBuilder::new().build();
let builder = ContextBuilder::new(&graph);
assert_eq!(builder.graph.node_count(), 0);
}
#[test]
fn test_build_complete_context() {
let graph = crate::graph::builder::GraphBuilder::new()
.add_node(crate::graph::hex_node::HexNode::new(
crate::graph::node_id::NodeId::from_name("TestEntity"),
crate::graph::layer::Layer::Domain,
crate::graph::role::Role::Entity,
"TestEntity",
"test::entity",
))
.build();
let builder = ContextBuilder::new(&graph);
let context = builder.build().unwrap();
assert_eq!(context.architecture, "hexagonal");
assert_eq!(context.components.len(), 1);
assert_eq!(context.components[0].type_name, "TestEntity");
assert_eq!(context.metadata.total_components, 1);
}
#[test]
fn test_build_components() {
let graph = crate::graph::builder::GraphBuilder::new()
.add_node(crate::graph::hex_node::HexNode::new(
crate::graph::node_id::NodeId::from_name("User"),
crate::graph::layer::Layer::Domain,
crate::graph::role::Role::Entity,
"User",
"domain::user",
))
.build();
let builder = ContextBuilder::new(&graph);
let components = builder.build_components();
assert_eq!(components.len(), 1);
assert_eq!(components[0].type_name, "User");
assert_eq!(components[0].layer, "Domain");
assert_eq!(components[0].role, "Entity");
}
#[test]
fn test_build_relationships() {
let graph = crate::graph::builder::GraphBuilder::new()
.add_node(crate::graph::hex_node::HexNode::new(
crate::graph::node_id::NodeId::from_name("User"),
crate::graph::layer::Layer::Domain,
crate::graph::role::Role::Entity,
"User",
"domain",
))
.add_node(crate::graph::hex_node::HexNode::new(
crate::graph::node_id::NodeId::from_name("UserRepo"),
crate::graph::layer::Layer::Port,
crate::graph::role::Role::Repository,
"UserRepo",
"ports",
))
.add_edge(crate::graph::hex_edge::HexEdge::new(
crate::graph::node_id::NodeId::from_name("UserRepo"),
crate::graph::node_id::NodeId::from_name("User"),
crate::graph::relationship::Relationship::Depends,
))
.build();
let builder = ContextBuilder::new(&graph);
let relationships = builder.build_relationships();
assert_eq!(relationships.len(), 1);
assert!(relationships[0].is_valid);
}
#[test]
fn test_build_constraints() {
let graph = crate::graph::builder::GraphBuilder::new().build();
let builder = ContextBuilder::new(&graph);
let constraints = builder.build_constraints();
assert!(!constraints.dependency_rules.is_empty());
assert!(!constraints.layer_boundaries.is_empty());
assert!(!constraints.naming_conventions.is_empty());
assert!(!constraints.required_patterns.is_empty());
}
#[test]
fn test_build_dependency_rules() {
let graph = crate::graph::builder::GraphBuilder::new().build();
let builder = ContextBuilder::new(&graph);
let rules = builder.build_dependency_rules();
assert!(rules.iter().any(|r| r.from_layer == "Domain" && !r.allowed));
assert!(
rules
.iter()
.any(|r| r.from_layer == "Application" && r.allowed)
);
}
#[test]
fn test_build_layer_boundaries() {
let graph = crate::graph::builder::GraphBuilder::new().build();
let builder = ContextBuilder::new(&graph);
let boundaries = builder.build_layer_boundaries();
let domain = boundaries.iter().find(|b| b.layer == "Domain");
assert!(domain.is_some());
assert!(domain.unwrap().can_depend_on.is_empty());
}
#[test]
fn test_build_naming_conventions() {
let graph = crate::graph::builder::GraphBuilder::new().build();
let builder = ContextBuilder::new(&graph);
let conventions = builder.build_naming_conventions();
assert!(
conventions
.iter()
.any(|c| c.applies_to == "Repository ports")
);
assert!(conventions.iter().any(|c| c.applies_to == "Directives"));
}
#[test]
fn test_validate_relationship() {
let graph = crate::graph::builder::GraphBuilder::new()
.add_node(crate::graph::hex_node::HexNode::new(
crate::graph::node_id::NodeId::from_name("A"),
crate::graph::layer::Layer::Domain,
crate::graph::role::Role::Entity,
"A",
"domain",
))
.add_node(crate::graph::hex_node::HexNode::new(
crate::graph::node_id::NodeId::from_name("B"),
crate::graph::layer::Layer::Port,
crate::graph::role::Role::Repository,
"B",
"ports",
))
.add_edge(crate::graph::hex_edge::HexEdge::new(
crate::graph::node_id::NodeId::from_name("A"),
crate::graph::node_id::NodeId::from_name("B"),
crate::graph::relationship::Relationship::Depends,
))
.build();
let builder = ContextBuilder::new(&graph);
let edge = graph.edges().iter().next().unwrap();
assert!(builder.validate_relationship(edge));
}
#[test]
fn test_generate_suggestions_with_ports_no_adapters() {
let graph = crate::graph::builder::GraphBuilder::new()
.add_node(crate::graph::hex_node::HexNode::new(
crate::graph::node_id::NodeId::from_name("UserRepository"),
crate::graph::layer::Layer::Port,
crate::graph::role::Role::Repository,
"UserRepository",
"ports",
))
.build();
let builder = ContextBuilder::new(&graph);
let context = builder.build().unwrap();
assert!(!context.suggestions.is_empty());
assert!(context.suggestions.iter().any(|s| matches!(
s.suggestion_type,
super::super::ai_context::SuggestionType::MissingImplementation
)));
}
#[test]
fn test_generate_suggestions_balanced_architecture() {
let graph = crate::graph::builder::GraphBuilder::new()
.add_node(crate::graph::hex_node::HexNode::new(
crate::graph::node_id::NodeId::from_name("UserRepository"),
crate::graph::layer::Layer::Port,
crate::graph::role::Role::Repository,
"UserRepository",
"ports",
))
.add_node(crate::graph::hex_node::HexNode::new(
crate::graph::node_id::NodeId::from_name("PostgresUserRepo"),
crate::graph::layer::Layer::Adapter,
crate::graph::role::Role::Adapter,
"PostgresUserRepo",
"adapters",
))
.build();
let builder = ContextBuilder::new(&graph);
let components = builder.build_components();
let relationships = builder.build_relationships();
let suggestions = builder.generate_suggestions(&components, &relationships);
assert!(suggestions.is_empty());
}
}