use serde::{de, Deserialize, Deserializer, Serialize};
#[derive(Deserialize)]
#[serde(untagged)]
enum FlexiBool {
Bool(bool),
String(String),
}
fn empty_is_none<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
where
D: Deserializer<'de>,
{
let s: Option<String> = Option::deserialize(deserializer)?;
Ok(s.filter(|s| !s.is_empty()))
}
fn optional_number<'de, D>(deserializer: D) -> Result<Option<u64>, D::Error>
where
D: Deserializer<'de>,
{
let s: Option<String> = Option::deserialize(deserializer)?;
Ok(s.filter(|s| !s.is_empty())
.and_then(|s| s.parse::<i64>().ok())
.filter(|&n| n >= 0)
.map(|n| n as u64))
}
fn flexible_bool<'de, D>(deserializer: D) -> Result<Option<bool>, D::Error>
where
D: Deserializer<'de>,
{
match Option::<FlexiBool>::deserialize(deserializer)? {
Some(FlexiBool::Bool(b)) => Ok(Some(b)),
Some(FlexiBool::String(s)) => {
match s.to_lowercase().as_str() {
"true" | "yes" | "1" => Ok(Some(true)),
"false" | "no" | "0" => Ok(Some(false)),
"" => Ok(None),
_ => {
Err(de::Error::invalid_value(
de::Unexpected::Str(&s),
&"a valid boolean (true/false, yes/no, 1/0)",
))
}
}
}
None => Ok(None),
}
}
#[derive(Debug, Default, Clone, Deserialize, Serialize)]
pub struct RemotePackage {
#[serde(default, deserialize_with = "flexible_bool", alias = "_disabled")]
pub disabled: Option<bool>,
#[serde(alias = "_disabled_reason")]
pub disabled_reason: Option<serde_json::Value>,
#[serde(default, deserialize_with = "optional_number")]
pub rank: Option<u64>,
#[serde(default, deserialize_with = "empty_is_none")]
pub pkg: Option<String>,
pub pkg_id: String,
pub pkg_name: String,
#[serde(default, deserialize_with = "empty_is_none")]
pub pkg_family: Option<String>,
#[serde(default, deserialize_with = "empty_is_none")]
pub pkg_type: Option<String>,
#[serde(default, deserialize_with = "empty_is_none")]
pub pkg_webpage: Option<String>,
pub description: String,
pub version: String,
#[serde(default, deserialize_with = "empty_is_none")]
pub version_upstream: Option<String>,
pub download_url: String,
#[serde(default, deserialize_with = "optional_number")]
pub size_raw: Option<u64>,
#[serde(default, deserialize_with = "empty_is_none")]
pub ghcr_pkg: Option<String>,
#[serde(default, deserialize_with = "optional_number")]
pub ghcr_size_raw: Option<u64>,
pub ghcr_files: Option<Vec<String>>,
#[serde(default, deserialize_with = "empty_is_none")]
pub ghcr_blob: Option<String>,
#[serde(default, deserialize_with = "empty_is_none")]
pub ghcr_url: Option<String>,
#[serde(alias = "src_url")]
pub src_urls: Option<Vec<String>>,
#[serde(alias = "homepage")]
pub homepages: Option<Vec<String>>,
#[serde(alias = "license")]
pub licenses: Option<Vec<String>>,
#[serde(alias = "maintainer")]
pub maintainers: Option<Vec<String>>,
#[serde(alias = "note")]
pub notes: Option<Vec<String>>,
#[serde(alias = "tag")]
pub tags: Option<Vec<String>>,
#[serde(default, deserialize_with = "empty_is_none")]
pub bsum: Option<String>,
#[serde(default, deserialize_with = "empty_is_none")]
pub build_id: Option<String>,
#[serde(default, deserialize_with = "empty_is_none")]
pub build_date: Option<String>,
#[serde(default, deserialize_with = "empty_is_none", alias = "build_gha")]
pub build_action: Option<String>,
#[serde(default, deserialize_with = "empty_is_none")]
pub build_script: Option<String>,
#[serde(default, deserialize_with = "empty_is_none")]
pub build_log: Option<String>,
#[serde(alias = "category")]
pub categories: Option<Vec<String>>,
pub provides: Option<Vec<String>>,
#[serde(default, deserialize_with = "empty_is_none")]
pub icon: Option<String>,
#[serde(default, deserialize_with = "empty_is_none")]
pub desktop: Option<String>,
#[serde(default, deserialize_with = "empty_is_none")]
pub appstream: Option<String>,
#[serde(default, deserialize_with = "empty_is_none")]
pub app_id: Option<String>,
#[serde(default, deserialize_with = "flexible_bool")]
pub soar_syms: Option<bool>,
#[serde(default, deserialize_with = "flexible_bool")]
pub deprecated: Option<bool>,
#[serde(default, deserialize_with = "flexible_bool")]
pub desktop_integration: Option<bool>,
#[serde(default, deserialize_with = "flexible_bool")]
pub portable: Option<bool>,
#[serde(default, deserialize_with = "flexible_bool")]
pub recurse_provides: Option<bool>,
pub repology: Option<Vec<String>>,
pub snapshots: Option<Vec<String>>,
pub replaces: Option<Vec<String>>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_remote_package_deserialization() {
let json = r#"{
"pkg_id": "test-pkg",
"pkg_name": "test",
"description": "A test package",
"version": "1.0.0",
"download_url": "https://example.com/test.tar.gz"
}"#;
let pkg: RemotePackage = serde_json::from_str(json).unwrap();
assert_eq!(pkg.pkg_id, "test-pkg");
assert_eq!(pkg.pkg_name, "test");
assert_eq!(pkg.version, "1.0.0");
}
#[test]
fn test_flexible_bool() {
let json = r#"{
"pkg_id": "test",
"pkg_name": "test",
"description": "test",
"version": "1.0.0",
"download_url": "https://example.com",
"disabled": "true"
}"#;
let pkg: RemotePackage = serde_json::from_str(json).unwrap();
assert_eq!(pkg.disabled, Some(true));
}
}