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}