1use std::collections::HashMap;
6use std::fs;
7use std::path::{Path, PathBuf};
8
9use crate::error::{SyncError, SyncResult};
10use crate::ipynb::{CellOutput, OutputData};
11
12pub struct OutputCache {
14 cache_dir: PathBuf,
16
17 outputs: HashMap<String, CellOutput>,
19
20 execution_count: u32,
22}
23
24impl OutputCache {
25 pub fn new(cache_dir: impl AsRef<Path>) -> SyncResult<Self> {
27 let cache_dir = cache_dir.as_ref().to_path_buf();
28 fs::create_dir_all(&cache_dir)?;
29
30 let mut cache = Self {
31 cache_dir,
32 outputs: HashMap::new(),
33 execution_count: 0,
34 };
35
36 cache.load_from_disk()?;
37 Ok(cache)
38 }
39
40 fn next_execution_count(&mut self) -> u32 {
42 self.execution_count += 1;
43 self.execution_count
44 }
45
46 fn make_execute_result(&mut self, data: OutputData) -> CellOutput {
48 CellOutput::ExecuteResult {
49 execution_count: self.next_execution_count(),
50 data,
51 metadata: serde_json::json!({}),
52 }
53 }
54
55 pub fn get_output(&self, cell_name: &str) -> Option<CellOutput> {
57 self.outputs.get(cell_name).cloned()
58 }
59
60 pub fn store_text(&mut self, cell_name: &str, text: &str) {
62 let data = OutputData {
63 text_plain: Some(vec![text.to_string()]),
64 ..Default::default()
65 };
66 let output = self.make_execute_result(data);
67 self.outputs.insert(cell_name.to_string(), output);
68 }
69
70 pub fn store_html(&mut self, cell_name: &str, html: &str) {
72 let data = OutputData {
73 text_html: Some(html.lines().map(String::from).collect()),
74 text_plain: Some(vec!["[HTML Output]".to_string()]),
75 ..Default::default()
76 };
77 let output = self.make_execute_result(data);
78 self.outputs.insert(cell_name.to_string(), output);
79 }
80
81 pub fn store_png(&mut self, cell_name: &str, png_data: &[u8]) {
83 use base64::Engine;
84 let encoded = base64::engine::general_purpose::STANDARD.encode(png_data);
85
86 let data = OutputData {
87 image_png: Some(encoded),
88 text_plain: Some(vec!["[Image]".to_string()]),
89 ..Default::default()
90 };
91 let output = self.make_execute_result(data);
92 self.outputs.insert(cell_name.to_string(), output);
93 }
94
95 pub fn store_svg(&mut self, cell_name: &str, svg: &str) {
97 let data = OutputData {
98 image_svg: Some(svg.lines().map(String::from).collect()),
99 text_plain: Some(vec!["[SVG Image]".to_string()]),
100 ..Default::default()
101 };
102 let output = self.make_execute_result(data);
103 self.outputs.insert(cell_name.to_string(), output);
104 }
105
106 pub fn store_json(&mut self, cell_name: &str, json: serde_json::Value) {
108 let text_repr = serde_json::to_string_pretty(&json).unwrap_or_default();
109 let data = OutputData {
110 application_json: Some(json),
111 text_plain: Some(vec![text_repr]),
112 ..Default::default()
113 };
114 let output = self.make_execute_result(data);
115 self.outputs.insert(cell_name.to_string(), output);
116 }
117
118 pub fn store_error(&mut self, cell_name: &str, error: &str) {
120 let output = CellOutput::Error {
121 ename: "ExecutionError".to_string(),
122 evalue: error.to_string(),
123 traceback: error.lines().map(String::from).collect(),
124 };
125 self.outputs.insert(cell_name.to_string(), output);
126 }
127
128 pub fn clear(&mut self) {
130 self.outputs.clear();
131 }
132
133 pub fn save_to_disk(&self) -> SyncResult<()> {
135 for (name, output) in &self.outputs {
136 let path = self.output_path(name);
137 let json = serde_json::to_string_pretty(output)?;
138 fs::write(&path, json).map_err(|e| SyncError::WriteError {
139 path: path.clone(),
140 message: e.to_string(),
141 })?;
142 }
143 Ok(())
144 }
145
146 fn load_from_disk(&mut self) -> SyncResult<()> {
148 if !self.cache_dir.exists() {
149 return Ok(());
150 }
151
152 for entry in fs::read_dir(&self.cache_dir)? {
153 let entry = entry?;
154 let path = entry.path();
155
156 if path.extension().is_some_and(|e| e == "json")
157 && let Some(name) = path.file_stem().and_then(|s| s.to_str())
158 && let Ok(content) = fs::read_to_string(&path)
159 && let Ok(output) = serde_json::from_str(&content)
160 {
161 self.outputs.insert(name.to_string(), output);
162 }
163 }
164
165 Ok(())
166 }
167
168 fn output_path(&self, cell_name: &str) -> PathBuf {
170 self.cache_dir.join(format!("{}.json", cell_name))
171 }
172}
173
174#[cfg(test)]
175mod tests {
176 use super::*;
177
178 #[test]
179 fn test_store_and_retrieve_text() {
180 let temp = tempfile::TempDir::new().unwrap();
181 let mut cache = OutputCache::new(temp.path()).unwrap();
182
183 cache.store_text("hello", "Hello, world!");
184
185 let output = cache.get_output("hello").unwrap();
186 match output {
187 CellOutput::ExecuteResult { data, .. } => {
188 assert!(data.text_plain.is_some());
189 assert!(data.text_plain.unwrap()[0].contains("Hello"));
190 }
191 _ => panic!("Expected ExecuteResult"),
192 }
193 }
194
195 #[test]
196 fn test_store_and_retrieve_html() {
197 let temp = tempfile::TempDir::new().unwrap();
198 let mut cache = OutputCache::new(temp.path()).unwrap();
199
200 cache.store_html("table", "<table><tr><td>Data</td></tr></table>");
201
202 let output = cache.get_output("table").unwrap();
203 match output {
204 CellOutput::ExecuteResult { data, .. } => {
205 assert!(data.text_html.is_some());
206 }
207 _ => panic!("Expected ExecuteResult"),
208 }
209 }
210
211 #[test]
212 fn test_persist_and_reload() {
213 let temp = tempfile::TempDir::new().unwrap();
214
215 {
217 let mut cache = OutputCache::new(temp.path()).unwrap();
218 cache.store_text("test", "Test output");
219 cache.save_to_disk().unwrap();
220 }
221
222 {
224 let cache = OutputCache::new(temp.path()).unwrap();
225 assert!(cache.get_output("test").is_some());
226 }
227 }
228}