use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum PackageStatus {
Newest,
Devel,
Unique,
Outdated,
Legacy,
Rolling,
Noscheme,
Incorrect,
Untrusted,
Ignored,
Unknown(String),
}
impl<'de> Deserialize<'de> for PackageStatus {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
Ok(match s.as_str() {
"newest" => Self::Newest,
"devel" => Self::Devel,
"unique" => Self::Unique,
"outdated" => Self::Outdated,
"legacy" => Self::Legacy,
"rolling" => Self::Rolling,
"noscheme" => Self::Noscheme,
"incorrect" => Self::Incorrect,
"untrusted" => Self::Untrusted,
"ignored" => Self::Ignored,
_ => Self::Unknown(s),
})
}
}
impl Serialize for PackageStatus {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let s = match self {
Self::Newest => "newest",
Self::Devel => "devel",
Self::Unique => "unique",
Self::Outdated => "outdated",
Self::Legacy => "legacy",
Self::Rolling => "rolling",
Self::Noscheme => "noscheme",
Self::Incorrect => "incorrect",
Self::Untrusted => "untrusted",
Self::Ignored => "ignored",
Self::Unknown(s) => s.as_str(),
};
serializer.serialize_str(s)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
#[non_exhaustive]
pub struct Package {
pub repo: String,
pub version: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub subrepo: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub srcname: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub binname: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub binnames: Option<Vec<String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub visiblename: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub origversion: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub status: Option<PackageStatus>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub summary: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub categories: Option<Vec<String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub licenses: Option<Vec<String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub maintainers: Option<Vec<String>>,
#[serde(default)]
pub vulnerable: bool,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn deserialize_minimal_package() {
let json = r#"{"repo": "freebsd", "version": "1.0"}"#;
let pkg: Package = serde_json::from_str(json).unwrap();
assert_eq!(pkg.repo, "freebsd");
assert_eq!(pkg.version, "1.0");
assert!(pkg.status.is_none());
assert!(pkg.srcname.is_none());
assert!(!pkg.vulnerable);
}
#[test]
fn deserialize_full_package() {
let json = r#"{
"repo": "freebsd",
"version": "50.1.0",
"subrepo": "main",
"srcname": "www/firefox",
"binname": "firefox",
"binnames": ["firefox", "firefox-esr"],
"visiblename": "www/firefox",
"origversion": "50.1.0_4,1",
"status": "newest",
"summary": "Widely used web browser",
"categories": ["www"],
"licenses": ["GPLv2+"],
"maintainers": ["gecko@FreeBSD.org"],
"vulnerable": true
}"#;
let pkg: Package = serde_json::from_str(json).unwrap();
assert_eq!(pkg.status, Some(PackageStatus::Newest));
assert_eq!(pkg.categories, Some(vec!["www".to_string()]));
assert_eq!(
pkg.binnames,
Some(vec!["firefox".into(), "firefox-esr".into()])
);
assert!(pkg.vulnerable);
}
#[test]
fn deserialize_null_origversion() {
let json = r#"{"repo": "homebrew", "version": "1.0", "origversion": null}"#;
let pkg: Package = serde_json::from_str(json).unwrap();
assert_eq!(pkg.origversion, None);
}
#[test]
fn deserialize_all_known_statuses() {
for status in [
"newest",
"devel",
"unique",
"outdated",
"legacy",
"rolling",
"noscheme",
"incorrect",
"untrusted",
"ignored",
] {
let json = format!(r#"{{"repo":"x","version":"1","status":"{}"}}"#, status);
let pkg: Package = serde_json::from_str(&json).unwrap();
assert!(
!matches!(pkg.status, Some(PackageStatus::Unknown(_))),
"status {status} should not be Unknown"
);
}
}
#[test]
fn deserialize_unknown_status() {
let json = r#"{"repo":"x","version":"1","status":"future_status"}"#;
let pkg: Package = serde_json::from_str(json).unwrap();
assert_eq!(
pkg.status,
Some(PackageStatus::Unknown("future_status".into()))
);
}
#[test]
fn status_round_trip() {
let json = r#"{"repo":"x","version":"1","status":"newest"}"#;
let pkg: Package = serde_json::from_str(json).unwrap();
let serialized = serde_json::to_value(&pkg).unwrap();
assert_eq!(serialized["status"], "newest");
}
#[test]
fn unknown_status_round_trip() {
let json = r#"{"repo":"x","version":"1","status":"future_thing"}"#;
let pkg: Package = serde_json::from_str(json).unwrap();
let serialized = serde_json::to_value(&pkg).unwrap();
assert_eq!(serialized["status"], "future_thing");
}
#[test]
fn vulnerable_absent_defaults_false() {
let json = r#"{"repo":"x","version":"1"}"#;
let pkg: Package = serde_json::from_str(json).unwrap();
assert!(!pkg.vulnerable);
}
#[test]
fn ignores_unknown_fields() {
let json = r#"{"repo":"x","version":"1","some_new_field":"value"}"#;
let pkg: Package = serde_json::from_str(json).unwrap();
assert_eq!(pkg.repo, "x");
}
}