Skip to main content

normalize_manifest/
elm.rs

1//! Parser for `elm.json` files (Elm package manager).
2//!
3//! Handles both application and package forms:
4//! - Application: `dependencies.direct`, `test-dependencies.direct/indirect`
5//! - Package: `dependencies`, `test-dependencies` (flat maps)
6
7use crate::{DeclaredDep, DepKind, ManifestError, ManifestParser, ParsedManifest};
8use serde_json::Value;
9
10/// Parser for `elm.json` files.
11///
12/// `dependencies.direct` (or `dependencies` in package form) → `Normal`.
13/// `test-dependencies.*` → `Dev`.
14/// `dependencies.indirect` is skipped (transitive, not declared by this project).
15pub struct ElmParser;
16
17impl ManifestParser for ElmParser {
18    fn filename(&self) -> &'static str {
19        "elm.json"
20    }
21
22    fn parse(&self, content: &str) -> Result<ParsedManifest, ManifestError> {
23        let json: Value =
24            serde_json::from_str(content).map_err(|e| ManifestError(e.to_string()))?;
25
26        let elm_type = json.get("type").and_then(|v| v.as_str());
27        let is_application = elm_type == Some("application");
28
29        // Package name from "name" field (only in package form)
30        let name = json
31            .get("name")
32            .and_then(|v| v.as_str())
33            .map(|s| s.to_string());
34        let version = json
35            .get("version")
36            .and_then(|v| v.as_str())
37            .map(|s| s.to_string());
38
39        let mut deps = Vec::new();
40
41        let dep_value = json.get("dependencies");
42        if is_application {
43            // Application form: { direct: {...}, indirect: {...} }
44            if let Some(direct) = dep_value
45                .and_then(|v| v.get("direct"))
46                .and_then(|v| v.as_object())
47            {
48                for (pkg, ver) in direct {
49                    deps.push(DeclaredDep {
50                        name: pkg.clone(),
51                        version_req: ver.as_str().map(|s| s.to_string()),
52                        kind: DepKind::Normal,
53                    });
54                }
55            }
56        } else {
57            // Package form: flat map
58            if let Some(obj) = dep_value.and_then(|v| v.as_object()) {
59                for (pkg, ver) in obj {
60                    deps.push(DeclaredDep {
61                        name: pkg.clone(),
62                        version_req: ver.as_str().map(|s| s.to_string()),
63                        kind: DepKind::Normal,
64                    });
65                }
66            }
67        }
68
69        // test-dependencies: both direct and indirect treated as Dev
70        if let Some(test_deps) = json.get("test-dependencies") {
71            // Application form has direct/indirect sub-keys; package form is flat
72            let direct_obj = test_deps.get("direct").and_then(|v| v.as_object());
73            let indirect_obj = test_deps.get("indirect").and_then(|v| v.as_object());
74
75            if direct_obj.is_some() || indirect_obj.is_some() {
76                for obj in [direct_obj, indirect_obj].into_iter().flatten() {
77                    for (pkg, ver) in obj {
78                        deps.push(DeclaredDep {
79                            name: pkg.clone(),
80                            version_req: ver.as_str().map(|s| s.to_string()),
81                            kind: DepKind::Dev,
82                        });
83                    }
84                }
85            } else if let Some(obj) = test_deps.as_object() {
86                for (pkg, ver) in obj {
87                    deps.push(DeclaredDep {
88                        name: pkg.clone(),
89                        version_req: ver.as_str().map(|s| s.to_string()),
90                        kind: DepKind::Dev,
91                    });
92                }
93            }
94        }
95
96        Ok(ParsedManifest {
97            ecosystem: "elm",
98            name,
99            version,
100            dependencies: deps,
101        })
102    }
103}
104
105#[cfg(test)]
106mod tests {
107    use super::*;
108    use crate::ManifestParser;
109
110    #[test]
111    fn test_elm_application() {
112        let content = r#"{
113    "type": "application",
114    "source-directories": ["src"],
115    "elm-version": "0.19.1",
116    "dependencies": {
117        "direct": {
118            "elm/core": "1.0.5",
119            "elm/html": "1.0.0"
120        },
121        "indirect": {
122            "elm/json": "1.1.3"
123        }
124    },
125    "test-dependencies": {
126        "direct": {
127            "elm-explorations/test": "2.1.2"
128        },
129        "indirect": {}
130    }
131}"#;
132        let m = ElmParser.parse(content).unwrap();
133        assert_eq!(m.ecosystem, "elm");
134
135        let normal: Vec<_> = m
136            .dependencies
137            .iter()
138            .filter(|d| d.kind == DepKind::Normal)
139            .collect();
140        assert_eq!(normal.len(), 2);
141        assert!(normal.iter().any(|d| d.name == "elm/core"));
142        assert!(normal.iter().any(|d| d.name == "elm/html"));
143
144        let dev: Vec<_> = m
145            .dependencies
146            .iter()
147            .filter(|d| d.kind == DepKind::Dev)
148            .collect();
149        assert_eq!(dev.len(), 1);
150        assert_eq!(dev[0].name, "elm-explorations/test");
151        assert_eq!(dev[0].version_req.as_deref(), Some("2.1.2"));
152    }
153
154    #[test]
155    fn test_elm_package() {
156        let content = r#"{
157    "type": "package",
158    "name": "elm/html",
159    "summary": "Fast HTML",
160    "version": "1.0.0",
161    "elm-version": "0.19.0 <= v < 0.20.0",
162    "exposed-modules": ["Html"],
163    "dependencies": {
164        "elm/core": "1.0.0 <= v < 2.0.0",
165        "elm/virtual-dom": "1.0.0 <= v < 2.0.0"
166    },
167    "test-dependencies": {
168        "elm-explorations/test": "1.0.0 <= v < 2.0.0"
169    }
170}"#;
171        let m = ElmParser.parse(content).unwrap();
172        assert_eq!(m.ecosystem, "elm");
173        assert_eq!(m.name.as_deref(), Some("elm/html"));
174        assert_eq!(m.version.as_deref(), Some("1.0.0"));
175
176        let normal: Vec<_> = m
177            .dependencies
178            .iter()
179            .filter(|d| d.kind == DepKind::Normal)
180            .collect();
181        assert_eq!(normal.len(), 2);
182
183        let dev: Vec<_> = m
184            .dependencies
185            .iter()
186            .filter(|d| d.kind == DepKind::Dev)
187            .collect();
188        assert_eq!(dev.len(), 1);
189        assert_eq!(dev[0].name, "elm-explorations/test");
190    }
191}