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