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}