catcolab_document_types/v2/
notebook.rs

1use crate::v1;
2
3use super::cell::NotebookCell;
4
5use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7use tsify::Tsify;
8use uuid::Uuid;
9
10#[derive(PartialEq, Eq, Debug, Serialize, Deserialize, Tsify)]
11#[tsify(into_wasm_abi, from_wasm_abi, hashmap_as_object)]
12pub struct Notebook<T> {
13    #[serde(rename = "cellContents")]
14    pub cell_contents: HashMap<Uuid, NotebookCell<T>>,
15    #[serde(rename = "cellOrder")]
16    pub cell_order: Vec<Uuid>,
17}
18
19#[derive(PartialEq, Eq, Debug, Serialize, Deserialize, Tsify)]
20#[tsify(into_wasm_abi, from_wasm_abi)]
21pub struct ModelNotebook(pub Notebook<super::model_judgment::ModelJudgment>);
22
23#[derive(PartialEq, Eq, Debug, Serialize, Deserialize, Tsify)]
24#[tsify(into_wasm_abi, from_wasm_abi)]
25pub struct DiagramNotebook(pub Notebook<super::diagram_judgment::DiagramJudgment>);
26
27/// Arbitrary instances for property-based testing.
28#[cfg(feature = "property-tests")]
29pub(crate) mod arbitrary {
30    use super::*;
31    use crate::v2::cell::arbitrary::arb_notebook_cell;
32    use proptest::prelude::*;
33
34    fn arb_uuid() -> BoxedStrategy<Uuid> {
35        any::<u128>().prop_map(Uuid::from_u128).boxed()
36    }
37
38    /// Strategy for a `Notebook<T>` given a strategy for `T`.
39    ///
40    /// Generates a consistent notebook where `cell_order` contains exactly
41    /// the keys in `cell_contents`.
42    pub fn arb_notebook<T: std::fmt::Debug + 'static>(
43        arb_t: impl Strategy<Value = T> + Clone + 'static,
44    ) -> BoxedStrategy<Notebook<T>> {
45        prop::collection::vec((arb_uuid(), arb_notebook_cell(arb_t)), 0..6)
46            .prop_map(|entries| {
47                let mut cell_contents = HashMap::new();
48                let mut cell_order = Vec::new();
49                for (id, cell) in entries {
50                    // Replace the cell's internal id with the map key for
51                    // consistency, matching how real notebooks work.
52                    let cell = match cell {
53                        NotebookCell::RichText { content, .. } => {
54                            NotebookCell::RichText { id, content }
55                        }
56                        NotebookCell::Formal { content, .. } => {
57                            NotebookCell::Formal { id, content }
58                        }
59                    };
60                    cell_contents.insert(id, cell);
61                    cell_order.push(id);
62                }
63                Notebook { cell_contents, cell_order }
64            })
65            .boxed()
66    }
67
68    impl Arbitrary for ModelNotebook {
69        type Parameters = ();
70        type Strategy = BoxedStrategy<Self>;
71
72        fn arbitrary_with(_: Self::Parameters) -> Self::Strategy {
73            arb_notebook(any::<super::super::model_judgment::ModelJudgment>())
74                .prop_map(ModelNotebook)
75                .boxed()
76        }
77    }
78}
79
80impl<T> Notebook<T> {
81    pub fn cells(&self) -> impl Iterator<Item = &NotebookCell<T>> {
82        self.cell_order.iter().filter_map(|id| self.cell_contents.get(id))
83    }
84
85    pub fn formal_content(&self) -> impl Iterator<Item = &T> {
86        self.cells().filter_map(|cell| match cell {
87            NotebookCell::Formal { content, .. } => Some(content),
88            _ => None,
89        })
90    }
91
92    /// Migrate a [`v1::Notebook`] to v2 by dropping stem cells.
93    ///
94    /// Both the cell contents map and the cell order are filtered to remove
95    /// stem cells; non-stem cells preserve their UUIDs and ordering.
96    pub fn migrate_from_v1(old: v1::Notebook<T>) -> Self {
97        let v1::Notebook { cell_contents, cell_order } = old;
98
99        let mut new_contents = HashMap::with_capacity(cell_contents.len());
100        for (id, cell) in cell_contents {
101            if let Some(new_cell) = NotebookCell::migrate_from_v1(cell) {
102                new_contents.insert(id, new_cell);
103            }
104        }
105
106        // Keep only ids that survived the migration, preserving the original
107        // order.
108        let new_order: Vec<Uuid> =
109            cell_order.into_iter().filter(|id| new_contents.contains_key(id)).collect();
110
111        Notebook {
112            cell_contents: new_contents,
113            cell_order: new_order,
114        }
115    }
116}