1pub mod handle;
22pub mod id;
23pub mod types;
24
25pub use handle::RecipeRegistryHandle;
26pub use id::RecipeId;
27pub use types::{OwnedShapedRecipe, OwnedShapelessRecipe, Recipe};
28
29use crate::events::{Event, EventBus};
30use crate::events::{RecipeRegisterEvent, RecipeRegisteredEvent, RecipeUnregisteredEvent};
31
32pub struct RecipeRegistrar<'a> {
39 registry: &'a mut dyn RecipeRegistryHandle,
40 bus: &'a mut EventBus,
41 ctx: &'a dyn crate::context::Context,
42}
43
44impl<'a> RecipeRegistrar<'a> {
45 pub(crate) fn new(
49 registry: &'a mut dyn RecipeRegistryHandle,
50 bus: &'a mut EventBus,
51 ctx: &'a dyn crate::context::Context,
52 ) -> Self {
53 Self { registry, bus, ctx }
54 }
55
56 pub fn add_shaped(&mut self, recipe: OwnedShapedRecipe) -> bool {
63 let id = recipe.id.clone();
64 let mut event = RecipeRegisterEvent {
65 recipe: Recipe::Shaped(recipe),
66 cancelled: false,
67 };
68 self.bus.dispatch(&mut event, self.ctx);
69 if event.is_cancelled() {
70 return false;
71 }
72 match event.recipe {
73 Recipe::Shaped(r) => self.registry.add_shaped(r),
74 Recipe::Shapeless(_) => {
75 return false;
78 }
79 }
80 let mut post = RecipeRegisteredEvent { recipe_id: id };
81 self.bus.dispatch(&mut post, self.ctx);
82 true
83 }
84
85 pub fn add_shapeless(&mut self, recipe: OwnedShapelessRecipe) -> bool {
91 let id = recipe.id.clone();
92 let mut event = RecipeRegisterEvent {
93 recipe: Recipe::Shapeless(recipe),
94 cancelled: false,
95 };
96 self.bus.dispatch(&mut event, self.ctx);
97 if event.is_cancelled() {
98 return false;
99 }
100 match event.recipe {
101 Recipe::Shapeless(r) => self.registry.add_shapeless(r),
102 Recipe::Shaped(_) => return false,
103 }
104 let mut post = RecipeRegisteredEvent { recipe_id: id };
105 self.bus.dispatch(&mut post, self.ctx);
106 true
107 }
108
109 pub fn remove_by_id(&mut self, id: &RecipeId) -> bool {
115 if self.registry.remove_by_id(id).is_some() {
116 let mut event = RecipeUnregisteredEvent {
117 recipe_id: id.clone(),
118 };
119 self.bus.dispatch(&mut event, self.ctx);
120 true
121 } else {
122 false
123 }
124 }
125
126 pub fn remove_by_result(&mut self, result_id: i32) -> usize {
130 let removed = self.registry.remove_by_result(result_id);
131 let count = removed.len();
132 for recipe_id in removed {
133 let mut event = RecipeUnregisteredEvent { recipe_id };
134 self.bus.dispatch(&mut event, self.ctx);
135 }
136 count
137 }
138
139 pub fn clear(&mut self) {
142 let removed = self.registry.clear();
143 for recipe_id in removed {
144 let mut event = RecipeUnregisteredEvent { recipe_id };
145 self.bus.dispatch(&mut event, self.ctx);
146 }
147 }
148
149 pub fn contains(&self, id: &RecipeId) -> bool {
151 self.registry.contains(id)
152 }
153
154 pub fn get(&self, id: &RecipeId) -> Option<Recipe> {
156 self.registry.find_by_id(id)
157 }
158
159 pub fn shaped_count(&self) -> usize {
161 self.registry.shaped_count()
162 }
163
164 pub fn shapeless_count(&self) -> usize {
166 self.registry.shapeless_count()
167 }
168}
169
170#[cfg(test)]
171mod tests {
172 use std::sync::Arc;
173 use std::sync::atomic::{AtomicU32, Ordering};
174
175 use crate::events::Stage;
176 use crate::testing::{MockRecipeRegistry, NoopContext};
177
178 use super::*;
179
180 fn shaped(path: &str) -> OwnedShapedRecipe {
181 OwnedShapedRecipe {
182 id: RecipeId::new("plugin", path),
183 width: 1,
184 height: 1,
185 pattern: vec![Some(1)],
186 result_id: 42,
187 result_count: 1,
188 }
189 }
190
191 fn shapeless(path: &str) -> OwnedShapelessRecipe {
192 OwnedShapelessRecipe {
193 id: RecipeId::new("plugin", path),
194 ingredients: vec![1, 2],
195 result_id: 99,
196 result_count: 1,
197 }
198 }
199
200 #[test]
201 fn add_shaped_dispatches_register_then_registered() {
202 let mut registry = MockRecipeRegistry::new();
203 let mut bus = EventBus::new();
204 let ctx = NoopContext;
205
206 let validate_seen: Arc<AtomicU32> = Arc::new(AtomicU32::new(0));
207 let post_seen = Arc::new(AtomicU32::new(0));
208
209 {
210 let v = Arc::clone(&validate_seen);
211 bus.on::<RecipeRegisterEvent>(Stage::Validate, 0, move |_, _| {
212 v.fetch_add(1, Ordering::Relaxed);
213 });
214 }
215 {
216 let p = Arc::clone(&post_seen);
217 bus.on::<RecipeRegisteredEvent>(Stage::Post, 0, move |_, _| {
218 p.fetch_add(1, Ordering::Relaxed);
219 });
220 }
221
222 let mut registrar = RecipeRegistrar::new(
223 &mut registry as &mut dyn RecipeRegistryHandle,
224 &mut bus,
225 &ctx as &dyn crate::context::Context,
226 );
227 let inserted = registrar.add_shaped(shaped("magic_sword"));
228
229 assert!(inserted);
230 assert_eq!(validate_seen.load(Ordering::Relaxed), 1);
231 assert_eq!(post_seen.load(Ordering::Relaxed), 1);
232 assert_eq!(registry.shaped_count(), 1);
233 }
234
235 #[test]
236 fn add_shaped_cancellation_skips_insert_and_post() {
237 let mut registry = MockRecipeRegistry::new();
238 let mut bus = EventBus::new();
239 let ctx = NoopContext;
240
241 bus.on::<RecipeRegisterEvent>(Stage::Validate, 0, |event, _| {
242 event.cancel();
243 });
244
245 let post_seen = Arc::new(AtomicU32::new(0));
246 {
247 let p = Arc::clone(&post_seen);
248 bus.on::<RecipeRegisteredEvent>(Stage::Post, 0, move |_, _| {
249 p.fetch_add(1, Ordering::Relaxed);
250 });
251 }
252
253 let mut registrar = RecipeRegistrar::new(
254 &mut registry as &mut dyn RecipeRegistryHandle,
255 &mut bus,
256 &ctx as &dyn crate::context::Context,
257 );
258 let inserted = registrar.add_shaped(shaped("forbidden"));
259
260 assert!(
261 !inserted,
262 "cancellation should make add_shaped return false"
263 );
264 assert_eq!(post_seen.load(Ordering::Relaxed), 0);
265 assert_eq!(registry.shaped_count(), 0);
266 }
267
268 #[test]
269 fn add_shapeless_round_trip() {
270 let mut registry = MockRecipeRegistry::new();
271 let mut bus = EventBus::new();
272 let ctx = NoopContext;
273
274 let mut registrar = RecipeRegistrar::new(
275 &mut registry as &mut dyn RecipeRegistryHandle,
276 &mut bus,
277 &ctx as &dyn crate::context::Context,
278 );
279 assert!(registrar.add_shapeless(shapeless("bread")));
280 assert!(registrar.contains(&RecipeId::new("plugin", "bread")));
281 }
282
283 #[test]
284 fn remove_by_id_dispatches_unregistered() {
285 let mut registry = MockRecipeRegistry::new();
286 registry.add_shaped(shaped("temp"));
287
288 let mut bus = EventBus::new();
289 let ctx = NoopContext;
290 let unreg_seen = Arc::new(AtomicU32::new(0));
291 {
292 let u = Arc::clone(&unreg_seen);
293 bus.on::<RecipeUnregisteredEvent>(Stage::Post, 0, move |_, _| {
294 u.fetch_add(1, Ordering::Relaxed);
295 });
296 }
297
298 let mut registrar = RecipeRegistrar::new(
299 &mut registry as &mut dyn RecipeRegistryHandle,
300 &mut bus,
301 &ctx as &dyn crate::context::Context,
302 );
303 let id = RecipeId::new("plugin", "temp");
304 assert!(registrar.remove_by_id(&id));
305 assert_eq!(unreg_seen.load(Ordering::Relaxed), 1);
306 assert!(!registry.contains(&id));
307 }
308
309 #[test]
310 fn remove_by_id_missing_does_not_dispatch() {
311 let mut registry = MockRecipeRegistry::new();
312 let mut bus = EventBus::new();
313 let ctx = NoopContext;
314 let unreg_seen = Arc::new(AtomicU32::new(0));
315 {
316 let u = Arc::clone(&unreg_seen);
317 bus.on::<RecipeUnregisteredEvent>(Stage::Post, 0, move |_, _| {
318 u.fetch_add(1, Ordering::Relaxed);
319 });
320 }
321
322 let mut registrar = RecipeRegistrar::new(
323 &mut registry as &mut dyn RecipeRegistryHandle,
324 &mut bus,
325 &ctx as &dyn crate::context::Context,
326 );
327 assert!(!registrar.remove_by_id(&RecipeId::new("plugin", "missing")));
328 assert_eq!(unreg_seen.load(Ordering::Relaxed), 0);
329 }
330
331 #[test]
332 fn remove_by_result_dispatches_per_removed() {
333 let mut registry = MockRecipeRegistry::new();
334 registry.add_shaped(shaped("a"));
335 registry.add_shaped(shaped("b"));
336 let mut bus = EventBus::new();
339 let ctx = NoopContext;
340 let unreg_seen = Arc::new(AtomicU32::new(0));
341 {
342 let u = Arc::clone(&unreg_seen);
343 bus.on::<RecipeUnregisteredEvent>(Stage::Post, 0, move |_, _| {
344 u.fetch_add(1, Ordering::Relaxed);
345 });
346 }
347
348 let mut registrar = RecipeRegistrar::new(
349 &mut registry as &mut dyn RecipeRegistryHandle,
350 &mut bus,
351 &ctx as &dyn crate::context::Context,
352 );
353 assert_eq!(registrar.remove_by_result(42), 2);
354 assert_eq!(unreg_seen.load(Ordering::Relaxed), 2);
355 }
356
357 #[test]
358 fn clear_dispatches_per_recipe() {
359 let mut registry = MockRecipeRegistry::new();
360 registry.add_shaped(shaped("a"));
361 registry.add_shapeless(shapeless("b"));
362
363 let mut bus = EventBus::new();
364 let ctx = NoopContext;
365 let unreg_seen = Arc::new(AtomicU32::new(0));
366 {
367 let u = Arc::clone(&unreg_seen);
368 bus.on::<RecipeUnregisteredEvent>(Stage::Post, 0, move |_, _| {
369 u.fetch_add(1, Ordering::Relaxed);
370 });
371 }
372
373 let mut registrar = RecipeRegistrar::new(
374 &mut registry as &mut dyn RecipeRegistryHandle,
375 &mut bus,
376 &ctx as &dyn crate::context::Context,
377 );
378 registrar.clear();
379 assert_eq!(unreg_seen.load(Ordering::Relaxed), 2);
380 assert_eq!(registry.shaped_count(), 0);
381 assert_eq!(registry.shapeless_count(), 0);
382 }
383
384 #[test]
385 fn registrar_accessors_expose_underlying_state() {
386 let mut registry = MockRecipeRegistry::new();
387 let mut bus = EventBus::new();
388 let ctx = NoopContext;
389 let mut registrar = RecipeRegistrar::new(
390 &mut registry as &mut dyn RecipeRegistryHandle,
391 &mut bus,
392 &ctx as &dyn crate::context::Context,
393 );
394 assert_eq!(registrar.shaped_count(), 0);
395 registrar.add_shaped(shaped("only"));
396 assert_eq!(registrar.shaped_count(), 1);
397 }
398}