Skip to main content

sqry_classpath/graph/
provenance.rs

1//! Provenance tracking types for classpath entries.
2//!
3//! Tracks the origin and dependency status of each JAR on the classpath,
4//! enabling FQN precedence decisions (workspace > direct > transitive) and
5//! metadata attachment to emitted graph nodes.
6
7use std::path::PathBuf;
8
9use serde::{Deserialize, Serialize};
10
11/// Module/root-level scope membership for a JAR.
12#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
13pub struct ClasspathScope {
14    /// Logical module name (may be empty for root projects).
15    pub module_name: String,
16    /// Concrete module root path used for importer scoping.
17    pub module_root: PathBuf,
18    /// Whether the JAR is direct within this scope.
19    pub is_direct: bool,
20}
21
22/// Tracks the origin and dependency status of a classpath entry.
23///
24/// Used during graph emission to:
25/// 1. Set `is_direct_dependency` on [`ClasspathNodeMetadata`].
26/// 2. Attach Maven/Gradle coordinates to nodes for provenance queries.
27///
28/// [`ClasspathNodeMetadata`]: sqry_core::graph::unified::storage::ClasspathNodeMetadata
29#[derive(Debug, Clone, Serialize, Deserialize)]
30pub struct ClasspathProvenance {
31    /// Absolute path to the JAR file.
32    pub jar_path: PathBuf,
33    /// Maven coordinates if known (e.g., `"com.google.guava:guava:33.0.0"`).
34    pub coordinates: Option<String>,
35    /// Conservative aggregate directness across all scopes.
36    ///
37    /// `true` means the JAR is direct in every recorded scope.
38    /// Mixed direct/transitive jars persist `false`; consult `scopes` for
39    /// precise per-module semantics.
40    pub is_direct: bool,
41    /// Module/root scopes where this JAR appears.
42    #[serde(default)]
43    pub scopes: Vec<ClasspathScope>,
44}
45
46impl ClasspathProvenance {
47    /// Returns whether the JAR is direct in at least one scope.
48    #[must_use]
49    pub fn has_direct_scope(&self) -> bool {
50        self.scopes.iter().any(|scope| scope.is_direct)
51    }
52}
53
54#[cfg(test)]
55mod tests {
56    use super::*;
57
58    #[test]
59    fn test_provenance_roundtrip_json() {
60        let prov = ClasspathProvenance {
61            jar_path: PathBuf::from("/home/user/.m2/repository/guava-33.0.0.jar"),
62            coordinates: Some("com.google.guava:guava:33.0.0".to_owned()),
63            is_direct: true,
64            scopes: vec![ClasspathScope {
65                module_name: "app".to_owned(),
66                module_root: PathBuf::from("/repo/app"),
67                is_direct: true,
68            }],
69        };
70
71        let json = serde_json::to_string(&prov).unwrap();
72        let deserialized: ClasspathProvenance = serde_json::from_str(&json).unwrap();
73        assert_eq!(deserialized.jar_path, prov.jar_path);
74        assert_eq!(deserialized.coordinates, prov.coordinates);
75        assert_eq!(deserialized.is_direct, prov.is_direct);
76    }
77
78    #[test]
79    fn test_provenance_transitive_no_coordinates() {
80        let prov = ClasspathProvenance {
81            jar_path: PathBuf::from("/tmp/some-transitive-1.0.jar"),
82            coordinates: None,
83            is_direct: false,
84            scopes: vec![ClasspathScope {
85                module_name: "lib".to_owned(),
86                module_root: PathBuf::from("/repo/lib"),
87                is_direct: false,
88            }],
89        };
90
91        assert!(!prov.is_direct);
92        assert!(prov.coordinates.is_none());
93    }
94
95    #[test]
96    fn test_provenance_postcard_roundtrip() {
97        let prov = ClasspathProvenance {
98            jar_path: PathBuf::from("/repo/.gradle/caches/guava-33.jar"),
99            coordinates: Some("com.google.guava:guava:33.0.0".to_owned()),
100            is_direct: false,
101            scopes: vec![ClasspathScope {
102                module_name: "app".to_owned(),
103                module_root: PathBuf::from("/repo/app"),
104                is_direct: false,
105            }],
106        };
107
108        let bytes = postcard::to_allocvec(&prov).unwrap();
109        let deserialized: ClasspathProvenance = postcard::from_bytes(&bytes).unwrap();
110        assert_eq!(deserialized.jar_path, prov.jar_path);
111        assert_eq!(deserialized.coordinates, prov.coordinates);
112        assert_eq!(deserialized.is_direct, prov.is_direct);
113    }
114}