buswatch_tui/source/
file.rs1use std::fs;
6use std::path::{Path, PathBuf};
7use std::time::SystemTime;
8
9use super::{DataSource, Snapshot};
10
11#[derive(Debug)]
19pub struct FileSource {
20 path: PathBuf,
21 description: String,
22 last_error: Option<String>,
23 last_modified: Option<SystemTime>,
24 cached_snapshot: Option<Snapshot>,
26}
27
28impl FileSource {
29 pub fn new<P: AsRef<Path>>(path: P) -> Self {
31 let path = path.as_ref().to_path_buf();
32 let description = format!("file: {}", path.display());
33 Self {
34 path,
35 description,
36 last_error: None,
37 last_modified: None,
38 cached_snapshot: None,
39 }
40 }
41
42 pub fn path(&self) -> &Path {
44 &self.path
45 }
46
47 fn get_modified_time(&self) -> Option<SystemTime> {
49 fs::metadata(&self.path).ok()?.modified().ok()
50 }
51
52 fn read_file(&mut self) -> Option<Snapshot> {
54 match fs::read_to_string(&self.path) {
55 Ok(content) => match serde_json::from_str(&content) {
56 Ok(snapshot) => {
57 self.last_error = None;
58 Some(snapshot)
59 }
60 Err(e) => {
61 self.last_error = Some(format!("Parse error: {}", e));
62 None
63 }
64 },
65 Err(e) => {
66 self.last_error = Some(format!("Read error: {}", e));
67 None
68 }
69 }
70 }
71}
72
73impl DataSource for FileSource {
74 fn poll(&mut self) -> Option<Snapshot> {
75 let current_modified = self.get_modified_time();
76
77 let file_changed = match (&self.last_modified, ¤t_modified) {
79 (None, _) => true, (Some(_), None) => false, (Some(last), Some(current)) => current > last,
82 };
83
84 if file_changed {
85 if let Some(snapshot) = self.read_file() {
86 self.last_modified = current_modified;
87 self.cached_snapshot = Some(snapshot.clone());
88 return Some(snapshot);
89 }
90 }
91
92 None
93 }
94
95 fn description(&self) -> &str {
96 &self.description
97 }
98
99 fn error(&self) -> Option<&str> {
100 self.last_error.as_deref()
101 }
102}
103
104#[cfg(test)]
105mod tests {
106 use super::*;
107 use std::io::{Seek, Write};
108 use tempfile::NamedTempFile;
109
110 fn sample_json() -> &'static str {
111 r#"{
112 "version": { "major": 1, "minor": 0 },
113 "timestamp_ms": 1703160000000,
114 "modules": {
115 "TestModule": {
116 "reads": {
117 "input": { "count": 100, "backlog": 5 }
118 },
119 "writes": {
120 "output": { "count": 50 }
121 }
122 }
123 }
124 }"#
125 }
126
127 #[test]
128 fn test_file_source_new() {
129 let source = FileSource::new("/tmp/test.json");
130 assert_eq!(source.path(), Path::new("/tmp/test.json"));
131 assert_eq!(source.description(), "file: /tmp/test.json");
132 assert!(source.error().is_none());
133 }
134
135 #[test]
136 fn test_file_source_poll_reads_file() {
137 let mut file = NamedTempFile::new().unwrap();
138 writeln!(file, "{}", sample_json()).unwrap();
139
140 let mut source = FileSource::new(file.path());
141
142 let snapshot = source.poll();
144 assert!(snapshot.is_some());
145 let snapshot = snapshot.unwrap();
146 assert!(snapshot.modules.contains_key("TestModule"));
147
148 let snapshot2 = source.poll();
150 assert!(snapshot2.is_none());
151 }
152
153 #[test]
154 fn test_file_source_detects_changes() {
155 let mut file = NamedTempFile::new().unwrap();
156 writeln!(file, "{}", sample_json()).unwrap();
157
158 let mut source = FileSource::new(file.path());
159
160 let _ = source.poll();
162
163 std::thread::sleep(std::time::Duration::from_millis(10));
165 file.rewind().unwrap();
166 writeln!(
167 file,
168 r#"{{
169 "version": {{ "major": 1, "minor": 0 }},
170 "timestamp_ms": 1703160000000,
171 "modules": {{
172 "ModifiedModule": {{
173 "reads": {{}},
174 "writes": {{}}
175 }}
176 }}
177 }}"#
178 )
179 .unwrap();
180 file.flush().unwrap();
181
182 let _ = std::fs::File::open(file.path());
184
185 let snapshot = source.poll();
188 if let Some(s) = snapshot {
189 assert!(s.modules.contains_key("ModifiedModule"));
190 }
191 }
192
193 #[test]
194 fn test_file_source_missing_file() {
195 let mut source = FileSource::new("/nonexistent/path/monitor.json");
196
197 let snapshot = source.poll();
198 assert!(snapshot.is_none());
199 assert!(source.error().is_some());
200 assert!(source.error().unwrap().contains("Read error"));
201 }
202
203 #[test]
204 fn test_file_source_invalid_json() {
205 let mut file = NamedTempFile::new().unwrap();
206 writeln!(file, "not valid json").unwrap();
207
208 let mut source = FileSource::new(file.path());
209
210 let snapshot = source.poll();
211 assert!(snapshot.is_none());
212 assert!(source.error().is_some());
213 assert!(source.error().unwrap().contains("Parse error"));
214 }
215}