use std::collections::{HashMap, HashSet};
use crate::recipes::RecipeId;
use crate::components::Component;
#[derive(Debug, Default, Clone)]
pub struct KnownRecipes {
ids: HashSet<RecipeId>,
display_ids: HashMap<RecipeId, i32>,
by_display: HashMap<i32, RecipeId>,
next_display_id: i32,
}
impl Component for KnownRecipes {}
impl KnownRecipes {
pub fn unlock(&mut self, id: RecipeId) -> i32 {
if !self.ids.insert(id.clone()) {
return self.display_ids[&id];
}
let display_id = self.next_display_id;
self.next_display_id += 1;
self.display_ids.insert(id.clone(), display_id);
self.by_display.insert(display_id, id);
display_id
}
pub fn lock(&mut self, id: &RecipeId) -> Option<i32> {
if !self.ids.remove(id) {
return None;
}
self.display_ids.remove(id)
}
pub fn has(&self, id: &RecipeId) -> bool {
self.ids.contains(id)
}
pub fn display_id(&self, id: &RecipeId) -> Option<i32> {
self.display_ids.get(id).copied()
}
pub fn recipe_for_display(&self, display_id: i32) -> Option<&RecipeId> {
self.by_display.get(&display_id)
}
pub fn len(&self) -> usize {
self.ids.len()
}
pub fn is_empty(&self) -> bool {
self.ids.is_empty()
}
pub fn iter(&self) -> impl Iterator<Item = (&RecipeId, i32)> {
self.display_ids.iter().map(|(id, d)| (id, *d))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn id(path: &str) -> RecipeId {
RecipeId::new("plugin", path)
}
#[test]
fn unlock_allocates_sequential_display_ids() {
let mut k = KnownRecipes::default();
assert_eq!(k.unlock(id("a")), 0);
assert_eq!(k.unlock(id("b")), 1);
assert_eq!(k.unlock(id("c")), 2);
}
#[test]
fn unlock_idempotent_returns_existing_display_id() {
let mut k = KnownRecipes::default();
let first = k.unlock(id("a"));
let second = k.unlock(id("a"));
assert_eq!(first, second);
assert_eq!(k.len(), 1);
}
#[test]
fn lock_returns_display_id_and_removes_forward_lookup() {
let mut k = KnownRecipes::default();
let display = k.unlock(id("a"));
assert_eq!(k.lock(&id("a")), Some(display));
assert!(!k.has(&id("a")));
assert_eq!(k.display_id(&id("a")), None);
}
#[test]
fn lock_keeps_reverse_lookup_for_stale_packets() {
let mut k = KnownRecipes::default();
let display = k.unlock(id("a"));
k.lock(&id("a"));
assert_eq!(k.recipe_for_display(display), Some(&id("a")));
}
#[test]
fn lock_returns_none_when_unknown() {
let mut k = KnownRecipes::default();
assert_eq!(k.lock(&id("missing")), None);
}
#[test]
fn display_ids_do_not_reuse_after_lock() {
let mut k = KnownRecipes::default();
k.unlock(id("a"));
k.unlock(id("b"));
k.lock(&id("a"));
assert_eq!(k.unlock(id("c")), 2);
}
#[test]
fn iter_yields_only_unlocked_pairs() {
let mut k = KnownRecipes::default();
k.unlock(id("a"));
k.unlock(id("b"));
k.lock(&id("a"));
let mut entries: Vec<_> = k.iter().map(|(id, d)| (id.clone(), d)).collect();
entries.sort_by_key(|(_, d)| *d);
assert_eq!(entries, vec![(id("b"), 1)]);
}
#[test]
fn has_returns_true_only_for_unlocked() {
let mut k = KnownRecipes::default();
assert!(!k.has(&id("a")));
k.unlock(id("a"));
assert!(k.has(&id("a")));
k.lock(&id("a"));
assert!(!k.has(&id("a")));
}
}