use super::funejson::Value; use super::dates_add_days::add_days; use super::hospitality_allergen_matrix_data::{Allergen, ALLERGENS, ALLERGENS_HISTORY}; fn list_in_force(jurisdiction: &str, on_date: &str) -> Vec<&'static Allergen> { let mut rows: Vec<&'static Allergen> = ALLERGENS .iter() .filter(|a| { a.jurisdiction == jurisdiction && a.valid_from <= on_date && a.valid_to.map_or(true, |to| on_date <= to) }) .collect(); if !rows.is_empty() { rows.sort_by_key(|a| a.annex_number); return rows; } // A history-pruned build may have dropped the list that applied then; say so // rather than answering with nothing. if ALLERGENS_HISTORY != "full" { let earliest = ALLERGENS.iter().filter(|a| a.jurisdiction == jurisdiction).map(|a| a.valid_from).min(); if let Some(earliest) = earliest { if on_date < earliest { panic!( "no allergen list for {} on {}: this build was installed with history={}, so it only carries lists from {}", jurisdiction, on_date, ALLERGENS_HISTORY, earliest ); } } } panic!("no allergen list for {} on {}", jurisdiction, on_date) } fn dedupe(names: Vec) -> Vec { let mut seen: Vec = Vec::new(); for name in names { if !seen.contains(&name) { seen.push(name); } } seen } /// The regulated allergens in a dish, from what its ingredients declare. /// /// Every allergen on the list gets a row, in the legal order, so a menu or /// allergen chart shows "none" as a decision rather than a gap. An allergen any /// ingredient contains is "contains"; otherwise one any ingredient may contain /// is "may-contain". The sources say which ingredients to check. /// /// # Panics /// Panics on a bad date, no list for the jurisdiction and date, or an /// allergen code that is not on the list. pub fn allergen_matrix(ingredients: &[IngredientAllergens], jurisdiction: &str, on_date: &str) -> Vec { add_days(on_date, 0); let list = list_in_force(jurisdiction, on_date); for ingredient in ingredients { for code in ingredient.contains.iter().chain(ingredient.may_contain.iter()) { if !list.iter().any(|a| a.code == code.as_str()) { panic!( "unknown allergen \"{}\" in \"{}\": not on the {} list on {}", code, ingredient.name, jurisdiction, on_date ); } } } list.iter() .map(|allergen| { let contains: Vec = ingredients .iter() .filter(|i| i.contains.iter().any(|c| c == allergen.code)) .map(|i| i.name.clone()) .collect(); let may: Vec = ingredients .iter() .filter(|i| i.may_contain.iter().any(|c| c == allergen.code)) .map(|i| i.name.clone()) .collect(); let status = if !contains.is_empty() { "contains" } else if !may.is_empty() { "may-contain" } else { "none" }; AllergenRow { code: allergen.code.to_string(), name: allergen.name.to_string(), status: status.to_string(), sources: dedupe(if contains.is_empty() { may } else { contains }), } }) .collect() } fn strings(v: &Value) -> Vec { v.as_arr().iter().map(|s| s.as_str().to_string()).collect() } pub fn ingredient_allergens_from_value(v: &Value) -> IngredientAllergens { IngredientAllergens { name: v.get("name").as_str().to_string(), contains: strings(v.get("contains")), may_contain: strings(v.get("mayContain")), } } pub fn allergen_row_to_value(r: &AllergenRow) -> Value { Value::obj(vec![ ("code", Value::str(&r.code)), ("name", Value::str(&r.name)), ("status", Value::str(&r.status)), ("sources", Value::Arr(r.sources.iter().map(|s| Value::str(s)).collect())), ]) } pub fn fune_vector(args: &[Value]) -> Value { let ingredients: Vec = args[0].as_arr().iter().map(ingredient_allergens_from_value).collect(); Value::Arr( allergen_matrix(&ingredients, args[1].as_str(), args[2].as_str()) .iter() .map(allergen_row_to_value) .collect(), ) }