pub mod handle;
pub mod id;
pub mod types;
pub use handle::RecipeRegistryHandle;
pub use id::RecipeId;
pub use types::{OwnedShapedRecipe, OwnedShapelessRecipe, Recipe};
use crate::events::{Event, EventBus};
use crate::events::{RecipeRegisterEvent, RecipeRegisteredEvent, RecipeUnregisteredEvent};
pub struct RecipeRegistrar<'a> {
registry: &'a mut dyn RecipeRegistryHandle,
bus: &'a mut EventBus,
ctx: &'a dyn crate::context::Context,
}
impl<'a> RecipeRegistrar<'a> {
pub(crate) fn new(
registry: &'a mut dyn RecipeRegistryHandle,
bus: &'a mut EventBus,
ctx: &'a dyn crate::context::Context,
) -> Self {
Self { registry, bus, ctx }
}
pub fn add_shaped(&mut self, recipe: OwnedShapedRecipe) -> bool {
let id = recipe.id.clone();
let mut event = RecipeRegisterEvent {
recipe: Recipe::Shaped(recipe),
cancelled: false,
};
self.bus.dispatch(&mut event, self.ctx);
if event.is_cancelled() {
return false;
}
match event.recipe {
Recipe::Shaped(r) => self.registry.add_shaped(r),
Recipe::Shapeless(_) => {
return false;
}
}
let mut post = RecipeRegisteredEvent { recipe_id: id };
self.bus.dispatch(&mut post, self.ctx);
true
}
pub fn add_shapeless(&mut self, recipe: OwnedShapelessRecipe) -> bool {
let id = recipe.id.clone();
let mut event = RecipeRegisterEvent {
recipe: Recipe::Shapeless(recipe),
cancelled: false,
};
self.bus.dispatch(&mut event, self.ctx);
if event.is_cancelled() {
return false;
}
match event.recipe {
Recipe::Shapeless(r) => self.registry.add_shapeless(r),
Recipe::Shaped(_) => return false,
}
let mut post = RecipeRegisteredEvent { recipe_id: id };
self.bus.dispatch(&mut post, self.ctx);
true
}
pub fn remove_by_id(&mut self, id: &RecipeId) -> bool {
if self.registry.remove_by_id(id).is_some() {
let mut event = RecipeUnregisteredEvent {
recipe_id: id.clone(),
};
self.bus.dispatch(&mut event, self.ctx);
true
} else {
false
}
}
pub fn remove_by_result(&mut self, result_id: i32) -> usize {
let removed = self.registry.remove_by_result(result_id);
let count = removed.len();
for recipe_id in removed {
let mut event = RecipeUnregisteredEvent { recipe_id };
self.bus.dispatch(&mut event, self.ctx);
}
count
}
pub fn clear(&mut self) {
let removed = self.registry.clear();
for recipe_id in removed {
let mut event = RecipeUnregisteredEvent { recipe_id };
self.bus.dispatch(&mut event, self.ctx);
}
}
pub fn contains(&self, id: &RecipeId) -> bool {
self.registry.contains(id)
}
pub fn get(&self, id: &RecipeId) -> Option<Recipe> {
self.registry.find_by_id(id)
}
pub fn shaped_count(&self) -> usize {
self.registry.shaped_count()
}
pub fn shapeless_count(&self) -> usize {
self.registry.shapeless_count()
}
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use std::sync::atomic::{AtomicU32, Ordering};
use crate::events::Stage;
use crate::testing::{MockRecipeRegistry, NoopContext};
use super::*;
fn shaped(path: &str) -> OwnedShapedRecipe {
OwnedShapedRecipe {
id: RecipeId::new("plugin", path),
width: 1,
height: 1,
pattern: vec![Some(1)],
result_id: 42,
result_count: 1,
}
}
fn shapeless(path: &str) -> OwnedShapelessRecipe {
OwnedShapelessRecipe {
id: RecipeId::new("plugin", path),
ingredients: vec![1, 2],
result_id: 99,
result_count: 1,
}
}
#[test]
fn add_shaped_dispatches_register_then_registered() {
let mut registry = MockRecipeRegistry::new();
let mut bus = EventBus::new();
let ctx = NoopContext;
let validate_seen: Arc<AtomicU32> = Arc::new(AtomicU32::new(0));
let post_seen = Arc::new(AtomicU32::new(0));
{
let v = Arc::clone(&validate_seen);
bus.on::<RecipeRegisterEvent>(Stage::Validate, 0, move |_, _| {
v.fetch_add(1, Ordering::Relaxed);
});
}
{
let p = Arc::clone(&post_seen);
bus.on::<RecipeRegisteredEvent>(Stage::Post, 0, move |_, _| {
p.fetch_add(1, Ordering::Relaxed);
});
}
let mut registrar = RecipeRegistrar::new(
&mut registry as &mut dyn RecipeRegistryHandle,
&mut bus,
&ctx as &dyn crate::context::Context,
);
let inserted = registrar.add_shaped(shaped("magic_sword"));
assert!(inserted);
assert_eq!(validate_seen.load(Ordering::Relaxed), 1);
assert_eq!(post_seen.load(Ordering::Relaxed), 1);
assert_eq!(registry.shaped_count(), 1);
}
#[test]
fn add_shaped_cancellation_skips_insert_and_post() {
let mut registry = MockRecipeRegistry::new();
let mut bus = EventBus::new();
let ctx = NoopContext;
bus.on::<RecipeRegisterEvent>(Stage::Validate, 0, |event, _| {
event.cancel();
});
let post_seen = Arc::new(AtomicU32::new(0));
{
let p = Arc::clone(&post_seen);
bus.on::<RecipeRegisteredEvent>(Stage::Post, 0, move |_, _| {
p.fetch_add(1, Ordering::Relaxed);
});
}
let mut registrar = RecipeRegistrar::new(
&mut registry as &mut dyn RecipeRegistryHandle,
&mut bus,
&ctx as &dyn crate::context::Context,
);
let inserted = registrar.add_shaped(shaped("forbidden"));
assert!(
!inserted,
"cancellation should make add_shaped return false"
);
assert_eq!(post_seen.load(Ordering::Relaxed), 0);
assert_eq!(registry.shaped_count(), 0);
}
#[test]
fn add_shapeless_round_trip() {
let mut registry = MockRecipeRegistry::new();
let mut bus = EventBus::new();
let ctx = NoopContext;
let mut registrar = RecipeRegistrar::new(
&mut registry as &mut dyn RecipeRegistryHandle,
&mut bus,
&ctx as &dyn crate::context::Context,
);
assert!(registrar.add_shapeless(shapeless("bread")));
assert!(registrar.contains(&RecipeId::new("plugin", "bread")));
}
#[test]
fn remove_by_id_dispatches_unregistered() {
let mut registry = MockRecipeRegistry::new();
registry.add_shaped(shaped("temp"));
let mut bus = EventBus::new();
let ctx = NoopContext;
let unreg_seen = Arc::new(AtomicU32::new(0));
{
let u = Arc::clone(&unreg_seen);
bus.on::<RecipeUnregisteredEvent>(Stage::Post, 0, move |_, _| {
u.fetch_add(1, Ordering::Relaxed);
});
}
let mut registrar = RecipeRegistrar::new(
&mut registry as &mut dyn RecipeRegistryHandle,
&mut bus,
&ctx as &dyn crate::context::Context,
);
let id = RecipeId::new("plugin", "temp");
assert!(registrar.remove_by_id(&id));
assert_eq!(unreg_seen.load(Ordering::Relaxed), 1);
assert!(!registry.contains(&id));
}
#[test]
fn remove_by_id_missing_does_not_dispatch() {
let mut registry = MockRecipeRegistry::new();
let mut bus = EventBus::new();
let ctx = NoopContext;
let unreg_seen = Arc::new(AtomicU32::new(0));
{
let u = Arc::clone(&unreg_seen);
bus.on::<RecipeUnregisteredEvent>(Stage::Post, 0, move |_, _| {
u.fetch_add(1, Ordering::Relaxed);
});
}
let mut registrar = RecipeRegistrar::new(
&mut registry as &mut dyn RecipeRegistryHandle,
&mut bus,
&ctx as &dyn crate::context::Context,
);
assert!(!registrar.remove_by_id(&RecipeId::new("plugin", "missing")));
assert_eq!(unreg_seen.load(Ordering::Relaxed), 0);
}
#[test]
fn remove_by_result_dispatches_per_removed() {
let mut registry = MockRecipeRegistry::new();
registry.add_shaped(shaped("a"));
registry.add_shaped(shaped("b"));
let mut bus = EventBus::new();
let ctx = NoopContext;
let unreg_seen = Arc::new(AtomicU32::new(0));
{
let u = Arc::clone(&unreg_seen);
bus.on::<RecipeUnregisteredEvent>(Stage::Post, 0, move |_, _| {
u.fetch_add(1, Ordering::Relaxed);
});
}
let mut registrar = RecipeRegistrar::new(
&mut registry as &mut dyn RecipeRegistryHandle,
&mut bus,
&ctx as &dyn crate::context::Context,
);
assert_eq!(registrar.remove_by_result(42), 2);
assert_eq!(unreg_seen.load(Ordering::Relaxed), 2);
}
#[test]
fn clear_dispatches_per_recipe() {
let mut registry = MockRecipeRegistry::new();
registry.add_shaped(shaped("a"));
registry.add_shapeless(shapeless("b"));
let mut bus = EventBus::new();
let ctx = NoopContext;
let unreg_seen = Arc::new(AtomicU32::new(0));
{
let u = Arc::clone(&unreg_seen);
bus.on::<RecipeUnregisteredEvent>(Stage::Post, 0, move |_, _| {
u.fetch_add(1, Ordering::Relaxed);
});
}
let mut registrar = RecipeRegistrar::new(
&mut registry as &mut dyn RecipeRegistryHandle,
&mut bus,
&ctx as &dyn crate::context::Context,
);
registrar.clear();
assert_eq!(unreg_seen.load(Ordering::Relaxed), 2);
assert_eq!(registry.shaped_count(), 0);
assert_eq!(registry.shapeless_count(), 0);
}
#[test]
fn registrar_accessors_expose_underlying_state() {
let mut registry = MockRecipeRegistry::new();
let mut bus = EventBus::new();
let ctx = NoopContext;
let mut registrar = RecipeRegistrar::new(
&mut registry as &mut dyn RecipeRegistryHandle,
&mut bus,
&ctx as &dyn crate::context::Context,
);
assert_eq!(registrar.shaped_count(), 0);
registrar.add_shaped(shaped("only"));
assert_eq!(registrar.shaped_count(), 1);
}
}