use tracing::debug;
use serde::{Serialize, Deserialize};
use semver::Version;
use crate::{PackageName, GroupName, PackageId, Error, Result, Target, MaybeVersion};
#[derive(Debug, Serialize, Deserialize)]
pub struct Package {
pub name: PackageName,
pub group: GroupName,
pub kind: PackageKind,
pub author: Option<String>,
pub description: Option<String>,
pub repository: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
releases: Vec<Release>,
}
impl Package {
pub fn new_binary<V>(
id: &PackageId<V>,
author: impl Into<String>,
desc: impl Into<String>,
repo: impl Into<String>,
) -> Self {
let author = author.into();
let description = desc.into();
let repository = repo.into();
Package {
name: id.name().clone(),
group: id.group().clone(),
kind: PackageKind::Binary,
author: Some(author),
description: Some(description),
repository: Some(repository),
releases: vec![],
}
}
pub fn latest_release(&self) -> Result<&Release> {
debug!(releases = ?&self.releases, "Finding latest release");
self.releases
.last()
.ok_or_else(|| Error::NoReleases(self.package_id().to_string()))
}
pub fn latest_release_for_target(&self, target: &Target, prerelease: bool) -> Result<&Release> {
self.releases
.iter()
.rev()
.find(|it| {
if !prerelease && (!it.version.pre.is_empty() || !it.version.build.is_empty()) {
return false;
}
it.targets.contains(target)
})
.ok_or_else(|| Error::MissingTarget(target.clone()))
}
fn package_id(&self) -> PackageId<MaybeVersion> {
PackageId::new_unversioned(self.name.clone(), self.group.clone())
}
pub fn add_release(&mut self, version: Version, target: Target) -> Result<()> {
let maybe_release = self
.releases
.iter_mut()
.find(|it| version_exactly_eq(&it.version, &version));
match maybe_release {
Some(release) => release.add_target(target),
None => {
let release = Release::new(version, target);
self.releases.push(release);
self.releases.sort_by(|a, b| a.version.cmp(&b.version));
}
}
Ok(())
}
pub fn releases_for_target(&self, target: &Target) -> Vec<&Release> {
self.releases
.iter()
.filter(|it| it.targets.contains(target))
.collect()
}
}
fn version_exactly_eq(a: &Version, b: &Version) -> bool {
a.eq(b) && a.build.eq(&b.build)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PackageKind {
Binary,
Unknown(String),
}
impl Serialize for PackageKind {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: serde::ser::Serializer,
{
let value = match self {
Self::Binary => "bin".serialize(serializer)?,
Self::Unknown(other) => other.serialize(serializer)?,
};
Ok(value)
}
}
impl<'de> Deserialize<'de> for PackageKind {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: serde::de::Deserializer<'de>,
{
let string = String::deserialize(deserializer)?;
let kind = match &*string {
"bin" => Self::Binary,
_ => Self::Unknown(string),
};
Ok(kind)
}
}
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
pub struct Release {
pub version: Version,
pub yanked: bool,
targets: Vec<Target>,
}
impl Release {
pub fn new(version: Version, target: Target) -> Self {
Self {
version,
yanked: false,
targets: vec![target],
}
}
pub fn add_target(&mut self, target: Target) {
if !self.target_exists(&target) {
self.targets.push(target);
}
}
pub fn target_exists(&self, target: &Target) -> bool {
self.targets.iter().any(|it| it == target)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn test_package() -> Package {
Package {
name: "my-package".parse().unwrap(),
group: "my-group".parse().unwrap(),
kind: PackageKind::Binary,
author: None,
description: None,
repository: None,
releases: vec![
Release {
version: Version::parse("0.1.0-alpha.1").unwrap(),
yanked: false,
targets: vec![Target::X86_64AppleDarwin],
},
Release {
version: Version::parse("0.1.0").unwrap(),
yanked: false,
targets: vec![Target::X86_64AppleDarwin],
},
Release {
version: Version::parse("0.2.0-alpha.1").unwrap(),
yanked: false,
targets: vec![Target::X86_64AppleDarwin],
},
Release {
version: Version::parse("0.2.0-alpha.2").unwrap(),
yanked: false,
targets: vec![Target::X86_64AppleDarwin],
},
],
}
}
#[test]
fn test_serialize_package() {
let id: PackageId<MaybeVersion> = "fluvio/fluvio".parse().unwrap();
let package = Package::new_binary(&id, "Bob", "A package", "https://github.com");
let stringified = serde_json::to_string(&package).unwrap();
assert_eq!(
stringified,
r#"{"name":"fluvio","group":"fluvio","kind":"bin","author":"Bob","description":"A package","repository":"https://github.com"}"#
)
}
#[test]
fn test_deserialize_package() {
let json = r#"{
"name": "fluvio",
"group": "fluvio",
"kind": "bin",
"author": "Fluvio Contributors",
"description": "The Fluvio CLI, an all-in-one toolkit for streaming with Fluvio",
"repository": "https://github.com/infinyon/fluvio",
"releases": [
{
"version": "0.8.4",
"yanked": false,
"targets": ["x86_64-unknown-linux-musl", "x86_64-apple-darwin", "armv7-unknown-linux-gnueabihf"]
}
]
}"#;
let package: Package = serde_json::from_str(json).unwrap();
assert_eq!(package.name, "fluvio".parse().unwrap());
assert_eq!(package.group, "fluvio".parse().unwrap());
assert_eq!(package.kind, PackageKind::Binary);
assert_eq!(package.author, Some("Fluvio Contributors".to_string()));
assert_eq!(
package.description,
Some("The Fluvio CLI, an all-in-one toolkit for streaming with Fluvio".to_string())
);
assert_eq!(package.releases.len(), 1);
let release = &package.releases[0];
assert_eq!(release.version, semver::Version::parse("0.8.4").unwrap());
assert!(!release.yanked);
assert_eq!(release.targets.len(), 3);
assert_eq!(release.targets[0], Target::X86_64UnknownLinuxMusl);
assert_eq!(release.targets[1], Target::X86_64AppleDarwin);
assert_eq!(
release.targets[2],
"armv7-unknown-linux-gnueabihf".parse().unwrap()
);
}
#[test]
fn test_deserialize_package_unknown_kind() {
let json = r#"{
"name": "fluvio-smartmodule-filter",
"group": "fluvio",
"kind": "wasm",
"releases": []
}"#;
let package: Package = serde_json::from_str(json).unwrap();
assert_eq!(package.name, "fluvio-smartmodule-filter".parse().unwrap());
assert_eq!(package.group, "fluvio".parse().unwrap());
assert_eq!(package.kind, PackageKind::Unknown("wasm".to_string()));
assert!(package.releases.is_empty());
}
#[test]
fn test_get_latest_prerelease() {
let package = test_package();
let release = package
.latest_release_for_target(&Target::X86_64AppleDarwin, true)
.unwrap();
assert_eq!(release.version, Version::parse("0.2.0-alpha.2").unwrap());
}
#[test]
fn test_get_latest_release() {
let package = test_package();
let release = package
.latest_release_for_target(&Target::X86_64AppleDarwin, false)
.unwrap();
assert_eq!(release.version, Version::parse("0.1.0").unwrap());
}
#[test]
fn test_deserialize_package_kind_bin() {
let name = "\"bin\"";
let kind: PackageKind = serde_json::from_str(name).unwrap();
assert_eq!(kind, PackageKind::Binary);
}
#[test]
fn test_serialize_package_kind_bin() {
let kind = PackageKind::Binary;
let json = serde_json::to_string(&kind).unwrap();
assert_eq!(json, "\"bin\"");
}
#[test]
fn test_deserialize_package_kind_unknown() {
let name = "\"wasm\"";
let kind: PackageKind = serde_json::from_str(name).unwrap();
assert_eq!(kind, PackageKind::Unknown("wasm".to_string()));
}
}