coman/
helper.rs

1use std::{
2    env,
3    fs::File,
4    io::{self, Read, Write},
5    path::Path,
6};
7
8use std::sync::OnceLock;
9use tempfile::NamedTempFile;
10
11pub static COMAN_FILE: &str = "coman.json";
12
13pub fn home_dir() -> &'static str {
14    static CACHE: OnceLock<String> = OnceLock::new();
15
16    CACHE.get_or_init(|| {
17        env::var("HOME")
18            .or_else(|_| env::var("USERPROFILE"))
19            .unwrap_or("/".to_string())
20    })
21}
22
23pub fn coman_json() -> &'static str {
24    static CACHE: OnceLock<String> = OnceLock::new();
25
26    CACHE.get_or_init(|| env::var("COMAN_JSON").unwrap_or_else(|_| COMAN_FILE.to_string()))
27}
28
29pub fn get_file_path() -> &'static str {
30    static CACHE: OnceLock<&'static str> = OnceLock::new();
31
32    CACHE.get_or_init(|| {
33        let json_path = coman_json();
34        // If env var was set (different from default), use it directly as full path
35        if json_path != COMAN_FILE {
36            json_path
37        } else {
38            // Leak the formatted string to get &'static str
39            Box::leak(format!("{}/{}", home_dir(), json_path).into_boxed_str())
40        }
41    })
42}
43
44/// Atomically writes JSON data to file with file locking.
45///
46/// This function:
47/// 1. Writes data to a temporary file in the same directory
48/// 2. Atomically renames the temp file to the target file
49///
50/// This ensures file integrity even if the process is interrupted.
51pub fn write_json_to_file<T: serde::Serialize>(data: &T) -> Result<(), Box<dyn std::error::Error>> {
52    let file_path = get_file_path();
53    let path = Path::new(&file_path);
54
55    // Get parent directory for temp file (must be on same filesystem for atomic rename)
56    let parent_dir = path.parent().unwrap_or(Path::new("."));
57
58    // Use a closure to ensure lock is released even on error
59    // Serialize data
60    let json = serde_json::to_string_pretty(data)?;
61
62    // Create temp file in the same directory (required for atomic rename)
63    let mut temp_file = NamedTempFile::new_in(parent_dir)?;
64
65    // Write JSON to temp file
66    temp_file.write_all(json.as_bytes())?;
67    temp_file.flush()?;
68
69    // Sync to disk to ensure durability
70    temp_file.as_file().sync_all()?;
71
72    // Atomically rename temp file to target (this is the atomic operation)
73    // persist() consumes the temp file and prevents auto-deletion
74    temp_file.persist(file_path)?;
75
76    Ok(())
77}
78
79/// Reads JSON data from file with shared file locking.
80///
81/// This function:
82/// 1. Reads and deserializes the JSON data
83///
84/// This prevents reading partially written data during concurrent access.
85pub fn read_json_from_file<T: serde::de::DeserializeOwned>() -> Result<T, Box<dyn std::error::Error>>
86{
87    let file_path = get_file_path();
88
89    // Open and read the actual data file
90    let mut file = match File::open(file_path) {
91        Ok(f) => f,
92        Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
93            return Err(Box::new(std::io::Error::new(
94                std::io::ErrorKind::NotFound,
95                format!("File not found: {}", file_path),
96            )));
97        }
98        Err(e) => return Err(Box::new(e)),
99    };
100
101    let mut json = String::new();
102    file.read_to_string(&mut json)?;
103
104    let data = serde_json::from_str(&json)?;
105    Ok(data)
106}
107
108pub fn confirm(prompt: &str) -> bool {
109    eprint!("{} (y/n): ", prompt);
110    io::stdout().flush().ok();
111    let mut response = String::new();
112    std::io::stdin().read_line(&mut response).ok();
113    response.to_lowercase().starts_with('y')
114}
115
116#[cfg(test)]
117pub mod tests {
118
119    use serial_test::serial;
120
121    #[test]
122    #[serial]
123    fn test_serial_01_read_write_json_from_file() {
124        let home = super::home_dir();
125
126        assert!(!home.is_empty());
127
128        std::env::set_var("COMAN_JSON", "test.json");
129
130        let path = "test.json".to_string();
131
132        assert_eq!(super::get_file_path(), path);
133
134        let result: Result<Vec<crate::models::collection::Collection>, Box<dyn std::error::Error>> =
135            super::read_json_from_file();
136
137        if let Err(e) = &result {
138            println!("Error: {}", e);
139        }
140
141        assert!(result.is_ok());
142
143        let result = super::write_json_to_file(&result.unwrap());
144
145        assert!(result.is_ok());
146    }
147}