catcolab_document_types/v1/
notebook.rs

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