1use crate::embed::recipes_from_embedded;
10use crate::model::RecipeCard;
11
12#[derive(Clone, Debug, Default)]
14pub struct RecipeStore {
15 cards: Vec<RecipeCard>,
16}
17
18impl RecipeStore {
19 pub fn new() -> Self {
21 Self::default()
22 }
23
24 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 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 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 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 pub fn card_mut(&mut self, id: &str) -> Option<&mut RecipeCard> {
75 self.cards.iter_mut().find(|c| c.id == id)
76 }
77
78 pub fn cards(&self) -> &[RecipeCard] {
80 &self.cards
81 }
82
83 pub fn card(&self, id: &str) -> Option<&RecipeCard> {
85 self.cards.iter().find(|c| c.id == id)
86 }
87
88 pub fn len(&self) -> usize {
90 self.cards.len()
91 }
92
93 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 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}