1use serde::{Deserialize, Deserializer, Serialize, Serializer};
12
13use crate::Provenance;
14
15#[derive(Debug, Clone, PartialEq, Eq)]
20pub enum NodeKind {
21 Fn,
23 Struct,
25 Enum,
27 Trait,
29 Module,
31 File,
33 Adr,
35 AdrSection,
37 Blueprint,
39 Doc,
41 Other(String),
43}
44
45impl NodeKind {
46 #[must_use]
48 pub fn as_str(&self) -> &str {
49 match self {
50 Self::Fn => "fn",
51 Self::Struct => "struct",
52 Self::Enum => "enum",
53 Self::Trait => "trait",
54 Self::Module => "module",
55 Self::File => "file",
56 Self::Adr => "adr",
57 Self::AdrSection => "adr_section",
58 Self::Blueprint => "blueprint",
59 Self::Doc => "doc",
60 Self::Other(s) => s,
61 }
62 }
63
64 #[must_use]
67 pub fn from_token(s: &str) -> Self {
68 match s {
69 "fn" => Self::Fn,
70 "struct" => Self::Struct,
71 "enum" => Self::Enum,
72 "trait" => Self::Trait,
73 "module" => Self::Module,
74 "file" => Self::File,
75 "adr" => Self::Adr,
76 "adr_section" => Self::AdrSection,
77 "blueprint" => Self::Blueprint,
78 "doc" => Self::Doc,
79 other => Self::Other(other.to_owned()),
80 }
81 }
82}
83
84#[derive(Debug, Clone, PartialEq, Eq)]
89pub enum EdgeKind {
90 Calls,
92 Imports,
94 Defines,
96 Contains,
98 References,
100 Supersedes,
102 AuthoredBy,
104 InferredFrom,
106 Other(String),
108}
109
110impl EdgeKind {
111 #[must_use]
113 pub fn as_str(&self) -> &str {
114 match self {
115 Self::Calls => "calls",
116 Self::Imports => "imports",
117 Self::Defines => "defines",
118 Self::Contains => "contains",
119 Self::References => "references",
120 Self::Supersedes => "supersedes",
121 Self::AuthoredBy => "authored_by",
122 Self::InferredFrom => "inferred_from",
123 Self::Other(s) => s,
124 }
125 }
126
127 #[must_use]
130 pub fn from_token(s: &str) -> Self {
131 match s {
132 "calls" => Self::Calls,
133 "imports" => Self::Imports,
134 "defines" => Self::Defines,
135 "contains" => Self::Contains,
136 "references" => Self::References,
137 "supersedes" => Self::Supersedes,
138 "authored_by" => Self::AuthoredBy,
139 "inferred_from" => Self::InferredFrom,
140 other => Self::Other(other.to_owned()),
141 }
142 }
143}
144
145impl Serialize for NodeKind {
148 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
149 serializer.serialize_str(self.as_str())
150 }
151}
152
153impl<'de> Deserialize<'de> for NodeKind {
154 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
155 let s = String::deserialize(deserializer)?;
156 Ok(Self::from_token(&s))
157 }
158}
159
160impl Serialize for EdgeKind {
161 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
162 serializer.serialize_str(self.as_str())
163 }
164}
165
166impl<'de> Deserialize<'de> for EdgeKind {
167 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
168 let s = String::deserialize(deserializer)?;
169 Ok(Self::from_token(&s))
170 }
171}
172
173#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
175pub struct Span {
176 pub start: u32,
178 pub end: u32,
180}
181
182impl Span {
183 #[must_use]
185 pub fn new(start: u32, end: u32) -> Self {
186 Self { start, end }
187 }
188}
189
190#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
195pub struct Node {
196 pub key: String,
198 pub kind: NodeKind,
200 pub name: String,
202 pub path: Option<String>,
204 pub lang: Option<String>,
206 pub blob_hash: Option<String>,
208 pub span: Option<Span>,
210 #[serde(default)]
212 pub meta: serde_json::Value,
213}
214
215impl Node {
216 #[must_use]
219 pub fn new(key: impl Into<String>, kind: NodeKind, name: impl Into<String>) -> Self {
220 Self {
221 key: key.into(),
222 kind,
223 name: name.into(),
224 path: None,
225 lang: None,
226 blob_hash: None,
227 span: None,
228 meta: serde_json::Value::Null,
229 }
230 }
231}
232
233#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
238pub struct Edge {
239 pub src: String,
241 pub dst: String,
243 pub kind: EdgeKind,
245 pub provenance: Provenance,
247 pub confidence: Option<f64>,
249 pub src_ref: Option<String>,
251}
252
253impl Edge {
254 #[must_use]
256 pub fn derived(src: impl Into<String>, dst: impl Into<String>, kind: EdgeKind) -> Self {
257 Self {
258 src: src.into(),
259 dst: dst.into(),
260 kind,
261 provenance: Provenance::Derived,
262 confidence: None,
263 src_ref: None,
264 }
265 }
266
267 #[must_use]
269 pub fn authored(src: impl Into<String>, dst: impl Into<String>, kind: EdgeKind) -> Self {
270 Self {
271 src: src.into(),
272 dst: dst.into(),
273 kind,
274 provenance: Provenance::Authored,
275 confidence: None,
276 src_ref: None,
277 }
278 }
279
280 #[must_use]
282 pub fn inferred(
283 src: impl Into<String>,
284 dst: impl Into<String>,
285 kind: EdgeKind,
286 confidence: f64,
287 ) -> Self {
288 Self {
289 src: src.into(),
290 dst: dst.into(),
291 kind,
292 provenance: Provenance::Inferred,
293 confidence: Some(confidence),
294 src_ref: None,
295 }
296 }
297
298 #[must_use]
302 pub fn is_valid(&self) -> bool {
303 let inferred = matches!(self.provenance, Provenance::Inferred);
304 match self.confidence {
305 Some(c) => inferred && (0.0..=1.0).contains(&c),
306 None => !inferred,
307 }
308 }
309}
310
311#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
314pub struct FactSet {
315 pub nodes: Vec<Node>,
317 pub edges: Vec<Edge>,
320}
321
322impl FactSet {
323 #[must_use]
325 pub fn new() -> Self {
326 Self::default()
327 }
328
329 #[must_use]
331 pub fn with_node(mut self, node: Node) -> Self {
332 self.nodes.push(node);
333 self
334 }
335
336 #[must_use]
338 pub fn with_edge(mut self, edge: Edge) -> Self {
339 self.edges.push(edge);
340 self
341 }
342
343 #[must_use]
345 pub fn is_empty(&self) -> bool {
346 self.nodes.is_empty() && self.edges.is_empty()
347 }
348}
349
350#[derive(Debug, Clone, Copy, PartialEq, Eq)]
352pub enum Direction {
353 Outgoing,
355 Incoming,
357 Both,
359}
360
361#[cfg(test)]
362mod tests {
363 use super::{Edge, EdgeKind, FactSet, Node, NodeKind};
364 use crate::Provenance;
365
366 #[test]
367 fn node_kind_tokens_round_trip() {
368 let kinds = [
369 NodeKind::Fn,
370 NodeKind::Struct,
371 NodeKind::Enum,
372 NodeKind::Trait,
373 NodeKind::Module,
374 NodeKind::File,
375 NodeKind::Adr,
376 NodeKind::AdrSection,
377 NodeKind::Blueprint,
378 NodeKind::Doc,
379 NodeKind::Other("weird".to_owned()),
380 ];
381 for k in kinds {
382 assert_eq!(NodeKind::from_token(k.as_str()), k);
383 }
384 }
385
386 #[test]
387 fn edge_kind_tokens_round_trip() {
388 let kinds = [
389 EdgeKind::Calls,
390 EdgeKind::Imports,
391 EdgeKind::Defines,
392 EdgeKind::Contains,
393 EdgeKind::References,
394 EdgeKind::Supersedes,
395 EdgeKind::AuthoredBy,
396 EdgeKind::InferredFrom,
397 EdgeKind::Other("weird".to_owned()),
398 ];
399 for k in kinds {
400 assert_eq!(EdgeKind::from_token(k.as_str()), k);
401 }
402 }
403
404 #[test]
405 fn kinds_serialize_as_bare_tokens() {
406 assert_eq!(
407 serde_json::to_string(&NodeKind::AdrSection).unwrap(),
408 "\"adr_section\""
409 );
410 assert_eq!(
411 serde_json::to_string(&EdgeKind::AuthoredBy).unwrap(),
412 "\"authored_by\""
413 );
414 let k: NodeKind = serde_json::from_str("\"struct\"").unwrap();
415 assert_eq!(k, NodeKind::Struct);
416 }
417
418 #[test]
419 fn edge_validity_tracks_provenance() {
420 assert!(Edge::derived("a", "b", EdgeKind::Calls).is_valid());
421 assert!(Edge::authored("a", "b", EdgeKind::AuthoredBy).is_valid());
422 assert!(Edge::inferred("a", "b", EdgeKind::References, 0.5).is_valid());
423 assert!(Edge::inferred("a", "b", EdgeKind::References, 0.0).is_valid());
425 assert!(Edge::inferred("a", "b", EdgeKind::References, 1.0).is_valid());
426
427 let inferred = Edge::inferred("a", "b", EdgeKind::References, 0.5);
428 let bad = Edge {
430 provenance: Provenance::Derived,
431 confidence: Some(0.9),
432 ..Edge::derived("a", "b", EdgeKind::Calls)
433 };
434 assert!(!bad.is_valid());
435 assert!(
437 !Edge {
438 confidence: None,
439 ..inferred.clone()
440 }
441 .is_valid()
442 );
443 for c in [-0.1, 1.1, f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
445 assert!(
446 !Edge {
447 confidence: Some(c),
448 ..inferred.clone()
449 }
450 .is_valid(),
451 "confidence {c} should be rejected"
452 );
453 }
454 }
455
456 #[test]
457 fn factset_builders() {
458 let fs = FactSet::new()
459 .with_node(Node::new("a", NodeKind::Fn, "a"))
460 .with_edge(Edge::derived("a", "a", EdgeKind::Calls));
461 assert_eq!(fs.nodes.len(), 1);
462 assert_eq!(fs.edges.len(), 1);
463 assert!(!fs.is_empty());
464 assert!(FactSet::new().is_empty());
465 }
466}