Skip to main content

sim_cookbook/
model.rs

1//! Plain data model for the cookbook.
2//!
3//! A recipe is a tiny runnable lesson shipped by the crate it teaches. These
4//! types carry one recipe's data (a [`RecipeCard`]) and the computed grouping
5//! of all loaded recipes into books and chapters (a [`CookbookView`]). They are
6//! deliberately free of kernel types: recipes flow through the existing
7//! Card/registry data surface, so the kernel gains no cookbook enum.
8
9/// Where a recipe came from.
10#[derive(Clone, Debug, PartialEq, Eq)]
11pub enum RecipeSource {
12    /// Shipped inside a crate, embedded in its compiled lib.
13    Crate {
14        /// The lib id that owns the recipe.
15        lib: String,
16    },
17    /// Supplied locally by the user overlay directory.
18    Overlay {
19        /// The overlay file path the recipe was loaded from.
20        path: String,
21    },
22}
23
24/// One declared expectation, turning a recipe into a generated test.
25///
26/// After running the recipe's setup, the encoded result of form `form`
27/// (0-based) must equal `result` (encoded in the recipe's codec).
28#[derive(Clone, Debug, PartialEq, Eq)]
29pub struct Expectation {
30    /// Index of the setup form whose result is checked.
31    pub form: usize,
32    /// Expected encoded result, in the recipe's codec.
33    pub result: String,
34}
35
36/// The in-memory record for one recipe. Registered as a Card of kind
37/// `"recipe"`; the cookbook view is a projection over these.
38#[derive(Clone, Debug, PartialEq, Eq)]
39pub struct RecipeCard {
40    /// Globally unique id: `"<book>/<chapter>/<recipe-id>"`.
41    pub id: String,
42    /// Owning lib id (the book).
43    pub book: String,
44    /// Chapter directory name (or overlay chapter).
45    pub chapter: String,
46    /// Resolved human chapter title.
47    pub chapter_title: String,
48    /// One-line chapter summary (may be empty).
49    pub chapter_summary: String,
50    /// Human recipe title.
51    pub title: String,
52    /// Registered codec name used to decode `setup`.
53    pub codec: String,
54    /// Raw setup bytes, decoded on demand through `codec`.
55    pub setup: Vec<u8>,
56    /// Purpose document contents (Markdown, ASCII).
57    pub purpose: String,
58    /// Sort key within the chapter; lower runs first.
59    pub order: i64,
60    /// Sort key of this recipe's chapter among chapters.
61    pub chapter_order: i64,
62    /// Sort key of this recipe's book among books.
63    pub book_order: i64,
64    /// Human book title.
65    pub book_title: String,
66    /// One-line book summary (may be empty).
67    pub book_summary: String,
68    /// Free tags for search and filtering.
69    pub tags: Vec<String>,
70    /// Lib ids that must be loaded for this recipe to run.
71    pub requires: Vec<String>,
72    /// Declared expectations (empty when the recipe is not a test).
73    pub expect: Vec<Expectation>,
74    /// Provenance of this recipe.
75    pub source: RecipeSource,
76}
77
78/// The full computed cookbook: every loaded recipe grouped into books.
79#[derive(Clone, Debug, PartialEq, Eq, Default)]
80pub struct CookbookView {
81    /// Books sorted by `book_order`, then book id.
82    pub books: Vec<BookView>,
83}
84
85/// One book (all recipes from one crate) in a [`CookbookView`].
86#[derive(Clone, Debug, PartialEq, Eq)]
87pub struct BookView {
88    /// Lib id of the book.
89    pub id: String,
90    /// Human book title.
91    pub title: String,
92    /// One-line book summary (may be empty).
93    pub summary: String,
94    /// Chapters sorted by `chapter_order`, then name.
95    pub chapters: Vec<ChapterView>,
96}
97
98/// The full catalog grouped into a two-level tree: family -> domain (book) ->
99/// chapter -> recipe (COOK8.00 COVERAGE axis).
100///
101/// The grouping needs NO per-recipe metadata: the level-1 family is derived from
102/// each book's id prefix (`numbers/cas` -> `numbers`, `organ/binding` ->
103/// `organ`, `audio-dsp` -> `audio`), and the level-2 domain is the book itself.
104/// So the whole constellation browses by subsystem without a hand-maintained
105/// group list.
106#[derive(Clone, Debug, PartialEq, Eq, Default)]
107pub struct GroupedView {
108    /// Families in the order their lowest-ordered book appears in
109    /// [`CookbookView`] (book order is total, so this order is deterministic).
110    pub families: Vec<FamilyView>,
111}
112
113/// One level-1 family (subsystem) in a [`GroupedView`], holding its domain books.
114#[derive(Clone, Debug, PartialEq, Eq)]
115pub struct FamilyView {
116    /// Family id derived from the book prefix (`numbers`, `organ`, `codec`, ...).
117    pub family: String,
118    /// The domain books in this family, in the same order as [`CookbookView`].
119    pub books: Vec<BookView>,
120}
121
122/// One chapter within a [`BookView`].
123#[derive(Clone, Debug, PartialEq, Eq)]
124pub struct ChapterView {
125    /// Chapter directory name (stable id within the book).
126    pub name: String,
127    /// Human chapter title.
128    pub title: String,
129    /// One-line chapter summary (may be empty).
130    pub summary: String,
131    /// Recipes sorted by `order`, then id.
132    pub recipes: Vec<RecipeCard>,
133}
134
135/// The outcome of running a recipe's setup through its codec.
136#[derive(Clone, Debug, PartialEq, Eq)]
137pub struct RecipeRun {
138    /// The recipe id that was run.
139    pub recipe: String,
140    /// Number of setup forms evaluated.
141    pub forms: usize,
142    /// Encoded result of each form, in the recipe's codec.
143    pub results: Vec<String>,
144    /// Expectation check results (empty when no expectations).
145    pub checks: Vec<CheckResult>,
146    /// True when every form evaluated and every check passed.
147    pub ok: bool,
148}
149
150/// The result of checking one [`Expectation`] after a run.
151#[derive(Clone, Debug, PartialEq, Eq)]
152pub struct CheckResult {
153    /// Index of the checked setup form.
154    pub form: usize,
155    /// Expected encoded result.
156    pub expected: String,
157    /// Actual encoded result.
158    pub actual: String,
159    /// True when `expected == actual`.
160    pub pass: bool,
161}
162
163#[cfg(test)]
164mod tests {
165    use super::*;
166
167    fn sample_card() -> RecipeCard {
168        RecipeCard {
169            id: "numbers-f64/01-basics/add-two-numbers".to_string(),
170            book: "numbers-f64".to_string(),
171            chapter: "01-basics".to_string(),
172            chapter_title: "Basics".to_string(),
173            chapter_summary: String::new(),
174            title: "Add two numbers".to_string(),
175            codec: "lisp".to_string(),
176            setup: b"(+ 1 2)".to_vec(),
177            purpose: "Add two f64 values.".to_string(),
178            order: 100,
179            chapter_order: 100,
180            book_order: 200,
181            book_title: "Numbers (f64)".to_string(),
182            book_summary: String::new(),
183            tags: vec!["arithmetic".to_string(), "intro".to_string()],
184            requires: vec!["numbers-f64".to_string()],
185            expect: vec![Expectation {
186                form: 0,
187                result: "3".to_string(),
188            }],
189            source: RecipeSource::Crate {
190                lib: "numbers-f64".to_string(),
191            },
192        }
193    }
194
195    #[test]
196    fn recipe_card_round_trips_its_fields() {
197        let card = sample_card();
198        let clone = card.clone();
199        assert_eq!(card, clone);
200        assert_eq!(card.id, "numbers-f64/01-basics/add-two-numbers");
201        assert_eq!(card.setup, b"(+ 1 2)");
202        assert_eq!(card.expect[0].form, 0);
203        assert_eq!(card.expect[0].result, "3");
204        assert_eq!(
205            card.source,
206            RecipeSource::Crate {
207                lib: "numbers-f64".to_string()
208            }
209        );
210    }
211
212    #[test]
213    fn cookbook_view_nests_book_chapter_recipe() {
214        let card = sample_card();
215        let view = CookbookView {
216            books: vec![BookView {
217                id: card.book.clone(),
218                title: card.book_title.clone(),
219                summary: String::new(),
220                chapters: vec![ChapterView {
221                    name: card.chapter.clone(),
222                    title: card.chapter_title.clone(),
223                    summary: String::new(),
224                    recipes: vec![card.clone()],
225                }],
226            }],
227        };
228        assert_eq!(view.books[0].chapters[0].recipes[0], card);
229        assert_eq!(view.books[0].id, "numbers-f64");
230    }
231
232    #[test]
233    fn recipe_run_reports_ok() {
234        let run = RecipeRun {
235            recipe: "numbers-f64/01-basics/add-two-numbers".to_string(),
236            forms: 1,
237            results: vec!["3".to_string()],
238            checks: vec![CheckResult {
239                form: 0,
240                expected: "3".to_string(),
241                actual: "3".to_string(),
242                pass: true,
243            }],
244            ok: true,
245        };
246        assert!(run.ok);
247        assert!(run.checks[0].pass);
248    }
249}