envelope_cli/storage/
file_io.rs1use std::fs::{self, File};
6use std::io::{BufReader, BufWriter, Write};
7use std::path::Path;
8
9use serde::{de::DeserializeOwned, Serialize};
10
11use crate::error::EnvelopeError;
12
13pub fn read_json<T, P>(path: P) -> Result<T, EnvelopeError>
15where
16 T: DeserializeOwned + Default,
17 P: AsRef<Path>,
18{
19 let path = path.as_ref();
20
21 if !path.exists() {
22 return Ok(T::default());
23 }
24
25 let file = File::open(path)
26 .map_err(|e| EnvelopeError::Storage(format!("Failed to open {}: {}", path.display(), e)))?;
27
28 let reader = BufReader::new(file);
29 serde_json::from_reader(reader)
30 .map_err(|e| EnvelopeError::Storage(format!("Failed to parse {}: {}", path.display(), e)))
31}
32
33pub fn read_json_required<T, P>(path: P) -> Result<T, EnvelopeError>
35where
36 T: DeserializeOwned,
37 P: AsRef<Path>,
38{
39 let path = path.as_ref();
40
41 if !path.exists() {
42 return Err(EnvelopeError::Storage(format!(
43 "File not found: {}",
44 path.display()
45 )));
46 }
47
48 let file = File::open(path)
49 .map_err(|e| EnvelopeError::Storage(format!("Failed to open {}: {}", path.display(), e)))?;
50
51 let reader = BufReader::new(file);
52 serde_json::from_reader(reader)
53 .map_err(|e| EnvelopeError::Storage(format!("Failed to parse {}: {}", path.display(), e)))
54}
55
56pub fn write_json_atomic<T, P>(path: P, data: &T) -> Result<(), EnvelopeError>
61where
62 T: Serialize,
63 P: AsRef<Path>,
64{
65 let path = path.as_ref();
66
67 if let Some(parent) = path.parent() {
69 fs::create_dir_all(parent).map_err(|e| {
70 EnvelopeError::Storage(format!(
71 "Failed to create directory {}: {}",
72 parent.display(),
73 e
74 ))
75 })?;
76 }
77
78 let temp_path = path.with_extension("json.tmp");
80
81 let file = File::create(&temp_path)
83 .map_err(|e| EnvelopeError::Storage(format!("Failed to create temp file: {}", e)))?;
84
85 let mut writer = BufWriter::new(file);
86 serde_json::to_writer_pretty(&mut writer, data)
87 .map_err(|e| EnvelopeError::Storage(format!("Failed to serialize data: {}", e)))?;
88
89 writer
90 .flush()
91 .map_err(|e| EnvelopeError::Storage(format!("Failed to flush data: {}", e)))?;
92
93 writer
95 .get_ref()
96 .sync_all()
97 .map_err(|e| EnvelopeError::Storage(format!("Failed to sync data: {}", e)))?;
98
99 fs::rename(&temp_path, path).map_err(|e| {
101 let _ = fs::remove_file(&temp_path);
103 EnvelopeError::Storage(format!("Failed to rename temp file: {}", e))
104 })?;
105
106 Ok(())
107}
108
109pub fn json_file_valid<P: AsRef<Path>>(path: P) -> bool {
111 let path = path.as_ref();
112 if !path.exists() {
113 return false;
114 }
115
116 if let Ok(file) = File::open(path) {
118 let reader = BufReader::new(file);
119 serde_json::from_reader::<_, serde_json::Value>(reader).is_ok()
120 } else {
121 false
122 }
123}
124
125#[cfg(test)]
126mod tests {
127 use super::*;
128 use serde::{Deserialize, Serialize};
129 use tempfile::TempDir;
130
131 #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
132 struct TestData {
133 name: String,
134 value: i32,
135 }
136
137 #[test]
138 fn test_read_nonexistent_returns_default() {
139 let temp_dir = TempDir::new().unwrap();
140 let path = temp_dir.path().join("nonexistent.json");
141
142 let data: TestData = read_json(&path).unwrap();
143 assert_eq!(data, TestData::default());
144 }
145
146 #[test]
147 fn test_write_and_read() {
148 let temp_dir = TempDir::new().unwrap();
149 let path = temp_dir.path().join("test.json");
150
151 let data = TestData {
152 name: "test".to_string(),
153 value: 42,
154 };
155
156 write_json_atomic(&path, &data).unwrap();
157 assert!(path.exists());
158
159 let loaded: TestData = read_json(&path).unwrap();
160 assert_eq!(data, loaded);
161 }
162
163 #[test]
164 fn test_atomic_write_no_temp_file_left() {
165 let temp_dir = TempDir::new().unwrap();
166 let path = temp_dir.path().join("test.json");
167 let temp_path = temp_dir.path().join("test.json.tmp");
168
169 let data = TestData {
170 name: "test".to_string(),
171 value: 42,
172 };
173
174 write_json_atomic(&path, &data).unwrap();
175
176 assert!(path.exists());
177 assert!(!temp_path.exists());
178 }
179
180 #[test]
181 fn test_write_creates_parent_directories() {
182 let temp_dir = TempDir::new().unwrap();
183 let path = temp_dir.path().join("nested").join("dir").join("test.json");
184
185 let data = TestData {
186 name: "test".to_string(),
187 value: 42,
188 };
189
190 write_json_atomic(&path, &data).unwrap();
191 assert!(path.exists());
192 }
193
194 #[test]
195 fn test_json_file_valid() {
196 let temp_dir = TempDir::new().unwrap();
197 let valid_path = temp_dir.path().join("valid.json");
198 let invalid_path = temp_dir.path().join("invalid.json");
199 let nonexistent_path = temp_dir.path().join("nonexistent.json");
200
201 fs::write(&valid_path, r#"{"name": "test"}"#).unwrap();
203 assert!(json_file_valid(&valid_path));
204
205 fs::write(&invalid_path, "not json at all").unwrap();
207 assert!(!json_file_valid(&invalid_path));
208
209 assert!(!json_file_valid(&nonexistent_path));
211 }
212
213 #[test]
214 fn test_read_json_required() {
215 let temp_dir = TempDir::new().unwrap();
216 let path = temp_dir.path().join("test.json");
217
218 assert!(read_json_required::<TestData, _>(&path).is_err());
220
221 let data = TestData {
223 name: "test".to_string(),
224 value: 42,
225 };
226 write_json_atomic(&path, &data).unwrap();
227
228 let loaded: TestData = read_json_required(&path).unwrap();
229 assert_eq!(data, loaded);
230 }
231}