tawasuyu

tawasuyu/pluma

Multi-canvas authoring (bundle of bodies) + reactive notebook, in Rust, on Llimphi.

Rust 193Markdown 141TOML 663959 Definitions

⎇ 30467600bcd9effa15e8338fb32d357c1edab660 00_unanchay/pluma/pluma-notebook-store/src/lib.rs Rust · 115 lines · 3.5 KB History Who touched what raw

feat: pluma standalone — autoría multilienzo (haz de cuerpos) + notebook reactivo (front-door, git-dep al monorepo) 30467600b · Sergio ·

1//! `pluma-notebook-store` — save/load atómico de un [`Notebook`] a disco.
2//!
3//! Formato: JSON con header de versión de esquema. La escritura es atómica
4//! (write → fsync → rename), así que un crash a media escritura no corrompe
5//! el archivo final. Convención de extensión: `.pluma-nb`.
6
7#![forbid(unsafe_code)]
8
9use std::fs::{self, File};
10use std::io::{BufReader, BufWriter, Write};
11use std::path::{Path, PathBuf};
12
13use pluma_notebook_core::Notebook;
14use serde::{Deserialize, Serialize};
15use thiserror::Error;
16
17/// Versión actual del esquema en disco. Subir solo ante cambios de formato
18/// incompatibles; la lectura rechaza versiones desconocidas.
19pub const SCHEMA: u32 = 1;
20
21#[derive(Debug, Error)]
22pub enum Error {
23    #[error("I/O: {0}")]
24    Io(#[from] std::io::Error),
25    #[error("JSON: {0}")]
26    Json(#[from] serde_json::Error),
27    #[error("versión de esquema desconocida: {found} (esta build soporta {SCHEMA})")]
28    SchemaDesconocida { found: u32 },
29}
30
31pub type Result<T> = std::result::Result<T, Error>;
32
33#[derive(Debug, Serialize, Deserialize)]
34struct Envelope {
35    schema: u32,
36    notebook: Notebook,
37}
38
39/// Carga un notebook desde un archivo `.pluma-nb`.
40pub fn load(ruta: &Path) -> Result<Notebook> {
41    let f = File::open(ruta)?;
42    let env: Envelope = serde_json::from_reader(BufReader::new(f))?;
43    if env.schema != SCHEMA {
44        return Err(Error::SchemaDesconocida { found: env.schema });
45    }
46    Ok(env.notebook)
47}
48
49/// Guarda un notebook de forma atómica: escribe a `<ruta>.tmp`, hace fsync,
50/// y renombra. Si algo falla a mitad, el archivo original queda intacto.
51pub fn save(ruta: &Path, notebook: &Notebook) -> Result<()> {
52    let env = Envelope { schema: SCHEMA, notebook: notebook.clone() };
53    let tmp = tmp_path(ruta);
54
55    {
56        let f = File::create(&tmp)?;
57        let mut w = BufWriter::new(f);
58        serde_json::to_writer_pretty(&mut w, &env)?;
59        w.flush()?;
60        w.into_inner()
61            .map_err(|e| std::io::Error::other(e.to_string()))?
62            .sync_all()?;
63    }
64
65    fs::rename(&tmp, ruta)?;
66    Ok(())
67}
68
69fn tmp_path(ruta: &Path) -> PathBuf {
70    let mut s = ruta.as_os_str().to_owned();
71    s.push(".tmp");
72    PathBuf::from(s)
73}
74
75#[cfg(test)]
76mod tests {
77    use super::*;
78    use pluma_notebook_core::CellKind;
79
80    fn notebook_ejemplo() -> Notebook {
81        let mut nb = Notebook::new();
82        let a = nb.push(CellKind::Markdown, "# titulo");
83        let b = nb.push(CellKind::Code { language: "rust".into() }, "let x = 1;");
84        nb.add_dependency(b, a);
85        nb
86    }
87
88    #[test]
89    fn save_load_roundtrip_preserva_digest() {
90        let dir = tempfile::tempdir().unwrap();
91        let ruta = dir.path().join("ejemplo.pluma-nb");
92        let original = notebook_ejemplo();
93        save(&ruta, &original).unwrap();
94        let cargado = load(&ruta).unwrap();
95        assert_eq!(original.notebook_digest(), cargado.notebook_digest());
96        assert_eq!(original.len(), cargado.len());
97    }
98
99    #[test]
100    fn save_es_atomico_no_deja_tmp() {
101        let dir = tempfile::tempdir().unwrap();
102        let ruta = dir.path().join("at.pluma-nb");
103        save(&ruta, &notebook_ejemplo()).unwrap();
104        assert!(ruta.exists());
105        assert!(!tmp_path(&ruta).exists());
106    }
107
108    #[test]
109    fn schema_desconocida_falla() {
110        let dir = tempfile::tempdir().unwrap();
111        let ruta = dir.path().join("futuro.pluma-nb");
112        fs::write(&ruta, r#"{"schema": 999, "notebook": {"cells": [], "next_id": 1}}"#).unwrap();
113        assert!(matches!(load(&ruta), Err(Error::SchemaDesconocida { found: 999 })));
114    }
115}