Skip to main content

normalize_manifest/
julia.rs

1//! Parser for `Project.toml` files (Julia package manager).
2
3use crate::{DeclaredDep, DepKind, ManifestError, ManifestParser, ParsedManifest};
4use toml::Value;
5
6/// Parser for Julia `Project.toml` files.
7///
8/// - `[deps]` → `Normal`. UUID values are package identifiers; version comes from
9///   `[compat]` if present, otherwise `None`.
10/// - `[weakdeps]` → `Optional`. Same version lookup via `[compat]`.
11/// - `[compat]` entries whose key is `"julia"` are skipped (not a dep).
12pub struct JuliaParser;
13
14impl ManifestParser for JuliaParser {
15    fn filename(&self) -> &'static str {
16        "Project.toml"
17    }
18
19    fn parse(&self, content: &str) -> Result<ParsedManifest, ManifestError> {
20        let toml: Value = content
21            .parse::<Value>()
22            .map_err(|e| ManifestError(e.to_string()))?;
23
24        let name = toml
25            .get("name")
26            .and_then(|v| v.as_str())
27            .map(|s| s.to_string());
28        let version = toml
29            .get("version")
30            .and_then(|v| v.as_str())
31            .map(|s| s.to_string());
32
33        // Build compat map: package name -> version constraint
34        let compat: std::collections::HashMap<String, String> = toml
35            .get("compat")
36            .and_then(|v| v.as_table())
37            .map(|t| {
38                t.iter()
39                    .filter(|(k, _)| k.as_str() != "julia")
40                    .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
41                    .collect()
42            })
43            .unwrap_or_default();
44
45        let mut deps = Vec::new();
46
47        // [deps] - Normal dependencies (values are UUIDs, not versions)
48        if let Some(table) = toml.get("deps").and_then(|v| v.as_table()) {
49            for (pkg_name, _uuid) in table {
50                let version_req = compat.get(pkg_name).cloned();
51                deps.push(DeclaredDep {
52                    name: pkg_name.clone(),
53                    version_req,
54                    kind: DepKind::Normal,
55                });
56            }
57        }
58
59        // [weakdeps] - Optional dependencies
60        if let Some(table) = toml.get("weakdeps").and_then(|v| v.as_table()) {
61            for (pkg_name, _uuid) in table {
62                let version_req = compat.get(pkg_name).cloned();
63                deps.push(DeclaredDep {
64                    name: pkg_name.clone(),
65                    version_req,
66                    kind: DepKind::Optional,
67                });
68            }
69        }
70
71        Ok(ParsedManifest {
72            ecosystem: "julia",
73            name,
74            version,
75            dependencies: deps,
76        })
77    }
78}
79
80#[cfg(test)]
81mod tests {
82    use super::*;
83    use crate::ManifestParser;
84
85    #[test]
86    fn test_julia_project_toml() {
87        let content = r#"
88name = "MyPackage"
89uuid = "a93c6f00-e57d-5684-b466-afe8fa294f38"
90version = "0.1.0"
91
92[deps]
93DataFrames = "a93c6f00-e57d-5684-b466-afe8fa294f38"
94HTTP = "cd3eb016-35fb-5094-929b-558a96fad6f3"
95
96[weakdeps]
97CUDA = "052768ef-5323-5732-b1bb-66c8b64840ba"
98
99[compat]
100julia = "1.6"
101DataFrames = "1"
102HTTP = "0.9, 1"
103"#;
104        let m = JuliaParser.parse(content).unwrap();
105        assert_eq!(m.ecosystem, "julia");
106        assert_eq!(m.name.as_deref(), Some("MyPackage"));
107        assert_eq!(m.version.as_deref(), Some("0.1.0"));
108        assert_eq!(m.dependencies.len(), 3);
109
110        let df = m
111            .dependencies
112            .iter()
113            .find(|d| d.name == "DataFrames")
114            .unwrap();
115        assert_eq!(df.kind, DepKind::Normal);
116        assert_eq!(df.version_req.as_deref(), Some("1"));
117
118        let http = m.dependencies.iter().find(|d| d.name == "HTTP").unwrap();
119        assert_eq!(http.kind, DepKind::Normal);
120        assert_eq!(http.version_req.as_deref(), Some("0.9, 1"));
121
122        let cuda = m.dependencies.iter().find(|d| d.name == "CUDA").unwrap();
123        assert_eq!(cuda.kind, DepKind::Optional);
124        assert!(cuda.version_req.is_none());
125    }
126
127    #[test]
128    fn test_julia_no_compat() {
129        let content = r#"
130name = "Simple"
131version = "0.1.0"
132
133[deps]
134JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6"
135"#;
136        let m = JuliaParser.parse(content).unwrap();
137        assert_eq!(m.dependencies.len(), 1);
138        assert_eq!(m.dependencies[0].name, "JSON");
139        assert!(m.dependencies[0].version_req.is_none());
140        assert_eq!(m.dependencies[0].kind, DepKind::Normal);
141    }
142
143    #[test]
144    fn test_julia_compat_julia_skipped() {
145        // Ensure "julia" entry in [compat] is not emitted as a dep
146        let content = r#"
147name = "OnlyJuliaCompat"
148version = "0.1.0"
149
150[deps]
151Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80"
152
153[compat]
154julia = "1.9"
155Plots = "1"
156"#;
157        let m = JuliaParser.parse(content).unwrap();
158        // Only "Plots" should be in deps, not "julia"
159        assert_eq!(m.dependencies.len(), 1);
160        assert_eq!(m.dependencies[0].name, "Plots");
161    }
162}