luaupm 0.1.0-beta.1

The Luau package manager: dependencies, tools and scripts for Luau and Roblox projects
use crate::error::Error;
use crate::project::manifest::Environment;
use crate::registry::index::DownloadSource;
use serde::{Deserialize, Serialize};
use std::path::Path;

pub const LOCKFILE: &str = "lpm.lock";

/// Records exactly what `lpm install` resolved, so `lpm install --locked`
/// can reproduce it without consulting any index.
#[derive(Serialize, Deserialize, Debug)]
pub struct Lockfile {
    /// Lockfile format version.
    pub version: u32,
    #[serde(default, rename = "package")]
    pub packages: Vec<LockedPackage>,
}

#[derive(Serialize, Deserialize, Debug)]
pub struct LockedPackage {
    pub name: String,
    pub version: String,
    pub environment: Environment,
    /// Link file name generated in the environment folder.
    pub link: String,
    pub index: String,
    #[serde(flatten)]
    pub source: DownloadSource,
}

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

    pub fn load() -> Result<Self, Error> {
        let path = Path::new(LOCKFILE);
        if !path.exists() {
            return Err(Error::LockfileMissing);
        }
        Ok(toml::from_str(&std::fs::read_to_string(path)?)?)
    }

    pub fn save(&self) -> Result<(), Error> {
        let header = "# This file is generated by `lpm install`; do not edit it manually.\n";
        let body = toml::to_string(self).expect("lockfile serializes");
        std::fs::write(LOCKFILE, format!("{header}{body}"))?;
        Ok(())
    }
}

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

    #[test]
    fn round_trips() {
        let lockfile = Lockfile::new(vec![
            LockedPackage {
                name: "evaera/promise".to_string(),
                version: "4.0.0".to_string(),
                environment: Environment::Shared,
                link: "Promise".to_string(),
                index: "https://github.com/UpliftGames/wally-index".to_string(),
                source: DownloadSource::Zip {
                    url: "https://api.wally.run/v1/package-contents/evaera/promise/4.0.0"
                        .to_string(),
                },
            },
            LockedPackage {
                name: "pesde/hello".to_string(),
                version: "1.0.2".to_string(),
                environment: Environment::Luau,
                link: "hello".to_string(),
                index: "https://github.com/pesde-pkg/index".to_string(),
                source: DownloadSource::TarGz {
                    url: "https://registry.pesde.daimond113.com/v1/packages/pesde%2Fhello/1.0.2/luau/archive".to_string(),
                },
            },
        ]);

        let text = toml::to_string(&lockfile).unwrap();
        let parsed: Lockfile = toml::from_str(&text).unwrap();
        assert_eq!(parsed.version, 1);
        assert_eq!(parsed.packages.len(), 2);
        assert_eq!(parsed.packages[0].name, "evaera/promise");
        assert!(matches!(
            &parsed.packages[1].source,
            DownloadSource::TarGz { url } if url.contains("pesde%2Fhello")
        ));
    }
}