package_parser/pkgs/
elm.rs1use 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}