soothfast_docs/
lockfile.rs1use std::collections::BTreeMap;
6use std::path::Path;
7
8use serde_json::{Value, json};
9
10pub const LOCKFILE: &str = "soothfast.lock";
12
13pub type Binds = BTreeMap<String, String>;
15
16pub const VERSION: u64 = 2;
20
21#[derive(Debug, Default)]
23pub struct Lock {
24 pub version: u64,
26 pub binds: Binds,
28}
29
30pub fn read(root: &Path) -> Result<Lock, String> {
34 let path = root.join(LOCKFILE);
35 if !path.exists() {
36 return Ok(Lock {
37 version: VERSION,
38 binds: Binds::new(),
39 });
40 }
41 let text = std::fs::read_to_string(&path).map_err(|e| e.to_string())?;
42 let doc: Value = serde_json::from_str(&text).map_err(|e| e.to_string())?;
43 let mut binds = Binds::new();
44 if let Some(map) = doc["binds"].as_object() {
45 for (k, v) in map {
46 if let Some(fp) = v.as_str() {
47 binds.insert(k.clone(), fp.to_string());
48 }
49 }
50 }
51 Ok(Lock {
52 version: doc["version"].as_u64().unwrap_or(1),
53 binds,
54 })
55}
56
57pub fn write(root: &Path, binds: &Binds) -> Result<(), String> {
59 let doc = json!({ "version": VERSION, "binds": binds });
60 let text = serde_json::to_string_pretty(&doc).map_err(|e| e.to_string())?;
61 std::fs::write(root.join(LOCKFILE), text + "\n").map_err(|e| e.to_string())
62}
63
64pub fn merge(existing: &Binds, fresh: &Binds, full_scope: bool) -> Binds {
68 if full_scope {
69 fresh.clone()
70 } else {
71 let mut merged = existing.clone();
72 merged.extend(fresh.iter().map(|(k, v)| (k.clone(), v.clone())));
73 merged
74 }
75}
76
77pub fn comparable(lock: Lock) -> Result<Binds, String> {
91 if lock.version > VERSION {
92 return Err(format!(
93 "{LOCKFILE} is format v{}, this build writes v{VERSION}: upgrade cargo-soothfast rather than overwriting a newer lock",
94 lock.version
95 ));
96 }
97 if lock.version == VERSION {
98 Ok(lock.binds)
99 } else {
100 Ok(Binds::new())
101 }
102}
103
104#[cfg(test)]
105mod tests {
106 use super::*;
107
108 fn binds(pairs: &[(&str, &str)]) -> Binds {
109 pairs
110 .iter()
111 .map(|(k, v)| (k.to_string(), v.to_string()))
112 .collect()
113 }
114
115 #[test]
116 fn partial_scope_merge_preserves_out_of_scope_entries() {
117 let existing = binds(&[("a::x", "1111"), ("b::y", "2222")]);
118 let fresh = binds(&[("a::x", "9999")]);
119 let merged = merge(&existing, &fresh, false);
120 assert_eq!(merged.get("a::x").map(String::as_str), Some("9999"));
121 assert_eq!(merged.get("b::y").map(String::as_str), Some("2222"));
122 assert_eq!(merged.len(), 2);
123 }
124
125 #[test]
126 fn entries_from_an_older_format_are_not_merged_over() {
127 let lock = Lock {
128 version: VERSION - 1,
129 binds: binds(&[("a::x", "1111")]),
130 };
131 assert!(comparable(lock).unwrap().is_empty());
132 }
133
134 #[test]
135 fn entries_from_this_format_survive_a_partial_accept() {
136 let lock = Lock {
137 version: VERSION,
138 binds: binds(&[("a::x", "1111")]),
139 };
140 assert_eq!(comparable(lock).unwrap().len(), 1);
141 }
142
143 #[test]
144 fn a_newer_lock_is_refused_rather_than_overwritten() {
145 let lock = Lock {
146 version: VERSION + 1,
147 binds: binds(&[("a::x", "1111")]),
148 };
149 assert!(comparable(lock).is_err());
150 }
151
152 #[test]
153 fn full_scope_merge_drops_binds_that_no_longer_exist() {
154 let existing = binds(&[("a::x", "1111"), ("gone::z", "2222")]);
155 let fresh = binds(&[("a::x", "9999")]);
156 assert_eq!(merge(&existing, &fresh, true), fresh);
157 }
158}