sara_core/graph/
builder.rs1#![allow(clippy::result_large_err)]
4
5use std::path::PathBuf;
6
7use crate::error::SaraError;
8use crate::graph::KnowledgeGraph;
9use crate::model::{Item, ItemId, RelationshipType};
10
11#[derive(Debug, Default)]
13pub struct GraphBuilder {
14 items: Vec<Item>,
15 strict_mode: bool,
16 repositories: Vec<PathBuf>,
17}
18
19impl GraphBuilder {
20 pub fn new() -> Self {
22 Self::default()
23 }
24
25 pub fn with_strict_mode(mut self, strict: bool) -> Self {
27 self.strict_mode = strict;
28 self
29 }
30
31 pub fn add_repository(mut self, path: impl Into<PathBuf>) -> Self {
33 self.repositories.push(path.into());
34 self
35 }
36
37 pub fn add_item(mut self, item: Item) -> Self {
39 self.items.push(item);
40 self
41 }
42
43 pub fn add_items(mut self, items: impl IntoIterator<Item = Item>) -> Self {
45 self.items.extend(items);
46 self
47 }
48
49 pub fn build(self) -> Result<KnowledgeGraph, SaraError> {
51 let mut graph = KnowledgeGraph::new(self.strict_mode);
52
53 for item in &self.items {
55 graph.add_item(item.clone());
56 }
57
58 for item in &self.items {
60 self.add_relationships_for_item(&mut graph, item);
61 }
62
63 Ok(graph)
64 }
65
66 fn add_relationships_for_item(&self, graph: &mut KnowledgeGraph, item: &Item) {
68 for target_id in &item.upstream.refines {
70 graph.add_relationship(&item.id, target_id, RelationshipType::Refines);
71 }
72 for target_id in &item.upstream.derives_from {
73 graph.add_relationship(&item.id, target_id, RelationshipType::DerivesFrom);
74 }
75 for target_id in &item.upstream.satisfies {
76 graph.add_relationship(&item.id, target_id, RelationshipType::Satisfies);
77 }
78
79 for target_id in &item.downstream.is_refined_by {
81 graph.add_relationship(&item.id, target_id, RelationshipType::IsRefinedBy);
82 graph.add_relationship(target_id, &item.id, RelationshipType::Refines);
84 }
85 for target_id in &item.downstream.derives {
86 graph.add_relationship(&item.id, target_id, RelationshipType::Derives);
87 graph.add_relationship(target_id, &item.id, RelationshipType::DerivesFrom);
89 }
90 for target_id in &item.downstream.is_satisfied_by {
91 graph.add_relationship(&item.id, target_id, RelationshipType::IsSatisfiedBy);
92 graph.add_relationship(target_id, &item.id, RelationshipType::Satisfies);
94 }
95
96 for target_id in &item.attributes.depends_on {
98 graph.add_relationship(&item.id, target_id, RelationshipType::DependsOn);
99 }
100 }
101}
102
103pub fn resolve_cross_repository_refs(graph: &mut KnowledgeGraph) -> Vec<(ItemId, ItemId)> {
105 let mut unresolved = Vec::new();
106
107 let referenced_ids: Vec<ItemId> = graph
109 .items()
110 .flat_map(|item| item.all_references())
111 .cloned()
112 .collect();
113
114 for ref_id in referenced_ids {
116 if !graph.contains(&ref_id) {
117 for item in graph.items() {
119 if item.all_references().iter().any(|id| **id == ref_id) {
120 unresolved.push((item.id.clone(), ref_id.clone()));
121 break;
122 }
123 }
124 }
125 }
126
127 unresolved
128}
129
130#[cfg(test)]
131mod tests {
132 use super::*;
133 use crate::model::{ItemBuilder, ItemType, SourceLocation, UpstreamRefs};
134 use std::path::PathBuf;
135
136 fn create_test_item(id: &str, item_type: ItemType) -> Item {
137 let source = SourceLocation::new(PathBuf::from("/repo"), format!("{}.md", id));
138 let mut builder = ItemBuilder::new()
139 .id(ItemId::new_unchecked(id))
140 .item_type(item_type)
141 .name(format!("Test {}", id))
142 .source(source);
143
144 if item_type.requires_specification() {
145 builder = builder.specification("Test specification");
146 }
147
148 builder.build().unwrap()
149 }
150
151 fn create_test_item_with_upstream(
152 id: &str,
153 item_type: ItemType,
154 upstream: UpstreamRefs,
155 ) -> Item {
156 let source = SourceLocation::new(PathBuf::from("/repo"), format!("{}.md", id));
157 let mut builder = ItemBuilder::new()
158 .id(ItemId::new_unchecked(id))
159 .item_type(item_type)
160 .name(format!("Test {}", id))
161 .source(source)
162 .upstream(upstream);
163
164 if item_type.requires_specification() {
165 builder = builder.specification("Test specification");
166 }
167
168 builder.build().unwrap()
169 }
170
171 #[test]
172 fn test_build_simple_graph() {
173 let graph = GraphBuilder::new()
174 .add_item(create_test_item("SOL-001", ItemType::Solution))
175 .build()
176 .unwrap();
177
178 assert_eq!(graph.item_count(), 1);
179 }
180
181 #[test]
182 fn test_build_graph_with_relationships() {
183 let sol = create_test_item("SOL-001", ItemType::Solution);
184 let uc = create_test_item_with_upstream(
185 "UC-001",
186 ItemType::UseCase,
187 UpstreamRefs {
188 refines: vec![ItemId::new_unchecked("SOL-001")],
189 ..Default::default()
190 },
191 );
192
193 let graph = GraphBuilder::new()
194 .add_item(sol)
195 .add_item(uc)
196 .build()
197 .unwrap();
198
199 assert_eq!(graph.item_count(), 2);
200 assert_eq!(graph.relationship_count(), 1);
201 }
202
203 #[test]
204 fn test_strict_mode() {
205 let graph = GraphBuilder::new()
206 .with_strict_mode(true)
207 .add_item(create_test_item("SOL-001", ItemType::Solution))
208 .build()
209 .unwrap();
210
211 assert!(graph.is_strict_mode());
212 }
213}