next_plaid_cli/index/
state.rs1use anyhow::Result;
2use serde::{Deserialize, Serialize};
3use std::collections::HashMap;
4use std::fs;
5use std::path::{Path, PathBuf};
6use std::time::SystemTime;
7use xxhash_rust::xxh3::xxh3_64;
8
9use super::paths::get_state_path;
10
11#[derive(Debug, Clone, Serialize, Deserialize, Default)]
12pub struct IndexState {
13 #[serde(default)]
15 pub cli_version: String,
16 pub files: HashMap<PathBuf, FileInfo>,
17 #[serde(default)]
19 pub search_count: u64,
20}
21
22#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct FileInfo {
24 pub content_hash: u64,
25 pub mtime: u64,
26}
27
28impl IndexState {
29 pub fn load(index_dir: &Path) -> Result<Self> {
31 let state_path = get_state_path(index_dir);
32 if state_path.exists() {
33 let content = fs::read_to_string(&state_path)?;
34 Ok(serde_json::from_str(&content)?)
35 } else {
36 Ok(Self::default())
37 }
38 }
39
40 pub fn save(&self, index_dir: &Path) -> Result<()> {
42 fs::create_dir_all(index_dir)?;
43
44 let mut state = self.clone();
46 state.cli_version = env!("CARGO_PKG_VERSION").to_string();
47
48 let state_path = get_state_path(index_dir);
49 let content = serde_json::to_string_pretty(&state)?;
50 fs::write(&state_path, content)?;
51 Ok(())
52 }
53
54 pub fn increment_search_count(&mut self) {
56 self.search_count += 1;
57 }
58
59 pub fn reset_search_count(&mut self) {
61 self.search_count = 0;
62 }
63}
64
65pub fn hash_file(path: &Path) -> Result<u64> {
67 let content = fs::read(path)?;
68 Ok(xxh3_64(&content))
69}
70
71pub fn get_mtime(path: &Path) -> Result<u64> {
73 let metadata = fs::metadata(path)?;
74 let mtime = metadata
75 .modified()?
76 .duration_since(SystemTime::UNIX_EPOCH)?
77 .as_secs();
78 Ok(mtime)
79}
80
81#[cfg(test)]
82mod tests {
83 use super::*;
84 use std::io::Write;
85 use tempfile::TempDir;
86
87 #[test]
88 fn test_index_state_default() {
89 let state = IndexState::default();
90 assert!(state.cli_version.is_empty());
91 assert!(state.files.is_empty());
92 }
93
94 #[test]
95 fn test_file_info_serialization() {
96 let info = FileInfo {
97 content_hash: 12345678901234567890,
98 mtime: 1700000000,
99 };
100
101 let json = serde_json::to_string(&info).unwrap();
102 assert!(json.contains("12345678901234567890"));
103 assert!(json.contains("1700000000"));
104
105 let deserialized: FileInfo = serde_json::from_str(&json).unwrap();
106 assert_eq!(deserialized.content_hash, 12345678901234567890);
107 assert_eq!(deserialized.mtime, 1700000000);
108 }
109
110 #[test]
111 fn test_index_state_serialization() {
112 let mut files = HashMap::new();
113 files.insert(
114 PathBuf::from("src/main.rs"),
115 FileInfo {
116 content_hash: 123456,
117 mtime: 1700000000,
118 },
119 );
120 let state = IndexState {
121 cli_version: "1.0.0".to_string(),
122 files,
123 search_count: 0,
124 };
125
126 let json = serde_json::to_string(&state).unwrap();
127 assert!(json.contains("1.0.0"));
128 assert!(json.contains("src/main.rs"));
129
130 let deserialized: IndexState = serde_json::from_str(&json).unwrap();
131 assert_eq!(deserialized.cli_version, "1.0.0");
132 assert!(deserialized
133 .files
134 .contains_key(&PathBuf::from("src/main.rs")));
135 }
136
137 #[test]
138 fn test_index_state_load_nonexistent() {
139 let temp_dir = TempDir::new().unwrap();
140 let result = IndexState::load(temp_dir.path());
141 assert!(result.is_ok());
142 let state = result.unwrap();
143 assert!(state.files.is_empty());
144 }
145
146 #[test]
147 fn test_index_state_save_and_load() {
148 let temp_dir = TempDir::new().unwrap();
149
150 let mut state = IndexState::default();
151 state.files.insert(
152 PathBuf::from("test.rs"),
153 FileInfo {
154 content_hash: 999999,
155 mtime: 1700000000,
156 },
157 );
158
159 state.save(temp_dir.path()).unwrap();
161
162 let loaded = IndexState::load(temp_dir.path()).unwrap();
164 assert!(loaded.files.contains_key(&PathBuf::from("test.rs")));
165 let file_info = loaded.files.get(&PathBuf::from("test.rs")).unwrap();
166 assert_eq!(file_info.content_hash, 999999);
167
168 assert!(!loaded.cli_version.is_empty());
170 }
171
172 #[test]
173 fn test_hash_file() {
174 let temp_dir = TempDir::new().unwrap();
175 let file_path = temp_dir.path().join("test.txt");
176
177 let mut file = fs::File::create(&file_path).unwrap();
179 file.write_all(b"Hello, World!").unwrap();
180
181 let hash = hash_file(&file_path).unwrap();
182 assert!(hash > 0);
183
184 let hash2 = hash_file(&file_path).unwrap();
186 assert_eq!(hash, hash2);
187 }
188
189 #[test]
190 fn test_hash_file_different_content() {
191 let temp_dir = TempDir::new().unwrap();
192
193 let file1 = temp_dir.path().join("file1.txt");
194 let file2 = temp_dir.path().join("file2.txt");
195
196 fs::write(&file1, "Content A").unwrap();
197 fs::write(&file2, "Content B").unwrap();
198
199 let hash1 = hash_file(&file1).unwrap();
200 let hash2 = hash_file(&file2).unwrap();
201
202 assert_ne!(hash1, hash2);
203 }
204
205 #[test]
206 fn test_get_mtime() {
207 let temp_dir = TempDir::new().unwrap();
208 let file_path = temp_dir.path().join("test.txt");
209
210 fs::write(&file_path, "test content").unwrap();
211
212 let mtime = get_mtime(&file_path).unwrap();
213 assert!(mtime > 946684800); }
216
217 #[test]
218 fn test_hash_file_nonexistent() {
219 let result = hash_file(Path::new("/nonexistent/file.txt"));
220 assert!(result.is_err());
221 }
222
223 #[test]
224 fn test_get_mtime_nonexistent() {
225 let result = get_mtime(Path::new("/nonexistent/file.txt"));
226 assert!(result.is_err());
227 }
228
229 #[test]
230 fn test_search_count_increment_and_reset() {
231 let temp_dir = TempDir::new().unwrap();
232
233 let mut state = IndexState::default();
235 assert_eq!(state.search_count, 0);
236
237 state.increment_search_count();
239 assert_eq!(state.search_count, 1);
240
241 state.increment_search_count();
242 state.increment_search_count();
243 assert_eq!(state.search_count, 3);
244
245 state.save(temp_dir.path()).unwrap();
247 let loaded = IndexState::load(temp_dir.path()).unwrap();
248 assert_eq!(loaded.search_count, 3);
249
250 let mut loaded = loaded;
252 loaded.reset_search_count();
253 assert_eq!(loaded.search_count, 0);
254
255 loaded.save(temp_dir.path()).unwrap();
257 let reloaded = IndexState::load(temp_dir.path()).unwrap();
258 assert_eq!(reloaded.search_count, 0);
259 }
260}