use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::Path;
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct Lockfile {
#[serde(default, rename = "package")]
pub packages: Vec<LockedPackage>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LockedPackage {
pub name: String,
pub version: String,
pub source: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub resolved: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub dependencies: Vec<String>,
}
impl Lockfile {
pub fn new() -> Self {
Self::default()
}
pub fn load(path: &Path) -> Result<Self, LockfileError> {
let content = std::fs::read_to_string(path).map_err(|e| LockfileError::Io {
path: path.to_path_buf(),
error: e.to_string(),
})?;
toml::from_str(&content).map_err(|e| LockfileError::Parse {
path: path.to_path_buf(),
error: e.to_string(),
})
}
pub fn save(&self, path: &Path) -> Result<(), LockfileError> {
let content = self.to_string();
std::fs::write(path, content).map_err(|e| LockfileError::Io {
path: path.to_path_buf(),
error: e.to_string(),
})
}
pub fn add_package(&mut self, pkg: LockedPackage) {
self.packages.retain(|p| p.name != pkg.name);
self.packages.push(pkg);
self.packages.sort_by(|a, b| a.name.cmp(&b.name));
}
pub fn get_package(&self, name: &str) -> Option<&LockedPackage> {
self.packages.iter().find(|p| p.name == name)
}
pub fn is_locked(&self, name: &str) -> bool {
self.packages.iter().any(|p| p.name == name)
}
pub fn as_map(&self) -> HashMap<String, &LockedPackage> {
self.packages.iter().map(|p| (p.name.clone(), p)).collect()
}
}
impl std::fmt::Display for Lockfile {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
writeln!(f, "# This file is auto-generated by fastc. Do not edit.")?;
writeln!(f, "# Commit this file to version control for reproducible builds.")?;
writeln!(f)?;
for pkg in &self.packages {
writeln!(f, "[[package]]")?;
writeln!(f, "name = \"{}\"", pkg.name)?;
writeln!(f, "version = \"{}\"", pkg.version)?;
writeln!(f, "source = \"{}\"", pkg.source)?;
if let Some(resolved) = &pkg.resolved {
writeln!(f, "resolved = \"{}\"", resolved)?;
}
if !pkg.dependencies.is_empty() {
write!(f, "dependencies = [")?;
for (i, dep) in pkg.dependencies.iter().enumerate() {
if i > 0 {
write!(f, ", ")?;
}
write!(f, "\"{}\"", dep)?;
}
writeln!(f, "]")?;
}
writeln!(f)?;
}
Ok(())
}
}
#[derive(Debug)]
pub enum LockfileError {
Io {
path: std::path::PathBuf,
error: String,
},
Parse {
path: std::path::PathBuf,
error: String,
},
}
impl std::fmt::Display for LockfileError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
LockfileError::Io { path, error } => {
write!(f, "failed to read {}: {}", path.display(), error)
}
LockfileError::Parse { path, error } => {
write!(f, "failed to parse {}: {}", path.display(), error)
}
}
}
}
impl std::error::Error for LockfileError {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_lockfile_roundtrip() {
let mut lockfile = Lockfile::new();
lockfile.add_package(LockedPackage {
name: "mylib".to_string(),
version: "1.0.0".to_string(),
source: "git+https://github.com/user/mylib?tag=v1.0.0".to_string(),
resolved: Some("abc123def456".to_string()),
dependencies: vec!["utils".to_string()],
});
lockfile.add_package(LockedPackage {
name: "utils".to_string(),
version: "0.5.0".to_string(),
source: "git+https://github.com/user/utils?branch=main".to_string(),
resolved: Some("789xyz".to_string()),
dependencies: vec![],
});
let serialized = lockfile.to_string();
let parsed: Lockfile = toml::from_str(&serialized).unwrap();
assert_eq!(parsed.packages.len(), 2);
assert_eq!(parsed.packages[0].name, "mylib");
assert_eq!(parsed.packages[1].name, "utils");
}
#[test]
fn test_add_package_replaces_existing() {
let mut lockfile = Lockfile::new();
lockfile.add_package(LockedPackage {
name: "test".to_string(),
version: "1.0.0".to_string(),
source: "old".to_string(),
resolved: None,
dependencies: vec![],
});
lockfile.add_package(LockedPackage {
name: "test".to_string(),
version: "2.0.0".to_string(),
source: "new".to_string(),
resolved: None,
dependencies: vec![],
});
assert_eq!(lockfile.packages.len(), 1);
assert_eq!(lockfile.packages[0].version, "2.0.0");
}
}