1use anyhow::{anyhow, Context, Result};
7use serde_json::Value as JsonValue;
8use std::{
9 fs,
10 path::{Path, PathBuf},
11};
12use time::OffsetDateTime;
13
14pub fn home() -> Result<PathBuf> {
16 dirs::home_dir().ok_or_else(|| anyhow!("no home directory found"))
17}
18
19pub fn find_git_root(start: &Path) -> PathBuf {
22 let mut cur = start.to_path_buf();
23 loop {
24 if cur.join(".git").exists() {
25 return cur;
26 }
27 if !cur.pop() {
28 return start.to_path_buf();
29 }
30 }
31}
32
33pub fn now_stamp() -> String {
35 let dt = OffsetDateTime::now_local().unwrap_or_else(|_| OffsetDateTime::now_utc());
36 dt.format(&time::format_description::parse("[year][month][day]-[hour][minute][second]").unwrap())
37 .unwrap()
38}
39
40pub fn backup(path: &Path) -> Result<()> {
48 if !path.exists() {
49 return Ok(());
50 }
51 let ext = path.extension().and_then(|s| s.to_str()).unwrap_or("");
52 let bak = path.with_extension(format!("{}.bak.{}", ext, now_stamp()));
53 fs::copy(path, &bak).with_context(|| format!("backup {:?} -> {:?}", path, bak))?;
54 Ok(())
55}
56
57pub fn ensure_parent(path: &Path) -> Result<()> {
59 if let Some(p) = path.parent() {
60 fs::create_dir_all(p).with_context(|| format!("create dirs {:?}", p))?;
61 }
62 Ok(())
63}
64
65#[inline]
67pub fn log_verbose(verbose: bool, msg: impl AsRef<str>) {
68 if verbose {
69 eprintln!("{}", msg.as_ref());
70 }
71}
72
73pub fn read_json_file(path: &Path) -> Result<JsonValue> {
77 if !path.exists() {
78 return Ok(JsonValue::Object(serde_json::Map::new()));
79 }
80 let s = fs::read_to_string(path).with_context(|| format!("read {:?}", path))?;
81 let v: JsonValue = serde_json::from_str(&s).with_context(|| format!("parse JSON {:?}", path))?;
82 Ok(v)
83}
84
85pub fn write_json_file(path: &Path, v: &JsonValue, dry_run: bool) -> Result<()> {
88 let s = serde_json::to_string_pretty(v).context("stringify JSON")? + "\n";
89 if dry_run {
90 return Ok(());
91 }
92 ensure_parent(path)?;
93 backup(path)?;
94 fs::write(path, s).with_context(|| format!("write {:?}", path))?;
95 Ok(())
96}
97
98pub fn json_obj_mut(v: &mut JsonValue) -> Result<&mut serde_json::Map<String, JsonValue>> {
100 v.as_object_mut()
101 .ok_or_else(|| anyhow!("expected JSON object"))
102}
103
104pub fn json_get_obj_mut<'a>(
106 root: &'a mut JsonValue,
107 key: &str,
108) -> Result<&'a mut serde_json::Map<String, JsonValue>> {
109 let obj = json_obj_mut(root)?;
110 if !obj.contains_key(key) {
111 obj.insert(key.to_string(), JsonValue::Object(serde_json::Map::new()));
112 }
113 obj.get_mut(key)
114 .and_then(|v| v.as_object_mut())
115 .ok_or_else(|| anyhow!("expected object at key {}", key))
116}
117
118#[cfg(test)]
119mod tests {
120 use super::*;
121 use std::fs;
122 use tempfile::TempDir;
123
124 #[test]
127 fn test_find_git_root_with_git_dir() {
128 let temp = TempDir::new().unwrap();
129 let git_dir = temp.path().join(".git");
130 fs::create_dir(&git_dir).unwrap();
131
132 let result = find_git_root(temp.path());
133 assert_eq!(result, temp.path());
134 }
135
136 #[test]
137 fn test_find_git_root_in_subdirectory() {
138 let temp = TempDir::new().unwrap();
139 let git_dir = temp.path().join(".git");
140 fs::create_dir(&git_dir).unwrap();
141
142 let subdir = temp.path().join("src").join("deep");
143 fs::create_dir_all(&subdir).unwrap();
144
145 let result = find_git_root(&subdir);
146 assert_eq!(result, temp.path());
147 }
148
149 #[test]
150 fn test_find_git_root_no_git_returns_start() {
151 let temp = TempDir::new().unwrap();
152 let subdir = temp.path().join("some").join("path");
153 fs::create_dir_all(&subdir).unwrap();
154
155 let result = find_git_root(&subdir);
156 assert_eq!(result, subdir);
157 }
158
159 #[test]
162 fn test_now_stamp_format() {
163 let stamp = now_stamp();
164 assert_eq!(stamp.len(), 15);
166 assert!(stamp.contains('-'));
167
168 let parts: Vec<&str> = stamp.split('-').collect();
170 assert_eq!(parts.len(), 2);
171 assert!(parts[0].chars().all(|c| c.is_ascii_digit()));
172 assert!(parts[1].chars().all(|c| c.is_ascii_digit()));
173 }
174
175 #[test]
178 fn test_ensure_parent_creates_directory() {
179 let temp = TempDir::new().unwrap();
180 let file_path = temp.path().join("deep").join("nested").join("file.json");
181
182 ensure_parent(&file_path).unwrap();
183
184 assert!(file_path.parent().unwrap().exists());
185 }
186
187 #[test]
188 fn test_ensure_parent_existing_directory() {
189 let temp = TempDir::new().unwrap();
190 let file_path = temp.path().join("file.json");
191
192 ensure_parent(&file_path).unwrap();
194 }
195
196 #[test]
199 fn test_backup_nonexistent_file_ok() {
200 let temp = TempDir::new().unwrap();
201 let file_path = temp.path().join("nonexistent.json");
202
203 backup(&file_path).unwrap();
205 }
206
207 #[test]
208 fn test_backup_creates_backup_file() {
209 let temp = TempDir::new().unwrap();
210 let file_path = temp.path().join("config.json");
211 fs::write(&file_path, r#"{"test": true}"#).unwrap();
212
213 backup(&file_path).unwrap();
214
215 let entries: Vec<_> = fs::read_dir(temp.path()).unwrap().collect();
217 assert_eq!(entries.len(), 2); let backup_file = entries.iter()
221 .map(|e| e.as_ref().unwrap().path())
222 .find(|p| p.to_string_lossy().contains(".bak."))
223 .unwrap();
224 let backup_content = fs::read_to_string(&backup_file).unwrap();
225 assert_eq!(backup_content, r#"{"test": true}"#);
226 }
227
228 #[test]
231 fn test_read_json_file_nonexistent_returns_empty_object() {
232 let temp = TempDir::new().unwrap();
233 let file_path = temp.path().join("nonexistent.json");
234
235 let result = read_json_file(&file_path).unwrap();
236
237 assert!(result.is_object());
238 assert!(result.as_object().unwrap().is_empty());
239 }
240
241 #[test]
242 fn test_read_json_file_valid_json() {
243 let temp = TempDir::new().unwrap();
244 let file_path = temp.path().join("config.json");
245 fs::write(&file_path, r#"{"name": "test", "value": 42}"#).unwrap();
246
247 let result = read_json_file(&file_path).unwrap();
248
249 assert_eq!(result["name"], "test");
250 assert_eq!(result["value"], 42);
251 }
252
253 #[test]
254 fn test_read_json_file_invalid_json_error() {
255 let temp = TempDir::new().unwrap();
256 let file_path = temp.path().join("invalid.json");
257 fs::write(&file_path, "not valid json {").unwrap();
258
259 let result = read_json_file(&file_path);
260
261 assert!(result.is_err());
262 }
263
264 #[test]
265 fn test_write_json_file_creates_file() {
266 let temp = TempDir::new().unwrap();
267 let file_path = temp.path().join("output.json");
268 let value = serde_json::json!({"key": "value"});
269
270 write_json_file(&file_path, &value, false).unwrap();
271
272 assert!(file_path.exists());
273 let content = fs::read_to_string(&file_path).unwrap();
274 assert!(content.contains(r#""key": "value""#));
275 }
276
277 #[test]
278 fn test_write_json_file_dry_run_no_write() {
279 let temp = TempDir::new().unwrap();
280 let file_path = temp.path().join("output.json");
281 let value = serde_json::json!({"key": "value"});
282
283 write_json_file(&file_path, &value, true).unwrap();
284
285 assert!(!file_path.exists());
286 }
287
288 #[test]
289 fn test_write_json_file_creates_parent_dirs() {
290 let temp = TempDir::new().unwrap();
291 let file_path = temp.path().join("deep").join("nested").join("output.json");
292 let value = serde_json::json!({});
293
294 write_json_file(&file_path, &value, false).unwrap();
295
296 assert!(file_path.exists());
297 }
298
299 #[test]
300 fn test_json_obj_mut_on_object() {
301 let mut value = serde_json::json!({"key": "value"});
302
303 let obj = json_obj_mut(&mut value).unwrap();
304 obj.insert("new_key".to_string(), JsonValue::String("new_value".to_string()));
305
306 assert_eq!(value["new_key"], "new_value");
307 }
308
309 #[test]
310 fn test_json_obj_mut_on_non_object_error() {
311 let mut value = serde_json::json!("string");
312
313 let result = json_obj_mut(&mut value);
314
315 assert!(result.is_err());
316 }
317
318 #[test]
319 fn test_json_get_obj_mut_existing_key() {
320 let mut value = serde_json::json!({"servers": {"test": {}}});
321
322 let servers = json_get_obj_mut(&mut value, "servers").unwrap();
323
324 assert!(servers.contains_key("test"));
325 }
326
327 #[test]
328 fn test_json_get_obj_mut_creates_key() {
329 let mut value = serde_json::json!({});
330
331 let servers = json_get_obj_mut(&mut value, "servers").unwrap();
332 servers.insert("test".to_string(), serde_json::json!({}));
333
334 assert!(value["servers"]["test"].is_object());
335 }
336
337 #[test]
338 fn test_json_get_obj_mut_nested_key_not_object_error() {
339 let mut value = serde_json::json!({"servers": "not an object"});
340
341 let result = json_get_obj_mut(&mut value, "servers");
342
343 assert!(result.is_err());
344 }
345
346 #[test]
349 fn test_log_verbose_false_no_panic() {
350 log_verbose(false, "test message");
352 }
353
354 #[test]
355 fn test_log_verbose_true_no_panic() {
356 log_verbose(true, "test message");
358 }
359
360 #[test]
363 fn test_home_returns_path() {
364 let result = home();
365 assert!(result.is_ok());
367 assert!(result.unwrap().is_absolute());
368 }
369}
370