1use std::path::PathBuf;
10
11use crate::language::LangId;
12use crate::model::{DataNodeId, FileId, SnapshotId, SourceRange, SymbolId, SymbolKind, Visibility};
13
14#[derive(Debug, Clone)]
16#[non_exhaustive]
17pub enum NodeData {
18 File(FileNode),
19 Symbol(SymbolNode),
20 External(ExternalNode),
21 Data(DataGraphNode),
22}
23
24impl NodeData {
25 pub fn kind_str(&self) -> &'static str {
27 match self {
28 NodeData::File(_) => "file",
29 NodeData::Symbol(_) => "symbol",
30 NodeData::External(_) => "external",
31 NodeData::Data(_) => "data",
32 }
33 }
34
35 pub fn as_file(&self) -> Option<&FileNode> {
37 if let NodeData::File(f) = self {
38 Some(f)
39 } else {
40 None
41 }
42 }
43
44 pub fn as_symbol(&self) -> Option<&SymbolNode> {
46 if let NodeData::Symbol(s) = self {
47 Some(s)
48 } else {
49 None
50 }
51 }
52
53 pub fn as_external(&self) -> Option<&ExternalNode> {
55 if let NodeData::External(e) = self {
56 Some(e)
57 } else {
58 None
59 }
60 }
61
62 pub fn as_data(&self) -> Option<&DataGraphNode> {
64 if let NodeData::Data(d) = self {
65 Some(d)
66 } else {
67 None
68 }
69 }
70
71 pub fn file_path(&self) -> Option<&PathBuf> {
73 self.as_file().map(|f| &f.path)
74 }
75
76 pub fn symbol_name(&self) -> Option<&str> {
78 self.as_symbol().map(|s| s.name.as_str())
79 }
80}
81
82#[derive(Debug, Clone)]
84pub struct FileNode {
85 pub id: FileId,
87 pub path: PathBuf,
89 pub language: LangId,
91 pub snapshot_id: SnapshotId,
93}
94
95#[derive(Debug, Clone)]
97pub struct ExternalNode {
98 pub raw_path: String,
100 pub language: LangId,
102 pub classification: Option<ExternalClassification>,
104}
105
106#[derive(Debug, Clone, serde::Serialize)]
108#[non_exhaustive]
109pub enum ExternalClassification {
110 Classified {
112 package_name: String,
113 version: Option<String>,
114 language: LangId,
115 source: DependencySource,
116 },
117 Unresolved { raw_path: String, reason: String },
119}
120
121#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
123#[non_exhaustive]
124pub enum DependencySource {
125 Lockfile,
126 Manifest,
127}
128
129#[derive(Debug, Clone)]
134pub struct SymbolNode {
135 pub id: SymbolId,
137 pub name: String,
139 pub kind: SymbolKind,
141 pub file_id: FileId,
143 pub visibility: Option<Visibility>,
145 pub source_range: SourceRange,
147}
148
149#[derive(Debug, Clone)]
151pub struct DataGraphNode {
152 pub id: DataNodeId,
153 pub symbol_id: Option<SymbolId>,
154 pub name: Option<String>,
155 pub scope: crate::model::DataScope,
156 pub type_hint: Option<String>,
157 pub source_range: SourceRange,
158}
159
160impl FileNode {
161 pub fn new(id: FileId, path: PathBuf, language: LangId, snapshot_id: SnapshotId) -> Self {
163 Self {
164 id,
165 path,
166 language,
167 snapshot_id,
168 }
169 }
170
171 pub fn file_name(&self) -> Option<&str> {
173 self.path.file_name().and_then(|n| n.to_str())
174 }
175
176 pub fn extension(&self) -> Option<&str> {
178 self.path.extension().and_then(|e| e.to_str())
179 }
180}
181
182impl SymbolNode {
183 pub fn from_symbol(symbol: &crate::model::Symbol, file_id: FileId) -> Self {
187 Self {
188 id: symbol.id,
189 name: symbol.name.clone(),
190 kind: symbol.kind,
191 file_id,
192 visibility: symbol.visibility,
193 source_range: symbol.source_range.clone(),
194 }
195 }
196}
197
198impl DataGraphNode {
199 pub fn from_data_node(data: &crate::model::DataNode) -> Self {
201 Self {
202 id: data.id,
203 symbol_id: data.symbol_id,
204 name: data.name.clone(),
205 scope: data.scope,
206 type_hint: data.type_hint.clone(),
207 source_range: data.source_range.clone(),
208 }
209 }
210}
211
212#[cfg(test)]
213mod tests {
214 use super::*;
215 use crate::model::{LineColumn, SourceRange};
216
217 fn test_path() -> PathBuf {
218 PathBuf::from("src/main.rs")
219 }
220
221 fn test_source_range() -> SourceRange {
222 SourceRange {
223 byte_start: 0,
224 byte_end: 10,
225 start: LineColumn { line: 0, column: 0 },
226 end: LineColumn {
227 line: 0,
228 column: 10,
229 },
230 }
231 }
232
233 #[test]
234 fn file_node_creation() {
235 let file_id = FileId::new(1).unwrap();
236 let snapshot_id = SnapshotId::new(1).unwrap();
237 let node = FileNode::new(file_id, test_path(), LangId::Rust, snapshot_id);
238
239 assert_eq!(node.id, file_id);
240 assert_eq!(node.path, test_path());
241 assert_eq!(node.language, LangId::Rust);
242 assert_eq!(node.snapshot_id, snapshot_id);
243 }
244
245 #[test]
246 fn file_node_file_name() {
247 let node = FileNode::new(
248 FileId::new(1).unwrap(),
249 PathBuf::from("src/main.rs"),
250 LangId::Rust,
251 SnapshotId::new(1).unwrap(),
252 );
253 assert_eq!(node.file_name(), Some("main.rs"));
254 }
255
256 #[test]
257 fn file_node_extension() {
258 let node = FileNode::new(
259 FileId::new(1).unwrap(),
260 PathBuf::from("test.py"),
261 LangId::Python,
262 SnapshotId::new(1).unwrap(),
263 );
264 assert_eq!(node.extension(), Some("py"));
265 }
266
267 #[test]
268 fn symbol_node_creation() {
269 let symbol = crate::model::Symbol {
270 id: SymbolId::new(42).unwrap(),
271 name: "test_function".to_string(),
272 kind: SymbolKind::Function,
273 language: LangId::Rust,
274 file_path: test_path(),
275 source_range: test_source_range(),
276 visibility: Some(Visibility::Public),
277 signature: None,
278 docstring: None,
279 is_async: false,
280 };
281
282 let file_id = FileId::new(7).unwrap();
283 let node = SymbolNode::from_symbol(&symbol, file_id);
284
285 assert_eq!(node.id, SymbolId::new(42).unwrap());
286 assert_eq!(node.name, "test_function");
287 assert_eq!(node.kind, SymbolKind::Function);
288 assert_eq!(node.file_id, file_id);
289 assert_eq!(node.visibility, Some(Visibility::Public));
290 }
291
292 #[test]
293 fn node_data_file_variant() {
294 let file_node = FileNode::new(
295 FileId::new(1).unwrap(),
296 test_path(),
297 LangId::Rust,
298 SnapshotId::new(1).unwrap(),
299 );
300 let node_data = NodeData::File(file_node);
301
302 assert_eq!(node_data.kind_str(), "file");
303 assert!(node_data.as_file().is_some());
304 assert!(node_data.as_symbol().is_none());
305 assert_eq!(node_data.file_path(), Some(&test_path()));
306 assert_eq!(node_data.symbol_name(), None);
307 }
308
309 #[test]
310 fn node_data_symbol_variant() {
311 let symbol_node = SymbolNode {
312 id: SymbolId::new(1).unwrap(),
313 name: "my_func".to_string(),
314 kind: SymbolKind::Function,
315 file_id: FileId::new(1).unwrap(),
316 visibility: None,
317 source_range: test_source_range(),
318 };
319 let node_data = NodeData::Symbol(symbol_node);
320
321 assert_eq!(node_data.kind_str(), "symbol");
322 assert!(node_data.as_symbol().is_some());
323 assert!(node_data.as_file().is_none());
324 assert_eq!(node_data.file_path(), None);
325 assert_eq!(node_data.symbol_name(), Some("my_func"));
326 }
327
328 #[test]
329 fn data_graph_node_from_model() {
330 use crate::model::{DataNode, DataNodeId, DataScope};
331 let data = DataNode {
332 id: DataNodeId::new(5).unwrap(),
333 symbol_id: None,
334 name: Some("var_x".into()),
335 scope: DataScope::Local,
336 type_hint: Some("int".into()),
337 source_range: test_source_range(),
338 };
339 let gnode = DataGraphNode::from_data_node(&data);
340 assert_eq!(gnode.id, DataNodeId::new(5).unwrap());
341 assert_eq!(gnode.name.as_deref(), Some("var_x"));
342 assert_eq!(gnode.scope, DataScope::Local);
343 assert_eq!(gnode.type_hint.as_deref(), Some("int"));
344 assert!(gnode.symbol_id.is_none());
345 }
346
347 #[test]
348 fn node_data_data_variant() {
349 let dnode = DataGraphNode {
350 id: crate::model::DataNodeId::new(1).unwrap(),
351 symbol_id: Some(SymbolId::new(3).unwrap()),
352 name: Some("param".into()),
353 scope: crate::model::DataScope::Parameter,
354 type_hint: Some("str".into()),
355 source_range: test_source_range(),
356 };
357 let node_data = NodeData::Data(dnode);
358 assert_eq!(node_data.kind_str(), "data");
359 assert!(node_data.as_data().is_some());
360 assert!(node_data.as_file().is_none());
361 assert!(node_data.as_symbol().is_none());
362 assert_eq!(node_data.as_data().unwrap().name.as_deref(), Some("param"));
363 }
364}