wally 0.3.2

Package manager for Roblox
Documentation
use std::collections::BTreeMap;
use std::path::Path;
use std::{
    fs::read_to_string,
    io::{self, BufWriter, Write},
};

use fs_err::File;
use semver::Version;
use serde::{Deserialize, Serialize};

use crate::package_id;
use crate::{
    manifest::Manifest, package_id::PackageId, package_name::PackageName, resolution::Resolve,
};

pub const LOCKFILE_NAME: &str = "wally.lock";

#[derive(Debug, Serialize, Deserialize)]
pub struct Lockfile {
    pub registry: String,

    #[serde(rename = "package")]
    pub packages: Vec<LockPackage>,
}

fn grab_dependencies(
    package_id: &PackageId,
    dependencies: &BTreeMap<
        package_id::PackageId,
        BTreeMap<std::string::String, package_id::PackageId>,
    >,
) -> Vec<(String, PackageId)> {
    dependencies
        .get(package_id)
        .map(|dependencies| {
            dependencies
                .iter()
                .map(|(key, value)| (key.clone(), value.clone()))
                .collect()
        })
        .unwrap_or_else(Vec::new)
}

impl Lockfile {
    pub fn from_manifest(manifest: &Manifest) -> Self {
        Self {
            registry: manifest.package.registry.clone(),
            packages: Vec::new(),
        }
    }

    pub fn from_resolve(resolve: &Resolve) -> Self {
        let mut packages = Vec::new();

        for package_id in &resolve.activated {
            let dependencies = [
                grab_dependencies(&package_id, &resolve.shared_dependencies),
                grab_dependencies(&package_id, &resolve.server_dependencies),
                grab_dependencies(&package_id, &resolve.dev_dependencies),
            ]
            .concat();

            packages.push(LockPackage::Registry(RegistryLockPackage {
                name: package_id.name().clone(),
                version: package_id.version().clone(),
                checksum: None,
                dependencies,
            }));
        }

        Self {
            registry: "test".to_owned(),
            packages,
        }
    }

    pub fn load(project_path: &Path) -> anyhow::Result<Option<Self>> {
        let lockfile_path = project_path.join(LOCKFILE_NAME);
        let contents = match read_to_string(&lockfile_path) {
            Ok(contents) => contents,
            Err(err) => {
                if err.kind() == io::ErrorKind::NotFound {
                    return Ok(None);
                } else {
                    return Err(err.into());
                }
            }
        };
        Ok(Some(toml::from_str(&contents)?))
    }

    pub fn save(&self, project_path: &Path) -> anyhow::Result<()> {
        let lockfile_path = project_path.join(LOCKFILE_NAME);
        let serialized = toml::to_string(self)?;

        let mut file = BufWriter::new(File::create(lockfile_path)?);
        writeln!(file, "# This file is automatically @generated by Wally.")?;
        writeln!(file, "# It is not intended for manual editing.")?;
        write!(file, "{}", serialized)?;
        file.flush()?;

        Ok(())
    }
}

#[derive(Debug, Serialize, Deserialize)]
#[serde(untagged)]
pub enum LockPackage {
    Registry(RegistryLockPackage),
    Git(GitLockPackage),
}

#[derive(Debug, Serialize, Deserialize)]
pub struct RegistryLockPackage {
    pub name: PackageName,
    pub version: Version,
    pub checksum: Option<String>,

    #[serde(default)]
    pub dependencies: Vec<(String, PackageId)>,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct GitLockPackage {
    pub name: String,
    pub rev: String,
    pub commit: String,

    #[serde(default)]
    pub dependencies: Vec<PackageId>,
}