1use std::fs;
6use std::path::Path;
7
8use serde::{Deserialize, Serialize};
9
10use crate::error::{SyncError, SyncResult};
11use crate::outputs::OutputCache;
12use crate::parser::{CellType, NotebookCell, NotebookMetadata};
13
14#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct JupyterNotebook {
17 pub metadata: JupyterMetadata,
19
20 pub nbformat: u32,
22
23 pub nbformat_minor: u32,
25
26 pub cells: Vec<JupyterCell>,
28}
29
30#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct JupyterMetadata {
33 pub kernelspec: KernelSpec,
35
36 pub language_info: LanguageInfo,
38
39 #[serde(skip_serializing_if = "Option::is_none")]
41 pub venus: Option<VenusMetadata>,
42}
43
44#[derive(Debug, Clone, Serialize, Deserialize)]
46pub struct KernelSpec {
47 pub display_name: String,
49
50 pub language: String,
52
53 pub name: String,
55}
56
57#[derive(Debug, Clone, Serialize, Deserialize)]
59pub struct LanguageInfo {
60 pub file_extension: String,
62
63 pub mimetype: String,
65
66 pub name: String,
68
69 pub version: String,
71}
72
73#[derive(Debug, Clone, Serialize, Deserialize)]
75pub struct VenusMetadata {
76 #[serde(skip_serializing_if = "Option::is_none")]
78 pub source_file: Option<String>,
79
80 pub version: String,
82}
83
84#[derive(Debug, Clone, Serialize, Deserialize)]
86pub struct JupyterCell {
87 pub cell_type: String,
89
90 pub metadata: CellMetadata,
92
93 pub source: Vec<String>,
95
96 #[serde(skip_serializing_if = "Option::is_none")]
98 pub outputs: Option<Vec<CellOutput>>,
99
100 #[serde(skip_serializing_if = "Option::is_none")]
102 pub execution_count: Option<u32>,
103}
104
105#[derive(Debug, Clone, Default, Serialize, Deserialize)]
107pub struct CellMetadata {
108 #[serde(skip_serializing_if = "Option::is_none")]
110 pub venus_cell: Option<String>,
111
112 #[serde(skip_serializing_if = "Option::is_none")]
114 pub editable: Option<bool>,
115
116 #[serde(skip_serializing_if = "Option::is_none")]
118 pub tags: Option<Vec<String>>,
119}
120
121#[derive(Debug, Clone, Serialize, Deserialize)]
123#[serde(tag = "output_type")]
124pub enum CellOutput {
125 #[serde(rename = "stream")]
127 Stream { name: String, text: Vec<String> },
128
129 #[serde(rename = "execute_result")]
131 ExecuteResult {
132 execution_count: u32,
133 data: OutputData,
134 metadata: serde_json::Value,
135 },
136
137 #[serde(rename = "display_data")]
139 DisplayData {
140 data: OutputData,
141 metadata: serde_json::Value,
142 },
143
144 #[serde(rename = "error")]
146 Error {
147 ename: String,
148 evalue: String,
149 traceback: Vec<String>,
150 },
151}
152
153#[derive(Debug, Clone, Default, Serialize, Deserialize)]
155pub struct OutputData {
156 #[serde(rename = "text/plain", skip_serializing_if = "Option::is_none")]
158 pub text_plain: Option<Vec<String>>,
159
160 #[serde(rename = "text/html", skip_serializing_if = "Option::is_none")]
162 pub text_html: Option<Vec<String>>,
163
164 #[serde(rename = "image/png", skip_serializing_if = "Option::is_none")]
166 pub image_png: Option<String>,
167
168 #[serde(rename = "image/svg+xml", skip_serializing_if = "Option::is_none")]
170 pub image_svg: Option<Vec<String>>,
171
172 #[serde(rename = "application/json", skip_serializing_if = "Option::is_none")]
174 pub application_json: Option<serde_json::Value>,
175}
176
177impl JupyterNotebook {
178 pub fn new() -> Self {
180 Self {
181 metadata: JupyterMetadata::default(),
182 nbformat: 4,
183 nbformat_minor: 5,
184 cells: Vec::new(),
185 }
186 }
187
188 pub fn write_to_file(&self, path: impl AsRef<Path>) -> SyncResult<()> {
190 let path = path.as_ref();
191 let json = serde_json::to_string_pretty(self)?;
192 fs::write(path, json).map_err(|e| SyncError::WriteError {
193 path: path.to_path_buf(),
194 message: e.to_string(),
195 })?;
196 Ok(())
197 }
198
199 pub fn read_from_file(path: impl AsRef<Path>) -> SyncResult<Self> {
201 let path = path.as_ref();
202 let content = fs::read_to_string(path).map_err(|e| SyncError::ReadError {
203 path: path.to_path_buf(),
204 message: e.to_string(),
205 })?;
206 let notebook: Self = serde_json::from_str(&content)?;
207 Ok(notebook)
208 }
209}
210
211impl Default for JupyterNotebook {
212 fn default() -> Self {
213 Self::new()
214 }
215}
216
217impl Default for JupyterMetadata {
218 fn default() -> Self {
219 Self {
220 kernelspec: KernelSpec {
221 display_name: "Rust (Venus)".to_string(),
222 language: "rust".to_string(),
223 name: "venus".to_string(),
224 },
225 language_info: LanguageInfo {
226 file_extension: ".rs".to_string(),
227 mimetype: "text/rust".to_string(),
228 name: "rust".to_string(),
229 version: "1.0".to_string(),
230 },
231 venus: Some(VenusMetadata {
232 source_file: None,
233 version: env!("CARGO_PKG_VERSION").to_string(),
234 }),
235 }
236 }
237}
238
239pub struct IpynbGenerator {
241 execution_count: u32,
243}
244
245impl IpynbGenerator {
246 pub fn new() -> Self {
248 Self { execution_count: 1 }
249 }
250
251 pub fn generate(
253 &mut self,
254 metadata: &NotebookMetadata,
255 cells: &[NotebookCell],
256 cache: Option<&OutputCache>,
257 ) -> SyncResult<JupyterNotebook> {
258 let mut notebook = JupyterNotebook::new();
259
260 if let Some(title) = &metadata.title {
262 notebook.metadata.venus = Some(VenusMetadata {
263 source_file: Some(title.clone()),
264 version: env!("CARGO_PKG_VERSION").to_string(),
265 });
266 }
267
268 for cell in cells {
270 let jupyter_cell = self.convert_cell(cell, cache)?;
271 notebook.cells.push(jupyter_cell);
272 }
273
274 Ok(notebook)
275 }
276
277 fn convert_cell(
279 &mut self,
280 cell: &NotebookCell,
281 cache: Option<&OutputCache>,
282 ) -> SyncResult<JupyterCell> {
283 match cell.cell_type {
284 CellType::Markdown => {
285 let source = cell.markdown.as_deref().unwrap_or("");
286 Ok(JupyterCell {
287 cell_type: "markdown".to_string(),
288 metadata: CellMetadata {
289 venus_cell: Some(cell.name.clone()),
290 editable: Some(true),
291 tags: None,
292 },
293 source: source.lines().map(|l| format!("{}\n", l)).collect(),
294 outputs: None,
295 execution_count: None,
296 })
297 }
298 CellType::Code => {
299 let source = cell.source.as_deref().unwrap_or("");
300 let exec_count = self.execution_count;
301 self.execution_count += 1;
302
303 let outputs = if let Some(cache) = cache {
305 cache.get_output(&cell.name).map(|o| vec![o])
306 } else {
307 None
308 };
309
310 Ok(JupyterCell {
311 cell_type: "code".to_string(),
312 metadata: CellMetadata {
313 venus_cell: Some(cell.name.clone()),
314 editable: Some(true),
315 tags: if cell.has_dependencies {
316 Some(vec!["has-dependencies".to_string()])
317 } else {
318 None
319 },
320 },
321 source: source.lines().map(|l| format!("{}\n", l)).collect(),
322 outputs: Some(outputs.unwrap_or_default()),
323 execution_count: Some(exec_count),
324 })
325 }
326 }
327 }
328}
329
330impl Default for IpynbGenerator {
331 fn default() -> Self {
332 Self::new()
333 }
334}
335
336#[cfg(test)]
337mod tests {
338 use super::*;
339
340 #[test]
341 fn test_empty_notebook() {
342 let notebook = JupyterNotebook::new();
343 assert_eq!(notebook.nbformat, 4);
344 assert!(notebook.cells.is_empty());
345 }
346
347 #[test]
348 fn test_generate_markdown_cell() {
349 let mut generator = IpynbGenerator::new();
350
351 let cell = NotebookCell {
352 name: "intro".to_string(),
353 cell_type: CellType::Markdown,
354 markdown: Some("# Hello\n\nThis is a test.".to_string()),
355 source: None,
356 has_dependencies: false,
357 };
358
359 let jupyter_cell = generator.convert_cell(&cell, None).unwrap();
360
361 assert_eq!(jupyter_cell.cell_type, "markdown");
362 assert!(jupyter_cell.source.len() >= 2);
363 assert!(jupyter_cell.outputs.is_none());
364 }
365
366 #[test]
367 fn test_generate_code_cell() {
368 let mut generator = IpynbGenerator::new();
369
370 let cell = NotebookCell {
371 name: "compute".to_string(),
372 cell_type: CellType::Code,
373 markdown: None,
374 source: Some("#[venus::cell]\npub fn compute() -> i32 { 42 }".to_string()),
375 has_dependencies: false,
376 };
377
378 let jupyter_cell = generator.convert_cell(&cell, None).unwrap();
379
380 assert_eq!(jupyter_cell.cell_type, "code");
381 assert!(jupyter_cell.execution_count.is_some());
382 assert!(jupyter_cell.outputs.is_some());
383 }
384
385 #[test]
386 fn test_notebook_serialization() {
387 let notebook = JupyterNotebook::new();
388 let json = serde_json::to_string_pretty(¬ebook).unwrap();
389
390 assert!(json.contains("nbformat"));
391 assert!(json.contains("metadata"));
392 assert!(json.contains("cells"));
393 }
394}