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/// One chapter within a [`BookView`].
99#[derive(Clone, Debug, PartialEq, Eq)]
100pub struct ChapterView {
101    /// Chapter directory name (stable id within the book).
102    pub name: String,
103    /// Human chapter title.
104    pub title: String,
105    /// One-line chapter summary (may be empty).
106    pub summary: String,
107    /// Recipes sorted by `order`, then id.
108    pub recipes: Vec<RecipeCard>,
109}
110
111/// The outcome of running a recipe's setup through its codec.
112#[derive(Clone, Debug, PartialEq, Eq)]
113pub struct RecipeRun {
114    /// The recipe id that was run.
115    pub recipe: String,
116    /// Number of setup forms evaluated.
117    pub forms: usize,
118    /// Encoded result of each form, in the recipe's codec.
119    pub results: Vec<String>,
120    /// Expectation check results (empty when no expectations).
121    pub checks: Vec<CheckResult>,
122    /// True when every form evaluated and every check passed.
123    pub ok: bool,
124}
125
126/// The result of checking one [`Expectation`] after a run.
127#[derive(Clone, Debug, PartialEq, Eq)]
128pub struct CheckResult {
129    /// Index of the checked setup form.
130    pub form: usize,
131    /// Expected encoded result.
132    pub expected: String,
133    /// Actual encoded result.
134    pub actual: String,
135    /// True when `expected == actual`.
136    pub pass: bool,
137}
138
139#[cfg(test)]
140mod tests {
141    use super::*;
142
143    fn sample_card() -> RecipeCard {
144        RecipeCard {
145            id: "numbers-f64/01-basics/add-two-numbers".to_string(),
146            book: "numbers-f64".to_string(),
147            chapter: "01-basics".to_string(),
148            chapter_title: "Basics".to_string(),
149            chapter_summary: String::new(),
150            title: "Add two numbers".to_string(),
151            codec: "lisp".to_string(),
152            setup: b"(+ 1 2)".to_vec(),
153            purpose: "Add two f64 values.".to_string(),
154            order: 100,
155            chapter_order: 100,
156            book_order: 200,
157            book_title: "Numbers (f64)".to_string(),
158            book_summary: String::new(),
159            tags: vec!["arithmetic".to_string(), "intro".to_string()],
160            requires: vec!["numbers-f64".to_string()],
161            expect: vec![Expectation {
162                form: 0,
163                result: "3".to_string(),
164            }],
165            source: RecipeSource::Crate {
166                lib: "numbers-f64".to_string(),
167            },
168        }
169    }
170
171    #[test]
172    fn recipe_card_round_trips_its_fields() {
173        let card = sample_card();
174        let clone = card.clone();
175        assert_eq!(card, clone);
176        assert_eq!(card.id, "numbers-f64/01-basics/add-two-numbers");
177        assert_eq!(card.setup, b"(+ 1 2)");
178        assert_eq!(card.expect[0].form, 0);
179        assert_eq!(card.expect[0].result, "3");
180        assert_eq!(
181            card.source,
182            RecipeSource::Crate {
183                lib: "numbers-f64".to_string()
184            }
185        );
186    }
187
188    #[test]
189    fn cookbook_view_nests_book_chapter_recipe() {
190        let card = sample_card();
191        let view = CookbookView {
192            books: vec![BookView {
193                id: card.book.clone(),
194                title: card.book_title.clone(),
195                summary: String::new(),
196                chapters: vec![ChapterView {
197                    name: card.chapter.clone(),
198                    title: card.chapter_title.clone(),
199                    summary: String::new(),
200                    recipes: vec![card.clone()],
201                }],
202            }],
203        };
204        assert_eq!(view.books[0].chapters[0].recipes[0], card);
205        assert_eq!(view.books[0].id, "numbers-f64");
206    }
207
208    #[test]
209    fn recipe_run_reports_ok() {
210        let run = RecipeRun {
211            recipe: "numbers-f64/01-basics/add-two-numbers".to_string(),
212            forms: 1,
213            results: vec!["3".to_string()],
214            checks: vec![CheckResult {
215                form: 0,
216                expected: "3".to_string(),
217                actual: "3".to_string(),
218                pass: true,
219            }],
220            ok: true,
221        };
222        assert!(run.ok);
223        assert!(run.checks[0].pass);
224    }
225}