1use std::{fmt::Display, str::FromStr};
2
3use serde::{Deserialize, Deserializer, de};
4use strum::{Display, EnumString};
5
6#[derive(Debug, Clone)]
8pub struct EcosystemWithSuffix(Ecosystem, Option<String>);
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Display, EnumString)]
13pub enum Ecosystem {
14 #[strum(to_string = "AlmaLinux")]
15 AlmaLinux,
16 #[strum(to_string = "Alpaquita")]
17 Alpaquita,
18 #[strum(to_string = "Alpine")]
19 Alpine,
20 #[strum(to_string = "Android")]
21 Android,
22 #[strum(to_string = "Azure Linux")]
23 AzureLinux,
24 #[strum(to_string = "BellSoft Hardened Containers")]
25 BellSoftHardenedContainers,
26 #[strum(to_string = "Bioconductor")]
27 Bioconductor,
28 #[strum(to_string = "Bitnami")]
29 Bitnami,
30 #[strum(to_string = "Chainguard")]
31 Chainguard,
32 #[strum(to_string = "CleanStart")]
33 CleanStart,
34 #[strum(to_string = "ConanCenter")]
35 ConanCenter,
36 #[strum(to_string = "CRAN")]
37 Cran,
38 #[strum(to_string = "crates.io")]
39 CratesIo,
40 #[strum(to_string = "Debian")]
41 Debian,
42 #[strum(to_string = "Docker Hardened Images")]
43 DockerHardenedImages,
44 #[strum(to_string = "Echo")]
45 Echo,
46 #[strum(to_string = "FreeBSD")]
47 FreeBSD,
48 #[strum(to_string = "GHC")]
49 Ghc,
50 #[strum(to_string = "GitHub Actions")]
51 GitHubActions,
52 #[strum(to_string = "Go")]
53 Go,
54 #[strum(to_string = "Hackage")]
55 Hackage,
56 #[strum(to_string = "Hex")]
57 Hex,
58 #[strum(to_string = "Julia")]
59 Julia,
60 #[strum(to_string = "Kubernetes")]
61 Kubernetes,
62 #[strum(to_string = "Linux")]
63 Linux,
64 #[strum(to_string = "Mageia")]
65 Mageia,
66 #[strum(to_string = "Maven")]
67 Maven,
68 #[strum(to_string = "MinimOS")]
69 MinimOS,
70 #[strum(to_string = "npm")]
71 Npm,
72 #[strum(to_string = "NuGet")]
73 NuGet,
74 #[strum(to_string = "opam")]
75 Opam,
76 #[strum(to_string = "openEuler")]
77 OpenEuler,
78 #[strum(to_string = "openSUSE")]
79 OpenSUSE,
80 #[strum(to_string = "OSS-Fuzz")]
81 OssFuzz,
82 #[strum(to_string = "Packagist")]
83 Packagist,
84 #[strum(to_string = "Photon OS")]
85 PhotonOS,
86 #[strum(to_string = "Pub")]
87 Pub,
88 #[strum(to_string = "PyPI")]
89 PyPI,
90 #[strum(to_string = "Red Hat")]
91 RedHat,
92 #[strum(to_string = "Rocky Linux")]
93 RockyLinux,
94 #[strum(to_string = "Root")]
95 Root,
96 #[strum(to_string = "RubyGems")]
97 RubyGems,
98 #[strum(to_string = "SUSE")]
99 Suse,
100 #[strum(to_string = "SwiftURL")]
101 SwiftURL,
102 #[strum(to_string = "Ubuntu")]
103 Ubuntu,
104 #[strum(to_string = "VSCode")]
105 VSCode,
106 #[strum(to_string = "Wolfi")]
107 Wolfi,
108 #[strum(to_string = "GIT")]
109 Git,
110}
111
112impl EcosystemWithSuffix {
113 #[must_use]
115 pub fn ecosystem(&self) -> Ecosystem {
116 self.0
117 }
118
119 #[must_use]
122 pub fn suffix(&self) -> Option<&str> {
123 self.1.as_deref()
124 }
125}
126
127impl Display for EcosystemWithSuffix {
128 fn fmt(
129 &self,
130 f: &mut std::fmt::Formatter<'_>,
131 ) -> std::fmt::Result {
132 write!(f, "{}", self.0)?;
133 if let Some(suffix) = &self.1 {
134 write!(f, ":{suffix}")?;
135 }
136 Ok(())
137 }
138}
139
140impl FromStr for EcosystemWithSuffix {
141 type Err = <Ecosystem as FromStr>::Err;
142
143 fn from_str(s: &str) -> Result<Self, Self::Err> {
144 if let Some((s, suffix)) = s.split_once(':') {
145 Ok(Self(s.parse()?, Some(suffix.to_string())))
146 } else {
147 Ok(Self(s.parse()?, None))
148 }
149 }
150}
151
152impl<'de> Deserialize<'de> for EcosystemWithSuffix {
153 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
154 let s = String::deserialize(deserializer)?;
155 s.parse().map_err(|_| de::Error::unknown_variant(&s, &[]))
156 }
157}
158
159impl<'de> Deserialize<'de> for Ecosystem {
160 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
161 let s = String::deserialize(deserializer)?;
162 s.parse().map_err(|_| de::Error::unknown_variant(&s, &[]))
163 }
164}
165
166#[cfg(test)]
167mod tests {
168 use serde_json::json;
169 use test_case::test_case;
170
171 use super::*;
172
173 #[test_case("AlmaLinux", Ecosystem::AlmaLinux)]
175 #[test_case("Alpaquita", Ecosystem::Alpaquita)]
176 #[test_case("Alpine", Ecosystem::Alpine)]
177 #[test_case("Android", Ecosystem::Android)]
178 #[test_case("Azure Linux", Ecosystem::AzureLinux)]
179 #[test_case("BellSoft Hardened Containers", Ecosystem::BellSoftHardenedContainers)]
180 #[test_case("Bioconductor", Ecosystem::Bioconductor)]
181 #[test_case("Bitnami", Ecosystem::Bitnami)]
182 #[test_case("Chainguard", Ecosystem::Chainguard)]
183 #[test_case("CleanStart", Ecosystem::CleanStart)]
184 #[test_case("ConanCenter", Ecosystem::ConanCenter)]
185 #[test_case("CRAN", Ecosystem::Cran)]
186 #[test_case("crates.io", Ecosystem::CratesIo)]
187 #[test_case("Debian", Ecosystem::Debian)]
188 #[test_case("Docker Hardened Images", Ecosystem::DockerHardenedImages)]
189 #[test_case("Echo", Ecosystem::Echo)]
190 #[test_case("FreeBSD", Ecosystem::FreeBSD)]
191 #[test_case("GHC", Ecosystem::Ghc)]
192 #[test_case("GitHub Actions", Ecosystem::GitHubActions)]
193 #[test_case("Go", Ecosystem::Go)]
194 #[test_case("Hackage", Ecosystem::Hackage)]
195 #[test_case("Hex", Ecosystem::Hex)]
196 #[test_case("Julia", Ecosystem::Julia)]
197 #[test_case("Kubernetes", Ecosystem::Kubernetes)]
198 #[test_case("Linux", Ecosystem::Linux)]
199 #[test_case("Mageia", Ecosystem::Mageia)]
200 #[test_case("Maven", Ecosystem::Maven)]
201 #[test_case("MinimOS", Ecosystem::MinimOS)]
202 #[test_case("npm", Ecosystem::Npm)]
203 #[test_case("NuGet", Ecosystem::NuGet)]
204 #[test_case("opam", Ecosystem::Opam)]
205 #[test_case("openEuler", Ecosystem::OpenEuler)]
206 #[test_case("openSUSE", Ecosystem::OpenSUSE)]
207 #[test_case("OSS-Fuzz", Ecosystem::OssFuzz)]
208 #[test_case("Packagist", Ecosystem::Packagist)]
209 #[test_case("Photon OS", Ecosystem::PhotonOS)]
210 #[test_case("Pub", Ecosystem::Pub)]
211 #[test_case("PyPI", Ecosystem::PyPI)]
212 #[test_case("Red Hat", Ecosystem::RedHat)]
213 #[test_case("Rocky Linux", Ecosystem::RockyLinux)]
214 #[test_case("Root", Ecosystem::Root)]
215 #[test_case("RubyGems", Ecosystem::RubyGems)]
216 #[test_case("SUSE", Ecosystem::Suse)]
217 #[test_case("SwiftURL", Ecosystem::SwiftURL)]
218 #[test_case("Ubuntu", Ecosystem::Ubuntu)]
219 #[test_case("VSCode", Ecosystem::VSCode)]
220 #[test_case("Wolfi", Ecosystem::Wolfi)]
221 #[test_case("GIT", Ecosystem::Git)]
222 #[allow(clippy::needless_pass_by_value)]
223 fn display_and_parse_roundtrip(
224 osv_string: &str,
225 expected: Ecosystem,
226 ) {
227 let eco_from_json: Ecosystem = serde_json::from_value(json!(osv_string)).unwrap();
228 let eco_from_str: Ecosystem = osv_string.parse().unwrap();
229
230 assert_eq!(expected, eco_from_str);
231 assert_eq!(expected, eco_from_json);
232 assert_eq!(expected.to_string(), osv_string);
233
234 let ews_from_str: EcosystemWithSuffix = osv_string.parse().unwrap();
235 let ews_from_json: EcosystemWithSuffix = serde_json::from_value(json!(osv_string)).unwrap();
236 assert_eq!(ews_from_str.to_string(), osv_string);
237 assert_eq!(ews_from_json.to_string(), osv_string);
238 }
239}