Skip to main content

sim_cookbook/
store.rs

1//! The in-memory store of loaded recipes.
2//!
3//! Each loaded lib registers its embedded book into a [`RecipeStore`]; the
4//! cookbook view (a later phase) is a projection over the store's cards. The
5//! store lives in this crate, not the kernel: recipes are plain data, so no
6//! kernel cookbook type is needed. A server or CLI builds one store at startup
7//! by registering every loaded lib's book.
8
9use crate::embed::recipes_from_embedded;
10use crate::model::RecipeCard;
11
12/// A registry of loaded recipe cards, keyed by their globally unique id.
13#[derive(Clone, Debug, Default)]
14pub struct RecipeStore {
15    cards: Vec<RecipeCard>,
16}
17
18impl RecipeStore {
19    /// An empty store.
20    pub fn new() -> Self {
21        Self::default()
22    }
23
24    /// Parse one crate's embedded `recipes/` tree and add its cards. Returns an
25    /// error if a manifest is malformed or a recipe id collides with one
26    /// already registered (ids must be globally unique).
27    pub fn register_book(&mut self, dir: &[(&str, &[u8])]) -> Result<(), String> {
28        let cards = recipes_from_embedded(dir)?;
29        for card in &cards {
30            if self.cards.iter().any(|existing| existing.id == card.id) {
31                return Err(format!("duplicate recipe id `{}`", card.id));
32            }
33        }
34        self.cards.extend(cards);
35        Ok(())
36    }
37
38    /// Add a single already-built card. Errors on a duplicate id.
39    pub fn insert_card(&mut self, card: RecipeCard) -> Result<(), String> {
40        if self.cards.iter().any(|existing| existing.id == card.id) {
41            return Err(format!("duplicate recipe id `{}`", card.id));
42        }
43        self.cards.push(card);
44        Ok(())
45    }
46
47    /// Insert a card, replacing any existing card with the same id (overlay
48    /// override semantics). Returns whether an existing card was replaced.
49    pub fn upsert_card(&mut self, card: RecipeCard) -> bool {
50        match self
51            .cards
52            .iter_mut()
53            .find(|existing| existing.id == card.id)
54        {
55            Some(existing) => {
56                *existing = card;
57                true
58            }
59            None => {
60                self.cards.push(card);
61                false
62            }
63        }
64    }
65
66    /// Remove a card by id. Returns whether one was removed.
67    pub fn remove(&mut self, id: &str) -> bool {
68        let before = self.cards.len();
69        self.cards.retain(|c| c.id != id);
70        self.cards.len() != before
71    }
72
73    /// Mutable access to one card by id (for overlay reorder/retitle).
74    pub fn card_mut(&mut self, id: &str) -> Option<&mut RecipeCard> {
75        self.cards.iter_mut().find(|c| c.id == id)
76    }
77
78    /// All registered cards, in registration order.
79    pub fn cards(&self) -> &[RecipeCard] {
80        &self.cards
81    }
82
83    /// Look up one card by its full id.
84    pub fn card(&self, id: &str) -> Option<&RecipeCard> {
85        self.cards.iter().find(|c| c.id == id)
86    }
87
88    /// Number of registered cards.
89    pub fn len(&self) -> usize {
90        self.cards.len()
91    }
92
93    /// Whether the store holds no cards.
94    pub fn is_empty(&self) -> bool {
95        self.cards.is_empty()
96    }
97}
98
99#[cfg(test)]
100mod tests {
101    use super::*;
102
103    fn book(id: &'static str) -> Vec<(&'static str, &'static [u8])> {
104        // One tiny book whose lib id is `id`. We leak `id` into the embedded
105        // strings via a fixed template per test input.
106        match id {
107            "alpha" => vec![
108                ("book.toml", b"book = \"alpha\"\ntitle = \"Alpha\"\n" as &[u8]),
109                (
110                    "c/r/recipe.toml",
111                    b"id = \"r\"\ntitle = \"R\"\ncodec = \"lisp\"\nsetup = \"s\"\npurpose = \"p\"\n",
112                ),
113                ("c/r/s", b"(quote a)"),
114                ("c/r/p", b"alpha recipe"),
115            ],
116            _ => vec![
117                ("book.toml", b"book = \"beta\"\ntitle = \"Beta\"\n" as &[u8]),
118                (
119                    "c/r/recipe.toml",
120                    b"id = \"r\"\ntitle = \"R\"\ncodec = \"lisp\"\nsetup = \"s\"\npurpose = \"p\"\n",
121                ),
122                ("c/r/s", b"(quote b)"),
123                ("c/r/p", b"beta recipe"),
124            ],
125        }
126    }
127
128    #[test]
129    fn registers_multiple_books() {
130        let mut store = RecipeStore::new();
131        store.register_book(&book("alpha")).unwrap();
132        store.register_book(&book("beta")).unwrap();
133        assert_eq!(store.len(), 2);
134        assert!(store.card("alpha/c/r").is_some());
135        assert!(store.card("beta/c/r").is_some());
136    }
137
138    #[test]
139    fn rejects_duplicate_book() {
140        let mut store = RecipeStore::new();
141        store.register_book(&book("alpha")).unwrap();
142        let err = store.register_book(&book("alpha")).unwrap_err();
143        assert!(err.contains("duplicate recipe id `alpha/c/r`"), "{err}");
144    }
145}