agpm_cli/utils/fs/
formats.rs1use crate::core::file_error::{FileOperation, FileResultExt};
41use anyhow::{Context, Result};
42use std::fs;
43use std::path::Path;
44use tokio_retry::Retry;
45use tokio_retry::strategy::ExponentialBackoff;
46
47pub fn read_text_file(path: &Path) -> Result<String> {
58 Ok(fs::read_to_string(path).with_file_context(
59 FileOperation::Read,
60 path,
61 "reading text file",
62 "utils::fs::formats::read_text_file",
63 )?)
64}
65
66pub async fn read_text_file_with_retry(path: &Path) -> Result<String> {
87 let strategy = ExponentialBackoff::from_millis(10)
88 .max_delay(std::time::Duration::from_millis(200))
89 .take(5);
90
91 let path_buf = path.to_path_buf();
92 let path_for_error = path.to_path_buf();
93
94 Retry::spawn(strategy, || {
95 let path = path_buf.clone();
96 async move {
97 match tokio::fs::read_to_string(&path).await {
98 Ok(content) => Ok(content),
99 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
100 tracing::debug!(
101 target: "fs::retry",
102 "File not found at {}, will retry",
103 path.display()
104 );
105 Err(e)
106 }
107 Err(e) => {
109 tracing::warn!(
110 target: "fs::retry",
111 "Non-retryable error reading {}: {:?} (kind: {:?})",
112 path.display(),
113 e,
114 e.kind()
115 );
116 Ok(Err(e)?)
117 }
118 }
119 }
120 })
121 .await
122 .map_err(|e| {
123 tracing::warn!(
124 target: "fs::retry",
125 "All retries exhausted for {}: {:?} (kind: {:?})",
126 path_for_error.display(),
127 e,
128 e.kind()
129 );
130 let file_error = crate::core::file_error::FileOperationError::new(
131 crate::core::file_error::FileOperationContext::new(
132 FileOperation::Read,
133 &path_for_error,
134 "reading text file with retry".to_string(),
135 "utils::fs::formats::read_text_file_with_retry",
136 ),
137 e,
138 );
139 anyhow::Error::from(file_error)
140 })
141}
142
143pub fn write_text_file(path: &Path, content: &str) -> Result<()> {
155 super::atomic::safe_write(path, content)
156 .with_context(|| format!("Failed to write file: {}", path.display()))
157}
158
159pub fn read_json_file<T>(path: &Path) -> Result<T>
173where
174 T: serde::de::DeserializeOwned,
175{
176 let content = read_text_file(path)?;
177 serde_json::from_str(&content)
178 .with_context(|| format!("Failed to parse JSON from file: {}", path.display()))
179}
180
181pub fn write_json_file<T>(path: &Path, data: &T, pretty: bool) -> Result<()>
197where
198 T: serde::Serialize,
199{
200 let json = if pretty {
201 serde_json::to_string_pretty(data)?
202 } else {
203 serde_json::to_string(data)?
204 };
205
206 write_text_file(path, &json)
207 .with_context(|| format!("Failed to write JSON file: {}", path.display()))
208}
209
210pub fn read_toml_file<T>(path: &Path) -> Result<T>
224where
225 T: serde::de::DeserializeOwned,
226{
227 let content = read_text_file(path)?;
228 toml::from_str(&content)
229 .with_context(|| format!("Failed to parse TOML from file: {}", path.display()))
230}
231
232pub fn write_toml_file<T>(path: &Path, data: &T) -> Result<()>
250where
251 T: serde::Serialize,
252{
253 let toml = toml::to_string_pretty(data)
254 .with_context(|| format!("Failed to serialize data to TOML for: {}", path.display()))?;
255
256 write_text_file(path, &toml)
257 .with_context(|| format!("Failed to write TOML file: {}", path.display()))
258}
259
260pub fn read_yaml_file<T>(path: &Path) -> Result<T>
274where
275 T: serde::de::DeserializeOwned,
276{
277 let content = read_text_file(path)?;
278 serde_yaml::from_str(&content)
279 .with_context(|| format!("Failed to parse YAML from file: {}", path.display()))
280}
281
282pub fn write_yaml_file<T>(path: &Path, data: &T) -> Result<()>
297where
298 T: serde::Serialize,
299{
300 let yaml = serde_yaml::to_string(data)
301 .with_context(|| format!("Failed to serialize data to YAML for: {}", path.display()))?;
302
303 write_text_file(path, &yaml)
304 .with_context(|| format!("Failed to write YAML file: {}", path.display()))
305}
306
307pub fn create_temp_file(prefix: &str, content: &str) -> Result<tempfile::TempPath> {
319 let temp_file = tempfile::Builder::new().prefix(prefix).suffix(".tmp").tempfile()?;
320
321 let path = temp_file.into_temp_path();
322 write_text_file(&path, content)?;
323
324 Ok(path)
325}
326
327#[cfg(test)]
328mod tests {
329 use super::*;
330 use serde::{Deserialize, Serialize};
331 use tempfile::tempdir;
332
333 #[derive(Debug, Serialize, Deserialize, PartialEq)]
334 struct TestData {
335 name: String,
336 value: i32,
337 }
338
339 #[test]
340 fn test_read_write_text_file() {
341 let temp = tempdir().unwrap();
342 let path = temp.path().join("test.txt");
343
344 write_text_file(&path, "test content").unwrap();
345 let content = read_text_file(&path).unwrap();
346 assert_eq!(content, "test content");
347 }
348
349 #[test]
350 fn test_read_write_json_file() {
351 let temp = tempdir().unwrap();
352 let path = temp.path().join("test.json");
353
354 let data = TestData {
355 name: "test".to_string(),
356 value: 42,
357 };
358
359 write_json_file(&path, &data, true).unwrap();
360 let loaded: TestData = read_json_file(&path).unwrap();
361 assert_eq!(loaded, data);
362 }
363
364 #[test]
365 fn test_read_write_json_file_compact() {
366 let temp = tempdir().unwrap();
367 let path = temp.path().join("test.json");
368
369 let data = TestData {
370 name: "test".to_string(),
371 value: 42,
372 };
373
374 write_json_file(&path, &data, false).unwrap();
375 let content = read_text_file(&path).unwrap();
376 assert!(!content.contains('\n')); let loaded: TestData = read_json_file(&path).unwrap();
378 assert_eq!(loaded, data);
379 }
380
381 #[test]
382 fn test_read_write_toml_file() {
383 let temp = tempdir().unwrap();
384 let path = temp.path().join("test.toml");
385
386 let data = TestData {
387 name: "test".to_string(),
388 value: 42,
389 };
390
391 write_toml_file(&path, &data).unwrap();
392 let loaded: TestData = read_toml_file(&path).unwrap();
393 assert_eq!(loaded, data);
394 }
395
396 #[test]
397 fn test_read_write_yaml_file() {
398 let temp = tempdir().unwrap();
399 let path = temp.path().join("test.yaml");
400
401 let data = TestData {
402 name: "test".to_string(),
403 value: 42,
404 };
405
406 write_yaml_file(&path, &data).unwrap();
407 let loaded: TestData = read_yaml_file(&path).unwrap();
408 assert_eq!(loaded, data);
409 }
410
411 #[test]
412 fn test_create_temp_file() {
413 let temp_file = create_temp_file("test", "content").unwrap();
414 assert!(temp_file.exists());
415
416 let content = read_text_file(&temp_file).unwrap();
417 assert_eq!(content, "content");
418
419 let path = temp_file.to_path_buf();
420 drop(temp_file);
421 assert!(!path.exists()); }
423
424 #[test]
425 fn test_read_nonexistent_file() {
426 let result = read_text_file(Path::new("/nonexistent/file.txt"));
427 assert!(result.is_err());
428 }
429
430 #[test]
431 fn test_write_creates_parent_directories() {
432 let temp = tempdir().unwrap();
433 let path = temp.path().join("nested").join("dirs").join("file.txt");
434
435 write_text_file(&path, "content").unwrap();
436 assert!(path.exists());
437 assert_eq!(read_text_file(&path).unwrap(), "content");
438 }
439
440 #[test]
441 fn test_json_parse_error() {
442 let temp = tempdir().unwrap();
443 let path = temp.path().join("invalid.json");
444
445 write_text_file(&path, "not valid json").unwrap();
446 let result: Result<TestData> = read_json_file(&path);
447 assert!(result.is_err());
448 }
449
450 #[test]
451 fn test_toml_parse_error() {
452 let temp = tempdir().unwrap();
453 let path = temp.path().join("invalid.toml");
454
455 write_text_file(&path, "not = valid = toml").unwrap();
456 let result: Result<TestData> = read_toml_file(&path);
457 assert!(result.is_err());
458 }
459
460 #[test]
461 fn test_yaml_parse_error() {
462 let temp = tempdir().unwrap();
463 let path = temp.path().join("invalid.yaml");
464
465 write_text_file(&path, "not: valid: yaml: [").unwrap();
466 let result: Result<TestData> = read_yaml_file(&path);
467 assert!(result.is_err());
468 }
469}