1use std::fmt;
9
10use serde::{
11 de::{self, Visitor},
12 Deserialize, Deserializer, Serialize,
13};
14
15#[derive(Deserialize)]
17#[serde(untagged)]
18enum FlexiBool {
19 Bool(bool),
20 String(String),
21}
22
23fn empty_is_none<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
24where
25 D: Deserializer<'de>,
26{
27 let s: Option<String> = Option::deserialize(deserializer)?;
28 Ok(s.filter(|s| !s.is_empty()))
29}
30
31fn optional_number<'de, D>(deserializer: D) -> Result<Option<u64>, D::Error>
32where
33 D: Deserializer<'de>,
34{
35 struct OptU64Visitor;
36
37 impl<'de> Visitor<'de> for OptU64Visitor {
38 type Value = Option<u64>;
39
40 fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
41 f.write_str("a positive integer, string, or null")
42 }
43
44 fn visit_none<E>(self) -> Result<Self::Value, E> {
45 Ok(None)
46 }
47
48 fn visit_unit<E>(self) -> Result<Self::Value, E> {
49 Ok(None)
50 }
51
52 fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E> {
53 Ok(Some(v))
54 }
55
56 fn visit_i64<E>(self, v: i64) -> Result<Self::Value, E>
57 where
58 E: de::Error,
59 {
60 Ok((v >= 0).then_some(v as u64))
61 }
62
63 fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
64 where
65 E: de::Error,
66 {
67 if v.is_empty() {
68 return Ok(None);
69 }
70
71 v.parse::<i64>()
72 .ok()
73 .filter(|&n| n >= 0)
74 .map(|n| n as u64)
75 .ok_or_else(|| E::custom("invalid number"))
76 .map(Some)
77 .or(Ok(None))
78 }
79 }
80
81 deserializer.deserialize_any(OptU64Visitor)
82}
83
84fn flexible_bool<'de, D>(deserializer: D) -> Result<Option<bool>, D::Error>
85where
86 D: Deserializer<'de>,
87{
88 match Option::<FlexiBool>::deserialize(deserializer)? {
89 Some(FlexiBool::Bool(b)) => Ok(Some(b)),
90 Some(FlexiBool::String(s)) => {
91 match s.to_lowercase().as_str() {
92 "true" | "yes" | "1" => Ok(Some(true)),
93 "false" | "no" | "0" => Ok(Some(false)),
94 "" => Ok(None),
95 _ => {
96 Err(de::Error::invalid_value(
97 de::Unexpected::Str(&s),
98 &"a valid boolean (true/false, yes/no, 1/0)",
99 ))
100 }
101 }
102 }
103 None => Ok(None),
104 }
105}
106
107#[derive(Debug, Default, Clone, Deserialize, Serialize)]
125pub struct RemotePackage {
126 #[serde(default, deserialize_with = "flexible_bool", alias = "_disabled")]
127 pub disabled: Option<bool>,
128
129 #[serde(alias = "_disabled_reason")]
130 pub disabled_reason: Option<serde_json::Value>,
131
132 #[serde(default, deserialize_with = "empty_is_none")]
136 pub pkg_id: Option<String>,
137 pub pkg_name: String,
138
139 #[serde(default, deserialize_with = "empty_is_none")]
140 pub pkg_family: Option<String>,
141
142 #[serde(default, deserialize_with = "empty_is_none")]
143 pub pkg_type: Option<String>,
144
145 pub description: String,
146 pub version: String,
147
148 pub download_url: String,
149
150 #[serde(default, deserialize_with = "optional_number")]
152 pub size_raw: Option<u64>,
153
154 #[serde(default, deserialize_with = "optional_number")]
158 pub size: Option<u64>,
159
160 #[serde(default, deserialize_with = "empty_is_none")]
161 pub ghcr_pkg: Option<String>,
162
163 #[serde(default, deserialize_with = "optional_number")]
164 pub ghcr_size_raw: Option<u64>,
165
166 pub ghcr_files: Option<Vec<String>>,
167
168 #[serde(default, deserialize_with = "empty_is_none")]
169 pub ghcr_blob: Option<String>,
170
171 #[serde(default, deserialize_with = "empty_is_none")]
172 pub ghcr_url: Option<String>,
173
174 #[serde(alias = "src_url")]
175 pub src_urls: Option<Vec<String>>,
176
177 #[serde(alias = "homepage")]
178 pub homepages: Option<Vec<String>>,
179
180 #[serde(alias = "license")]
181 pub licenses: Option<Vec<String>>,
182
183 #[serde(alias = "maintainer")]
184 pub maintainers: Option<Vec<String>>,
185
186 #[serde(alias = "note")]
187 pub notes: Option<Vec<String>>,
188
189 #[serde(default, deserialize_with = "empty_is_none")]
190 pub bsum: Option<String>,
191
192 #[serde(default, deserialize_with = "empty_is_none")]
193 pub build_id: Option<String>,
194
195 #[serde(default, deserialize_with = "empty_is_none", alias = "date")]
196 pub build_date: Option<String>,
197
198 #[serde(default, deserialize_with = "empty_is_none", alias = "build_gha")]
199 pub build_action: Option<String>,
200
201 #[serde(default, deserialize_with = "empty_is_none")]
202 pub build_script: Option<String>,
203
204 #[serde(default, deserialize_with = "empty_is_none")]
205 pub build_log: Option<String>,
206
207 #[serde(alias = "category")]
208 pub categories: Option<Vec<String>>,
209
210 pub provides: Option<Vec<String>>,
211
212 #[serde(default, deserialize_with = "empty_is_none")]
213 pub icon: Option<String>,
214
215 #[serde(default, deserialize_with = "empty_is_none")]
216 pub desktop: Option<String>,
217
218 #[serde(default, deserialize_with = "empty_is_none")]
219 pub appstream: Option<String>,
220
221 #[serde(default, deserialize_with = "empty_is_none")]
222 pub app_id: Option<String>,
223
224 #[serde(default, deserialize_with = "flexible_bool")]
225 pub soar_syms: Option<bool>,
226
227 #[serde(default, deserialize_with = "flexible_bool")]
228 pub deprecated: Option<bool>,
229
230 #[serde(default, deserialize_with = "flexible_bool")]
231 pub desktop_integration: Option<bool>,
232
233 #[serde(default, deserialize_with = "flexible_bool")]
234 pub portable: Option<bool>,
235
236 pub repology: Option<Vec<String>>,
237 pub snapshots: Option<Vec<String>>,
238 pub replaces: Option<Vec<String>>,
239 #[serde(default, skip_serializing_if = "Option::is_none")]
242 pub extra: Option<Vec<RemoteExtra>>,
243 #[serde(default, skip_serializing_if = "Option::is_none")]
246 pub files: Option<Vec<RemoteFile>>,
247}
248
249#[cfg(test)]
250mod tests {
251 use super::*;
252
253 #[test]
254 fn test_remote_package_deserialization() {
255 let json = r#"{
256 "pkg_id": "test-pkg",
257 "pkg_name": "test",
258 "description": "A test package",
259 "version": "1.0.0",
260 "download_url": "https://example.com/test.tar.gz"
261 }"#;
262
263 let pkg: RemotePackage = serde_json::from_str(json).unwrap();
264 assert_eq!(pkg.pkg_id.as_deref(), Some("test-pkg"));
265 assert_eq!(pkg.pkg_name, "test");
266 assert_eq!(pkg.version, "1.0.0");
267 }
268
269 #[test]
270 fn test_pkg_id_is_optional() {
271 let json = r#"{
272 "pkg_name": "test",
273 "description": "A test package",
274 "version": "1.0.0",
275 "download_url": "https://example.com/test.tar.gz"
276 }"#;
277
278 let pkg: RemotePackage = serde_json::from_str(json).unwrap();
279 assert_eq!(pkg.pkg_id, None);
280 assert_eq!(pkg.pkg_name, "test");
281 }
282
283 #[test]
284 fn test_flexible_bool() {
285 let json = r#"{
286 "pkg_id": "test",
287 "pkg_name": "test",
288 "description": "test",
289 "version": "1.0.0",
290 "download_url": "https://example.com",
291 "disabled": "true"
292 }"#;
293
294 let pkg: RemotePackage = serde_json::from_str(json).unwrap();
295 assert_eq!(pkg.disabled, Some(true));
296 }
297}
298
299#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
305pub struct RemoteFile {
306 #[serde(default)]
307 pub source: String,
308 pub to: String,
309 #[serde(default, skip_serializing_if = "Vec::is_empty")]
311 pub alias: Vec<String>,
312}
313
314#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
316pub struct RemoteExtra {
317 pub url: String,
318 pub to: String,
319 #[serde(default, skip_serializing_if = "Option::is_none")]
320 pub blake3: Option<String>,
321 #[serde(default, skip_serializing_if = "Option::is_none")]
322 pub sha256: Option<String>,
323}