1use std::collections::HashMap;
9use std::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard};
10
11#[cfg(feature = "wasm")]
12use wasm_bindgen::prelude::*;
13
14use crate::{
15 error::{Error, Result},
16 ingredient::{Category, Ingredient},
17 specs::IngredientSpec,
18};
19
20#[cfg(doc)]
21use crate::composition::Composition;
22
23#[cfg_attr(feature = "wasm", wasm_bindgen)]
37#[derive(Debug)]
38pub struct IngredientDatabase {
39 map: RwLock<HashMap<String, Ingredient>>,
40}
41
42impl IngredientDatabase {
43 #[must_use]
45 pub fn new_seeded(ingredients: &[Ingredient]) -> Self {
46 let mut map = HashMap::new();
47 for ingredient in ingredients {
48 let _unused = map.insert(ingredient.name.clone(), ingredient.clone());
50 }
51
52 Self { map: RwLock::new(map) }
53 }
54
55 fn specs_into_ingredients(specs: &[IngredientSpec]) -> Result<Vec<Ingredient>> {
56 specs
57 .iter()
58 .map(|spec| spec.clone().into_ingredient())
59 .collect::<Result<_>>()
60 }
61
62 pub fn new_seeded_from_specs(specs: &[IngredientSpec]) -> Result<Self> {
70 Ok(Self::new_seeded(&Self::specs_into_ingredients(specs)?))
71 }
72
73 #[allow(clippy::missing_panics_doc)] #[cfg(feature = "data")]
78 #[must_use]
79 pub fn new_seeded_from_embedded_data() -> Self {
80 Self::new_seeded_from_specs(&crate::data::get_all_ingredient_specs())
81 .expect("Embedded ingredients specs should all `into_ingredient` successfully")
82 }
83
84 fn acquire_read_lock(&self) -> RwLockReadGuard<'_, HashMap<String, Ingredient>> {
85 self.map
86 .read()
87 .expect("Read lock on the ingredient database should be acquired successfully")
88 }
89
90 fn acquire_write_lock(&self) -> RwLockWriteGuard<'_, HashMap<String, Ingredient>> {
91 self.map
92 .write()
93 .expect("Write lock on the ingredient database should be acquired successfully")
94 }
95
96 pub fn seed(&self, ingredients: &[Ingredient]) {
98 let mut db = self.acquire_write_lock();
99 for ingredient in ingredients {
100 let _unused = db.insert(ingredient.name.clone(), ingredient.clone());
101 }
102 }
103
104 pub fn seed_from_specs(&self, specs: &[IngredientSpec]) -> Result<()> {
112 self.seed(&Self::specs_into_ingredients(specs)?);
113 Ok(())
114 }
115
116 pub fn get_ingredient_by_name(&self, name: &str) -> Result<Ingredient> {
122 self.acquire_read_lock()
123 .get(name)
124 .cloned()
125 .ok_or_else(|| Error::IngredientNotFound(name.to_string()))
126 }
127}
128
129#[cfg_attr(feature = "wasm", wasm_bindgen)]
130impl IngredientDatabase {
131 #[cfg_attr(feature = "wasm", wasm_bindgen(constructor))]
133 #[must_use]
134 pub fn new() -> Self {
135 Self {
136 map: RwLock::new(HashMap::new()),
137 }
138 }
139
140 pub fn get_all_ingredients(&self) -> Vec<Ingredient> {
142 self.acquire_read_lock().values().cloned().collect()
143 }
144
145 pub fn get_ingredients_by_category(&self, category: Category) -> Vec<Ingredient> {
147 self.acquire_read_lock()
148 .values()
149 .filter(|ingredient| ingredient.category == category)
150 .cloned()
151 .collect()
152 }
153}
154
155impl Default for IngredientDatabase {
156 fn default() -> Self {
157 Self::new()
158 }
159}
160
161#[cfg(feature = "wasm")]
163#[cfg_attr(coverage, coverage(off))]
164pub mod wasm {
165 use wasm_bindgen::prelude::*;
166
167 use super::IngredientDatabase;
168
169 use crate::{ingredient::Ingredient, specs::IngredientSpec};
170
171 #[cfg(doc)]
172 use crate::error::Error;
173
174 fn specs_from_jsvalues(specs: &[JsValue]) -> Result<Vec<IngredientSpec>, JsValue> {
175 specs
176 .iter()
177 .map(|spec| serde_wasm_bindgen::from_value::<IngredientSpec>(spec.clone()).map_err(Into::into))
178 .collect::<Result<_, JsValue>>()
179 }
180
181 #[wasm_bindgen]
182 impl IngredientDatabase {
183 #[wasm_bindgen(js_name = "seed")]
185 #[allow(clippy::needless_pass_by_value)]
186 pub fn seed_wasm(&self, ingredients: Box<[Ingredient]>) {
187 self.seed(&ingredients);
188 }
189
190 #[wasm_bindgen(js_name = "seed_from_specs")]
198 #[allow(clippy::needless_pass_by_value)]
199 pub fn seed_from_specs_wasm(&self, specs: Box<[JsValue]>) -> Result<(), JsValue> {
200 self.seed_from_specs(&specs_from_jsvalues(&specs)?).map_err(Into::into)
201 }
202
203 #[wasm_bindgen(js_name = "get_ingredient_by_name")]
209 pub fn get_ingredient_by_name_wasm(&self, name: &str) -> Result<Ingredient, JsValue> {
210 self.get_ingredient_by_name(name).map_err(Into::into)
211 }
212 }
213
214 #[wasm_bindgen]
216 #[allow(clippy::needless_pass_by_value)]
217 #[must_use]
218 pub fn new_ingredient_database_seeded(ingredients: Box<[Ingredient]>) -> IngredientDatabase {
219 IngredientDatabase::new_seeded(&ingredients)
220 }
221
222 #[wasm_bindgen]
230 #[allow(clippy::needless_pass_by_value)]
231 pub fn new_ingredient_database_seeded_from_specs(specs: Box<[JsValue]>) -> Result<IngredientDatabase, JsValue> {
232 IngredientDatabase::new_seeded_from_specs(&specs_from_jsvalues(&specs)?).map_err(Into::into)
233 }
234
235 #[cfg(feature = "data")]
239 #[wasm_bindgen]
240 #[must_use]
241 pub fn new_ingredient_database_seeded_from_embedded_data() -> IngredientDatabase {
242 IngredientDatabase::new_seeded_from_embedded_data()
243 }
244}
245
246#[cfg(test)]
247#[cfg_attr(coverage, coverage(off))]
248#[allow(clippy::unwrap_used)]
249pub(crate) mod tests {
250 use strum::IntoEnumIterator;
251
252 use crate::tests::asserts::shadow_asserts::assert_eq;
253 use crate::tests::asserts::*;
254
255 use super::*;
256 use crate::data::get_all_ingredient_specs;
257
258 #[test]
259 fn ingredient_database_empty() {
260 let db = IngredientDatabase::new();
261 assert_eq!(db.get_all_ingredients().len(), 0);
262 }
263
264 #[test]
265 fn ingredient_database_new_seeded() {
266 let db = IngredientDatabase::new_seeded(
267 &get_all_ingredient_specs()
268 .iter()
269 .map(|spec| spec.clone().into_ingredient().unwrap())
270 .collect::<Vec<Ingredient>>(),
271 );
272
273 assert_eq!(db.get_all_ingredients().len(), get_all_ingredient_specs().len());
274
275 for spec in get_all_ingredient_specs() {
276 let ingredient = db.get_ingredient_by_name(&spec.name).unwrap();
277 assert_eq!(ingredient, spec.into_ingredient().unwrap());
278 }
279 }
280
281 #[test]
282 fn ingredient_database_new_seeded_from_specs() {
283 let db = IngredientDatabase::new_seeded_from_specs(&get_all_ingredient_specs()).unwrap();
284
285 assert_eq!(db.get_all_ingredients().len(), get_all_ingredient_specs().len());
286
287 for spec in get_all_ingredient_specs() {
288 let ingredient = db.get_ingredient_by_name(&spec.name).unwrap();
289 assert_eq!(ingredient, spec.into_ingredient().unwrap());
290 }
291 }
292
293 #[test]
294 fn ingredient_database_seed() {
295 let db = IngredientDatabase::new();
296 assert_eq!(db.get_all_ingredients().len(), 0);
297
298 let ingredients = get_all_ingredient_specs()[..10]
299 .iter()
300 .map(|spec| spec.clone().into_ingredient().unwrap())
301 .collect::<Vec<Ingredient>>();
302
303 db.seed(&ingredients);
304 assert_eq!(db.get_all_ingredients().len(), 10);
305
306 for ingredient in ingredients {
307 let fetched_ingredient = db.get_ingredient_by_name(&ingredient.name).unwrap();
308 assert_eq!(fetched_ingredient, ingredient);
309 }
310 }
311
312 #[test]
313 fn ingredient_database_seed_from_specs() {
314 let db = IngredientDatabase::new();
315 assert_eq!(db.get_all_ingredients().len(), 0);
316
317 let specs = get_all_ingredient_specs()[..10].to_vec();
318
319 db.seed_from_specs(&specs).unwrap();
320 assert_eq!(db.get_all_ingredients().len(), 10);
321
322 for spec in specs {
323 let fetched_ingredient = db.get_ingredient_by_name(&spec.name).unwrap();
324 let ingredient = spec.into_ingredient().unwrap();
325 assert_eq!(fetched_ingredient, ingredient);
326 }
327 }
328
329 #[test]
330 fn ingredient_database_seeded_from_embedded_data() {
331 let db = IngredientDatabase::new_seeded_from_embedded_data();
332
333 assert_eq!(db.get_all_ingredients().len(), get_all_ingredient_specs().len());
334
335 for spec in get_all_ingredient_specs() {
336 let ingredient = db.get_ingredient_by_name(&spec.name).unwrap();
337 assert_eq!(ingredient, spec.into_ingredient().unwrap());
338 }
339 }
340
341 #[test]
342 fn ingredient_database_get_ingredients_by_category() {
343 let db = IngredientDatabase::new_seeded_from_specs(&get_all_ingredient_specs()).unwrap();
344
345 for category in Category::iter() {
346 let ingredients = db.get_ingredients_by_category(category);
347 assert_false!(ingredients.is_empty());
348
349 for ingredient in ingredients {
350 assert_eq!(ingredient.category, category);
351 }
352 }
353 }
354
355 #[test]
356 fn ingredient_database_get_ingredient_by_name_not_found() {
357 let db = IngredientDatabase::new();
358 let result = db.get_ingredient_by_name("non_existent_ingredient");
359 assert!(matches!(result, Err(Error::IngredientNotFound(_))));
360 }
361
362 #[test]
363 fn ingredient_database_thread_safety() {
364 use std::sync::Arc;
365 use std::thread;
366
367 let db = Arc::new(IngredientDatabase::new_seeded_from_specs(&get_all_ingredient_specs()).unwrap());
368
369 let mut handles = vec![];
370
371 for _ in 0..10 {
372 let db_clone = Arc::clone(&db);
373 let handle = thread::spawn(move || {
374 for spec in get_all_ingredient_specs() {
375 let ingredient = db_clone.get_ingredient_by_name(&spec.name).unwrap();
376 assert_eq!(ingredient, spec.into_ingredient().unwrap());
377 }
378 });
379 handles.push(handle);
380 }
381
382 for handle in handles {
383 handle.join().unwrap();
384 }
385 }
386}