1use serde::Serialize;
7
8use super::{Adr, Status};
9
10#[derive(Debug, Clone, Serialize)]
12pub struct Node {
13 pub id: String,
15 pub status: String,
17 #[serde(skip_serializing_if = "Option::is_none")]
19 pub title: Option<String>,
20}
21
22impl Node {
23 #[must_use]
25 pub fn from_adr(adr: &Adr) -> Self {
26 Self {
27 id: adr.id().as_str().to_string(),
28 status: adr.status().as_str().to_string(),
29 title: Some(adr.title().to_string()),
30 }
31 }
32
33 #[must_use]
35 pub fn placeholder(id: impl Into<String>) -> Self {
36 Self {
37 id: id.into(),
38 status: Status::default().as_str().to_string(),
39 title: None,
40 }
41 }
42}
43
44#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
46#[serde(rename_all = "lowercase")]
47pub enum EdgeType {
48 Related,
50 Supersedes,
52}
53
54impl EdgeType {
55 #[must_use]
57 pub const fn as_str(&self) -> &'static str {
58 match self {
59 Self::Related => "related",
60 Self::Supersedes => "supersedes",
61 }
62 }
63}
64
65#[derive(Debug, Clone, Serialize)]
67pub struct Edge {
68 pub source: String,
70 pub target: String,
72 #[serde(rename = "type")]
74 pub edge_type: EdgeType,
75}
76
77impl Edge {
78 #[must_use]
80 pub fn new(source: impl Into<String>, target: impl Into<String>, edge_type: EdgeType) -> Self {
81 Self {
82 source: source.into(),
83 target: target.into(),
84 edge_type,
85 }
86 }
87
88 #[must_use]
90 pub fn related(source: impl Into<String>, target: impl Into<String>) -> Self {
91 Self::new(source, target, EdgeType::Related)
92 }
93
94 #[must_use]
96 pub fn supersedes(source: impl Into<String>, target: impl Into<String>) -> Self {
97 Self::new(source, target, EdgeType::Supersedes)
98 }
99}
100
101#[derive(Debug, Clone, Serialize)]
103pub struct Graph {
104 pub nodes: Vec<Node>,
106 pub edges: Vec<Edge>,
108}
109
110impl Graph {
111 #[must_use]
113 pub fn new() -> Self {
114 Self {
115 nodes: Vec::new(),
116 edges: Vec::new(),
117 }
118 }
119
120 #[must_use]
122 pub fn from_adrs(adrs: &[Adr]) -> Self {
123 let mut nodes: Vec<Node> = adrs.iter().map(Node::from_adr).collect();
124 let mut edges: Vec<Edge> = Vec::new();
125
126 let known_ids: std::collections::HashSet<&str> =
128 adrs.iter().map(|a| a.id().as_str()).collect();
129
130 for adr in adrs {
132 let source_id = adr.id().as_str();
133
134 for related_ref in adr.related() {
136 let target_id = extract_id_from_ref(related_ref);
138
139 edges.push(Edge::related(source_id, &target_id));
141
142 if !known_ids.contains(target_id.as_str()) {
144 nodes.push(Node::placeholder(&target_id));
145 }
146 }
147 }
148
149 nodes.dedup_by(|a, b| a.id == b.id);
151
152 Self { nodes, edges }
153 }
154
155 #[must_use]
157 pub fn node_count(&self) -> usize {
158 self.nodes.len()
159 }
160
161 #[must_use]
163 pub fn edge_count(&self) -> usize {
164 self.edges.len()
165 }
166
167 #[must_use]
169 pub fn is_empty(&self) -> bool {
170 self.nodes.is_empty()
171 }
172}
173
174impl Default for Graph {
175 fn default() -> Self {
176 Self::new()
177 }
178}
179
180fn extract_id_from_ref(reference: &str) -> String {
184 reference
185 .strip_suffix(".md")
186 .unwrap_or(reference)
187 .to_string()
188}
189
190#[cfg(test)]
191mod tests {
192 use super::*;
193 use crate::domain::{AdrId, Frontmatter};
194 use std::path::PathBuf;
195
196 fn create_test_adr(id: &str, related: Vec<String>) -> Adr {
197 let frontmatter = Frontmatter::new(format!("Test {id}")).with_related(related);
198 Adr::new(
199 AdrId::new(id),
200 format!("{id}.md"),
201 PathBuf::from(format!("{id}.md")),
202 frontmatter,
203 String::new(),
204 String::new(),
205 String::new(),
206 )
207 }
208
209 #[test]
210 fn test_node_from_adr() {
211 let adr = create_test_adr("adr_0001", vec![]);
212 let node = Node::from_adr(&adr);
213
214 assert_eq!(node.id, "adr_0001");
215 assert_eq!(node.status, "proposed");
216 assert_eq!(node.title, Some("Test adr_0001".to_string()));
217 }
218
219 #[test]
220 fn test_node_placeholder() {
221 let node = Node::placeholder("adr_0005");
222 assert_eq!(node.id, "adr_0005");
223 assert!(node.title.is_none());
224 }
225
226 #[test]
227 fn test_edge_creation() {
228 let edge = Edge::related("adr_0001", "adr_0005");
229 assert_eq!(edge.source, "adr_0001");
230 assert_eq!(edge.target, "adr_0005");
231 assert_eq!(edge.edge_type, EdgeType::Related);
232 }
233
234 #[test]
235 fn test_graph_from_adrs() {
236 let adrs = vec![
237 create_test_adr("adr_0001", vec!["adr_0002.md".to_string()]),
238 create_test_adr("adr_0002", vec![]),
239 ];
240
241 let graph = Graph::from_adrs(&adrs);
242
243 assert_eq!(graph.node_count(), 2);
244 assert_eq!(graph.edge_count(), 1);
245 assert_eq!(graph.edges[0].source, "adr_0001");
246 assert_eq!(graph.edges[0].target, "adr_0002");
247 }
248
249 #[test]
250 fn test_graph_with_missing_reference() {
251 let adrs = vec![create_test_adr(
252 "adr_0001",
253 vec!["adr_missing.md".to_string()],
254 )];
255
256 let graph = Graph::from_adrs(&adrs);
257
258 assert_eq!(graph.node_count(), 2);
260 assert!(graph.nodes.iter().any(|n| n.id == "adr_missing"));
261 }
262
263 #[test]
264 fn test_extract_id_from_ref() {
265 assert_eq!(extract_id_from_ref("adr_0005.md"), "adr_0005");
266 assert_eq!(extract_id_from_ref("adr_0005"), "adr_0005");
267 }
268
269 #[test]
270 fn test_edge_type_as_str() {
271 assert_eq!(EdgeType::Related.as_str(), "related");
272 assert_eq!(EdgeType::Supersedes.as_str(), "supersedes");
273 }
274
275 #[test]
276 fn test_edge_supersedes() {
277 let edge = Edge::supersedes("adr_0002", "adr_0001");
278 assert_eq!(edge.source, "adr_0002");
279 assert_eq!(edge.target, "adr_0001");
280 assert_eq!(edge.edge_type, EdgeType::Supersedes);
281 }
282
283 #[test]
284 fn test_graph_new() {
285 let graph = Graph::new();
286 assert!(graph.is_empty());
287 assert_eq!(graph.node_count(), 0);
288 assert_eq!(graph.edge_count(), 0);
289 }
290
291 #[test]
292 fn test_graph_default() {
293 let graph = Graph::default();
294 assert!(graph.is_empty());
295 assert_eq!(graph.nodes.len(), 0);
296 assert_eq!(graph.edges.len(), 0);
297 }
298
299 #[test]
300 fn test_graph_is_empty() {
301 let empty_graph = Graph::new();
302 assert!(empty_graph.is_empty());
303
304 let non_empty_graph = Graph::from_adrs(&[create_test_adr("adr_0001", vec![])]);
305 assert!(!non_empty_graph.is_empty());
306 }
307
308 #[test]
309 fn test_edge_new() {
310 let edge = Edge::new("source", "target", EdgeType::Related);
311 assert_eq!(edge.source, "source");
312 assert_eq!(edge.target, "target");
313 assert_eq!(edge.edge_type, EdgeType::Related);
314 }
315
316 #[test]
317 fn test_node_serialization() {
318 let node = Node::placeholder("adr_0001");
319 let json = serde_json::to_string(&node).expect("should serialize");
320 assert!(json.contains("\"id\":\"adr_0001\""));
321 assert!(!json.contains("\"title\":null"));
323 }
324
325 #[test]
326 fn test_edge_serialization() {
327 let edge = Edge::supersedes("adr_0002", "adr_0001");
328 let json = serde_json::to_string(&edge).expect("should serialize");
329 assert!(json.contains("\"source\":\"adr_0002\""));
330 assert!(json.contains("\"target\":\"adr_0001\""));
331 assert!(json.contains("\"type\":\"supersedes\""));
332 }
333
334 #[test]
335 fn test_graph_serialization() {
336 let adrs = vec![
337 create_test_adr("adr_0001", vec!["adr_0002.md".to_string()]),
338 create_test_adr("adr_0002", vec![]),
339 ];
340 let graph = Graph::from_adrs(&adrs);
341 let json = serde_json::to_string(&graph).expect("should serialize");
342 assert!(json.contains("\"nodes\""));
343 assert!(json.contains("\"edges\""));
344 }
345}