Skip to main content

package_parser/pkgs/
elm.rs

1use std::{
2    collections::{HashMap, HashSet},
3    path::Path,
4};
5
6use packageurl::PackageUrl;
7use crate::types::{DependentPackage, Package};
8use serde::Deserialize;
9
10use crate::{error::SourcePkgError, PackageManifest};
11
12#[derive(Deserialize, Debug)]
13#[serde(tag = "type", rename_all = "camelCase")]
14enum ElmManifest {
15    Application {
16        #[serde(default)]
17        dependencies: ApplicationDeps,
18    },
19    Package {
20        #[serde(default)]
21        dependencies: HashMap<String, String>,
22    },
23}
24
25#[derive(Deserialize, Debug, Default)]
26#[serde(default)]
27struct ApplicationDeps {
28    direct: HashMap<String, String>,
29    indirect: HashMap<String, String>,
30}
31
32fn make_purl(name: &str, version: Option<&str>) -> Result<String, SourcePkgError> {
33    let (ns, name) = name
34        .split_once('/')
35        .ok_or_else(|| SourcePkgError::GenericsError2(format!("Invalid package name: {name}")))?;
36
37    let mut purl = PackageUrl::new("elm", name).unwrap();
38    purl.with_namespace(ns);
39    if let Some(v) = version {
40        purl.with_version(v);
41    }
42
43    Ok(purl.to_string())
44}
45
46pub struct ElmJson {}
47
48impl ElmJson {
49    pub fn new() -> Self {
50        Self {}
51    }
52
53    fn parse(path: &Path) -> Result<Package, SourcePkgError> {
54        let content = std::fs::read_to_string(path).map_err(SourcePkgError::Io)?;
55
56        let parsed: ElmManifest =
57            serde_json::from_str(&content).map_err(SourcePkgError::JsonParse)?;
58
59        let mut ret = vec![];
60
61        match parsed {
62            ElmManifest::Application { dependencies } => {
63                for (name, version) in dependencies.direct {
64                    let purl = make_purl(&name, Some(&version))?;
65
66                    ret.push(DependentPackage {
67                        purl,
68                        requirement: version,
69                        scope: "prod".into(),
70                        is_runtime: true,
71                        is_optional: false,
72                        is_resolved: true,
73                        relation: maplit::hashset! { crate::types::Relation::Direct },
74                        parents: HashSet::new(),
75                        reachable: Default::default(),
76                    });
77                }
78
79                for (name, version) in dependencies.indirect {
80                    let purl = make_purl(&name, Some(&version))?;
81
82                    ret.push(DependentPackage {
83                        purl,
84                        requirement: version,
85                        scope: "prod".into(),
86                        is_runtime: true,
87                        is_optional: false,
88                        is_resolved: true,
89                        relation: maplit::hashset! { crate::types::Relation::Indirect },
90                        parents: HashSet::new(),
91                        reachable: Default::default(),
92                    });
93                }
94            }
95            ElmManifest::Package { dependencies } => {
96                for (name, req) in dependencies {
97                    let purl = make_purl(&name, None)?;
98
99                    ret.push(DependentPackage {
100                        purl,
101                        requirement: req,
102                        scope: "prod".into(),
103                        is_runtime: true,
104                        is_optional: false,
105                        is_resolved: false,
106                        relation: maplit::hashset! { crate::types::Relation::Direct },
107                        parents: HashSet::new(),
108                        reachable: Default::default(),
109                    });
110                }
111            }
112        }
113
114        Ok(Package {
115            dependencies: ret,
116            ..Default::default()
117        })
118    }
119}
120
121#[async_trait::async_trait]
122impl PackageManifest for ElmJson {
123    fn get_name(&self) -> String {
124        "elm".into()
125    }
126
127    async fn recognize(&self, path: &Path) -> Result<Package, SourcePkgError> {
128        Self::parse(path)
129    }
130
131    fn file_name_patterns(&self) -> &'static [&'static str] {
132        &["elm.json"]
133    }
134}
135
136#[cfg(test)]
137mod test {
138    use std::path::PathBuf;
139
140    use super::ElmJson;
141
142    #[test]
143    fn package() {
144        let filepath = PathBuf::from(concat!(
145            env!("CARGO_MANIFEST_DIR"),
146            "/testdata/elm/elm-package.json"
147        ));
148
149        dbg!(ElmJson::parse(&filepath).unwrap());
150    }
151
152    #[test]
153    fn application() {
154        let filepath = PathBuf::from(concat!(
155            env!("CARGO_MANIFEST_DIR"),
156            "/testdata/elm/elm-application.json"
157        ));
158
159        dbg!(ElmJson::parse(&filepath).unwrap());
160    }
161}