basalt_api/components/
recipe_book.rs1use std::collections::{HashMap, HashSet};
9
10use crate::recipes::RecipeId;
11
12use crate::components::Component;
13
14#[derive(Debug, Default, Clone)]
28pub struct KnownRecipes {
29 ids: HashSet<RecipeId>,
31 display_ids: HashMap<RecipeId, i32>,
33 by_display: HashMap<i32, RecipeId>,
36 next_display_id: i32,
38}
39
40impl Component for KnownRecipes {}
41
42impl KnownRecipes {
43 pub fn unlock(&mut self, id: RecipeId) -> i32 {
50 if !self.ids.insert(id.clone()) {
51 return self.display_ids[&id];
53 }
54 let display_id = self.next_display_id;
55 self.next_display_id += 1;
56 self.display_ids.insert(id.clone(), display_id);
57 self.by_display.insert(display_id, id);
58 display_id
59 }
60
61 pub fn lock(&mut self, id: &RecipeId) -> Option<i32> {
68 if !self.ids.remove(id) {
69 return None;
70 }
71 self.display_ids.remove(id)
72 }
73
74 pub fn has(&self, id: &RecipeId) -> bool {
76 self.ids.contains(id)
77 }
78
79 pub fn display_id(&self, id: &RecipeId) -> Option<i32> {
84 self.display_ids.get(id).copied()
85 }
86
87 pub fn recipe_for_display(&self, display_id: i32) -> Option<&RecipeId> {
93 self.by_display.get(&display_id)
94 }
95
96 pub fn len(&self) -> usize {
98 self.ids.len()
99 }
100
101 pub fn is_empty(&self) -> bool {
103 self.ids.is_empty()
104 }
105
106 pub fn iter(&self) -> impl Iterator<Item = (&RecipeId, i32)> {
111 self.display_ids.iter().map(|(id, d)| (id, *d))
112 }
113}
114
115#[cfg(test)]
116mod tests {
117 use super::*;
118
119 fn id(path: &str) -> RecipeId {
120 RecipeId::new("plugin", path)
121 }
122
123 #[test]
124 fn unlock_allocates_sequential_display_ids() {
125 let mut k = KnownRecipes::default();
126 assert_eq!(k.unlock(id("a")), 0);
127 assert_eq!(k.unlock(id("b")), 1);
128 assert_eq!(k.unlock(id("c")), 2);
129 }
130
131 #[test]
132 fn unlock_idempotent_returns_existing_display_id() {
133 let mut k = KnownRecipes::default();
134 let first = k.unlock(id("a"));
135 let second = k.unlock(id("a"));
136 assert_eq!(first, second);
137 assert_eq!(k.len(), 1);
138 }
139
140 #[test]
141 fn lock_returns_display_id_and_removes_forward_lookup() {
142 let mut k = KnownRecipes::default();
143 let display = k.unlock(id("a"));
144 assert_eq!(k.lock(&id("a")), Some(display));
145 assert!(!k.has(&id("a")));
146 assert_eq!(k.display_id(&id("a")), None);
147 }
148
149 #[test]
150 fn lock_keeps_reverse_lookup_for_stale_packets() {
151 let mut k = KnownRecipes::default();
152 let display = k.unlock(id("a"));
153 k.lock(&id("a"));
154 assert_eq!(k.recipe_for_display(display), Some(&id("a")));
155 }
156
157 #[test]
158 fn lock_returns_none_when_unknown() {
159 let mut k = KnownRecipes::default();
160 assert_eq!(k.lock(&id("missing")), None);
161 }
162
163 #[test]
164 fn display_ids_do_not_reuse_after_lock() {
165 let mut k = KnownRecipes::default();
166 k.unlock(id("a"));
167 k.unlock(id("b"));
168 k.lock(&id("a"));
169 assert_eq!(k.unlock(id("c")), 2);
172 }
173
174 #[test]
175 fn iter_yields_only_unlocked_pairs() {
176 let mut k = KnownRecipes::default();
177 k.unlock(id("a"));
178 k.unlock(id("b"));
179 k.lock(&id("a"));
180
181 let mut entries: Vec<_> = k.iter().map(|(id, d)| (id.clone(), d)).collect();
182 entries.sort_by_key(|(_, d)| *d);
183 assert_eq!(entries, vec![(id("b"), 1)]);
184 }
185
186 #[test]
187 fn has_returns_true_only_for_unlocked() {
188 let mut k = KnownRecipes::default();
189 assert!(!k.has(&id("a")));
190 k.unlock(id("a"));
191 assert!(k.has(&id("a")));
192 k.lock(&id("a"));
193 assert!(!k.has(&id("a")));
194 }
195}