Skip to main content

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 + ?Sized>(
52    data: &T,
53) -> Result<(), Box<dyn std::error::Error>> {
54    let file_path = get_file_path();
55    let path = Path::new(&file_path);
56
57    // Get parent directory for temp file (must be on same filesystem for atomic rename)
58    let parent_dir = path.parent().unwrap_or(Path::new("."));
59
60    // Use a closure to ensure lock is released even on error
61    // Serialize data
62    let json = serde_json::to_string_pretty(data)?;
63
64    // Create temp file in the same directory (required for atomic rename)
65    let mut temp_file = NamedTempFile::new_in(parent_dir)?;
66
67    // Write JSON to temp file
68    temp_file.write_all(json.as_bytes())?;
69    temp_file.flush()?;
70
71    // Sync to disk to ensure durability
72    temp_file.as_file().sync_all()?;
73
74    // Atomically rename temp file to target (this is the atomic operation)
75    // persist() consumes the temp file and prevents auto-deletion
76    temp_file.persist(file_path)?;
77
78    Ok(())
79}
80
81/// Reads JSON data from file with shared file locking.
82///
83/// This function:
84/// 1. Reads and deserializes the JSON data
85///
86/// This prevents reading partially written data during concurrent access.
87pub fn read_json_from_file<T: serde::de::DeserializeOwned>() -> Result<T, Box<dyn std::error::Error>>
88{
89    let file_path = get_file_path();
90
91    // Open and read the actual data file
92    let mut file = match File::open(file_path) {
93        Ok(f) => f,
94        Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
95            return Err(Box::new(std::io::Error::new(
96                std::io::ErrorKind::NotFound,
97                format!("File not found: {}", file_path),
98            )));
99        }
100        Err(e) => return Err(Box::new(e)),
101    };
102
103    let mut json = String::new();
104    file.read_to_string(&mut json)?;
105
106    let data = serde_json::from_str(&json)?;
107    Ok(data)
108}
109
110pub fn confirm(prompt: &str) -> bool {
111    eprint!("{} (y/n): ", prompt);
112    io::stdout().flush().ok();
113    let mut response = String::new();
114    std::io::stdin().read_line(&mut response).ok();
115    response.to_lowercase().starts_with('y')
116}
117
118#[cfg(test)]
119pub mod tests {
120
121    use serial_test::serial;
122
123    #[test]
124    #[serial]
125    fn test_serial_01_read_write_json_from_file() {
126        let home = super::home_dir();
127
128        assert!(!home.is_empty());
129
130        std::env::set_var("COMAN_JSON", "test.json");
131
132        let path = "test.json".to_string();
133
134        assert_eq!(super::get_file_path(), path);
135
136        let result: Result<Vec<crate::models::collection::Collection>, Box<dyn std::error::Error>> =
137            super::read_json_from_file();
138
139        if let Err(e) = &result {
140            println!("Error: {}", e);
141        }
142
143        assert!(result.is_ok());
144
145        let result = super::write_json_to_file(&result.unwrap());
146
147        assert!(result.is_ok());
148    }
149}