use std::collections::BTreeMap;
use std::path::Path;
use serde_json::{Value, json};
pub const LOCKFILE: &str = "soothfast.lock";
pub type Binds = BTreeMap<String, String>;
pub const VERSION: u64 = 2;
#[derive(Debug, Default)]
pub struct Lock {
pub version: u64,
pub binds: Binds,
}
pub fn read(root: &Path) -> Result<Lock, String> {
let path = root.join(LOCKFILE);
if !path.exists() {
return Ok(Lock {
version: VERSION,
binds: Binds::new(),
});
}
let text = std::fs::read_to_string(&path).map_err(|e| e.to_string())?;
let doc: Value = serde_json::from_str(&text).map_err(|e| e.to_string())?;
let mut binds = Binds::new();
if let Some(map) = doc["binds"].as_object() {
for (k, v) in map {
if let Some(fp) = v.as_str() {
binds.insert(k.clone(), fp.to_string());
}
}
}
Ok(Lock {
version: doc["version"].as_u64().unwrap_or(1),
binds,
})
}
pub fn write(root: &Path, binds: &Binds) -> Result<(), String> {
let doc = json!({ "version": VERSION, "binds": binds });
let text = serde_json::to_string_pretty(&doc).map_err(|e| e.to_string())?;
std::fs::write(root.join(LOCKFILE), text + "\n").map_err(|e| e.to_string())
}
pub fn merge(existing: &Binds, fresh: &Binds, full_scope: bool) -> Binds {
if full_scope {
fresh.clone()
} else {
let mut merged = existing.clone();
merged.extend(fresh.iter().map(|(k, v)| (k.clone(), v.clone())));
merged
}
}
pub fn comparable(lock: Lock) -> Result<Binds, String> {
if lock.version > VERSION {
return Err(format!(
"{LOCKFILE} is format v{}, this build writes v{VERSION}: upgrade cargo-soothfast rather than overwriting a newer lock",
lock.version
));
}
if lock.version == VERSION {
Ok(lock.binds)
} else {
Ok(Binds::new())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn binds(pairs: &[(&str, &str)]) -> Binds {
pairs
.iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect()
}
#[test]
fn partial_scope_merge_preserves_out_of_scope_entries() {
let existing = binds(&[("a::x", "1111"), ("b::y", "2222")]);
let fresh = binds(&[("a::x", "9999")]);
let merged = merge(&existing, &fresh, false);
assert_eq!(merged.get("a::x").map(String::as_str), Some("9999"));
assert_eq!(merged.get("b::y").map(String::as_str), Some("2222"));
assert_eq!(merged.len(), 2);
}
#[test]
fn entries_from_an_older_format_are_not_merged_over() {
let lock = Lock {
version: VERSION - 1,
binds: binds(&[("a::x", "1111")]),
};
assert!(comparable(lock).unwrap().is_empty());
}
#[test]
fn entries_from_this_format_survive_a_partial_accept() {
let lock = Lock {
version: VERSION,
binds: binds(&[("a::x", "1111")]),
};
assert_eq!(comparable(lock).unwrap().len(), 1);
}
#[test]
fn a_newer_lock_is_refused_rather_than_overwritten() {
let lock = Lock {
version: VERSION + 1,
binds: binds(&[("a::x", "1111")]),
};
assert!(comparable(lock).is_err());
}
#[test]
fn full_scope_merge_drops_binds_that_no_longer_exist() {
let existing = binds(&[("a::x", "1111"), ("gone::z", "2222")]);
let fresh = binds(&[("a::x", "9999")]);
assert_eq!(merge(&existing, &fresh, true), fresh);
}
}