use std::collections::BTreeMap;
use std::path::Path;
use semver::Version;
use serde::{Deserialize, Serialize};
use crate::error::PkgError;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Lockfile {
#[serde(default = "default_version")]
pub version: u32,
#[serde(default, rename = "package")]
pub packages: Vec<LockedPackage>,
}
fn default_version() -> u32 {
1
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct LockedPackage {
pub name: String,
pub version: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub source: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub checksum: Option<String>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub dependencies: BTreeMap<String, String>,
}
impl Lockfile {
#[must_use]
pub fn from_resolved(resolved: &BTreeMap<String, Version>) -> Self {
let packages = resolved
.iter()
.map(|(name, version)| LockedPackage {
name: name.clone(),
version: version.to_string(),
source: None,
checksum: None,
dependencies: BTreeMap::new(),
})
.collect();
Lockfile {
version: 1,
packages,
}
}
pub fn parse(s: &str) -> Result<Self, PkgError> {
toml::from_str(s).map_err(|e| PkgError::LockfileParse(e.to_string()))
}
pub fn from_file(path: &Path) -> Result<Self, PkgError> {
let content = std::fs::read_to_string(path).map_err(|e| PkgError::Io(e.to_string()))?;
Self::parse(&content)
}
pub fn to_toml_string(&self) -> Result<String, PkgError> {
let mut output = String::new();
output.push_str("# This file is automatically generated by `bock pkg`.\n");
output.push_str("# Do not edit manually.\n\n");
output.push_str(&format!("version = {}\n", self.version));
for pkg in &self.packages {
output.push('\n');
output.push_str("[[package]]\n");
output.push_str(&format!("name = \"{}\"\n", pkg.name));
output.push_str(&format!("version = \"{}\"\n", pkg.version));
if let Some(source) = &pkg.source {
output.push_str(&format!("source = \"{source}\"\n"));
}
if let Some(checksum) = &pkg.checksum {
output.push_str(&format!("checksum = \"{checksum}\"\n"));
}
if !pkg.dependencies.is_empty() {
output.push_str("\n[package.dependencies]\n");
for (name, ver) in &pkg.dependencies {
output.push_str(&format!("{name} = \"{ver}\"\n"));
}
}
}
Ok(output)
}
pub fn write_to_file(&self, path: &Path) -> Result<(), PkgError> {
let content = self.to_toml_string()?;
std::fs::write(path, content).map_err(|e| PkgError::Io(e.to_string()))
}
#[must_use]
pub fn get_version(&self, name: &str) -> Option<&str> {
self.packages
.iter()
.find(|p| p.name == name)
.map(|p| p.version.as_str())
}
pub fn to_resolved(&self) -> Result<BTreeMap<String, Version>, PkgError> {
let mut result = BTreeMap::new();
for pkg in &self.packages {
let ver = crate::version::parse_version(&pkg.version)?;
result.insert(pkg.name.clone(), ver);
}
Ok(result)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn create_from_resolved() {
let mut resolved = BTreeMap::new();
resolved.insert("foo".to_string(), Version::new(1, 2, 3));
resolved.insert("bar".to_string(), Version::new(0, 5, 0));
let lockfile = Lockfile::from_resolved(&resolved);
assert_eq!(lockfile.packages.len(), 2);
assert_eq!(lockfile.get_version("foo"), Some("1.2.3"));
assert_eq!(lockfile.get_version("bar"), Some("0.5.0"));
}
#[test]
fn roundtrip_serialize() {
let mut resolved = BTreeMap::new();
resolved.insert("dep-a".to_string(), Version::new(1, 0, 0));
resolved.insert("dep-b".to_string(), Version::new(2, 3, 4));
let lockfile = Lockfile::from_resolved(&resolved);
let toml_str = lockfile.to_toml_string().unwrap();
let reparsed = Lockfile::parse(&toml_str).unwrap();
assert_eq!(reparsed.packages.len(), 2);
assert_eq!(reparsed.get_version("dep-a"), Some("1.0.0"));
assert_eq!(reparsed.get_version("dep-b"), Some("2.3.4"));
}
#[test]
fn to_resolved_map() {
let lockfile = Lockfile {
version: 1,
packages: vec![LockedPackage {
name: "x".into(),
version: "1.0.0".into(),
source: None,
checksum: None,
dependencies: BTreeMap::new(),
}],
};
let resolved = lockfile.to_resolved().unwrap();
assert_eq!(resolved["x"], Version::new(1, 0, 0));
}
#[test]
fn file_roundtrip() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("bock.lock");
let mut resolved = BTreeMap::new();
resolved.insert("pkg-a".to_string(), Version::new(3, 1, 4));
let lockfile = Lockfile::from_resolved(&resolved);
lockfile.write_to_file(&path).unwrap();
let loaded = Lockfile::from_file(&path).unwrap();
assert_eq!(loaded.get_version("pkg-a"), Some("3.1.4"));
}
}