loadsmith-registry 0.2.0

Registry abstraction and built-in sources for the loadsmith mod-manager library
Documentation
use std::{collections::HashMap, pin::Pin};

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

use crate::{Error, Registry, ResolvedVersion, Result, VersionInfo};

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Package {
    pub id: PackageId,
    pub versions: Vec<PackageVersion>,
}

impl Package {
    pub fn new(id: impl Into<PackageId>, versions: Vec<PackageVersion>) -> Self {
        Self {
            id: id.into(),
            versions,
        }
    }

    fn version_by_version<'a>(&'a self, version: &Version) -> Result<&'a PackageVersion> {
        self.versions
            .iter()
            .find(|v| v.version == *version)
            .ok_or(Error::VersionNotFound)
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PackageVersion {
    pub version: Version,
    pub url: FileUrl,
    #[serde(default)]
    pub size: Option<u64>,
    #[serde(default)]
    pub checksum: Option<Checksum>,
    pub deps: Vec<Dependency>,
}

impl PackageVersion {
    pub fn new(version: impl Into<Version>, download_url: impl Into<FileUrl>) -> Self {
        Self {
            version: version.into(),
            url: download_url.into(),
            size: None,
            checksum: None,
            deps: Vec::new(),
        }
    }

    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
    }
}

#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct OfflineRegistry {
    packages: HashMap<PackageId, Package>,
}

impl OfflineRegistry {
    pub fn new(packages: HashMap<PackageId, Package>) -> Self {
        Self { packages }
    }

    fn package_by_id<'a>(&'a self, id: &PackageId) -> Result<&'a Package> {
        self.packages.get(id).ok_or(Error::PackageNotFound)
    }
}

impl Registry for OfflineRegistry {
    fn version_info<'a>(
        &'a self,
        id: &'a PackageId,
        _metadata: Option<&'a serde_json::Value>,
    ) -> Pin<Box<dyn Future<Output = Result<Vec<VersionInfo>>> + 'a>> {
        Box::pin(async move {
            let package = self.package_by_id(id)?;

            let versions = package
                .versions
                .iter()
                .map(|v| VersionInfo {
                    version: v.version.clone(),
                })
                .collect();

            Ok(versions)
        })
    }

    fn resolve<'a>(
        &'a self,
        ref_: &'a PackageRef,
        _metadata: Option<&'a serde_json::Value>,
    ) -> Pin<Box<dyn Future<Output = Result<ResolvedVersion>> + 'a>> {
        Box::pin(async move {
            let version = self
                .package_by_id(ref_.id())?
                .version_by_version(ref_.version())?;

            Ok(ResolvedVersion {
                url: version.url.clone(),
                size: version.size,
                checksum: version.checksum.clone(),
                deps: version.deps.clone(),
            })
        })
    }
}

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

    use std::assert_matches;

    #[tokio::test]
    async fn it_works() {
        let registry = OfflineRegistry::new(HashMap::from_iter([(
            PackageId::new("author-name"),
            Package::new(
                PackageId::new("author-name"),
                vec![
                    PackageVersion::new(
                        Version::new(1, 0, 0),
                        FileUrl::try_from_url("https://example.com/package-1.0.0.zip").unwrap(),
                    ),
                    PackageVersion::new(
                        Version::new(1, 1, 0),
                        FileUrl::try_from_url("https://example.com/package-1.1.0.zip").unwrap(),
                    ),
                ],
            ),
        )]));

        let versions = registry
            .version_info(&PackageId::new("author-name"), None)
            .await
            .unwrap();

        assert_eq!(versions.len(), 2);

        let non_existent_package = registry
            .version_info(&PackageId::new("non-existent"), None)
            .await;

        assert_matches!(non_existent_package, Err(Error::PackageNotFound));
    }
}