loadsmith-manifest 0.2.0

Manifest and lockfile format, resolver, and profile state for the loadsmith mod-manager library
use std::collections::HashMap;

use loadsmith_core::{Checksum, Dependency, FileUrl, PackageId, PackageRef};
use serde::{Deserialize, Serialize};

use crate::{Diff, Diffable, PackageStoreEntry};

#[derive(Debug, Serialize, Deserialize, Default)]
pub struct Lockfile {
    packages: Vec<LockedPackage>,
}

impl Lockfile {
    pub fn new(packages: Vec<LockedPackage>) -> Self {
        Self { packages }
    }

    pub fn packages(&self) -> &[LockedPackage] {
        &self.packages
    }

    pub fn package_by_id(&self, id: &PackageId) -> Option<&LockedPackage> {
        self.packages.iter().find(|locked| locked.ref_.id() == id)
    }

    pub(crate) fn id_to_package_map(&self) -> HashMap<&PackageId, &LockedPackage> {
        self.packages.iter().map(|p| (p.ref_.id(), p)).collect()
    }

    pub fn diff<'a>(&'a self, new: &'a Lockfile) -> Diff<'a, LockedPackage, LockedPackage> {
        Diff::compute(self.id_to_package_map(), new.id_to_package_map())
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct LockedPackage {
    #[serde(rename = "package")]
    pub ref_: PackageRef,
    pub source: String,
    pub url: FileUrl,
    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
    pub transitive: bool,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub registry_metadata: Option<serde_json::Value>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub size: Option<u64>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub checksum: Option<Checksum>,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub deps: Vec<Dependency>,
}

impl LockedPackage {
    pub fn new(
        package: impl Into<PackageRef>,
        source: impl Into<String>,
        url: impl Into<FileUrl>,
    ) -> Self {
        Self {
            ref_: package.into(),
            source: source.into(),
            url: url.into(),
            transitive: false,
            registry_metadata: None,
            size: None,
            checksum: None,
            deps: Vec::new(),
        }
    }

    pub fn with_transitive(mut self, transitive: bool) -> Self {
        self.transitive = transitive;
        self
    }

    pub fn with_size(mut self, size: u64) -> Self {
        self.size = Some(size);
        self
    }

    pub fn with_checksum(mut self, checksum: impl Into<Checksum>) -> Self {
        self.checksum = Some(checksum.into());
        self
    }

    pub fn with_deps(mut self, deps: Vec<Dependency>) -> Self {
        self.deps = deps;
        self
    }

    pub fn with_registry_metadata(mut self, metadata: serde_json::Value) -> Self {
        self.registry_metadata = Some(metadata);
        self
    }

    pub fn store_entry(&self) -> PackageStoreEntry {
        PackageStoreEntry::new(self.ref_.clone(), self.checksum.clone())
    }

    pub fn into_store_entry(self) -> PackageStoreEntry {
        PackageStoreEntry::new(self.ref_, self.checksum)
    }
}

impl Diffable for LockedPackage {
    fn checksum(&self) -> Option<&Checksum> {
        self.checksum.as_ref()
    }

    fn version(&self) -> &loadsmith_core::Version {
        self.ref_.version()
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    use loadsmith_core::{PackageRef, Version};

    #[test]
    fn diff() {
        let example_com = FileUrl::try_from_url("https://example.com").unwrap();

        let a = Lockfile::new(vec![
            LockedPackage::new(
                PackageRef::new("A".to_string(), Version::new(1, 0, 0)),
                "local",
                example_com.clone(),
            ),
            LockedPackage::new(
                PackageRef::new("B".to_string(), Version::new(1, 0, 0)),
                "local",
                example_com.clone(),
            ),
        ]);

        let b = Lockfile::new(vec![
            LockedPackage::new(
                PackageRef::new("B".to_string(), Version::new(2, 0, 0)),
                "local",
                example_com.clone(),
            ),
            LockedPackage::new(
                PackageRef::new("C".to_string(), Version::new(1, 0, 0)),
                "local",
                example_com.clone(),
            ),
        ]);

        let diff = a.diff(&b);

        assert_eq!(
            diff.added,
            vec![&LockedPackage::new(
                PackageRef::new("C".to_string(), Version::new(1, 0, 0)),
                "local",
                example_com.clone(),
            )]
        );
        assert_eq!(
            diff.removed,
            vec![&LockedPackage::new(
                PackageRef::new("A".to_string(), Version::new(1, 0, 0)),
                "local",
                example_com.clone(),
            )]
        );
        assert_eq!(
            diff.changed,
            vec![(
                &LockedPackage::new(
                    PackageRef::new("B".to_string(), Version::new(1, 0, 0)),
                    "local",
                    example_com.clone(),
                ),
                &LockedPackage::new(
                    PackageRef::new("B".to_string(), Version::new(2, 0, 0)),
                    "local",
                    example_com.clone(),
                )
            )]
        );
    }
}