use serde::{Deserialize, Serialize};
use std::path::Path;
pub const LOCK_FORMAT_VERSION: u32 = 1;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct LockEntry {
pub name: String,
pub registry: String,
pub constraint: String,
pub version: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub head_op: Option<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct LockFile {
pub version: u32,
#[serde(default, rename = "package")]
pub packages: Vec<LockEntry>,
}
impl LockFile {
pub fn entry(&self, name: &str) -> Option<&LockEntry> {
self.packages.iter().find(|e| e.name == name)
}
pub fn load_dir(dir: &Path) -> Option<LockFile> {
let raw = std::fs::read_to_string(dir.join("lex.lock")).ok()?;
Self::from_toml(&raw).ok()
}
pub fn from_toml(s: &str) -> Result<Self, String> {
toml::from_str(s).map_err(|e| format!("parsing lex.lock: {e}"))
}
pub fn to_toml(&self) -> Result<String, String> {
let mut sorted = self.clone();
sorted.packages.sort_by(|a, b| a.name.cmp(&b.name));
let body = toml::to_string(&sorted).map_err(|e| format!("serializing lex.lock: {e}"))?;
Ok(format!(
"# lex.lock — resolved dependency pins, generated by `lex pkg lock`.\n\
# Do not edit by hand; run `lex pkg lock` (or `lex pkg update`) instead.\n\n{body}"
))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn toml_round_trips_and_sorts_by_name() {
let lf = LockFile {
version: LOCK_FORMAT_VERSION,
packages: vec![
LockEntry {
name: "zeta".into(),
registry: "reg".into(),
constraint: "^1".into(),
version: "1.0.0".into(),
head_op: Some("op_z".into()),
},
LockEntry {
name: "alpha".into(),
registry: "reg".into(),
constraint: "^2".into(),
version: "2.1.0".into(),
head_op: None,
},
],
};
let toml = lf.to_toml().unwrap();
assert!(toml.find("alpha").unwrap() < toml.find("zeta").unwrap());
let back = LockFile::from_toml(&toml).unwrap();
assert_eq!(back.version, LOCK_FORMAT_VERSION);
assert_eq!(back.entry("alpha").unwrap().head_op, None);
assert_eq!(back.entry("zeta").unwrap().version, "1.0.0");
}
}