1use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8
9use super::EdgeType;
10
11#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct GraphNode {
14 pub chunk_id: String,
16
17 pub content_hash: String,
19
20 pub primary_alias: Option<String>,
22
23 pub aliases: Vec<String>,
25
26 pub symbols_defined: Vec<String>,
28
29 pub symbols_referenced: Vec<String>,
31
32 pub language: String,
34
35 pub granularity: String,
37
38 pub byte_size: usize,
40
41 pub token_estimate: usize,
43
44 pub source_file: Option<String>,
46
47 pub source_lines: Option<(usize, usize)>,
49
50 pub outgoing_edges: Vec<(EdgeType, String)>,
52
53 pub incoming_edges: Vec<(EdgeType, String)>,
55
56 #[serde(default)]
58 pub metadata: HashMap<String, String>,
59
60 pub created_at: String,
62
63 pub updated_at: String,
65}
66
67impl GraphNode {
68 pub fn new(chunk_id: impl Into<String>, content_hash: impl Into<String>) -> Self {
70 let now = chrono::Utc::now().to_rfc3339();
71 Self {
72 chunk_id: chunk_id.into(),
73 content_hash: content_hash.into(),
74 primary_alias: None,
75 aliases: Vec::new(),
76 symbols_defined: Vec::new(),
77 symbols_referenced: Vec::new(),
78 language: "unknown".to_string(),
79 granularity: "unknown".to_string(),
80 byte_size: 0,
81 token_estimate: 0,
82 source_file: None,
83 source_lines: None,
84 outgoing_edges: Vec::new(),
85 incoming_edges: Vec::new(),
86 metadata: HashMap::new(),
87 created_at: now.clone(),
88 updated_at: now,
89 }
90 }
91
92 pub fn with_alias(mut self, alias: impl Into<String>) -> Self {
94 let alias = alias.into();
95 self.primary_alias = Some(alias.clone());
96 if !self.aliases.contains(&alias) {
97 self.aliases.push(alias);
98 }
99 self
100 }
101
102 pub fn with_language(mut self, language: impl Into<String>) -> Self {
104 self.language = language.into();
105 self
106 }
107
108 pub fn with_granularity(mut self, granularity: impl Into<String>) -> Self {
110 self.granularity = granularity.into();
111 self
112 }
113
114 pub fn with_size(mut self, byte_size: usize) -> Self {
116 self.byte_size = byte_size;
117 self.token_estimate = byte_size / 4;
119 self
120 }
121
122 pub fn with_source(mut self, file: impl Into<String>, start: usize, end: usize) -> Self {
124 self.source_file = Some(file.into());
125 self.source_lines = Some((start, end));
126 self
127 }
128
129 pub fn with_defines(mut self, symbols: Vec<String>) -> Self {
131 self.symbols_defined = symbols;
132 self
133 }
134
135 pub fn with_references(mut self, symbols: Vec<String>) -> Self {
137 self.symbols_referenced = symbols;
138 self
139 }
140
141 pub fn add_dependency(&mut self, edge_type: EdgeType, target_id: String) {
143 self.outgoing_edges.push((edge_type, target_id));
144 self.updated_at = chrono::Utc::now().to_rfc3339();
145 }
146
147 pub fn add_dependent(&mut self, edge_type: EdgeType, source_id: String) {
149 self.incoming_edges.push((edge_type, source_id));
150 self.updated_at = chrono::Utc::now().to_rfc3339();
151 }
152
153 pub fn dependencies(&self) -> impl Iterator<Item = &String> {
155 self.outgoing_edges.iter().map(|(_, id)| id)
156 }
157
158 pub fn dependents(&self) -> impl Iterator<Item = &String> {
160 self.incoming_edges.iter().map(|(_, id)| id)
161 }
162
163 pub fn dependencies_of_type(&self, edge_type: EdgeType) -> Vec<&String> {
165 self.outgoing_edges
166 .iter()
167 .filter(|(et, _)| *et == edge_type)
168 .map(|(_, id)| id)
169 .collect()
170 }
171
172 pub fn to_bytes(&self) -> Result<Vec<u8>, serde_json::Error> {
174 serde_json::to_vec(self)
175 }
176
177 pub fn from_bytes(bytes: &[u8]) -> Result<Self, serde_json::Error> {
179 serde_json::from_slice(bytes)
180 }
181}
182
183impl Default for GraphNode {
184 fn default() -> Self {
185 Self::new("", "")
186 }
187}
188
189#[cfg(test)]
190mod tests {
191 use super::*;
192
193 #[test]
194 fn test_node_creation() {
195 let node = GraphNode::new("chunk:sha256:abc123", "abc123")
196 .with_alias("myproject/utils/tax")
197 .with_language("rust")
198 .with_granularity("function")
199 .with_size(1000)
200 .with_defines(vec!["calculate_tax".to_string()])
201 .with_references(vec!["TaxRate".to_string()]);
202
203 assert_eq!(node.chunk_id, "chunk:sha256:abc123");
204 assert_eq!(node.primary_alias, Some("myproject/utils/tax".to_string()));
205 assert_eq!(node.language, "rust");
206 assert_eq!(node.token_estimate, 250); assert!(node.symbols_defined.contains(&"calculate_tax".to_string()));
208 }
209
210 #[test]
211 fn test_edge_operations() {
212 let mut node = GraphNode::new("chunk:sha256:abc123", "abc123");
213
214 node.add_dependency(EdgeType::Imports, "chunk:sha256:def456".to_string());
215 node.add_dependency(EdgeType::TypeRef, "chunk:sha256:ghi789".to_string());
216
217 assert_eq!(node.dependencies().count(), 2);
218 assert_eq!(node.dependencies_of_type(EdgeType::Imports).len(), 1);
219 }
220
221 #[test]
222 fn test_serialization() {
223 let node = GraphNode::new("chunk:sha256:abc123", "abc123")
224 .with_alias("test/node");
225
226 let bytes = node.to_bytes().unwrap();
227 let restored = GraphNode::from_bytes(&bytes).unwrap();
228
229 assert_eq!(node.chunk_id, restored.chunk_id);
230 assert_eq!(node.primary_alias, restored.primary_alias);
231 }
232}